honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "bytes"
20 "crypto/rand"
21 "crypto/rsa"
22 "database/sql"
23 "fmt"
24 "html"
25 "html/template"
26 "image"
27 _ "image/gif"
28 _ "image/jpeg"
29 _ "image/png"
30 "io"
31 "log"
32 notrand "math/rand"
33 "net/http"
34 "os"
35 "sort"
36 "strings"
37 "sync"
38 "time"
39
40 "github.com/gorilla/mux"
41)
42
43type UserInfo struct {
44 UserID int64
45 Username string
46}
47
48type WhatAbout struct {
49 ID int64
50 Name string
51 Display string
52 About string
53 Key string
54 URL string
55}
56
57type Honk struct {
58 ID int64
59 UserID int64
60 Username string
61 What string
62 Honker string
63 XID string
64 RID string
65 Date time.Time
66 URL string
67 Noise string
68 Convoy string
69 Audience []string
70 HTML template.HTML
71 Donks []*Donk
72}
73
74type Donk struct {
75 FileID int64
76 XID string
77 Name string
78 URL string
79 Media string
80 Content []byte
81}
82
83type Honker struct {
84 ID int64
85 UserID int64
86 Name string
87 XID string
88 Flavor string
89}
90
91var serverName string
92var iconName = "icon.png"
93
94var readviews *Template
95
96func getInfo(r *http.Request) map[string]interface{} {
97 templinfo := make(map[string]interface{})
98 templinfo["StyleParam"] = getstyleparam("views/style.css")
99 templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
100 templinfo["ServerName"] = serverName
101 templinfo["IconName"] = iconName
102 templinfo["UserInfo"] = GetUserInfo(r)
103 templinfo["LogoutCSRF"] = GetCSRF("logout", r)
104 return templinfo
105}
106
107func homepage(w http.ResponseWriter, r *http.Request) {
108 templinfo := getInfo(r)
109 u := GetUserInfo(r)
110 var honks []*Honk
111 if u != nil {
112 if r.URL.Path == "/atme" {
113 honks = gethonksforme(u.UserID)
114 } else {
115 honks = gethonksforuser(u.UserID)
116 }
117 templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
118 } else {
119 honks = gethonks()
120 }
121 sort.Slice(honks, func(i, j int) bool {
122 return honks[i].Date.After(honks[j].Date)
123 })
124
125 var modtime time.Time
126 if len(honks) > 0 {
127 modtime = honks[0].Date
128 }
129 debug := false
130 getconfig("debug", &debug)
131 imh := r.Header.Get("If-Modified-Since")
132 if !debug && imh != "" && !modtime.IsZero() {
133 ifmod, err := time.Parse(http.TimeFormat, imh)
134 if err == nil && !modtime.After(ifmod) {
135 w.WriteHeader(http.StatusNotModified)
136 return
137 }
138 }
139 reverbolate(honks)
140
141 msg := "Things happen."
142 getconfig("servermsg", &msg)
143 templinfo["Honks"] = honks
144 templinfo["ShowRSS"] = true
145 templinfo["ServerMessage"] = msg
146 if u == nil {
147 w.Header().Set("Cache-Control", "max-age=60")
148 } else {
149 w.Header().Set("Cache-Control", "max-age=0")
150 }
151 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
152 err := readviews.ExecuteTemplate(w, "homepage.html", templinfo)
153 if err != nil {
154 log.Print(err)
155 }
156}
157
158func showrss(w http.ResponseWriter, r *http.Request) {
159 name := mux.Vars(r)["name"]
160
161 var honks []*Honk
162 if name != "" {
163 honks = gethonksbyuser(name)
164 } else {
165 honks = gethonks()
166 }
167 sort.Slice(honks, func(i, j int) bool {
168 return honks[i].Date.After(honks[j].Date)
169 })
170 reverbolate(honks)
171
172 home := fmt.Sprintf("https://%s/", serverName)
173 base := home
174 if name != "" {
175 home += "u/" + name
176 name += " "
177 }
178 feed := RssFeed{
179 Title: name + "honk",
180 Link: home,
181 Description: name + "honk rss",
182 FeedImage: &RssFeedImage{
183 URL: base + "icon.png",
184 Title: name + "honk rss",
185 Link: home,
186 },
187 }
188 var modtime time.Time
189 past := time.Now().UTC().Add(-3 * 24 * time.Hour)
190 for _, honk := range honks {
191 if honk.Date.Before(past) {
192 break
193 }
194 desc := string(honk.HTML)
195 for _, d := range honk.Donks {
196 desc += fmt.Sprintf(`<p><a href="%sd/%s">Attachment: %s</a>`,
197 base, d.XID, html.EscapeString(d.Name))
198 }
199
200 feed.Items = append(feed.Items, &RssItem{
201 Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
202 Description: RssCData{desc},
203 Link: honk.URL,
204 PubDate: honk.Date.Format(time.RFC1123),
205 })
206 if honk.Date.After(modtime) {
207 modtime = honk.Date
208 }
209 }
210 w.Header().Set("Cache-Control", "max-age=300")
211 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
212
213 err := feed.Write(w)
214 if err != nil {
215 log.Printf("error writing rss: %s", err)
216 }
217}
218
219func butwhatabout(name string) (*WhatAbout, error) {
220 row := stmtWhatAbout.QueryRow(name)
221 var user WhatAbout
222 err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key)
223 user.URL = fmt.Sprintf("https://%s/u/%s", serverName, user.Name)
224 return &user, err
225}
226
227func crappola(j map[string]interface{}) bool {
228 t, _ := jsongetstring(j, "type")
229 a, _ := jsongetstring(j, "actor")
230 o, _ := jsongetstring(j, "object")
231 if t == "Delete" && a == o {
232 log.Printf("crappola from %s", a)
233 return true
234 }
235 return false
236}
237
238func ping(user *WhatAbout, who string) {
239 box, err := getboxes(who)
240 if err != nil {
241 log.Printf("no inbox for ping: %s", err)
242 return
243 }
244 j := NewJunk()
245 j["@context"] = itiswhatitis
246 j["type"] = "Ping"
247 j["id"] = user.URL + "/ping/" + xfiltrate()
248 j["actor"] = user.URL
249 j["to"] = who
250 keyname, key := ziggy(user.Name)
251 err = PostJunk(keyname, key, box.In, j)
252 if err != nil {
253 log.Printf("can't send ping: %s", err)
254 return
255 }
256 log.Printf("sent ping to %s: %s", who, j["id"])
257}
258
259func pong(user *WhatAbout, who string, obj string) {
260 box, err := getboxes(who)
261 if err != nil {
262 log.Printf("no inbox for pong %s : %s", who, err)
263 return
264 }
265 j := NewJunk()
266 j["@context"] = itiswhatitis
267 j["type"] = "Pong"
268 j["id"] = user.URL + "/pong/" + xfiltrate()
269 j["actor"] = user.URL
270 j["to"] = who
271 j["object"] = obj
272 keyname, key := ziggy(user.Name)
273 err = PostJunk(keyname, key, box.In, j)
274 if err != nil {
275 log.Printf("can't send pong: %s", err)
276 return
277 }
278}
279
280func inbox(w http.ResponseWriter, r *http.Request) {
281 name := mux.Vars(r)["name"]
282 user, err := butwhatabout(name)
283 if err != nil {
284 http.NotFound(w, r)
285 return
286 }
287 var buf bytes.Buffer
288 io.Copy(&buf, r.Body)
289 payload := buf.Bytes()
290 j, err := ReadJunk(bytes.NewReader(payload))
291 if err != nil {
292 log.Printf("bad payload: %s", err)
293 io.WriteString(os.Stdout, "bad payload\n")
294 os.Stdout.Write(payload)
295 io.WriteString(os.Stdout, "\n")
296 return
297 }
298 if crappola(j) {
299 return
300 }
301 keyname, err := zag(r, payload)
302 if err != nil {
303 log.Printf("inbox message failed signature: %s", err)
304 if keyname != "" {
305 keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
306 }
307 if err != nil {
308 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
309 io.WriteString(fd, "bad signature:\n")
310 WriteJunk(fd, j)
311 io.WriteString(fd, "\n")
312 fd.Close()
313 return
314 }
315 }
316 what, _ := jsongetstring(j, "type")
317 if what == "Like" {
318 return
319 }
320 who, _ := jsongetstring(j, "actor")
321 if !keymatch(keyname, who) {
322 log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
323 return
324 }
325 if thoudostbitethythumb(user.ID, who) {
326 log.Printf("ignoring thumb sucker %s", who)
327 return
328 }
329 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
330 WriteJunk(fd, j)
331 io.WriteString(fd, "\n")
332 fd.Close()
333 switch what {
334 case "Ping":
335 obj, _ := jsongetstring(j, "id")
336 log.Printf("ping from %s: %s", who, obj)
337 pong(user, who, obj)
338 case "Pong":
339 obj, _ := jsongetstring(j, "object")
340 log.Printf("pong from %s: %s", who, obj)
341 case "Follow":
342 log.Printf("updating honker follow: %s", who)
343 rubadubdub(user, j)
344 case "Accept":
345 db := opendatabase()
346 log.Printf("updating honker accept: %s", who)
347 db.Exec("update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'", who)
348 case "Undo":
349 obj, ok := jsongetmap(j, "object")
350 if !ok {
351 log.Printf("unknown undo no object")
352 } else {
353 what, _ := jsongetstring(obj, "type")
354 switch what {
355 case "Follow":
356 log.Printf("updating honker undo: %s", who)
357 db := opendatabase()
358 db.Exec("update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'", who)
359 case "Like":
360 default:
361 log.Printf("unknown undo: %s", what)
362 }
363 }
364 default:
365 xonk := xonkxonk(j)
366 if xonk != nil && needxonk(user, xonk) {
367 xonk.UserID = user.ID
368 savexonk(user, xonk)
369 }
370 }
371}
372
373func outbox(w http.ResponseWriter, r *http.Request) {
374 name := mux.Vars(r)["name"]
375 user, err := butwhatabout(name)
376 if err != nil {
377 http.NotFound(w, r)
378 return
379 }
380 honks := gethonksbyuser(name)
381
382 var jonks []map[string]interface{}
383 for _, h := range honks {
384 j, _ := jonkjonk(user, h)
385 jonks = append(jonks, j)
386 }
387
388 j := NewJunk()
389 j["@context"] = itiswhatitis
390 j["id"] = user.URL + "/outbox"
391 j["type"] = "OrderedCollection"
392 j["totalItems"] = len(jonks)
393 j["orderedItems"] = jonks
394
395 w.Header().Set("Cache-Control", "max-age=60")
396 w.Header().Set("Content-Type", theonetruename)
397 WriteJunk(w, j)
398}
399
400func viewuser(w http.ResponseWriter, r *http.Request) {
401 name := mux.Vars(r)["name"]
402 user, err := butwhatabout(name)
403 if err != nil {
404 http.NotFound(w, r)
405 return
406 }
407 if friendorfoe(r.Header.Get("Accept")) {
408 j := asjonker(user)
409 w.Header().Set("Cache-Control", "max-age=600")
410 w.Header().Set("Content-Type", theonetruename)
411 WriteJunk(w, j)
412 return
413 }
414 honks := gethonksbyuser(name)
415 u := GetUserInfo(r)
416 honkpage(w, r, u, user, honks)
417}
418
419func viewhonker(w http.ResponseWriter, r *http.Request) {
420 name := mux.Vars(r)["name"]
421 u := GetUserInfo(r)
422 honks := gethonksbyhonker(u.UserID, name)
423 honkpage(w, r, nil, nil, honks)
424}
425
426func fingerlicker(w http.ResponseWriter, r *http.Request) {
427 orig := r.FormValue("resource")
428
429 log.Printf("finger lick: %s", orig)
430
431 if strings.HasPrefix(orig, "acct:") {
432 orig = orig[5:]
433 }
434
435 name := orig
436 idx := strings.LastIndexByte(name, '/')
437 if idx != -1 {
438 name = name[idx+1:]
439 if "https://"+serverName+"/u/"+name != orig {
440 log.Printf("foreign request rejected")
441 name = ""
442 }
443 } else {
444 idx = strings.IndexByte(name, '@')
445 if idx != -1 {
446 name = name[:idx]
447 if name+"@"+serverName != orig {
448 log.Printf("foreign request rejected")
449 name = ""
450 }
451 }
452 }
453 user, err := butwhatabout(name)
454 if err != nil {
455 http.NotFound(w, r)
456 return
457 }
458
459 j := NewJunk()
460 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
461 j["aliases"] = []string{user.URL}
462 var links []map[string]interface{}
463 l := NewJunk()
464 l["rel"] = "self"
465 l["type"] = `application/activity+json`
466 l["href"] = user.URL
467 links = append(links, l)
468 j["links"] = links
469
470 w.Header().Set("Cache-Control", "max-age=3600")
471 w.Header().Set("Content-Type", "application/jrd+json")
472 WriteJunk(w, j)
473}
474
475func viewhonk(w http.ResponseWriter, r *http.Request) {
476 name := mux.Vars(r)["name"]
477 xid := mux.Vars(r)["xid"]
478 user, err := butwhatabout(name)
479 if err != nil {
480 http.NotFound(w, r)
481 return
482 }
483 h := getxonk(name, xid)
484 if h == nil {
485 http.NotFound(w, r)
486 return
487 }
488 if friendorfoe(r.Header.Get("Accept")) {
489 _, j := jonkjonk(user, h)
490 j["@context"] = itiswhatitis
491 w.Header().Set("Cache-Control", "max-age=3600")
492 w.Header().Set("Content-Type", theonetruename)
493 WriteJunk(w, j)
494 return
495 }
496 honkpage(w, r, nil, nil, []*Honk{h})
497}
498
499func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk) {
500 reverbolate(honks)
501 templinfo := getInfo(r)
502 if u != nil && u.Username == user.Name {
503 templinfo["UserCSRF"] = GetCSRF("saveuser", r)
504 templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
505 }
506 if u == nil {
507 w.Header().Set("Cache-Control", "max-age=60")
508 }
509 if user != nil {
510 templinfo["Name"] = user.Name
511 whatabout := user.About
512 templinfo["RawWhatAbout"] = whatabout
513 whatabout = obfusbreak(whatabout)
514 templinfo["WhatAbout"] = cleanstring(whatabout)
515 }
516 templinfo["Honks"] = honks
517 err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
518 if err != nil {
519 log.Print(err)
520 }
521}
522
523func saveuser(w http.ResponseWriter, r *http.Request) {
524 whatabout := r.FormValue("whatabout")
525 u := GetUserInfo(r)
526 db := opendatabase()
527 _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
528 if err != nil {
529 log.Printf("error bouting what: %s", err)
530 }
531
532 http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
533}
534
535func gethonkers(userid int64) []*Honker {
536 rows, err := stmtHonkers.Query(userid)
537 if err != nil {
538 log.Printf("error querying honkers: %s", err)
539 return nil
540 }
541 defer rows.Close()
542 var honkers []*Honker
543 for rows.Next() {
544 var f Honker
545 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
546 if err != nil {
547 log.Printf("error scanning honker: %s", err)
548 return nil
549 }
550 honkers = append(honkers, &f)
551 }
552 return honkers
553}
554
555func getdubs(userid int64) []*Honker {
556 rows, err := stmtDubbers.Query(userid)
557 if err != nil {
558 log.Printf("error querying dubs: %s", err)
559 return nil
560 }
561 defer rows.Close()
562 var honkers []*Honker
563 for rows.Next() {
564 var f Honker
565 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
566 if err != nil {
567 log.Printf("error scanning honker: %s", err)
568 return nil
569 }
570 honkers = append(honkers, &f)
571 }
572 return honkers
573}
574
575func getxonk(name, xid string) *Honk {
576 var h Honk
577 var dt, aud string
578 row := stmtOneXonk.QueryRow(xid)
579 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
580 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
581 if err != nil {
582 if err != sql.ErrNoRows {
583 log.Printf("error scanning xonk: %s", err)
584 }
585 return nil
586 }
587 if name != "" && h.Username != name {
588 log.Printf("user xonk mismatch")
589 return nil
590 }
591 h.Date, _ = time.Parse(dbtimeformat, dt)
592 h.Audience = strings.Split(aud, " ")
593 donksforhonks([]*Honk{&h})
594 return &h
595}
596
597func gethonks() []*Honk {
598 rows, err := stmtHonks.Query()
599 return getsomehonks(rows, err)
600}
601func gethonksbyuser(name string) []*Honk {
602 rows, err := stmtUserHonks.Query(name)
603 return getsomehonks(rows, err)
604}
605func gethonksforuser(userid int64) []*Honk {
606 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
607 rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
608 return getsomehonks(rows, err)
609}
610func gethonksforme(userid int64) []*Honk {
611 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
612 rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
613 return getsomehonks(rows, err)
614}
615func gethonksbyhonker(userid int64, honker string) []*Honk {
616 rows, err := stmtHonksByHonker.Query(userid, honker)
617 return getsomehonks(rows, err)
618}
619
620func getsomehonks(rows *sql.Rows, err error) []*Honk {
621 if err != nil {
622 log.Printf("error querying honks: %s", err)
623 return nil
624 }
625 defer rows.Close()
626 var honks []*Honk
627 for rows.Next() {
628 var h Honk
629 var dt, aud string
630 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
631 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
632 if err != nil {
633 log.Printf("error scanning honks: %s", err)
634 return nil
635 }
636 h.Date, _ = time.Parse(dbtimeformat, dt)
637 h.Audience = strings.Split(aud, " ")
638 honks = append(honks, &h)
639 }
640 rows.Close()
641 donksforhonks(honks)
642 return honks
643}
644
645func donksforhonks(honks []*Honk) {
646 db := opendatabase()
647 var ids []string
648 hmap := make(map[int64]*Honk)
649 for _, h := range honks {
650 if h.What == "zonk" {
651 continue
652 }
653 ids = append(ids, fmt.Sprintf("%d", h.ID))
654 hmap[h.ID] = h
655 }
656 q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
657 rows, err := db.Query(q)
658 if err != nil {
659 log.Printf("error querying donks: %s", err)
660 return
661 }
662 defer rows.Close()
663 for rows.Next() {
664 var hid int64
665 var d Donk
666 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
667 if err != nil {
668 log.Printf("error scanning donk: %s", err)
669 continue
670 }
671 h := hmap[hid]
672 h.Donks = append(h.Donks, &d)
673 }
674}
675
676func savebonk(w http.ResponseWriter, r *http.Request) {
677 xid := r.FormValue("xid")
678
679 log.Printf("bonking %s", xid)
680
681 xonk := getxonk("", xid)
682 if xonk == nil {
683 return
684 }
685 if xonk.Honker == "" {
686 xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
687 }
688 convoy := xonk.Convoy
689
690 userinfo := GetUserInfo(r)
691
692 dt := time.Now().UTC()
693 bonk := Honk{
694 UserID: userinfo.UserID,
695 Username: userinfo.Username,
696 Honker: xonk.Honker,
697 What: "bonk",
698 XID: xonk.XID,
699 Date: dt,
700 Noise: xonk.Noise,
701 Convoy: convoy,
702 Donks: xonk.Donks,
703 Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
704 }
705
706 user, _ := butwhatabout(userinfo.Username)
707
708 aud := strings.Join(bonk.Audience, " ")
709 whofore := 0
710 if strings.Contains(aud, user.URL) {
711 whofore = 1
712 }
713 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
714 dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
715 if err != nil {
716 log.Printf("error saving bonk: %s", err)
717 return
718 }
719 bonk.ID, _ = res.LastInsertId()
720 for _, d := range bonk.Donks {
721 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
722 if err != nil {
723 log.Printf("err saving donk: %s", err)
724 return
725 }
726 }
727
728 go honkworldwide(user, &bonk)
729
730}
731
732func zonkit(w http.ResponseWriter, r *http.Request) {
733 xid := r.FormValue("xid")
734
735 log.Printf("zonking %s", xid)
736 userinfo := GetUserInfo(r)
737 stmtZonkIt.Exec(userinfo.UserID, xid)
738}
739
740func savehonk(w http.ResponseWriter, r *http.Request) {
741 rid := r.FormValue("rid")
742 noise := r.FormValue("noise")
743
744 userinfo := GetUserInfo(r)
745
746 dt := time.Now().UTC()
747 xid := xfiltrate()
748 what := "honk"
749 if rid != "" {
750 what = "tonk"
751 }
752 honk := Honk{
753 UserID: userinfo.UserID,
754 Username: userinfo.Username,
755 What: "honk",
756 XID: xid,
757 RID: rid,
758 Date: dt,
759 }
760 if noise[0] == '@' {
761 honk.Audience = append(grapevine(noise), thewholeworld)
762 } else {
763 honk.Audience = prepend(thewholeworld, grapevine(noise))
764 }
765 var convoy string
766 if rid != "" {
767 xonk := getxonk("", rid)
768 if xonk != nil {
769 honk.Audience = append(honk.Audience, xonk.Audience...)
770 convoy = xonk.Convoy
771 } else {
772 xonkaud, c := whosthere(rid)
773 honk.Audience = append(honk.Audience, xonkaud...)
774 convoy = c
775 }
776 }
777 if convoy == "" {
778 convoy = "data:,electrichonkytonk-" + xfiltrate()
779 }
780 honk.Audience = oneofakind(honk.Audience)
781 noise = obfusbreak(noise)
782 honk.Noise = noise
783 honk.Convoy = convoy
784
785 file, filehdr, err := r.FormFile("donk")
786 if err == nil {
787 var buf bytes.Buffer
788 io.Copy(&buf, file)
789 file.Close()
790 data := buf.Bytes()
791 xid := xfiltrate()
792 var media, name string
793 img, format, err := image.Decode(&buf)
794 if err == nil {
795 data, format, err = vacuumwrap(img, format)
796 if err != nil {
797 log.Printf("can't vacuum image: %s", err)
798 return
799 }
800 media = "image/" + format
801 if format == "jpeg" {
802 format = "jpg"
803 }
804 name = xid + "." + format
805 xid = name
806 } else {
807 maxsize := 100000
808 if len(data) > maxsize {
809 log.Printf("bad image: %s too much text: %d", err, len(data))
810 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
811 return
812 }
813 for i := 0; i < len(data); i++ {
814 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
815 log.Printf("bad image: %s not text: %d", err, data[i])
816 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
817 return
818 }
819 }
820 media = "text/plain"
821 name = filehdr.Filename
822 if name == "" {
823 name = xid + ".txt"
824 }
825 xid += ".txt"
826 }
827 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
828 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
829 if err != nil {
830 log.Printf("unable to save image: %s", err)
831 return
832 }
833 var d Donk
834 d.FileID, _ = res.LastInsertId()
835 d.XID = name
836 d.Name = name
837 d.Media = media
838 d.URL = url
839 honk.Donks = append(honk.Donks, &d)
840 }
841 herd := herdofemus(honk.Noise)
842 for _, e := range herd {
843 donk := savedonk(e.ID, e.Name, "image/png")
844 if donk != nil {
845 donk.Name = e.Name
846 honk.Donks = append(honk.Donks, donk)
847 }
848 }
849
850 user, _ := butwhatabout(userinfo.Username)
851
852 aud := strings.Join(honk.Audience, " ")
853 whofore := 0
854 if strings.Contains(aud, user.URL) {
855 whofore = 1
856 }
857 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
858 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
859 if err != nil {
860 log.Printf("error saving honk: %s", err)
861 return
862 }
863 honk.ID, _ = res.LastInsertId()
864 for _, d := range honk.Donks {
865 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
866 if err != nil {
867 log.Printf("err saving donk: %s", err)
868 return
869 }
870 }
871
872 go honkworldwide(user, &honk)
873
874 http.Redirect(w, r, "/", http.StatusSeeOther)
875}
876
877func showhonkers(w http.ResponseWriter, r *http.Request) {
878 userinfo := GetUserInfo(r)
879 templinfo := getInfo(r)
880 templinfo["Honkers"] = gethonkers(userinfo.UserID)
881 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
882 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
883 if err != nil {
884 log.Print(err)
885 }
886}
887
888var handfull = make(map[string]string)
889var handlock sync.Mutex
890
891func gofish(name string) string {
892 if name[0] == '@' {
893 name = name[1:]
894 }
895 m := strings.Split(name, "@")
896 if len(m) != 2 {
897 log.Printf("bad fish name: %s", name)
898 return ""
899 }
900 handlock.Lock()
901 ref, ok := handfull[name]
902 handlock.Unlock()
903 if ok {
904 return ref
905 }
906 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
907 handlock.Lock()
908 defer handlock.Unlock()
909 if err != nil {
910 log.Printf("failed to go fish %s: %s", name, err)
911 handfull[name] = ""
912 return ""
913 }
914 links, _ := jsongetarray(j, "links")
915 for _, l := range links {
916 href, _ := jsongetstring(l, "href")
917 rel, _ := jsongetstring(l, "rel")
918 t, _ := jsongetstring(l, "type")
919 if rel == "self" && friendorfoe(t) {
920 handfull[name] = href
921 return href
922 }
923 }
924 handfull[name] = ""
925 return ""
926}
927
928func savehonker(w http.ResponseWriter, r *http.Request) {
929 name := r.FormValue("name")
930 url := r.FormValue("url")
931 peep := r.FormValue("peep")
932 flavor := "presub"
933 if peep == "peep" {
934 flavor = "peep"
935 }
936
937 if url == "" {
938 return
939 }
940 if url[0] == '@' {
941 url = gofish(url)
942 }
943 if url == "" {
944 return
945 }
946
947 u := GetUserInfo(r)
948 db := opendatabase()
949 _, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
950 u.UserID, name, url, flavor)
951 if err != nil {
952 log.Print(err)
953 }
954 if flavor == "presub" {
955 user, _ := butwhatabout(u.Username)
956 go subsub(user, url)
957 }
958 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
959}
960
961type Zonker struct {
962 Name string
963 Wherefore string
964}
965
966func killzone(w http.ResponseWriter, r *http.Request) {
967 db := opendatabase()
968 userinfo := GetUserInfo(r)
969 rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
970 if err != nil {
971 log.Printf("err: %s", err)
972 return
973 }
974 var zonkers []Zonker
975 for rows.Next() {
976 var z Zonker
977 rows.Scan(&z.Name, &z.Wherefore)
978 zonkers = append(zonkers, z)
979 }
980 templinfo := getInfo(r)
981 templinfo["Zonkers"] = zonkers
982 templinfo["KillCSRF"] = GetCSRF("killitwithfire", r)
983 err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
984 if err != nil {
985 log.Print(err)
986 }
987}
988
989func killitwithfire(w http.ResponseWriter, r *http.Request) {
990 userinfo := GetUserInfo(r)
991 wherefore := r.FormValue("wherefore")
992 name := r.FormValue("name")
993 if name == "" {
994 return
995 }
996 switch wherefore {
997 case "zonker":
998 case "zurl":
999 case "zonvoy":
1000 default:
1001 return
1002 }
1003 db := opendatabase()
1004 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1005 userinfo.UserID, name, wherefore)
1006
1007 http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1008}
1009
1010func somedays() string {
1011 secs := 432000 + notrand.Int63n(432000)
1012 return fmt.Sprintf("%d", secs)
1013}
1014
1015func avatate(w http.ResponseWriter, r *http.Request) {
1016 n := r.FormValue("a")
1017 a := avatar(n)
1018 w.Header().Set("Cache-Control", "max-age="+somedays())
1019 w.Write(a)
1020}
1021
1022func servecss(w http.ResponseWriter, r *http.Request) {
1023 w.Header().Set("Cache-Control", "max-age=7776000")
1024 http.ServeFile(w, r, "views"+r.URL.Path)
1025}
1026func servehtml(w http.ResponseWriter, r *http.Request) {
1027 templinfo := getInfo(r)
1028 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1029 if err != nil {
1030 log.Print(err)
1031 }
1032}
1033func serveemu(w http.ResponseWriter, r *http.Request) {
1034 xid := mux.Vars(r)["xid"]
1035 w.Header().Set("Cache-Control", "max-age="+somedays())
1036 http.ServeFile(w, r, "emus/"+xid)
1037}
1038
1039func servefile(w http.ResponseWriter, r *http.Request) {
1040 xid := mux.Vars(r)["xid"]
1041 row := stmtFileData.QueryRow(xid)
1042 var media string
1043 var data []byte
1044 err := row.Scan(&media, &data)
1045 if err != nil {
1046 log.Printf("error loading file: %s", err)
1047 http.NotFound(w, r)
1048 return
1049 }
1050 w.Header().Set("Content-Type", media)
1051 w.Header().Set("X-Content-Type-Options", "nosniff")
1052 w.Header().Set("Cache-Control", "max-age="+somedays())
1053 w.Write(data)
1054}
1055
1056func serve() {
1057 db := opendatabase()
1058 LoginInit(db)
1059
1060 listener, err := openListener()
1061 if err != nil {
1062 log.Fatal(err)
1063 }
1064 go redeliverator()
1065
1066 debug := false
1067 getconfig("debug", &debug)
1068 readviews = ParseTemplates(debug,
1069 "views/homepage.html",
1070 "views/honkpage.html",
1071 "views/honkers.html",
1072 "views/zonkers.html",
1073 "views/honkform.html",
1074 "views/honk.html",
1075 "views/login.html",
1076 "views/header.html",
1077 )
1078 if !debug {
1079 s := "views/style.css"
1080 savedstyleparams[s] = getstyleparam(s)
1081 s = "views/local.css"
1082 savedstyleparams[s] = getstyleparam(s)
1083 }
1084
1085 mux := mux.NewRouter()
1086 mux.Use(LoginChecker)
1087
1088 posters := mux.Methods("POST").Subrouter()
1089 getters := mux.Methods("GET").Subrouter()
1090
1091 getters.HandleFunc("/", homepage)
1092 getters.Handle("/atme", LoginRequired(http.HandlerFunc(homepage)))
1093 getters.Handle("/killzone", LoginRequired(http.HandlerFunc(killzone)))
1094 getters.HandleFunc("/rss", showrss)
1095 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1096 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1097 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1098 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1099 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1100 getters.HandleFunc("/a", avatate)
1101 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1102 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1103 getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1104 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1105
1106 getters.HandleFunc("/style.css", servecss)
1107 getters.HandleFunc("/local.css", servecss)
1108 getters.HandleFunc("/login", servehtml)
1109 posters.HandleFunc("/dologin", dologin)
1110 getters.HandleFunc("/logout", dologout)
1111
1112 loggedin := mux.NewRoute().Subrouter()
1113 loggedin.Use(LoginRequired)
1114 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1115 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1116 loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1117 loggedin.Handle("/killitwithfire", CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1118 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1119 loggedin.HandleFunc("/honkers", showhonkers)
1120 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1121
1122 err = http.Serve(listener, mux)
1123 if err != nil {
1124 log.Fatal(err)
1125 }
1126}
1127
1128var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1129var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1130var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1131var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1132var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1133var stmtThumbBiter, stmtZonkIt *sql.Stmt
1134
1135func preparetodie(db *sql.DB, s string) *sql.Stmt {
1136 stmt, err := db.Prepare(s)
1137 if err != nil {
1138 log.Fatalf("error %s: %s", err, s)
1139 }
1140 return stmt
1141}
1142
1143func prepareStatements(db *sql.DB) {
1144 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1145 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1146 stmtOneXonk = preparetodie(db, "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where xid = ?")
1147 stmtHonks = preparetodie(db, "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honker = '' order by honkid desc limit 50")
1148 stmtUserHonks = preparetodie(db, "select honkid, honks.userid, username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honker = '' and username = ? order by honkid desc limit 50")
1149 stmtHonksForUser = preparetodie(db, "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honks.userid = ? and dt > ? and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100) order by honkid desc limit 250")
1150 stmtHonksForMe = preparetodie(db, "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honks.userid = ? and dt > ? and whofore = 1 and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100) order by honkid desc limit 250")
1151 stmtHonksByHonker = preparetodie(db, "select honkid, honks.userid, users.username, what, honker, honks.xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ? order by honkid desc limit 50")
1152 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1153 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1154 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1155 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1156 stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1157 stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1158 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1159 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1160 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1161 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1162 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1163 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1164 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1165 stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1166 stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1167}
1168
1169func ElaborateUnitTests() {
1170}
1171
1172func finishusersetup() error {
1173 db := opendatabase()
1174 k, err := rsa.GenerateKey(rand.Reader, 2048)
1175 if err != nil {
1176 return err
1177 }
1178 pubkey, err := zem(&k.PublicKey)
1179 if err != nil {
1180 return err
1181 }
1182 seckey, err := zem(k)
1183 if err != nil {
1184 return err
1185 }
1186 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1187 if err != nil {
1188 return err
1189 }
1190 return nil
1191}
1192
1193func main() {
1194 cmd := "run"
1195 if len(os.Args) > 1 {
1196 cmd = os.Args[1]
1197 }
1198 switch cmd {
1199 case "init":
1200 initdb()
1201 case "upgrade":
1202 upgradedb()
1203 }
1204 db := opendatabase()
1205 dbversion := 0
1206 getconfig("dbversion", &dbversion)
1207 if dbversion != myVersion {
1208 log.Fatal("incorrect database version. run upgrade.")
1209 }
1210 getconfig("servername", &serverName)
1211 prepareStatements(db)
1212 switch cmd {
1213 case "ping":
1214 if len(os.Args) < 4 {
1215 fmt.Printf("usage: honk ping from to\n")
1216 return
1217 }
1218 name := os.Args[2]
1219 targ := os.Args[3]
1220 user, err := butwhatabout(name)
1221 if err != nil {
1222 log.Printf("unknown user")
1223 return
1224 }
1225 ping(user, targ)
1226 case "peep":
1227 peeppeep()
1228 case "run":
1229 serve()
1230 case "test":
1231 ElaborateUnitTests()
1232 default:
1233 log.Fatal("unknown command")
1234 }
1235}