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