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