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