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