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(user, j)
369 if 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 Date: dt,
817 }
818 if noise[0] == '@' {
819 honk.Audience = append(grapevine(noise), thewholeworld)
820 } else {
821 honk.Audience = prepend(thewholeworld, grapevine(noise))
822 }
823 var convoy string
824 if rid != "" {
825 xonk := getxonk("", rid)
826 if xonk != nil {
827 if xonk.Honker == "" {
828 rid = "https://" + serverName + "/u/" + xonk.Username + "/h/" + rid
829 }
830 honk.Audience = append(honk.Audience, xonk.Audience...)
831 convoy = xonk.Convoy
832 } else {
833 xonkaud, c := whosthere(rid)
834 honk.Audience = append(honk.Audience, xonkaud...)
835 convoy = c
836 }
837 honk.RID = rid
838 }
839 if convoy == "" {
840 convoy = "data:,electrichonkytonk-" + xfiltrate()
841 }
842 honk.Audience = oneofakind(honk.Audience)
843 noise = obfusbreak(noise)
844 honk.Noise = noise
845 honk.Convoy = convoy
846
847 file, filehdr, err := r.FormFile("donk")
848 if err == nil {
849 var buf bytes.Buffer
850 io.Copy(&buf, file)
851 file.Close()
852 data := buf.Bytes()
853 xid := xfiltrate()
854 var media, name string
855 img, format, err := image.Decode(&buf)
856 if err == nil {
857 data, format, err = vacuumwrap(img, format)
858 if err != nil {
859 log.Printf("can't vacuum image: %s", err)
860 return
861 }
862 media = "image/" + format
863 if format == "jpeg" {
864 format = "jpg"
865 }
866 name = xid + "." + format
867 xid = name
868 } else {
869 maxsize := 100000
870 if len(data) > maxsize {
871 log.Printf("bad image: %s too much text: %d", err, len(data))
872 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
873 return
874 }
875 for i := 0; i < len(data); i++ {
876 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
877 log.Printf("bad image: %s not text: %d", err, data[i])
878 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
879 return
880 }
881 }
882 media = "text/plain"
883 name = filehdr.Filename
884 if name == "" {
885 name = xid + ".txt"
886 }
887 xid += ".txt"
888 }
889 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
890 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
891 if err != nil {
892 log.Printf("unable to save image: %s", err)
893 return
894 }
895 var d Donk
896 d.FileID, _ = res.LastInsertId()
897 d.XID = name
898 d.Name = name
899 d.Media = media
900 d.URL = url
901 honk.Donks = append(honk.Donks, &d)
902 }
903 herd := herdofemus(honk.Noise)
904 for _, e := range herd {
905 donk := savedonk(e.ID, e.Name, "image/png")
906 if donk != nil {
907 donk.Name = e.Name
908 honk.Donks = append(honk.Donks, donk)
909 }
910 }
911
912 user, _ := butwhatabout(userinfo.Username)
913
914 aud := strings.Join(honk.Audience, " ")
915 whofore := 0
916 if strings.Contains(aud, user.URL) {
917 whofore = 1
918 }
919 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
920 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
921 if err != nil {
922 log.Printf("error saving honk: %s", err)
923 return
924 }
925 honk.ID, _ = res.LastInsertId()
926 for _, d := range honk.Donks {
927 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
928 if err != nil {
929 log.Printf("err saving donk: %s", err)
930 return
931 }
932 }
933
934 go honkworldwide(user, &honk)
935
936 http.Redirect(w, r, "/", http.StatusSeeOther)
937}
938
939func viewhonkers(w http.ResponseWriter, r *http.Request) {
940 userinfo := login.GetUserInfo(r)
941 templinfo := getInfo(r)
942 templinfo["Honkers"] = gethonkers(userinfo.UserID)
943 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
944 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
945 if err != nil {
946 log.Print(err)
947 }
948}
949
950var handfull = make(map[string]string)
951var handlock sync.Mutex
952
953func gofish(name string) string {
954 if name[0] == '@' {
955 name = name[1:]
956 }
957 m := strings.Split(name, "@")
958 if len(m) != 2 {
959 log.Printf("bad fish name: %s", name)
960 return ""
961 }
962 handlock.Lock()
963 ref, ok := handfull[name]
964 handlock.Unlock()
965 if ok {
966 return ref
967 }
968 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
969 handlock.Lock()
970 defer handlock.Unlock()
971 if err != nil {
972 log.Printf("failed to go fish %s: %s", name, err)
973 handfull[name] = ""
974 return ""
975 }
976 links, _ := jsongetarray(j, "links")
977 for _, l := range links {
978 href, _ := jsongetstring(l, "href")
979 rel, _ := jsongetstring(l, "rel")
980 t, _ := jsongetstring(l, "type")
981 if rel == "self" && friendorfoe(t) {
982 handfull[name] = href
983 return href
984 }
985 }
986 handfull[name] = ""
987 return ""
988}
989
990func savehonker(w http.ResponseWriter, r *http.Request) {
991 u := login.GetUserInfo(r)
992 name := r.FormValue("name")
993 url := r.FormValue("url")
994 peep := r.FormValue("peep")
995 combos := r.FormValue("combos")
996 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
997
998 if honkerid > 0 {
999 combos = " " + strings.TrimSpace(combos) + " "
1000 _, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
1001 if err != nil {
1002 log.Printf("update honker err: %s", err)
1003 return
1004 }
1005 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1006 }
1007
1008 flavor := "presub"
1009 if peep == "peep" {
1010 flavor = "peep"
1011 }
1012 if url == "" {
1013 return
1014 }
1015 if url[0] == '@' {
1016 url = gofish(url)
1017 }
1018 if url == "" {
1019 return
1020 }
1021 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1022 if err != nil {
1023 log.Print(err)
1024 return
1025 }
1026 if flavor == "presub" {
1027 user, _ := butwhatabout(u.Username)
1028 go subsub(user, url)
1029 }
1030 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1031}
1032
1033type Zonker struct {
1034 Name string
1035 Wherefore string
1036}
1037
1038func killzone(w http.ResponseWriter, r *http.Request) {
1039 db := opendatabase()
1040 userinfo := login.GetUserInfo(r)
1041 rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
1042 if err != nil {
1043 log.Printf("err: %s", err)
1044 return
1045 }
1046 var zonkers []Zonker
1047 for rows.Next() {
1048 var z Zonker
1049 rows.Scan(&z.Name, &z.Wherefore)
1050 zonkers = append(zonkers, z)
1051 }
1052 templinfo := getInfo(r)
1053 templinfo["Zonkers"] = zonkers
1054 templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r)
1055 err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1056 if err != nil {
1057 log.Print(err)
1058 }
1059}
1060
1061func killitwithfire(w http.ResponseWriter, r *http.Request) {
1062 userinfo := login.GetUserInfo(r)
1063 wherefore := r.FormValue("wherefore")
1064 name := r.FormValue("name")
1065 if name == "" {
1066 return
1067 }
1068 switch wherefore {
1069 case "zonker":
1070 case "zurl":
1071 case "zonvoy":
1072 default:
1073 return
1074 }
1075 db := opendatabase()
1076 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1077 userinfo.UserID, name, wherefore)
1078
1079 http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1080}
1081
1082func somedays() string {
1083 secs := 432000 + notrand.Int63n(432000)
1084 return fmt.Sprintf("%d", secs)
1085}
1086
1087func avatate(w http.ResponseWriter, r *http.Request) {
1088 n := r.FormValue("a")
1089 a := avatar(n)
1090 w.Header().Set("Cache-Control", "max-age="+somedays())
1091 w.Write(a)
1092}
1093
1094func servecss(w http.ResponseWriter, r *http.Request) {
1095 w.Header().Set("Cache-Control", "max-age=7776000")
1096 http.ServeFile(w, r, "views"+r.URL.Path)
1097}
1098func servehtml(w http.ResponseWriter, r *http.Request) {
1099 templinfo := getInfo(r)
1100 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1101 if err != nil {
1102 log.Print(err)
1103 }
1104}
1105func serveemu(w http.ResponseWriter, r *http.Request) {
1106 xid := mux.Vars(r)["xid"]
1107 w.Header().Set("Cache-Control", "max-age="+somedays())
1108 http.ServeFile(w, r, "emus/"+xid)
1109}
1110
1111func servefile(w http.ResponseWriter, r *http.Request) {
1112 xid := mux.Vars(r)["xid"]
1113 row := stmtFileData.QueryRow(xid)
1114 var media string
1115 var data []byte
1116 err := row.Scan(&media, &data)
1117 if err != nil {
1118 log.Printf("error loading file: %s", err)
1119 http.NotFound(w, r)
1120 return
1121 }
1122 w.Header().Set("Content-Type", media)
1123 w.Header().Set("X-Content-Type-Options", "nosniff")
1124 w.Header().Set("Cache-Control", "max-age="+somedays())
1125 w.Write(data)
1126}
1127
1128func serve() {
1129 db := opendatabase()
1130 login.Init(db)
1131
1132 listener, err := openListener()
1133 if err != nil {
1134 log.Fatal(err)
1135 }
1136 go redeliverator()
1137
1138 debug := false
1139 getconfig("debug", &debug)
1140 readviews = ParseTemplates(debug,
1141 "views/honkpage.html",
1142 "views/honkers.html",
1143 "views/zonkers.html",
1144 "views/honkform.html",
1145 "views/honk.html",
1146 "views/login.html",
1147 "views/header.html",
1148 )
1149 if !debug {
1150 s := "views/style.css"
1151 savedstyleparams[s] = getstyleparam(s)
1152 s = "views/local.css"
1153 savedstyleparams[s] = getstyleparam(s)
1154 }
1155
1156 mux := mux.NewRouter()
1157 mux.Use(login.Checker)
1158
1159 posters := mux.Methods("POST").Subrouter()
1160 getters := mux.Methods("GET").Subrouter()
1161
1162 getters.HandleFunc("/", homepage)
1163 getters.HandleFunc("/rss", showrss)
1164 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1165 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1166 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1167 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1168 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1169 getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1170 getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1171 getters.HandleFunc("/a", avatate)
1172 getters.HandleFunc("/t", viewconvoy)
1173 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1174 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1175 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1176
1177 getters.HandleFunc("/style.css", servecss)
1178 getters.HandleFunc("/local.css", servecss)
1179 getters.HandleFunc("/login", servehtml)
1180 posters.HandleFunc("/dologin", login.LoginFunc)
1181 getters.HandleFunc("/logout", login.LogoutFunc)
1182
1183 loggedin := mux.NewRoute().Subrouter()
1184 loggedin.Use(login.Required)
1185 loggedin.HandleFunc("/atme", homepage)
1186 loggedin.HandleFunc("/killzone", killzone)
1187 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1188 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1189 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1190 loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1191 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1192 loggedin.HandleFunc("/honkers", viewhonkers)
1193 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1194 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1195 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1196
1197 err = http.Serve(listener, mux)
1198 if err != nil {
1199 log.Fatal(err)
1200 }
1201}
1202
1203var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1204var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1205var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1206var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1207var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1208var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1209var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1210
1211func preparetodie(db *sql.DB, s string) *sql.Stmt {
1212 stmt, err := db.Prepare(s)
1213 if err != nil {
1214 log.Fatalf("error %s: %s", err, s)
1215 }
1216 return stmt
1217}
1218
1219func prepareStatements(db *sql.DB) {
1220 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1221 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1222 stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1223 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1224 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1225
1226 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 "
1227 limit := " order by honkid desc limit "
1228 stmtOneXonk = preparetodie(db, selecthonks+"where xid = ?")
1229 stmtPublicHonks = preparetodie(db, selecthonks+"where honker = ''"+limit+"50")
1230 stmtUserHonks = preparetodie(db, selecthonks+"where honker = '' and username = ?"+limit+"50")
1231 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")
1232 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")
1233 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+limit+"50")
1234 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+limit+"50")
1235 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or honker = '') and convoy = ?"+limit+"50")
1236
1237 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1238 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1239 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1240 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1241 stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ? and userid = ?")
1242 stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1243 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1244 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1245 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1246 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1247 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1248 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1249 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1250 stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1251 stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1252}
1253
1254func ElaborateUnitTests() {
1255}
1256
1257func finishusersetup() error {
1258 db := opendatabase()
1259 k, err := rsa.GenerateKey(rand.Reader, 2048)
1260 if err != nil {
1261 return err
1262 }
1263 pubkey, err := zem(&k.PublicKey)
1264 if err != nil {
1265 return err
1266 }
1267 seckey, err := zem(k)
1268 if err != nil {
1269 return err
1270 }
1271 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1272 if err != nil {
1273 return err
1274 }
1275 return nil
1276}
1277
1278func main() {
1279 cmd := "run"
1280 if len(os.Args) > 1 {
1281 cmd = os.Args[1]
1282 }
1283 switch cmd {
1284 case "init":
1285 initdb()
1286 case "upgrade":
1287 upgradedb()
1288 }
1289 db := opendatabase()
1290 dbversion := 0
1291 getconfig("dbversion", &dbversion)
1292 if dbversion != myVersion {
1293 log.Fatal("incorrect database version. run upgrade.")
1294 }
1295 getconfig("servername", &serverName)
1296 prepareStatements(db)
1297 switch cmd {
1298 case "ping":
1299 if len(os.Args) < 4 {
1300 fmt.Printf("usage: honk ping from to\n")
1301 return
1302 }
1303 name := os.Args[2]
1304 targ := os.Args[3]
1305 user, err := butwhatabout(name)
1306 if err != nil {
1307 log.Printf("unknown user")
1308 return
1309 }
1310 ping(user, targ)
1311 case "peep":
1312 peeppeep()
1313 case "run":
1314 serve()
1315 case "test":
1316 ElaborateUnitTests()
1317 default:
1318 log.Fatal("unknown command")
1319 }
1320}