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