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