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