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