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 u := login.GetUserInfo(r)
388 var honkers []string
389 rows, err := stmtRecentHonkers.Query(u.UserID)
390 if err != nil {
391 log.Printf("query err: %s", err)
392 return
393 }
394 defer rows.Close()
395 for rows.Next() {
396 var s string
397 rows.Scan(&s)
398 honkers = append(honkers, s)
399 }
400
401 templinfo := getInfo(r)
402 templinfo["XCSRF"] = login.GetCSRF("ximport", r)
403 templinfo["Honkers"] = honkers
404 err = readviews.Execute(w, "xzone.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 := junk.New()
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 j.Write(w)
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 := junk.New()
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 j.Write(w)
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 j.Write(w)
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 u := login.GetUserInfo(r)
480 name := mux.Vars(r)["name"]
481 var honks []*Honk
482 if name == "" {
483 name = r.FormValue("xid")
484 honks = gethonksbyxonker(u.UserID, name)
485 } else {
486 honks = gethonksbyhonker(u.UserID, name)
487 }
488 honkpage(w, r, u, nil, honks, "honks by honker: "+name)
489}
490
491func showcombo(w http.ResponseWriter, r *http.Request) {
492 name := mux.Vars(r)["name"]
493 u := login.GetUserInfo(r)
494 honks := gethonksbycombo(u.UserID, name)
495 honkpage(w, r, u, nil, honks, "honks by combo: "+name)
496}
497func showconvoy(w http.ResponseWriter, r *http.Request) {
498 c := r.FormValue("c")
499 var userid int64 = -1
500 u := login.GetUserInfo(r)
501 if u != nil {
502 userid = u.UserID
503 }
504 honks := gethonksbyconvoy(userid, c)
505 honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
506}
507
508func showhonk(w http.ResponseWriter, r *http.Request) {
509 name := mux.Vars(r)["name"]
510 user, err := butwhatabout(name)
511 if err != nil {
512 http.NotFound(w, r)
513 return
514 }
515 xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
516 h := getxonk(user.ID, xid)
517 if h == nil || !h.Public {
518 http.NotFound(w, r)
519 return
520 }
521 if friendorfoe(r.Header.Get("Accept")) {
522 donksforhonks([]*Honk{h})
523 _, j := jonkjonk(user, h)
524 j["@context"] = itiswhatitis
525 w.Header().Set("Cache-Control", "max-age=3600")
526 w.Header().Set("Content-Type", theonetruename)
527 j.Write(w)
528 return
529 }
530 honks := gethonksbyconvoy(-1, h.Convoy)
531 u := login.GetUserInfo(r)
532 honkpage(w, r, u, nil, honks, "one honk maybe more")
533}
534
535func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
536 honks []*Honk, infomsg string) {
537 reverbolate(honks)
538 templinfo := getInfo(r)
539 if u != nil {
540 honks = osmosis(honks, u.UserID)
541 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
542 }
543 if u == nil {
544 w.Header().Set("Cache-Control", "max-age=60")
545 }
546 if user != nil {
547 filt := htfilter.New()
548 templinfo["Name"] = user.Name
549 whatabout := user.About
550 whatabout = obfusbreak(user.About)
551 templinfo["WhatAbout"], _ = filt.String(whatabout)
552 }
553 templinfo["Honks"] = honks
554 templinfo["ServerMessage"] = infomsg
555 err := readviews.Execute(w, "honkpage.html", templinfo)
556 if err != nil {
557 log.Print(err)
558 }
559}
560
561func saveuser(w http.ResponseWriter, r *http.Request) {
562 whatabout := r.FormValue("whatabout")
563 u := login.GetUserInfo(r)
564 db := opendatabase()
565 _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
566 if err != nil {
567 log.Printf("error bouting what: %s", err)
568 }
569
570 http.Redirect(w, r, "/account", http.StatusSeeOther)
571}
572
573func gethonkers(userid int64) []*Honker {
574 rows, err := stmtHonkers.Query(userid)
575 if err != nil {
576 log.Printf("error querying honkers: %s", err)
577 return nil
578 }
579 defer rows.Close()
580 var honkers []*Honker
581 for rows.Next() {
582 var f Honker
583 var combos string
584 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
585 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
586 if err != nil {
587 log.Printf("error scanning honker: %s", err)
588 return nil
589 }
590 honkers = append(honkers, &f)
591 }
592 return honkers
593}
594
595func getdubs(userid int64) []*Honker {
596 rows, err := stmtDubbers.Query(userid)
597 if err != nil {
598 log.Printf("error querying dubs: %s", err)
599 return nil
600 }
601 defer rows.Close()
602 var honkers []*Honker
603 for rows.Next() {
604 var f Honker
605 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
606 if err != nil {
607 log.Printf("error scanning honker: %s", err)
608 return nil
609 }
610 honkers = append(honkers, &f)
611 }
612 return honkers
613}
614
615func allusers() []login.UserInfo {
616 var users []login.UserInfo
617 rows, _ := opendatabase().Query("select userid, username from users")
618 defer rows.Close()
619 for rows.Next() {
620 var u login.UserInfo
621 rows.Scan(&u.UserID, &u.Username)
622 users = append(users, u)
623 }
624 return users
625}
626
627func getxonk(userid int64, xid string) *Honk {
628 h := new(Honk)
629 var dt, aud string
630 row := stmtOneXonk.QueryRow(userid, xid)
631 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
632 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
633 if err != nil {
634 if err != sql.ErrNoRows {
635 log.Printf("error scanning xonk: %s", err)
636 }
637 return nil
638 }
639 h.Date, _ = time.Parse(dbtimeformat, dt)
640 h.Audience = strings.Split(aud, " ")
641 h.Public = !keepitquiet(h.Audience)
642 return h
643}
644
645func getpublichonks() []*Honk {
646 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
647 rows, err := stmtPublicHonks.Query(dt)
648 return getsomehonks(rows, err)
649}
650func gethonksbyuser(name string, includeprivate bool) []*Honk {
651 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
652 whofore := 2
653 if includeprivate {
654 whofore = 3
655 }
656 rows, err := stmtUserHonks.Query(whofore, name, dt)
657 return getsomehonks(rows, err)
658}
659func gethonksforuser(userid int64) []*Honk {
660 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
661 rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
662 return getsomehonks(rows, err)
663}
664func gethonksforme(userid int64) []*Honk {
665 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
666 rows, err := stmtHonksForMe.Query(userid, dt, userid)
667 return getsomehonks(rows, err)
668}
669func gethonksbyhonker(userid int64, honker string) []*Honk {
670 rows, err := stmtHonksByHonker.Query(userid, honker, userid)
671 return getsomehonks(rows, err)
672}
673func gethonksbyxonker(userid int64, xonker string) []*Honk {
674 rows, err := stmtHonksByXonker.Query(userid, xonker, userid)
675 return getsomehonks(rows, err)
676}
677func gethonksbycombo(userid int64, combo string) []*Honk {
678 combo = "% " + combo + " %"
679 rows, err := stmtHonksByCombo.Query(userid, combo, userid)
680 return getsomehonks(rows, err)
681}
682func gethonksbyconvoy(userid int64, convoy string) []*Honk {
683 rows, err := stmtHonksByConvoy.Query(userid, convoy)
684 honks := getsomehonks(rows, err)
685 for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
686 honks[i], honks[j] = honks[j], honks[i]
687 }
688 return honks
689}
690
691func getsomehonks(rows *sql.Rows, err error) []*Honk {
692 if err != nil {
693 log.Printf("error querying honks: %s", err)
694 return nil
695 }
696 defer rows.Close()
697 var honks []*Honk
698 for rows.Next() {
699 var h Honk
700 var dt, aud string
701 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
702 &h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
703 if err != nil {
704 log.Printf("error scanning honks: %s", err)
705 return nil
706 }
707 h.Date, _ = time.Parse(dbtimeformat, dt)
708 h.Audience = strings.Split(aud, " ")
709 h.Public = !keepitquiet(h.Audience)
710 honks = append(honks, &h)
711 }
712 rows.Close()
713 donksforhonks(honks)
714 return honks
715}
716
717func donksforhonks(honks []*Honk) {
718 db := opendatabase()
719 var ids []string
720 hmap := make(map[int64]*Honk)
721 for _, h := range honks {
722 ids = append(ids, fmt.Sprintf("%d", h.ID))
723 hmap[h.ID] = h
724 }
725 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, ","))
726 rows, err := db.Query(q)
727 if err != nil {
728 log.Printf("error querying donks: %s", err)
729 return
730 }
731 defer rows.Close()
732 for rows.Next() {
733 var hid int64
734 var d Donk
735 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
736 if err != nil {
737 log.Printf("error scanning donk: %s", err)
738 continue
739 }
740 h := hmap[hid]
741 h.Donks = append(h.Donks, &d)
742 }
743}
744
745func savebonk(w http.ResponseWriter, r *http.Request) {
746 xid := r.FormValue("xid")
747 userinfo := login.GetUserInfo(r)
748 user, _ := butwhatabout(userinfo.Username)
749
750 log.Printf("bonking %s", xid)
751
752 xonk := getxonk(userinfo.UserID, xid)
753 if xonk == nil {
754 return
755 }
756 if !xonk.Public {
757 return
758 }
759 donksforhonks([]*Honk{xonk})
760
761 dt := time.Now().UTC()
762 bonk := Honk{
763 UserID: userinfo.UserID,
764 Username: userinfo.Username,
765 What: "bonk",
766 Honker: user.URL,
767 XID: xonk.XID,
768 Date: dt,
769 Donks: xonk.Donks,
770 Audience: []string{thewholeworld},
771 Public: true,
772 }
773
774 oonker := xonk.Oonker
775 if oonker == "" {
776 oonker = xonk.Honker
777 }
778 aud := strings.Join(bonk.Audience, " ")
779 whofore := 2
780 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
781 dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
782 xonk.Precis, oonker)
783 if err != nil {
784 log.Printf("error saving bonk: %s", err)
785 return
786 }
787 bonk.ID, _ = res.LastInsertId()
788 for _, d := range bonk.Donks {
789 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
790 if err != nil {
791 log.Printf("err saving donk: %s", err)
792 return
793 }
794 }
795
796 go honkworldwide(user, &bonk)
797}
798
799func zonkit(w http.ResponseWriter, r *http.Request) {
800 wherefore := r.FormValue("wherefore")
801 var what string
802 switch wherefore {
803 case "this honk":
804 what = r.FormValue("honk")
805 wherefore = "zonk"
806 case "this honker":
807 what = r.FormValue("honker")
808 wherefore = "zonker"
809 case "this convoy":
810 what = r.FormValue("convoy")
811 wherefore = "zonvoy"
812 }
813 if what == "" {
814 return
815 }
816
817 log.Printf("zonking %s %s", wherefore, what)
818 userinfo := login.GetUserInfo(r)
819 if wherefore == "zonk" {
820 xonk := getxonk(userinfo.UserID, what)
821 if xonk != nil {
822 stmtZonkDonks.Exec(xonk.ID)
823 stmtZonkIt.Exec(userinfo.UserID, what)
824 if xonk.Whofore == 2 || xonk.Whofore == 3 {
825 zonk := Honk{
826 What: "zonk",
827 XID: xonk.XID,
828 Date: time.Now().UTC(),
829 Audience: oneofakind(xonk.Audience),
830 }
831
832 user, _ := butwhatabout(userinfo.Username)
833 log.Printf("announcing deleted honk: %s", what)
834 go honkworldwide(user, &zonk)
835 }
836 }
837 } else {
838 _, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
839 if err != nil {
840 log.Printf("error saving zonker: %s", err)
841 return
842 }
843 }
844}
845
846func savehonk(w http.ResponseWriter, r *http.Request) {
847 rid := r.FormValue("rid")
848 noise := r.FormValue("noise")
849
850 userinfo := login.GetUserInfo(r)
851 user, _ := butwhatabout(userinfo.Username)
852
853 dt := time.Now().UTC()
854 xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
855 what := "honk"
856 if rid != "" {
857 what = "tonk"
858 }
859 honk := Honk{
860 UserID: userinfo.UserID,
861 Username: userinfo.Username,
862 What: "honk",
863 Honker: user.URL,
864 XID: xid,
865 Date: dt,
866 }
867 if strings.HasPrefix(noise, "DZ:") {
868 idx := strings.Index(noise, "\n")
869 if idx == -1 {
870 honk.Precis = noise
871 noise = ""
872 } else {
873 honk.Precis = noise[:idx]
874 noise = noise[idx+1:]
875 }
876 }
877 noise = hooterize(noise)
878 noise = strings.TrimSpace(noise)
879 honk.Precis = strings.TrimSpace(honk.Precis)
880
881 var convoy string
882 if rid != "" {
883 xonk := getxonk(userinfo.UserID, rid)
884 if xonk != nil {
885 if xonk.Public {
886 honk.Audience = append(honk.Audience, xonk.Audience...)
887 }
888 convoy = xonk.Convoy
889 } else {
890 xonkaud, c := whosthere(rid)
891 honk.Audience = append(honk.Audience, xonkaud...)
892 convoy = c
893 }
894 for i, a := range honk.Audience {
895 if a == thewholeworld {
896 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
897 break
898 }
899 }
900 honk.RID = rid
901 } else {
902 honk.Audience = []string{thewholeworld}
903 }
904 if noise != "" && noise[0] == '@' {
905 honk.Audience = append(grapevine(noise), honk.Audience...)
906 } else {
907 honk.Audience = append(honk.Audience, grapevine(noise)...)
908 }
909 if convoy == "" {
910 convoy = "data:,electrichonkytonk-" + xfiltrate()
911 }
912 butnottooloud(honk.Audience)
913 honk.Audience = oneofakind(honk.Audience)
914 if len(honk.Audience) == 0 {
915 log.Printf("honk to nowhere")
916 return
917 }
918 honk.Public = !keepitquiet(honk.Audience)
919 noise = obfusbreak(noise)
920 honk.Noise = noise
921 honk.Convoy = convoy
922
923 file, filehdr, err := r.FormFile("donk")
924 if err == nil {
925 var buf bytes.Buffer
926 io.Copy(&buf, file)
927 file.Close()
928 data := buf.Bytes()
929 xid := xfiltrate()
930 var media, name string
931 img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
932 if err == nil {
933 data = img.Data
934 format := img.Format
935 media = "image/" + format
936 if format == "jpeg" {
937 format = "jpg"
938 }
939 name = xid + "." + format
940 xid = name
941 } else {
942 maxsize := 100000
943 if len(data) > maxsize {
944 log.Printf("bad image: %s too much text: %d", err, len(data))
945 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
946 return
947 }
948 for i := 0; i < len(data); i++ {
949 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
950 log.Printf("bad image: %s not text: %d", err, data[i])
951 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
952 return
953 }
954 }
955 media = "text/plain"
956 name = filehdr.Filename
957 if name == "" {
958 name = xid + ".txt"
959 }
960 xid += ".txt"
961 }
962 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
963 res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
964 if err != nil {
965 log.Printf("unable to save image: %s", err)
966 return
967 }
968 var d Donk
969 d.FileID, _ = res.LastInsertId()
970 d.XID = name
971 d.Name = name
972 d.Media = media
973 d.URL = url
974 d.Local = true
975 honk.Donks = append(honk.Donks, &d)
976 }
977 herd := herdofemus(honk.Noise)
978 for _, e := range herd {
979 donk := savedonk(e.ID, e.Name, "image/png", true)
980 if donk != nil {
981 donk.Name = e.Name
982 honk.Donks = append(honk.Donks, donk)
983 }
984 }
985 honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
986
987 aud := strings.Join(honk.Audience, " ")
988 whofore := 2
989 if !honk.Public {
990 whofore = 3
991 }
992 if r.FormValue("preview") == "preview" {
993 honks := []*Honk{&honk}
994 reverbolate(honks)
995 templinfo := getInfo(r)
996 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
997 templinfo["Honks"] = honks
998 templinfo["Noise"] = r.FormValue("noise")
999 templinfo["ServerMessage"] = "honk preview"
1000 err := readviews.Execute(w, "honkpage.html", templinfo)
1001 if err != nil {
1002 log.Print(err)
1003 }
1004 return
1005 }
1006 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1007 dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1008 if err != nil {
1009 log.Printf("error saving honk: %s", err)
1010 return
1011 }
1012 honk.ID, _ = res.LastInsertId()
1013 for _, d := range honk.Donks {
1014 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1015 if err != nil {
1016 log.Printf("err saving donk: %s", err)
1017 return
1018 }
1019 }
1020
1021 go honkworldwide(user, &honk)
1022
1023 http.Redirect(w, r, "/", http.StatusSeeOther)
1024}
1025
1026func showhonkers(w http.ResponseWriter, r *http.Request) {
1027 userinfo := login.GetUserInfo(r)
1028 templinfo := getInfo(r)
1029 templinfo["Honkers"] = gethonkers(userinfo.UserID)
1030 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1031 err := readviews.Execute(w, "honkers.html", templinfo)
1032 if err != nil {
1033 log.Print(err)
1034 }
1035}
1036
1037func showcombos(w http.ResponseWriter, r *http.Request) {
1038 userinfo := login.GetUserInfo(r)
1039 templinfo := getInfo(r)
1040 honkers := gethonkers(userinfo.UserID)
1041 var combos []string
1042 for _, h := range honkers {
1043 combos = append(combos, h.Combos...)
1044 }
1045 for i, c := range combos {
1046 if c == "-" {
1047 combos[i] = ""
1048 }
1049 }
1050 combos = oneofakind(combos)
1051 sort.Strings(combos)
1052 templinfo["Combos"] = combos
1053 err := readviews.Execute(w, "combos.html", templinfo)
1054 if err != nil {
1055 log.Print(err)
1056 }
1057}
1058
1059func savehonker(w http.ResponseWriter, r *http.Request) {
1060 u := login.GetUserInfo(r)
1061 name := r.FormValue("name")
1062 url := r.FormValue("url")
1063 peep := r.FormValue("peep")
1064 combos := r.FormValue("combos")
1065 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1066
1067 if honkerid > 0 {
1068 goodbye := r.FormValue("goodbye")
1069 if goodbye == "F" {
1070 db := opendatabase()
1071 row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1072 honkerid, u.UserID)
1073 var xid string
1074 err := row.Scan(&xid)
1075 if err != nil {
1076 log.Printf("can't get honker xid: %s", err)
1077 return
1078 }
1079 log.Printf("unsubscribing from %s", xid)
1080 user, _ := butwhatabout(u.Username)
1081 go itakeitallback(user, xid)
1082 _, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1083 if err != nil {
1084 log.Printf("error updating honker: %s", err)
1085 return
1086 }
1087
1088 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1089 return
1090 }
1091 combos = " " + strings.TrimSpace(combos) + " "
1092 _, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1093 if err != nil {
1094 log.Printf("update honker err: %s", err)
1095 return
1096 }
1097 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1098 }
1099
1100 flavor := "presub"
1101 if peep == "peep" {
1102 flavor = "peep"
1103 }
1104 url = investigate(url)
1105 if url == "" {
1106 return
1107 }
1108 _, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1109 if err != nil {
1110 log.Print(err)
1111 return
1112 }
1113 if flavor == "presub" {
1114 user, _ := butwhatabout(u.Username)
1115 go subsub(user, url)
1116 }
1117 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1118}
1119
1120type Zonker struct {
1121 ID int64
1122 Name string
1123 Wherefore string
1124}
1125
1126func zonkzone(w http.ResponseWriter, r *http.Request) {
1127 userinfo := login.GetUserInfo(r)
1128 rows, err := stmtGetZonkers.Query(userinfo.UserID)
1129 if err != nil {
1130 log.Printf("err: %s", err)
1131 return
1132 }
1133 defer rows.Close()
1134 var zonkers []Zonker
1135 for rows.Next() {
1136 var z Zonker
1137 rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1138 zonkers = append(zonkers, z)
1139 }
1140 sort.Slice(zonkers, func(i, j int) bool {
1141 w1 := zonkers[i].Wherefore
1142 w2 := zonkers[j].Wherefore
1143 if w1 == w2 {
1144 return zonkers[i].Name < zonkers[j].Name
1145 }
1146 if w1 == "zonvoy" {
1147 w1 = "zzzzzzz"
1148 }
1149 if w2 == "zonvoy" {
1150 w2 = "zzzzzzz"
1151 }
1152 return w1 < w2
1153 })
1154
1155 templinfo := getInfo(r)
1156 templinfo["Zonkers"] = zonkers
1157 templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1158 err = readviews.Execute(w, "zonkers.html", templinfo)
1159 if err != nil {
1160 log.Print(err)
1161 }
1162}
1163
1164func zonkzonk(w http.ResponseWriter, r *http.Request) {
1165 userinfo := login.GetUserInfo(r)
1166 itsok := r.FormValue("itsok")
1167 if itsok == "iforgiveyou" {
1168 zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1169 db := opendatabase()
1170 db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1171 userinfo.UserID, zonkerid)
1172 bitethethumbs()
1173 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1174 return
1175 }
1176 wherefore := r.FormValue("wherefore")
1177 name := r.FormValue("name")
1178 if name == "" {
1179 return
1180 }
1181 switch wherefore {
1182 case "zonker":
1183 case "zurl":
1184 case "zonvoy":
1185 case "zword":
1186 default:
1187 return
1188 }
1189 db := opendatabase()
1190 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1191 userinfo.UserID, name, wherefore)
1192 if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1193 bitethethumbs()
1194 }
1195
1196 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1197}
1198
1199func accountpage(w http.ResponseWriter, r *http.Request) {
1200 u := login.GetUserInfo(r)
1201 user, _ := butwhatabout(u.Username)
1202 templinfo := getInfo(r)
1203 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1204 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1205 templinfo["WhatAbout"] = user.About
1206 err := readviews.Execute(w, "account.html", templinfo)
1207 if err != nil {
1208 log.Print(err)
1209 }
1210}
1211
1212func dochpass(w http.ResponseWriter, r *http.Request) {
1213 err := login.ChangePassword(w, r)
1214 if err != nil {
1215 log.Printf("error changing password: %s", err)
1216 }
1217 http.Redirect(w, r, "/account", http.StatusSeeOther)
1218}
1219
1220func fingerlicker(w http.ResponseWriter, r *http.Request) {
1221 orig := r.FormValue("resource")
1222
1223 log.Printf("finger lick: %s", orig)
1224
1225 if strings.HasPrefix(orig, "acct:") {
1226 orig = orig[5:]
1227 }
1228
1229 name := orig
1230 idx := strings.LastIndexByte(name, '/')
1231 if idx != -1 {
1232 name = name[idx+1:]
1233 if "https://"+serverName+"/u/"+name != orig {
1234 log.Printf("foreign request rejected")
1235 name = ""
1236 }
1237 } else {
1238 idx = strings.IndexByte(name, '@')
1239 if idx != -1 {
1240 name = name[:idx]
1241 if name+"@"+serverName != orig {
1242 log.Printf("foreign request rejected")
1243 name = ""
1244 }
1245 }
1246 }
1247 user, err := butwhatabout(name)
1248 if err != nil {
1249 http.NotFound(w, r)
1250 return
1251 }
1252
1253 j := junk.New()
1254 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1255 j["aliases"] = []string{user.URL}
1256 var links []map[string]interface{}
1257 l := junk.New()
1258 l["rel"] = "self"
1259 l["type"] = `application/activity+json`
1260 l["href"] = user.URL
1261 links = append(links, l)
1262 j["links"] = links
1263
1264 w.Header().Set("Cache-Control", "max-age=3600")
1265 w.Header().Set("Content-Type", "application/jrd+json")
1266 j.Write(w)
1267}
1268
1269func somedays() string {
1270 secs := 432000 + notrand.Int63n(432000)
1271 return fmt.Sprintf("%d", secs)
1272}
1273
1274func avatate(w http.ResponseWriter, r *http.Request) {
1275 n := r.FormValue("a")
1276 a := avatar(n)
1277 w.Header().Set("Cache-Control", "max-age="+somedays())
1278 w.Write(a)
1279}
1280
1281func servecss(w http.ResponseWriter, r *http.Request) {
1282 w.Header().Set("Cache-Control", "max-age=7776000")
1283 http.ServeFile(w, r, "views"+r.URL.Path)
1284}
1285func servehtml(w http.ResponseWriter, r *http.Request) {
1286 templinfo := getInfo(r)
1287 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1288 if err != nil {
1289 log.Print(err)
1290 }
1291}
1292func serveemu(w http.ResponseWriter, r *http.Request) {
1293 xid := mux.Vars(r)["xid"]
1294 w.Header().Set("Cache-Control", "max-age="+somedays())
1295 http.ServeFile(w, r, "emus/"+xid)
1296}
1297func servememe(w http.ResponseWriter, r *http.Request) {
1298 xid := mux.Vars(r)["xid"]
1299 w.Header().Set("Cache-Control", "max-age="+somedays())
1300 http.ServeFile(w, r, "memes/"+xid)
1301}
1302
1303func servefile(w http.ResponseWriter, r *http.Request) {
1304 xid := mux.Vars(r)["xid"]
1305 row := stmtFileData.QueryRow(xid)
1306 var media string
1307 var data []byte
1308 err := row.Scan(&media, &data)
1309 if err != nil {
1310 log.Printf("error loading file: %s", err)
1311 http.NotFound(w, r)
1312 return
1313 }
1314 w.Header().Set("Content-Type", media)
1315 w.Header().Set("X-Content-Type-Options", "nosniff")
1316 w.Header().Set("Cache-Control", "max-age="+somedays())
1317 w.Write(data)
1318}
1319
1320func serve() {
1321 db := opendatabase()
1322 login.Init(db)
1323
1324 listener, err := openListener()
1325 if err != nil {
1326 log.Fatal(err)
1327 }
1328 go redeliverator()
1329
1330 debug := false
1331 getconfig("debug", &debug)
1332 readviews = templates.Load(debug,
1333 "views/honkpage.html",
1334 "views/honkers.html",
1335 "views/zonkers.html",
1336 "views/combos.html",
1337 "views/honkform.html",
1338 "views/honk.html",
1339 "views/account.html",
1340 "views/about.html",
1341 "views/login.html",
1342 "views/xzone.html",
1343 "views/header.html",
1344 )
1345 if !debug {
1346 s := "views/style.css"
1347 savedstyleparams[s] = getstyleparam(s)
1348 s = "views/local.css"
1349 savedstyleparams[s] = getstyleparam(s)
1350 }
1351
1352 bitethethumbs()
1353
1354 mux := mux.NewRouter()
1355 mux.Use(login.Checker)
1356
1357 posters := mux.Methods("POST").Subrouter()
1358 getters := mux.Methods("GET").Subrouter()
1359
1360 getters.HandleFunc("/", homepage)
1361 getters.HandleFunc("/rss", showrss)
1362 getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1363 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1364 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1365 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1366 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1367 getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1368 getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1369 getters.HandleFunc("/a", avatate)
1370 getters.HandleFunc("/t", showconvoy)
1371 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1372 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1373 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1374 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1375
1376 getters.HandleFunc("/style.css", servecss)
1377 getters.HandleFunc("/local.css", servecss)
1378 getters.HandleFunc("/about", servehtml)
1379 getters.HandleFunc("/login", servehtml)
1380 posters.HandleFunc("/dologin", login.LoginFunc)
1381 getters.HandleFunc("/logout", login.LogoutFunc)
1382
1383 loggedin := mux.NewRoute().Subrouter()
1384 loggedin.Use(login.Required)
1385 loggedin.HandleFunc("/account", accountpage)
1386 loggedin.HandleFunc("/chpass", dochpass)
1387 loggedin.HandleFunc("/atme", homepage)
1388 loggedin.HandleFunc("/zonkzone", zonkzone)
1389 loggedin.HandleFunc("/xzone", xzone)
1390 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1391 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1392 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1393 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1394 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1395 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1396 loggedin.HandleFunc("/honkers", showhonkers)
1397 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1398 loggedin.HandleFunc("/h", showhonker)
1399 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1400 loggedin.HandleFunc("/c", showcombos)
1401 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1402
1403 err = http.Serve(listener, mux)
1404 if err != nil {
1405 log.Fatal(err)
1406 }
1407}
1408
1409func cleanupdb(days int) {
1410 db := opendatabase()
1411 expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1412 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)
1413 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)
1414 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1415}
1416
1417func reducedb(honker string) {
1418 db := opendatabase()
1419 expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1420 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1421 doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1422 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1423}
1424
1425var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1426var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1427var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1428var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1429var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1430var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1431var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1432var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker *sql.Stmt
1433
1434func preparetodie(db *sql.DB, s string) *sql.Stmt {
1435 stmt, err := db.Prepare(s)
1436 if err != nil {
1437 log.Fatalf("error %s: %s", err, s)
1438 }
1439 return stmt
1440}
1441
1442func prepareStatements(db *sql.DB) {
1443 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")
1444 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1445 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1446 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1447 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1448 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1449
1450 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 "
1451 limit := " order by honkid desc limit 250"
1452 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1453 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1454 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1455 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1456 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)
1457 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1458 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1459 stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and honker = ?"+butnotthose+limit)
1460 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1461 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1462
1463 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1464 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1465 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1466 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1467 stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1468 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1469 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1470 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1471 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1472 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1473 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1474 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1475 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1476 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1477 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1478 stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ?")
1479 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1480 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1481 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1482 stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1483}
1484
1485func ElaborateUnitTests() {
1486}
1487
1488func main() {
1489 var err error
1490 cmd := "run"
1491 if len(os.Args) > 1 {
1492 cmd = os.Args[1]
1493 }
1494 switch cmd {
1495 case "init":
1496 initdb()
1497 case "upgrade":
1498 upgradedb()
1499 }
1500 db := opendatabase()
1501 dbversion := 0
1502 getconfig("dbversion", &dbversion)
1503 if dbversion != myVersion {
1504 log.Fatal("incorrect database version. run upgrade.")
1505 }
1506 getconfig("servermsg", &serverMsg)
1507 getconfig("servername", &serverName)
1508 prepareStatements(db)
1509 switch cmd {
1510 case "adduser":
1511 adduser()
1512 case "cleanup":
1513 days := 30
1514 if len(os.Args) > 2 {
1515 days, err = strconv.Atoi(os.Args[2])
1516 if err != nil {
1517 log.Fatal(err)
1518 }
1519 }
1520 cleanupdb(days)
1521 case "reduce":
1522 if len(os.Args) < 3 {
1523 log.Fatal("need a honker name")
1524 }
1525 reducedb(os.Args[2])
1526 case "ping":
1527 if len(os.Args) < 4 {
1528 fmt.Printf("usage: honk ping from to\n")
1529 return
1530 }
1531 name := os.Args[2]
1532 targ := os.Args[3]
1533 user, err := butwhatabout(name)
1534 if err != nil {
1535 log.Printf("unknown user")
1536 return
1537 }
1538 ping(user, targ)
1539 case "peep":
1540 peeppeep()
1541 case "run":
1542 serve()
1543 case "test":
1544 ElaborateUnitTests()
1545 default:
1546 log.Fatal("unknown command")
1547 }
1548}