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