all repos — honk @ 90903da8e866912e74605556a6d8300afc7fbfa2

my fork of honk

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