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, "homepage.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, nil, 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, nil, nil, honks)
433}
434
435func fingerlicker(w http.ResponseWriter, r *http.Request) {
436 orig := r.FormValue("resource")
437
438 log.Printf("finger lick: %s", orig)
439
440 if strings.HasPrefix(orig, "acct:") {
441 orig = orig[5:]
442 }
443
444 name := orig
445 idx := strings.LastIndexByte(name, '/')
446 if idx != -1 {
447 name = name[idx+1:]
448 if "https://"+serverName+"/u/"+name != orig {
449 log.Printf("foreign request rejected")
450 name = ""
451 }
452 } else {
453 idx = strings.IndexByte(name, '@')
454 if idx != -1 {
455 name = name[:idx]
456 if name+"@"+serverName != orig {
457 log.Printf("foreign request rejected")
458 name = ""
459 }
460 }
461 }
462 user, err := butwhatabout(name)
463 if err != nil {
464 http.NotFound(w, r)
465 return
466 }
467
468 j := NewJunk()
469 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
470 j["aliases"] = []string{user.URL}
471 var links []map[string]interface{}
472 l := NewJunk()
473 l["rel"] = "self"
474 l["type"] = `application/activity+json`
475 l["href"] = user.URL
476 links = append(links, l)
477 j["links"] = links
478
479 w.Header().Set("Cache-Control", "max-age=3600")
480 w.Header().Set("Content-Type", "application/jrd+json")
481 WriteJunk(w, j)
482}
483
484func viewhonk(w http.ResponseWriter, r *http.Request) {
485 name := mux.Vars(r)["name"]
486 xid := mux.Vars(r)["xid"]
487 user, err := butwhatabout(name)
488 if err != nil {
489 http.NotFound(w, r)
490 return
491 }
492 h := getxonk(name, xid)
493 if h == nil {
494 http.NotFound(w, r)
495 return
496 }
497 if friendorfoe(r.Header.Get("Accept")) {
498 _, j := jonkjonk(user, h)
499 j["@context"] = itiswhatitis
500 w.Header().Set("Cache-Control", "max-age=3600")
501 w.Header().Set("Content-Type", theonetruename)
502 WriteJunk(w, j)
503 return
504 }
505 honkpage(w, r, nil, nil, []*Honk{h})
506}
507
508func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk) {
509 reverbolate(honks)
510 templinfo := getInfo(r)
511 if u != nil && u.Username == user.Name {
512 templinfo["UserCSRF"] = GetCSRF("saveuser", r)
513 templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
514 }
515 if u == nil {
516 w.Header().Set("Cache-Control", "max-age=60")
517 }
518 if user != nil {
519 templinfo["Name"] = user.Name
520 whatabout := user.About
521 templinfo["RawWhatAbout"] = whatabout
522 whatabout = obfusbreak(whatabout)
523 templinfo["WhatAbout"] = cleanstring(whatabout)
524 }
525 templinfo["Honks"] = honks
526 err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
527 if err != nil {
528 log.Print(err)
529 }
530}
531
532func saveuser(w http.ResponseWriter, r *http.Request) {
533 whatabout := r.FormValue("whatabout")
534 u := GetUserInfo(r)
535 db := opendatabase()
536 _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
537 if err != nil {
538 log.Printf("error bouting what: %s", err)
539 }
540
541 http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
542}
543
544func gethonkers(userid int64) []*Honker {
545 rows, err := stmtHonkers.Query(userid)
546 if err != nil {
547 log.Printf("error querying honkers: %s", err)
548 return nil
549 }
550 defer rows.Close()
551 var honkers []*Honker
552 for rows.Next() {
553 var f Honker
554 var combos string
555 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
556 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
557 if err != nil {
558 log.Printf("error scanning honker: %s", err)
559 return nil
560 }
561 honkers = append(honkers, &f)
562 }
563 return honkers
564}
565
566func getdubs(userid int64) []*Honker {
567 rows, err := stmtDubbers.Query(userid)
568 if err != nil {
569 log.Printf("error querying dubs: %s", err)
570 return nil
571 }
572 defer rows.Close()
573 var honkers []*Honker
574 for rows.Next() {
575 var f Honker
576 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
577 if err != nil {
578 log.Printf("error scanning honker: %s", err)
579 return nil
580 }
581 honkers = append(honkers, &f)
582 }
583 return honkers
584}
585
586func getxonk(name, xid string) *Honk {
587 var h Honk
588 var dt, aud string
589 row := stmtOneXonk.QueryRow(xid)
590 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
591 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
592 if err != nil {
593 if err != sql.ErrNoRows {
594 log.Printf("error scanning xonk: %s", err)
595 }
596 return nil
597 }
598 if name != "" && h.Username != name {
599 log.Printf("user xonk mismatch")
600 return nil
601 }
602 h.Date, _ = time.Parse(dbtimeformat, dt)
603 h.Audience = strings.Split(aud, " ")
604 donksforhonks([]*Honk{&h})
605 return &h
606}
607
608func gethonks() []*Honk {
609 rows, err := stmtHonks.Query()
610 return getsomehonks(rows, err)
611}
612func gethonksbyuser(name string) []*Honk {
613 rows, err := stmtUserHonks.Query(name)
614 return getsomehonks(rows, err)
615}
616func gethonksforuser(userid int64) []*Honk {
617 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
618 rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
619 return getsomehonks(rows, err)
620}
621func gethonksforme(userid int64) []*Honk {
622 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
623 rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
624 return getsomehonks(rows, err)
625}
626func gethonksbyhonker(userid int64, honker string) []*Honk {
627 rows, err := stmtHonksByHonker.Query(userid, honker)
628 return getsomehonks(rows, err)
629}
630func gethonksbycombo(userid int64, combo string) []*Honk {
631 combo = "% " + combo + " %"
632 rows, err := stmtHonksByCombo.Query(userid, combo)
633 return getsomehonks(rows, err)
634}
635
636func getsomehonks(rows *sql.Rows, err error) []*Honk {
637 if err != nil {
638 log.Printf("error querying honks: %s", err)
639 return nil
640 }
641 defer rows.Close()
642 var honks []*Honk
643 for rows.Next() {
644 var h Honk
645 var dt, aud string
646 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
647 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
648 if err != nil {
649 log.Printf("error scanning honks: %s", err)
650 return nil
651 }
652 h.Date, _ = time.Parse(dbtimeformat, dt)
653 h.Audience = strings.Split(aud, " ")
654 honks = append(honks, &h)
655 }
656 rows.Close()
657 donksforhonks(honks)
658 return honks
659}
660
661func donksforhonks(honks []*Honk) {
662 db := opendatabase()
663 var ids []string
664 hmap := make(map[int64]*Honk)
665 for _, h := range honks {
666 if h.What == "zonk" {
667 continue
668 }
669 ids = append(ids, fmt.Sprintf("%d", h.ID))
670 hmap[h.ID] = h
671 }
672 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, ","))
673 rows, err := db.Query(q)
674 if err != nil {
675 log.Printf("error querying donks: %s", err)
676 return
677 }
678 defer rows.Close()
679 for rows.Next() {
680 var hid int64
681 var d Donk
682 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
683 if err != nil {
684 log.Printf("error scanning donk: %s", err)
685 continue
686 }
687 h := hmap[hid]
688 h.Donks = append(h.Donks, &d)
689 }
690}
691
692func savebonk(w http.ResponseWriter, r *http.Request) {
693 xid := r.FormValue("xid")
694
695 log.Printf("bonking %s", xid)
696
697 xonk := getxonk("", xid)
698 if xonk == nil {
699 return
700 }
701 if xonk.Honker == "" {
702 xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
703 }
704 convoy := xonk.Convoy
705
706 userinfo := GetUserInfo(r)
707
708 dt := time.Now().UTC()
709 bonk := Honk{
710 UserID: userinfo.UserID,
711 Username: userinfo.Username,
712 Honker: xonk.Honker,
713 What: "bonk",
714 XID: xonk.XID,
715 Date: dt,
716 Noise: xonk.Noise,
717 Convoy: convoy,
718 Donks: xonk.Donks,
719 Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
720 }
721
722 user, _ := butwhatabout(userinfo.Username)
723
724 aud := strings.Join(bonk.Audience, " ")
725 whofore := 0
726 if strings.Contains(aud, user.URL) {
727 whofore = 1
728 }
729 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
730 dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
731 if err != nil {
732 log.Printf("error saving bonk: %s", err)
733 return
734 }
735 bonk.ID, _ = res.LastInsertId()
736 for _, d := range bonk.Donks {
737 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
738 if err != nil {
739 log.Printf("err saving donk: %s", err)
740 return
741 }
742 }
743
744 go honkworldwide(user, &bonk)
745
746}
747
748func zonkit(w http.ResponseWriter, r *http.Request) {
749 xid := r.FormValue("xid")
750
751 log.Printf("zonking %s", xid)
752 userinfo := GetUserInfo(r)
753 stmtZonkIt.Exec(userinfo.UserID, xid)
754}
755
756func savehonk(w http.ResponseWriter, r *http.Request) {
757 rid := r.FormValue("rid")
758 noise := r.FormValue("noise")
759
760 userinfo := GetUserInfo(r)
761
762 dt := time.Now().UTC()
763 xid := xfiltrate()
764 what := "honk"
765 if rid != "" {
766 what = "tonk"
767 }
768 honk := Honk{
769 UserID: userinfo.UserID,
770 Username: userinfo.Username,
771 What: "honk",
772 XID: xid,
773 RID: rid,
774 Date: dt,
775 }
776 if noise[0] == '@' {
777 honk.Audience = append(grapevine(noise), thewholeworld)
778 } else {
779 honk.Audience = prepend(thewholeworld, grapevine(noise))
780 }
781 var convoy string
782 if rid != "" {
783 xonk := getxonk("", rid)
784 if xonk != nil {
785 honk.Audience = append(honk.Audience, xonk.Audience...)
786 convoy = xonk.Convoy
787 } else {
788 xonkaud, c := whosthere(rid)
789 honk.Audience = append(honk.Audience, xonkaud...)
790 convoy = c
791 }
792 }
793 if convoy == "" {
794 convoy = "data:,electrichonkytonk-" + xfiltrate()
795 }
796 honk.Audience = oneofakind(honk.Audience)
797 noise = obfusbreak(noise)
798 honk.Noise = noise
799 honk.Convoy = convoy
800
801 file, filehdr, err := r.FormFile("donk")
802 if err == nil {
803 var buf bytes.Buffer
804 io.Copy(&buf, file)
805 file.Close()
806 data := buf.Bytes()
807 xid := xfiltrate()
808 var media, name string
809 img, format, err := image.Decode(&buf)
810 if err == nil {
811 data, format, err = vacuumwrap(img, format)
812 if err != nil {
813 log.Printf("can't vacuum image: %s", err)
814 return
815 }
816 media = "image/" + format
817 if format == "jpeg" {
818 format = "jpg"
819 }
820 name = xid + "." + format
821 xid = name
822 } else {
823 maxsize := 100000
824 if len(data) > maxsize {
825 log.Printf("bad image: %s too much text: %d", err, len(data))
826 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
827 return
828 }
829 for i := 0; i < len(data); i++ {
830 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
831 log.Printf("bad image: %s not text: %d", err, data[i])
832 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
833 return
834 }
835 }
836 media = "text/plain"
837 name = filehdr.Filename
838 if name == "" {
839 name = xid + ".txt"
840 }
841 xid += ".txt"
842 }
843 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
844 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
845 if err != nil {
846 log.Printf("unable to save image: %s", err)
847 return
848 }
849 var d Donk
850 d.FileID, _ = res.LastInsertId()
851 d.XID = name
852 d.Name = name
853 d.Media = media
854 d.URL = url
855 honk.Donks = append(honk.Donks, &d)
856 }
857 herd := herdofemus(honk.Noise)
858 for _, e := range herd {
859 donk := savedonk(e.ID, e.Name, "image/png")
860 if donk != nil {
861 donk.Name = e.Name
862 honk.Donks = append(honk.Donks, donk)
863 }
864 }
865
866 user, _ := butwhatabout(userinfo.Username)
867
868 aud := strings.Join(honk.Audience, " ")
869 whofore := 0
870 if strings.Contains(aud, user.URL) {
871 whofore = 1
872 }
873 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
874 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
875 if err != nil {
876 log.Printf("error saving honk: %s", err)
877 return
878 }
879 honk.ID, _ = res.LastInsertId()
880 for _, d := range honk.Donks {
881 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
882 if err != nil {
883 log.Printf("err saving donk: %s", err)
884 return
885 }
886 }
887
888 go honkworldwide(user, &honk)
889
890 http.Redirect(w, r, "/", http.StatusSeeOther)
891}
892
893func viewhonkers(w http.ResponseWriter, r *http.Request) {
894 userinfo := GetUserInfo(r)
895 templinfo := getInfo(r)
896 templinfo["Honkers"] = gethonkers(userinfo.UserID)
897 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
898 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
899 if err != nil {
900 log.Print(err)
901 }
902}
903
904var handfull = make(map[string]string)
905var handlock sync.Mutex
906
907func gofish(name string) string {
908 if name[0] == '@' {
909 name = name[1:]
910 }
911 m := strings.Split(name, "@")
912 if len(m) != 2 {
913 log.Printf("bad fish name: %s", name)
914 return ""
915 }
916 handlock.Lock()
917 ref, ok := handfull[name]
918 handlock.Unlock()
919 if ok {
920 return ref
921 }
922 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
923 handlock.Lock()
924 defer handlock.Unlock()
925 if err != nil {
926 log.Printf("failed to go fish %s: %s", name, err)
927 handfull[name] = ""
928 return ""
929 }
930 links, _ := jsongetarray(j, "links")
931 for _, l := range links {
932 href, _ := jsongetstring(l, "href")
933 rel, _ := jsongetstring(l, "rel")
934 t, _ := jsongetstring(l, "type")
935 if rel == "self" && friendorfoe(t) {
936 handfull[name] = href
937 return href
938 }
939 }
940 handfull[name] = ""
941 return ""
942}
943
944func savehonker(w http.ResponseWriter, r *http.Request) {
945 u := GetUserInfo(r)
946 name := r.FormValue("name")
947 url := r.FormValue("url")
948 peep := r.FormValue("peep")
949 combos := r.FormValue("combos")
950 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
951
952 if honkerid > 0 {
953 combos = " " + strings.TrimSpace(combos) + " "
954 _, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
955 if err != nil {
956 log.Printf("update honker err: %s", err)
957 return
958 }
959 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
960 }
961
962 flavor := "presub"
963 if peep == "peep" {
964 flavor = "peep"
965 }
966 if url == "" {
967 return
968 }
969 if url[0] == '@' {
970 url = gofish(url)
971 }
972 if url == "" {
973 return
974 }
975 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
976 if err != nil {
977 log.Print(err)
978 return
979 }
980 if flavor == "presub" {
981 user, _ := butwhatabout(u.Username)
982 go subsub(user, url)
983 }
984 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
985}
986
987type Zonker struct {
988 Name string
989 Wherefore string
990}
991
992func killzone(w http.ResponseWriter, r *http.Request) {
993 db := opendatabase()
994 userinfo := GetUserInfo(r)
995 rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
996 if err != nil {
997 log.Printf("err: %s", err)
998 return
999 }
1000 var zonkers []Zonker
1001 for rows.Next() {
1002 var z Zonker
1003 rows.Scan(&z.Name, &z.Wherefore)
1004 zonkers = append(zonkers, z)
1005 }
1006 templinfo := getInfo(r)
1007 templinfo["Zonkers"] = zonkers
1008 templinfo["KillCSRF"] = GetCSRF("killitwithfire", r)
1009 err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1010 if err != nil {
1011 log.Print(err)
1012 }
1013}
1014
1015func killitwithfire(w http.ResponseWriter, r *http.Request) {
1016 userinfo := GetUserInfo(r)
1017 wherefore := r.FormValue("wherefore")
1018 name := r.FormValue("name")
1019 if name == "" {
1020 return
1021 }
1022 switch wherefore {
1023 case "zonker":
1024 case "zurl":
1025 case "zonvoy":
1026 default:
1027 return
1028 }
1029 db := opendatabase()
1030 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1031 userinfo.UserID, name, wherefore)
1032
1033 http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1034}
1035
1036func somedays() string {
1037 secs := 432000 + notrand.Int63n(432000)
1038 return fmt.Sprintf("%d", secs)
1039}
1040
1041func avatate(w http.ResponseWriter, r *http.Request) {
1042 n := r.FormValue("a")
1043 a := avatar(n)
1044 w.Header().Set("Cache-Control", "max-age="+somedays())
1045 w.Write(a)
1046}
1047
1048func servecss(w http.ResponseWriter, r *http.Request) {
1049 w.Header().Set("Cache-Control", "max-age=7776000")
1050 http.ServeFile(w, r, "views"+r.URL.Path)
1051}
1052func servehtml(w http.ResponseWriter, r *http.Request) {
1053 templinfo := getInfo(r)
1054 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1055 if err != nil {
1056 log.Print(err)
1057 }
1058}
1059func serveemu(w http.ResponseWriter, r *http.Request) {
1060 xid := mux.Vars(r)["xid"]
1061 w.Header().Set("Cache-Control", "max-age="+somedays())
1062 http.ServeFile(w, r, "emus/"+xid)
1063}
1064
1065func servefile(w http.ResponseWriter, r *http.Request) {
1066 xid := mux.Vars(r)["xid"]
1067 row := stmtFileData.QueryRow(xid)
1068 var media string
1069 var data []byte
1070 err := row.Scan(&media, &data)
1071 if err != nil {
1072 log.Printf("error loading file: %s", err)
1073 http.NotFound(w, r)
1074 return
1075 }
1076 w.Header().Set("Content-Type", media)
1077 w.Header().Set("X-Content-Type-Options", "nosniff")
1078 w.Header().Set("Cache-Control", "max-age="+somedays())
1079 w.Write(data)
1080}
1081
1082func serve() {
1083 db := opendatabase()
1084 LoginInit(db)
1085
1086 listener, err := openListener()
1087 if err != nil {
1088 log.Fatal(err)
1089 }
1090 go redeliverator()
1091
1092 debug := false
1093 getconfig("debug", &debug)
1094 readviews = ParseTemplates(debug,
1095 "views/homepage.html",
1096 "views/honkpage.html",
1097 "views/honkers.html",
1098 "views/zonkers.html",
1099 "views/honkform.html",
1100 "views/honk.html",
1101 "views/login.html",
1102 "views/header.html",
1103 )
1104 if !debug {
1105 s := "views/style.css"
1106 savedstyleparams[s] = getstyleparam(s)
1107 s = "views/local.css"
1108 savedstyleparams[s] = getstyleparam(s)
1109 }
1110
1111 mux := mux.NewRouter()
1112 mux.Use(LoginChecker)
1113
1114 posters := mux.Methods("POST").Subrouter()
1115 getters := mux.Methods("GET").Subrouter()
1116
1117 getters.HandleFunc("/", homepage)
1118 getters.HandleFunc("/rss", showrss)
1119 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1120 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1121 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1122 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1123 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1124 getters.HandleFunc("/a", avatate)
1125 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1126 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1127 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1128
1129 getters.HandleFunc("/style.css", servecss)
1130 getters.HandleFunc("/local.css", servecss)
1131 getters.HandleFunc("/login", servehtml)
1132 posters.HandleFunc("/dologin", dologin)
1133 getters.HandleFunc("/logout", dologout)
1134
1135 loggedin := mux.NewRoute().Subrouter()
1136 loggedin.Use(LoginRequired)
1137 loggedin.HandleFunc("/atme", homepage)
1138 loggedin.HandleFunc("/killzone", killzone)
1139 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1140 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1141 loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1142 loggedin.Handle("/killitwithfire", CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1143 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1144 loggedin.HandleFunc("/honkers", viewhonkers)
1145 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1146 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1147 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1148
1149 err = http.Serve(listener, mux)
1150 if err != nil {
1151 log.Fatal(err)
1152 }
1153}
1154
1155var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1156var stmtOneXonk, stmtHonks, stmtUserHonks, stmtHonksByCombo *sql.Stmt
1157var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1158var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1159var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1160var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1161var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1162
1163func preparetodie(db *sql.DB, s string) *sql.Stmt {
1164 stmt, err := db.Prepare(s)
1165 if err != nil {
1166 log.Fatalf("error %s: %s", err, s)
1167 }
1168 return stmt
1169}
1170
1171func prepareStatements(db *sql.DB) {
1172 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1173 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1174 stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1175 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1176 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1177 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 = ?")
1178 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")
1179 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")
1180 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")
1181 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")
1182 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")
1183 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")
1184 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1185 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1186 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1187 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1188 stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1189 stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1190 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1191 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1192 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1193 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1194 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1195 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1196 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1197 stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1198 stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1199}
1200
1201func ElaborateUnitTests() {
1202}
1203
1204func finishusersetup() error {
1205 db := opendatabase()
1206 k, err := rsa.GenerateKey(rand.Reader, 2048)
1207 if err != nil {
1208 return err
1209 }
1210 pubkey, err := zem(&k.PublicKey)
1211 if err != nil {
1212 return err
1213 }
1214 seckey, err := zem(k)
1215 if err != nil {
1216 return err
1217 }
1218 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1219 if err != nil {
1220 return err
1221 }
1222 return nil
1223}
1224
1225func main() {
1226 cmd := "run"
1227 if len(os.Args) > 1 {
1228 cmd = os.Args[1]
1229 }
1230 switch cmd {
1231 case "init":
1232 initdb()
1233 case "upgrade":
1234 upgradedb()
1235 }
1236 db := opendatabase()
1237 dbversion := 0
1238 getconfig("dbversion", &dbversion)
1239 if dbversion != myVersion {
1240 log.Fatal("incorrect database version. run upgrade.")
1241 }
1242 getconfig("servername", &serverName)
1243 prepareStatements(db)
1244 switch cmd {
1245 case "ping":
1246 if len(os.Args) < 4 {
1247 fmt.Printf("usage: honk ping from to\n")
1248 return
1249 }
1250 name := os.Args[2]
1251 targ := os.Args[3]
1252 user, err := butwhatabout(name)
1253 if err != nil {
1254 log.Printf("unknown user")
1255 return
1256 }
1257 ping(user, targ)
1258 case "peep":
1259 peeppeep()
1260 case "run":
1261 serve()
1262 case "test":
1263 ElaborateUnitTests()
1264 default:
1265 log.Fatal("unknown command")
1266 }
1267}