all repos — honk @ 7c66f73f21acb630e66dbbec3ded3277ee9016e3

my fork of honk

honk.go (view raw)

   1//
   2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
   3//
   4// Permission to use, copy, modify, and distribute this software for any
   5// purpose with or without fee is hereby granted, provided that the above
   6// copyright notice and this permission notice appear in all copies.
   7//
   8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15
  16package main
  17
  18import (
  19	"bytes"
  20	"database/sql"
  21	"fmt"
  22	"html"
  23	"html/template"
  24	"io"
  25	"log"
  26	notrand "math/rand"
  27	"net/http"
  28	"os"
  29	"sort"
  30	"strconv"
  31	"strings"
  32	"time"
  33
  34	"github.com/gorilla/mux"
  35	"humungus.tedunangst.com/r/webs/htfilter"
  36	"humungus.tedunangst.com/r/webs/image"
  37	"humungus.tedunangst.com/r/webs/login"
  38	"humungus.tedunangst.com/r/webs/rss"
  39	"humungus.tedunangst.com/r/webs/templates"
  40)
  41
  42type WhatAbout struct {
  43	ID      int64
  44	Name    string
  45	Display string
  46	About   string
  47	Key     string
  48	URL     string
  49}
  50
  51type Honk struct {
  52	ID       int64
  53	UserID   int64
  54	Username string
  55	What     string
  56	Honker   string
  57	Handle   string
  58	Oonker   string
  59	XID      string
  60	RID      string
  61	Date     time.Time
  62	URL      string
  63	Noise    string
  64	Precis   string
  65	Convoy   string
  66	Audience []string
  67	Public   bool
  68	Whofore  int64
  69	HTML     template.HTML
  70	Donks    []*Donk
  71}
  72
  73type Donk struct {
  74	FileID  int64
  75	XID     string
  76	Name    string
  77	URL     string
  78	Media   string
  79	Local   bool
  80	Content []byte
  81}
  82
  83type Honker struct {
  84	ID     int64
  85	UserID int64
  86	Name   string
  87	XID    string
  88	Flavor string
  89	Combos []string
  90}
  91
  92var serverName string
  93var iconName = "icon.png"
  94
  95var readviews *templates.Template
  96
  97func getInfo(r *http.Request) map[string]interface{} {
  98	templinfo := make(map[string]interface{})
  99	templinfo["StyleParam"] = getstyleparam("views/style.css")
 100	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 101	templinfo["ServerName"] = serverName
 102	templinfo["IconName"] = iconName
 103	templinfo["UserInfo"] = login.GetUserInfo(r)
 104	return templinfo
 105}
 106
 107func homepage(w http.ResponseWriter, r *http.Request) {
 108	templinfo := getInfo(r)
 109	u := login.GetUserInfo(r)
 110	var honks []*Honk
 111	if u != nil {
 112		if r.URL.Path == "/atme" {
 113			honks = gethonksforme(u.UserID)
 114		} else {
 115			honks = gethonksforuser(u.UserID)
 116			honks = osmosis(honks, u.UserID)
 117		}
 118		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 119	} else {
 120		honks = getpublichonks()
 121	}
 122
 123	var modtime time.Time
 124	if len(honks) > 0 {
 125		modtime = honks[0].Date
 126	}
 127	debug := false
 128	getconfig("debug", &debug)
 129	imh := r.Header.Get("If-Modified-Since")
 130	if !debug && imh != "" && !modtime.IsZero() {
 131		ifmod, err := time.Parse(http.TimeFormat, imh)
 132		if err == nil && !modtime.After(ifmod) {
 133			w.WriteHeader(http.StatusNotModified)
 134			return
 135		}
 136	}
 137	reverbolate(honks)
 138
 139	msg := "Things happen."
 140	getconfig("servermsg", &msg)
 141	templinfo["Honks"] = honks
 142	templinfo["ShowRSS"] = true
 143	templinfo["ServerMessage"] = msg
 144	if u == nil {
 145		w.Header().Set("Cache-Control", "max-age=60")
 146	} else {
 147		w.Header().Set("Cache-Control", "max-age=0")
 148	}
 149	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 150	err := readviews.Execute(w, "honkpage.html", templinfo)
 151	if err != nil {
 152		log.Print(err)
 153	}
 154}
 155
 156func showrss(w http.ResponseWriter, r *http.Request) {
 157	name := mux.Vars(r)["name"]
 158
 159	var honks []*Honk
 160	if name != "" {
 161		honks = gethonksbyuser(name, false)
 162	} else {
 163		honks = getpublichonks()
 164	}
 165	reverbolate(honks)
 166
 167	home := fmt.Sprintf("https://%s/", serverName)
 168	base := home
 169	if name != "" {
 170		home += "u/" + name
 171		name += " "
 172	}
 173	feed := rss.Feed{
 174		Title:       name + "honk",
 175		Link:        home,
 176		Description: name + "honk rss",
 177		Image: &rss.Image{
 178			URL:   base + "icon.png",
 179			Title: name + "honk rss",
 180			Link:  home,
 181		},
 182	}
 183	var modtime time.Time
 184	for _, honk := range honks {
 185		desc := string(honk.HTML)
 186		for _, d := range honk.Donks {
 187			desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 188				d.URL, html.EscapeString(d.Name))
 189		}
 190
 191		feed.Items = append(feed.Items, &rss.Item{
 192			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 193			Description: rss.CData{desc},
 194			Link:        honk.URL,
 195			PubDate:     honk.Date.Format(time.RFC1123),
 196			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 197		})
 198		if honk.Date.After(modtime) {
 199			modtime = honk.Date
 200		}
 201	}
 202	w.Header().Set("Cache-Control", "max-age=300")
 203	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 204
 205	err := feed.Write(w)
 206	if err != nil {
 207		log.Printf("error writing rss: %s", err)
 208	}
 209}
 210
 211func butwhatabout(name string) (*WhatAbout, error) {
 212	row := stmtWhatAbout.QueryRow(name)
 213	var user WhatAbout
 214	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key)
 215	user.URL = fmt.Sprintf("https://%s/u/%s", serverName, user.Name)
 216	return &user, err
 217}
 218
 219func crappola(j map[string]interface{}) bool {
 220	t, _ := jsongetstring(j, "type")
 221	a, _ := jsongetstring(j, "actor")
 222	o, _ := jsongetstring(j, "object")
 223	if t == "Delete" && a == o {
 224		log.Printf("crappola from %s", a)
 225		return true
 226	}
 227	return false
 228}
 229
 230func ping(user *WhatAbout, who string) {
 231	box, err := getboxes(who)
 232	if err != nil {
 233		log.Printf("no inbox for ping: %s", err)
 234		return
 235	}
 236	j := NewJunk()
 237	j["@context"] = itiswhatitis
 238	j["type"] = "Ping"
 239	j["id"] = user.URL + "/ping/" + xfiltrate()
 240	j["actor"] = user.URL
 241	j["to"] = who
 242	keyname, key := ziggy(user.Name)
 243	err = PostJunk(keyname, key, box.In, j)
 244	if err != nil {
 245		log.Printf("can't send ping: %s", err)
 246		return
 247	}
 248	log.Printf("sent ping to %s: %s", who, j["id"])
 249}
 250
 251func pong(user *WhatAbout, who string, obj string) {
 252	box, err := getboxes(who)
 253	if err != nil {
 254		log.Printf("no inbox for pong %s : %s", who, err)
 255		return
 256	}
 257	j := NewJunk()
 258	j["@context"] = itiswhatitis
 259	j["type"] = "Pong"
 260	j["id"] = user.URL + "/pong/" + xfiltrate()
 261	j["actor"] = user.URL
 262	j["to"] = who
 263	j["object"] = obj
 264	keyname, key := ziggy(user.Name)
 265	err = PostJunk(keyname, key, box.In, j)
 266	if err != nil {
 267		log.Printf("can't send pong: %s", err)
 268		return
 269	}
 270}
 271
 272func inbox(w http.ResponseWriter, r *http.Request) {
 273	name := mux.Vars(r)["name"]
 274	user, err := butwhatabout(name)
 275	if err != nil {
 276		http.NotFound(w, r)
 277		return
 278	}
 279	var buf bytes.Buffer
 280	io.Copy(&buf, r.Body)
 281	payload := buf.Bytes()
 282	j, err := ReadJunk(bytes.NewReader(payload))
 283	if err != nil {
 284		log.Printf("bad payload: %s", err)
 285		io.WriteString(os.Stdout, "bad payload\n")
 286		os.Stdout.Write(payload)
 287		io.WriteString(os.Stdout, "\n")
 288		return
 289	}
 290	if crappola(j) {
 291		return
 292	}
 293	keyname, err := zag(r, payload)
 294	if err != nil {
 295		log.Printf("inbox message failed signature: %s", err)
 296		if keyname != "" {
 297			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 298		}
 299		if err != nil {
 300			return
 301		}
 302	}
 303	what, _ := jsongetstring(j, "type")
 304	if what == "Like" {
 305		return
 306	}
 307	who, _ := jsongetstring(j, "actor")
 308	origin := keymatch(keyname, who)
 309	if origin == "" {
 310		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 311		return
 312	}
 313	objid, _ := jsongetstring(j, "id")
 314	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 315		log.Printf("ignoring thumb sucker %s", who)
 316		return
 317	}
 318	switch what {
 319	case "Ping":
 320		obj, _ := jsongetstring(j, "id")
 321		log.Printf("ping from %s: %s", who, obj)
 322		pong(user, who, obj)
 323	case "Pong":
 324		obj, _ := jsongetstring(j, "object")
 325		log.Printf("pong from %s: %s", who, obj)
 326	case "Follow":
 327		obj, _ := jsongetstring(j, "object")
 328		if obj == user.URL {
 329			log.Printf("updating honker follow: %s", who)
 330			rubadubdub(user, j)
 331		} else {
 332			log.Printf("can't follow %s", obj)
 333		}
 334	case "Accept":
 335		log.Printf("updating honker accept: %s", who)
 336		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 337		if err != nil {
 338			log.Printf("error updating honker: %s", err)
 339			return
 340		}
 341	case "Undo":
 342		obj, ok := jsongetmap(j, "object")
 343		if !ok {
 344			log.Printf("unknown undo no object")
 345		} else {
 346			what, _ := jsongetstring(obj, "type")
 347			switch what {
 348			case "Follow":
 349				log.Printf("updating honker undo: %s", who)
 350				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 351				if err != nil {
 352					log.Printf("error updating honker: %s", err)
 353					return
 354				}
 355			case "Like":
 356			case "Announce":
 357			default:
 358				log.Printf("unknown undo: %s", what)
 359			}
 360		}
 361	default:
 362		go consumeactivity(user, j, origin)
 363	}
 364}
 365
 366func outbox(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	honks := gethonksbyuser(name, false)
 374
 375	var jonks []map[string]interface{}
 376	for _, h := range honks {
 377		j, _ := jonkjonk(user, h)
 378		jonks = append(jonks, j)
 379	}
 380
 381	j := NewJunk()
 382	j["@context"] = itiswhatitis
 383	j["id"] = user.URL + "/outbox"
 384	j["type"] = "OrderedCollection"
 385	j["totalItems"] = len(jonks)
 386	j["orderedItems"] = jonks
 387
 388	w.Header().Set("Cache-Control", "max-age=60")
 389	w.Header().Set("Content-Type", theonetruename)
 390	WriteJunk(w, j)
 391}
 392
 393func emptiness(w http.ResponseWriter, r *http.Request) {
 394	name := mux.Vars(r)["name"]
 395	user, err := butwhatabout(name)
 396	if err != nil {
 397		http.NotFound(w, r)
 398		return
 399	}
 400	colname := "/followers"
 401	if strings.HasSuffix(r.URL.Path, "/following") {
 402		colname = "/following"
 403	}
 404	j := NewJunk()
 405	j["@context"] = itiswhatitis
 406	j["id"] = user.URL + colname
 407	j["type"] = "OrderedCollection"
 408	j["totalItems"] = 0
 409	j["orderedItems"] = []interface{}{}
 410
 411	w.Header().Set("Cache-Control", "max-age=60")
 412	w.Header().Set("Content-Type", theonetruename)
 413	WriteJunk(w, j)
 414}
 415
 416func showuser(w http.ResponseWriter, r *http.Request) {
 417	name := mux.Vars(r)["name"]
 418	user, err := butwhatabout(name)
 419	if err != nil {
 420		http.NotFound(w, r)
 421		return
 422	}
 423	if friendorfoe(r.Header.Get("Accept")) {
 424		j := asjonker(user)
 425		w.Header().Set("Cache-Control", "max-age=600")
 426		w.Header().Set("Content-Type", theonetruename)
 427		WriteJunk(w, j)
 428		return
 429	}
 430	u := login.GetUserInfo(r)
 431	honks := gethonksbyuser(name, u != nil && u.Username == name)
 432	honkpage(w, r, u, user, honks, "")
 433}
 434
 435func showhonker(w http.ResponseWriter, r *http.Request) {
 436	name := mux.Vars(r)["name"]
 437	u := login.GetUserInfo(r)
 438	honks := gethonksbyhonker(u.UserID, name)
 439	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 440}
 441
 442func showcombo(w http.ResponseWriter, r *http.Request) {
 443	name := mux.Vars(r)["name"]
 444	u := login.GetUserInfo(r)
 445	honks := gethonksbycombo(u.UserID, name)
 446	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 447}
 448func showconvoy(w http.ResponseWriter, r *http.Request) {
 449	c := r.FormValue("c")
 450	var userid int64 = -1
 451	u := login.GetUserInfo(r)
 452	if u != nil {
 453		userid = u.UserID
 454	}
 455	honks := gethonksbyconvoy(userid, c)
 456	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 457}
 458
 459func showhonk(w http.ResponseWriter, r *http.Request) {
 460	name := mux.Vars(r)["name"]
 461	user, err := butwhatabout(name)
 462	if err != nil {
 463		http.NotFound(w, r)
 464		return
 465	}
 466	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 467	h := getxonk(user.ID, xid)
 468	if h == nil || !h.Public {
 469		http.NotFound(w, r)
 470		return
 471	}
 472	if friendorfoe(r.Header.Get("Accept")) {
 473		donksforhonks([]*Honk{h})
 474		_, j := jonkjonk(user, h)
 475		j["@context"] = itiswhatitis
 476		w.Header().Set("Cache-Control", "max-age=3600")
 477		w.Header().Set("Content-Type", theonetruename)
 478		WriteJunk(w, j)
 479		return
 480	}
 481	honks := gethonksbyconvoy(-1, h.Convoy)
 482	u := login.GetUserInfo(r)
 483	honkpage(w, r, u, nil, honks, "one honk maybe more")
 484}
 485
 486func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 487	honks []*Honk, infomsg string) {
 488	reverbolate(honks)
 489	templinfo := getInfo(r)
 490	if u != nil {
 491		honks = osmosis(honks, u.UserID)
 492		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 493	}
 494	if u == nil {
 495		w.Header().Set("Cache-Control", "max-age=60")
 496	}
 497	if user != nil {
 498		filt := htfilter.New()
 499		templinfo["Name"] = user.Name
 500		whatabout := user.About
 501		whatabout = obfusbreak(user.About)
 502		templinfo["WhatAbout"], _ = filt.String(whatabout)
 503	}
 504	templinfo["Honks"] = honks
 505	templinfo["ServerMessage"] = infomsg
 506	err := readviews.Execute(w, "honkpage.html", templinfo)
 507	if err != nil {
 508		log.Print(err)
 509	}
 510}
 511
 512func saveuser(w http.ResponseWriter, r *http.Request) {
 513	whatabout := r.FormValue("whatabout")
 514	u := login.GetUserInfo(r)
 515	db := opendatabase()
 516	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 517	if err != nil {
 518		log.Printf("error bouting what: %s", err)
 519	}
 520
 521	http.Redirect(w, r, "/account", http.StatusSeeOther)
 522}
 523
 524func gethonkers(userid int64) []*Honker {
 525	rows, err := stmtHonkers.Query(userid)
 526	if err != nil {
 527		log.Printf("error querying honkers: %s", err)
 528		return nil
 529	}
 530	defer rows.Close()
 531	var honkers []*Honker
 532	for rows.Next() {
 533		var f Honker
 534		var combos string
 535		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 536		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 537		if err != nil {
 538			log.Printf("error scanning honker: %s", err)
 539			return nil
 540		}
 541		honkers = append(honkers, &f)
 542	}
 543	return honkers
 544}
 545
 546func getdubs(userid int64) []*Honker {
 547	rows, err := stmtDubbers.Query(userid)
 548	if err != nil {
 549		log.Printf("error querying dubs: %s", err)
 550		return nil
 551	}
 552	defer rows.Close()
 553	var honkers []*Honker
 554	for rows.Next() {
 555		var f Honker
 556		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 557		if err != nil {
 558			log.Printf("error scanning honker: %s", err)
 559			return nil
 560		}
 561		honkers = append(honkers, &f)
 562	}
 563	return honkers
 564}
 565
 566func allusers() []login.UserInfo {
 567	var users []login.UserInfo
 568	rows, _ := opendatabase().Query("select userid, username from users")
 569	defer rows.Close()
 570	for rows.Next() {
 571		var u login.UserInfo
 572		rows.Scan(&u.UserID, &u.Username)
 573		users = append(users, u)
 574	}
 575	return users
 576}
 577
 578func getxonk(userid int64, xid string) *Honk {
 579	h := new(Honk)
 580	var dt, aud string
 581	row := stmtOneXonk.QueryRow(userid, xid)
 582	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 583		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 584	if err != nil {
 585		if err != sql.ErrNoRows {
 586			log.Printf("error scanning xonk: %s", err)
 587		}
 588		return nil
 589	}
 590	h.Date, _ = time.Parse(dbtimeformat, dt)
 591	h.Audience = strings.Split(aud, " ")
 592	h.Public = !keepitquiet(h.Audience)
 593	return h
 594}
 595
 596func getpublichonks() []*Honk {
 597	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 598	rows, err := stmtPublicHonks.Query(dt)
 599	return getsomehonks(rows, err)
 600}
 601func gethonksbyuser(name string, includeprivate bool) []*Honk {
 602	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 603	whofore := 2
 604	if includeprivate {
 605		whofore = 3
 606	}
 607	rows, err := stmtUserHonks.Query(whofore, name, dt)
 608	return getsomehonks(rows, err)
 609}
 610func gethonksforuser(userid int64) []*Honk {
 611	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 612	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 613	return getsomehonks(rows, err)
 614}
 615func gethonksforme(userid int64) []*Honk {
 616	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 617	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 618	return getsomehonks(rows, err)
 619}
 620func gethonksbyhonker(userid int64, honker string) []*Honk {
 621	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 622	return getsomehonks(rows, err)
 623}
 624func gethonksbycombo(userid int64, combo string) []*Honk {
 625	combo = "% " + combo + " %"
 626	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 627	return getsomehonks(rows, err)
 628}
 629func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 630	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 631	honks := getsomehonks(rows, err)
 632	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 633		honks[i], honks[j] = honks[j], honks[i]
 634	}
 635	return honks
 636}
 637
 638func getsomehonks(rows *sql.Rows, err error) []*Honk {
 639	if err != nil {
 640		log.Printf("error querying honks: %s", err)
 641		return nil
 642	}
 643	defer rows.Close()
 644	var honks []*Honk
 645	for rows.Next() {
 646		var h Honk
 647		var dt, aud string
 648		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 649			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 650		if err != nil {
 651			log.Printf("error scanning honks: %s", err)
 652			return nil
 653		}
 654		h.Date, _ = time.Parse(dbtimeformat, dt)
 655		h.Audience = strings.Split(aud, " ")
 656		h.Public = !keepitquiet(h.Audience)
 657		honks = append(honks, &h)
 658	}
 659	rows.Close()
 660	donksforhonks(honks)
 661	return honks
 662}
 663
 664func donksforhonks(honks []*Honk) {
 665	db := opendatabase()
 666	var ids []string
 667	hmap := make(map[int64]*Honk)
 668	for _, h := range honks {
 669		ids = append(ids, fmt.Sprintf("%d", h.ID))
 670		hmap[h.ID] = h
 671	}
 672	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, ","))
 673	rows, err := db.Query(q)
 674	if err != nil {
 675		log.Printf("error querying donks: %s", err)
 676		return
 677	}
 678	defer rows.Close()
 679	for rows.Next() {
 680		var hid int64
 681		var d Donk
 682		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 683		if err != nil {
 684			log.Printf("error scanning donk: %s", err)
 685			continue
 686		}
 687		h := hmap[hid]
 688		h.Donks = append(h.Donks, &d)
 689	}
 690}
 691
 692func savebonk(w http.ResponseWriter, r *http.Request) {
 693	xid := r.FormValue("xid")
 694	userinfo := login.GetUserInfo(r)
 695	user, _ := butwhatabout(userinfo.Username)
 696
 697	log.Printf("bonking %s", xid)
 698
 699	xonk := getxonk(userinfo.UserID, xid)
 700	if xonk == nil {
 701		return
 702	}
 703	if !xonk.Public {
 704		return
 705	}
 706	donksforhonks([]*Honk{xonk})
 707
 708	dt := time.Now().UTC()
 709	bonk := Honk{
 710		UserID:   userinfo.UserID,
 711		Username: userinfo.Username,
 712		What:     "bonk",
 713		Honker:   user.URL,
 714		XID:      xonk.XID,
 715		Date:     dt,
 716		Donks:    xonk.Donks,
 717		Audience: []string{thewholeworld},
 718		Public:   true,
 719	}
 720
 721	oonker := xonk.Oonker
 722	if oonker == "" {
 723		oonker = xonk.Honker
 724	}
 725	aud := strings.Join(bonk.Audience, " ")
 726	whofore := 2
 727	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 728		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 729		xonk.Precis, oonker)
 730	if err != nil {
 731		log.Printf("error saving bonk: %s", err)
 732		return
 733	}
 734	bonk.ID, _ = res.LastInsertId()
 735	for _, d := range bonk.Donks {
 736		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 737		if err != nil {
 738			log.Printf("err saving donk: %s", err)
 739			return
 740		}
 741	}
 742
 743	go honkworldwide(user, &bonk)
 744}
 745
 746func zonkit(w http.ResponseWriter, r *http.Request) {
 747	wherefore := r.FormValue("wherefore")
 748	var what string
 749	switch wherefore {
 750	case "this honk":
 751		what = r.FormValue("honk")
 752		wherefore = "zonk"
 753	case "this honker":
 754		what = r.FormValue("honker")
 755		wherefore = "zonker"
 756	case "this convoy":
 757		what = r.FormValue("convoy")
 758		wherefore = "zonvoy"
 759	}
 760	if what == "" {
 761		return
 762	}
 763
 764	log.Printf("zonking %s %s", wherefore, what)
 765	userinfo := login.GetUserInfo(r)
 766	if wherefore == "zonk" {
 767		xonk := getxonk(userinfo.UserID, what)
 768		if xonk != nil {
 769			stmtZonkDonks.Exec(xonk.ID)
 770			stmtZonkIt.Exec(userinfo.UserID, what)
 771			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 772				zonk := Honk{
 773					What:     "zonk",
 774					XID:      xonk.XID,
 775					Date:     time.Now().UTC(),
 776					Audience: oneofakind(xonk.Audience),
 777				}
 778
 779				user, _ := butwhatabout(userinfo.Username)
 780				log.Printf("announcing deleted honk: %s", what)
 781				go honkworldwide(user, &zonk)
 782			}
 783		}
 784	} else {
 785		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 786		if err != nil {
 787			log.Printf("error saving zonker: %s", err)
 788			return
 789		}
 790	}
 791}
 792
 793func savehonk(w http.ResponseWriter, r *http.Request) {
 794	rid := r.FormValue("rid")
 795	noise := r.FormValue("noise")
 796
 797	userinfo := login.GetUserInfo(r)
 798	user, _ := butwhatabout(userinfo.Username)
 799
 800	dt := time.Now().UTC()
 801	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 802	what := "honk"
 803	if rid != "" {
 804		what = "tonk"
 805	}
 806	honk := Honk{
 807		UserID:   userinfo.UserID,
 808		Username: userinfo.Username,
 809		What:     "honk",
 810		Honker:   user.URL,
 811		XID:      xid,
 812		Date:     dt,
 813	}
 814	if strings.HasPrefix(noise, "DZ:") {
 815		idx := strings.Index(noise, "\n")
 816		if idx == -1 {
 817			honk.Precis = noise
 818			noise = ""
 819		} else {
 820			honk.Precis = noise[:idx]
 821			noise = noise[idx+1:]
 822		}
 823	}
 824	noise = strings.TrimSpace(noise)
 825	honk.Precis = strings.TrimSpace(honk.Precis)
 826
 827	var convoy string
 828	if rid != "" {
 829		xonk := getxonk(userinfo.UserID, rid)
 830		if xonk != nil {
 831			if xonk.Public {
 832				honk.Audience = append(honk.Audience, xonk.Audience...)
 833			}
 834			convoy = xonk.Convoy
 835		} else {
 836			xonkaud, c := whosthere(rid)
 837			honk.Audience = append(honk.Audience, xonkaud...)
 838			convoy = c
 839		}
 840		for i, a := range honk.Audience {
 841			if a == thewholeworld {
 842				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 843				break
 844			}
 845		}
 846		honk.RID = rid
 847	} else {
 848		honk.Audience = []string{thewholeworld}
 849	}
 850	if noise != "" && noise[0] == '@' {
 851		honk.Audience = append(grapevine(noise), honk.Audience...)
 852	} else {
 853		honk.Audience = append(honk.Audience, grapevine(noise)...)
 854	}
 855	if convoy == "" {
 856		convoy = "data:,electrichonkytonk-" + xfiltrate()
 857	}
 858	butnottooloud(honk.Audience)
 859	honk.Audience = oneofakind(honk.Audience)
 860	if len(honk.Audience) == 0 {
 861		log.Printf("honk to nowhere")
 862		return
 863	}
 864	honk.Public = !keepitquiet(honk.Audience)
 865	noise = obfusbreak(noise)
 866	honk.Noise = noise
 867	honk.Convoy = convoy
 868
 869	file, filehdr, err := r.FormFile("donk")
 870	if err == nil {
 871		var buf bytes.Buffer
 872		io.Copy(&buf, file)
 873		file.Close()
 874		data := buf.Bytes()
 875		xid := xfiltrate()
 876		var media, name string
 877		img, err := image.Vacuum(&buf)
 878		if err == nil {
 879			data = img.Data
 880			format := img.Format
 881			media = "image/" + format
 882			if format == "jpeg" {
 883				format = "jpg"
 884			}
 885			name = xid + "." + format
 886			xid = name
 887		} else {
 888			maxsize := 100000
 889			if len(data) > maxsize {
 890				log.Printf("bad image: %s too much text: %d", err, len(data))
 891				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 892				return
 893			}
 894			for i := 0; i < len(data); i++ {
 895				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 896					log.Printf("bad image: %s not text: %d", err, data[i])
 897					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 898					return
 899				}
 900			}
 901			media = "text/plain"
 902			name = filehdr.Filename
 903			if name == "" {
 904				name = xid + ".txt"
 905			}
 906			xid += ".txt"
 907		}
 908		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 909		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 910		if err != nil {
 911			log.Printf("unable to save image: %s", err)
 912			return
 913		}
 914		var d Donk
 915		d.FileID, _ = res.LastInsertId()
 916		d.XID = name
 917		d.Name = name
 918		d.Media = media
 919		d.URL = url
 920		honk.Donks = append(honk.Donks, &d)
 921	}
 922	herd := herdofemus(honk.Noise)
 923	for _, e := range herd {
 924		donk := savedonk(e.ID, e.Name, "image/png", true)
 925		if donk != nil {
 926			donk.Name = e.Name
 927			honk.Donks = append(honk.Donks, donk)
 928		}
 929	}
 930	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 931
 932	aud := strings.Join(honk.Audience, " ")
 933	whofore := 2
 934	if !honk.Public {
 935		whofore = 3
 936	}
 937	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 938		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 939	if err != nil {
 940		log.Printf("error saving honk: %s", err)
 941		return
 942	}
 943	honk.ID, _ = res.LastInsertId()
 944	for _, d := range honk.Donks {
 945		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 946		if err != nil {
 947			log.Printf("err saving donk: %s", err)
 948			return
 949		}
 950	}
 951
 952	go honkworldwide(user, &honk)
 953
 954	http.Redirect(w, r, "/", http.StatusSeeOther)
 955}
 956
 957func showhonkers(w http.ResponseWriter, r *http.Request) {
 958	userinfo := login.GetUserInfo(r)
 959	templinfo := getInfo(r)
 960	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 961	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 962	err := readviews.Execute(w, "honkers.html", templinfo)
 963	if err != nil {
 964		log.Print(err)
 965	}
 966}
 967
 968func showcombos(w http.ResponseWriter, r *http.Request) {
 969	userinfo := login.GetUserInfo(r)
 970	templinfo := getInfo(r)
 971	honkers := gethonkers(userinfo.UserID)
 972	var combos []string
 973	for _, h := range honkers {
 974		combos = append(combos, h.Combos...)
 975	}
 976	for i, c := range combos {
 977		if c == "-" {
 978			combos[i] = ""
 979		}
 980	}
 981	combos = oneofakind(combos)
 982	sort.Strings(combos)
 983	templinfo["Combos"] = combos
 984	err := readviews.Execute(w, "combos.html", templinfo)
 985	if err != nil {
 986		log.Print(err)
 987	}
 988}
 989
 990func savehonker(w http.ResponseWriter, r *http.Request) {
 991	u := login.GetUserInfo(r)
 992	name := r.FormValue("name")
 993	url := r.FormValue("url")
 994	peep := r.FormValue("peep")
 995	combos := r.FormValue("combos")
 996	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 997
 998	if honkerid > 0 {
 999		goodbye := r.FormValue("goodbye")
1000		if goodbye == "F" {
1001			db := opendatabase()
1002			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1003				honkerid, u.UserID)
1004			var xid string
1005			err := row.Scan(&xid)
1006			if err != nil {
1007				log.Printf("can't get honker xid: %s", err)
1008				return
1009			}
1010			log.Printf("unsubscribing from %s", xid)
1011			user, _ := butwhatabout(u.Username)
1012			err = itakeitallback(user, xid)
1013			if err != nil {
1014				log.Printf("can't take it back: %s", err)
1015			} else {
1016				_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1017				if err != nil {
1018					log.Printf("error updating honker: %s", err)
1019					return
1020				}
1021			}
1022
1023			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1024			return
1025		}
1026		combos = " " + strings.TrimSpace(combos) + " "
1027		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1028		if err != nil {
1029			log.Printf("update honker err: %s", err)
1030			return
1031		}
1032		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1033	}
1034
1035	flavor := "presub"
1036	if peep == "peep" {
1037		flavor = "peep"
1038	}
1039	if url == "" {
1040		return
1041	}
1042	if url[0] == '@' {
1043		url = gofish(url)
1044	}
1045	if url == "" {
1046		return
1047	}
1048	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1049	if err != nil {
1050		log.Print(err)
1051		return
1052	}
1053	if flavor == "presub" {
1054		user, _ := butwhatabout(u.Username)
1055		go subsub(user, url)
1056	}
1057	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1058}
1059
1060type Zonker struct {
1061	ID        int64
1062	Name      string
1063	Wherefore string
1064}
1065
1066func zonkzone(w http.ResponseWriter, r *http.Request) {
1067	db := opendatabase()
1068	userinfo := login.GetUserInfo(r)
1069	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1070	if err != nil {
1071		log.Printf("err: %s", err)
1072		return
1073	}
1074	var zonkers []Zonker
1075	for rows.Next() {
1076		var z Zonker
1077		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1078		zonkers = append(zonkers, z)
1079	}
1080	templinfo := getInfo(r)
1081	templinfo["Zonkers"] = zonkers
1082	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1083	err = readviews.Execute(w, "zonkers.html", templinfo)
1084	if err != nil {
1085		log.Print(err)
1086	}
1087}
1088
1089func zonkzonk(w http.ResponseWriter, r *http.Request) {
1090	userinfo := login.GetUserInfo(r)
1091	itsok := r.FormValue("itsok")
1092	if itsok == "iforgiveyou" {
1093		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1094		db := opendatabase()
1095		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1096			userinfo.UserID, zonkerid)
1097		bitethethumbs()
1098		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1099		return
1100	}
1101	wherefore := r.FormValue("wherefore")
1102	name := r.FormValue("name")
1103	if name == "" {
1104		return
1105	}
1106	switch wherefore {
1107	case "zonker":
1108	case "zurl":
1109	case "zonvoy":
1110	case "zword":
1111	default:
1112		return
1113	}
1114	db := opendatabase()
1115	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1116		userinfo.UserID, name, wherefore)
1117	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1118		bitethethumbs()
1119	}
1120
1121	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1122}
1123
1124func accountpage(w http.ResponseWriter, r *http.Request) {
1125	u := login.GetUserInfo(r)
1126	user, _ := butwhatabout(u.Username)
1127	templinfo := getInfo(r)
1128	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1129	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1130	templinfo["WhatAbout"] = user.About
1131	err := readviews.Execute(w, "account.html", templinfo)
1132	if err != nil {
1133		log.Print(err)
1134	}
1135}
1136
1137func dochpass(w http.ResponseWriter, r *http.Request) {
1138	err := login.ChangePassword(w, r)
1139	if err != nil {
1140		log.Printf("error changing password: %s", err)
1141	}
1142	http.Redirect(w, r, "/account", http.StatusSeeOther)
1143}
1144
1145func fingerlicker(w http.ResponseWriter, r *http.Request) {
1146	orig := r.FormValue("resource")
1147
1148	log.Printf("finger lick: %s", orig)
1149
1150	if strings.HasPrefix(orig, "acct:") {
1151		orig = orig[5:]
1152	}
1153
1154	name := orig
1155	idx := strings.LastIndexByte(name, '/')
1156	if idx != -1 {
1157		name = name[idx+1:]
1158		if "https://"+serverName+"/u/"+name != orig {
1159			log.Printf("foreign request rejected")
1160			name = ""
1161		}
1162	} else {
1163		idx = strings.IndexByte(name, '@')
1164		if idx != -1 {
1165			name = name[:idx]
1166			if name+"@"+serverName != orig {
1167				log.Printf("foreign request rejected")
1168				name = ""
1169			}
1170		}
1171	}
1172	user, err := butwhatabout(name)
1173	if err != nil {
1174		http.NotFound(w, r)
1175		return
1176	}
1177
1178	j := NewJunk()
1179	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1180	j["aliases"] = []string{user.URL}
1181	var links []map[string]interface{}
1182	l := NewJunk()
1183	l["rel"] = "self"
1184	l["type"] = `application/activity+json`
1185	l["href"] = user.URL
1186	links = append(links, l)
1187	j["links"] = links
1188
1189	w.Header().Set("Cache-Control", "max-age=3600")
1190	w.Header().Set("Content-Type", "application/jrd+json")
1191	WriteJunk(w, j)
1192}
1193
1194func somedays() string {
1195	secs := 432000 + notrand.Int63n(432000)
1196	return fmt.Sprintf("%d", secs)
1197}
1198
1199func avatate(w http.ResponseWriter, r *http.Request) {
1200	n := r.FormValue("a")
1201	a := avatar(n)
1202	w.Header().Set("Cache-Control", "max-age="+somedays())
1203	w.Write(a)
1204}
1205
1206func servecss(w http.ResponseWriter, r *http.Request) {
1207	w.Header().Set("Cache-Control", "max-age=7776000")
1208	http.ServeFile(w, r, "views"+r.URL.Path)
1209}
1210func servehtml(w http.ResponseWriter, r *http.Request) {
1211	templinfo := getInfo(r)
1212	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1213	if err != nil {
1214		log.Print(err)
1215	}
1216}
1217func serveemu(w http.ResponseWriter, r *http.Request) {
1218	xid := mux.Vars(r)["xid"]
1219	w.Header().Set("Cache-Control", "max-age="+somedays())
1220	http.ServeFile(w, r, "emus/"+xid)
1221}
1222func servememe(w http.ResponseWriter, r *http.Request) {
1223	xid := mux.Vars(r)["xid"]
1224	w.Header().Set("Cache-Control", "max-age="+somedays())
1225	http.ServeFile(w, r, "memes/"+xid)
1226}
1227
1228func servefile(w http.ResponseWriter, r *http.Request) {
1229	xid := mux.Vars(r)["xid"]
1230	row := stmtFileData.QueryRow(xid)
1231	var media string
1232	var data []byte
1233	err := row.Scan(&media, &data)
1234	if err != nil {
1235		log.Printf("error loading file: %s", err)
1236		http.NotFound(w, r)
1237		return
1238	}
1239	w.Header().Set("Content-Type", media)
1240	w.Header().Set("X-Content-Type-Options", "nosniff")
1241	w.Header().Set("Cache-Control", "max-age="+somedays())
1242	w.Write(data)
1243}
1244
1245func serve() {
1246	db := opendatabase()
1247	login.Init(db)
1248
1249	listener, err := openListener()
1250	if err != nil {
1251		log.Fatal(err)
1252	}
1253	go redeliverator()
1254
1255	debug := false
1256	getconfig("debug", &debug)
1257	readviews = templates.Load(debug,
1258		"views/honkpage.html",
1259		"views/honkers.html",
1260		"views/zonkers.html",
1261		"views/combos.html",
1262		"views/honkform.html",
1263		"views/honk.html",
1264		"views/account.html",
1265		"views/login.html",
1266		"views/header.html",
1267	)
1268	if !debug {
1269		s := "views/style.css"
1270		savedstyleparams[s] = getstyleparam(s)
1271		s = "views/local.css"
1272		savedstyleparams[s] = getstyleparam(s)
1273	}
1274
1275	bitethethumbs()
1276
1277	mux := mux.NewRouter()
1278	mux.Use(login.Checker)
1279
1280	posters := mux.Methods("POST").Subrouter()
1281	getters := mux.Methods("GET").Subrouter()
1282
1283	getters.HandleFunc("/", homepage)
1284	getters.HandleFunc("/rss", showrss)
1285	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1286	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1287	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1288	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1289	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1290	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1291	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1292	getters.HandleFunc("/a", avatate)
1293	getters.HandleFunc("/t", showconvoy)
1294	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1295	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1296	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1297	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1298
1299	getters.HandleFunc("/style.css", servecss)
1300	getters.HandleFunc("/local.css", servecss)
1301	getters.HandleFunc("/login", servehtml)
1302	posters.HandleFunc("/dologin", login.LoginFunc)
1303	getters.HandleFunc("/logout", login.LogoutFunc)
1304
1305	loggedin := mux.NewRoute().Subrouter()
1306	loggedin.Use(login.Required)
1307	loggedin.HandleFunc("/account", accountpage)
1308	loggedin.HandleFunc("/chpass", dochpass)
1309	loggedin.HandleFunc("/atme", homepage)
1310	loggedin.HandleFunc("/zonkzone", zonkzone)
1311	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1312	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1313	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1314	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1315	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1316	loggedin.HandleFunc("/honkers", showhonkers)
1317	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1318	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1319	loggedin.HandleFunc("/c", showcombos)
1320	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1321
1322	err = http.Serve(listener, mux)
1323	if err != nil {
1324		log.Fatal(err)
1325	}
1326}
1327
1328func cleanupdb() {
1329	db := opendatabase()
1330	rows, _ := db.Query("select userid, name from zonkers where wherefore = 'zonvoy'")
1331	deadthreads := make(map[int64][]string)
1332	for rows.Next() {
1333		var userid int64
1334		var name string
1335		rows.Scan(&userid, &name)
1336		deadthreads[userid] = append(deadthreads[userid], name)
1337	}
1338	rows.Close()
1339	for userid, threads := range deadthreads {
1340		for _, t := range threads {
1341			doordie(db, "delete from donks where honkid in (select honkid from honks where userid = ? and convoy = ?)", userid, t)
1342			doordie(db, "delete from honks where userid = ? and convoy = ?", userid, t)
1343		}
1344	}
1345	expdate := time.Now().UTC().Add(-30 * 24 * time.Hour).Format(dbtimeformat)
1346	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)
1347	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)
1348	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1349}
1350
1351var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1352var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1353var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1354var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1355var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1356var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1357var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1358var stmtGetXonker, stmtSaveXonker *sql.Stmt
1359
1360func preparetodie(db *sql.DB, s string) *sql.Stmt {
1361	stmt, err := db.Prepare(s)
1362	if err != nil {
1363		log.Fatalf("error %s: %s", err, s)
1364	}
1365	return stmt
1366}
1367
1368func prepareStatements(db *sql.DB) {
1369	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")
1370	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1371	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1372	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1373	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1374	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1375
1376	selecthonks := "select honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore from honks join users on honks.userid = users.userid "
1377	limit := " order by honkid desc limit 250"
1378	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1379	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1380	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1381	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1382	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)
1383	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1384	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1385	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1386	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1387
1388	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1389	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1390	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1391	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1392	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1393	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1394	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1395	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1396	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1397	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1398	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1399	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1400	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1401	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1402	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1403	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1404	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1405	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1406}
1407
1408func ElaborateUnitTests() {
1409}
1410
1411func main() {
1412	cmd := "run"
1413	if len(os.Args) > 1 {
1414		cmd = os.Args[1]
1415	}
1416	switch cmd {
1417	case "init":
1418		initdb()
1419	case "upgrade":
1420		upgradedb()
1421	}
1422	db := opendatabase()
1423	dbversion := 0
1424	getconfig("dbversion", &dbversion)
1425	if dbversion != myVersion {
1426		log.Fatal("incorrect database version. run upgrade.")
1427	}
1428	getconfig("servername", &serverName)
1429	prepareStatements(db)
1430	switch cmd {
1431	case "adduser":
1432		adduser()
1433	case "cleanup":
1434		cleanupdb()
1435	case "ping":
1436		if len(os.Args) < 4 {
1437			fmt.Printf("usage: honk ping from to\n")
1438			return
1439		}
1440		name := os.Args[2]
1441		targ := os.Args[3]
1442		user, err := butwhatabout(name)
1443		if err != nil {
1444			log.Printf("unknown user")
1445			return
1446		}
1447		ping(user, targ)
1448	case "peep":
1449		peeppeep()
1450	case "run":
1451		serve()
1452	case "test":
1453		ElaborateUnitTests()
1454	default:
1455		log.Fatal("unknown command")
1456	}
1457}