all repos — honk @ 461126c939183e2c99baf290c00e6613dc9b0ca7

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 := stmtOneBonk.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 := stmtZonkDonks.Exec(xonk.ID)
1087			if err != nil {
1088				log.Printf("error zonking: %s", err)
1089			}
1090			_, err = stmtZonkIt.Exec(xonk.ID)
1091			if err != nil {
1092				log.Printf("error zonking: %s", err)
1093			}
1094			xonk = getxonk(userinfo.UserID, what)
1095			_, err = stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1096			if err != nil {
1097				log.Printf("error unbonking: %s", err)
1098			}
1099			sendzonkofsorts(xonk, user, "unbonk")
1100		}
1101		return
1102	}
1103
1104	log.Printf("zonking %s %s", wherefore, what)
1105	if wherefore == "zonk" {
1106		xonk := getxonk(userinfo.UserID, what)
1107		if xonk != nil {
1108			_, err := stmtZonkDonks.Exec(xonk.ID)
1109			if err != nil {
1110				log.Printf("error zonking: %s", err)
1111			}
1112			_, err = stmtZonkIt.Exec(xonk.ID)
1113			if err != nil {
1114				log.Printf("error zonking: %s", err)
1115			}
1116			if xonk.Whofore == 2 || xonk.Whofore == 3 {
1117				sendzonkofsorts(xonk, user, "zonk")
1118			}
1119		}
1120	}
1121	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1122	if err != nil {
1123		log.Printf("error saving zonker: %s", err)
1124		return
1125	}
1126}
1127
1128func savehonk(w http.ResponseWriter, r *http.Request) {
1129	rid := r.FormValue("rid")
1130	noise := r.FormValue("noise")
1131
1132	userinfo := login.GetUserInfo(r)
1133	user, _ := butwhatabout(userinfo.Username)
1134
1135	dt := time.Now().UTC()
1136	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1137	what := "honk"
1138	if rid != "" {
1139		what = "tonk"
1140	}
1141	honk := Honk{
1142		UserID:   userinfo.UserID,
1143		Username: userinfo.Username,
1144		What:     "honk",
1145		Honker:   user.URL,
1146		XID:      xid,
1147		Date:     dt,
1148	}
1149	if strings.HasPrefix(noise, "DZ:") {
1150		idx := strings.Index(noise, "\n")
1151		if idx == -1 {
1152			honk.Precis = noise
1153			noise = ""
1154		} else {
1155			honk.Precis = noise[:idx]
1156			noise = noise[idx+1:]
1157		}
1158	}
1159	noise = hooterize(noise)
1160	noise = strings.TrimSpace(noise)
1161	honk.Precis = strings.TrimSpace(honk.Precis)
1162
1163	var convoy string
1164	if rid != "" {
1165		xonk := getxonk(userinfo.UserID, rid)
1166		if xonk != nil {
1167			if xonk.Public {
1168				honk.Audience = append(honk.Audience, xonk.Audience...)
1169			}
1170			convoy = xonk.Convoy
1171		} else {
1172			xonkaud, c := whosthere(rid)
1173			honk.Audience = append(honk.Audience, xonkaud...)
1174			convoy = c
1175		}
1176		for i, a := range honk.Audience {
1177			if a == thewholeworld {
1178				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1179				break
1180			}
1181		}
1182		honk.RID = rid
1183	} else {
1184		honk.Audience = []string{thewholeworld}
1185	}
1186	if noise != "" && noise[0] == '@' {
1187		honk.Audience = append(grapevine(noise), honk.Audience...)
1188	} else {
1189		honk.Audience = append(honk.Audience, grapevine(noise)...)
1190	}
1191	if convoy == "" {
1192		convoy = "data:,electrichonkytonk-" + xfiltrate()
1193	}
1194	butnottooloud(honk.Audience)
1195	honk.Audience = oneofakind(honk.Audience)
1196	if len(honk.Audience) == 0 {
1197		log.Printf("honk to nowhere")
1198		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1199		return
1200	}
1201	honk.Public = !keepitquiet(honk.Audience)
1202	noise = obfusbreak(noise)
1203	honk.Noise = noise
1204	honk.Convoy = convoy
1205
1206	donkxid := r.FormValue("donkxid")
1207	if donkxid == "" {
1208		file, filehdr, err := r.FormFile("donk")
1209		if err == nil {
1210			var buf bytes.Buffer
1211			io.Copy(&buf, file)
1212			file.Close()
1213			data := buf.Bytes()
1214			xid := xfiltrate()
1215			var media, name string
1216			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1217			if err == nil {
1218				data = img.Data
1219				format := img.Format
1220				media = "image/" + format
1221				if format == "jpeg" {
1222					format = "jpg"
1223				}
1224				name = xid + "." + format
1225				xid = name
1226			} else {
1227				maxsize := 100000
1228				if len(data) > maxsize {
1229					log.Printf("bad image: %s too much text: %d", err, len(data))
1230					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1231					return
1232				}
1233				for i := 0; i < len(data); i++ {
1234					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1235						log.Printf("bad image: %s not text: %d", err, data[i])
1236						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1237						return
1238					}
1239				}
1240				media = "text/plain"
1241				name = filehdr.Filename
1242				if name == "" {
1243					name = xid + ".txt"
1244				}
1245				xid += ".txt"
1246			}
1247			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1248			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1249			if err != nil {
1250				log.Printf("unable to save image: %s", err)
1251				return
1252			}
1253			var d Donk
1254			d.FileID, _ = res.LastInsertId()
1255			d.XID = name
1256			d.Name = name
1257			d.Media = media
1258			d.URL = url
1259			d.Local = true
1260			honk.Donks = append(honk.Donks, &d)
1261			donkxid = d.XID
1262		}
1263	} else {
1264		xid := donkxid
1265		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1266		var donk Donk
1267		row := stmtFindFile.QueryRow(url)
1268		err := row.Scan(&donk.FileID)
1269		if err == nil {
1270			donk.XID = xid
1271			donk.Local = true
1272			donk.URL = url
1273			honk.Donks = append(honk.Donks, &donk)
1274		} else {
1275			log.Printf("can't find file: %s", xid)
1276		}
1277	}
1278	herd := herdofemus(honk.Noise)
1279	for _, e := range herd {
1280		donk := savedonk(e.ID, e.Name, "image/png", true)
1281		if donk != nil {
1282			donk.Name = e.Name
1283			honk.Donks = append(honk.Donks, donk)
1284		}
1285	}
1286	memetize(&honk)
1287
1288	aud := strings.Join(honk.Audience, " ")
1289	whofore := 2
1290	if !honk.Public {
1291		whofore = 3
1292	}
1293	if r.FormValue("preview") == "preview" {
1294		honks := []*Honk{&honk}
1295		reverbolate(userinfo.UserID, honks)
1296		templinfo := getInfo(r)
1297		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1298		templinfo["Honks"] = honks
1299		templinfo["InReplyTo"] = r.FormValue("rid")
1300		templinfo["Noise"] = r.FormValue("noise")
1301		templinfo["SavedFile"] = donkxid
1302		templinfo["ServerMessage"] = "honk preview"
1303		err := readviews.Execute(w, "honkpage.html", templinfo)
1304		if err != nil {
1305			log.Print(err)
1306		}
1307		return
1308	}
1309	onts := ontologies(honk.Noise)
1310	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1311		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1312		honk.Precis, honk.Oonker, 0, strings.Join(onts, " "))
1313	if err != nil {
1314		log.Printf("error saving honk: %s", err)
1315		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1316		return
1317	}
1318	honk.ID, _ = res.LastInsertId()
1319	for _, d := range honk.Donks {
1320		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1321		if err != nil {
1322			log.Printf("err saving donk: %s", err)
1323			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1324			return
1325		}
1326	}
1327	for _, o := range onts {
1328		_, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1329		if err != nil {
1330			log.Printf("error saving ont: %s", err)
1331		}
1332	}
1333
1334	go honkworldwide(user, &honk)
1335
1336	http.Redirect(w, r, xid, http.StatusSeeOther)
1337}
1338
1339func showhonkers(w http.ResponseWriter, r *http.Request) {
1340	userinfo := login.GetUserInfo(r)
1341	templinfo := getInfo(r)
1342	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1343	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1344	err := readviews.Execute(w, "honkers.html", templinfo)
1345	if err != nil {
1346		log.Print(err)
1347	}
1348}
1349
1350func showcombos(w http.ResponseWriter, r *http.Request) {
1351	userinfo := login.GetUserInfo(r)
1352	templinfo := getInfo(r)
1353	honkers := gethonkers(userinfo.UserID)
1354	var combos []string
1355	for _, h := range honkers {
1356		combos = append(combos, h.Combos...)
1357	}
1358	for i, c := range combos {
1359		if c == "-" {
1360			combos[i] = ""
1361		}
1362	}
1363	combos = oneofakind(combos)
1364	sort.Strings(combos)
1365	templinfo["Combos"] = combos
1366	err := readviews.Execute(w, "combos.html", templinfo)
1367	if err != nil {
1368		log.Print(err)
1369	}
1370}
1371
1372func savehonker(w http.ResponseWriter, r *http.Request) {
1373	u := login.GetUserInfo(r)
1374	name := r.FormValue("name")
1375	url := r.FormValue("url")
1376	peep := r.FormValue("peep")
1377	combos := r.FormValue("combos")
1378	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1379
1380	if honkerid > 0 {
1381		goodbye := r.FormValue("goodbye")
1382		if goodbye == "F" {
1383			db := opendatabase()
1384			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1385				honkerid, u.UserID)
1386			var xid string
1387			err := row.Scan(&xid)
1388			if err != nil {
1389				log.Printf("can't get honker xid: %s", err)
1390				return
1391			}
1392			log.Printf("unsubscribing from %s", xid)
1393			user, _ := butwhatabout(u.Username)
1394			go itakeitallback(user, xid)
1395			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1396			if err != nil {
1397				log.Printf("error updating honker: %s", err)
1398				return
1399			}
1400
1401			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1402			return
1403		}
1404		combos = " " + strings.TrimSpace(combos) + " "
1405		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1406		if err != nil {
1407			log.Printf("update honker err: %s", err)
1408			return
1409		}
1410		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1411	}
1412
1413	flavor := "presub"
1414	if peep == "peep" {
1415		flavor = "peep"
1416	}
1417	p := investigate(url)
1418	if p == nil {
1419		log.Printf("failed to investigate honker")
1420		return
1421	}
1422	url = p.XID
1423	if name == "" {
1424		name = p.Handle
1425	}
1426	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1427	if err != nil {
1428		log.Print(err)
1429		return
1430	}
1431	if flavor == "presub" {
1432		user, _ := butwhatabout(u.Username)
1433		go subsub(user, url)
1434	}
1435	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1436}
1437
1438type Zonker struct {
1439	ID        int64
1440	Name      string
1441	Wherefore string
1442}
1443
1444func zonkzone(w http.ResponseWriter, r *http.Request) {
1445	userinfo := login.GetUserInfo(r)
1446	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1447	if err != nil {
1448		log.Printf("err: %s", err)
1449		return
1450	}
1451	defer rows.Close()
1452	var zonkers []Zonker
1453	for rows.Next() {
1454		var z Zonker
1455		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1456		zonkers = append(zonkers, z)
1457	}
1458	sort.Slice(zonkers, func(i, j int) bool {
1459		w1 := zonkers[i].Wherefore
1460		w2 := zonkers[j].Wherefore
1461		if w1 == w2 {
1462			return zonkers[i].Name < zonkers[j].Name
1463		}
1464		if w1 == "zonvoy" {
1465			w1 = "zzzzzzz"
1466		}
1467		if w2 == "zonvoy" {
1468			w2 = "zzzzzzz"
1469		}
1470		return w1 < w2
1471	})
1472
1473	templinfo := getInfo(r)
1474	templinfo["Zonkers"] = zonkers
1475	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1476	err = readviews.Execute(w, "zonkers.html", templinfo)
1477	if err != nil {
1478		log.Print(err)
1479	}
1480}
1481
1482func zonkzonk(w http.ResponseWriter, r *http.Request) {
1483	userinfo := login.GetUserInfo(r)
1484	itsok := r.FormValue("itsok")
1485	if itsok == "iforgiveyou" {
1486		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1487		db := opendatabase()
1488		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1489			userinfo.UserID, zonkerid)
1490		bitethethumbs()
1491		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1492		return
1493	}
1494	wherefore := r.FormValue("wherefore")
1495	name := r.FormValue("name")
1496	if name == "" {
1497		return
1498	}
1499	switch wherefore {
1500	case "zonker":
1501	case "zomain":
1502	case "zonvoy":
1503	case "zord":
1504	case "zilence":
1505	default:
1506		return
1507	}
1508	db := opendatabase()
1509	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1510		userinfo.UserID, name, wherefore)
1511	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1512		bitethethumbs()
1513	}
1514
1515	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1516}
1517
1518func accountpage(w http.ResponseWriter, r *http.Request) {
1519	u := login.GetUserInfo(r)
1520	user, _ := butwhatabout(u.Username)
1521	templinfo := getInfo(r)
1522	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1523	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1524	templinfo["User"] = user
1525	err := readviews.Execute(w, "account.html", templinfo)
1526	if err != nil {
1527		log.Print(err)
1528	}
1529}
1530
1531func dochpass(w http.ResponseWriter, r *http.Request) {
1532	err := login.ChangePassword(w, r)
1533	if err != nil {
1534		log.Printf("error changing password: %s", err)
1535	}
1536	http.Redirect(w, r, "/account", http.StatusSeeOther)
1537}
1538
1539func fingerlicker(w http.ResponseWriter, r *http.Request) {
1540	orig := r.FormValue("resource")
1541
1542	log.Printf("finger lick: %s", orig)
1543
1544	if strings.HasPrefix(orig, "acct:") {
1545		orig = orig[5:]
1546	}
1547
1548	name := orig
1549	idx := strings.LastIndexByte(name, '/')
1550	if idx != -1 {
1551		name = name[idx+1:]
1552		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1553			log.Printf("foreign request rejected")
1554			name = ""
1555		}
1556	} else {
1557		idx = strings.IndexByte(name, '@')
1558		if idx != -1 {
1559			name = name[:idx]
1560			if name+"@"+serverName != orig {
1561				log.Printf("foreign request rejected")
1562				name = ""
1563			}
1564		}
1565	}
1566	user, err := butwhatabout(name)
1567	if err != nil {
1568		http.NotFound(w, r)
1569		return
1570	}
1571
1572	j := junk.New()
1573	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1574	j["aliases"] = []string{user.URL}
1575	var links []junk.Junk
1576	l := junk.New()
1577	l["rel"] = "self"
1578	l["type"] = `application/activity+json`
1579	l["href"] = user.URL
1580	links = append(links, l)
1581	j["links"] = links
1582
1583	w.Header().Set("Cache-Control", "max-age=3600")
1584	w.Header().Set("Content-Type", "application/jrd+json")
1585	j.Write(w)
1586}
1587
1588func somedays() string {
1589	secs := 432000 + notrand.Int63n(432000)
1590	return fmt.Sprintf("%d", secs)
1591}
1592
1593func avatate(w http.ResponseWriter, r *http.Request) {
1594	n := r.FormValue("a")
1595	a := avatar(n)
1596	w.Header().Set("Cache-Control", "max-age="+somedays())
1597	w.Write(a)
1598}
1599
1600func servecss(w http.ResponseWriter, r *http.Request) {
1601	w.Header().Set("Cache-Control", "max-age=7776000")
1602	http.ServeFile(w, r, "views"+r.URL.Path)
1603}
1604func servehtml(w http.ResponseWriter, r *http.Request) {
1605	templinfo := getInfo(r)
1606	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1607	if err != nil {
1608		log.Print(err)
1609	}
1610}
1611func serveemu(w http.ResponseWriter, r *http.Request) {
1612	xid := mux.Vars(r)["xid"]
1613	w.Header().Set("Cache-Control", "max-age="+somedays())
1614	http.ServeFile(w, r, "emus/"+xid)
1615}
1616func servememe(w http.ResponseWriter, r *http.Request) {
1617	xid := mux.Vars(r)["xid"]
1618	w.Header().Set("Cache-Control", "max-age="+somedays())
1619	http.ServeFile(w, r, "memes/"+xid)
1620}
1621
1622func servefile(w http.ResponseWriter, r *http.Request) {
1623	xid := mux.Vars(r)["xid"]
1624	row := stmtFileData.QueryRow(xid)
1625	var media string
1626	var data []byte
1627	err := row.Scan(&media, &data)
1628	if err != nil {
1629		log.Printf("error loading file: %s", err)
1630		http.NotFound(w, r)
1631		return
1632	}
1633	w.Header().Set("Content-Type", media)
1634	w.Header().Set("X-Content-Type-Options", "nosniff")
1635	w.Header().Set("Cache-Control", "max-age="+somedays())
1636	w.Write(data)
1637}
1638
1639func nomoroboto(w http.ResponseWriter, r *http.Request) {
1640	io.WriteString(w, "User-agent: *\n")
1641	io.WriteString(w, "Disallow: /a\n")
1642	io.WriteString(w, "Disallow: /d\n")
1643	io.WriteString(w, "Disallow: /meme\n")
1644	for _, u := range allusers() {
1645		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1646	}
1647}
1648
1649func serve() {
1650	db := opendatabase()
1651	login.Init(db)
1652
1653	listener, err := openListener()
1654	if err != nil {
1655		log.Fatal(err)
1656	}
1657	go redeliverator()
1658
1659	debug := false
1660	getconfig("debug", &debug)
1661	readviews = templates.Load(debug,
1662		"views/honkpage.html",
1663		"views/honkfrags.html",
1664		"views/honkers.html",
1665		"views/zonkers.html",
1666		"views/combos.html",
1667		"views/honkform.html",
1668		"views/honk.html",
1669		"views/account.html",
1670		"views/about.html",
1671		"views/funzone.html",
1672		"views/login.html",
1673		"views/xzone.html",
1674		"views/header.html",
1675		"views/onts.html",
1676	)
1677	if !debug {
1678		s := "views/style.css"
1679		savedstyleparams[s] = getstyleparam(s)
1680		s = "views/local.css"
1681		savedstyleparams[s] = getstyleparam(s)
1682	}
1683
1684	bitethethumbs()
1685
1686	mux := mux.NewRouter()
1687	mux.Use(login.Checker)
1688
1689	posters := mux.Methods("POST").Subrouter()
1690	getters := mux.Methods("GET").Subrouter()
1691
1692	getters.HandleFunc("/", homepage)
1693	getters.HandleFunc("/front", homepage)
1694	getters.HandleFunc("/robots.txt", nomoroboto)
1695	getters.HandleFunc("/rss", showrss)
1696	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1697	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1698	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1699	posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1700	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1701	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1702	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1703	getters.HandleFunc("/a", avatate)
1704	getters.HandleFunc("/o", thelistingoftheontologies)
1705	getters.HandleFunc("/o/{name:[a-z0-9-]+}", showontology)
1706	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1707	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1708	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1709	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1710
1711	getters.HandleFunc("/style.css", servecss)
1712	getters.HandleFunc("/local.css", servecss)
1713	getters.HandleFunc("/about", servehtml)
1714	getters.HandleFunc("/login", servehtml)
1715	posters.HandleFunc("/dologin", login.LoginFunc)
1716	getters.HandleFunc("/logout", login.LogoutFunc)
1717
1718	loggedin := mux.NewRoute().Subrouter()
1719	loggedin.Use(login.Required)
1720	loggedin.HandleFunc("/account", accountpage)
1721	loggedin.HandleFunc("/funzone", showfunzone)
1722	loggedin.HandleFunc("/chpass", dochpass)
1723	loggedin.HandleFunc("/atme", homepage)
1724	loggedin.HandleFunc("/zonkzone", zonkzone)
1725	loggedin.HandleFunc("/xzone", xzone)
1726	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1727	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1728	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1729	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1730	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1731	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1732	loggedin.HandleFunc("/honkers", showhonkers)
1733	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1734	loggedin.HandleFunc("/h", showhonker)
1735	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1736	loggedin.HandleFunc("/c", showcombos)
1737	loggedin.HandleFunc("/t", showconvoy)
1738	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1739
1740	err = http.Serve(listener, mux)
1741	if err != nil {
1742		log.Fatal(err)
1743	}
1744}
1745
1746func cleanupdb(arg string) {
1747	db := opendatabase()
1748	days, err := strconv.Atoi(arg)
1749	if err != nil {
1750		honker := arg
1751		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1752		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1753		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1754	} else {
1755		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1756		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)
1757		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)
1758	}
1759	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1760	for _, u := range allusers() {
1761		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)
1762	}
1763}
1764
1765var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1766var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1767var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1768var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1769var stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1770var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1771var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1772var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1773var stmtSelectOnts, stmtSaveOnts, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1774
1775func preparetodie(db *sql.DB, s string) *sql.Stmt {
1776	stmt, err := db.Prepare(s)
1777	if err != nil {
1778		log.Fatalf("error %s: %s", err, s)
1779	}
1780	return stmt
1781}
1782
1783func prepareStatements(db *sql.DB) {
1784	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")
1785	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1786	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1787	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1788	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1789	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1790
1791	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 "
1792	limit := " order by honks.honkid desc limit 250"
1793	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1794	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1795	stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2")
1796	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1797	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1798	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)
1799	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1800	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1801	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1802	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1803	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1804	stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
1805
1806	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, onts) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1807	stmtSaveOnts = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)")
1808	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1809	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1810	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1811	stmtZonkIt = preparetodie(db, "delete from honks where honkid = ?")
1812	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1813	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1814	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1815	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1816	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1817	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1818	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1819	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1820	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1821	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1822	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1823	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1824	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1825	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1826	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1827	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1828	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")
1829	stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?")
1830	stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?")
1831	stmtSelectOnts = preparetodie(db, "select distinct(ontology) from onts join honks on onts.honkid = honks.honkid where (honks.userid = ? or honks.whofore = 2)")
1832}
1833
1834func ElaborateUnitTests() {
1835}
1836
1837func main() {
1838	cmd := "run"
1839	if len(os.Args) > 1 {
1840		cmd = os.Args[1]
1841	}
1842	switch cmd {
1843	case "init":
1844		initdb()
1845	case "upgrade":
1846		upgradedb()
1847	}
1848	db := opendatabase()
1849	dbversion := 0
1850	getconfig("dbversion", &dbversion)
1851	if dbversion != myVersion {
1852		log.Fatal("incorrect database version. run upgrade.")
1853	}
1854	getconfig("servermsg", &serverMsg)
1855	getconfig("servername", &serverName)
1856	getconfig("usersep", &userSep)
1857	getconfig("honksep", &honkSep)
1858	getconfig("dnf", &donotfedafterdark)
1859	prepareStatements(db)
1860	switch cmd {
1861	case "adduser":
1862		adduser()
1863	case "cleanup":
1864		arg := "30"
1865		if len(os.Args) > 2 {
1866			arg = os.Args[2]
1867		}
1868		cleanupdb(arg)
1869	case "ping":
1870		if len(os.Args) < 4 {
1871			fmt.Printf("usage: honk ping from to\n")
1872			return
1873		}
1874		name := os.Args[2]
1875		targ := os.Args[3]
1876		user, err := butwhatabout(name)
1877		if err != nil {
1878			log.Printf("unknown user")
1879			return
1880		}
1881		ping(user, targ)
1882	case "peep":
1883		peeppeep()
1884	case "run":
1885		serve()
1886	case "test":
1887		ElaborateUnitTests()
1888	default:
1889		log.Fatal("unknown command")
1890	}
1891}