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