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