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