all repos — honk @ 2d86a3c038308aaa2c0958ad5796a0a28ece273a

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	if u == nil {
 673		w.Header().Set("Cache-Control", "max-age=300")
 674	}
 675	templinfo := getInfo(r)
 676	templinfo["Onts"] = onts
 677	err = readviews.Execute(w, "onts.html", templinfo)
 678	if err != nil {
 679		log.Print(err)
 680	}
 681}
 682
 683func showhonk(w http.ResponseWriter, r *http.Request) {
 684	name := mux.Vars(r)["name"]
 685	user, err := butwhatabout(name)
 686	if err != nil {
 687		http.NotFound(w, r)
 688		return
 689	}
 690	if stealthed(r) {
 691		http.NotFound(w, r)
 692		return
 693	}
 694
 695	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 696	honk := getxonk(user.ID, xid)
 697	if honk == nil {
 698		http.NotFound(w, r)
 699		return
 700	}
 701	u := login.GetUserInfo(r)
 702	if u != nil && u.UserID != user.ID {
 703		u = nil
 704	}
 705	if !honk.Public {
 706		if u == nil {
 707			http.NotFound(w, r)
 708			return
 709
 710		}
 711		honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
 712		return
 713	}
 714	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
 715	if friendorfoe(r.Header.Get("Accept")) {
 716		for _, h := range rawhonks {
 717			if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
 718				honk.Replies = append(honk.Replies, h)
 719			}
 720		}
 721		donksforhonks([]*Honk{honk})
 722		_, j := jonkjonk(user, honk)
 723		j["@context"] = itiswhatitis
 724		w.Header().Set("Content-Type", theonetruename)
 725		j.Write(w)
 726		return
 727	}
 728	var honks []*Honk
 729	for _, h := range rawhonks {
 730		if h.Public && (h.Whofore == 2 || h.IsAcked()) {
 731			honks = append(honks, h)
 732		}
 733	}
 734
 735	honkpage(w, r, u, nil, honks, "one honk maybe more")
 736}
 737
 738func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 739	honks []*Honk, infomsg template.HTML) {
 740	templinfo := getInfo(r)
 741	var userid int64 = -1
 742	if u != nil {
 743		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 744		userid = u.UserID
 745	}
 746	if u == nil {
 747		w.Header().Set("Cache-Control", "max-age=60")
 748	}
 749	reverbolate(userid, honks)
 750	if user != nil {
 751		filt := htfilter.New()
 752		templinfo["Name"] = user.Name
 753		whatabout := user.About
 754		whatabout = obfusbreak(user.About)
 755		templinfo["WhatAbout"], _ = filt.String(whatabout)
 756	}
 757	templinfo["Honks"] = honks
 758	templinfo["ServerMessage"] = infomsg
 759	err := readviews.Execute(w, "honkpage.html", templinfo)
 760	if err != nil {
 761		log.Print(err)
 762	}
 763}
 764
 765func saveuser(w http.ResponseWriter, r *http.Request) {
 766	whatabout := r.FormValue("whatabout")
 767	u := login.GetUserInfo(r)
 768	db := opendatabase()
 769	options := ""
 770	if r.FormValue("skinny") == "skinny" {
 771		options += " skinny "
 772	}
 773	_, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
 774	if err != nil {
 775		log.Printf("error bouting what: %s", err)
 776	}
 777
 778	http.Redirect(w, r, "/account", http.StatusSeeOther)
 779}
 780
 781func gethonkers(userid int64) []*Honker {
 782	rows, err := stmtHonkers.Query(userid)
 783	if err != nil {
 784		log.Printf("error querying honkers: %s", err)
 785		return nil
 786	}
 787	defer rows.Close()
 788	var honkers []*Honker
 789	for rows.Next() {
 790		var f Honker
 791		var combos string
 792		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 793		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 794		if err != nil {
 795			log.Printf("error scanning honker: %s", err)
 796			return nil
 797		}
 798		honkers = append(honkers, &f)
 799	}
 800	return honkers
 801}
 802
 803func getdubs(userid int64) []*Honker {
 804	rows, err := stmtDubbers.Query(userid)
 805	if err != nil {
 806		log.Printf("error querying dubs: %s", err)
 807		return nil
 808	}
 809	defer rows.Close()
 810	var honkers []*Honker
 811	for rows.Next() {
 812		var f Honker
 813		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 814		if err != nil {
 815			log.Printf("error scanning honker: %s", err)
 816			return nil
 817		}
 818		honkers = append(honkers, &f)
 819	}
 820	return honkers
 821}
 822
 823func allusers() []login.UserInfo {
 824	var users []login.UserInfo
 825	rows, _ := opendatabase().Query("select userid, username from users")
 826	defer rows.Close()
 827	for rows.Next() {
 828		var u login.UserInfo
 829		rows.Scan(&u.UserID, &u.Username)
 830		users = append(users, u)
 831	}
 832	return users
 833}
 834
 835func getxonk(userid int64, xid string) *Honk {
 836	h := new(Honk)
 837	var dt, aud, onts string
 838	row := stmtOneXonk.QueryRow(userid, xid)
 839	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 840		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
 841	if err != nil {
 842		if err != sql.ErrNoRows {
 843			log.Printf("error scanning xonk: %s", err)
 844		}
 845		return nil
 846	}
 847	h.Date, _ = time.Parse(dbtimeformat, dt)
 848	h.Audience = strings.Split(aud, " ")
 849	h.Public = !keepitquiet(h.Audience)
 850	h.Onts = strings.Split(onts, " ")
 851	return h
 852}
 853
 854func getbonk(userid int64, xid string) *Honk {
 855	h := new(Honk)
 856	var dt, aud, onts string
 857	row := stmtOneBonk.QueryRow(userid, xid)
 858	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 859		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
 860	if err != nil {
 861		if err != sql.ErrNoRows {
 862			log.Printf("error scanning xonk: %s", err)
 863		}
 864		return nil
 865	}
 866	h.Date, _ = time.Parse(dbtimeformat, dt)
 867	h.Audience = strings.Split(aud, " ")
 868	h.Public = !keepitquiet(h.Audience)
 869	h.Onts = strings.Split(onts, " ")
 870	return h
 871}
 872
 873func getpublichonks() []*Honk {
 874	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 875	rows, err := stmtPublicHonks.Query(dt)
 876	return getsomehonks(rows, err)
 877}
 878func gethonksbyuser(name string, includeprivate bool) []*Honk {
 879	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 880	whofore := 2
 881	if includeprivate {
 882		whofore = 3
 883	}
 884	rows, err := stmtUserHonks.Query(whofore, name, dt)
 885	return getsomehonks(rows, err)
 886}
 887func gethonksforuser(userid int64) []*Honk {
 888	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 889	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 890	return getsomehonks(rows, err)
 891}
 892func gethonksforme(userid int64) []*Honk {
 893	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 894	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 895	return getsomehonks(rows, err)
 896}
 897func gethonksbyhonker(userid int64, honker string) []*Honk {
 898	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 899	return getsomehonks(rows, err)
 900}
 901func gethonksbyxonker(userid int64, xonker string) []*Honk {
 902	rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
 903	return getsomehonks(rows, err)
 904}
 905func gethonksbycombo(userid int64, combo string) []*Honk {
 906	combo = "% " + combo + " %"
 907	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 908	return getsomehonks(rows, err)
 909}
 910func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 911	rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
 912	honks := getsomehonks(rows, err)
 913	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 914		honks[i], honks[j] = honks[j], honks[i]
 915	}
 916	return honks
 917}
 918func gethonksbyontology(userid int64, name string) []*Honk {
 919	rows, err := stmtHonksByOntology.Query(name, userid, userid)
 920	honks := getsomehonks(rows, err)
 921	return honks
 922}
 923
 924func getsomehonks(rows *sql.Rows, err error) []*Honk {
 925	if err != nil {
 926		log.Printf("error querying honks: %s", err)
 927		return nil
 928	}
 929	defer rows.Close()
 930	var honks []*Honk
 931	for rows.Next() {
 932		var h Honk
 933		var dt, aud, onts string
 934		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID,
 935			&h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
 936		if err != nil {
 937			log.Printf("error scanning honks: %s", err)
 938			return nil
 939		}
 940		h.Date, _ = time.Parse(dbtimeformat, dt)
 941		h.Audience = strings.Split(aud, " ")
 942		h.Public = !keepitquiet(h.Audience)
 943		h.Onts = strings.Split(onts, " ")
 944		honks = append(honks, &h)
 945	}
 946	rows.Close()
 947	donksforhonks(honks)
 948	return honks
 949}
 950
 951func donksforhonks(honks []*Honk) {
 952	db := opendatabase()
 953	var ids []string
 954	hmap := make(map[int64]*Honk)
 955	for _, h := range honks {
 956		ids = append(ids, fmt.Sprintf("%d", h.ID))
 957		hmap[h.ID] = h
 958	}
 959	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, ","))
 960	rows, err := db.Query(q)
 961	if err != nil {
 962		log.Printf("error querying donks: %s", err)
 963		return
 964	}
 965	defer rows.Close()
 966	for rows.Next() {
 967		var hid int64
 968		var d Donk
 969		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 970		if err != nil {
 971			log.Printf("error scanning donk: %s", err)
 972			continue
 973		}
 974		h := hmap[hid]
 975		h.Donks = append(h.Donks, &d)
 976	}
 977}
 978
 979func savebonk(w http.ResponseWriter, r *http.Request) {
 980	xid := r.FormValue("xid")
 981	userinfo := login.GetUserInfo(r)
 982	user, _ := butwhatabout(userinfo.Username)
 983
 984	log.Printf("bonking %s", xid)
 985
 986	xonk := getxonk(userinfo.UserID, xid)
 987	if xonk == nil {
 988		return
 989	}
 990	if !xonk.Public {
 991		return
 992	}
 993	donksforhonks([]*Honk{xonk})
 994
 995	_, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
 996	if err != nil {
 997		log.Printf("error acking bonk: %s", err)
 998	}
 999
1000	oonker := xonk.Oonker
1001	if oonker == "" {
1002		oonker = xonk.Honker
1003	}
1004	dt := time.Now().UTC()
1005	bonk := Honk{
1006		UserID:   userinfo.UserID,
1007		Username: userinfo.Username,
1008		What:     "bonk",
1009		Honker:   user.URL,
1010		XID:      xonk.XID,
1011		Date:     dt,
1012		Donks:    xonk.Donks,
1013		Convoy:   xonk.Convoy,
1014		Audience: []string{thewholeworld, oonker},
1015		Public:   true,
1016	}
1017
1018	aud := strings.Join(bonk.Audience, " ")
1019	whofore := 2
1020	onts := xonk.Onts
1021	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
1022		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
1023		xonk.Precis, oonker, 0, strings.Join(onts, " "))
1024	if err != nil {
1025		log.Printf("error saving bonk: %s", err)
1026		return
1027	}
1028	bonk.ID, _ = res.LastInsertId()
1029	for _, d := range bonk.Donks {
1030		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
1031		if err != nil {
1032			log.Printf("err saving donk: %s", err)
1033			return
1034		}
1035	}
1036	for _, o := range onts {
1037		_, err = stmtSaveOnts.Exec(strings.ToLower(o), bonk.ID)
1038		if err != nil {
1039			log.Printf("error saving ont: %s", err)
1040		}
1041	}
1042
1043	go honkworldwide(user, &bonk)
1044}
1045
1046func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
1047	zonk := Honk{
1048		What:     what,
1049		XID:      xonk.XID,
1050		Date:     time.Now().UTC(),
1051		Audience: oneofakind(xonk.Audience),
1052	}
1053	zonk.Public = !keepitquiet(zonk.Audience)
1054
1055	log.Printf("announcing %sed honk: %s", what, xonk.XID)
1056	go honkworldwide(user, &zonk)
1057}
1058
1059func zonkit(w http.ResponseWriter, r *http.Request) {
1060	wherefore := r.FormValue("wherefore")
1061	what := r.FormValue("what")
1062	userinfo := login.GetUserInfo(r)
1063	user, _ := butwhatabout(userinfo.Username)
1064
1065	if wherefore == "ack" {
1066		xonk := getxonk(userinfo.UserID, what)
1067		if xonk != nil {
1068			_, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1069			if err != nil {
1070				log.Printf("error acking: %s", err)
1071			}
1072			sendzonkofsorts(xonk, user, "ack")
1073		}
1074		return
1075	}
1076
1077	if wherefore == "deack" {
1078		xonk := getxonk(userinfo.UserID, what)
1079		if xonk != nil {
1080			_, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1081			if err != nil {
1082				log.Printf("error deacking: %s", err)
1083			}
1084			sendzonkofsorts(xonk, user, "deack")
1085		}
1086		return
1087	}
1088
1089	if wherefore == "unbonk" {
1090		xonk := getbonk(userinfo.UserID, what)
1091		if xonk != nil {
1092			_, err := stmtZonkDonks.Exec(xonk.ID)
1093			if err != nil {
1094				log.Printf("error zonking: %s", err)
1095			}
1096			_, err = stmtZonkIt.Exec(xonk.ID)
1097			if err != nil {
1098				log.Printf("error zonking: %s", err)
1099			}
1100			xonk = getxonk(userinfo.UserID, what)
1101			_, err = stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1102			if err != nil {
1103				log.Printf("error unbonking: %s", err)
1104			}
1105			sendzonkofsorts(xonk, user, "unbonk")
1106		}
1107		return
1108	}
1109
1110	log.Printf("zonking %s %s", wherefore, what)
1111	if wherefore == "zonk" {
1112		xonk := getxonk(userinfo.UserID, what)
1113		if xonk != nil {
1114			_, err := stmtZonkDonks.Exec(xonk.ID)
1115			if err != nil {
1116				log.Printf("error zonking: %s", err)
1117			}
1118			_, err = stmtZonkIt.Exec(xonk.ID)
1119			if err != nil {
1120				log.Printf("error zonking: %s", err)
1121			}
1122			if xonk.Whofore == 2 || xonk.Whofore == 3 {
1123				sendzonkofsorts(xonk, user, "zonk")
1124			}
1125		}
1126	}
1127	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1128	if err != nil {
1129		log.Printf("error saving zonker: %s", err)
1130		return
1131	}
1132}
1133
1134func savehonk(w http.ResponseWriter, r *http.Request) {
1135	rid := r.FormValue("rid")
1136	noise := r.FormValue("noise")
1137
1138	userinfo := login.GetUserInfo(r)
1139	user, _ := butwhatabout(userinfo.Username)
1140
1141	dt := time.Now().UTC()
1142	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1143	what := "honk"
1144	if rid != "" {
1145		what = "tonk"
1146	}
1147	honk := Honk{
1148		UserID:   userinfo.UserID,
1149		Username: userinfo.Username,
1150		What:     "honk",
1151		Honker:   user.URL,
1152		XID:      xid,
1153		Date:     dt,
1154	}
1155	if strings.HasPrefix(noise, "DZ:") {
1156		idx := strings.Index(noise, "\n")
1157		if idx == -1 {
1158			honk.Precis = noise
1159			noise = ""
1160		} else {
1161			honk.Precis = noise[:idx]
1162			noise = noise[idx+1:]
1163		}
1164	}
1165	noise = hooterize(noise)
1166	noise = strings.TrimSpace(noise)
1167	honk.Precis = strings.TrimSpace(honk.Precis)
1168
1169	var convoy string
1170	if rid != "" {
1171		xonk := getxonk(userinfo.UserID, rid)
1172		if xonk != nil {
1173			if xonk.Public {
1174				honk.Audience = append(honk.Audience, xonk.Audience...)
1175			}
1176			convoy = xonk.Convoy
1177		} else {
1178			xonkaud, c := whosthere(rid)
1179			honk.Audience = append(honk.Audience, xonkaud...)
1180			convoy = c
1181		}
1182		for i, a := range honk.Audience {
1183			if a == thewholeworld {
1184				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1185				break
1186			}
1187		}
1188		honk.RID = rid
1189	} else {
1190		honk.Audience = []string{thewholeworld}
1191	}
1192	if noise != "" && noise[0] == '@' {
1193		honk.Audience = append(grapevine(noise), honk.Audience...)
1194	} else {
1195		honk.Audience = append(honk.Audience, grapevine(noise)...)
1196	}
1197	if convoy == "" {
1198		convoy = "data:,electrichonkytonk-" + xfiltrate()
1199	}
1200	butnottooloud(honk.Audience)
1201	honk.Audience = oneofakind(honk.Audience)
1202	if len(honk.Audience) == 0 {
1203		log.Printf("honk to nowhere")
1204		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1205		return
1206	}
1207	honk.Public = !keepitquiet(honk.Audience)
1208	noise = obfusbreak(noise)
1209	honk.Noise = noise
1210	honk.Convoy = convoy
1211
1212	donkxid := r.FormValue("donkxid")
1213	if donkxid == "" {
1214		file, filehdr, err := r.FormFile("donk")
1215		if err == nil {
1216			var buf bytes.Buffer
1217			io.Copy(&buf, file)
1218			file.Close()
1219			data := buf.Bytes()
1220			xid := xfiltrate()
1221			var media, name string
1222			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1223			if err == nil {
1224				data = img.Data
1225				format := img.Format
1226				media = "image/" + format
1227				if format == "jpeg" {
1228					format = "jpg"
1229				}
1230				name = xid + "." + format
1231				xid = name
1232			} else {
1233				maxsize := 100000
1234				if len(data) > maxsize {
1235					log.Printf("bad image: %s too much text: %d", err, len(data))
1236					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1237					return
1238				}
1239				for i := 0; i < len(data); i++ {
1240					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1241						log.Printf("bad image: %s not text: %d", err, data[i])
1242						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1243						return
1244					}
1245				}
1246				media = "text/plain"
1247				name = filehdr.Filename
1248				if name == "" {
1249					name = xid + ".txt"
1250				}
1251				xid += ".txt"
1252			}
1253			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1254			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1255			if err != nil {
1256				log.Printf("unable to save image: %s", err)
1257				return
1258			}
1259			var d Donk
1260			d.FileID, _ = res.LastInsertId()
1261			d.XID = name
1262			d.Name = name
1263			d.Media = media
1264			d.URL = url
1265			d.Local = true
1266			honk.Donks = append(honk.Donks, &d)
1267			donkxid = d.XID
1268		}
1269	} else {
1270		xid := donkxid
1271		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1272		var donk Donk
1273		row := stmtFindFile.QueryRow(url)
1274		err := row.Scan(&donk.FileID)
1275		if err == nil {
1276			donk.XID = xid
1277			donk.Local = true
1278			donk.URL = url
1279			honk.Donks = append(honk.Donks, &donk)
1280		} else {
1281			log.Printf("can't find file: %s", xid)
1282		}
1283	}
1284	herd := herdofemus(honk.Noise)
1285	for _, e := range herd {
1286		donk := savedonk(e.ID, e.Name, "image/png", true)
1287		if donk != nil {
1288			donk.Name = e.Name
1289			honk.Donks = append(honk.Donks, donk)
1290		}
1291	}
1292	memetize(&honk)
1293
1294	aud := strings.Join(honk.Audience, " ")
1295	whofore := 2
1296	if !honk.Public {
1297		whofore = 3
1298	}
1299	if r.FormValue("preview") == "preview" {
1300		honks := []*Honk{&honk}
1301		reverbolate(userinfo.UserID, honks)
1302		templinfo := getInfo(r)
1303		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1304		templinfo["Honks"] = honks
1305		templinfo["InReplyTo"] = r.FormValue("rid")
1306		templinfo["Noise"] = r.FormValue("noise")
1307		templinfo["SavedFile"] = donkxid
1308		templinfo["ServerMessage"] = "honk preview"
1309		err := readviews.Execute(w, "honkpage.html", templinfo)
1310		if err != nil {
1311			log.Print(err)
1312		}
1313		return
1314	}
1315	honk.Onts = oneofakind(ontologies(honk.Noise))
1316	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1317		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1318		honk.Precis, honk.Oonker, 0, strings.Join(honk.Onts, " "))
1319	if err != nil {
1320		log.Printf("error saving honk: %s", err)
1321		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1322		return
1323	}
1324	honk.ID, _ = res.LastInsertId()
1325	for _, d := range honk.Donks {
1326		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1327		if err != nil {
1328			log.Printf("err saving donk: %s", err)
1329			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1330			return
1331		}
1332	}
1333	for _, o := range honk.Onts {
1334		_, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1335		if err != nil {
1336			log.Printf("error saving ont: %s", err)
1337		}
1338	}
1339
1340	go honkworldwide(user, &honk)
1341
1342	http.Redirect(w, r, xid, http.StatusSeeOther)
1343}
1344
1345func showhonkers(w http.ResponseWriter, r *http.Request) {
1346	userinfo := login.GetUserInfo(r)
1347	templinfo := getInfo(r)
1348	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1349	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1350	err := readviews.Execute(w, "honkers.html", templinfo)
1351	if err != nil {
1352		log.Print(err)
1353	}
1354}
1355
1356func showcombos(w http.ResponseWriter, r *http.Request) {
1357	userinfo := login.GetUserInfo(r)
1358	templinfo := getInfo(r)
1359	honkers := gethonkers(userinfo.UserID)
1360	var combos []string
1361	for _, h := range honkers {
1362		combos = append(combos, h.Combos...)
1363	}
1364	for i, c := range combos {
1365		if c == "-" {
1366			combos[i] = ""
1367		}
1368	}
1369	combos = oneofakind(combos)
1370	sort.Strings(combos)
1371	templinfo["Combos"] = combos
1372	err := readviews.Execute(w, "combos.html", templinfo)
1373	if err != nil {
1374		log.Print(err)
1375	}
1376}
1377
1378func savehonker(w http.ResponseWriter, r *http.Request) {
1379	u := login.GetUserInfo(r)
1380	name := r.FormValue("name")
1381	url := r.FormValue("url")
1382	peep := r.FormValue("peep")
1383	combos := r.FormValue("combos")
1384	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1385
1386	if honkerid > 0 {
1387		goodbye := r.FormValue("goodbye")
1388		if goodbye == "F" {
1389			db := opendatabase()
1390			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1391				honkerid, u.UserID)
1392			var xid string
1393			err := row.Scan(&xid)
1394			if err != nil {
1395				log.Printf("can't get honker xid: %s", err)
1396				return
1397			}
1398			log.Printf("unsubscribing from %s", xid)
1399			user, _ := butwhatabout(u.Username)
1400			go itakeitallback(user, xid)
1401			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1402			if err != nil {
1403				log.Printf("error updating honker: %s", err)
1404				return
1405			}
1406
1407			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1408			return
1409		}
1410		combos = " " + strings.TrimSpace(combos) + " "
1411		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1412		if err != nil {
1413			log.Printf("update honker err: %s", err)
1414			return
1415		}
1416		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1417	}
1418
1419	flavor := "presub"
1420	if peep == "peep" {
1421		flavor = "peep"
1422	}
1423	p := investigate(url)
1424	if p == nil {
1425		log.Printf("failed to investigate honker")
1426		return
1427	}
1428	url = p.XID
1429	if name == "" {
1430		name = p.Handle
1431	}
1432	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1433	if err != nil {
1434		log.Print(err)
1435		return
1436	}
1437	if flavor == "presub" {
1438		user, _ := butwhatabout(u.Username)
1439		go subsub(user, url)
1440	}
1441	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1442}
1443
1444type Zonker struct {
1445	ID        int64
1446	Name      string
1447	Wherefore string
1448}
1449
1450func zonkzone(w http.ResponseWriter, r *http.Request) {
1451	userinfo := login.GetUserInfo(r)
1452	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1453	if err != nil {
1454		log.Printf("err: %s", err)
1455		return
1456	}
1457	defer rows.Close()
1458	var zonkers []Zonker
1459	for rows.Next() {
1460		var z Zonker
1461		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1462		zonkers = append(zonkers, z)
1463	}
1464	sort.Slice(zonkers, func(i, j int) bool {
1465		w1 := zonkers[i].Wherefore
1466		w2 := zonkers[j].Wherefore
1467		if w1 == w2 {
1468			return zonkers[i].Name < zonkers[j].Name
1469		}
1470		if w1 == "zonvoy" {
1471			w1 = "zzzzzzz"
1472		}
1473		if w2 == "zonvoy" {
1474			w2 = "zzzzzzz"
1475		}
1476		return w1 < w2
1477	})
1478
1479	templinfo := getInfo(r)
1480	templinfo["Zonkers"] = zonkers
1481	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1482	err = readviews.Execute(w, "zonkers.html", templinfo)
1483	if err != nil {
1484		log.Print(err)
1485	}
1486}
1487
1488func zonkzonk(w http.ResponseWriter, r *http.Request) {
1489	userinfo := login.GetUserInfo(r)
1490	itsok := r.FormValue("itsok")
1491	if itsok == "iforgiveyou" {
1492		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1493		db := opendatabase()
1494		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1495			userinfo.UserID, zonkerid)
1496		bitethethumbs()
1497		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1498		return
1499	}
1500	wherefore := r.FormValue("wherefore")
1501	name := r.FormValue("name")
1502	if name == "" {
1503		return
1504	}
1505	switch wherefore {
1506	case "zonker":
1507	case "zomain":
1508	case "zonvoy":
1509	case "zord":
1510	case "zilence":
1511	default:
1512		return
1513	}
1514	db := opendatabase()
1515	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1516		userinfo.UserID, name, wherefore)
1517	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1518		bitethethumbs()
1519	}
1520
1521	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1522}
1523
1524func accountpage(w http.ResponseWriter, r *http.Request) {
1525	u := login.GetUserInfo(r)
1526	user, _ := butwhatabout(u.Username)
1527	templinfo := getInfo(r)
1528	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1529	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1530	templinfo["User"] = user
1531	err := readviews.Execute(w, "account.html", templinfo)
1532	if err != nil {
1533		log.Print(err)
1534	}
1535}
1536
1537func dochpass(w http.ResponseWriter, r *http.Request) {
1538	err := login.ChangePassword(w, r)
1539	if err != nil {
1540		log.Printf("error changing password: %s", err)
1541	}
1542	http.Redirect(w, r, "/account", http.StatusSeeOther)
1543}
1544
1545func fingerlicker(w http.ResponseWriter, r *http.Request) {
1546	orig := r.FormValue("resource")
1547
1548	log.Printf("finger lick: %s", orig)
1549
1550	if strings.HasPrefix(orig, "acct:") {
1551		orig = orig[5:]
1552	}
1553
1554	name := orig
1555	idx := strings.LastIndexByte(name, '/')
1556	if idx != -1 {
1557		name = name[idx+1:]
1558		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1559			log.Printf("foreign request rejected")
1560			name = ""
1561		}
1562	} else {
1563		idx = strings.IndexByte(name, '@')
1564		if idx != -1 {
1565			name = name[:idx]
1566			if name+"@"+serverName != orig {
1567				log.Printf("foreign request rejected")
1568				name = ""
1569			}
1570		}
1571	}
1572	user, err := butwhatabout(name)
1573	if err != nil {
1574		http.NotFound(w, r)
1575		return
1576	}
1577
1578	j := junk.New()
1579	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1580	j["aliases"] = []string{user.URL}
1581	var links []junk.Junk
1582	l := junk.New()
1583	l["rel"] = "self"
1584	l["type"] = `application/activity+json`
1585	l["href"] = user.URL
1586	links = append(links, l)
1587	j["links"] = links
1588
1589	w.Header().Set("Cache-Control", "max-age=3600")
1590	w.Header().Set("Content-Type", "application/jrd+json")
1591	j.Write(w)
1592}
1593
1594func somedays() string {
1595	secs := 432000 + notrand.Int63n(432000)
1596	return fmt.Sprintf("%d", secs)
1597}
1598
1599func avatate(w http.ResponseWriter, r *http.Request) {
1600	n := r.FormValue("a")
1601	a := avatar(n)
1602	w.Header().Set("Cache-Control", "max-age="+somedays())
1603	w.Write(a)
1604}
1605
1606func servecss(w http.ResponseWriter, r *http.Request) {
1607	w.Header().Set("Cache-Control", "max-age=7776000")
1608	http.ServeFile(w, r, "views"+r.URL.Path)
1609}
1610func servehtml(w http.ResponseWriter, r *http.Request) {
1611	templinfo := getInfo(r)
1612	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1613	if err != nil {
1614		log.Print(err)
1615	}
1616}
1617func serveemu(w http.ResponseWriter, r *http.Request) {
1618	xid := mux.Vars(r)["xid"]
1619	w.Header().Set("Cache-Control", "max-age="+somedays())
1620	http.ServeFile(w, r, "emus/"+xid)
1621}
1622func servememe(w http.ResponseWriter, r *http.Request) {
1623	xid := mux.Vars(r)["xid"]
1624	w.Header().Set("Cache-Control", "max-age="+somedays())
1625	http.ServeFile(w, r, "memes/"+xid)
1626}
1627
1628func servefile(w http.ResponseWriter, r *http.Request) {
1629	xid := mux.Vars(r)["xid"]
1630	row := stmtFileData.QueryRow(xid)
1631	var media string
1632	var data []byte
1633	err := row.Scan(&media, &data)
1634	if err != nil {
1635		log.Printf("error loading file: %s", err)
1636		http.NotFound(w, r)
1637		return
1638	}
1639	w.Header().Set("Content-Type", media)
1640	w.Header().Set("X-Content-Type-Options", "nosniff")
1641	w.Header().Set("Cache-Control", "max-age="+somedays())
1642	w.Write(data)
1643}
1644
1645func nomoroboto(w http.ResponseWriter, r *http.Request) {
1646	io.WriteString(w, "User-agent: *\n")
1647	io.WriteString(w, "Disallow: /a\n")
1648	io.WriteString(w, "Disallow: /d\n")
1649	io.WriteString(w, "Disallow: /meme\n")
1650	for _, u := range allusers() {
1651		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1652	}
1653}
1654
1655func serve() {
1656	db := opendatabase()
1657	login.Init(db)
1658
1659	listener, err := openListener()
1660	if err != nil {
1661		log.Fatal(err)
1662	}
1663	go redeliverator()
1664
1665	debug := false
1666	getconfig("debug", &debug)
1667	readviews = templates.Load(debug,
1668		"views/honkpage.html",
1669		"views/honkfrags.html",
1670		"views/honkers.html",
1671		"views/zonkers.html",
1672		"views/combos.html",
1673		"views/honkform.html",
1674		"views/honk.html",
1675		"views/account.html",
1676		"views/about.html",
1677		"views/funzone.html",
1678		"views/login.html",
1679		"views/xzone.html",
1680		"views/header.html",
1681		"views/onts.html",
1682	)
1683	if !debug {
1684		s := "views/style.css"
1685		savedstyleparams[s] = getstyleparam(s)
1686		s = "views/local.css"
1687		savedstyleparams[s] = getstyleparam(s)
1688	}
1689
1690	bitethethumbs()
1691
1692	mux := mux.NewRouter()
1693	mux.Use(login.Checker)
1694
1695	posters := mux.Methods("POST").Subrouter()
1696	getters := mux.Methods("GET").Subrouter()
1697
1698	getters.HandleFunc("/", homepage)
1699	getters.HandleFunc("/front", homepage)
1700	getters.HandleFunc("/robots.txt", nomoroboto)
1701	getters.HandleFunc("/rss", showrss)
1702	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1703	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1704	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1705	posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1706	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1707	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1708	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1709	getters.HandleFunc("/a", avatate)
1710	getters.HandleFunc("/o", thelistingoftheontologies)
1711	getters.HandleFunc("/o/{name:[a-z0-9-]+}", showontology)
1712	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1713	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1714	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1715	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1716
1717	getters.HandleFunc("/style.css", servecss)
1718	getters.HandleFunc("/local.css", servecss)
1719	getters.HandleFunc("/about", servehtml)
1720	getters.HandleFunc("/login", servehtml)
1721	posters.HandleFunc("/dologin", login.LoginFunc)
1722	getters.HandleFunc("/logout", login.LogoutFunc)
1723
1724	loggedin := mux.NewRoute().Subrouter()
1725	loggedin.Use(login.Required)
1726	loggedin.HandleFunc("/account", accountpage)
1727	loggedin.HandleFunc("/funzone", showfunzone)
1728	loggedin.HandleFunc("/chpass", dochpass)
1729	loggedin.HandleFunc("/atme", homepage)
1730	loggedin.HandleFunc("/zonkzone", zonkzone)
1731	loggedin.HandleFunc("/xzone", xzone)
1732	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1733	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1734	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1735	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1736	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1737	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1738	loggedin.HandleFunc("/honkers", showhonkers)
1739	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1740	loggedin.HandleFunc("/h", showhonker)
1741	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1742	loggedin.HandleFunc("/c", showcombos)
1743	loggedin.HandleFunc("/t", showconvoy)
1744	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1745
1746	err = http.Serve(listener, mux)
1747	if err != nil {
1748		log.Fatal(err)
1749	}
1750}
1751
1752func cleanupdb(arg string) {
1753	db := opendatabase()
1754	days, err := strconv.Atoi(arg)
1755	if err != nil {
1756		honker := arg
1757		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1758		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1759		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1760	} else {
1761		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1762		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)
1763		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)
1764	}
1765	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1766	for _, u := range allusers() {
1767		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)
1768	}
1769}
1770
1771var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1772var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1773var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1774var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1775var stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1776var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1777var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1778var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1779var stmtSelectOnts, stmtSaveOnts, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1780
1781func preparetodie(db *sql.DB, s string) *sql.Stmt {
1782	stmt, err := db.Prepare(s)
1783	if err != nil {
1784		log.Fatalf("error %s: %s", err, s)
1785	}
1786	return stmt
1787}
1788
1789func prepareStatements(db *sql.DB) {
1790	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")
1791	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1792	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1793	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1794	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1795	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1796
1797	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 "
1798	limit := " order by honks.honkid desc limit 250"
1799	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1800	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1801	stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2")
1802	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1803	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1804	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)
1805	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1806	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1807	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1808	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1809	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1810	stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
1811
1812	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, onts) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1813	stmtSaveOnts = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)")
1814	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1815	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1816	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1817	stmtZonkIt = preparetodie(db, "delete from honks where honkid = ?")
1818	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1819	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1820	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1821	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1822	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1823	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1824	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1825	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1826	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1827	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1828	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1829	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1830	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1831	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1832	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1833	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1834	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")
1835	stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?")
1836	stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?")
1837	stmtSelectOnts = preparetodie(db, "select distinct(ontology) from onts join honks on onts.honkid = honks.honkid where (honks.userid = ? or honks.whofore = 2)")
1838}
1839
1840func ElaborateUnitTests() {
1841}
1842
1843func main() {
1844	cmd := "run"
1845	if len(os.Args) > 1 {
1846		cmd = os.Args[1]
1847	}
1848	switch cmd {
1849	case "init":
1850		initdb()
1851	case "upgrade":
1852		upgradedb()
1853	}
1854	db := opendatabase()
1855	dbversion := 0
1856	getconfig("dbversion", &dbversion)
1857	if dbversion != myVersion {
1858		log.Fatal("incorrect database version. run upgrade.")
1859	}
1860	getconfig("servermsg", &serverMsg)
1861	getconfig("servername", &serverName)
1862	getconfig("usersep", &userSep)
1863	getconfig("honksep", &honkSep)
1864	getconfig("dnf", &donotfedafterdark)
1865	prepareStatements(db)
1866	switch cmd {
1867	case "adduser":
1868		adduser()
1869	case "cleanup":
1870		arg := "30"
1871		if len(os.Args) > 2 {
1872			arg = os.Args[2]
1873		}
1874		cleanupdb(arg)
1875	case "ping":
1876		if len(os.Args) < 4 {
1877			fmt.Printf("usage: honk ping from to\n")
1878			return
1879		}
1880		name := os.Args[2]
1881		targ := os.Args[3]
1882		user, err := butwhatabout(name)
1883		if err != nil {
1884			log.Printf("unknown user")
1885			return
1886		}
1887		ping(user, targ)
1888	case "peep":
1889		peeppeep()
1890	case "run":
1891		serve()
1892	case "test":
1893		ElaborateUnitTests()
1894	default:
1895		log.Fatal("unknown command")
1896	}
1897}