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