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