all repos — honk @ d849697b9bc210110bc9d293960bfb374e44d2a5

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	URL     string
 103	Media   string
 104	Local   bool
 105	Content []byte
 106}
 107
 108type Honker struct {
 109	ID     int64
 110	UserID int64
 111	Name   string
 112	XID    string
 113	Handle string
 114	Flavor string
 115	Combos []string
 116}
 117
 118var serverName string
 119var iconName = "icon.png"
 120var serverMsg = "Things happen."
 121
 122var userSep = "u"
 123var honkSep = "h"
 124
 125var readviews *templates.Template
 126
 127func getuserstyle(u *login.UserInfo) template.CSS {
 128	if u == nil {
 129		return ""
 130	}
 131	user, _ := butwhatabout(u.Username)
 132	if user.SkinnyCSS {
 133		return "main { max-width: 700px; }"
 134	}
 135	return ""
 136}
 137
 138func getInfo(r *http.Request) map[string]interface{} {
 139	u := login.GetUserInfo(r)
 140	templinfo := make(map[string]interface{})
 141	templinfo["StyleParam"] = getstyleparam("views/style.css")
 142	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 143	templinfo["UserStyle"] = getuserstyle(u)
 144	templinfo["ServerName"] = serverName
 145	templinfo["IconName"] = iconName
 146	templinfo["UserInfo"] = u
 147	templinfo["UserSep"] = userSep
 148	return templinfo
 149}
 150
 151var donotfedafterdark = make(map[string]bool)
 152
 153func stealthed(r *http.Request) bool {
 154	addr := r.Header.Get("X-Forwarded-For")
 155	fake := donotfedafterdark[addr]
 156	if fake {
 157		log.Printf("faking 404 for %s", addr)
 158	}
 159	return fake
 160}
 161
 162func homepage(w http.ResponseWriter, r *http.Request) {
 163	templinfo := getInfo(r)
 164	u := login.GetUserInfo(r)
 165	var honks []*Honk
 166	var userid int64 = -1
 167	if r.URL.Path == "/front" || u == nil {
 168		honks = getpublichonks()
 169	} else {
 170		userid = u.UserID
 171		if r.URL.Path == "/atme" {
 172			templinfo["PageName"] = "atme"
 173			honks = gethonksforme(userid)
 174		} else {
 175			templinfo["PageName"] = "home"
 176			honks = gethonksforuser(userid)
 177			honks = osmosis(honks, userid)
 178		}
 179		if len(honks) > 0 {
 180			templinfo["TopXID"] = honks[0].XID
 181		}
 182		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 183	}
 184
 185	tname := "honkpage.html"
 186	if topxid := r.FormValue("topxid"); topxid != "" {
 187		for i, h := range honks {
 188			if h.XID == topxid {
 189				honks = honks[0:i]
 190				break
 191			}
 192		}
 193		log.Printf("topxid %d frags", len(honks))
 194		tname = "honkfrags.html"
 195	}
 196
 197	reverbolate(userid, honks)
 198
 199	templinfo["Honks"] = honks
 200	templinfo["ShowRSS"] = true
 201	templinfo["ServerMessage"] = serverMsg
 202	if u == nil {
 203		w.Header().Set("Cache-Control", "max-age=60")
 204	} else {
 205		w.Header().Set("Cache-Control", "max-age=0")
 206	}
 207	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 208
 209	err := readviews.Execute(w, tname, templinfo)
 210	if err != nil {
 211		log.Print(err)
 212	}
 213}
 214
 215func showfunzone(w http.ResponseWriter, r *http.Request) {
 216	var emunames, memenames []string
 217	dir, err := os.Open("emus")
 218	if err == nil {
 219		emunames, _ = dir.Readdirnames(0)
 220		dir.Close()
 221	}
 222	for i, e := range emunames {
 223		if len(e) > 4 {
 224			emunames[i] = e[:len(e)-4]
 225		}
 226	}
 227	dir, err = os.Open("memes")
 228	if err == nil {
 229		memenames, _ = dir.Readdirnames(0)
 230		dir.Close()
 231	}
 232	templinfo := getInfo(r)
 233	templinfo["Emus"] = emunames
 234	templinfo["Memes"] = memenames
 235	err = readviews.Execute(w, "funzone.html", templinfo)
 236	if err != nil {
 237		log.Print(err)
 238	}
 239
 240}
 241
 242func showrss(w http.ResponseWriter, r *http.Request) {
 243	name := mux.Vars(r)["name"]
 244
 245	var honks []*Honk
 246	if name != "" {
 247		honks = gethonksbyuser(name, false)
 248	} else {
 249		honks = getpublichonks()
 250	}
 251	if len(honks) > 20 {
 252		honks = honks[0:20]
 253	}
 254	reverbolate(-1, honks)
 255
 256	home := fmt.Sprintf("https://%s/", serverName)
 257	base := home
 258	if name != "" {
 259		home += "u/" + name
 260		name += " "
 261	}
 262	feed := rss.Feed{
 263		Title:       name + "honk",
 264		Link:        home,
 265		Description: name + "honk rss",
 266		Image: &rss.Image{
 267			URL:   base + "icon.png",
 268			Title: name + "honk rss",
 269			Link:  home,
 270		},
 271	}
 272	var modtime time.Time
 273	for _, honk := range honks {
 274		if !firstclass(honk) {
 275			continue
 276		}
 277		desc := string(honk.HTML)
 278		for _, d := range honk.Donks {
 279			desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 280				d.URL, html.EscapeString(d.Name))
 281		}
 282
 283		feed.Items = append(feed.Items, &rss.Item{
 284			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 285			Description: rss.CData{desc},
 286			Link:        honk.URL,
 287			PubDate:     honk.Date.Format(time.RFC1123),
 288			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 289		})
 290		if honk.Date.After(modtime) {
 291			modtime = honk.Date
 292		}
 293	}
 294	w.Header().Set("Cache-Control", "max-age=300")
 295	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 296
 297	err := feed.Write(w)
 298	if err != nil {
 299		log.Printf("error writing rss: %s", err)
 300	}
 301}
 302
 303func butwhatabout(name string) (*WhatAbout, error) {
 304	row := stmtWhatAbout.QueryRow(name)
 305	var user WhatAbout
 306	var options string
 307	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &options)
 308	user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
 309	user.SkinnyCSS = strings.Contains(options, " skinny ")
 310	return &user, err
 311}
 312
 313func crappola(j junk.Junk) bool {
 314	t, _ := j.GetString("type")
 315	a, _ := j.GetString("actor")
 316	o, _ := j.GetString("object")
 317	if t == "Delete" && a == o {
 318		log.Printf("crappola from %s", a)
 319		return true
 320	}
 321	return false
 322}
 323
 324func ping(user *WhatAbout, who string) {
 325	box, err := getboxes(who)
 326	if err != nil {
 327		log.Printf("no inbox for ping: %s", err)
 328		return
 329	}
 330	j := junk.New()
 331	j["@context"] = itiswhatitis
 332	j["type"] = "Ping"
 333	j["id"] = user.URL + "/ping/" + xfiltrate()
 334	j["actor"] = user.URL
 335	j["to"] = who
 336	keyname, key := ziggy(user.Name)
 337	err = PostJunk(keyname, key, box.In, j)
 338	if err != nil {
 339		log.Printf("can't send ping: %s", err)
 340		return
 341	}
 342	log.Printf("sent ping to %s: %s", who, j["id"])
 343}
 344
 345func pong(user *WhatAbout, who string, obj string) {
 346	box, err := getboxes(who)
 347	if err != nil {
 348		log.Printf("no inbox for pong %s : %s", who, err)
 349		return
 350	}
 351	j := junk.New()
 352	j["@context"] = itiswhatitis
 353	j["type"] = "Pong"
 354	j["id"] = user.URL + "/pong/" + xfiltrate()
 355	j["actor"] = user.URL
 356	j["to"] = who
 357	j["object"] = obj
 358	keyname, key := ziggy(user.Name)
 359	err = PostJunk(keyname, key, box.In, j)
 360	if err != nil {
 361		log.Printf("can't send pong: %s", err)
 362		return
 363	}
 364}
 365
 366func inbox(w http.ResponseWriter, r *http.Request) {
 367	name := mux.Vars(r)["name"]
 368	user, err := butwhatabout(name)
 369	if err != nil {
 370		http.NotFound(w, r)
 371		return
 372	}
 373	var buf bytes.Buffer
 374	io.Copy(&buf, r.Body)
 375	payload := buf.Bytes()
 376	j, err := junk.Read(bytes.NewReader(payload))
 377	if err != nil {
 378		log.Printf("bad payload: %s", err)
 379		io.WriteString(os.Stdout, "bad payload\n")
 380		os.Stdout.Write(payload)
 381		io.WriteString(os.Stdout, "\n")
 382		return
 383	}
 384	if crappola(j) {
 385		return
 386	}
 387	keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
 388	if err != nil {
 389		log.Printf("inbox message failed signature: %s", err)
 390		if keyname != "" {
 391			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 392			if err != nil {
 393				log.Printf("still failed: %s", err)
 394			}
 395		}
 396		if err != nil {
 397			return
 398		}
 399	}
 400	what, _ := j.GetString("type")
 401	if what == "Like" {
 402		return
 403	}
 404	who, _ := j.GetString("actor")
 405	origin := keymatch(keyname, who)
 406	if origin == "" {
 407		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 408		return
 409	}
 410	objid, _ := j.GetString("id")
 411	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 412		log.Printf("ignoring thumb sucker %s", who)
 413		return
 414	}
 415	switch what {
 416	case "Ping":
 417		obj, _ := j.GetString("id")
 418		log.Printf("ping from %s: %s", who, obj)
 419		pong(user, who, obj)
 420	case "Pong":
 421		obj, _ := j.GetString("object")
 422		log.Printf("pong from %s: %s", who, obj)
 423	case "Follow":
 424		obj, _ := j.GetString("object")
 425		if obj == user.URL {
 426			log.Printf("updating honker follow: %s", who)
 427			stmtSaveDub.Exec(user.ID, who, who, "dub")
 428			go rubadubdub(user, j)
 429		} else {
 430			log.Printf("can't follow %s", obj)
 431		}
 432	case "Accept":
 433		log.Printf("updating honker accept: %s", who)
 434		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 435		if err != nil {
 436			log.Printf("error updating honker: %s", err)
 437			return
 438		}
 439	case "Update":
 440		obj, ok := j.GetMap("object")
 441		if ok {
 442			what, _ := obj.GetString("type")
 443			switch what {
 444			case "Person":
 445				return
 446			case "Question":
 447				return
 448			}
 449		}
 450		log.Printf("unknown Update activity")
 451		fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 452		j.Write(fd)
 453		io.WriteString(fd, "\n")
 454		fd.Close()
 455
 456	case "Undo":
 457		obj, ok := j.GetMap("object")
 458		if !ok {
 459			log.Printf("unknown undo no object")
 460		} else {
 461			what, _ := obj.GetString("type")
 462			switch what {
 463			case "Follow":
 464				log.Printf("updating honker undo: %s", who)
 465				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 466				if err != nil {
 467					log.Printf("error updating honker: %s", err)
 468					return
 469				}
 470			case "Announce":
 471				xid, _ := obj.GetString("object")
 472				log.Printf("undo announce: %s", xid)
 473			case "Like":
 474			default:
 475				log.Printf("unknown undo: %s", what)
 476			}
 477		}
 478	default:
 479		go consumeactivity(user, j, origin)
 480	}
 481}
 482
 483func ximport(w http.ResponseWriter, r *http.Request) {
 484	xid := r.FormValue("xid")
 485	p, _ := investigate(xid)
 486	if p != nil {
 487		xid = p.XID
 488	}
 489	j, err := GetJunk(xid)
 490	if err != nil {
 491		http.Error(w, "error getting external object", http.StatusInternalServerError)
 492		log.Printf("error getting external object: %s", err)
 493		return
 494	}
 495	log.Printf("importing %s", xid)
 496	u := login.GetUserInfo(r)
 497	user, _ := butwhatabout(u.Username)
 498
 499	what, _ := j.GetString("type")
 500	if isactor(what) {
 501		outbox, _ := j.GetString("outbox")
 502		gimmexonks(user, outbox)
 503		http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
 504		return
 505	}
 506	xonk := xonkxonk(user, j, originate(xid))
 507	convoy := ""
 508	if xonk != nil {
 509		convoy = xonk.Convoy
 510		savexonk(user, xonk)
 511	}
 512	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 513}
 514
 515func xzone(w http.ResponseWriter, r *http.Request) {
 516	u := login.GetUserInfo(r)
 517	rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
 518	if err != nil {
 519		log.Printf("query err: %s", err)
 520		return
 521	}
 522	defer rows.Close()
 523	var honkers []Honker
 524	for rows.Next() {
 525		var xid string
 526		rows.Scan(&xid)
 527		honkers = append(honkers, Honker{XID: xid})
 528	}
 529	rows.Close()
 530	for i, _ := range honkers {
 531		_, honkers[i].Handle = handles(honkers[i].XID)
 532	}
 533	templinfo := getInfo(r)
 534	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 535	templinfo["Honkers"] = honkers
 536	err = readviews.Execute(w, "xzone.html", templinfo)
 537	if err != nil {
 538		log.Print(err)
 539	}
 540}
 541
 542func outbox(w http.ResponseWriter, r *http.Request) {
 543	name := mux.Vars(r)["name"]
 544	user, err := butwhatabout(name)
 545	if err != nil {
 546		http.NotFound(w, r)
 547		return
 548	}
 549	if stealthed(r) {
 550		http.NotFound(w, r)
 551		return
 552	}
 553
 554	honks := gethonksbyuser(name, false)
 555	if len(honks) > 20 {
 556		honks = honks[0:20]
 557	}
 558
 559	var jonks []junk.Junk
 560	for _, h := range honks {
 561		j, _ := jonkjonk(user, h)
 562		jonks = append(jonks, j)
 563	}
 564
 565	j := junk.New()
 566	j["@context"] = itiswhatitis
 567	j["id"] = user.URL + "/outbox"
 568	j["type"] = "OrderedCollection"
 569	j["totalItems"] = len(jonks)
 570	j["orderedItems"] = jonks
 571
 572	w.Header().Set("Content-Type", theonetruename)
 573	j.Write(w)
 574}
 575
 576func emptiness(w http.ResponseWriter, r *http.Request) {
 577	name := mux.Vars(r)["name"]
 578	user, err := butwhatabout(name)
 579	if err != nil {
 580		http.NotFound(w, r)
 581		return
 582	}
 583	colname := "/followers"
 584	if strings.HasSuffix(r.URL.Path, "/following") {
 585		colname = "/following"
 586	}
 587	j := junk.New()
 588	j["@context"] = itiswhatitis
 589	j["id"] = user.URL + colname
 590	j["type"] = "OrderedCollection"
 591	j["totalItems"] = 0
 592	j["orderedItems"] = []junk.Junk{}
 593
 594	w.Header().Set("Content-Type", theonetruename)
 595	j.Write(w)
 596}
 597
 598func showuser(w http.ResponseWriter, r *http.Request) {
 599	name := mux.Vars(r)["name"]
 600	user, err := butwhatabout(name)
 601	if err != nil {
 602		log.Printf("user not found %s: %s", name, err)
 603		http.NotFound(w, r)
 604		return
 605	}
 606	if friendorfoe(r.Header.Get("Accept")) {
 607		j := asjonker(user)
 608		w.Header().Set("Content-Type", theonetruename)
 609		j.Write(w)
 610		return
 611	}
 612	u := login.GetUserInfo(r)
 613	honks := gethonksbyuser(name, u != nil && u.Username == name)
 614	honkpage(w, r, u, user, honks, "")
 615}
 616
 617func showhonker(w http.ResponseWriter, r *http.Request) {
 618	u := login.GetUserInfo(r)
 619	name := mux.Vars(r)["name"]
 620	var honks []*Honk
 621	if name == "" {
 622		name = r.FormValue("xid")
 623		honks = gethonksbyxonker(u.UserID, name)
 624	} else {
 625		honks = gethonksbyhonker(u.UserID, name)
 626	}
 627	name = html.EscapeString(name)
 628	msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
 629	honkpage(w, r, u, nil, honks, template.HTML(msg))
 630}
 631
 632func showcombo(w http.ResponseWriter, r *http.Request) {
 633	name := mux.Vars(r)["name"]
 634	u := login.GetUserInfo(r)
 635	honks := gethonksbycombo(u.UserID, name)
 636	honks = osmosis(honks, u.UserID)
 637	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
 638}
 639func showconvoy(w http.ResponseWriter, r *http.Request) {
 640	c := r.FormValue("c")
 641	u := login.GetUserInfo(r)
 642	honks := gethonksbyconvoy(u.UserID, c)
 643	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
 644}
 645func showsearch(w http.ResponseWriter, r *http.Request) {
 646	q := r.FormValue("q")
 647	u := login.GetUserInfo(r)
 648	honks := gethonksbysearch(u.UserID, q)
 649	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks for search: "+q)))
 650}
 651func showontology(w http.ResponseWriter, r *http.Request) {
 652	name := mux.Vars(r)["name"]
 653	u := login.GetUserInfo(r)
 654	var userid int64 = -1
 655	if u != nil {
 656		userid = u.UserID
 657	}
 658	honks := gethonksbyontology(userid, "#"+name)
 659	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by ontology: "+name)))
 660}
 661
 662func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) {
 663	u := login.GetUserInfo(r)
 664	var userid int64 = -1
 665	if u != nil {
 666		userid = u.UserID
 667	}
 668	rows, err := stmtSelectOnts.Query(userid)
 669	if err != nil {
 670		log.Printf("selection error: %s", err)
 671		return
 672	}
 673	var onts [][]string
 674	for rows.Next() {
 675		var o string
 676		err := rows.Scan(&o)
 677		if err != nil {
 678			log.Printf("error scanning ont: %s", err)
 679			continue
 680		}
 681		onts = append(onts, []string{o, o[1:]})
 682	}
 683	if u == nil {
 684		w.Header().Set("Cache-Control", "max-age=300")
 685	}
 686	templinfo := getInfo(r)
 687	templinfo["Onts"] = onts
 688	err = readviews.Execute(w, "onts.html", templinfo)
 689	if err != nil {
 690		log.Print(err)
 691	}
 692}
 693
 694func showhonk(w http.ResponseWriter, r *http.Request) {
 695	name := mux.Vars(r)["name"]
 696	user, err := butwhatabout(name)
 697	if err != nil {
 698		http.NotFound(w, r)
 699		return
 700	}
 701	if stealthed(r) {
 702		http.NotFound(w, r)
 703		return
 704	}
 705
 706	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 707	honk := getxonk(user.ID, xid)
 708	if honk == nil {
 709		http.NotFound(w, r)
 710		return
 711	}
 712	u := login.GetUserInfo(r)
 713	if u != nil && u.UserID != user.ID {
 714		u = nil
 715	}
 716	if !honk.Public {
 717		if u == nil {
 718			http.NotFound(w, r)
 719			return
 720
 721		}
 722		honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
 723		return
 724	}
 725	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
 726	if friendorfoe(r.Header.Get("Accept")) {
 727		for _, h := range rawhonks {
 728			if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
 729				honk.Replies = append(honk.Replies, h)
 730			}
 731		}
 732		donksforhonks([]*Honk{honk})
 733		_, j := jonkjonk(user, honk)
 734		j["@context"] = itiswhatitis
 735		w.Header().Set("Content-Type", theonetruename)
 736		j.Write(w)
 737		return
 738	}
 739	var honks []*Honk
 740	for _, h := range rawhonks {
 741		if h.Public && (h.Whofore == 2 || h.IsAcked()) {
 742			honks = append(honks, h)
 743		}
 744	}
 745
 746	honkpage(w, r, u, nil, honks, "one honk maybe more")
 747}
 748
 749func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 750	honks []*Honk, infomsg template.HTML) {
 751	templinfo := getInfo(r)
 752	var userid int64 = -1
 753	if u != nil {
 754		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 755		userid = u.UserID
 756	}
 757	if u == nil {
 758		w.Header().Set("Cache-Control", "max-age=60")
 759	}
 760	reverbolate(userid, honks)
 761	if user != nil {
 762		filt := htfilter.New()
 763		templinfo["Name"] = user.Name
 764		whatabout := user.About
 765		whatabout = obfusbreak(user.About)
 766		templinfo["WhatAbout"], _ = filt.String(whatabout)
 767	}
 768	templinfo["Honks"] = honks
 769	templinfo["ServerMessage"] = infomsg
 770	err := readviews.Execute(w, "honkpage.html", templinfo)
 771	if err != nil {
 772		log.Print(err)
 773	}
 774}
 775
 776func saveuser(w http.ResponseWriter, r *http.Request) {
 777	whatabout := r.FormValue("whatabout")
 778	u := login.GetUserInfo(r)
 779	db := opendatabase()
 780	options := ""
 781	if r.FormValue("skinny") == "skinny" {
 782		options += " skinny "
 783	}
 784	_, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
 785	if err != nil {
 786		log.Printf("error bouting what: %s", err)
 787	}
 788
 789	http.Redirect(w, r, "/account", http.StatusSeeOther)
 790}
 791
 792func gethonkers(userid int64) []*Honker {
 793	rows, err := stmtHonkers.Query(userid)
 794	if err != nil {
 795		log.Printf("error querying honkers: %s", err)
 796		return nil
 797	}
 798	defer rows.Close()
 799	var honkers []*Honker
 800	for rows.Next() {
 801		var f Honker
 802		var combos string
 803		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 804		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 805		if err != nil {
 806			log.Printf("error scanning honker: %s", err)
 807			return nil
 808		}
 809		honkers = append(honkers, &f)
 810	}
 811	return honkers
 812}
 813
 814func getdubs(userid int64) []*Honker {
 815	rows, err := stmtDubbers.Query(userid)
 816	if err != nil {
 817		log.Printf("error querying dubs: %s", err)
 818		return nil
 819	}
 820	defer rows.Close()
 821	var honkers []*Honker
 822	for rows.Next() {
 823		var f Honker
 824		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 825		if err != nil {
 826			log.Printf("error scanning honker: %s", err)
 827			return nil
 828		}
 829		honkers = append(honkers, &f)
 830	}
 831	return honkers
 832}
 833
 834func allusers() []login.UserInfo {
 835	var users []login.UserInfo
 836	rows, _ := opendatabase().Query("select userid, username from users")
 837	defer rows.Close()
 838	for rows.Next() {
 839		var u login.UserInfo
 840		rows.Scan(&u.UserID, &u.Username)
 841		users = append(users, u)
 842	}
 843	return users
 844}
 845
 846func getxonk(userid int64, xid string) *Honk {
 847	h := new(Honk)
 848	var dt, aud, onts string
 849	row := stmtOneXonk.QueryRow(userid, xid)
 850	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 851		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
 852	if err != nil {
 853		if err != sql.ErrNoRows {
 854			log.Printf("error scanning xonk: %s", err)
 855		}
 856		return nil
 857	}
 858	h.Date, _ = time.Parse(dbtimeformat, dt)
 859	h.Audience = strings.Split(aud, " ")
 860	h.Public = !keepitquiet(h.Audience)
 861	if len(onts) > 0 {
 862		h.Onts = strings.Split(onts, " ")
 863	}
 864	return h
 865}
 866
 867func getbonk(userid int64, xid string) *Honk {
 868	h := new(Honk)
 869	var dt, aud, onts string
 870	row := stmtOneBonk.QueryRow(userid, xid)
 871	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 872		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
 873	if err != nil {
 874		if err != sql.ErrNoRows {
 875			log.Printf("error scanning xonk: %s", err)
 876		}
 877		return nil
 878	}
 879	h.Date, _ = time.Parse(dbtimeformat, dt)
 880	h.Audience = strings.Split(aud, " ")
 881	h.Public = !keepitquiet(h.Audience)
 882	if len(onts) > 0 {
 883		h.Onts = strings.Split(onts, " ")
 884	}
 885	return h
 886}
 887
 888func getpublichonks() []*Honk {
 889	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 890	rows, err := stmtPublicHonks.Query(dt)
 891	return getsomehonks(rows, err)
 892}
 893func gethonksbyuser(name string, includeprivate bool) []*Honk {
 894	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 895	whofore := 2
 896	if includeprivate {
 897		whofore = 3
 898	}
 899	rows, err := stmtUserHonks.Query(whofore, name, dt)
 900	return getsomehonks(rows, err)
 901}
 902func gethonksforuser(userid int64) []*Honk {
 903	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 904	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 905	return getsomehonks(rows, err)
 906}
 907func gethonksforme(userid int64) []*Honk {
 908	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 909	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 910	return getsomehonks(rows, err)
 911}
 912func gethonksbyhonker(userid int64, honker string) []*Honk {
 913	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 914	return getsomehonks(rows, err)
 915}
 916func gethonksbyxonker(userid int64, xonker string) []*Honk {
 917	rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
 918	return getsomehonks(rows, err)
 919}
 920func gethonksbycombo(userid int64, combo string) []*Honk {
 921	combo = "% " + combo + " %"
 922	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 923	return getsomehonks(rows, err)
 924}
 925func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 926	rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
 927	honks := getsomehonks(rows, err)
 928	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 929		honks[i], honks[j] = honks[j], honks[i]
 930	}
 931	return honks
 932}
 933func gethonksbysearch(userid int64, q string) []*Honk {
 934	q = "%" + q + "%"
 935	rows, err := stmtHonksBySearch.Query(userid, q)
 936	honks := getsomehonks(rows, err)
 937	return honks
 938}
 939func gethonksbyontology(userid int64, name string) []*Honk {
 940	rows, err := stmtHonksByOntology.Query(name, userid, userid)
 941	honks := getsomehonks(rows, err)
 942	return honks
 943}
 944
 945func getsomehonks(rows *sql.Rows, err error) []*Honk {
 946	if err != nil {
 947		log.Printf("error querying honks: %s", err)
 948		return nil
 949	}
 950	defer rows.Close()
 951	var honks []*Honk
 952	for rows.Next() {
 953		var h Honk
 954		var dt, aud, onts string
 955		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID,
 956			&h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
 957		if err != nil {
 958			log.Printf("error scanning honks: %s", err)
 959			return nil
 960		}
 961		h.Date, _ = time.Parse(dbtimeformat, dt)
 962		h.Audience = strings.Split(aud, " ")
 963		h.Public = !keepitquiet(h.Audience)
 964		if len(onts) > 0 {
 965			h.Onts = strings.Split(onts, " ")
 966		}
 967		honks = append(honks, &h)
 968	}
 969	rows.Close()
 970	donksforhonks(honks)
 971	return honks
 972}
 973
 974func donksforhonks(honks []*Honk) {
 975	db := opendatabase()
 976	var ids []string
 977	hmap := make(map[int64]*Honk)
 978	for _, h := range honks {
 979		ids = append(ids, fmt.Sprintf("%d", h.ID))
 980		hmap[h.ID] = h
 981	}
 982	q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
 983	rows, err := db.Query(q)
 984	if err != nil {
 985		log.Printf("error querying donks: %s", err)
 986		return
 987	}
 988	defer rows.Close()
 989	for rows.Next() {
 990		var hid int64
 991		var d Donk
 992		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 993		if err != nil {
 994			log.Printf("error scanning donk: %s", err)
 995			continue
 996		}
 997		h := hmap[hid]
 998		h.Donks = append(h.Donks, &d)
 999	}
1000}
1001
1002func savebonk(w http.ResponseWriter, r *http.Request) {
1003	xid := r.FormValue("xid")
1004	userinfo := login.GetUserInfo(r)
1005	user, _ := butwhatabout(userinfo.Username)
1006
1007	log.Printf("bonking %s", xid)
1008
1009	xonk := getxonk(userinfo.UserID, xid)
1010	if xonk == nil {
1011		return
1012	}
1013	if !xonk.Public {
1014		return
1015	}
1016	donksforhonks([]*Honk{xonk})
1017
1018	_, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
1019	if err != nil {
1020		log.Printf("error acking bonk: %s", err)
1021	}
1022
1023	oonker := xonk.Oonker
1024	if oonker == "" {
1025		oonker = xonk.Honker
1026	}
1027	dt := time.Now().UTC()
1028	bonk := Honk{
1029		UserID:   userinfo.UserID,
1030		Username: userinfo.Username,
1031		What:     "bonk",
1032		Honker:   user.URL,
1033		XID:      xonk.XID,
1034		Date:     dt,
1035		Donks:    xonk.Donks,
1036		Convoy:   xonk.Convoy,
1037		Audience: []string{thewholeworld, oonker},
1038		Public:   true,
1039	}
1040
1041	aud := strings.Join(bonk.Audience, " ")
1042	whofore := 2
1043	onts := xonk.Onts
1044	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
1045		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
1046		xonk.Precis, oonker, 0, strings.Join(onts, " "))
1047	if err != nil {
1048		log.Printf("error saving bonk: %s", err)
1049		return
1050	}
1051	bonk.ID, _ = res.LastInsertId()
1052	for _, d := range bonk.Donks {
1053		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
1054		if err != nil {
1055			log.Printf("err saving donk: %s", err)
1056			return
1057		}
1058	}
1059	for _, o := range onts {
1060		_, err = stmtSaveOnts.Exec(strings.ToLower(o), bonk.ID)
1061		if err != nil {
1062			log.Printf("error saving ont: %s", err)
1063		}
1064	}
1065
1066	go honkworldwide(user, &bonk)
1067}
1068
1069func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
1070	zonk := Honk{
1071		What:     what,
1072		XID:      xonk.XID,
1073		Date:     time.Now().UTC(),
1074		Audience: oneofakind(xonk.Audience),
1075	}
1076	zonk.Public = !keepitquiet(zonk.Audience)
1077
1078	log.Printf("announcing %sed honk: %s", what, xonk.XID)
1079	go honkworldwide(user, &zonk)
1080}
1081
1082func zonkit(w http.ResponseWriter, r *http.Request) {
1083	wherefore := r.FormValue("wherefore")
1084	what := r.FormValue("what")
1085	userinfo := login.GetUserInfo(r)
1086	user, _ := butwhatabout(userinfo.Username)
1087
1088	if wherefore == "ack" {
1089		xonk := getxonk(userinfo.UserID, what)
1090		if xonk != nil {
1091			_, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1092			if err != nil {
1093				log.Printf("error acking: %s", err)
1094			}
1095			sendzonkofsorts(xonk, user, "ack")
1096		}
1097		return
1098	}
1099
1100	if wherefore == "deack" {
1101		xonk := getxonk(userinfo.UserID, what)
1102		if xonk != nil {
1103			_, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1104			if err != nil {
1105				log.Printf("error deacking: %s", err)
1106			}
1107			sendzonkofsorts(xonk, user, "deack")
1108		}
1109		return
1110	}
1111
1112	if wherefore == "unbonk" {
1113		xonk := getbonk(userinfo.UserID, what)
1114		if xonk != nil {
1115			_, err := stmtZonkDonks.Exec(xonk.ID)
1116			if err != nil {
1117				log.Printf("error zonking: %s", err)
1118			}
1119			_, err = stmtZonkIt.Exec(xonk.ID)
1120			if err != nil {
1121				log.Printf("error zonking: %s", err)
1122			}
1123			xonk = getxonk(userinfo.UserID, what)
1124			_, err = stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1125			if err != nil {
1126				log.Printf("error unbonking: %s", err)
1127			}
1128			sendzonkofsorts(xonk, user, "unbonk")
1129		}
1130		return
1131	}
1132
1133	log.Printf("zonking %s %s", wherefore, what)
1134	if wherefore == "zonk" {
1135		xonk := getxonk(userinfo.UserID, what)
1136		if xonk != nil {
1137			_, err := stmtZonkDonks.Exec(xonk.ID)
1138			if err != nil {
1139				log.Printf("error zonking: %s", err)
1140			}
1141			_, err = stmtZonkIt.Exec(xonk.ID)
1142			if err != nil {
1143				log.Printf("error zonking: %s", err)
1144			}
1145			if xonk.Whofore == 2 || xonk.Whofore == 3 {
1146				sendzonkofsorts(xonk, user, "zonk")
1147			}
1148		}
1149	}
1150	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1151	if err != nil {
1152		log.Printf("error saving zonker: %s", err)
1153		return
1154	}
1155}
1156
1157func savehonk(w http.ResponseWriter, r *http.Request) {
1158	rid := r.FormValue("rid")
1159	noise := r.FormValue("noise")
1160
1161	userinfo := login.GetUserInfo(r)
1162	user, _ := butwhatabout(userinfo.Username)
1163
1164	dt := time.Now().UTC()
1165	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1166	what := "honk"
1167	if rid != "" {
1168		what = "tonk"
1169	}
1170	honk := Honk{
1171		UserID:   userinfo.UserID,
1172		Username: userinfo.Username,
1173		What:     "honk",
1174		Honker:   user.URL,
1175		XID:      xid,
1176		Date:     dt,
1177	}
1178	if strings.HasPrefix(noise, "DZ:") {
1179		idx := strings.Index(noise, "\n")
1180		if idx == -1 {
1181			honk.Precis = noise
1182			noise = ""
1183		} else {
1184			honk.Precis = noise[:idx]
1185			noise = noise[idx+1:]
1186		}
1187	}
1188	noise = hooterize(noise)
1189	noise = strings.TrimSpace(noise)
1190	honk.Precis = strings.TrimSpace(honk.Precis)
1191
1192	var convoy string
1193	if rid != "" {
1194		xonk := getxonk(userinfo.UserID, rid)
1195		if xonk != nil {
1196			if xonk.Public {
1197				honk.Audience = append(honk.Audience, xonk.Audience...)
1198			}
1199			convoy = xonk.Convoy
1200		} else {
1201			xonkaud, c := whosthere(rid)
1202			honk.Audience = append(honk.Audience, xonkaud...)
1203			convoy = c
1204		}
1205		for i, a := range honk.Audience {
1206			if a == thewholeworld {
1207				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1208				break
1209			}
1210		}
1211		honk.RID = rid
1212	} else {
1213		honk.Audience = []string{thewholeworld}
1214	}
1215	if noise != "" && noise[0] == '@' {
1216		honk.Audience = append(grapevine(noise), honk.Audience...)
1217	} else {
1218		honk.Audience = append(honk.Audience, grapevine(noise)...)
1219	}
1220	if convoy == "" {
1221		convoy = "data:,electrichonkytonk-" + xfiltrate()
1222	}
1223	butnottooloud(honk.Audience)
1224	honk.Audience = oneofakind(honk.Audience)
1225	if len(honk.Audience) == 0 {
1226		log.Printf("honk to nowhere")
1227		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1228		return
1229	}
1230	honk.Public = !keepitquiet(honk.Audience)
1231	noise = obfusbreak(noise)
1232	honk.Noise = noise
1233	honk.Convoy = convoy
1234
1235	donkxid := r.FormValue("donkxid")
1236	if donkxid == "" {
1237		file, filehdr, err := r.FormFile("donk")
1238		if err == nil {
1239			var buf bytes.Buffer
1240			io.Copy(&buf, file)
1241			file.Close()
1242			data := buf.Bytes()
1243			xid := xfiltrate()
1244			var media, name string
1245			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1246			if err == nil {
1247				data = img.Data
1248				format := img.Format
1249				media = "image/" + format
1250				if format == "jpeg" {
1251					format = "jpg"
1252				}
1253				name = xid + "." + format
1254				xid = name
1255			} else {
1256				maxsize := 100000
1257				if len(data) > maxsize {
1258					log.Printf("bad image: %s too much text: %d", err, len(data))
1259					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1260					return
1261				}
1262				for i := 0; i < len(data); i++ {
1263					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1264						log.Printf("bad image: %s not text: %d", err, data[i])
1265						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1266						return
1267					}
1268				}
1269				media = "text/plain"
1270				name = filehdr.Filename
1271				if name == "" {
1272					name = xid + ".txt"
1273				}
1274				xid += ".txt"
1275			}
1276			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1277			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1278			if err != nil {
1279				log.Printf("unable to save image: %s", err)
1280				return
1281			}
1282			var d Donk
1283			d.FileID, _ = res.LastInsertId()
1284			d.XID = name
1285			d.Name = name
1286			d.Media = media
1287			d.URL = url
1288			d.Local = true
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			donk.XID = xid
1300			donk.Local = true
1301			donk.URL = url
1302			honk.Donks = append(honk.Donks, &donk)
1303		} else {
1304			log.Printf("can't find file: %s", xid)
1305		}
1306	}
1307	herd := herdofemus(honk.Noise)
1308	for _, e := range herd {
1309		donk := savedonk(e.ID, e.Name, "image/png", true)
1310		if donk != nil {
1311			donk.Name = e.Name
1312			honk.Donks = append(honk.Donks, donk)
1313		}
1314	}
1315	memetize(&honk)
1316
1317	aud := strings.Join(honk.Audience, " ")
1318	whofore := 2
1319	if !honk.Public {
1320		whofore = 3
1321	}
1322	if r.FormValue("preview") == "preview" {
1323		honks := []*Honk{&honk}
1324		reverbolate(userinfo.UserID, honks)
1325		templinfo := getInfo(r)
1326		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1327		templinfo["Honks"] = honks
1328		templinfo["InReplyTo"] = r.FormValue("rid")
1329		templinfo["Noise"] = r.FormValue("noise")
1330		templinfo["SavedFile"] = donkxid
1331		templinfo["ServerMessage"] = "honk preview"
1332		err := readviews.Execute(w, "honkpage.html", templinfo)
1333		if err != nil {
1334			log.Print(err)
1335		}
1336		return
1337	}
1338	honk.Onts = oneofakind(ontologies(honk.Noise))
1339	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1340		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1341		honk.Precis, honk.Oonker, 0, strings.Join(honk.Onts, " "))
1342	if err != nil {
1343		log.Printf("error saving honk: %s", err)
1344		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1345		return
1346	}
1347	honk.ID, _ = res.LastInsertId()
1348	for _, d := range honk.Donks {
1349		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1350		if err != nil {
1351			log.Printf("err saving donk: %s", err)
1352			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1353			return
1354		}
1355	}
1356	for _, o := range honk.Onts {
1357		_, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1358		if err != nil {
1359			log.Printf("error saving ont: %s", err)
1360		}
1361	}
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 *sql.Stmt
1806var stmtHasHonker, 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	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? 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, 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}