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
961func somedays() string {
962 secs := 432000 + notrand.Int63n(432000)
963 return fmt.Sprintf("%d", secs)
964}
965
966func avatate(w http.ResponseWriter, r *http.Request) {
967 n := r.FormValue("a")
968 a := avatar(n)
969 w.Header().Set("Cache-Control", "max-age="+somedays())
970 w.Write(a)
971}
972
973func servecss(w http.ResponseWriter, r *http.Request) {
974 w.Header().Set("Cache-Control", "max-age=7776000")
975 http.ServeFile(w, r, "views"+r.URL.Path)
976}
977func servehtml(w http.ResponseWriter, r *http.Request) {
978 templinfo := getInfo(r)
979 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
980 if err != nil {
981 log.Print(err)
982 }
983}
984func serveemu(w http.ResponseWriter, r *http.Request) {
985 xid := mux.Vars(r)["xid"]
986 w.Header().Set("Cache-Control", "max-age="+somedays())
987 http.ServeFile(w, r, "emus/"+xid)
988}
989
990func servefile(w http.ResponseWriter, r *http.Request) {
991 xid := mux.Vars(r)["xid"]
992 row := stmtFileData.QueryRow(xid)
993 var media string
994 var data []byte
995 err := row.Scan(&media, &data)
996 if err != nil {
997 log.Printf("error loading file: %s", err)
998 http.NotFound(w, r)
999 return
1000 }
1001 w.Header().Set("Content-Type", media)
1002 w.Header().Set("X-Content-Type-Options", "nosniff")
1003 w.Header().Set("Cache-Control", "max-age="+somedays())
1004 w.Write(data)
1005}
1006
1007func serve() {
1008 db := opendatabase()
1009 LoginInit(db)
1010
1011 listener, err := openListener()
1012 if err != nil {
1013 log.Fatal(err)
1014 }
1015 go redeliverator()
1016
1017 debug := false
1018 getconfig("debug", &debug)
1019 readviews = ParseTemplates(debug,
1020 "views/homepage.html",
1021 "views/honkpage.html",
1022 "views/honkers.html",
1023 "views/honkform.html",
1024 "views/honk.html",
1025 "views/login.html",
1026 "views/header.html",
1027 )
1028 if !debug {
1029 s := "views/style.css"
1030 savedstyleparams[s] = getstyleparam(s)
1031 s = "views/local.css"
1032 savedstyleparams[s] = getstyleparam(s)
1033 }
1034
1035 mux := mux.NewRouter()
1036 mux.Use(LoginChecker)
1037
1038 posters := mux.Methods("POST").Subrouter()
1039 getters := mux.Methods("GET").Subrouter()
1040
1041 getters.HandleFunc("/", homepage)
1042 getters.Handle("/atme", LoginRequired(http.HandlerFunc(homepage)))
1043 getters.HandleFunc("/rss", showrss)
1044 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1045 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1046 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1047 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1048 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1049 getters.HandleFunc("/a", avatate)
1050 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1051 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1052 getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1053 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1054
1055 getters.HandleFunc("/style.css", servecss)
1056 getters.HandleFunc("/local.css", servecss)
1057 getters.HandleFunc("/login", servehtml)
1058 posters.HandleFunc("/dologin", dologin)
1059 getters.HandleFunc("/logout", dologout)
1060
1061 loggedin := mux.NewRoute().Subrouter()
1062 loggedin.Use(LoginRequired)
1063 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1064 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1065 loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1066 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1067 loggedin.HandleFunc("/honkers", showhonkers)
1068 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1069
1070 err = http.Serve(listener, mux)
1071 if err != nil {
1072 log.Fatal(err)
1073 }
1074}
1075
1076var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1077var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1078var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1079var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1080var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1081var stmtThumbBiter, stmtZonkIt *sql.Stmt
1082
1083func preparetodie(db *sql.DB, s string) *sql.Stmt {
1084 stmt, err := db.Prepare(s)
1085 if err != nil {
1086 log.Fatalf("error %s: %s", err, s)
1087 }
1088 return stmt
1089}
1090
1091func prepareStatements(db *sql.DB) {
1092 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1093 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1094 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 = ?")
1095 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")
1096 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")
1097 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")
1098 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")
1099 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")
1100 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1101 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1102 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1103 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1104 stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1105 stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1106 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1107 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1108 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1109 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1110 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1111 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1112 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1113 stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1114 stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1115}
1116
1117func ElaborateUnitTests() {
1118}
1119
1120func finishusersetup() error {
1121 db := opendatabase()
1122 k, err := rsa.GenerateKey(rand.Reader, 2048)
1123 if err != nil {
1124 return err
1125 }
1126 pubkey, err := zem(&k.PublicKey)
1127 if err != nil {
1128 return err
1129 }
1130 seckey, err := zem(k)
1131 if err != nil {
1132 return err
1133 }
1134 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1135 if err != nil {
1136 return err
1137 }
1138 return nil
1139}
1140
1141func main() {
1142 cmd := "run"
1143 if len(os.Args) > 1 {
1144 cmd = os.Args[1]
1145 }
1146 switch cmd {
1147 case "init":
1148 initdb()
1149 case "upgrade":
1150 upgradedb()
1151 }
1152 db := opendatabase()
1153 dbversion := 0
1154 getconfig("dbversion", &dbversion)
1155 if dbversion != myVersion {
1156 log.Fatal("incorrect database version. run upgrade.")
1157 }
1158 getconfig("servername", &serverName)
1159 prepareStatements(db)
1160 switch cmd {
1161 case "ping":
1162 if len(os.Args) < 4 {
1163 fmt.Printf("usage: honk ping from to\n")
1164 return
1165 }
1166 name := os.Args[2]
1167 targ := os.Args[3]
1168 user, err := butwhatabout(name)
1169 if err != nil {
1170 log.Printf("unknown user")
1171 return
1172 }
1173 ping(user, targ)
1174 case "peep":
1175 peeppeep()
1176 case "run":
1177 serve()
1178 case "test":
1179 ElaborateUnitTests()
1180 default:
1181 log.Fatal("unknown command")
1182 }
1183}