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