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 j, err := GetJunk(xid)
427 if err != nil {
428 log.Printf("error getting external object: %s", err)
429 return
430 }
431 log.Printf("importing %s", xid)
432 u := login.GetUserInfo(r)
433 user, _ := butwhatabout(u.Username)
434
435 what, _ := j.GetString("type")
436 if what == "Person" {
437 outbox, _ := j.GetString("outbox")
438 gimmexonks(user, outbox)
439 http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
440 return
441 }
442 xonk := xonkxonk(user, j, originate(xid))
443 convoy := ""
444 if xonk != nil {
445 convoy = xonk.Convoy
446 savexonk(user, xonk)
447 }
448 http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
449}
450
451func xzone(w http.ResponseWriter, r *http.Request) {
452 u := login.GetUserInfo(r)
453 var honkers []string
454 rows, err := stmtRecentHonkers.Query(u.UserID)
455 if err != nil {
456 log.Printf("query err: %s", err)
457 return
458 }
459 defer rows.Close()
460 for rows.Next() {
461 var s string
462 rows.Scan(&s)
463 honkers = append(honkers, s)
464 }
465
466 templinfo := getInfo(r)
467 templinfo["XCSRF"] = login.GetCSRF("ximport", r)
468 templinfo["Honkers"] = honkers
469 err = readviews.Execute(w, "xzone.html", templinfo)
470 if err != nil {
471 log.Print(err)
472 }
473}
474
475func outbox(w http.ResponseWriter, r *http.Request) {
476 name := mux.Vars(r)["name"]
477 user, err := butwhatabout(name)
478 if err != nil {
479 http.NotFound(w, r)
480 return
481 }
482 if stealthed(r) {
483 http.NotFound(w, r)
484 return
485 }
486
487 honks := gethonksbyuser(name, false)
488
489 var jonks []map[string]interface{}
490 for _, h := range honks {
491 j, _ := jonkjonk(user, h)
492 jonks = append(jonks, j)
493 }
494
495 j := junk.New()
496 j["@context"] = itiswhatitis
497 j["id"] = user.URL + "/outbox"
498 j["type"] = "OrderedCollection"
499 j["totalItems"] = len(jonks)
500 j["orderedItems"] = jonks
501
502 w.Header().Set("Content-Type", theonetruename)
503 j.Write(w)
504}
505
506func emptiness(w http.ResponseWriter, r *http.Request) {
507 name := mux.Vars(r)["name"]
508 user, err := butwhatabout(name)
509 if err != nil {
510 http.NotFound(w, r)
511 return
512 }
513 colname := "/followers"
514 if strings.HasSuffix(r.URL.Path, "/following") {
515 colname = "/following"
516 }
517 j := junk.New()
518 j["@context"] = itiswhatitis
519 j["id"] = user.URL + colname
520 j["type"] = "OrderedCollection"
521 j["totalItems"] = 0
522 j["orderedItems"] = []interface{}{}
523
524 w.Header().Set("Content-Type", theonetruename)
525 j.Write(w)
526}
527
528func showuser(w http.ResponseWriter, r *http.Request) {
529 name := mux.Vars(r)["name"]
530 user, err := butwhatabout(name)
531 if err != nil {
532 http.NotFound(w, r)
533 return
534 }
535 if friendorfoe(r.Header.Get("Accept")) {
536 j := asjonker(user)
537 w.Header().Set("Content-Type", theonetruename)
538 j.Write(w)
539 return
540 }
541 u := login.GetUserInfo(r)
542 honks := gethonksbyuser(name, u != nil && u.Username == name)
543 honkpage(w, r, u, user, honks, "")
544}
545
546func showhonker(w http.ResponseWriter, r *http.Request) {
547 u := login.GetUserInfo(r)
548 name := mux.Vars(r)["name"]
549 var honks []*Honk
550 if name == "" {
551 name = r.FormValue("xid")
552 honks = gethonksbyxonker(u.UserID, name)
553 } else {
554 honks = gethonksbyhonker(u.UserID, name)
555 }
556 honkpage(w, r, u, nil, honks, "honks by honker: "+name)
557}
558
559func showcombo(w http.ResponseWriter, r *http.Request) {
560 name := mux.Vars(r)["name"]
561 u := login.GetUserInfo(r)
562 honks := gethonksbycombo(u.UserID, name)
563 honks = osmosis(honks, u.UserID)
564 honkpage(w, r, u, nil, honks, "honks by combo: "+name)
565}
566func showconvoy(w http.ResponseWriter, r *http.Request) {
567 c := r.FormValue("c")
568 var userid int64 = -1
569 u := login.GetUserInfo(r)
570 if u != nil {
571 userid = u.UserID
572 }
573 honks := gethonksbyconvoy(userid, c)
574 honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
575}
576
577func showhonk(w http.ResponseWriter, r *http.Request) {
578 name := mux.Vars(r)["name"]
579 user, err := butwhatabout(name)
580 if err != nil {
581 http.NotFound(w, r)
582 return
583 }
584 if stealthed(r) {
585 http.NotFound(w, r)
586 return
587 }
588
589 xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
590 h := getxonk(user.ID, xid)
591 if h == nil {
592 http.NotFound(w, r)
593 return
594 }
595 u := login.GetUserInfo(r)
596 if !h.Public {
597 if u == nil || u.UserID != h.UserID {
598 http.NotFound(w, r)
599 return
600
601 }
602 honkpage(w, r, u, nil, []*Honk{h}, "one honk maybe more")
603 return
604 }
605 if friendorfoe(r.Header.Get("Accept")) {
606 donksforhonks([]*Honk{h})
607 _, j := jonkjonk(user, h)
608 j["@context"] = itiswhatitis
609 w.Header().Set("Content-Type", theonetruename)
610 j.Write(w)
611 return
612 }
613 honks := gethonksbyconvoy(-1, h.Convoy)
614 honkpage(w, r, u, nil, honks, "one honk maybe more")
615}
616
617func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
618 honks []*Honk, infomsg string) {
619 reverbolate(honks)
620 templinfo := getInfo(r)
621 if u != nil {
622 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
623 }
624 if u == nil {
625 w.Header().Set("Cache-Control", "max-age=60")
626 }
627 if user != nil {
628 filt := htfilter.New()
629 templinfo["Name"] = user.Name
630 whatabout := user.About
631 whatabout = obfusbreak(user.About)
632 templinfo["WhatAbout"], _ = filt.String(whatabout)
633 }
634 templinfo["Honks"] = honks
635 templinfo["ServerMessage"] = infomsg
636 err := readviews.Execute(w, "honkpage.html", templinfo)
637 if err != nil {
638 log.Print(err)
639 }
640}
641
642func saveuser(w http.ResponseWriter, r *http.Request) {
643 whatabout := r.FormValue("whatabout")
644 u := login.GetUserInfo(r)
645 db := opendatabase()
646 options := ""
647 if r.FormValue("skinny") == "skinny" {
648 options += " skinny "
649 }
650 _, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
651 if err != nil {
652 log.Printf("error bouting what: %s", err)
653 }
654
655 http.Redirect(w, r, "/account", http.StatusSeeOther)
656}
657
658func gethonkers(userid int64) []*Honker {
659 rows, err := stmtHonkers.Query(userid)
660 if err != nil {
661 log.Printf("error querying honkers: %s", err)
662 return nil
663 }
664 defer rows.Close()
665 var honkers []*Honker
666 for rows.Next() {
667 var f Honker
668 var combos string
669 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
670 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
671 if err != nil {
672 log.Printf("error scanning honker: %s", err)
673 return nil
674 }
675 honkers = append(honkers, &f)
676 }
677 return honkers
678}
679
680func getdubs(userid int64) []*Honker {
681 rows, err := stmtDubbers.Query(userid)
682 if err != nil {
683 log.Printf("error querying dubs: %s", err)
684 return nil
685 }
686 defer rows.Close()
687 var honkers []*Honker
688 for rows.Next() {
689 var f Honker
690 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
691 if err != nil {
692 log.Printf("error scanning honker: %s", err)
693 return nil
694 }
695 honkers = append(honkers, &f)
696 }
697 return honkers
698}
699
700func allusers() []login.UserInfo {
701 var users []login.UserInfo
702 rows, _ := opendatabase().Query("select userid, username from users")
703 defer rows.Close()
704 for rows.Next() {
705 var u login.UserInfo
706 rows.Scan(&u.UserID, &u.Username)
707 users = append(users, u)
708 }
709 return users
710}
711
712func getxonk(userid int64, xid string) *Honk {
713 h := new(Honk)
714 var dt, aud string
715 row := stmtOneXonk.QueryRow(userid, xid)
716 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
717 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
718 if err != nil {
719 if err != sql.ErrNoRows {
720 log.Printf("error scanning xonk: %s", err)
721 }
722 return nil
723 }
724 h.Date, _ = time.Parse(dbtimeformat, dt)
725 h.Audience = strings.Split(aud, " ")
726 h.Public = !keepitquiet(h.Audience)
727 return h
728}
729
730func getpublichonks() []*Honk {
731 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
732 rows, err := stmtPublicHonks.Query(dt)
733 return getsomehonks(rows, err)
734}
735func gethonksbyuser(name string, includeprivate bool) []*Honk {
736 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
737 whofore := 2
738 if includeprivate {
739 whofore = 3
740 }
741 rows, err := stmtUserHonks.Query(whofore, name, dt)
742 return getsomehonks(rows, err)
743}
744func gethonksforuser(userid int64) []*Honk {
745 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
746 rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
747 return getsomehonks(rows, err)
748}
749func gethonksforme(userid int64) []*Honk {
750 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
751 rows, err := stmtHonksForMe.Query(userid, dt, userid)
752 return getsomehonks(rows, err)
753}
754func gethonksbyhonker(userid int64, honker string) []*Honk {
755 rows, err := stmtHonksByHonker.Query(userid, honker, userid)
756 return getsomehonks(rows, err)
757}
758func gethonksbyxonker(userid int64, xonker string) []*Honk {
759 rows, err := stmtHonksByXonker.Query(userid, xonker, userid)
760 return getsomehonks(rows, err)
761}
762func gethonksbycombo(userid int64, combo string) []*Honk {
763 combo = "% " + combo + " %"
764 rows, err := stmtHonksByCombo.Query(userid, combo, userid)
765 return getsomehonks(rows, err)
766}
767func gethonksbyconvoy(userid int64, convoy string) []*Honk {
768 rows, err := stmtHonksByConvoy.Query(userid, convoy)
769 honks := getsomehonks(rows, err)
770 for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
771 honks[i], honks[j] = honks[j], honks[i]
772 }
773 return honks
774}
775
776func getsomehonks(rows *sql.Rows, err error) []*Honk {
777 if err != nil {
778 log.Printf("error querying honks: %s", err)
779 return nil
780 }
781 defer rows.Close()
782 var honks []*Honk
783 for rows.Next() {
784 var h Honk
785 var dt, aud string
786 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
787 &h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
788 if err != nil {
789 log.Printf("error scanning honks: %s", err)
790 return nil
791 }
792 h.Date, _ = time.Parse(dbtimeformat, dt)
793 h.Audience = strings.Split(aud, " ")
794 h.Public = !keepitquiet(h.Audience)
795 honks = append(honks, &h)
796 }
797 rows.Close()
798 donksforhonks(honks)
799 return honks
800}
801
802func donksforhonks(honks []*Honk) {
803 db := opendatabase()
804 var ids []string
805 hmap := make(map[int64]*Honk)
806 for _, h := range honks {
807 ids = append(ids, fmt.Sprintf("%d", h.ID))
808 hmap[h.ID] = h
809 }
810 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, ","))
811 rows, err := db.Query(q)
812 if err != nil {
813 log.Printf("error querying donks: %s", err)
814 return
815 }
816 defer rows.Close()
817 for rows.Next() {
818 var hid int64
819 var d Donk
820 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
821 if err != nil {
822 log.Printf("error scanning donk: %s", err)
823 continue
824 }
825 h := hmap[hid]
826 h.Donks = append(h.Donks, &d)
827 }
828}
829
830func savebonk(w http.ResponseWriter, r *http.Request) {
831 xid := r.FormValue("xid")
832 userinfo := login.GetUserInfo(r)
833 user, _ := butwhatabout(userinfo.Username)
834
835 log.Printf("bonking %s", xid)
836
837 xonk := getxonk(userinfo.UserID, xid)
838 if xonk == nil {
839 return
840 }
841 if !xonk.Public {
842 return
843 }
844 donksforhonks([]*Honk{xonk})
845
846 oonker := xonk.Oonker
847 if oonker == "" {
848 oonker = xonk.Honker
849 }
850 dt := time.Now().UTC()
851 bonk := Honk{
852 UserID: userinfo.UserID,
853 Username: userinfo.Username,
854 What: "bonk",
855 Honker: user.URL,
856 XID: xonk.XID,
857 Date: dt,
858 Donks: xonk.Donks,
859 Convoy: xonk.Convoy,
860 Audience: []string{oonker, thewholeworld},
861 Public: true,
862 }
863
864 aud := strings.Join(bonk.Audience, " ")
865 whofore := 2
866 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
867 dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
868 xonk.Precis, oonker)
869 if err != nil {
870 log.Printf("error saving bonk: %s", err)
871 return
872 }
873 bonk.ID, _ = res.LastInsertId()
874 for _, d := range bonk.Donks {
875 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
876 if err != nil {
877 log.Printf("err saving donk: %s", err)
878 return
879 }
880 }
881
882 go honkworldwide(user, &bonk)
883}
884
885func zonkit(w http.ResponseWriter, r *http.Request) {
886 wherefore := r.FormValue("wherefore")
887 what := r.FormValue("what")
888 switch wherefore {
889 case "zonk":
890 case "zonvoy":
891 }
892
893 log.Printf("zonking %s %s", wherefore, what)
894 userinfo := login.GetUserInfo(r)
895 if wherefore == "zonk" {
896 xonk := getxonk(userinfo.UserID, what)
897 if xonk != nil {
898 stmtZonkDonks.Exec(xonk.ID)
899 stmtZonkIt.Exec(userinfo.UserID, what)
900 if xonk.Whofore == 2 || xonk.Whofore == 3 {
901 zonk := Honk{
902 What: "zonk",
903 XID: xonk.XID,
904 Date: time.Now().UTC(),
905 Audience: oneofakind(xonk.Audience),
906 }
907
908 user, _ := butwhatabout(userinfo.Username)
909 log.Printf("announcing deleted honk: %s", what)
910 go honkworldwide(user, &zonk)
911 }
912 }
913 }
914 _, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
915 if err != nil {
916 log.Printf("error saving zonker: %s", err)
917 return
918 }
919}
920
921func savehonk(w http.ResponseWriter, r *http.Request) {
922 rid := r.FormValue("rid")
923 noise := r.FormValue("noise")
924
925 userinfo := login.GetUserInfo(r)
926 user, _ := butwhatabout(userinfo.Username)
927
928 dt := time.Now().UTC()
929 xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
930 what := "honk"
931 if rid != "" {
932 what = "tonk"
933 }
934 honk := Honk{
935 UserID: userinfo.UserID,
936 Username: userinfo.Username,
937 What: "honk",
938 Honker: user.URL,
939 XID: xid,
940 Date: dt,
941 }
942 if strings.HasPrefix(noise, "DZ:") {
943 idx := strings.Index(noise, "\n")
944 if idx == -1 {
945 honk.Precis = noise
946 noise = ""
947 } else {
948 honk.Precis = noise[:idx]
949 noise = noise[idx+1:]
950 }
951 }
952 noise = hooterize(noise)
953 noise = strings.TrimSpace(noise)
954 honk.Precis = strings.TrimSpace(honk.Precis)
955
956 var convoy string
957 if rid != "" {
958 xonk := getxonk(userinfo.UserID, rid)
959 if xonk != nil {
960 if xonk.Public {
961 honk.Audience = append(honk.Audience, xonk.Audience...)
962 }
963 convoy = xonk.Convoy
964 } else {
965 xonkaud, c := whosthere(rid)
966 honk.Audience = append(honk.Audience, xonkaud...)
967 convoy = c
968 }
969 for i, a := range honk.Audience {
970 if a == thewholeworld {
971 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
972 break
973 }
974 }
975 honk.RID = rid
976 } else {
977 honk.Audience = []string{thewholeworld}
978 }
979 if noise != "" && noise[0] == '@' {
980 honk.Audience = append(grapevine(noise), honk.Audience...)
981 } else {
982 honk.Audience = append(honk.Audience, grapevine(noise)...)
983 }
984 if convoy == "" {
985 convoy = "data:,electrichonkytonk-" + xfiltrate()
986 }
987 butnottooloud(honk.Audience)
988 honk.Audience = oneofakind(honk.Audience)
989 if len(honk.Audience) == 0 {
990 log.Printf("honk to nowhere")
991 http.Error(w, "honk to nowhere...", http.StatusNotFound)
992 return
993 }
994 honk.Public = !keepitquiet(honk.Audience)
995 noise = obfusbreak(noise)
996 honk.Noise = noise
997 honk.Convoy = convoy
998
999 file, filehdr, err := r.FormFile("donk")
1000 if err == nil {
1001 var buf bytes.Buffer
1002 io.Copy(&buf, file)
1003 file.Close()
1004 data := buf.Bytes()
1005 xid := xfiltrate()
1006 var media, name string
1007 img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1008 if err == nil {
1009 data = img.Data
1010 format := img.Format
1011 media = "image/" + format
1012 if format == "jpeg" {
1013 format = "jpg"
1014 }
1015 name = xid + "." + format
1016 xid = name
1017 } else {
1018 maxsize := 100000
1019 if len(data) > maxsize {
1020 log.Printf("bad image: %s too much text: %d", err, len(data))
1021 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1022 return
1023 }
1024 for i := 0; i < len(data); i++ {
1025 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1026 log.Printf("bad image: %s not text: %d", err, data[i])
1027 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1028 return
1029 }
1030 }
1031 media = "text/plain"
1032 name = filehdr.Filename
1033 if name == "" {
1034 name = xid + ".txt"
1035 }
1036 xid += ".txt"
1037 }
1038 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1039 res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1040 if err != nil {
1041 log.Printf("unable to save image: %s", err)
1042 return
1043 }
1044 var d Donk
1045 d.FileID, _ = res.LastInsertId()
1046 d.XID = name
1047 d.Name = name
1048 d.Media = media
1049 d.URL = url
1050 d.Local = true
1051 honk.Donks = append(honk.Donks, &d)
1052 }
1053 herd := herdofemus(honk.Noise)
1054 for _, e := range herd {
1055 donk := savedonk(e.ID, e.Name, "image/png", true)
1056 if donk != nil {
1057 donk.Name = e.Name
1058 honk.Donks = append(honk.Donks, donk)
1059 }
1060 }
1061 memetize(&honk)
1062
1063 aud := strings.Join(honk.Audience, " ")
1064 whofore := 2
1065 if !honk.Public {
1066 whofore = 3
1067 }
1068 if r.FormValue("preview") == "preview" {
1069 honks := []*Honk{&honk}
1070 reverbolate(honks)
1071 templinfo := getInfo(r)
1072 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1073 templinfo["Honks"] = honks
1074 templinfo["Noise"] = r.FormValue("noise")
1075 templinfo["ServerMessage"] = "honk preview"
1076 err := readviews.Execute(w, "honkpage.html", templinfo)
1077 if err != nil {
1078 log.Print(err)
1079 }
1080 return
1081 }
1082 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1083 dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1084 if err != nil {
1085 log.Printf("error saving honk: %s", err)
1086 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1087 return
1088 }
1089 honk.ID, _ = res.LastInsertId()
1090 for _, d := range honk.Donks {
1091 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1092 if err != nil {
1093 log.Printf("err saving donk: %s", err)
1094 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1095 return
1096 }
1097 }
1098
1099 go honkworldwide(user, &honk)
1100
1101 http.Redirect(w, r, xid, http.StatusSeeOther)
1102}
1103
1104func showhonkers(w http.ResponseWriter, r *http.Request) {
1105 userinfo := login.GetUserInfo(r)
1106 templinfo := getInfo(r)
1107 templinfo["Honkers"] = gethonkers(userinfo.UserID)
1108 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1109 err := readviews.Execute(w, "honkers.html", templinfo)
1110 if err != nil {
1111 log.Print(err)
1112 }
1113}
1114
1115func showcombos(w http.ResponseWriter, r *http.Request) {
1116 userinfo := login.GetUserInfo(r)
1117 templinfo := getInfo(r)
1118 honkers := gethonkers(userinfo.UserID)
1119 var combos []string
1120 for _, h := range honkers {
1121 combos = append(combos, h.Combos...)
1122 }
1123 for i, c := range combos {
1124 if c == "-" {
1125 combos[i] = ""
1126 }
1127 }
1128 combos = oneofakind(combos)
1129 sort.Strings(combos)
1130 templinfo["Combos"] = combos
1131 err := readviews.Execute(w, "combos.html", templinfo)
1132 if err != nil {
1133 log.Print(err)
1134 }
1135}
1136
1137func savehonker(w http.ResponseWriter, r *http.Request) {
1138 u := login.GetUserInfo(r)
1139 name := r.FormValue("name")
1140 url := r.FormValue("url")
1141 peep := r.FormValue("peep")
1142 combos := r.FormValue("combos")
1143 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1144
1145 if honkerid > 0 {
1146 goodbye := r.FormValue("goodbye")
1147 if goodbye == "F" {
1148 db := opendatabase()
1149 row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1150 honkerid, u.UserID)
1151 var xid string
1152 err := row.Scan(&xid)
1153 if err != nil {
1154 log.Printf("can't get honker xid: %s", err)
1155 return
1156 }
1157 log.Printf("unsubscribing from %s", xid)
1158 user, _ := butwhatabout(u.Username)
1159 go itakeitallback(user, xid)
1160 _, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1161 if err != nil {
1162 log.Printf("error updating honker: %s", err)
1163 return
1164 }
1165
1166 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1167 return
1168 }
1169 combos = " " + strings.TrimSpace(combos) + " "
1170 _, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1171 if err != nil {
1172 log.Printf("update honker err: %s", err)
1173 return
1174 }
1175 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1176 }
1177
1178 flavor := "presub"
1179 if peep == "peep" {
1180 flavor = "peep"
1181 }
1182 url = investigate(url)
1183 if url == "" {
1184 return
1185 }
1186 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1187 if err != nil {
1188 log.Print(err)
1189 return
1190 }
1191 if flavor == "presub" {
1192 user, _ := butwhatabout(u.Username)
1193 go subsub(user, url)
1194 }
1195 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1196}
1197
1198type Zonker struct {
1199 ID int64
1200 Name string
1201 Wherefore string
1202}
1203
1204func zonkzone(w http.ResponseWriter, r *http.Request) {
1205 userinfo := login.GetUserInfo(r)
1206 rows, err := stmtGetZonkers.Query(userinfo.UserID)
1207 if err != nil {
1208 log.Printf("err: %s", err)
1209 return
1210 }
1211 defer rows.Close()
1212 var zonkers []Zonker
1213 for rows.Next() {
1214 var z Zonker
1215 rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1216 zonkers = append(zonkers, z)
1217 }
1218 sort.Slice(zonkers, func(i, j int) bool {
1219 w1 := zonkers[i].Wherefore
1220 w2 := zonkers[j].Wherefore
1221 if w1 == w2 {
1222 return zonkers[i].Name < zonkers[j].Name
1223 }
1224 if w1 == "zonvoy" {
1225 w1 = "zzzzzzz"
1226 }
1227 if w2 == "zonvoy" {
1228 w2 = "zzzzzzz"
1229 }
1230 return w1 < w2
1231 })
1232
1233 templinfo := getInfo(r)
1234 templinfo["Zonkers"] = zonkers
1235 templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1236 err = readviews.Execute(w, "zonkers.html", templinfo)
1237 if err != nil {
1238 log.Print(err)
1239 }
1240}
1241
1242func zonkzonk(w http.ResponseWriter, r *http.Request) {
1243 userinfo := login.GetUserInfo(r)
1244 itsok := r.FormValue("itsok")
1245 if itsok == "iforgiveyou" {
1246 zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1247 db := opendatabase()
1248 db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1249 userinfo.UserID, zonkerid)
1250 bitethethumbs()
1251 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1252 return
1253 }
1254 wherefore := r.FormValue("wherefore")
1255 name := r.FormValue("name")
1256 if name == "" {
1257 return
1258 }
1259 switch wherefore {
1260 case "zonker":
1261 case "zomain":
1262 case "zonvoy":
1263 case "zord":
1264 default:
1265 return
1266 }
1267 db := opendatabase()
1268 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1269 userinfo.UserID, name, wherefore)
1270 if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" {
1271 bitethethumbs()
1272 }
1273
1274 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1275}
1276
1277func accountpage(w http.ResponseWriter, r *http.Request) {
1278 u := login.GetUserInfo(r)
1279 user, _ := butwhatabout(u.Username)
1280 templinfo := getInfo(r)
1281 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1282 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1283 templinfo["User"] = user
1284 err := readviews.Execute(w, "account.html", templinfo)
1285 if err != nil {
1286 log.Print(err)
1287 }
1288}
1289
1290func dochpass(w http.ResponseWriter, r *http.Request) {
1291 err := login.ChangePassword(w, r)
1292 if err != nil {
1293 log.Printf("error changing password: %s", err)
1294 }
1295 http.Redirect(w, r, "/account", http.StatusSeeOther)
1296}
1297
1298func fingerlicker(w http.ResponseWriter, r *http.Request) {
1299 orig := r.FormValue("resource")
1300
1301 log.Printf("finger lick: %s", orig)
1302
1303 if strings.HasPrefix(orig, "acct:") {
1304 orig = orig[5:]
1305 }
1306
1307 name := orig
1308 idx := strings.LastIndexByte(name, '/')
1309 if idx != -1 {
1310 name = name[idx+1:]
1311 if "https://"+serverName+"/u/"+name != orig {
1312 log.Printf("foreign request rejected")
1313 name = ""
1314 }
1315 } else {
1316 idx = strings.IndexByte(name, '@')
1317 if idx != -1 {
1318 name = name[:idx]
1319 if name+"@"+serverName != orig {
1320 log.Printf("foreign request rejected")
1321 name = ""
1322 }
1323 }
1324 }
1325 user, err := butwhatabout(name)
1326 if err != nil {
1327 http.NotFound(w, r)
1328 return
1329 }
1330
1331 j := junk.New()
1332 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1333 j["aliases"] = []string{user.URL}
1334 var links []map[string]interface{}
1335 l := junk.New()
1336 l["rel"] = "self"
1337 l["type"] = `application/activity+json`
1338 l["href"] = user.URL
1339 links = append(links, l)
1340 j["links"] = links
1341
1342 w.Header().Set("Cache-Control", "max-age=3600")
1343 w.Header().Set("Content-Type", "application/jrd+json")
1344 j.Write(w)
1345}
1346
1347func somedays() string {
1348 secs := 432000 + notrand.Int63n(432000)
1349 return fmt.Sprintf("%d", secs)
1350}
1351
1352func avatate(w http.ResponseWriter, r *http.Request) {
1353 n := r.FormValue("a")
1354 a := avatar(n)
1355 w.Header().Set("Cache-Control", "max-age="+somedays())
1356 w.Write(a)
1357}
1358
1359func servecss(w http.ResponseWriter, r *http.Request) {
1360 w.Header().Set("Cache-Control", "max-age=7776000")
1361 http.ServeFile(w, r, "views"+r.URL.Path)
1362}
1363func servehtml(w http.ResponseWriter, r *http.Request) {
1364 templinfo := getInfo(r)
1365 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1366 if err != nil {
1367 log.Print(err)
1368 }
1369}
1370func serveemu(w http.ResponseWriter, r *http.Request) {
1371 xid := mux.Vars(r)["xid"]
1372 w.Header().Set("Cache-Control", "max-age="+somedays())
1373 http.ServeFile(w, r, "emus/"+xid)
1374}
1375func servememe(w http.ResponseWriter, r *http.Request) {
1376 xid := mux.Vars(r)["xid"]
1377 w.Header().Set("Cache-Control", "max-age="+somedays())
1378 http.ServeFile(w, r, "memes/"+xid)
1379}
1380
1381func servefile(w http.ResponseWriter, r *http.Request) {
1382 xid := mux.Vars(r)["xid"]
1383 row := stmtFileData.QueryRow(xid)
1384 var media string
1385 var data []byte
1386 err := row.Scan(&media, &data)
1387 if err != nil {
1388 log.Printf("error loading file: %s", err)
1389 http.NotFound(w, r)
1390 return
1391 }
1392 w.Header().Set("Content-Type", media)
1393 w.Header().Set("X-Content-Type-Options", "nosniff")
1394 w.Header().Set("Cache-Control", "max-age="+somedays())
1395 w.Write(data)
1396}
1397
1398func nomoroboto(w http.ResponseWriter, r *http.Request) {
1399 io.WriteString(w, "User-agent: *\n")
1400 io.WriteString(w, "Disallow: /t\n")
1401 for _, u := range allusers() {
1402 fmt.Fprintf(w, "Disallow: /u/%s/h/\n", u.Username)
1403 }
1404}
1405
1406func serve() {
1407 db := opendatabase()
1408 login.Init(db)
1409
1410 listener, err := openListener()
1411 if err != nil {
1412 log.Fatal(err)
1413 }
1414 go redeliverator()
1415
1416 debug := false
1417 getconfig("debug", &debug)
1418 readviews = templates.Load(debug,
1419 "views/honkpage.html",
1420 "views/honkers.html",
1421 "views/zonkers.html",
1422 "views/combos.html",
1423 "views/honkform.html",
1424 "views/honk.html",
1425 "views/account.html",
1426 "views/about.html",
1427 "views/funzone.html",
1428 "views/login.html",
1429 "views/xzone.html",
1430 "views/header.html",
1431 )
1432 if !debug {
1433 s := "views/style.css"
1434 savedstyleparams[s] = getstyleparam(s)
1435 s = "views/local.css"
1436 savedstyleparams[s] = getstyleparam(s)
1437 }
1438
1439 bitethethumbs()
1440
1441 mux := mux.NewRouter()
1442 mux.Use(login.Checker)
1443
1444 posters := mux.Methods("POST").Subrouter()
1445 getters := mux.Methods("GET").Subrouter()
1446
1447 getters.HandleFunc("/", homepage)
1448 getters.HandleFunc("/front", homepage)
1449 getters.HandleFunc("/robots.txt", nomoroboto)
1450 getters.HandleFunc("/rss", showrss)
1451 getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1452 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1453 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1454 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1455 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1456 getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1457 getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1458 getters.HandleFunc("/a", avatate)
1459 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1460 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1461 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1462 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1463
1464 getters.HandleFunc("/style.css", servecss)
1465 getters.HandleFunc("/local.css", servecss)
1466 getters.HandleFunc("/about", servehtml)
1467 getters.HandleFunc("/login", servehtml)
1468 posters.HandleFunc("/dologin", login.LoginFunc)
1469 getters.HandleFunc("/logout", login.LogoutFunc)
1470
1471 loggedin := mux.NewRoute().Subrouter()
1472 loggedin.Use(login.Required)
1473 loggedin.HandleFunc("/account", accountpage)
1474 loggedin.HandleFunc("/funzone", showfunzone)
1475 loggedin.HandleFunc("/chpass", dochpass)
1476 loggedin.HandleFunc("/atme", homepage)
1477 loggedin.HandleFunc("/zonkzone", zonkzone)
1478 loggedin.HandleFunc("/xzone", xzone)
1479 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1480 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1481 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1482 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1483 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1484 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1485 loggedin.HandleFunc("/honkers", showhonkers)
1486 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1487 loggedin.HandleFunc("/h", showhonker)
1488 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1489 loggedin.HandleFunc("/c", showcombos)
1490 loggedin.HandleFunc("/t", showconvoy)
1491 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1492
1493 err = http.Serve(listener, mux)
1494 if err != nil {
1495 log.Fatal(err)
1496 }
1497}
1498
1499func cleanupdb(days int) {
1500 db := opendatabase()
1501 expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1502 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)
1503 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)
1504 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1505}
1506
1507func reducedb(honker string) {
1508 db := opendatabase()
1509 expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1510 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1511 doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1512 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1513}
1514
1515var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1516var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1517var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1518var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1519var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1520var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1521var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1522var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1523
1524func preparetodie(db *sql.DB, s string) *sql.Stmt {
1525 stmt, err := db.Prepare(s)
1526 if err != nil {
1527 log.Fatalf("error %s: %s", err, s)
1528 }
1529 return stmt
1530}
1531
1532func prepareStatements(db *sql.DB) {
1533 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")
1534 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1535 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1536 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1537 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1538 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1539
1540 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 "
1541 limit := " order by honkid desc limit 250"
1542 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1543 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1544 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1545 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1546 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)
1547 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1548 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1549 stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and honker = ?"+butnotthose+limit)
1550 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1551 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1552
1553 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1554 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1555 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1556 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1557 stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1558 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1559 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1560 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1561 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1562 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1563 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1564 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1565 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1566 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1567 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord')")
1568 stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1569 stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1570 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1571 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1572 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1573 stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1574 stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1575}
1576
1577func ElaborateUnitTests() {
1578}
1579
1580func main() {
1581 var err error
1582 cmd := "run"
1583 if len(os.Args) > 1 {
1584 cmd = os.Args[1]
1585 }
1586 switch cmd {
1587 case "init":
1588 initdb()
1589 case "upgrade":
1590 upgradedb()
1591 }
1592 db := opendatabase()
1593 dbversion := 0
1594 getconfig("dbversion", &dbversion)
1595 if dbversion != myVersion {
1596 log.Fatal("incorrect database version. run upgrade.")
1597 }
1598 getconfig("servermsg", &serverMsg)
1599 getconfig("servername", &serverName)
1600 getconfig("dnf", &donotfedafterdark)
1601 prepareStatements(db)
1602 switch cmd {
1603 case "adduser":
1604 adduser()
1605 case "cleanup":
1606 days := 30
1607 if len(os.Args) > 2 {
1608 days, err = strconv.Atoi(os.Args[2])
1609 if err != nil {
1610 log.Fatal(err)
1611 }
1612 }
1613 cleanupdb(days)
1614 case "reduce":
1615 if len(os.Args) < 3 {
1616 log.Fatal("need a honker name")
1617 }
1618 reducedb(os.Args[2])
1619 case "ping":
1620 if len(os.Args) < 4 {
1621 fmt.Printf("usage: honk ping from to\n")
1622 return
1623 }
1624 name := os.Args[2]
1625 targ := os.Args[3]
1626 user, err := butwhatabout(name)
1627 if err != nil {
1628 log.Printf("unknown user")
1629 return
1630 }
1631 ping(user, targ)
1632 case "peep":
1633 peeppeep()
1634 case "run":
1635 serve()
1636 case "test":
1637 ElaborateUnitTests()
1638 default:
1639 log.Fatal("unknown command")
1640 }
1641}