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 = getpublichonks()
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 = getpublichonks()
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}
434func viewconvoy(w http.ResponseWriter, r *http.Request) {
435 c := r.FormValue("c")
436 var userid int64 = -1
437 u := GetUserInfo(r)
438 if u != nil {
439 userid = u.UserID
440 }
441 honks := gethonksbyconvoy(userid, c)
442 honkpage(w, r, u, nil, honks)
443}
444
445func fingerlicker(w http.ResponseWriter, r *http.Request) {
446 orig := r.FormValue("resource")
447
448 log.Printf("finger lick: %s", orig)
449
450 if strings.HasPrefix(orig, "acct:") {
451 orig = orig[5:]
452 }
453
454 name := orig
455 idx := strings.LastIndexByte(name, '/')
456 if idx != -1 {
457 name = name[idx+1:]
458 if "https://"+serverName+"/u/"+name != orig {
459 log.Printf("foreign request rejected")
460 name = ""
461 }
462 } else {
463 idx = strings.IndexByte(name, '@')
464 if idx != -1 {
465 name = name[:idx]
466 if name+"@"+serverName != orig {
467 log.Printf("foreign request rejected")
468 name = ""
469 }
470 }
471 }
472 user, err := butwhatabout(name)
473 if err != nil {
474 http.NotFound(w, r)
475 return
476 }
477
478 j := NewJunk()
479 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
480 j["aliases"] = []string{user.URL}
481 var links []map[string]interface{}
482 l := NewJunk()
483 l["rel"] = "self"
484 l["type"] = `application/activity+json`
485 l["href"] = user.URL
486 links = append(links, l)
487 j["links"] = links
488
489 w.Header().Set("Cache-Control", "max-age=3600")
490 w.Header().Set("Content-Type", "application/jrd+json")
491 WriteJunk(w, j)
492}
493
494func viewhonk(w http.ResponseWriter, r *http.Request) {
495 name := mux.Vars(r)["name"]
496 xid := mux.Vars(r)["xid"]
497 user, err := butwhatabout(name)
498 if err != nil {
499 http.NotFound(w, r)
500 return
501 }
502 h := getxonk(name, xid)
503 if h == nil {
504 http.NotFound(w, r)
505 return
506 }
507 if friendorfoe(r.Header.Get("Accept")) {
508 _, j := jonkjonk(user, h)
509 j["@context"] = itiswhatitis
510 w.Header().Set("Cache-Control", "max-age=3600")
511 w.Header().Set("Content-Type", theonetruename)
512 WriteJunk(w, j)
513 return
514 }
515 u := GetUserInfo(r)
516 honkpage(w, r, u, nil, []*Honk{h})
517}
518
519func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk) {
520 reverbolate(honks)
521 templinfo := getInfo(r)
522 if u != nil {
523 if user != nil && u.Username == user.Name {
524 templinfo["UserCSRF"] = GetCSRF("saveuser", r)
525 }
526 templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
527 }
528 if u == nil {
529 w.Header().Set("Cache-Control", "max-age=60")
530 }
531 if user != nil {
532 templinfo["Name"] = user.Name
533 whatabout := user.About
534 templinfo["RawWhatAbout"] = whatabout
535 whatabout = obfusbreak(whatabout)
536 templinfo["WhatAbout"] = cleanstring(whatabout)
537 }
538 templinfo["Honks"] = honks
539 templinfo["ServerMessage"] = "(some info goes here)"
540 err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
541 if err != nil {
542 log.Print(err)
543 }
544}
545
546func saveuser(w http.ResponseWriter, r *http.Request) {
547 whatabout := r.FormValue("whatabout")
548 u := GetUserInfo(r)
549 db := opendatabase()
550 _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
551 if err != nil {
552 log.Printf("error bouting what: %s", err)
553 }
554
555 http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
556}
557
558func gethonkers(userid int64) []*Honker {
559 rows, err := stmtHonkers.Query(userid)
560 if err != nil {
561 log.Printf("error querying honkers: %s", err)
562 return nil
563 }
564 defer rows.Close()
565 var honkers []*Honker
566 for rows.Next() {
567 var f Honker
568 var combos string
569 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
570 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
571 if err != nil {
572 log.Printf("error scanning honker: %s", err)
573 return nil
574 }
575 honkers = append(honkers, &f)
576 }
577 return honkers
578}
579
580func getdubs(userid int64) []*Honker {
581 rows, err := stmtDubbers.Query(userid)
582 if err != nil {
583 log.Printf("error querying dubs: %s", err)
584 return nil
585 }
586 defer rows.Close()
587 var honkers []*Honker
588 for rows.Next() {
589 var f Honker
590 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
591 if err != nil {
592 log.Printf("error scanning honker: %s", err)
593 return nil
594 }
595 honkers = append(honkers, &f)
596 }
597 return honkers
598}
599
600func getxonk(name, xid string) *Honk {
601 var h Honk
602 var dt, aud string
603 row := stmtOneXonk.QueryRow(xid)
604 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
605 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
606 if err != nil {
607 if err != sql.ErrNoRows {
608 log.Printf("error scanning xonk: %s", err)
609 }
610 return nil
611 }
612 if name != "" && h.Username != name {
613 log.Printf("user xonk mismatch")
614 return nil
615 }
616 h.Date, _ = time.Parse(dbtimeformat, dt)
617 h.Audience = strings.Split(aud, " ")
618 donksforhonks([]*Honk{&h})
619 return &h
620}
621
622func getpublichonks() []*Honk {
623 rows, err := stmtPublicHonks.Query()
624 return getsomehonks(rows, err)
625}
626func gethonksbyuser(name string) []*Honk {
627 rows, err := stmtUserHonks.Query(name)
628 return getsomehonks(rows, err)
629}
630func gethonksforuser(userid int64) []*Honk {
631 dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
632 rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
633 return getsomehonks(rows, err)
634}
635func gethonksforme(userid int64) []*Honk {
636 dt := time.Now().UTC().Add(-4 * 24 * time.Hour)
637 rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
638 return getsomehonks(rows, err)
639}
640func gethonksbyhonker(userid int64, honker string) []*Honk {
641 rows, err := stmtHonksByHonker.Query(userid, honker)
642 return getsomehonks(rows, err)
643}
644func gethonksbycombo(userid int64, combo string) []*Honk {
645 combo = "% " + combo + " %"
646 rows, err := stmtHonksByCombo.Query(userid, combo)
647 return getsomehonks(rows, err)
648}
649func gethonksbyconvoy(userid int64, convoy string) []*Honk {
650 rows, err := stmtHonksByConvoy.Query(userid, convoy)
651 return getsomehonks(rows, err)
652}
653
654func getsomehonks(rows *sql.Rows, err error) []*Honk {
655 if err != nil {
656 log.Printf("error querying honks: %s", err)
657 return nil
658 }
659 defer rows.Close()
660 var honks []*Honk
661 for rows.Next() {
662 var h Honk
663 var dt, aud string
664 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
665 &dt, &h.URL, &aud, &h.Noise, &h.Convoy)
666 if err != nil {
667 log.Printf("error scanning honks: %s", err)
668 return nil
669 }
670 h.Date, _ = time.Parse(dbtimeformat, dt)
671 h.Audience = strings.Split(aud, " ")
672 honks = append(honks, &h)
673 }
674 rows.Close()
675 donksforhonks(honks)
676 return honks
677}
678
679func donksforhonks(honks []*Honk) {
680 db := opendatabase()
681 var ids []string
682 hmap := make(map[int64]*Honk)
683 for _, h := range honks {
684 if h.What == "zonk" {
685 continue
686 }
687 ids = append(ids, fmt.Sprintf("%d", h.ID))
688 hmap[h.ID] = h
689 }
690 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, ","))
691 rows, err := db.Query(q)
692 if err != nil {
693 log.Printf("error querying donks: %s", err)
694 return
695 }
696 defer rows.Close()
697 for rows.Next() {
698 var hid int64
699 var d Donk
700 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
701 if err != nil {
702 log.Printf("error scanning donk: %s", err)
703 continue
704 }
705 h := hmap[hid]
706 h.Donks = append(h.Donks, &d)
707 }
708}
709
710func savebonk(w http.ResponseWriter, r *http.Request) {
711 xid := r.FormValue("xid")
712
713 log.Printf("bonking %s", xid)
714
715 xonk := getxonk("", xid)
716 if xonk == nil {
717 return
718 }
719 if xonk.Honker == "" {
720 xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
721 }
722 convoy := xonk.Convoy
723
724 userinfo := GetUserInfo(r)
725
726 dt := time.Now().UTC()
727 bonk := Honk{
728 UserID: userinfo.UserID,
729 Username: userinfo.Username,
730 Honker: xonk.Honker,
731 What: "bonk",
732 XID: xonk.XID,
733 Date: dt,
734 Noise: xonk.Noise,
735 Convoy: convoy,
736 Donks: xonk.Donks,
737 Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
738 }
739
740 user, _ := butwhatabout(userinfo.Username)
741
742 aud := strings.Join(bonk.Audience, " ")
743 whofore := 0
744 if strings.Contains(aud, user.URL) {
745 whofore = 1
746 }
747 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
748 dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
749 if err != nil {
750 log.Printf("error saving bonk: %s", err)
751 return
752 }
753 bonk.ID, _ = res.LastInsertId()
754 for _, d := range bonk.Donks {
755 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
756 if err != nil {
757 log.Printf("err saving donk: %s", err)
758 return
759 }
760 }
761
762 go honkworldwide(user, &bonk)
763
764}
765
766func zonkit(w http.ResponseWriter, r *http.Request) {
767 xid := r.FormValue("xid")
768
769 log.Printf("zonking %s", xid)
770 userinfo := GetUserInfo(r)
771 stmtZonkIt.Exec(userinfo.UserID, xid)
772}
773
774func savehonk(w http.ResponseWriter, r *http.Request) {
775 rid := r.FormValue("rid")
776 noise := r.FormValue("noise")
777
778 userinfo := GetUserInfo(r)
779
780 dt := time.Now().UTC()
781 xid := xfiltrate()
782 what := "honk"
783 if rid != "" {
784 what = "tonk"
785 }
786 honk := Honk{
787 UserID: userinfo.UserID,
788 Username: userinfo.Username,
789 What: "honk",
790 XID: xid,
791 RID: rid,
792 Date: dt,
793 }
794 if noise[0] == '@' {
795 honk.Audience = append(grapevine(noise), thewholeworld)
796 } else {
797 honk.Audience = prepend(thewholeworld, grapevine(noise))
798 }
799 var convoy string
800 if rid != "" {
801 xonk := getxonk("", rid)
802 if xonk != nil {
803 honk.Audience = append(honk.Audience, xonk.Audience...)
804 convoy = xonk.Convoy
805 } else {
806 xonkaud, c := whosthere(rid)
807 honk.Audience = append(honk.Audience, xonkaud...)
808 convoy = c
809 }
810 }
811 if convoy == "" {
812 convoy = "data:,electrichonkytonk-" + xfiltrate()
813 }
814 honk.Audience = oneofakind(honk.Audience)
815 noise = obfusbreak(noise)
816 honk.Noise = noise
817 honk.Convoy = convoy
818
819 file, filehdr, err := r.FormFile("donk")
820 if err == nil {
821 var buf bytes.Buffer
822 io.Copy(&buf, file)
823 file.Close()
824 data := buf.Bytes()
825 xid := xfiltrate()
826 var media, name string
827 img, format, err := image.Decode(&buf)
828 if err == nil {
829 data, format, err = vacuumwrap(img, format)
830 if err != nil {
831 log.Printf("can't vacuum image: %s", err)
832 return
833 }
834 media = "image/" + format
835 if format == "jpeg" {
836 format = "jpg"
837 }
838 name = xid + "." + format
839 xid = name
840 } else {
841 maxsize := 100000
842 if len(data) > maxsize {
843 log.Printf("bad image: %s too much text: %d", err, len(data))
844 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
845 return
846 }
847 for i := 0; i < len(data); i++ {
848 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
849 log.Printf("bad image: %s not text: %d", err, data[i])
850 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
851 return
852 }
853 }
854 media = "text/plain"
855 name = filehdr.Filename
856 if name == "" {
857 name = xid + ".txt"
858 }
859 xid += ".txt"
860 }
861 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
862 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
863 if err != nil {
864 log.Printf("unable to save image: %s", err)
865 return
866 }
867 var d Donk
868 d.FileID, _ = res.LastInsertId()
869 d.XID = name
870 d.Name = name
871 d.Media = media
872 d.URL = url
873 honk.Donks = append(honk.Donks, &d)
874 }
875 herd := herdofemus(honk.Noise)
876 for _, e := range herd {
877 donk := savedonk(e.ID, e.Name, "image/png")
878 if donk != nil {
879 donk.Name = e.Name
880 honk.Donks = append(honk.Donks, donk)
881 }
882 }
883
884 user, _ := butwhatabout(userinfo.Username)
885
886 aud := strings.Join(honk.Audience, " ")
887 whofore := 0
888 if strings.Contains(aud, user.URL) {
889 whofore = 1
890 }
891 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
892 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
893 if err != nil {
894 log.Printf("error saving honk: %s", err)
895 return
896 }
897 honk.ID, _ = res.LastInsertId()
898 for _, d := range honk.Donks {
899 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
900 if err != nil {
901 log.Printf("err saving donk: %s", err)
902 return
903 }
904 }
905
906 go honkworldwide(user, &honk)
907
908 http.Redirect(w, r, "/", http.StatusSeeOther)
909}
910
911func viewhonkers(w http.ResponseWriter, r *http.Request) {
912 userinfo := GetUserInfo(r)
913 templinfo := getInfo(r)
914 templinfo["Honkers"] = gethonkers(userinfo.UserID)
915 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
916 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
917 if err != nil {
918 log.Print(err)
919 }
920}
921
922var handfull = make(map[string]string)
923var handlock sync.Mutex
924
925func gofish(name string) string {
926 if name[0] == '@' {
927 name = name[1:]
928 }
929 m := strings.Split(name, "@")
930 if len(m) != 2 {
931 log.Printf("bad fish name: %s", name)
932 return ""
933 }
934 handlock.Lock()
935 ref, ok := handfull[name]
936 handlock.Unlock()
937 if ok {
938 return ref
939 }
940 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
941 handlock.Lock()
942 defer handlock.Unlock()
943 if err != nil {
944 log.Printf("failed to go fish %s: %s", name, err)
945 handfull[name] = ""
946 return ""
947 }
948 links, _ := jsongetarray(j, "links")
949 for _, l := range links {
950 href, _ := jsongetstring(l, "href")
951 rel, _ := jsongetstring(l, "rel")
952 t, _ := jsongetstring(l, "type")
953 if rel == "self" && friendorfoe(t) {
954 handfull[name] = href
955 return href
956 }
957 }
958 handfull[name] = ""
959 return ""
960}
961
962func savehonker(w http.ResponseWriter, r *http.Request) {
963 u := GetUserInfo(r)
964 name := r.FormValue("name")
965 url := r.FormValue("url")
966 peep := r.FormValue("peep")
967 combos := r.FormValue("combos")
968 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
969
970 if honkerid > 0 {
971 combos = " " + strings.TrimSpace(combos) + " "
972 _, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
973 if err != nil {
974 log.Printf("update honker err: %s", err)
975 return
976 }
977 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
978 }
979
980 flavor := "presub"
981 if peep == "peep" {
982 flavor = "peep"
983 }
984 if url == "" {
985 return
986 }
987 if url[0] == '@' {
988 url = gofish(url)
989 }
990 if url == "" {
991 return
992 }
993 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
994 if err != nil {
995 log.Print(err)
996 return
997 }
998 if flavor == "presub" {
999 user, _ := butwhatabout(u.Username)
1000 go subsub(user, url)
1001 }
1002 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1003}
1004
1005type Zonker struct {
1006 Name string
1007 Wherefore string
1008}
1009
1010func killzone(w http.ResponseWriter, r *http.Request) {
1011 db := opendatabase()
1012 userinfo := GetUserInfo(r)
1013 rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
1014 if err != nil {
1015 log.Printf("err: %s", err)
1016 return
1017 }
1018 var zonkers []Zonker
1019 for rows.Next() {
1020 var z Zonker
1021 rows.Scan(&z.Name, &z.Wherefore)
1022 zonkers = append(zonkers, z)
1023 }
1024 templinfo := getInfo(r)
1025 templinfo["Zonkers"] = zonkers
1026 templinfo["KillCSRF"] = GetCSRF("killitwithfire", r)
1027 err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1028 if err != nil {
1029 log.Print(err)
1030 }
1031}
1032
1033func killitwithfire(w http.ResponseWriter, r *http.Request) {
1034 userinfo := GetUserInfo(r)
1035 wherefore := r.FormValue("wherefore")
1036 name := r.FormValue("name")
1037 if name == "" {
1038 return
1039 }
1040 switch wherefore {
1041 case "zonker":
1042 case "zurl":
1043 case "zonvoy":
1044 default:
1045 return
1046 }
1047 db := opendatabase()
1048 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1049 userinfo.UserID, name, wherefore)
1050
1051 http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1052}
1053
1054func somedays() string {
1055 secs := 432000 + notrand.Int63n(432000)
1056 return fmt.Sprintf("%d", secs)
1057}
1058
1059func avatate(w http.ResponseWriter, r *http.Request) {
1060 n := r.FormValue("a")
1061 a := avatar(n)
1062 w.Header().Set("Cache-Control", "max-age="+somedays())
1063 w.Write(a)
1064}
1065
1066func servecss(w http.ResponseWriter, r *http.Request) {
1067 w.Header().Set("Cache-Control", "max-age=7776000")
1068 http.ServeFile(w, r, "views"+r.URL.Path)
1069}
1070func servehtml(w http.ResponseWriter, r *http.Request) {
1071 templinfo := getInfo(r)
1072 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1073 if err != nil {
1074 log.Print(err)
1075 }
1076}
1077func serveemu(w http.ResponseWriter, r *http.Request) {
1078 xid := mux.Vars(r)["xid"]
1079 w.Header().Set("Cache-Control", "max-age="+somedays())
1080 http.ServeFile(w, r, "emus/"+xid)
1081}
1082
1083func servefile(w http.ResponseWriter, r *http.Request) {
1084 xid := mux.Vars(r)["xid"]
1085 row := stmtFileData.QueryRow(xid)
1086 var media string
1087 var data []byte
1088 err := row.Scan(&media, &data)
1089 if err != nil {
1090 log.Printf("error loading file: %s", err)
1091 http.NotFound(w, r)
1092 return
1093 }
1094 w.Header().Set("Content-Type", media)
1095 w.Header().Set("X-Content-Type-Options", "nosniff")
1096 w.Header().Set("Cache-Control", "max-age="+somedays())
1097 w.Write(data)
1098}
1099
1100func serve() {
1101 db := opendatabase()
1102 LoginInit(db)
1103
1104 listener, err := openListener()
1105 if err != nil {
1106 log.Fatal(err)
1107 }
1108 go redeliverator()
1109
1110 debug := false
1111 getconfig("debug", &debug)
1112 readviews = ParseTemplates(debug,
1113 "views/honkpage.html",
1114 "views/honkers.html",
1115 "views/zonkers.html",
1116 "views/honkform.html",
1117 "views/honk.html",
1118 "views/login.html",
1119 "views/header.html",
1120 )
1121 if !debug {
1122 s := "views/style.css"
1123 savedstyleparams[s] = getstyleparam(s)
1124 s = "views/local.css"
1125 savedstyleparams[s] = getstyleparam(s)
1126 }
1127
1128 mux := mux.NewRouter()
1129 mux.Use(LoginChecker)
1130
1131 posters := mux.Methods("POST").Subrouter()
1132 getters := mux.Methods("GET").Subrouter()
1133
1134 getters.HandleFunc("/", homepage)
1135 getters.HandleFunc("/rss", showrss)
1136 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1137 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1138 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1139 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1140 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1141 getters.HandleFunc("/a", avatate)
1142 getters.HandleFunc("/t", viewconvoy)
1143 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1144 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1145 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1146
1147 getters.HandleFunc("/style.css", servecss)
1148 getters.HandleFunc("/local.css", servecss)
1149 getters.HandleFunc("/login", servehtml)
1150 posters.HandleFunc("/dologin", dologin)
1151 getters.HandleFunc("/logout", dologout)
1152
1153 loggedin := mux.NewRoute().Subrouter()
1154 loggedin.Use(LoginRequired)
1155 loggedin.HandleFunc("/atme", homepage)
1156 loggedin.HandleFunc("/killzone", killzone)
1157 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1158 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1159 loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1160 loggedin.Handle("/killitwithfire", CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1161 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1162 loggedin.HandleFunc("/honkers", viewhonkers)
1163 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1164 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1165 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1166
1167 err = http.Serve(listener, mux)
1168 if err != nil {
1169 log.Fatal(err)
1170 }
1171}
1172
1173var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1174var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1175var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1176var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1177var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1178var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1179var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1180
1181func preparetodie(db *sql.DB, s string) *sql.Stmt {
1182 stmt, err := db.Prepare(s)
1183 if err != nil {
1184 log.Fatalf("error %s: %s", err, s)
1185 }
1186 return stmt
1187}
1188
1189func prepareStatements(db *sql.DB) {
1190 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1191 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1192 stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1193 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1194 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1195
1196 selecthonks := "select honkid, honks.userid, username, what, honker, honks.xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid "
1197 limit := " order by honkid desc limit "
1198 stmtOneXonk = preparetodie(db, selecthonks+"where xid = ?")
1199 stmtPublicHonks = preparetodie(db, selecthonks+"where honker = ''"+limit+"50")
1200 stmtUserHonks = preparetodie(db, selecthonks+"where honker = '' and username = ?"+limit+"50")
1201 stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"+limit+"250")
1202 stmtHonksForMe = preparetodie(db, selecthonks+"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)"+limit+"150")
1203 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+limit+"50")
1204 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+limit+"50")
1205 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or honker = '') and convoy = ?"+limit+"50")
1206
1207 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1208 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1209 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1210 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1211 stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1212 stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1213 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1214 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1215 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1216 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1217 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1218 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1219 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1220 stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1221 stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1222}
1223
1224func ElaborateUnitTests() {
1225}
1226
1227func finishusersetup() error {
1228 db := opendatabase()
1229 k, err := rsa.GenerateKey(rand.Reader, 2048)
1230 if err != nil {
1231 return err
1232 }
1233 pubkey, err := zem(&k.PublicKey)
1234 if err != nil {
1235 return err
1236 }
1237 seckey, err := zem(k)
1238 if err != nil {
1239 return err
1240 }
1241 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1242 if err != nil {
1243 return err
1244 }
1245 return nil
1246}
1247
1248func main() {
1249 cmd := "run"
1250 if len(os.Args) > 1 {
1251 cmd = os.Args[1]
1252 }
1253 switch cmd {
1254 case "init":
1255 initdb()
1256 case "upgrade":
1257 upgradedb()
1258 }
1259 db := opendatabase()
1260 dbversion := 0
1261 getconfig("dbversion", &dbversion)
1262 if dbversion != myVersion {
1263 log.Fatal("incorrect database version. run upgrade.")
1264 }
1265 getconfig("servername", &serverName)
1266 prepareStatements(db)
1267 switch cmd {
1268 case "ping":
1269 if len(os.Args) < 4 {
1270 fmt.Printf("usage: honk ping from to\n")
1271 return
1272 }
1273 name := os.Args[2]
1274 targ := os.Args[3]
1275 user, err := butwhatabout(name)
1276 if err != nil {
1277 log.Printf("unknown user")
1278 return
1279 }
1280 ping(user, targ)
1281 case "peep":
1282 peeppeep()
1283 case "run":
1284 serve()
1285 case "test":
1286 ElaborateUnitTests()
1287 default:
1288 log.Fatal("unknown command")
1289 }
1290}