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