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 = hooterize(noise)
825 noise = strings.TrimSpace(noise)
826 honk.Precis = strings.TrimSpace(honk.Precis)
827
828 var convoy string
829 if rid != "" {
830 xonk := getxonk(userinfo.UserID, rid)
831 if xonk != nil {
832 if xonk.Public {
833 honk.Audience = append(honk.Audience, xonk.Audience...)
834 }
835 convoy = xonk.Convoy
836 } else {
837 xonkaud, c := whosthere(rid)
838 honk.Audience = append(honk.Audience, xonkaud...)
839 convoy = c
840 }
841 for i, a := range honk.Audience {
842 if a == thewholeworld {
843 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
844 break
845 }
846 }
847 honk.RID = rid
848 } else {
849 honk.Audience = []string{thewholeworld}
850 }
851 if noise != "" && noise[0] == '@' {
852 honk.Audience = append(grapevine(noise), honk.Audience...)
853 } else {
854 honk.Audience = append(honk.Audience, grapevine(noise)...)
855 }
856 if convoy == "" {
857 convoy = "data:,electrichonkytonk-" + xfiltrate()
858 }
859 butnottooloud(honk.Audience)
860 honk.Audience = oneofakind(honk.Audience)
861 if len(honk.Audience) == 0 {
862 log.Printf("honk to nowhere")
863 return
864 }
865 honk.Public = !keepitquiet(honk.Audience)
866 noise = obfusbreak(noise)
867 honk.Noise = noise
868 honk.Convoy = convoy
869
870 file, filehdr, err := r.FormFile("donk")
871 if err == nil {
872 var buf bytes.Buffer
873 io.Copy(&buf, file)
874 file.Close()
875 data := buf.Bytes()
876 xid := xfiltrate()
877 var media, name string
878 img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
879 if err == nil {
880 data = img.Data
881 format := img.Format
882 media = "image/" + format
883 if format == "jpeg" {
884 format = "jpg"
885 }
886 name = xid + "." + format
887 xid = name
888 } else {
889 maxsize := 100000
890 if len(data) > maxsize {
891 log.Printf("bad image: %s too much text: %d", err, len(data))
892 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
893 return
894 }
895 for i := 0; i < len(data); i++ {
896 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
897 log.Printf("bad image: %s not text: %d", err, data[i])
898 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
899 return
900 }
901 }
902 media = "text/plain"
903 name = filehdr.Filename
904 if name == "" {
905 name = xid + ".txt"
906 }
907 xid += ".txt"
908 }
909 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
910 res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
911 if err != nil {
912 log.Printf("unable to save image: %s", err)
913 return
914 }
915 var d Donk
916 d.FileID, _ = res.LastInsertId()
917 d.XID = name
918 d.Name = name
919 d.Media = media
920 d.URL = url
921 honk.Donks = append(honk.Donks, &d)
922 }
923 herd := herdofemus(honk.Noise)
924 for _, e := range herd {
925 donk := savedonk(e.ID, e.Name, "image/png", true)
926 if donk != nil {
927 donk.Name = e.Name
928 honk.Donks = append(honk.Donks, donk)
929 }
930 }
931 honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
932
933 aud := strings.Join(honk.Audience, " ")
934 whofore := 2
935 if !honk.Public {
936 whofore = 3
937 }
938 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
939 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
940 if err != nil {
941 log.Printf("error saving honk: %s", err)
942 return
943 }
944 honk.ID, _ = res.LastInsertId()
945 for _, d := range honk.Donks {
946 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
947 if err != nil {
948 log.Printf("err saving donk: %s", err)
949 return
950 }
951 }
952
953 go honkworldwide(user, &honk)
954
955 http.Redirect(w, r, "/", http.StatusSeeOther)
956}
957
958func showhonkers(w http.ResponseWriter, r *http.Request) {
959 userinfo := login.GetUserInfo(r)
960 templinfo := getInfo(r)
961 templinfo["Honkers"] = gethonkers(userinfo.UserID)
962 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
963 err := readviews.Execute(w, "honkers.html", templinfo)
964 if err != nil {
965 log.Print(err)
966 }
967}
968
969func showcombos(w http.ResponseWriter, r *http.Request) {
970 userinfo := login.GetUserInfo(r)
971 templinfo := getInfo(r)
972 honkers := gethonkers(userinfo.UserID)
973 var combos []string
974 for _, h := range honkers {
975 combos = append(combos, h.Combos...)
976 }
977 for i, c := range combos {
978 if c == "-" {
979 combos[i] = ""
980 }
981 }
982 combos = oneofakind(combos)
983 sort.Strings(combos)
984 templinfo["Combos"] = combos
985 err := readviews.Execute(w, "combos.html", templinfo)
986 if err != nil {
987 log.Print(err)
988 }
989}
990
991func savehonker(w http.ResponseWriter, r *http.Request) {
992 u := login.GetUserInfo(r)
993 name := r.FormValue("name")
994 url := r.FormValue("url")
995 peep := r.FormValue("peep")
996 combos := r.FormValue("combos")
997 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
998
999 if honkerid > 0 {
1000 goodbye := r.FormValue("goodbye")
1001 if goodbye == "F" {
1002 db := opendatabase()
1003 row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1004 honkerid, u.UserID)
1005 var xid string
1006 err := row.Scan(&xid)
1007 if err != nil {
1008 log.Printf("can't get honker xid: %s", err)
1009 return
1010 }
1011 log.Printf("unsubscribing from %s", xid)
1012 user, _ := butwhatabout(u.Username)
1013 err = itakeitallback(user, xid)
1014 if err != nil {
1015 log.Printf("can't take it back: %s", err)
1016 } else {
1017 _, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1018 if err != nil {
1019 log.Printf("error updating honker: %s", err)
1020 return
1021 }
1022 }
1023
1024 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1025 return
1026 }
1027 combos = " " + strings.TrimSpace(combos) + " "
1028 _, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1029 if err != nil {
1030 log.Printf("update honker err: %s", err)
1031 return
1032 }
1033 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1034 }
1035
1036 flavor := "presub"
1037 if peep == "peep" {
1038 flavor = "peep"
1039 }
1040 url = investigate(url)
1041 if url == "" {
1042 return
1043 }
1044 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1045 if err != nil {
1046 log.Print(err)
1047 return
1048 }
1049 if flavor == "presub" {
1050 user, _ := butwhatabout(u.Username)
1051 go subsub(user, url)
1052 }
1053 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1054}
1055
1056type Zonker struct {
1057 ID int64
1058 Name string
1059 Wherefore string
1060}
1061
1062func zonkzone(w http.ResponseWriter, r *http.Request) {
1063 db := opendatabase()
1064 userinfo := login.GetUserInfo(r)
1065 rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1066 if err != nil {
1067 log.Printf("err: %s", err)
1068 return
1069 }
1070 var zonkers []Zonker
1071 for rows.Next() {
1072 var z Zonker
1073 rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1074 zonkers = append(zonkers, z)
1075 }
1076 templinfo := getInfo(r)
1077 templinfo["Zonkers"] = zonkers
1078 templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1079 err = readviews.Execute(w, "zonkers.html", templinfo)
1080 if err != nil {
1081 log.Print(err)
1082 }
1083}
1084
1085func zonkzonk(w http.ResponseWriter, r *http.Request) {
1086 userinfo := login.GetUserInfo(r)
1087 itsok := r.FormValue("itsok")
1088 if itsok == "iforgiveyou" {
1089 zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1090 db := opendatabase()
1091 db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1092 userinfo.UserID, zonkerid)
1093 bitethethumbs()
1094 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1095 return
1096 }
1097 wherefore := r.FormValue("wherefore")
1098 name := r.FormValue("name")
1099 if name == "" {
1100 return
1101 }
1102 switch wherefore {
1103 case "zonker":
1104 case "zurl":
1105 case "zonvoy":
1106 case "zword":
1107 default:
1108 return
1109 }
1110 db := opendatabase()
1111 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1112 userinfo.UserID, name, wherefore)
1113 if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1114 bitethethumbs()
1115 }
1116
1117 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1118}
1119
1120func accountpage(w http.ResponseWriter, r *http.Request) {
1121 u := login.GetUserInfo(r)
1122 user, _ := butwhatabout(u.Username)
1123 templinfo := getInfo(r)
1124 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1125 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1126 templinfo["WhatAbout"] = user.About
1127 err := readviews.Execute(w, "account.html", templinfo)
1128 if err != nil {
1129 log.Print(err)
1130 }
1131}
1132
1133func dochpass(w http.ResponseWriter, r *http.Request) {
1134 err := login.ChangePassword(w, r)
1135 if err != nil {
1136 log.Printf("error changing password: %s", err)
1137 }
1138 http.Redirect(w, r, "/account", http.StatusSeeOther)
1139}
1140
1141func fingerlicker(w http.ResponseWriter, r *http.Request) {
1142 orig := r.FormValue("resource")
1143
1144 log.Printf("finger lick: %s", orig)
1145
1146 if strings.HasPrefix(orig, "acct:") {
1147 orig = orig[5:]
1148 }
1149
1150 name := orig
1151 idx := strings.LastIndexByte(name, '/')
1152 if idx != -1 {
1153 name = name[idx+1:]
1154 if "https://"+serverName+"/u/"+name != orig {
1155 log.Printf("foreign request rejected")
1156 name = ""
1157 }
1158 } else {
1159 idx = strings.IndexByte(name, '@')
1160 if idx != -1 {
1161 name = name[:idx]
1162 if name+"@"+serverName != orig {
1163 log.Printf("foreign request rejected")
1164 name = ""
1165 }
1166 }
1167 }
1168 user, err := butwhatabout(name)
1169 if err != nil {
1170 http.NotFound(w, r)
1171 return
1172 }
1173
1174 j := NewJunk()
1175 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1176 j["aliases"] = []string{user.URL}
1177 var links []map[string]interface{}
1178 l := NewJunk()
1179 l["rel"] = "self"
1180 l["type"] = `application/activity+json`
1181 l["href"] = user.URL
1182 links = append(links, l)
1183 j["links"] = links
1184
1185 w.Header().Set("Cache-Control", "max-age=3600")
1186 w.Header().Set("Content-Type", "application/jrd+json")
1187 WriteJunk(w, j)
1188}
1189
1190func somedays() string {
1191 secs := 432000 + notrand.Int63n(432000)
1192 return fmt.Sprintf("%d", secs)
1193}
1194
1195func avatate(w http.ResponseWriter, r *http.Request) {
1196 n := r.FormValue("a")
1197 a := avatar(n)
1198 w.Header().Set("Cache-Control", "max-age="+somedays())
1199 w.Write(a)
1200}
1201
1202func servecss(w http.ResponseWriter, r *http.Request) {
1203 w.Header().Set("Cache-Control", "max-age=7776000")
1204 http.ServeFile(w, r, "views"+r.URL.Path)
1205}
1206func servehtml(w http.ResponseWriter, r *http.Request) {
1207 templinfo := getInfo(r)
1208 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1209 if err != nil {
1210 log.Print(err)
1211 }
1212}
1213func serveemu(w http.ResponseWriter, r *http.Request) {
1214 xid := mux.Vars(r)["xid"]
1215 w.Header().Set("Cache-Control", "max-age="+somedays())
1216 http.ServeFile(w, r, "emus/"+xid)
1217}
1218func servememe(w http.ResponseWriter, r *http.Request) {
1219 xid := mux.Vars(r)["xid"]
1220 w.Header().Set("Cache-Control", "max-age="+somedays())
1221 http.ServeFile(w, r, "memes/"+xid)
1222}
1223
1224func servefile(w http.ResponseWriter, r *http.Request) {
1225 xid := mux.Vars(r)["xid"]
1226 row := stmtFileData.QueryRow(xid)
1227 var media string
1228 var data []byte
1229 err := row.Scan(&media, &data)
1230 if err != nil {
1231 log.Printf("error loading file: %s", err)
1232 http.NotFound(w, r)
1233 return
1234 }
1235 w.Header().Set("Content-Type", media)
1236 w.Header().Set("X-Content-Type-Options", "nosniff")
1237 w.Header().Set("Cache-Control", "max-age="+somedays())
1238 w.Write(data)
1239}
1240
1241func serve() {
1242 db := opendatabase()
1243 login.Init(db)
1244
1245 listener, err := openListener()
1246 if err != nil {
1247 log.Fatal(err)
1248 }
1249 go redeliverator()
1250
1251 debug := false
1252 getconfig("debug", &debug)
1253 readviews = templates.Load(debug,
1254 "views/honkpage.html",
1255 "views/honkers.html",
1256 "views/zonkers.html",
1257 "views/combos.html",
1258 "views/honkform.html",
1259 "views/honk.html",
1260 "views/account.html",
1261 "views/login.html",
1262 "views/header.html",
1263 )
1264 if !debug {
1265 s := "views/style.css"
1266 savedstyleparams[s] = getstyleparam(s)
1267 s = "views/local.css"
1268 savedstyleparams[s] = getstyleparam(s)
1269 }
1270
1271 bitethethumbs()
1272
1273 mux := mux.NewRouter()
1274 mux.Use(login.Checker)
1275
1276 posters := mux.Methods("POST").Subrouter()
1277 getters := mux.Methods("GET").Subrouter()
1278
1279 getters.HandleFunc("/", homepage)
1280 getters.HandleFunc("/rss", showrss)
1281 getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1282 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1283 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1284 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1285 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1286 getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1287 getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1288 getters.HandleFunc("/a", avatate)
1289 getters.HandleFunc("/t", showconvoy)
1290 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1291 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1292 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1293 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1294
1295 getters.HandleFunc("/style.css", servecss)
1296 getters.HandleFunc("/local.css", servecss)
1297 getters.HandleFunc("/login", servehtml)
1298 posters.HandleFunc("/dologin", login.LoginFunc)
1299 getters.HandleFunc("/logout", login.LogoutFunc)
1300
1301 loggedin := mux.NewRoute().Subrouter()
1302 loggedin.Use(login.Required)
1303 loggedin.HandleFunc("/account", accountpage)
1304 loggedin.HandleFunc("/chpass", dochpass)
1305 loggedin.HandleFunc("/atme", homepage)
1306 loggedin.HandleFunc("/zonkzone", zonkzone)
1307 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1308 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1309 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1310 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1311 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1312 loggedin.HandleFunc("/honkers", showhonkers)
1313 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1314 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1315 loggedin.HandleFunc("/c", showcombos)
1316 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1317
1318 err = http.Serve(listener, mux)
1319 if err != nil {
1320 log.Fatal(err)
1321 }
1322}
1323
1324func cleanupdb() {
1325 db := opendatabase()
1326 rows, _ := db.Query("select userid, name from zonkers where wherefore = 'zonvoy'")
1327 deadthreads := make(map[int64][]string)
1328 for rows.Next() {
1329 var userid int64
1330 var name string
1331 rows.Scan(&userid, &name)
1332 deadthreads[userid] = append(deadthreads[userid], name)
1333 }
1334 rows.Close()
1335 for userid, threads := range deadthreads {
1336 for _, t := range threads {
1337 doordie(db, "delete from donks where honkid in (select honkid from honks where userid = ? and convoy = ?)", userid, t)
1338 doordie(db, "delete from honks where userid = ? and convoy = ?", userid, t)
1339 }
1340 }
1341 expdate := time.Now().UTC().Add(-30 * 24 * time.Hour).Format(dbtimeformat)
1342 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)
1343 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)
1344 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1345}
1346
1347var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1348var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1349var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1350var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1351var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1352var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1353var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1354var stmtGetXonker, stmtSaveXonker *sql.Stmt
1355
1356func preparetodie(db *sql.DB, s string) *sql.Stmt {
1357 stmt, err := db.Prepare(s)
1358 if err != nil {
1359 log.Fatalf("error %s: %s", err, s)
1360 }
1361 return stmt
1362}
1363
1364func prepareStatements(db *sql.DB) {
1365 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")
1366 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1367 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1368 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1369 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1370 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1371
1372 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 "
1373 limit := " order by honkid desc limit 250"
1374 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1375 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1376 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1377 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1378 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)
1379 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1380 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1381 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1382 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1383
1384 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1385 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1386 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1387 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1388 stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1389 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1390 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1391 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1392 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1393 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1394 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1395 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1396 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1397 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1398 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1399 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1400 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1401 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1402}
1403
1404func ElaborateUnitTests() {
1405}
1406
1407func main() {
1408 cmd := "run"
1409 if len(os.Args) > 1 {
1410 cmd = os.Args[1]
1411 }
1412 switch cmd {
1413 case "init":
1414 initdb()
1415 case "upgrade":
1416 upgradedb()
1417 }
1418 db := opendatabase()
1419 dbversion := 0
1420 getconfig("dbversion", &dbversion)
1421 if dbversion != myVersion {
1422 log.Fatal("incorrect database version. run upgrade.")
1423 }
1424 getconfig("servername", &serverName)
1425 prepareStatements(db)
1426 switch cmd {
1427 case "adduser":
1428 adduser()
1429 case "cleanup":
1430 cleanupdb()
1431 case "ping":
1432 if len(os.Args) < 4 {
1433 fmt.Printf("usage: honk ping from to\n")
1434 return
1435 }
1436 name := os.Args[2]
1437 targ := os.Args[3]
1438 user, err := butwhatabout(name)
1439 if err != nil {
1440 log.Printf("unknown user")
1441 return
1442 }
1443 ping(user, targ)
1444 case "peep":
1445 peeppeep()
1446 case "run":
1447 serve()
1448 case "test":
1449 ElaborateUnitTests()
1450 default:
1451 log.Fatal("unknown command")
1452 }
1453}