all repos — honk @ a64ddd2ab4a4e7a10fbdd2cd96c84f5d0c880a3e

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