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