honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "bytes"
20 "database/sql"
21 "fmt"
22 "html"
23 "html/template"
24 "io"
25 "io/ioutil"
26 "log"
27 notrand "math/rand"
28 "net/http"
29 "net/url"
30 "os"
31 "sort"
32 "strconv"
33 "strings"
34 "time"
35
36 "github.com/gorilla/mux"
37 "humungus.tedunangst.com/r/webs/css"
38 "humungus.tedunangst.com/r/webs/htfilter"
39 "humungus.tedunangst.com/r/webs/httpsig"
40 "humungus.tedunangst.com/r/webs/image"
41 "humungus.tedunangst.com/r/webs/junk"
42 "humungus.tedunangst.com/r/webs/login"
43 "humungus.tedunangst.com/r/webs/rss"
44 "humungus.tedunangst.com/r/webs/templates"
45)
46
47type WhatAbout struct {
48 ID int64
49 Name string
50 Display string
51 About string
52 Key string
53 URL string
54 SkinnyCSS bool
55}
56
57type Honk struct {
58 ID int64
59 UserID int64
60 Username string
61 What string
62 Honker string
63 Handle string
64 Oonker string
65 Oondle string
66 XID string
67 RID string
68 Date time.Time
69 URL string
70 Noise string
71 Precis string
72 Convoy string
73 Audience []string
74 Public bool
75 Whofore int64
76 Replies []*Honk
77 Flags int64
78 HTML template.HTML
79 Style string
80 Open string
81 Donks []*Donk
82 Onts []string
83}
84
85const (
86 flagIsAcked = 1
87 flagIsBonked = 2
88)
89
90func (honk *Honk) IsAcked() bool {
91 return honk.Flags&flagIsAcked != 0
92}
93
94func (honk *Honk) IsBonked() bool {
95 return honk.Flags&flagIsBonked != 0
96}
97
98type Donk struct {
99 FileID int64
100 XID string
101 Name string
102 Desc string
103 URL string
104 Media string
105 Local bool
106 Content []byte
107}
108
109type Honker struct {
110 ID int64
111 UserID int64
112 Name string
113 XID string
114 Handle string
115 Flavor string
116 Combos []string
117}
118
119var serverName string
120var iconName = "icon.png"
121var serverMsg = "Things happen."
122
123var userSep = "u"
124var honkSep = "h"
125
126var readviews *templates.Template
127
128func getuserstyle(u *login.UserInfo) template.CSS {
129 if u == nil {
130 return ""
131 }
132 user, _ := butwhatabout(u.Username)
133 if user.SkinnyCSS {
134 return "main { max-width: 700px; }"
135 }
136 return ""
137}
138
139func getInfo(r *http.Request) map[string]interface{} {
140 u := login.GetUserInfo(r)
141 templinfo := make(map[string]interface{})
142 templinfo["StyleParam"] = getstyleparam("views/style.css")
143 templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
144 templinfo["UserStyle"] = getuserstyle(u)
145 templinfo["ServerName"] = serverName
146 templinfo["IconName"] = iconName
147 templinfo["UserInfo"] = u
148 templinfo["UserSep"] = userSep
149 return templinfo
150}
151
152var donotfedafterdark = make(map[string]bool)
153
154func stealthed(r *http.Request) bool {
155 addr := r.Header.Get("X-Forwarded-For")
156 fake := donotfedafterdark[addr]
157 if fake {
158 log.Printf("faking 404 for %s", addr)
159 }
160 return fake
161}
162
163func homepage(w http.ResponseWriter, r *http.Request) {
164 templinfo := getInfo(r)
165 u := login.GetUserInfo(r)
166 var honks []*Honk
167 var userid int64 = -1
168 if r.URL.Path == "/front" || u == nil {
169 honks = getpublichonks()
170 } else {
171 userid = u.UserID
172 if r.URL.Path == "/atme" {
173 templinfo["PageName"] = "atme"
174 honks = gethonksforme(userid)
175 } else {
176 templinfo["PageName"] = "home"
177 honks = gethonksforuser(userid)
178 honks = osmosis(honks, userid)
179 }
180 if len(honks) > 0 {
181 templinfo["TopXID"] = honks[0].XID
182 }
183 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
184 }
185
186 tname := "honkpage.html"
187 if topxid := r.FormValue("topxid"); topxid != "" {
188 for i, h := range honks {
189 if h.XID == topxid {
190 honks = honks[0:i]
191 break
192 }
193 }
194 log.Printf("topxid %d frags", len(honks))
195 tname = "honkfrags.html"
196 }
197
198 reverbolate(userid, honks)
199
200 templinfo["Honks"] = honks
201 templinfo["ShowRSS"] = true
202 templinfo["ServerMessage"] = serverMsg
203 if u == nil {
204 w.Header().Set("Cache-Control", "max-age=60")
205 } else {
206 w.Header().Set("Cache-Control", "max-age=0")
207 }
208 w.Header().Set("Content-Type", "text/html; charset=utf-8")
209
210 err := readviews.Execute(w, tname, templinfo)
211 if err != nil {
212 log.Print(err)
213 }
214}
215
216func showfunzone(w http.ResponseWriter, r *http.Request) {
217 var emunames, memenames []string
218 dir, err := os.Open("emus")
219 if err == nil {
220 emunames, _ = dir.Readdirnames(0)
221 dir.Close()
222 }
223 for i, e := range emunames {
224 if len(e) > 4 {
225 emunames[i] = e[:len(e)-4]
226 }
227 }
228 dir, err = os.Open("memes")
229 if err == nil {
230 memenames, _ = dir.Readdirnames(0)
231 dir.Close()
232 }
233 templinfo := getInfo(r)
234 templinfo["Emus"] = emunames
235 templinfo["Memes"] = memenames
236 err = readviews.Execute(w, "funzone.html", templinfo)
237 if err != nil {
238 log.Print(err)
239 }
240
241}
242
243func showrss(w http.ResponseWriter, r *http.Request) {
244 name := mux.Vars(r)["name"]
245
246 var honks []*Honk
247 if name != "" {
248 honks = gethonksbyuser(name, false)
249 } else {
250 honks = getpublichonks()
251 }
252 if len(honks) > 20 {
253 honks = honks[0:20]
254 }
255 reverbolate(-1, honks)
256
257 home := fmt.Sprintf("https://%s/", serverName)
258 base := home
259 if name != "" {
260 home += "u/" + name
261 name += " "
262 }
263 feed := rss.Feed{
264 Title: name + "honk",
265 Link: home,
266 Description: name + "honk rss",
267 Image: &rss.Image{
268 URL: base + "icon.png",
269 Title: name + "honk rss",
270 Link: home,
271 },
272 }
273 var modtime time.Time
274 for _, honk := range honks {
275 if !firstclass(honk) {
276 continue
277 }
278 desc := string(honk.HTML)
279 for _, d := range honk.Donks {
280 desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
281 d.URL, html.EscapeString(d.Name))
282 }
283
284 feed.Items = append(feed.Items, &rss.Item{
285 Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
286 Description: rss.CData{desc},
287 Link: honk.URL,
288 PubDate: honk.Date.Format(time.RFC1123),
289 Guid: &rss.Guid{IsPermaLink: true, Value: honk.URL},
290 })
291 if honk.Date.After(modtime) {
292 modtime = honk.Date
293 }
294 }
295 w.Header().Set("Cache-Control", "max-age=300")
296 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
297
298 err := feed.Write(w)
299 if err != nil {
300 log.Printf("error writing rss: %s", err)
301 }
302}
303
304func butwhatabout(name string) (*WhatAbout, error) {
305 row := stmtWhatAbout.QueryRow(name)
306 var user WhatAbout
307 var options string
308 err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &options)
309 user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
310 user.SkinnyCSS = strings.Contains(options, " skinny ")
311 return &user, err
312}
313
314func crappola(j junk.Junk) bool {
315 t, _ := j.GetString("type")
316 a, _ := j.GetString("actor")
317 o, _ := j.GetString("object")
318 if t == "Delete" && a == o {
319 log.Printf("crappola from %s", a)
320 return true
321 }
322 return false
323}
324
325func ping(user *WhatAbout, who string) {
326 box, err := getboxes(who)
327 if err != nil {
328 log.Printf("no inbox for ping: %s", err)
329 return
330 }
331 j := junk.New()
332 j["@context"] = itiswhatitis
333 j["type"] = "Ping"
334 j["id"] = user.URL + "/ping/" + xfiltrate()
335 j["actor"] = user.URL
336 j["to"] = who
337 keyname, key := ziggy(user.Name)
338 err = PostJunk(keyname, key, box.In, j)
339 if err != nil {
340 log.Printf("can't send ping: %s", err)
341 return
342 }
343 log.Printf("sent ping to %s: %s", who, j["id"])
344}
345
346func pong(user *WhatAbout, who string, obj string) {
347 box, err := getboxes(who)
348 if err != nil {
349 log.Printf("no inbox for pong %s : %s", who, err)
350 return
351 }
352 j := junk.New()
353 j["@context"] = itiswhatitis
354 j["type"] = "Pong"
355 j["id"] = user.URL + "/pong/" + xfiltrate()
356 j["actor"] = user.URL
357 j["to"] = who
358 j["object"] = obj
359 keyname, key := ziggy(user.Name)
360 err = PostJunk(keyname, key, box.In, j)
361 if err != nil {
362 log.Printf("can't send pong: %s", err)
363 return
364 }
365}
366
367func inbox(w http.ResponseWriter, r *http.Request) {
368 name := mux.Vars(r)["name"]
369 user, err := butwhatabout(name)
370 if err != nil {
371 http.NotFound(w, r)
372 return
373 }
374 var buf bytes.Buffer
375 io.Copy(&buf, r.Body)
376 payload := buf.Bytes()
377 j, err := junk.Read(bytes.NewReader(payload))
378 if err != nil {
379 log.Printf("bad payload: %s", err)
380 io.WriteString(os.Stdout, "bad payload\n")
381 os.Stdout.Write(payload)
382 io.WriteString(os.Stdout, "\n")
383 return
384 }
385 if crappola(j) {
386 return
387 }
388 keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
389 if err != nil {
390 log.Printf("inbox message failed signature: %s", err)
391 if keyname != "" {
392 keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
393 if err != nil {
394 log.Printf("still failed: %s", err)
395 }
396 }
397 if err != nil {
398 return
399 }
400 }
401 what, _ := j.GetString("type")
402 if what == "Like" {
403 return
404 }
405 who, _ := j.GetString("actor")
406 origin := keymatch(keyname, who)
407 if origin == "" {
408 log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
409 return
410 }
411 objid, _ := j.GetString("id")
412 if thoudostbitethythumb(user.ID, []string{who}, objid) {
413 log.Printf("ignoring thumb sucker %s", who)
414 return
415 }
416 switch what {
417 case "Ping":
418 obj, _ := j.GetString("id")
419 log.Printf("ping from %s: %s", who, obj)
420 pong(user, who, obj)
421 case "Pong":
422 obj, _ := j.GetString("object")
423 log.Printf("pong from %s: %s", who, obj)
424 case "Follow":
425 obj, _ := j.GetString("object")
426 if obj == user.URL {
427 log.Printf("updating honker follow: %s", who)
428 stmtSaveDub.Exec(user.ID, who, who, "dub")
429 go rubadubdub(user, j)
430 } else {
431 log.Printf("can't follow %s", obj)
432 }
433 case "Accept":
434 log.Printf("updating honker accept: %s", who)
435 _, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
436 if err != nil {
437 log.Printf("error updating honker: %s", err)
438 return
439 }
440 case "Update":
441 obj, ok := j.GetMap("object")
442 if ok {
443 what, _ := obj.GetString("type")
444 switch what {
445 case "Person":
446 return
447 case "Question":
448 return
449 }
450 }
451 log.Printf("unknown Update activity")
452 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
453 j.Write(fd)
454 io.WriteString(fd, "\n")
455 fd.Close()
456
457 case "Undo":
458 obj, ok := j.GetMap("object")
459 if !ok {
460 log.Printf("unknown undo no object")
461 } else {
462 what, _ := obj.GetString("type")
463 switch what {
464 case "Follow":
465 log.Printf("updating honker undo: %s", who)
466 _, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
467 if err != nil {
468 log.Printf("error updating honker: %s", err)
469 return
470 }
471 case "Announce":
472 xid, _ := obj.GetString("object")
473 log.Printf("undo announce: %s", xid)
474 case "Like":
475 default:
476 log.Printf("unknown undo: %s", what)
477 }
478 }
479 default:
480 go consumeactivity(user, j, origin)
481 }
482}
483
484func ximport(w http.ResponseWriter, r *http.Request) {
485 xid := r.FormValue("xid")
486 p, _ := investigate(xid)
487 if p != nil {
488 xid = p.XID
489 }
490 j, err := GetJunk(xid)
491 if err != nil {
492 http.Error(w, "error getting external object", http.StatusInternalServerError)
493 log.Printf("error getting external object: %s", err)
494 return
495 }
496 log.Printf("importing %s", xid)
497 u := login.GetUserInfo(r)
498 user, _ := butwhatabout(u.Username)
499
500 what, _ := j.GetString("type")
501 if isactor(what) {
502 outbox, _ := j.GetString("outbox")
503 gimmexonks(user, outbox)
504 http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
505 return
506 }
507 xonk := xonkxonk(user, j, originate(xid))
508 convoy := ""
509 if xonk != nil {
510 convoy = xonk.Convoy
511 savexonk(user, xonk)
512 }
513 http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
514}
515
516func xzone(w http.ResponseWriter, r *http.Request) {
517 u := login.GetUserInfo(r)
518 rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
519 if err != nil {
520 log.Printf("query err: %s", err)
521 return
522 }
523 defer rows.Close()
524 var honkers []Honker
525 for rows.Next() {
526 var xid string
527 rows.Scan(&xid)
528 honkers = append(honkers, Honker{XID: xid})
529 }
530 rows.Close()
531 for i, _ := range honkers {
532 _, honkers[i].Handle = handles(honkers[i].XID)
533 }
534 templinfo := getInfo(r)
535 templinfo["XCSRF"] = login.GetCSRF("ximport", r)
536 templinfo["Honkers"] = honkers
537 err = readviews.Execute(w, "xzone.html", templinfo)
538 if err != nil {
539 log.Print(err)
540 }
541}
542
543func outbox(w http.ResponseWriter, r *http.Request) {
544 name := mux.Vars(r)["name"]
545 user, err := butwhatabout(name)
546 if err != nil {
547 http.NotFound(w, r)
548 return
549 }
550 if stealthed(r) {
551 http.NotFound(w, r)
552 return
553 }
554
555 honks := gethonksbyuser(name, false)
556 if len(honks) > 20 {
557 honks = honks[0:20]
558 }
559
560 var jonks []junk.Junk
561 for _, h := range honks {
562 j, _ := jonkjonk(user, h)
563 jonks = append(jonks, j)
564 }
565
566 j := junk.New()
567 j["@context"] = itiswhatitis
568 j["id"] = user.URL + "/outbox"
569 j["type"] = "OrderedCollection"
570 j["totalItems"] = len(jonks)
571 j["orderedItems"] = jonks
572
573 w.Header().Set("Content-Type", theonetruename)
574 j.Write(w)
575}
576
577func emptiness(w http.ResponseWriter, r *http.Request) {
578 name := mux.Vars(r)["name"]
579 user, err := butwhatabout(name)
580 if err != nil {
581 http.NotFound(w, r)
582 return
583 }
584 colname := "/followers"
585 if strings.HasSuffix(r.URL.Path, "/following") {
586 colname = "/following"
587 }
588 j := junk.New()
589 j["@context"] = itiswhatitis
590 j["id"] = user.URL + colname
591 j["type"] = "OrderedCollection"
592 j["totalItems"] = 0
593 j["orderedItems"] = []junk.Junk{}
594
595 w.Header().Set("Content-Type", theonetruename)
596 j.Write(w)
597}
598
599func showuser(w http.ResponseWriter, r *http.Request) {
600 name := mux.Vars(r)["name"]
601 user, err := butwhatabout(name)
602 if err != nil {
603 log.Printf("user not found %s: %s", name, err)
604 http.NotFound(w, r)
605 return
606 }
607 if friendorfoe(r.Header.Get("Accept")) {
608 j := asjonker(user)
609 w.Header().Set("Content-Type", theonetruename)
610 j.Write(w)
611 return
612 }
613 u := login.GetUserInfo(r)
614 honks := gethonksbyuser(name, u != nil && u.Username == name)
615 honkpage(w, r, u, user, honks, "")
616}
617
618func showhonker(w http.ResponseWriter, r *http.Request) {
619 u := login.GetUserInfo(r)
620 name := mux.Vars(r)["name"]
621 var honks []*Honk
622 if name == "" {
623 name = r.FormValue("xid")
624 honks = gethonksbyxonker(u.UserID, name)
625 } else {
626 honks = gethonksbyhonker(u.UserID, name)
627 }
628 name = html.EscapeString(name)
629 msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
630 honkpage(w, r, u, nil, honks, template.HTML(msg))
631}
632
633func showcombo(w http.ResponseWriter, r *http.Request) {
634 name := mux.Vars(r)["name"]
635 u := login.GetUserInfo(r)
636 honks := gethonksbycombo(u.UserID, name)
637 honks = osmosis(honks, u.UserID)
638 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
639}
640func showconvoy(w http.ResponseWriter, r *http.Request) {
641 c := r.FormValue("c")
642 u := login.GetUserInfo(r)
643 honks := gethonksbyconvoy(u.UserID, c)
644 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
645}
646func showsearch(w http.ResponseWriter, r *http.Request) {
647 q := r.FormValue("q")
648 u := login.GetUserInfo(r)
649 honks := gethonksbysearch(u.UserID, q)
650 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks for search: "+q)))
651}
652func showontology(w http.ResponseWriter, r *http.Request) {
653 name := mux.Vars(r)["name"]
654 u := login.GetUserInfo(r)
655 var userid int64 = -1
656 if u != nil {
657 userid = u.UserID
658 }
659 honks := gethonksbyontology(userid, "#"+name)
660 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by ontology: "+name)))
661}
662
663func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) {
664 u := login.GetUserInfo(r)
665 var userid int64 = -1
666 if u != nil {
667 userid = u.UserID
668 }
669 rows, err := stmtSelectOnts.Query(userid)
670 if err != nil {
671 log.Printf("selection error: %s", err)
672 return
673 }
674 var onts [][]string
675 for rows.Next() {
676 var o string
677 err := rows.Scan(&o)
678 if err != nil {
679 log.Printf("error scanning ont: %s", err)
680 continue
681 }
682 onts = append(onts, []string{o, o[1:]})
683 }
684 if u == nil {
685 w.Header().Set("Cache-Control", "max-age=300")
686 }
687 templinfo := getInfo(r)
688 templinfo["Onts"] = onts
689 err = readviews.Execute(w, "onts.html", templinfo)
690 if err != nil {
691 log.Print(err)
692 }
693}
694
695func showhonk(w http.ResponseWriter, r *http.Request) {
696 name := mux.Vars(r)["name"]
697 user, err := butwhatabout(name)
698 if err != nil {
699 http.NotFound(w, r)
700 return
701 }
702 if stealthed(r) {
703 http.NotFound(w, r)
704 return
705 }
706
707 xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
708 honk := getxonk(user.ID, xid)
709 if honk == nil {
710 http.NotFound(w, r)
711 return
712 }
713 u := login.GetUserInfo(r)
714 if u != nil && u.UserID != user.ID {
715 u = nil
716 }
717 if !honk.Public {
718 if u == nil {
719 http.NotFound(w, r)
720 return
721
722 }
723 honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
724 return
725 }
726 rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
727 if friendorfoe(r.Header.Get("Accept")) {
728 for _, h := range rawhonks {
729 if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
730 honk.Replies = append(honk.Replies, h)
731 }
732 }
733 donksforhonks([]*Honk{honk})
734 _, j := jonkjonk(user, honk)
735 j["@context"] = itiswhatitis
736 w.Header().Set("Content-Type", theonetruename)
737 j.Write(w)
738 return
739 }
740 var honks []*Honk
741 for _, h := range rawhonks {
742 if h.Public && (h.Whofore == 2 || h.IsAcked()) {
743 honks = append(honks, h)
744 }
745 }
746
747 honkpage(w, r, u, nil, honks, "one honk maybe more")
748}
749
750func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
751 honks []*Honk, infomsg template.HTML) {
752 templinfo := getInfo(r)
753 var userid int64 = -1
754 if u != nil {
755 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
756 userid = u.UserID
757 }
758 if u == nil {
759 w.Header().Set("Cache-Control", "max-age=60")
760 }
761 reverbolate(userid, honks)
762 if user != nil {
763 filt := htfilter.New()
764 templinfo["Name"] = user.Name
765 whatabout := user.About
766 whatabout = obfusbreak(user.About)
767 templinfo["WhatAbout"], _ = filt.String(whatabout)
768 }
769 templinfo["Honks"] = honks
770 templinfo["ServerMessage"] = infomsg
771 err := readviews.Execute(w, "honkpage.html", templinfo)
772 if err != nil {
773 log.Print(err)
774 }
775}
776
777func saveuser(w http.ResponseWriter, r *http.Request) {
778 whatabout := r.FormValue("whatabout")
779 u := login.GetUserInfo(r)
780 db := opendatabase()
781 options := ""
782 if r.FormValue("skinny") == "skinny" {
783 options += " skinny "
784 }
785 _, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
786 if err != nil {
787 log.Printf("error bouting what: %s", err)
788 }
789
790 http.Redirect(w, r, "/account", http.StatusSeeOther)
791}
792
793func gethonkers(userid int64) []*Honker {
794 rows, err := stmtHonkers.Query(userid)
795 if err != nil {
796 log.Printf("error querying honkers: %s", err)
797 return nil
798 }
799 defer rows.Close()
800 var honkers []*Honker
801 for rows.Next() {
802 var f Honker
803 var combos string
804 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
805 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
806 if err != nil {
807 log.Printf("error scanning honker: %s", err)
808 return nil
809 }
810 honkers = append(honkers, &f)
811 }
812 return honkers
813}
814
815func getdubs(userid int64) []*Honker {
816 rows, err := stmtDubbers.Query(userid)
817 if err != nil {
818 log.Printf("error querying dubs: %s", err)
819 return nil
820 }
821 defer rows.Close()
822 var honkers []*Honker
823 for rows.Next() {
824 var f Honker
825 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
826 if err != nil {
827 log.Printf("error scanning honker: %s", err)
828 return nil
829 }
830 honkers = append(honkers, &f)
831 }
832 return honkers
833}
834
835func allusers() []login.UserInfo {
836 var users []login.UserInfo
837 rows, _ := opendatabase().Query("select userid, username from users")
838 defer rows.Close()
839 for rows.Next() {
840 var u login.UserInfo
841 rows.Scan(&u.UserID, &u.Username)
842 users = append(users, u)
843 }
844 return users
845}
846
847func getxonk(userid int64, xid string) *Honk {
848 h := new(Honk)
849 var dt, aud, onts string
850 row := stmtOneXonk.QueryRow(userid, xid)
851 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
852 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
853 if err != nil {
854 if err != sql.ErrNoRows {
855 log.Printf("error scanning xonk: %s", err)
856 }
857 return nil
858 }
859 h.Date, _ = time.Parse(dbtimeformat, dt)
860 h.Audience = strings.Split(aud, " ")
861 h.Public = !keepitquiet(h.Audience)
862 if len(onts) > 0 {
863 h.Onts = strings.Split(onts, " ")
864 }
865 return h
866}
867
868func getbonk(userid int64, xid string) *Honk {
869 h := new(Honk)
870 var dt, aud, onts string
871 row := stmtOneBonk.QueryRow(userid, xid)
872 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
873 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
874 if err != nil {
875 if err != sql.ErrNoRows {
876 log.Printf("error scanning xonk: %s", err)
877 }
878 return nil
879 }
880 h.Date, _ = time.Parse(dbtimeformat, dt)
881 h.Audience = strings.Split(aud, " ")
882 h.Public = !keepitquiet(h.Audience)
883 if len(onts) > 0 {
884 h.Onts = strings.Split(onts, " ")
885 }
886 return h
887}
888
889func getpublichonks() []*Honk {
890 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
891 rows, err := stmtPublicHonks.Query(dt)
892 return getsomehonks(rows, err)
893}
894func gethonksbyuser(name string, includeprivate bool) []*Honk {
895 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
896 whofore := 2
897 if includeprivate {
898 whofore = 3
899 }
900 rows, err := stmtUserHonks.Query(whofore, name, dt)
901 return getsomehonks(rows, err)
902}
903func gethonksforuser(userid int64) []*Honk {
904 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
905 rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
906 return getsomehonks(rows, err)
907}
908func gethonksforme(userid int64) []*Honk {
909 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
910 rows, err := stmtHonksForMe.Query(userid, dt, userid)
911 return getsomehonks(rows, err)
912}
913func gethonksbyhonker(userid int64, honker string) []*Honk {
914 rows, err := stmtHonksByHonker.Query(userid, honker, userid)
915 return getsomehonks(rows, err)
916}
917func gethonksbyxonker(userid int64, xonker string) []*Honk {
918 rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
919 return getsomehonks(rows, err)
920}
921func gethonksbycombo(userid int64, combo string) []*Honk {
922 combo = "% " + combo + " %"
923 rows, err := stmtHonksByCombo.Query(userid, combo, userid)
924 return getsomehonks(rows, err)
925}
926func gethonksbyconvoy(userid int64, convoy string) []*Honk {
927 rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
928 honks := getsomehonks(rows, err)
929 for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
930 honks[i], honks[j] = honks[j], honks[i]
931 }
932 return honks
933}
934func gethonksbysearch(userid int64, q string) []*Honk {
935 q = "%" + q + "%"
936 rows, err := stmtHonksBySearch.Query(userid, q)
937 honks := getsomehonks(rows, err)
938 return honks
939}
940func gethonksbyontology(userid int64, name string) []*Honk {
941 rows, err := stmtHonksByOntology.Query(name, userid, userid)
942 honks := getsomehonks(rows, err)
943 return honks
944}
945
946func getsomehonks(rows *sql.Rows, err error) []*Honk {
947 if err != nil {
948 log.Printf("error querying honks: %s", err)
949 return nil
950 }
951 defer rows.Close()
952 var honks []*Honk
953 for rows.Next() {
954 var h Honk
955 var dt, aud, onts string
956 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID,
957 &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
958 if err != nil {
959 log.Printf("error scanning honks: %s", err)
960 return nil
961 }
962 h.Date, _ = time.Parse(dbtimeformat, dt)
963 h.Audience = strings.Split(aud, " ")
964 h.Public = !keepitquiet(h.Audience)
965 if len(onts) > 0 {
966 h.Onts = strings.Split(onts, " ")
967 }
968 honks = append(honks, &h)
969 }
970 rows.Close()
971 donksforhonks(honks)
972 return honks
973}
974
975func donksforhonks(honks []*Honk) {
976 db := opendatabase()
977 var ids []string
978 hmap := make(map[int64]*Honk)
979 for _, h := range honks {
980 ids = append(ids, fmt.Sprintf("%d", h.ID))
981 hmap[h.ID] = h
982 }
983 q := fmt.Sprintf("select honkid, donks.fileid, xid, name, description, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
984 rows, err := db.Query(q)
985 if err != nil {
986 log.Printf("error querying donks: %s", err)
987 return
988 }
989 defer rows.Close()
990 for rows.Next() {
991 var hid int64
992 var d Donk
993 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local)
994 if err != nil {
995 log.Printf("error scanning donk: %s", err)
996 continue
997 }
998 h := hmap[hid]
999 h.Donks = append(h.Donks, &d)
1000 }
1001}
1002
1003func savebonk(w http.ResponseWriter, r *http.Request) {
1004 xid := r.FormValue("xid")
1005 userinfo := login.GetUserInfo(r)
1006 user, _ := butwhatabout(userinfo.Username)
1007
1008 log.Printf("bonking %s", xid)
1009
1010 xonk := getxonk(userinfo.UserID, xid)
1011 if xonk == nil {
1012 return
1013 }
1014 if !xonk.Public {
1015 return
1016 }
1017 donksforhonks([]*Honk{xonk})
1018
1019 _, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
1020 if err != nil {
1021 log.Printf("error acking bonk: %s", err)
1022 }
1023
1024 oonker := xonk.Oonker
1025 if oonker == "" {
1026 oonker = xonk.Honker
1027 }
1028 dt := time.Now().UTC()
1029 bonk := Honk{
1030 UserID: userinfo.UserID,
1031 Username: userinfo.Username,
1032 What: "bonk",
1033 Honker: user.URL,
1034 XID: xonk.XID,
1035 Date: dt,
1036 Donks: xonk.Donks,
1037 Convoy: xonk.Convoy,
1038 Audience: []string{thewholeworld, oonker},
1039 Public: true,
1040 }
1041
1042 aud := strings.Join(bonk.Audience, " ")
1043 whofore := 2
1044 onts := xonk.Onts
1045 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
1046 dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
1047 xonk.Precis, oonker, 0, strings.Join(onts, " "))
1048 if err != nil {
1049 log.Printf("error saving bonk: %s", err)
1050 return
1051 }
1052 bonk.ID, _ = res.LastInsertId()
1053 for _, d := range bonk.Donks {
1054 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
1055 if err != nil {
1056 log.Printf("err saving donk: %s", err)
1057 return
1058 }
1059 }
1060 for _, o := range onts {
1061 _, err = stmtSaveOnts.Exec(strings.ToLower(o), bonk.ID)
1062 if err != nil {
1063 log.Printf("error saving ont: %s", err)
1064 }
1065 }
1066
1067 go honkworldwide(user, &bonk)
1068}
1069
1070func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
1071 zonk := Honk{
1072 What: what,
1073 XID: xonk.XID,
1074 Date: time.Now().UTC(),
1075 Audience: oneofakind(xonk.Audience),
1076 }
1077 zonk.Public = !keepitquiet(zonk.Audience)
1078
1079 log.Printf("announcing %sed honk: %s", what, xonk.XID)
1080 go honkworldwide(user, &zonk)
1081}
1082
1083func zonkit(w http.ResponseWriter, r *http.Request) {
1084 wherefore := r.FormValue("wherefore")
1085 what := r.FormValue("what")
1086 userinfo := login.GetUserInfo(r)
1087 user, _ := butwhatabout(userinfo.Username)
1088
1089 if wherefore == "ack" {
1090 xonk := getxonk(userinfo.UserID, what)
1091 if xonk != nil {
1092 _, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1093 if err != nil {
1094 log.Printf("error acking: %s", err)
1095 }
1096 sendzonkofsorts(xonk, user, "ack")
1097 }
1098 return
1099 }
1100
1101 if wherefore == "deack" {
1102 xonk := getxonk(userinfo.UserID, what)
1103 if xonk != nil {
1104 _, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1105 if err != nil {
1106 log.Printf("error deacking: %s", err)
1107 }
1108 sendzonkofsorts(xonk, user, "deack")
1109 }
1110 return
1111 }
1112
1113 if wherefore == "unbonk" {
1114 xonk := getbonk(userinfo.UserID, what)
1115 if xonk != nil {
1116 _, err := stmtZonkDonks.Exec(xonk.ID)
1117 if err != nil {
1118 log.Printf("error zonking: %s", err)
1119 }
1120 _, err = stmtZonkIt.Exec(xonk.ID)
1121 if err != nil {
1122 log.Printf("error zonking: %s", err)
1123 }
1124 xonk = getxonk(userinfo.UserID, what)
1125 _, err = stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1126 if err != nil {
1127 log.Printf("error unbonking: %s", err)
1128 }
1129 sendzonkofsorts(xonk, user, "unbonk")
1130 }
1131 return
1132 }
1133
1134 log.Printf("zonking %s %s", wherefore, what)
1135 if wherefore == "zonk" {
1136 xonk := getxonk(userinfo.UserID, what)
1137 if xonk != nil {
1138 _, err := stmtZonkDonks.Exec(xonk.ID)
1139 if err != nil {
1140 log.Printf("error zonking: %s", err)
1141 }
1142 _, err = stmtZonkIt.Exec(xonk.ID)
1143 if err != nil {
1144 log.Printf("error zonking: %s", err)
1145 }
1146 if xonk.Whofore == 2 || xonk.Whofore == 3 {
1147 sendzonkofsorts(xonk, user, "zonk")
1148 }
1149 }
1150 }
1151 _, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1152 if err != nil {
1153 log.Printf("error saving zonker: %s", err)
1154 return
1155 }
1156}
1157
1158func savehonk(w http.ResponseWriter, r *http.Request) {
1159 rid := r.FormValue("rid")
1160 noise := r.FormValue("noise")
1161
1162 userinfo := login.GetUserInfo(r)
1163 user, _ := butwhatabout(userinfo.Username)
1164
1165 dt := time.Now().UTC()
1166 xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1167 what := "honk"
1168 if rid != "" {
1169 what = "tonk"
1170 }
1171 honk := Honk{
1172 UserID: userinfo.UserID,
1173 Username: userinfo.Username,
1174 What: "honk",
1175 Honker: user.URL,
1176 XID: xid,
1177 Date: dt,
1178 }
1179 if strings.HasPrefix(noise, "DZ:") {
1180 idx := strings.Index(noise, "\n")
1181 if idx == -1 {
1182 honk.Precis = noise
1183 noise = ""
1184 } else {
1185 honk.Precis = noise[:idx]
1186 noise = noise[idx+1:]
1187 }
1188 }
1189 noise = quickrename(noise, userinfo.UserID)
1190 noise = hooterize(noise)
1191 noise = strings.TrimSpace(noise)
1192 honk.Precis = strings.TrimSpace(honk.Precis)
1193
1194 var convoy string
1195 if rid != "" {
1196 xonk := getxonk(userinfo.UserID, rid)
1197 if xonk != nil {
1198 if xonk.Public {
1199 honk.Audience = append(honk.Audience, xonk.Audience...)
1200 }
1201 convoy = xonk.Convoy
1202 } else {
1203 xonkaud, c := whosthere(rid)
1204 honk.Audience = append(honk.Audience, xonkaud...)
1205 convoy = c
1206 }
1207 for i, a := range honk.Audience {
1208 if a == thewholeworld {
1209 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1210 break
1211 }
1212 }
1213 honk.RID = rid
1214 } else {
1215 honk.Audience = []string{thewholeworld}
1216 }
1217 if noise != "" && noise[0] == '@' {
1218 honk.Audience = append(grapevine(noise), honk.Audience...)
1219 } else {
1220 honk.Audience = append(honk.Audience, grapevine(noise)...)
1221 }
1222 if convoy == "" {
1223 convoy = "data:,electrichonkytonk-" + xfiltrate()
1224 }
1225 butnottooloud(honk.Audience)
1226 honk.Audience = oneofakind(honk.Audience)
1227 if len(honk.Audience) == 0 {
1228 log.Printf("honk to nowhere")
1229 http.Error(w, "honk to nowhere...", http.StatusNotFound)
1230 return
1231 }
1232 honk.Public = !keepitquiet(honk.Audience)
1233 noise = obfusbreak(noise)
1234 honk.Noise = noise
1235 honk.Convoy = convoy
1236
1237 donkxid := r.FormValue("donkxid")
1238 if donkxid == "" {
1239 file, filehdr, err := r.FormFile("donk")
1240 if err == nil {
1241 var buf bytes.Buffer
1242 io.Copy(&buf, file)
1243 file.Close()
1244 data := buf.Bytes()
1245 xid := xfiltrate()
1246 var media, name string
1247 img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1248 if err == nil {
1249 data = img.Data
1250 format := img.Format
1251 media = "image/" + format
1252 if format == "jpeg" {
1253 format = "jpg"
1254 }
1255 name = xid + "." + format
1256 xid = name
1257 } else {
1258 maxsize := 100000
1259 if len(data) > maxsize {
1260 log.Printf("bad image: %s too much text: %d", err, len(data))
1261 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1262 return
1263 }
1264 for i := 0; i < len(data); i++ {
1265 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1266 log.Printf("bad image: %s not text: %d", err, data[i])
1267 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1268 return
1269 }
1270 }
1271 media = "text/plain"
1272 name = filehdr.Filename
1273 if name == "" {
1274 name = xid + ".txt"
1275 }
1276 xid += ".txt"
1277 }
1278 desc := r.FormValue("donkdesc")
1279 if desc == "" {
1280 desc = name
1281 }
1282 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1283 res, err := stmtSaveFile.Exec(xid, name, desc, url, media, 1, data)
1284 if err != nil {
1285 log.Printf("unable to save image: %s", err)
1286 return
1287 }
1288 var d Donk
1289 d.FileID, _ = res.LastInsertId()
1290 honk.Donks = append(honk.Donks, &d)
1291 donkxid = d.XID
1292 }
1293 } else {
1294 xid := donkxid
1295 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1296 var donk Donk
1297 row := stmtFindFile.QueryRow(url)
1298 err := row.Scan(&donk.FileID)
1299 if err == nil {
1300 honk.Donks = append(honk.Donks, &donk)
1301 } else {
1302 log.Printf("can't find file: %s", xid)
1303 }
1304 }
1305 herd := herdofemus(honk.Noise)
1306 for _, e := range herd {
1307 donk := savedonk(e.ID, e.Name, e.Name, "image/png", true)
1308 if donk != nil {
1309 donk.Name = e.Name
1310 honk.Donks = append(honk.Donks, donk)
1311 }
1312 }
1313 memetize(&honk)
1314
1315 aud := strings.Join(honk.Audience, " ")
1316 whofore := 2
1317 if !honk.Public {
1318 whofore = 3
1319 }
1320 if r.FormValue("preview") == "preview" {
1321 honks := []*Honk{&honk}
1322 reverbolate(userinfo.UserID, honks)
1323 templinfo := getInfo(r)
1324 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1325 templinfo["Honks"] = honks
1326 templinfo["InReplyTo"] = r.FormValue("rid")
1327 templinfo["Noise"] = r.FormValue("noise")
1328 templinfo["SavedFile"] = donkxid
1329 templinfo["ServerMessage"] = "honk preview"
1330 err := readviews.Execute(w, "honkpage.html", templinfo)
1331 if err != nil {
1332 log.Print(err)
1333 }
1334 return
1335 }
1336 honk.Onts = oneofakind(ontologies(honk.Noise))
1337 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1338 dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1339 honk.Precis, honk.Oonker, 0, strings.Join(honk.Onts, " "))
1340 if err != nil {
1341 log.Printf("error saving honk: %s", err)
1342 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1343 return
1344 }
1345 honk.ID, _ = res.LastInsertId()
1346 for _, d := range honk.Donks {
1347 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1348 if err != nil {
1349 log.Printf("err saving donk: %s", err)
1350 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1351 return
1352 }
1353 }
1354 for _, o := range honk.Onts {
1355 _, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1356 if err != nil {
1357 log.Printf("error saving ont: %s", err)
1358 }
1359 }
1360 honk.Donks = nil
1361 donksforhonks([]*Honk{&honk})
1362
1363 go honkworldwide(user, &honk)
1364
1365 http.Redirect(w, r, xid, http.StatusSeeOther)
1366}
1367
1368func showhonkers(w http.ResponseWriter, r *http.Request) {
1369 userinfo := login.GetUserInfo(r)
1370 templinfo := getInfo(r)
1371 templinfo["Honkers"] = gethonkers(userinfo.UserID)
1372 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1373 err := readviews.Execute(w, "honkers.html", templinfo)
1374 if err != nil {
1375 log.Print(err)
1376 }
1377}
1378
1379func showcombos(w http.ResponseWriter, r *http.Request) {
1380 userinfo := login.GetUserInfo(r)
1381 templinfo := getInfo(r)
1382 honkers := gethonkers(userinfo.UserID)
1383 var combos []string
1384 for _, h := range honkers {
1385 combos = append(combos, h.Combos...)
1386 }
1387 for i, c := range combos {
1388 if c == "-" {
1389 combos[i] = ""
1390 }
1391 }
1392 combos = oneofakind(combos)
1393 sort.Strings(combos)
1394 templinfo["Combos"] = combos
1395 err := readviews.Execute(w, "combos.html", templinfo)
1396 if err != nil {
1397 log.Print(err)
1398 }
1399}
1400
1401func savehonker(w http.ResponseWriter, r *http.Request) {
1402 u := login.GetUserInfo(r)
1403 name := r.FormValue("name")
1404 url := r.FormValue("url")
1405 peep := r.FormValue("peep")
1406 combos := r.FormValue("combos")
1407 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1408
1409 if honkerid > 0 {
1410 goodbye := r.FormValue("goodbye")
1411 if goodbye == "F" {
1412 db := opendatabase()
1413 row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1414 honkerid, u.UserID)
1415 var xid string
1416 err := row.Scan(&xid)
1417 if err != nil {
1418 log.Printf("can't get honker xid: %s", err)
1419 return
1420 }
1421 log.Printf("unsubscribing from %s", xid)
1422 user, _ := butwhatabout(u.Username)
1423 go itakeitallback(user, xid)
1424 _, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1425 if err != nil {
1426 log.Printf("error updating honker: %s", err)
1427 return
1428 }
1429
1430 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1431 return
1432 }
1433 combos = " " + strings.TrimSpace(combos) + " "
1434 _, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1435 if err != nil {
1436 log.Printf("update honker err: %s", err)
1437 return
1438 }
1439 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1440 }
1441
1442 flavor := "presub"
1443 if peep == "peep" {
1444 flavor = "peep"
1445 }
1446 p, err := investigate(url)
1447 if err != nil {
1448 http.Error(w, "error investigating: "+err.Error(), http.StatusInternalServerError)
1449 log.Printf("failed to investigate honker")
1450 return
1451 }
1452 url = p.XID
1453 if name == "" {
1454 name = p.Handle
1455 }
1456 _, err = stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1457 if err != nil {
1458 log.Print(err)
1459 return
1460 }
1461 if flavor == "presub" {
1462 user, _ := butwhatabout(u.Username)
1463 go subsub(user, url)
1464 }
1465 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1466}
1467
1468type Zonker struct {
1469 ID int64
1470 Name string
1471 Wherefore string
1472}
1473
1474func zonkzone(w http.ResponseWriter, r *http.Request) {
1475 userinfo := login.GetUserInfo(r)
1476 rows, err := stmtGetZonkers.Query(userinfo.UserID)
1477 if err != nil {
1478 log.Printf("err: %s", err)
1479 return
1480 }
1481 defer rows.Close()
1482 var zonkers []Zonker
1483 for rows.Next() {
1484 var z Zonker
1485 rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1486 zonkers = append(zonkers, z)
1487 }
1488 sort.Slice(zonkers, func(i, j int) bool {
1489 w1 := zonkers[i].Wherefore
1490 w2 := zonkers[j].Wherefore
1491 if w1 == w2 {
1492 return zonkers[i].Name < zonkers[j].Name
1493 }
1494 if w1 == "zonvoy" {
1495 w1 = "zzzzzzz"
1496 }
1497 if w2 == "zonvoy" {
1498 w2 = "zzzzzzz"
1499 }
1500 return w1 < w2
1501 })
1502
1503 templinfo := getInfo(r)
1504 templinfo["Zonkers"] = zonkers
1505 templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1506 err = readviews.Execute(w, "zonkers.html", templinfo)
1507 if err != nil {
1508 log.Print(err)
1509 }
1510}
1511
1512func zonkzonk(w http.ResponseWriter, r *http.Request) {
1513 userinfo := login.GetUserInfo(r)
1514 itsok := r.FormValue("itsok")
1515 if itsok == "iforgiveyou" {
1516 zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1517 db := opendatabase()
1518 db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1519 userinfo.UserID, zonkerid)
1520 bitethethumbs()
1521 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1522 return
1523 }
1524 wherefore := r.FormValue("wherefore")
1525 name := r.FormValue("name")
1526 if name == "" {
1527 return
1528 }
1529 switch wherefore {
1530 case "zonker":
1531 case "zomain":
1532 case "zonvoy":
1533 case "zord":
1534 case "zilence":
1535 default:
1536 return
1537 }
1538 db := opendatabase()
1539 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1540 userinfo.UserID, name, wherefore)
1541 if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1542 bitethethumbs()
1543 }
1544
1545 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1546}
1547
1548func accountpage(w http.ResponseWriter, r *http.Request) {
1549 u := login.GetUserInfo(r)
1550 user, _ := butwhatabout(u.Username)
1551 templinfo := getInfo(r)
1552 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1553 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1554 templinfo["User"] = user
1555 err := readviews.Execute(w, "account.html", templinfo)
1556 if err != nil {
1557 log.Print(err)
1558 }
1559}
1560
1561func dochpass(w http.ResponseWriter, r *http.Request) {
1562 err := login.ChangePassword(w, r)
1563 if err != nil {
1564 log.Printf("error changing password: %s", err)
1565 }
1566 http.Redirect(w, r, "/account", http.StatusSeeOther)
1567}
1568
1569func fingerlicker(w http.ResponseWriter, r *http.Request) {
1570 orig := r.FormValue("resource")
1571
1572 log.Printf("finger lick: %s", orig)
1573
1574 if strings.HasPrefix(orig, "acct:") {
1575 orig = orig[5:]
1576 }
1577
1578 name := orig
1579 idx := strings.LastIndexByte(name, '/')
1580 if idx != -1 {
1581 name = name[idx+1:]
1582 if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1583 log.Printf("foreign request rejected")
1584 name = ""
1585 }
1586 } else {
1587 idx = strings.IndexByte(name, '@')
1588 if idx != -1 {
1589 name = name[:idx]
1590 if name+"@"+serverName != orig {
1591 log.Printf("foreign request rejected")
1592 name = ""
1593 }
1594 }
1595 }
1596 user, err := butwhatabout(name)
1597 if err != nil {
1598 http.NotFound(w, r)
1599 return
1600 }
1601
1602 j := junk.New()
1603 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1604 j["aliases"] = []string{user.URL}
1605 var links []junk.Junk
1606 l := junk.New()
1607 l["rel"] = "self"
1608 l["type"] = `application/activity+json`
1609 l["href"] = user.URL
1610 links = append(links, l)
1611 j["links"] = links
1612
1613 w.Header().Set("Cache-Control", "max-age=3600")
1614 w.Header().Set("Content-Type", "application/jrd+json")
1615 j.Write(w)
1616}
1617
1618func somedays() string {
1619 secs := 432000 + notrand.Int63n(432000)
1620 return fmt.Sprintf("%d", secs)
1621}
1622
1623func avatate(w http.ResponseWriter, r *http.Request) {
1624 n := r.FormValue("a")
1625 a := avatar(n)
1626 w.Header().Set("Cache-Control", "max-age="+somedays())
1627 w.Write(a)
1628}
1629
1630func servecss(w http.ResponseWriter, r *http.Request) {
1631 data, _ := ioutil.ReadFile("views" + r.URL.Path)
1632 s := css.Process(string(data))
1633 w.Header().Set("Cache-Control", "max-age=7776000")
1634 w.Header().Set("Content-Type", "text/css; charset=utf-8")
1635 w.Write([]byte(s))
1636}
1637func servehtml(w http.ResponseWriter, r *http.Request) {
1638 templinfo := getInfo(r)
1639 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1640 if err != nil {
1641 log.Print(err)
1642 }
1643}
1644func serveemu(w http.ResponseWriter, r *http.Request) {
1645 xid := mux.Vars(r)["xid"]
1646 w.Header().Set("Cache-Control", "max-age="+somedays())
1647 http.ServeFile(w, r, "emus/"+xid)
1648}
1649func servememe(w http.ResponseWriter, r *http.Request) {
1650 xid := mux.Vars(r)["xid"]
1651 w.Header().Set("Cache-Control", "max-age="+somedays())
1652 http.ServeFile(w, r, "memes/"+xid)
1653}
1654
1655func servefile(w http.ResponseWriter, r *http.Request) {
1656 xid := mux.Vars(r)["xid"]
1657 row := stmtFileData.QueryRow(xid)
1658 var media string
1659 var data []byte
1660 err := row.Scan(&media, &data)
1661 if err != nil {
1662 log.Printf("error loading file: %s", err)
1663 http.NotFound(w, r)
1664 return
1665 }
1666 w.Header().Set("Content-Type", media)
1667 w.Header().Set("X-Content-Type-Options", "nosniff")
1668 w.Header().Set("Cache-Control", "max-age="+somedays())
1669 w.Write(data)
1670}
1671
1672func nomoroboto(w http.ResponseWriter, r *http.Request) {
1673 io.WriteString(w, "User-agent: *\n")
1674 io.WriteString(w, "Disallow: /a\n")
1675 io.WriteString(w, "Disallow: /d\n")
1676 io.WriteString(w, "Disallow: /meme\n")
1677 for _, u := range allusers() {
1678 fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1679 }
1680}
1681
1682func serve() {
1683 db := opendatabase()
1684 login.Init(db)
1685
1686 listener, err := openListener()
1687 if err != nil {
1688 log.Fatal(err)
1689 }
1690 go redeliverator()
1691
1692 debug := false
1693 getconfig("debug", &debug)
1694 readviews = templates.Load(debug,
1695 "views/honkpage.html",
1696 "views/honkfrags.html",
1697 "views/honkers.html",
1698 "views/zonkers.html",
1699 "views/combos.html",
1700 "views/honkform.html",
1701 "views/honk.html",
1702 "views/account.html",
1703 "views/about.html",
1704 "views/funzone.html",
1705 "views/login.html",
1706 "views/xzone.html",
1707 "views/header.html",
1708 "views/onts.html",
1709 )
1710 if !debug {
1711 s := "views/style.css"
1712 savedstyleparams[s] = getstyleparam(s)
1713 s = "views/local.css"
1714 savedstyleparams[s] = getstyleparam(s)
1715 }
1716
1717 bitethethumbs()
1718
1719 mux := mux.NewRouter()
1720 mux.Use(login.Checker)
1721
1722 posters := mux.Methods("POST").Subrouter()
1723 getters := mux.Methods("GET").Subrouter()
1724
1725 getters.HandleFunc("/", homepage)
1726 getters.HandleFunc("/home", homepage)
1727 getters.HandleFunc("/front", homepage)
1728 getters.HandleFunc("/robots.txt", nomoroboto)
1729 getters.HandleFunc("/rss", showrss)
1730 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1731 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1732 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1733 posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1734 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1735 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1736 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1737 getters.HandleFunc("/a", avatate)
1738 getters.HandleFunc("/o", thelistingoftheontologies)
1739 getters.HandleFunc("/o/{name:[a-z0-9-]+}", showontology)
1740 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1741 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1742 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1743 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1744
1745 getters.HandleFunc("/style.css", servecss)
1746 getters.HandleFunc("/local.css", servecss)
1747 getters.HandleFunc("/about", servehtml)
1748 getters.HandleFunc("/login", servehtml)
1749 posters.HandleFunc("/dologin", login.LoginFunc)
1750 getters.HandleFunc("/logout", login.LogoutFunc)
1751
1752 loggedin := mux.NewRoute().Subrouter()
1753 loggedin.Use(login.Required)
1754 loggedin.HandleFunc("/account", accountpage)
1755 loggedin.HandleFunc("/funzone", showfunzone)
1756 loggedin.HandleFunc("/chpass", dochpass)
1757 loggedin.HandleFunc("/atme", homepage)
1758 loggedin.HandleFunc("/zonkzone", zonkzone)
1759 loggedin.HandleFunc("/xzone", xzone)
1760 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1761 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1762 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1763 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1764 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1765 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1766 loggedin.HandleFunc("/honkers", showhonkers)
1767 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1768 loggedin.HandleFunc("/h", showhonker)
1769 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1770 loggedin.HandleFunc("/c", showcombos)
1771 loggedin.HandleFunc("/t", showconvoy)
1772 loggedin.HandleFunc("/q", showsearch)
1773 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1774
1775 err = http.Serve(listener, mux)
1776 if err != nil {
1777 log.Fatal(err)
1778 }
1779}
1780
1781func cleanupdb(arg string) {
1782 db := opendatabase()
1783 days, err := strconv.Atoi(arg)
1784 if err != nil {
1785 honker := arg
1786 expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1787 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1788 doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1789 } else {
1790 expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1791 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3))", expdate)
1792 doordie(db, "delete from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3)", expdate)
1793 }
1794 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1795 for _, u := range allusers() {
1796 doordie(db, "delete from zonkers where userid = ? and wherefore = 'zonvoy' and zonkerid < (select zonkerid from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 1 offset 200)", u.UserID, u.UserID)
1797 }
1798}
1799
1800var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1801var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1802var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1803var stmtHonksBySearch, stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1804var stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1805var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover, stmtOneHonker *sql.Stmt
1806var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1807var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1808var stmtSelectOnts, stmtSaveOnts, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1809
1810func preparetodie(db *sql.DB, s string) *sql.Stmt {
1811 stmt, err := db.Prepare(s)
1812 if err != nil {
1813 log.Fatalf("error %s: %s", err, s)
1814 }
1815 return stmt
1816}
1817
1818func prepareStatements(db *sql.DB) {
1819 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")
1820 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1821 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1822 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1823 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1824 stmtOneHonker = preparetodie(db, "select xid from honkers where name = ? and userid = ?")
1825 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1826
1827 selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore, flags, onts from honks join users on honks.userid = users.userid "
1828 limit := " order by honks.honkid desc limit 250"
1829 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1830 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1831 stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2")
1832 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1833 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1834 stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and honker in (select xid from honkers where userid = ? and flavor = 'sub' and combos not like '% - %')"+butnotthose+limit)
1835 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1836 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1837 stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1838 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1839 stmtHonksBySearch = preparetodie(db, selecthonks+"where honks.userid = ? and noise like ?"+limit)
1840 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1841 stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
1842
1843 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, onts) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1844 stmtSaveOnts = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)")
1845 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1846 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1847 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1848 stmtZonkIt = preparetodie(db, "delete from honks where honkid = ?")
1849 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1850 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1851 stmtSaveFile = preparetodie(db, "insert into files (xid, name, description, url, media, local, content) values (?, ?, ?, ?, ?, ?, ?)")
1852 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1853 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1854 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1855 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1856 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1857 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1858 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1859 stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1860 stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1861 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1862 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1863 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1864 stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1865 stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? and honker not in (select xid from honkers where userid = ? and flavor = 'sub') order by honkid desc limit 100")
1866 stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?")
1867 stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?")
1868 stmtSelectOnts = preparetodie(db, "select distinct(ontology) from onts join honks on onts.honkid = honks.honkid where (honks.userid = ? or honks.whofore = 2)")
1869}
1870
1871func ElaborateUnitTests() {
1872}
1873
1874func main() {
1875 cmd := "run"
1876 if len(os.Args) > 1 {
1877 cmd = os.Args[1]
1878 }
1879 switch cmd {
1880 case "init":
1881 initdb()
1882 case "upgrade":
1883 upgradedb()
1884 }
1885 db := opendatabase()
1886 dbversion := 0
1887 getconfig("dbversion", &dbversion)
1888 if dbversion != myVersion {
1889 log.Fatal("incorrect database version. run upgrade.")
1890 }
1891 getconfig("servermsg", &serverMsg)
1892 getconfig("servername", &serverName)
1893 getconfig("usersep", &userSep)
1894 getconfig("honksep", &honkSep)
1895 getconfig("dnf", &donotfedafterdark)
1896 prepareStatements(db)
1897 switch cmd {
1898 case "adduser":
1899 adduser()
1900 case "cleanup":
1901 arg := "30"
1902 if len(os.Args) > 2 {
1903 arg = os.Args[2]
1904 }
1905 cleanupdb(arg)
1906 case "ping":
1907 if len(os.Args) < 4 {
1908 fmt.Printf("usage: honk ping from to\n")
1909 return
1910 }
1911 name := os.Args[2]
1912 targ := os.Args[3]
1913 user, err := butwhatabout(name)
1914 if err != nil {
1915 log.Printf("unknown user")
1916 return
1917 }
1918 ping(user, targ)
1919 case "peep":
1920 peeppeep()
1921 case "run":
1922 serve()
1923 case "test":
1924 ElaborateUnitTests()
1925 default:
1926 log.Fatal("unknown command")
1927 }
1928}