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