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