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