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