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