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