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