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 := xonk.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 honk := Honk{
727 UserID: userinfo.UserID,
728 Username: userinfo.Username,
729 What: "honk",
730 XID: xid,
731 RID: rid,
732 Date: dt,
733 }
734 if noise[0] == '@' {
735 honk.Audience = append(grapevine(noise), thewholeworld)
736 } else {
737 honk.Audience = prepend(thewholeworld, grapevine(noise))
738 }
739 var convoy string
740 if rid != "" {
741 xonk := getxonk("", rid)
742 if xonk != nil {
743 honk.Audience = append(honk.Audience, xonk.Audience...)
744 convoy = xonk.Convoy
745 } else {
746 xonkaud, c := whosthere(rid)
747 honk.Audience = append(honk.Audience, xonkaud...)
748 convoy = c
749 }
750 }
751 if convoy == "" {
752 convoy = "data:,electrichonkytonk-" + xfiltrate()
753 }
754 honk.Audience = oneofakind(honk.Audience)
755 noise = obfusbreak(noise)
756 honk.Noise = noise
757 honk.Convoy = convoy
758
759 file, filehdr, err := r.FormFile("donk")
760 if err == nil {
761 var buf bytes.Buffer
762 io.Copy(&buf, file)
763 file.Close()
764 data := buf.Bytes()
765 xid := xfiltrate()
766 var media, name string
767 img, format, err := image.Decode(&buf)
768 if err == nil {
769 data, format, err = vacuumwrap(img, format)
770 if err != nil {
771 log.Printf("can't vacuum image: %s", err)
772 return
773 }
774 media = "image/" + format
775 if format == "jpeg" {
776 format = "jpg"
777 }
778 name = xid + "." + format
779 xid = name
780 } else {
781 maxsize := 100000
782 if len(data) > maxsize {
783 log.Printf("bad image: %s too much text: %d", err, len(data))
784 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
785 return
786 }
787 for i := 0; i < len(data); i++ {
788 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
789 log.Printf("bad image: %s not text: %d", err, data[i])
790 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
791 return
792 }
793 }
794 media = "text/plain"
795 name = filehdr.Filename
796 if name == "" {
797 name = xid + ".txt"
798 }
799 xid += ".txt"
800 }
801 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
802 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
803 if err != nil {
804 log.Printf("unable to save image: %s", err)
805 return
806 }
807 var d Donk
808 d.FileID, _ = res.LastInsertId()
809 d.XID = name
810 d.Name = name
811 d.Media = media
812 d.URL = url
813 honk.Donks = append(honk.Donks, &d)
814 }
815 herd := herdofemus(honk.Noise)
816 for _, e := range herd {
817 donk := savedonk(e.ID, e.Name, "image/png")
818 if donk != nil {
819 donk.Name = e.Name
820 honk.Donks = append(honk.Donks, donk)
821 }
822 }
823
824 aud := strings.Join(honk.Audience, " ")
825 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
826 dt.Format(dbtimeformat), "", aud, noise, convoy)
827 if err != nil {
828 log.Printf("error saving honk: %s", err)
829 return
830 }
831 honk.ID, _ = res.LastInsertId()
832 for _, d := range honk.Donks {
833 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
834 if err != nil {
835 log.Printf("err saving donk: %s", err)
836 return
837 }
838 }
839
840 user, _ := butwhatabout(userinfo.Username)
841
842 go honkworldwide(user, &honk)
843
844 http.Redirect(w, r, "/", http.StatusSeeOther)
845}
846
847func showhonkers(w http.ResponseWriter, r *http.Request) {
848 userinfo := GetUserInfo(r)
849 templinfo := getInfo(r)
850 templinfo["Honkers"] = gethonkers(userinfo.UserID)
851 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
852 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
853 if err != nil {
854 log.Print(err)
855 }
856}
857
858var handfull = make(map[string]string)
859var handlock sync.Mutex
860
861func gofish(name string) string {
862 if name[0] == '@' {
863 name = name[1:]
864 }
865 m := strings.Split(name, "@")
866 if len(m) != 2 {
867 log.Printf("bad fish name: %s", name)
868 return ""
869 }
870 handlock.Lock()
871 ref, ok := handfull[name]
872 handlock.Unlock()
873 if ok {
874 return ref
875 }
876 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
877 handlock.Lock()
878 defer handlock.Unlock()
879 if err != nil {
880 log.Printf("failed to go fish %s: %s", name, err)
881 handfull[name] = ""
882 return ""
883 }
884 links, _ := jsongetarray(j, "links")
885 for _, l := range links {
886 href, _ := jsongetstring(l, "href")
887 rel, _ := jsongetstring(l, "rel")
888 t, _ := jsongetstring(l, "type")
889 if rel == "self" && friendorfoe(t) {
890 handfull[name] = href
891 return href
892 }
893 }
894 handfull[name] = ""
895 return ""
896}
897
898func savehonker(w http.ResponseWriter, r *http.Request) {
899 name := r.FormValue("name")
900 url := r.FormValue("url")
901 peep := r.FormValue("peep")
902 flavor := "presub"
903 if peep == "peep" {
904 flavor = "peep"
905 }
906
907 if url == "" {
908 return
909 }
910 if url[0] == '@' {
911 url = gofish(url)
912 }
913 if url == "" {
914 return
915 }
916
917 u := GetUserInfo(r)
918 db := opendatabase()
919 _, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
920 u.UserID, name, url, flavor)
921 if err != nil {
922 log.Print(err)
923 }
924 if flavor == "presub" {
925 user, _ := butwhatabout(u.Username)
926 go subsub(user, url)
927 }
928 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
929}
930
931func somedays() string {
932 secs := 432000 + notrand.Int63n(432000)
933 return fmt.Sprintf("%d", secs)
934}
935
936func avatate(w http.ResponseWriter, r *http.Request) {
937 n := r.FormValue("a")
938 a := avatar(n)
939 w.Header().Set("Cache-Control", "max-age="+somedays())
940 w.Write(a)
941}
942
943func servecss(w http.ResponseWriter, r *http.Request) {
944 w.Header().Set("Cache-Control", "max-age=7776000")
945 http.ServeFile(w, r, "views"+r.URL.Path)
946}
947func servehtml(w http.ResponseWriter, r *http.Request) {
948 templinfo := getInfo(r)
949 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
950 if err != nil {
951 log.Print(err)
952 }
953}
954func serveemu(w http.ResponseWriter, r *http.Request) {
955 xid := mux.Vars(r)["xid"]
956 w.Header().Set("Cache-Control", "max-age="+somedays())
957 http.ServeFile(w, r, "emus/"+xid)
958}
959
960func servefile(w http.ResponseWriter, r *http.Request) {
961 xid := mux.Vars(r)["xid"]
962 row := stmtFileData.QueryRow(xid)
963 var media string
964 var data []byte
965 err := row.Scan(&media, &data)
966 if err != nil {
967 log.Printf("error loading file: %s", err)
968 http.NotFound(w, r)
969 return
970 }
971 w.Header().Set("Content-Type", media)
972 w.Header().Set("X-Content-Type-Options", "nosniff")
973 w.Header().Set("Cache-Control", "max-age="+somedays())
974 w.Write(data)
975}
976
977func serve() {
978 db := opendatabase()
979 LoginInit(db)
980
981 listener, err := openListener()
982 if err != nil {
983 log.Fatal(err)
984 }
985 go redeliverator()
986
987 debug := false
988 getconfig("debug", &debug)
989 readviews = ParseTemplates(debug,
990 "views/homepage.html",
991 "views/honkpage.html",
992 "views/honkers.html",
993 "views/honkform.html",
994 "views/honk.html",
995 "views/login.html",
996 "views/header.html",
997 )
998 if !debug {
999 s := "views/style.css"
1000 savedstyleparams[s] = getstyleparam(s)
1001 s = "views/local.css"
1002 savedstyleparams[s] = getstyleparam(s)
1003 }
1004
1005 mux := mux.NewRouter()
1006 mux.Use(LoginChecker)
1007
1008 posters := mux.Methods("POST").Subrouter()
1009 getters := mux.Methods("GET").Subrouter()
1010
1011 getters.HandleFunc("/", homepage)
1012 getters.HandleFunc("/rss", showrss)
1013 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1014 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1015 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1016 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1017 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1018 getters.HandleFunc("/a", avatate)
1019 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1020 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1021 getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1022 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1023
1024 getters.HandleFunc("/style.css", servecss)
1025 getters.HandleFunc("/local.css", servecss)
1026 getters.HandleFunc("/login", servehtml)
1027 posters.HandleFunc("/dologin", dologin)
1028 getters.HandleFunc("/logout", dologout)
1029
1030 loggedin := mux.NewRoute().Subrouter()
1031 loggedin.Use(LoginRequired)
1032 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1033 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1034 loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1035 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1036 loggedin.HandleFunc("/honkers", showhonkers)
1037 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1038
1039 err = http.Serve(listener, mux)
1040 if err != nil {
1041 log.Fatal(err)
1042 }
1043}
1044
1045var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1046var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1047var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1048var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1049var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1050var stmtZonkIt *sql.Stmt
1051
1052func preparetodie(db *sql.DB, s string) *sql.Stmt {
1053 stmt, err := db.Prepare(s)
1054 if err != nil {
1055 log.Fatalf("error %s: %s", err, s)
1056 }
1057 return stmt
1058}
1059
1060func prepareStatements(db *sql.DB) {
1061 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1062 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1063 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 = ?")
1064 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")
1065 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")
1066 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")
1067 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")
1068 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1069 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1070 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1071 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1072 stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1073 stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1074 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1075 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1076 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1077 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1078 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1079 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1080 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1081 stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1082}
1083
1084func ElaborateUnitTests() {
1085}
1086
1087func finishusersetup() error {
1088 db := opendatabase()
1089 k, err := rsa.GenerateKey(rand.Reader, 2048)
1090 if err != nil {
1091 return err
1092 }
1093 pubkey, err := zem(&k.PublicKey)
1094 if err != nil {
1095 return err
1096 }
1097 seckey, err := zem(k)
1098 if err != nil {
1099 return err
1100 }
1101 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1102 if err != nil {
1103 return err
1104 }
1105 return nil
1106}
1107
1108func main() {
1109 cmd := "run"
1110 if len(os.Args) > 1 {
1111 cmd = os.Args[1]
1112 }
1113 switch cmd {
1114 case "init":
1115 initdb()
1116 case "upgrade":
1117 upgradedb()
1118 }
1119 db := opendatabase()
1120 dbversion := 0
1121 getconfig("dbversion", &dbversion)
1122 if dbversion != myVersion {
1123 log.Fatal("incorrect database version. run upgrade.")
1124 }
1125 getconfig("servername", &serverName)
1126 prepareStatements(db)
1127 switch cmd {
1128 case "ping":
1129 if len(os.Args) < 4 {
1130 fmt.Printf("usage: honk ping from to\n")
1131 return
1132 }
1133 name := os.Args[2]
1134 targ := os.Args[3]
1135 user, err := butwhatabout(name)
1136 if err != nil {
1137 log.Printf("unknown user")
1138 return
1139 }
1140 ping(user, targ)
1141 case "peep":
1142 peeppeep()
1143 case "run":
1144 serve()
1145 case "test":
1146 ElaborateUnitTests()
1147 default:
1148 log.Fatal("unknown command")
1149 }
1150}