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