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