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 case "zilence":
1274 default:
1275 return
1276 }
1277 db := opendatabase()
1278 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1279 userinfo.UserID, name, wherefore)
1280 if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1281 bitethethumbs()
1282 }
1283
1284 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1285}
1286
1287func accountpage(w http.ResponseWriter, r *http.Request) {
1288 u := login.GetUserInfo(r)
1289 user, _ := butwhatabout(u.Username)
1290 templinfo := getInfo(r)
1291 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1292 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1293 templinfo["User"] = user
1294 err := readviews.Execute(w, "account.html", templinfo)
1295 if err != nil {
1296 log.Print(err)
1297 }
1298}
1299
1300func dochpass(w http.ResponseWriter, r *http.Request) {
1301 err := login.ChangePassword(w, r)
1302 if err != nil {
1303 log.Printf("error changing password: %s", err)
1304 }
1305 http.Redirect(w, r, "/account", http.StatusSeeOther)
1306}
1307
1308func fingerlicker(w http.ResponseWriter, r *http.Request) {
1309 orig := r.FormValue("resource")
1310
1311 log.Printf("finger lick: %s", orig)
1312
1313 if strings.HasPrefix(orig, "acct:") {
1314 orig = orig[5:]
1315 }
1316
1317 name := orig
1318 idx := strings.LastIndexByte(name, '/')
1319 if idx != -1 {
1320 name = name[idx+1:]
1321 if "https://"+serverName+"/u/"+name != orig {
1322 log.Printf("foreign request rejected")
1323 name = ""
1324 }
1325 } else {
1326 idx = strings.IndexByte(name, '@')
1327 if idx != -1 {
1328 name = name[:idx]
1329 if name+"@"+serverName != orig {
1330 log.Printf("foreign request rejected")
1331 name = ""
1332 }
1333 }
1334 }
1335 user, err := butwhatabout(name)
1336 if err != nil {
1337 http.NotFound(w, r)
1338 return
1339 }
1340
1341 j := junk.New()
1342 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1343 j["aliases"] = []string{user.URL}
1344 var links []map[string]interface{}
1345 l := junk.New()
1346 l["rel"] = "self"
1347 l["type"] = `application/activity+json`
1348 l["href"] = user.URL
1349 links = append(links, l)
1350 j["links"] = links
1351
1352 w.Header().Set("Cache-Control", "max-age=3600")
1353 w.Header().Set("Content-Type", "application/jrd+json")
1354 j.Write(w)
1355}
1356
1357func somedays() string {
1358 secs := 432000 + notrand.Int63n(432000)
1359 return fmt.Sprintf("%d", secs)
1360}
1361
1362func avatate(w http.ResponseWriter, r *http.Request) {
1363 n := r.FormValue("a")
1364 a := avatar(n)
1365 w.Header().Set("Cache-Control", "max-age="+somedays())
1366 w.Write(a)
1367}
1368
1369func servecss(w http.ResponseWriter, r *http.Request) {
1370 w.Header().Set("Cache-Control", "max-age=7776000")
1371 http.ServeFile(w, r, "views"+r.URL.Path)
1372}
1373func servehtml(w http.ResponseWriter, r *http.Request) {
1374 templinfo := getInfo(r)
1375 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1376 if err != nil {
1377 log.Print(err)
1378 }
1379}
1380func serveemu(w http.ResponseWriter, r *http.Request) {
1381 xid := mux.Vars(r)["xid"]
1382 w.Header().Set("Cache-Control", "max-age="+somedays())
1383 http.ServeFile(w, r, "emus/"+xid)
1384}
1385func servememe(w http.ResponseWriter, r *http.Request) {
1386 xid := mux.Vars(r)["xid"]
1387 w.Header().Set("Cache-Control", "max-age="+somedays())
1388 http.ServeFile(w, r, "memes/"+xid)
1389}
1390
1391func servefile(w http.ResponseWriter, r *http.Request) {
1392 xid := mux.Vars(r)["xid"]
1393 row := stmtFileData.QueryRow(xid)
1394 var media string
1395 var data []byte
1396 err := row.Scan(&media, &data)
1397 if err != nil {
1398 log.Printf("error loading file: %s", err)
1399 http.NotFound(w, r)
1400 return
1401 }
1402 w.Header().Set("Content-Type", media)
1403 w.Header().Set("X-Content-Type-Options", "nosniff")
1404 w.Header().Set("Cache-Control", "max-age="+somedays())
1405 w.Write(data)
1406}
1407
1408func nomoroboto(w http.ResponseWriter, r *http.Request) {
1409 io.WriteString(w, "User-agent: *\n")
1410 io.WriteString(w, "Disallow: /t\n")
1411 for _, u := range allusers() {
1412 fmt.Fprintf(w, "Disallow: /u/%s/h/\n", u.Username)
1413 }
1414}
1415
1416func serve() {
1417 db := opendatabase()
1418 login.Init(db)
1419
1420 listener, err := openListener()
1421 if err != nil {
1422 log.Fatal(err)
1423 }
1424 go redeliverator()
1425
1426 debug := false
1427 getconfig("debug", &debug)
1428 readviews = templates.Load(debug,
1429 "views/honkpage.html",
1430 "views/honkers.html",
1431 "views/zonkers.html",
1432 "views/combos.html",
1433 "views/honkform.html",
1434 "views/honk.html",
1435 "views/account.html",
1436 "views/about.html",
1437 "views/funzone.html",
1438 "views/login.html",
1439 "views/xzone.html",
1440 "views/header.html",
1441 )
1442 if !debug {
1443 s := "views/style.css"
1444 savedstyleparams[s] = getstyleparam(s)
1445 s = "views/local.css"
1446 savedstyleparams[s] = getstyleparam(s)
1447 }
1448
1449 bitethethumbs()
1450
1451 mux := mux.NewRouter()
1452 mux.Use(login.Checker)
1453
1454 posters := mux.Methods("POST").Subrouter()
1455 getters := mux.Methods("GET").Subrouter()
1456
1457 getters.HandleFunc("/", homepage)
1458 getters.HandleFunc("/front", homepage)
1459 getters.HandleFunc("/robots.txt", nomoroboto)
1460 getters.HandleFunc("/rss", showrss)
1461 getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1462 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1463 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1464 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1465 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1466 getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1467 getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1468 getters.HandleFunc("/a", avatate)
1469 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1470 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1471 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1472 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1473
1474 getters.HandleFunc("/style.css", servecss)
1475 getters.HandleFunc("/local.css", servecss)
1476 getters.HandleFunc("/about", servehtml)
1477 getters.HandleFunc("/login", servehtml)
1478 posters.HandleFunc("/dologin", login.LoginFunc)
1479 getters.HandleFunc("/logout", login.LogoutFunc)
1480
1481 loggedin := mux.NewRoute().Subrouter()
1482 loggedin.Use(login.Required)
1483 loggedin.HandleFunc("/account", accountpage)
1484 loggedin.HandleFunc("/funzone", showfunzone)
1485 loggedin.HandleFunc("/chpass", dochpass)
1486 loggedin.HandleFunc("/atme", homepage)
1487 loggedin.HandleFunc("/zonkzone", zonkzone)
1488 loggedin.HandleFunc("/xzone", xzone)
1489 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1490 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1491 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1492 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1493 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1494 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1495 loggedin.HandleFunc("/honkers", showhonkers)
1496 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1497 loggedin.HandleFunc("/h", showhonker)
1498 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1499 loggedin.HandleFunc("/c", showcombos)
1500 loggedin.HandleFunc("/t", showconvoy)
1501 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1502
1503 err = http.Serve(listener, mux)
1504 if err != nil {
1505 log.Fatal(err)
1506 }
1507}
1508
1509func cleanupdb(days int) {
1510 db := opendatabase()
1511 expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1512 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)
1513 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)
1514 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1515}
1516
1517func reducedb(honker string) {
1518 db := opendatabase()
1519 expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1520 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1521 doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1522 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1523}
1524
1525var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1526var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1527var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1528var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1529var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1530var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1531var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1532var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1533
1534func preparetodie(db *sql.DB, s string) *sql.Stmt {
1535 stmt, err := db.Prepare(s)
1536 if err != nil {
1537 log.Fatalf("error %s: %s", err, s)
1538 }
1539 return stmt
1540}
1541
1542func prepareStatements(db *sql.DB) {
1543 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")
1544 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1545 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1546 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1547 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1548 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1549
1550 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 "
1551 limit := " order by honkid desc limit 250"
1552 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1553 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1554 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1555 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1556 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)
1557 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1558 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1559 stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1560 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1561 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1562
1563 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1564 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1565 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1566 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1567 stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1568 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1569 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1570 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1571 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1572 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1573 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1574 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1575 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1576 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1577 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1578 stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1579 stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1580 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1581 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1582 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1583 stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1584 stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1585}
1586
1587func ElaborateUnitTests() {
1588}
1589
1590func main() {
1591 var err error
1592 cmd := "run"
1593 if len(os.Args) > 1 {
1594 cmd = os.Args[1]
1595 }
1596 switch cmd {
1597 case "init":
1598 initdb()
1599 case "upgrade":
1600 upgradedb()
1601 }
1602 db := opendatabase()
1603 dbversion := 0
1604 getconfig("dbversion", &dbversion)
1605 if dbversion != myVersion {
1606 log.Fatal("incorrect database version. run upgrade.")
1607 }
1608 getconfig("servermsg", &serverMsg)
1609 getconfig("servername", &serverName)
1610 getconfig("dnf", &donotfedafterdark)
1611 prepareStatements(db)
1612 switch cmd {
1613 case "adduser":
1614 adduser()
1615 case "cleanup":
1616 days := 30
1617 if len(os.Args) > 2 {
1618 days, err = strconv.Atoi(os.Args[2])
1619 if err != nil {
1620 log.Fatal(err)
1621 }
1622 }
1623 cleanupdb(days)
1624 case "reduce":
1625 if len(os.Args) < 3 {
1626 log.Fatal("need a honker name")
1627 }
1628 reducedb(os.Args[2])
1629 case "ping":
1630 if len(os.Args) < 4 {
1631 fmt.Printf("usage: honk ping from to\n")
1632 return
1633 }
1634 name := os.Args[2]
1635 targ := os.Args[3]
1636 user, err := butwhatabout(name)
1637 if err != nil {
1638 log.Printf("unknown user")
1639 return
1640 }
1641 ping(user, targ)
1642 case "peep":
1643 peeppeep()
1644 case "run":
1645 serve()
1646 case "test":
1647 ElaborateUnitTests()
1648 default:
1649 log.Fatal("unknown command")
1650 }
1651}