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