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