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 "database/sql"
21 "fmt"
22 "html"
23 "html/template"
24 "io"
25 "log"
26 notrand "math/rand"
27 "net/http"
28 "net/url"
29 "os"
30 "sort"
31 "strconv"
32 "strings"
33 "time"
34
35 "github.com/gorilla/mux"
36 "humungus.tedunangst.com/r/webs/htfilter"
37 "humungus.tedunangst.com/r/webs/httpsig"
38 "humungus.tedunangst.com/r/webs/image"
39 "humungus.tedunangst.com/r/webs/junk"
40 "humungus.tedunangst.com/r/webs/login"
41 "humungus.tedunangst.com/r/webs/rss"
42 "humungus.tedunangst.com/r/webs/templates"
43)
44
45type WhatAbout struct {
46 ID int64
47 Name string
48 Display string
49 About string
50 Key string
51 URL string
52 SkinnyCSS bool
53}
54
55type Honk struct {
56 ID int64
57 UserID int64
58 Username string
59 What string
60 Honker string
61 Handle string
62 Oonker string
63 Oondle string
64 XID string
65 RID string
66 Date time.Time
67 URL string
68 Noise string
69 Precis string
70 Convoy string
71 Audience []string
72 Public bool
73 Whofore int64
74 Replies []*Honk
75 Flags int64
76 HTML template.HTML
77 Style string
78 Open string
79 Donks []*Donk
80}
81
82const (
83 flagIsAcked = 1
84 flagIsBonked = 2
85)
86
87func (honk *Honk) IsAcked() bool {
88 return honk.Flags&flagIsAcked != 0
89}
90
91func (honk *Honk) IsBonked() bool {
92 return honk.Flags&flagIsBonked != 0
93}
94
95type Donk struct {
96 FileID int64
97 XID string
98 Name string
99 URL string
100 Media string
101 Local bool
102 Content []byte
103}
104
105type Honker struct {
106 ID int64
107 UserID int64
108 Name string
109 XID string
110 Handle string
111 Flavor string
112 Combos []string
113}
114
115var serverName string
116var iconName = "icon.png"
117var serverMsg = "Things happen."
118
119var userSep = "u"
120var honkSep = "h"
121
122var readviews *templates.Template
123
124func getuserstyle(u *login.UserInfo) template.CSS {
125 if u == nil {
126 return ""
127 }
128 user, _ := butwhatabout(u.Username)
129 if user.SkinnyCSS {
130 return "main { max-width: 700px; }"
131 }
132 return ""
133}
134
135func getInfo(r *http.Request) map[string]interface{} {
136 u := login.GetUserInfo(r)
137 templinfo := make(map[string]interface{})
138 templinfo["StyleParam"] = getstyleparam("views/style.css")
139 templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
140 templinfo["UserStyle"] = getuserstyle(u)
141 templinfo["ServerName"] = serverName
142 templinfo["IconName"] = iconName
143 templinfo["UserInfo"] = u
144 templinfo["UserSep"] = userSep
145 return templinfo
146}
147
148var donotfedafterdark = make(map[string]bool)
149
150func stealthed(r *http.Request) bool {
151 addr := r.Header.Get("X-Forwarded-For")
152 fake := donotfedafterdark[addr]
153 if fake {
154 log.Printf("faking 404 for %s", addr)
155 }
156 return fake
157}
158
159func homepage(w http.ResponseWriter, r *http.Request) {
160 templinfo := getInfo(r)
161 u := login.GetUserInfo(r)
162 var honks []*Honk
163 var userid int64 = -1
164 if r.URL.Path == "/front" || u == nil {
165 honks = getpublichonks()
166 } else {
167 userid = u.UserID
168 if r.URL.Path == "/atme" {
169 honks = gethonksforme(userid)
170 } else {
171 honks = gethonksforuser(userid)
172 honks = osmosis(honks, userid)
173 if len(honks) > 0 {
174 templinfo["TopXID"] = honks[0].XID
175 }
176 }
177 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
178 }
179
180 tname := "honkpage.html"
181 if topxid := r.FormValue("topxid"); topxid != "" {
182 for i, h := range honks {
183 if h.XID == topxid {
184 honks = honks[0:i]
185 break
186 }
187 }
188 log.Printf("topxid %d frags", len(honks))
189 tname = "honkfrags.html"
190 }
191
192 reverbolate(userid, honks)
193
194 templinfo["Honks"] = honks
195 templinfo["ShowRSS"] = true
196 templinfo["ServerMessage"] = serverMsg
197 if u == nil {
198 w.Header().Set("Cache-Control", "max-age=60")
199 } else {
200 w.Header().Set("Cache-Control", "max-age=0")
201 }
202 w.Header().Set("Content-Type", "text/html; charset=utf-8")
203
204 err := readviews.Execute(w, tname, templinfo)
205 if err != nil {
206 log.Print(err)
207 }
208}
209
210func showfunzone(w http.ResponseWriter, r *http.Request) {
211 var emunames, memenames []string
212 dir, err := os.Open("emus")
213 if err == nil {
214 emunames, _ = dir.Readdirnames(0)
215 dir.Close()
216 }
217 for i, e := range emunames {
218 if len(e) > 4 {
219 emunames[i] = e[:len(e)-4]
220 }
221 }
222 dir, err = os.Open("memes")
223 if err == nil {
224 memenames, _ = dir.Readdirnames(0)
225 dir.Close()
226 }
227 templinfo := getInfo(r)
228 templinfo["Emus"] = emunames
229 templinfo["Memes"] = memenames
230 err = readviews.Execute(w, "funzone.html", templinfo)
231 if err != nil {
232 log.Print(err)
233 }
234
235}
236
237func showrss(w http.ResponseWriter, r *http.Request) {
238 name := mux.Vars(r)["name"]
239
240 var honks []*Honk
241 if name != "" {
242 honks = gethonksbyuser(name, false)
243 } else {
244 honks = getpublichonks()
245 }
246 reverbolate(-1, honks)
247
248 home := fmt.Sprintf("https://%s/", serverName)
249 base := home
250 if name != "" {
251 home += "u/" + name
252 name += " "
253 }
254 feed := rss.Feed{
255 Title: name + "honk",
256 Link: home,
257 Description: name + "honk rss",
258 Image: &rss.Image{
259 URL: base + "icon.png",
260 Title: name + "honk rss",
261 Link: home,
262 },
263 }
264 var modtime time.Time
265 for _, honk := range honks {
266 if !firstclass(honk) {
267 continue
268 }
269 desc := string(honk.HTML)
270 for _, d := range honk.Donks {
271 desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
272 d.URL, html.EscapeString(d.Name))
273 }
274
275 feed.Items = append(feed.Items, &rss.Item{
276 Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
277 Description: rss.CData{desc},
278 Link: honk.URL,
279 PubDate: honk.Date.Format(time.RFC1123),
280 Guid: &rss.Guid{IsPermaLink: true, Value: honk.URL},
281 })
282 if honk.Date.After(modtime) {
283 modtime = honk.Date
284 }
285 }
286 w.Header().Set("Cache-Control", "max-age=300")
287 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
288
289 err := feed.Write(w)
290 if err != nil {
291 log.Printf("error writing rss: %s", err)
292 }
293}
294
295func butwhatabout(name string) (*WhatAbout, error) {
296 row := stmtWhatAbout.QueryRow(name)
297 var user WhatAbout
298 var options string
299 err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &options)
300 user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
301 user.SkinnyCSS = strings.Contains(options, " skinny ")
302 return &user, err
303}
304
305func crappola(j junk.Junk) bool {
306 t, _ := j.GetString("type")
307 a, _ := j.GetString("actor")
308 o, _ := j.GetString("object")
309 if t == "Delete" && a == o {
310 log.Printf("crappola from %s", a)
311 return true
312 }
313 return false
314}
315
316func ping(user *WhatAbout, who string) {
317 box, err := getboxes(who)
318 if err != nil {
319 log.Printf("no inbox for ping: %s", err)
320 return
321 }
322 j := junk.New()
323 j["@context"] = itiswhatitis
324 j["type"] = "Ping"
325 j["id"] = user.URL + "/ping/" + xfiltrate()
326 j["actor"] = user.URL
327 j["to"] = who
328 keyname, key := ziggy(user.Name)
329 err = PostJunk(keyname, key, box.In, j)
330 if err != nil {
331 log.Printf("can't send ping: %s", err)
332 return
333 }
334 log.Printf("sent ping to %s: %s", who, j["id"])
335}
336
337func pong(user *WhatAbout, who string, obj string) {
338 box, err := getboxes(who)
339 if err != nil {
340 log.Printf("no inbox for pong %s : %s", who, err)
341 return
342 }
343 j := junk.New()
344 j["@context"] = itiswhatitis
345 j["type"] = "Pong"
346 j["id"] = user.URL + "/pong/" + xfiltrate()
347 j["actor"] = user.URL
348 j["to"] = who
349 j["object"] = obj
350 keyname, key := ziggy(user.Name)
351 err = PostJunk(keyname, key, box.In, j)
352 if err != nil {
353 log.Printf("can't send pong: %s", err)
354 return
355 }
356}
357
358func inbox(w http.ResponseWriter, r *http.Request) {
359 name := mux.Vars(r)["name"]
360 user, err := butwhatabout(name)
361 if err != nil {
362 http.NotFound(w, r)
363 return
364 }
365 var buf bytes.Buffer
366 io.Copy(&buf, r.Body)
367 payload := buf.Bytes()
368 j, err := junk.Read(bytes.NewReader(payload))
369 if err != nil {
370 log.Printf("bad payload: %s", err)
371 io.WriteString(os.Stdout, "bad payload\n")
372 os.Stdout.Write(payload)
373 io.WriteString(os.Stdout, "\n")
374 return
375 }
376 if crappola(j) {
377 return
378 }
379 keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
380 if err != nil {
381 log.Printf("inbox message failed signature: %s", err)
382 if keyname != "" {
383 keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
384 if err != nil {
385 log.Printf("still failed: %s", err)
386 }
387 }
388 if err != nil {
389 return
390 }
391 }
392 what, _ := j.GetString("type")
393 if what == "Like" {
394 return
395 }
396 who, _ := j.GetString("actor")
397 origin := keymatch(keyname, who)
398 if origin == "" {
399 log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
400 return
401 }
402 objid, _ := j.GetString("id")
403 if thoudostbitethythumb(user.ID, []string{who}, objid) {
404 log.Printf("ignoring thumb sucker %s", who)
405 return
406 }
407 switch what {
408 case "Ping":
409 obj, _ := j.GetString("id")
410 log.Printf("ping from %s: %s", who, obj)
411 pong(user, who, obj)
412 case "Pong":
413 obj, _ := j.GetString("object")
414 log.Printf("pong from %s: %s", who, obj)
415 case "Follow":
416 obj, _ := j.GetString("object")
417 if obj == user.URL {
418 log.Printf("updating honker follow: %s", who)
419 stmtSaveDub.Exec(user.ID, who, who, "dub")
420 go rubadubdub(user, j)
421 } else {
422 log.Printf("can't follow %s", obj)
423 }
424 case "Accept":
425 log.Printf("updating honker accept: %s", who)
426 _, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
427 if err != nil {
428 log.Printf("error updating honker: %s", err)
429 return
430 }
431 case "Update":
432 obj, ok := j.GetMap("object")
433 if ok {
434 what, _ := obj.GetString("type")
435 switch what {
436 case "Person":
437 return
438 case "Question":
439 return
440 }
441 }
442 log.Printf("unknown Update activity")
443 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
444 j.Write(fd)
445 io.WriteString(fd, "\n")
446 fd.Close()
447
448 case "Undo":
449 obj, ok := j.GetMap("object")
450 if !ok {
451 log.Printf("unknown undo no object")
452 } else {
453 what, _ := obj.GetString("type")
454 switch what {
455 case "Follow":
456 log.Printf("updating honker undo: %s", who)
457 _, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
458 if err != nil {
459 log.Printf("error updating honker: %s", err)
460 return
461 }
462 case "Like":
463 case "Announce":
464 default:
465 log.Printf("unknown undo: %s", what)
466 }
467 }
468 default:
469 go consumeactivity(user, j, origin)
470 }
471}
472
473func ximport(w http.ResponseWriter, r *http.Request) {
474 xid := r.FormValue("xid")
475 p := investigate(xid)
476 if p != nil {
477 xid = p.XID
478 }
479 j, err := GetJunk(xid)
480 if err != nil {
481 http.Error(w, "error getting external object", http.StatusInternalServerError)
482 log.Printf("error getting external object: %s", err)
483 return
484 }
485 log.Printf("importing %s", xid)
486 u := login.GetUserInfo(r)
487 user, _ := butwhatabout(u.Username)
488
489 what, _ := j.GetString("type")
490 if isactor(what) {
491 outbox, _ := j.GetString("outbox")
492 gimmexonks(user, outbox)
493 http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
494 return
495 }
496 xonk := xonkxonk(user, j, originate(xid))
497 convoy := ""
498 if xonk != nil {
499 convoy = xonk.Convoy
500 savexonk(user, xonk)
501 }
502 http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
503}
504
505func xzone(w http.ResponseWriter, r *http.Request) {
506 u := login.GetUserInfo(r)
507 rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
508 if err != nil {
509 log.Printf("query err: %s", err)
510 return
511 }
512 defer rows.Close()
513 var honkers []Honker
514 for rows.Next() {
515 var xid string
516 rows.Scan(&xid)
517 honkers = append(honkers, Honker{XID: xid})
518 }
519 rows.Close()
520 for i, _ := range honkers {
521 _, honkers[i].Handle = handles(honkers[i].XID)
522 }
523 templinfo := getInfo(r)
524 templinfo["XCSRF"] = login.GetCSRF("ximport", r)
525 templinfo["Honkers"] = honkers
526 err = readviews.Execute(w, "xzone.html", templinfo)
527 if err != nil {
528 log.Print(err)
529 }
530}
531
532func outbox(w http.ResponseWriter, r *http.Request) {
533 name := mux.Vars(r)["name"]
534 user, err := butwhatabout(name)
535 if err != nil {
536 http.NotFound(w, r)
537 return
538 }
539 if stealthed(r) {
540 http.NotFound(w, r)
541 return
542 }
543
544 honks := gethonksbyuser(name, false)
545
546 var jonks []junk.Junk
547 for _, h := range honks {
548 j, _ := jonkjonk(user, h)
549 jonks = append(jonks, j)
550 }
551
552 j := junk.New()
553 j["@context"] = itiswhatitis
554 j["id"] = user.URL + "/outbox"
555 j["type"] = "OrderedCollection"
556 j["totalItems"] = len(jonks)
557 j["orderedItems"] = jonks
558
559 w.Header().Set("Content-Type", theonetruename)
560 j.Write(w)
561}
562
563func emptiness(w http.ResponseWriter, r *http.Request) {
564 name := mux.Vars(r)["name"]
565 user, err := butwhatabout(name)
566 if err != nil {
567 http.NotFound(w, r)
568 return
569 }
570 colname := "/followers"
571 if strings.HasSuffix(r.URL.Path, "/following") {
572 colname = "/following"
573 }
574 j := junk.New()
575 j["@context"] = itiswhatitis
576 j["id"] = user.URL + colname
577 j["type"] = "OrderedCollection"
578 j["totalItems"] = 0
579 j["orderedItems"] = []junk.Junk{}
580
581 w.Header().Set("Content-Type", theonetruename)
582 j.Write(w)
583}
584
585func showuser(w http.ResponseWriter, r *http.Request) {
586 name := mux.Vars(r)["name"]
587 user, err := butwhatabout(name)
588 if err != nil {
589 log.Printf("user not found %s: %s", name, err)
590 http.NotFound(w, r)
591 return
592 }
593 if friendorfoe(r.Header.Get("Accept")) {
594 j := asjonker(user)
595 w.Header().Set("Content-Type", theonetruename)
596 j.Write(w)
597 return
598 }
599 u := login.GetUserInfo(r)
600 honks := gethonksbyuser(name, u != nil && u.Username == name)
601 honkpage(w, r, u, user, honks, "")
602}
603
604func showhonker(w http.ResponseWriter, r *http.Request) {
605 u := login.GetUserInfo(r)
606 name := mux.Vars(r)["name"]
607 var honks []*Honk
608 if name == "" {
609 name = r.FormValue("xid")
610 honks = gethonksbyxonker(u.UserID, name)
611 } else {
612 honks = gethonksbyhonker(u.UserID, name)
613 }
614 name = html.EscapeString(name)
615 msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
616 honkpage(w, r, u, nil, honks, template.HTML(msg))
617}
618
619func showcombo(w http.ResponseWriter, r *http.Request) {
620 name := mux.Vars(r)["name"]
621 u := login.GetUserInfo(r)
622 honks := gethonksbycombo(u.UserID, name)
623 honks = osmosis(honks, u.UserID)
624 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
625}
626func showconvoy(w http.ResponseWriter, r *http.Request) {
627 c := r.FormValue("c")
628 u := login.GetUserInfo(r)
629 honks := gethonksbyconvoy(u.UserID, c)
630 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
631}
632
633func showhonk(w http.ResponseWriter, r *http.Request) {
634 name := mux.Vars(r)["name"]
635 user, err := butwhatabout(name)
636 if err != nil {
637 http.NotFound(w, r)
638 return
639 }
640 if stealthed(r) {
641 http.NotFound(w, r)
642 return
643 }
644
645 xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
646 honk := getxonk(user.ID, xid)
647 if honk == nil {
648 http.NotFound(w, r)
649 return
650 }
651 u := login.GetUserInfo(r)
652 if u != nil && u.UserID != user.ID {
653 u = nil
654 }
655 if !honk.Public {
656 if u == nil {
657 http.NotFound(w, r)
658 return
659
660 }
661 honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
662 return
663 }
664 rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
665 if friendorfoe(r.Header.Get("Accept")) {
666 for _, h := range rawhonks {
667 if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
668 honk.Replies = append(honk.Replies, h)
669 }
670 }
671 donksforhonks([]*Honk{honk})
672 _, j := jonkjonk(user, honk)
673 j["@context"] = itiswhatitis
674 w.Header().Set("Content-Type", theonetruename)
675 j.Write(w)
676 return
677 }
678 var honks []*Honk
679 for _, h := range rawhonks {
680 if h.Public && (h.Whofore == 2 || h.IsAcked()) {
681 honks = append(honks, h)
682 }
683 }
684
685 honkpage(w, r, u, nil, honks, "one honk maybe more")
686}
687
688func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
689 honks []*Honk, infomsg template.HTML) {
690 templinfo := getInfo(r)
691 var userid int64 = -1
692 if u != nil {
693 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
694 userid = u.UserID
695 }
696 if u == nil {
697 w.Header().Set("Cache-Control", "max-age=60")
698 }
699 reverbolate(userid, honks)
700 if user != nil {
701 filt := htfilter.New()
702 templinfo["Name"] = user.Name
703 whatabout := user.About
704 whatabout = obfusbreak(user.About)
705 templinfo["WhatAbout"], _ = filt.String(whatabout)
706 }
707 templinfo["Honks"] = honks
708 templinfo["ServerMessage"] = infomsg
709 err := readviews.Execute(w, "honkpage.html", templinfo)
710 if err != nil {
711 log.Print(err)
712 }
713}
714
715func saveuser(w http.ResponseWriter, r *http.Request) {
716 whatabout := r.FormValue("whatabout")
717 u := login.GetUserInfo(r)
718 db := opendatabase()
719 options := ""
720 if r.FormValue("skinny") == "skinny" {
721 options += " skinny "
722 }
723 _, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
724 if err != nil {
725 log.Printf("error bouting what: %s", err)
726 }
727
728 http.Redirect(w, r, "/account", http.StatusSeeOther)
729}
730
731func gethonkers(userid int64) []*Honker {
732 rows, err := stmtHonkers.Query(userid)
733 if err != nil {
734 log.Printf("error querying honkers: %s", err)
735 return nil
736 }
737 defer rows.Close()
738 var honkers []*Honker
739 for rows.Next() {
740 var f Honker
741 var combos string
742 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
743 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
744 if err != nil {
745 log.Printf("error scanning honker: %s", err)
746 return nil
747 }
748 honkers = append(honkers, &f)
749 }
750 return honkers
751}
752
753func getdubs(userid int64) []*Honker {
754 rows, err := stmtDubbers.Query(userid)
755 if err != nil {
756 log.Printf("error querying dubs: %s", err)
757 return nil
758 }
759 defer rows.Close()
760 var honkers []*Honker
761 for rows.Next() {
762 var f Honker
763 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
764 if err != nil {
765 log.Printf("error scanning honker: %s", err)
766 return nil
767 }
768 honkers = append(honkers, &f)
769 }
770 return honkers
771}
772
773func allusers() []login.UserInfo {
774 var users []login.UserInfo
775 rows, _ := opendatabase().Query("select userid, username from users")
776 defer rows.Close()
777 for rows.Next() {
778 var u login.UserInfo
779 rows.Scan(&u.UserID, &u.Username)
780 users = append(users, u)
781 }
782 return users
783}
784
785func getxonk(userid int64, xid string) *Honk {
786 h := new(Honk)
787 var dt, aud string
788 row := stmtOneXonk.QueryRow(userid, xid)
789 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
790 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
791 if err != nil {
792 if err != sql.ErrNoRows {
793 log.Printf("error scanning xonk: %s", err)
794 }
795 return nil
796 }
797 h.Date, _ = time.Parse(dbtimeformat, dt)
798 h.Audience = strings.Split(aud, " ")
799 h.Public = !keepitquiet(h.Audience)
800 return h
801}
802
803func getpublichonks() []*Honk {
804 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
805 rows, err := stmtPublicHonks.Query(dt)
806 return getsomehonks(rows, err)
807}
808func gethonksbyuser(name string, includeprivate bool) []*Honk {
809 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
810 whofore := 2
811 if includeprivate {
812 whofore = 3
813 }
814 rows, err := stmtUserHonks.Query(whofore, name, dt)
815 return getsomehonks(rows, err)
816}
817func gethonksforuser(userid int64) []*Honk {
818 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
819 rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
820 return getsomehonks(rows, err)
821}
822func gethonksforme(userid int64) []*Honk {
823 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
824 rows, err := stmtHonksForMe.Query(userid, dt, userid)
825 return getsomehonks(rows, err)
826}
827func gethonksbyhonker(userid int64, honker string) []*Honk {
828 rows, err := stmtHonksByHonker.Query(userid, honker, userid)
829 return getsomehonks(rows, err)
830}
831func gethonksbyxonker(userid int64, xonker string) []*Honk {
832 rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
833 return getsomehonks(rows, err)
834}
835func gethonksbycombo(userid int64, combo string) []*Honk {
836 combo = "% " + combo + " %"
837 rows, err := stmtHonksByCombo.Query(userid, combo, userid)
838 return getsomehonks(rows, err)
839}
840func gethonksbyconvoy(userid int64, convoy string) []*Honk {
841 rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
842 honks := getsomehonks(rows, err)
843 for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
844 honks[i], honks[j] = honks[j], honks[i]
845 }
846 return honks
847}
848
849func getsomehonks(rows *sql.Rows, err error) []*Honk {
850 if err != nil {
851 log.Printf("error querying honks: %s", err)
852 return nil
853 }
854 defer rows.Close()
855 var honks []*Honk
856 for rows.Next() {
857 var h Honk
858 var dt, aud string
859 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
860 &h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
861 if err != nil {
862 log.Printf("error scanning honks: %s", err)
863 return nil
864 }
865 h.Date, _ = time.Parse(dbtimeformat, dt)
866 h.Audience = strings.Split(aud, " ")
867 h.Public = !keepitquiet(h.Audience)
868 honks = append(honks, &h)
869 }
870 rows.Close()
871 donksforhonks(honks)
872 return honks
873}
874
875func donksforhonks(honks []*Honk) {
876 db := opendatabase()
877 var ids []string
878 hmap := make(map[int64]*Honk)
879 for _, h := range honks {
880 ids = append(ids, fmt.Sprintf("%d", h.ID))
881 hmap[h.ID] = h
882 }
883 q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
884 rows, err := db.Query(q)
885 if err != nil {
886 log.Printf("error querying donks: %s", err)
887 return
888 }
889 defer rows.Close()
890 for rows.Next() {
891 var hid int64
892 var d Donk
893 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
894 if err != nil {
895 log.Printf("error scanning donk: %s", err)
896 continue
897 }
898 h := hmap[hid]
899 h.Donks = append(h.Donks, &d)
900 }
901}
902
903func savebonk(w http.ResponseWriter, r *http.Request) {
904 xid := r.FormValue("xid")
905 userinfo := login.GetUserInfo(r)
906 user, _ := butwhatabout(userinfo.Username)
907
908 log.Printf("bonking %s", xid)
909
910 xonk := getxonk(userinfo.UserID, xid)
911 if xonk == nil {
912 return
913 }
914 if !xonk.Public {
915 return
916 }
917 donksforhonks([]*Honk{xonk})
918
919 _, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.XID, userinfo.UserID)
920 if err != nil {
921 log.Printf("error acking bonk: %s", err)
922 }
923
924 oonker := xonk.Oonker
925 if oonker == "" {
926 oonker = xonk.Honker
927 }
928 dt := time.Now().UTC()
929 bonk := Honk{
930 UserID: userinfo.UserID,
931 Username: userinfo.Username,
932 What: "bonk",
933 Honker: user.URL,
934 XID: xonk.XID,
935 Date: dt,
936 Donks: xonk.Donks,
937 Convoy: xonk.Convoy,
938 Audience: []string{thewholeworld, oonker},
939 Public: true,
940 }
941
942 aud := strings.Join(bonk.Audience, " ")
943 whofore := 2
944 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
945 dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
946 xonk.Precis, oonker, 0)
947 if err != nil {
948 log.Printf("error saving bonk: %s", err)
949 return
950 }
951 bonk.ID, _ = res.LastInsertId()
952 for _, d := range bonk.Donks {
953 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
954 if err != nil {
955 log.Printf("err saving donk: %s", err)
956 return
957 }
958 }
959
960 go honkworldwide(user, &bonk)
961}
962
963func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
964 zonk := Honk{
965 What: what,
966 XID: xonk.XID,
967 Date: time.Now().UTC(),
968 Audience: oneofakind(xonk.Audience),
969 }
970 zonk.Public = !keepitquiet(zonk.Audience)
971
972 log.Printf("announcing %sed honk: %s", what, xonk.XID)
973 go honkworldwide(user, &zonk)
974}
975
976func zonkit(w http.ResponseWriter, r *http.Request) {
977 wherefore := r.FormValue("wherefore")
978 what := r.FormValue("what")
979 userinfo := login.GetUserInfo(r)
980 user, _ := butwhatabout(userinfo.Username)
981
982 if wherefore == "ack" {
983 _, err := stmtUpdateFlags.Exec(flagIsAcked, what, userinfo.UserID)
984 if err != nil {
985 log.Printf("error acking: %s", err)
986 }
987 xonk := getxonk(userinfo.UserID, what)
988 if xonk != nil {
989 sendzonkofsorts(xonk, user, "ack")
990 }
991 return
992 }
993
994 if wherefore == "deack" {
995 _, err := stmtClearFlags.Exec(flagIsAcked, what, userinfo.UserID)
996 if err != nil {
997 log.Printf("error deacking: %s", err)
998 }
999 xonk := getxonk(userinfo.UserID, what)
1000 if xonk != nil {
1001 sendzonkofsorts(xonk, user, "deack")
1002 }
1003 return
1004 }
1005
1006 if wherefore == "unbonk" {
1007 // todo
1008 return
1009 }
1010
1011 log.Printf("zonking %s %s", wherefore, what)
1012 if wherefore == "zonk" {
1013 xonk := getxonk(userinfo.UserID, what)
1014 if xonk != nil {
1015 _, err := stmtZonkDonks.Exec(xonk.ID)
1016 if err != nil {
1017 log.Printf("error zonking: %s", err)
1018 }
1019 _, err = stmtZonkIt.Exec(userinfo.UserID, what)
1020 if err != nil {
1021 log.Printf("error zonking: %s", err)
1022 }
1023 if xonk.Whofore == 2 || xonk.Whofore == 3 {
1024 sendzonkofsorts(xonk, user, "zonk")
1025 }
1026 }
1027 }
1028 _, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1029 if err != nil {
1030 log.Printf("error saving zonker: %s", err)
1031 return
1032 }
1033}
1034
1035func savehonk(w http.ResponseWriter, r *http.Request) {
1036 rid := r.FormValue("rid")
1037 noise := r.FormValue("noise")
1038
1039 userinfo := login.GetUserInfo(r)
1040 user, _ := butwhatabout(userinfo.Username)
1041
1042 dt := time.Now().UTC()
1043 xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1044 what := "honk"
1045 if rid != "" {
1046 what = "tonk"
1047 }
1048 honk := Honk{
1049 UserID: userinfo.UserID,
1050 Username: userinfo.Username,
1051 What: "honk",
1052 Honker: user.URL,
1053 XID: xid,
1054 Date: dt,
1055 }
1056 if strings.HasPrefix(noise, "DZ:") {
1057 idx := strings.Index(noise, "\n")
1058 if idx == -1 {
1059 honk.Precis = noise
1060 noise = ""
1061 } else {
1062 honk.Precis = noise[:idx]
1063 noise = noise[idx+1:]
1064 }
1065 }
1066 noise = hooterize(noise)
1067 noise = strings.TrimSpace(noise)
1068 honk.Precis = strings.TrimSpace(honk.Precis)
1069
1070 var convoy string
1071 if rid != "" {
1072 xonk := getxonk(userinfo.UserID, rid)
1073 if xonk != nil {
1074 if xonk.Public {
1075 honk.Audience = append(honk.Audience, xonk.Audience...)
1076 }
1077 convoy = xonk.Convoy
1078 } else {
1079 xonkaud, c := whosthere(rid)
1080 honk.Audience = append(honk.Audience, xonkaud...)
1081 convoy = c
1082 }
1083 for i, a := range honk.Audience {
1084 if a == thewholeworld {
1085 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1086 break
1087 }
1088 }
1089 honk.RID = rid
1090 } else {
1091 honk.Audience = []string{thewholeworld}
1092 }
1093 if noise != "" && noise[0] == '@' {
1094 honk.Audience = append(grapevine(noise), honk.Audience...)
1095 } else {
1096 honk.Audience = append(honk.Audience, grapevine(noise)...)
1097 }
1098 if convoy == "" {
1099 convoy = "data:,electrichonkytonk-" + xfiltrate()
1100 }
1101 butnottooloud(honk.Audience)
1102 honk.Audience = oneofakind(honk.Audience)
1103 if len(honk.Audience) == 0 {
1104 log.Printf("honk to nowhere")
1105 http.Error(w, "honk to nowhere...", http.StatusNotFound)
1106 return
1107 }
1108 honk.Public = !keepitquiet(honk.Audience)
1109 noise = obfusbreak(noise)
1110 honk.Noise = noise
1111 honk.Convoy = convoy
1112
1113 donkxid := r.FormValue("donkxid")
1114 if donkxid == "" {
1115 file, filehdr, err := r.FormFile("donk")
1116 if err == nil {
1117 var buf bytes.Buffer
1118 io.Copy(&buf, file)
1119 file.Close()
1120 data := buf.Bytes()
1121 xid := xfiltrate()
1122 var media, name string
1123 img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1124 if err == nil {
1125 data = img.Data
1126 format := img.Format
1127 media = "image/" + format
1128 if format == "jpeg" {
1129 format = "jpg"
1130 }
1131 name = xid + "." + format
1132 xid = name
1133 } else {
1134 maxsize := 100000
1135 if len(data) > maxsize {
1136 log.Printf("bad image: %s too much text: %d", err, len(data))
1137 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1138 return
1139 }
1140 for i := 0; i < len(data); i++ {
1141 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1142 log.Printf("bad image: %s not text: %d", err, data[i])
1143 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1144 return
1145 }
1146 }
1147 media = "text/plain"
1148 name = filehdr.Filename
1149 if name == "" {
1150 name = xid + ".txt"
1151 }
1152 xid += ".txt"
1153 }
1154 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1155 res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1156 if err != nil {
1157 log.Printf("unable to save image: %s", err)
1158 return
1159 }
1160 var d Donk
1161 d.FileID, _ = res.LastInsertId()
1162 d.XID = name
1163 d.Name = name
1164 d.Media = media
1165 d.URL = url
1166 d.Local = true
1167 honk.Donks = append(honk.Donks, &d)
1168 donkxid = d.XID
1169 }
1170 } else {
1171 xid := donkxid
1172 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1173 var donk Donk
1174 row := stmtFindFile.QueryRow(url)
1175 err := row.Scan(&donk.FileID)
1176 if err == nil {
1177 donk.XID = xid
1178 donk.Local = true
1179 donk.URL = url
1180 honk.Donks = append(honk.Donks, &donk)
1181 } else {
1182 log.Printf("can't find file: %s", xid)
1183 }
1184 }
1185 herd := herdofemus(honk.Noise)
1186 for _, e := range herd {
1187 donk := savedonk(e.ID, e.Name, "image/png", true)
1188 if donk != nil {
1189 donk.Name = e.Name
1190 honk.Donks = append(honk.Donks, donk)
1191 }
1192 }
1193 memetize(&honk)
1194
1195 aud := strings.Join(honk.Audience, " ")
1196 whofore := 2
1197 if !honk.Public {
1198 whofore = 3
1199 }
1200 if r.FormValue("preview") == "preview" {
1201 honks := []*Honk{&honk}
1202 reverbolate(userinfo.UserID, honks)
1203 templinfo := getInfo(r)
1204 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1205 templinfo["Honks"] = honks
1206 templinfo["InReplyTo"] = r.FormValue("rid")
1207 templinfo["Noise"] = r.FormValue("noise")
1208 templinfo["SavedFile"] = donkxid
1209 templinfo["ServerMessage"] = "honk preview"
1210 err := readviews.Execute(w, "honkpage.html", templinfo)
1211 if err != nil {
1212 log.Print(err)
1213 }
1214 return
1215 }
1216 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1217 dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1218 honk.Precis, honk.Oonker, 0)
1219 if err != nil {
1220 log.Printf("error saving honk: %s", err)
1221 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1222 return
1223 }
1224 honk.ID, _ = res.LastInsertId()
1225 for _, d := range honk.Donks {
1226 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1227 if err != nil {
1228 log.Printf("err saving donk: %s", err)
1229 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1230 return
1231 }
1232 }
1233
1234 go honkworldwide(user, &honk)
1235
1236 http.Redirect(w, r, xid, http.StatusSeeOther)
1237}
1238
1239func showhonkers(w http.ResponseWriter, r *http.Request) {
1240 userinfo := login.GetUserInfo(r)
1241 templinfo := getInfo(r)
1242 templinfo["Honkers"] = gethonkers(userinfo.UserID)
1243 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1244 err := readviews.Execute(w, "honkers.html", templinfo)
1245 if err != nil {
1246 log.Print(err)
1247 }
1248}
1249
1250func showcombos(w http.ResponseWriter, r *http.Request) {
1251 userinfo := login.GetUserInfo(r)
1252 templinfo := getInfo(r)
1253 honkers := gethonkers(userinfo.UserID)
1254 var combos []string
1255 for _, h := range honkers {
1256 combos = append(combos, h.Combos...)
1257 }
1258 for i, c := range combos {
1259 if c == "-" {
1260 combos[i] = ""
1261 }
1262 }
1263 combos = oneofakind(combos)
1264 sort.Strings(combos)
1265 templinfo["Combos"] = combos
1266 err := readviews.Execute(w, "combos.html", templinfo)
1267 if err != nil {
1268 log.Print(err)
1269 }
1270}
1271
1272func savehonker(w http.ResponseWriter, r *http.Request) {
1273 u := login.GetUserInfo(r)
1274 name := r.FormValue("name")
1275 url := r.FormValue("url")
1276 peep := r.FormValue("peep")
1277 combos := r.FormValue("combos")
1278 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1279
1280 if honkerid > 0 {
1281 goodbye := r.FormValue("goodbye")
1282 if goodbye == "F" {
1283 db := opendatabase()
1284 row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1285 honkerid, u.UserID)
1286 var xid string
1287 err := row.Scan(&xid)
1288 if err != nil {
1289 log.Printf("can't get honker xid: %s", err)
1290 return
1291 }
1292 log.Printf("unsubscribing from %s", xid)
1293 user, _ := butwhatabout(u.Username)
1294 go itakeitallback(user, xid)
1295 _, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1296 if err != nil {
1297 log.Printf("error updating honker: %s", err)
1298 return
1299 }
1300
1301 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1302 return
1303 }
1304 combos = " " + strings.TrimSpace(combos) + " "
1305 _, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1306 if err != nil {
1307 log.Printf("update honker err: %s", err)
1308 return
1309 }
1310 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1311 }
1312
1313 flavor := "presub"
1314 if peep == "peep" {
1315 flavor = "peep"
1316 }
1317 p := investigate(url)
1318 if p == nil {
1319 log.Printf("failed to investigate honker")
1320 return
1321 }
1322 url = p.XID
1323 if name == "" {
1324 name = p.Handle
1325 }
1326 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1327 if err != nil {
1328 log.Print(err)
1329 return
1330 }
1331 if flavor == "presub" {
1332 user, _ := butwhatabout(u.Username)
1333 go subsub(user, url)
1334 }
1335 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1336}
1337
1338type Zonker struct {
1339 ID int64
1340 Name string
1341 Wherefore string
1342}
1343
1344func zonkzone(w http.ResponseWriter, r *http.Request) {
1345 userinfo := login.GetUserInfo(r)
1346 rows, err := stmtGetZonkers.Query(userinfo.UserID)
1347 if err != nil {
1348 log.Printf("err: %s", err)
1349 return
1350 }
1351 defer rows.Close()
1352 var zonkers []Zonker
1353 for rows.Next() {
1354 var z Zonker
1355 rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1356 zonkers = append(zonkers, z)
1357 }
1358 sort.Slice(zonkers, func(i, j int) bool {
1359 w1 := zonkers[i].Wherefore
1360 w2 := zonkers[j].Wherefore
1361 if w1 == w2 {
1362 return zonkers[i].Name < zonkers[j].Name
1363 }
1364 if w1 == "zonvoy" {
1365 w1 = "zzzzzzz"
1366 }
1367 if w2 == "zonvoy" {
1368 w2 = "zzzzzzz"
1369 }
1370 return w1 < w2
1371 })
1372
1373 templinfo := getInfo(r)
1374 templinfo["Zonkers"] = zonkers
1375 templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1376 err = readviews.Execute(w, "zonkers.html", templinfo)
1377 if err != nil {
1378 log.Print(err)
1379 }
1380}
1381
1382func zonkzonk(w http.ResponseWriter, r *http.Request) {
1383 userinfo := login.GetUserInfo(r)
1384 itsok := r.FormValue("itsok")
1385 if itsok == "iforgiveyou" {
1386 zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1387 db := opendatabase()
1388 db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1389 userinfo.UserID, zonkerid)
1390 bitethethumbs()
1391 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1392 return
1393 }
1394 wherefore := r.FormValue("wherefore")
1395 name := r.FormValue("name")
1396 if name == "" {
1397 return
1398 }
1399 switch wherefore {
1400 case "zonker":
1401 case "zomain":
1402 case "zonvoy":
1403 case "zord":
1404 case "zilence":
1405 default:
1406 return
1407 }
1408 db := opendatabase()
1409 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1410 userinfo.UserID, name, wherefore)
1411 if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1412 bitethethumbs()
1413 }
1414
1415 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1416}
1417
1418func accountpage(w http.ResponseWriter, r *http.Request) {
1419 u := login.GetUserInfo(r)
1420 user, _ := butwhatabout(u.Username)
1421 templinfo := getInfo(r)
1422 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1423 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1424 templinfo["User"] = user
1425 err := readviews.Execute(w, "account.html", templinfo)
1426 if err != nil {
1427 log.Print(err)
1428 }
1429}
1430
1431func dochpass(w http.ResponseWriter, r *http.Request) {
1432 err := login.ChangePassword(w, r)
1433 if err != nil {
1434 log.Printf("error changing password: %s", err)
1435 }
1436 http.Redirect(w, r, "/account", http.StatusSeeOther)
1437}
1438
1439func fingerlicker(w http.ResponseWriter, r *http.Request) {
1440 orig := r.FormValue("resource")
1441
1442 log.Printf("finger lick: %s", orig)
1443
1444 if strings.HasPrefix(orig, "acct:") {
1445 orig = orig[5:]
1446 }
1447
1448 name := orig
1449 idx := strings.LastIndexByte(name, '/')
1450 if idx != -1 {
1451 name = name[idx+1:]
1452 if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1453 log.Printf("foreign request rejected")
1454 name = ""
1455 }
1456 } else {
1457 idx = strings.IndexByte(name, '@')
1458 if idx != -1 {
1459 name = name[:idx]
1460 if name+"@"+serverName != orig {
1461 log.Printf("foreign request rejected")
1462 name = ""
1463 }
1464 }
1465 }
1466 user, err := butwhatabout(name)
1467 if err != nil {
1468 http.NotFound(w, r)
1469 return
1470 }
1471
1472 j := junk.New()
1473 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1474 j["aliases"] = []string{user.URL}
1475 var links []junk.Junk
1476 l := junk.New()
1477 l["rel"] = "self"
1478 l["type"] = `application/activity+json`
1479 l["href"] = user.URL
1480 links = append(links, l)
1481 j["links"] = links
1482
1483 w.Header().Set("Cache-Control", "max-age=3600")
1484 w.Header().Set("Content-Type", "application/jrd+json")
1485 j.Write(w)
1486}
1487
1488func somedays() string {
1489 secs := 432000 + notrand.Int63n(432000)
1490 return fmt.Sprintf("%d", secs)
1491}
1492
1493func avatate(w http.ResponseWriter, r *http.Request) {
1494 n := r.FormValue("a")
1495 a := avatar(n)
1496 w.Header().Set("Cache-Control", "max-age="+somedays())
1497 w.Write(a)
1498}
1499
1500func servecss(w http.ResponseWriter, r *http.Request) {
1501 w.Header().Set("Cache-Control", "max-age=7776000")
1502 http.ServeFile(w, r, "views"+r.URL.Path)
1503}
1504func servehtml(w http.ResponseWriter, r *http.Request) {
1505 templinfo := getInfo(r)
1506 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1507 if err != nil {
1508 log.Print(err)
1509 }
1510}
1511func serveemu(w http.ResponseWriter, r *http.Request) {
1512 xid := mux.Vars(r)["xid"]
1513 w.Header().Set("Cache-Control", "max-age="+somedays())
1514 http.ServeFile(w, r, "emus/"+xid)
1515}
1516func servememe(w http.ResponseWriter, r *http.Request) {
1517 xid := mux.Vars(r)["xid"]
1518 w.Header().Set("Cache-Control", "max-age="+somedays())
1519 http.ServeFile(w, r, "memes/"+xid)
1520}
1521
1522func servefile(w http.ResponseWriter, r *http.Request) {
1523 xid := mux.Vars(r)["xid"]
1524 row := stmtFileData.QueryRow(xid)
1525 var media string
1526 var data []byte
1527 err := row.Scan(&media, &data)
1528 if err != nil {
1529 log.Printf("error loading file: %s", err)
1530 http.NotFound(w, r)
1531 return
1532 }
1533 w.Header().Set("Content-Type", media)
1534 w.Header().Set("X-Content-Type-Options", "nosniff")
1535 w.Header().Set("Cache-Control", "max-age="+somedays())
1536 w.Write(data)
1537}
1538
1539func nomoroboto(w http.ResponseWriter, r *http.Request) {
1540 io.WriteString(w, "User-agent: *\n")
1541 io.WriteString(w, "Disallow: /a\n")
1542 io.WriteString(w, "Disallow: /d\n")
1543 io.WriteString(w, "Disallow: /meme\n")
1544 for _, u := range allusers() {
1545 fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1546 }
1547}
1548
1549func serve() {
1550 db := opendatabase()
1551 login.Init(db)
1552
1553 listener, err := openListener()
1554 if err != nil {
1555 log.Fatal(err)
1556 }
1557 go redeliverator()
1558
1559 debug := false
1560 getconfig("debug", &debug)
1561 readviews = templates.Load(debug,
1562 "views/honkpage.html",
1563 "views/honkfrags.html",
1564 "views/honkers.html",
1565 "views/zonkers.html",
1566 "views/combos.html",
1567 "views/honkform.html",
1568 "views/honk.html",
1569 "views/account.html",
1570 "views/about.html",
1571 "views/funzone.html",
1572 "views/login.html",
1573 "views/xzone.html",
1574 "views/header.html",
1575 )
1576 if !debug {
1577 s := "views/style.css"
1578 savedstyleparams[s] = getstyleparam(s)
1579 s = "views/local.css"
1580 savedstyleparams[s] = getstyleparam(s)
1581 }
1582
1583 bitethethumbs()
1584
1585 mux := mux.NewRouter()
1586 mux.Use(login.Checker)
1587
1588 posters := mux.Methods("POST").Subrouter()
1589 getters := mux.Methods("GET").Subrouter()
1590
1591 getters.HandleFunc("/", homepage)
1592 getters.HandleFunc("/front", homepage)
1593 getters.HandleFunc("/robots.txt", nomoroboto)
1594 getters.HandleFunc("/rss", showrss)
1595 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1596 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1597 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1598 posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1599 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1600 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1601 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1602 getters.HandleFunc("/a", avatate)
1603 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1604 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1605 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1606 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1607
1608 getters.HandleFunc("/style.css", servecss)
1609 getters.HandleFunc("/local.css", servecss)
1610 getters.HandleFunc("/about", servehtml)
1611 getters.HandleFunc("/login", servehtml)
1612 posters.HandleFunc("/dologin", login.LoginFunc)
1613 getters.HandleFunc("/logout", login.LogoutFunc)
1614
1615 loggedin := mux.NewRoute().Subrouter()
1616 loggedin.Use(login.Required)
1617 loggedin.HandleFunc("/account", accountpage)
1618 loggedin.HandleFunc("/funzone", showfunzone)
1619 loggedin.HandleFunc("/chpass", dochpass)
1620 loggedin.HandleFunc("/atme", homepage)
1621 loggedin.HandleFunc("/zonkzone", zonkzone)
1622 loggedin.HandleFunc("/xzone", xzone)
1623 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1624 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1625 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1626 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1627 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1628 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1629 loggedin.HandleFunc("/honkers", showhonkers)
1630 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1631 loggedin.HandleFunc("/h", showhonker)
1632 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1633 loggedin.HandleFunc("/c", showcombos)
1634 loggedin.HandleFunc("/t", showconvoy)
1635 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1636
1637 err = http.Serve(listener, mux)
1638 if err != nil {
1639 log.Fatal(err)
1640 }
1641}
1642
1643func cleanupdb(arg string) {
1644 db := opendatabase()
1645 days, err := strconv.Atoi(arg)
1646 if err != nil {
1647 honker := arg
1648 expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1649 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1650 doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1651 } else {
1652 expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1653 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3))", expdate)
1654 doordie(db, "delete from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3)", expdate)
1655 }
1656 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1657 for _, u := range allusers() {
1658 doordie(db, "delete from zonkers where userid = ? and wherefore = 'zonvoy' and zonkerid < (select zonkerid from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 1 offset 200)", u.UserID, u.UserID)
1659 }
1660}
1661
1662var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1663var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1664var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1665var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1666var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1667var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1668var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1669var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1670var stmtUpdateFlags, stmtClearFlags *sql.Stmt
1671
1672func preparetodie(db *sql.DB, s string) *sql.Stmt {
1673 stmt, err := db.Prepare(s)
1674 if err != nil {
1675 log.Fatalf("error %s: %s", err, s)
1676 }
1677 return stmt
1678}
1679
1680func prepareStatements(db *sql.DB) {
1681 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name")
1682 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1683 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1684 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1685 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1686 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1687
1688 selecthonks := "select honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore, flags from honks join users on honks.userid = users.userid "
1689 limit := " order by honkid desc limit 250"
1690 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1691 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1692 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1693 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1694 stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and honker in (select xid from honkers where userid = ? and flavor = 'sub' and combos not like '% - %')"+butnotthose+limit)
1695 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1696 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1697 stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1698 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1699 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1700
1701 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1702 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1703 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1704 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1705 stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1706 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1707 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1708 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1709 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1710 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1711 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1712 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1713 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1714 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1715 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1716 stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1717 stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1718 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1719 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1720 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1721 stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1722 stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? and honker not in (select xid from honkers where userid = ? and flavor = 'sub') order by honkid desc limit 100")
1723 stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where xid = ? and userid = ?")
1724 stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where xid = ? and userid = ?")
1725}
1726
1727func ElaborateUnitTests() {
1728}
1729
1730func main() {
1731 cmd := "run"
1732 if len(os.Args) > 1 {
1733 cmd = os.Args[1]
1734 }
1735 switch cmd {
1736 case "init":
1737 initdb()
1738 case "upgrade":
1739 upgradedb()
1740 }
1741 db := opendatabase()
1742 dbversion := 0
1743 getconfig("dbversion", &dbversion)
1744 if dbversion != myVersion {
1745 log.Fatal("incorrect database version. run upgrade.")
1746 }
1747 getconfig("servermsg", &serverMsg)
1748 getconfig("servername", &serverName)
1749 getconfig("usersep", &userSep)
1750 getconfig("honksep", &honkSep)
1751 getconfig("dnf", &donotfedafterdark)
1752 prepareStatements(db)
1753 switch cmd {
1754 case "adduser":
1755 adduser()
1756 case "cleanup":
1757 arg := "30"
1758 if len(os.Args) > 2 {
1759 arg = os.Args[2]
1760 }
1761 cleanupdb(arg)
1762 case "ping":
1763 if len(os.Args) < 4 {
1764 fmt.Printf("usage: honk ping from to\n")
1765 return
1766 }
1767 name := os.Args[2]
1768 targ := os.Args[3]
1769 user, err := butwhatabout(name)
1770 if err != nil {
1771 log.Printf("unknown user")
1772 return
1773 }
1774 ping(user, targ)
1775 case "peep":
1776 peeppeep()
1777 case "run":
1778 serve()
1779 case "test":
1780 ElaborateUnitTests()
1781 default:
1782 log.Fatal("unknown command")
1783 }
1784}