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