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