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