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, 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 templinfo["ServerMessage"] = "(some info goes here)"
527 err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
528 if err != nil {
529 log.Print(err)
530 }
531}
532
533func saveuser(w http.ResponseWriter, r *http.Request) {
534 whatabout := r.FormValue("whatabout")
535 u := GetUserInfo(r)
536 db := opendatabase()
537 _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
538 if err != nil {
539 log.Printf("error bouting what: %s", err)
540 }
541
542 http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
543}
544
545func gethonkers(userid int64) []*Honker {
546 rows, err := stmtHonkers.Query(userid)
547 if err != nil {
548 log.Printf("error querying honkers: %s", err)
549 return nil
550 }
551 defer rows.Close()
552 var honkers []*Honker
553 for rows.Next() {
554 var f Honker
555 var combos string
556 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
557 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
558 if err != nil {
559 log.Printf("error scanning honker: %s", err)
560 return nil
561 }
562 honkers = append(honkers, &f)
563 }
564 return honkers
565}
566
567func getdubs(userid int64) []*Honker {
568 rows, err := stmtDubbers.Query(userid)
569 if err != nil {
570 log.Printf("error querying dubs: %s", err)
571 return nil
572 }
573 defer rows.Close()
574 var honkers []*Honker
575 for rows.Next() {
576 var f Honker
577 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
578 if err != nil {
579 log.Printf("error scanning honker: %s", err)
580 return nil
581 }
582 honkers = append(honkers, &f)
583 }
584 return honkers
585}
586
587func getxonk(name, xid string) *Honk {
588 var h Honk
589 var dt, aud string
590 row := stmtOneXonk.QueryRow(xid)
591 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
592 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
593 if err != nil {
594 if err != sql.ErrNoRows {
595 log.Printf("error scanning xonk: %s", err)
596 }
597 return nil
598 }
599 if name != "" && h.Username != name {
600 log.Printf("user xonk mismatch")
601 return nil
602 }
603 h.Date, _ = time.Parse(dbtimeformat, dt)
604 h.Audience = strings.Split(aud, " ")
605 donksforhonks([]*Honk{&h})
606 return &h
607}
608
609func gethonks() []*Honk {
610 rows, err := stmtHonks.Query()
611 return getsomehonks(rows, err)
612}
613func gethonksbyuser(name string) []*Honk {
614 rows, err := stmtUserHonks.Query(name)
615 return getsomehonks(rows, err)
616}
617func gethonksforuser(userid int64) []*Honk {
618 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
619 rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
620 return getsomehonks(rows, err)
621}
622func gethonksforme(userid int64) []*Honk {
623 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
624 rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
625 return getsomehonks(rows, err)
626}
627func gethonksbyhonker(userid int64, honker string) []*Honk {
628 rows, err := stmtHonksByHonker.Query(userid, honker)
629 return getsomehonks(rows, err)
630}
631func gethonksbycombo(userid int64, combo string) []*Honk {
632 combo = "% " + combo + " %"
633 rows, err := stmtHonksByCombo.Query(userid, combo)
634 return getsomehonks(rows, err)
635}
636
637func getsomehonks(rows *sql.Rows, err error) []*Honk {
638 if err != nil {
639 log.Printf("error querying honks: %s", err)
640 return nil
641 }
642 defer rows.Close()
643 var honks []*Honk
644 for rows.Next() {
645 var h Honk
646 var dt, aud string
647 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
648 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
649 if err != nil {
650 log.Printf("error scanning honks: %s", err)
651 return nil
652 }
653 h.Date, _ = time.Parse(dbtimeformat, dt)
654 h.Audience = strings.Split(aud, " ")
655 honks = append(honks, &h)
656 }
657 rows.Close()
658 donksforhonks(honks)
659 return honks
660}
661
662func donksforhonks(honks []*Honk) {
663 db := opendatabase()
664 var ids []string
665 hmap := make(map[int64]*Honk)
666 for _, h := range honks {
667 if h.What == "zonk" {
668 continue
669 }
670 ids = append(ids, fmt.Sprintf("%d", h.ID))
671 hmap[h.ID] = h
672 }
673 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, ","))
674 rows, err := db.Query(q)
675 if err != nil {
676 log.Printf("error querying donks: %s", err)
677 return
678 }
679 defer rows.Close()
680 for rows.Next() {
681 var hid int64
682 var d Donk
683 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
684 if err != nil {
685 log.Printf("error scanning donk: %s", err)
686 continue
687 }
688 h := hmap[hid]
689 h.Donks = append(h.Donks, &d)
690 }
691}
692
693func savebonk(w http.ResponseWriter, r *http.Request) {
694 xid := r.FormValue("xid")
695
696 log.Printf("bonking %s", xid)
697
698 xonk := getxonk("", xid)
699 if xonk == nil {
700 return
701 }
702 if xonk.Honker == "" {
703 xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
704 }
705 convoy := xonk.Convoy
706
707 userinfo := GetUserInfo(r)
708
709 dt := time.Now().UTC()
710 bonk := Honk{
711 UserID: userinfo.UserID,
712 Username: userinfo.Username,
713 Honker: xonk.Honker,
714 What: "bonk",
715 XID: xonk.XID,
716 Date: dt,
717 Noise: xonk.Noise,
718 Convoy: convoy,
719 Donks: xonk.Donks,
720 Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
721 }
722
723 user, _ := butwhatabout(userinfo.Username)
724
725 aud := strings.Join(bonk.Audience, " ")
726 whofore := 0
727 if strings.Contains(aud, user.URL) {
728 whofore = 1
729 }
730 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
731 dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
732 if err != nil {
733 log.Printf("error saving bonk: %s", err)
734 return
735 }
736 bonk.ID, _ = res.LastInsertId()
737 for _, d := range bonk.Donks {
738 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
739 if err != nil {
740 log.Printf("err saving donk: %s", err)
741 return
742 }
743 }
744
745 go honkworldwide(user, &bonk)
746
747}
748
749func zonkit(w http.ResponseWriter, r *http.Request) {
750 xid := r.FormValue("xid")
751
752 log.Printf("zonking %s", xid)
753 userinfo := GetUserInfo(r)
754 stmtZonkIt.Exec(userinfo.UserID, xid)
755}
756
757func savehonk(w http.ResponseWriter, r *http.Request) {
758 rid := r.FormValue("rid")
759 noise := r.FormValue("noise")
760
761 userinfo := GetUserInfo(r)
762
763 dt := time.Now().UTC()
764 xid := xfiltrate()
765 what := "honk"
766 if rid != "" {
767 what = "tonk"
768 }
769 honk := Honk{
770 UserID: userinfo.UserID,
771 Username: userinfo.Username,
772 What: "honk",
773 XID: xid,
774 RID: rid,
775 Date: dt,
776 }
777 if noise[0] == '@' {
778 honk.Audience = append(grapevine(noise), thewholeworld)
779 } else {
780 honk.Audience = prepend(thewholeworld, grapevine(noise))
781 }
782 var convoy string
783 if rid != "" {
784 xonk := getxonk("", rid)
785 if xonk != nil {
786 honk.Audience = append(honk.Audience, xonk.Audience...)
787 convoy = xonk.Convoy
788 } else {
789 xonkaud, c := whosthere(rid)
790 honk.Audience = append(honk.Audience, xonkaud...)
791 convoy = c
792 }
793 }
794 if convoy == "" {
795 convoy = "data:,electrichonkytonk-" + xfiltrate()
796 }
797 honk.Audience = oneofakind(honk.Audience)
798 noise = obfusbreak(noise)
799 honk.Noise = noise
800 honk.Convoy = convoy
801
802 file, filehdr, err := r.FormFile("donk")
803 if err == nil {
804 var buf bytes.Buffer
805 io.Copy(&buf, file)
806 file.Close()
807 data := buf.Bytes()
808 xid := xfiltrate()
809 var media, name string
810 img, format, err := image.Decode(&buf)
811 if err == nil {
812 data, format, err = vacuumwrap(img, format)
813 if err != nil {
814 log.Printf("can't vacuum image: %s", err)
815 return
816 }
817 media = "image/" + format
818 if format == "jpeg" {
819 format = "jpg"
820 }
821 name = xid + "." + format
822 xid = name
823 } else {
824 maxsize := 100000
825 if len(data) > maxsize {
826 log.Printf("bad image: %s too much text: %d", err, len(data))
827 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
828 return
829 }
830 for i := 0; i < len(data); i++ {
831 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
832 log.Printf("bad image: %s not text: %d", err, data[i])
833 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
834 return
835 }
836 }
837 media = "text/plain"
838 name = filehdr.Filename
839 if name == "" {
840 name = xid + ".txt"
841 }
842 xid += ".txt"
843 }
844 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
845 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
846 if err != nil {
847 log.Printf("unable to save image: %s", err)
848 return
849 }
850 var d Donk
851 d.FileID, _ = res.LastInsertId()
852 d.XID = name
853 d.Name = name
854 d.Media = media
855 d.URL = url
856 honk.Donks = append(honk.Donks, &d)
857 }
858 herd := herdofemus(honk.Noise)
859 for _, e := range herd {
860 donk := savedonk(e.ID, e.Name, "image/png")
861 if donk != nil {
862 donk.Name = e.Name
863 honk.Donks = append(honk.Donks, donk)
864 }
865 }
866
867 user, _ := butwhatabout(userinfo.Username)
868
869 aud := strings.Join(honk.Audience, " ")
870 whofore := 0
871 if strings.Contains(aud, user.URL) {
872 whofore = 1
873 }
874 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
875 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
876 if err != nil {
877 log.Printf("error saving honk: %s", err)
878 return
879 }
880 honk.ID, _ = res.LastInsertId()
881 for _, d := range honk.Donks {
882 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
883 if err != nil {
884 log.Printf("err saving donk: %s", err)
885 return
886 }
887 }
888
889 go honkworldwide(user, &honk)
890
891 http.Redirect(w, r, "/", http.StatusSeeOther)
892}
893
894func viewhonkers(w http.ResponseWriter, r *http.Request) {
895 userinfo := GetUserInfo(r)
896 templinfo := getInfo(r)
897 templinfo["Honkers"] = gethonkers(userinfo.UserID)
898 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
899 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
900 if err != nil {
901 log.Print(err)
902 }
903}
904
905var handfull = make(map[string]string)
906var handlock sync.Mutex
907
908func gofish(name string) string {
909 if name[0] == '@' {
910 name = name[1:]
911 }
912 m := strings.Split(name, "@")
913 if len(m) != 2 {
914 log.Printf("bad fish name: %s", name)
915 return ""
916 }
917 handlock.Lock()
918 ref, ok := handfull[name]
919 handlock.Unlock()
920 if ok {
921 return ref
922 }
923 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
924 handlock.Lock()
925 defer handlock.Unlock()
926 if err != nil {
927 log.Printf("failed to go fish %s: %s", name, err)
928 handfull[name] = ""
929 return ""
930 }
931 links, _ := jsongetarray(j, "links")
932 for _, l := range links {
933 href, _ := jsongetstring(l, "href")
934 rel, _ := jsongetstring(l, "rel")
935 t, _ := jsongetstring(l, "type")
936 if rel == "self" && friendorfoe(t) {
937 handfull[name] = href
938 return href
939 }
940 }
941 handfull[name] = ""
942 return ""
943}
944
945func savehonker(w http.ResponseWriter, r *http.Request) {
946 u := GetUserInfo(r)
947 name := r.FormValue("name")
948 url := r.FormValue("url")
949 peep := r.FormValue("peep")
950 combos := r.FormValue("combos")
951 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
952
953 if honkerid > 0 {
954 combos = " " + strings.TrimSpace(combos) + " "
955 _, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
956 if err != nil {
957 log.Printf("update honker err: %s", err)
958 return
959 }
960 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
961 }
962
963 flavor := "presub"
964 if peep == "peep" {
965 flavor = "peep"
966 }
967 if url == "" {
968 return
969 }
970 if url[0] == '@' {
971 url = gofish(url)
972 }
973 if url == "" {
974 return
975 }
976 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
977 if err != nil {
978 log.Print(err)
979 return
980 }
981 if flavor == "presub" {
982 user, _ := butwhatabout(u.Username)
983 go subsub(user, url)
984 }
985 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
986}
987
988type Zonker struct {
989 Name string
990 Wherefore string
991}
992
993func killzone(w http.ResponseWriter, r *http.Request) {
994 db := opendatabase()
995 userinfo := GetUserInfo(r)
996 rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
997 if err != nil {
998 log.Printf("err: %s", err)
999 return
1000 }
1001 var zonkers []Zonker
1002 for rows.Next() {
1003 var z Zonker
1004 rows.Scan(&z.Name, &z.Wherefore)
1005 zonkers = append(zonkers, z)
1006 }
1007 templinfo := getInfo(r)
1008 templinfo["Zonkers"] = zonkers
1009 templinfo["KillCSRF"] = GetCSRF("killitwithfire", r)
1010 err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1011 if err != nil {
1012 log.Print(err)
1013 }
1014}
1015
1016func killitwithfire(w http.ResponseWriter, r *http.Request) {
1017 userinfo := GetUserInfo(r)
1018 wherefore := r.FormValue("wherefore")
1019 name := r.FormValue("name")
1020 if name == "" {
1021 return
1022 }
1023 switch wherefore {
1024 case "zonker":
1025 case "zurl":
1026 case "zonvoy":
1027 default:
1028 return
1029 }
1030 db := opendatabase()
1031 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1032 userinfo.UserID, name, wherefore)
1033
1034 http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1035}
1036
1037func somedays() string {
1038 secs := 432000 + notrand.Int63n(432000)
1039 return fmt.Sprintf("%d", secs)
1040}
1041
1042func avatate(w http.ResponseWriter, r *http.Request) {
1043 n := r.FormValue("a")
1044 a := avatar(n)
1045 w.Header().Set("Cache-Control", "max-age="+somedays())
1046 w.Write(a)
1047}
1048
1049func servecss(w http.ResponseWriter, r *http.Request) {
1050 w.Header().Set("Cache-Control", "max-age=7776000")
1051 http.ServeFile(w, r, "views"+r.URL.Path)
1052}
1053func servehtml(w http.ResponseWriter, r *http.Request) {
1054 templinfo := getInfo(r)
1055 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1056 if err != nil {
1057 log.Print(err)
1058 }
1059}
1060func serveemu(w http.ResponseWriter, r *http.Request) {
1061 xid := mux.Vars(r)["xid"]
1062 w.Header().Set("Cache-Control", "max-age="+somedays())
1063 http.ServeFile(w, r, "emus/"+xid)
1064}
1065
1066func servefile(w http.ResponseWriter, r *http.Request) {
1067 xid := mux.Vars(r)["xid"]
1068 row := stmtFileData.QueryRow(xid)
1069 var media string
1070 var data []byte
1071 err := row.Scan(&media, &data)
1072 if err != nil {
1073 log.Printf("error loading file: %s", err)
1074 http.NotFound(w, r)
1075 return
1076 }
1077 w.Header().Set("Content-Type", media)
1078 w.Header().Set("X-Content-Type-Options", "nosniff")
1079 w.Header().Set("Cache-Control", "max-age="+somedays())
1080 w.Write(data)
1081}
1082
1083func serve() {
1084 db := opendatabase()
1085 LoginInit(db)
1086
1087 listener, err := openListener()
1088 if err != nil {
1089 log.Fatal(err)
1090 }
1091 go redeliverator()
1092
1093 debug := false
1094 getconfig("debug", &debug)
1095 readviews = ParseTemplates(debug,
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}