all repos — honk @ d8aabac0766e3060770b5e34fc9c3c7707982061

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