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