all repos — honk @ 0b8fb01b8a4c0efcf1cbb285e1a2450eeeee0e6d

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