all repos — honk @ 18e94bc69b730b9cbcdbdf68665cae2a4cf93323

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 = hooterize(noise)
 825	noise = strings.TrimSpace(noise)
 826	honk.Precis = strings.TrimSpace(honk.Precis)
 827
 828	var convoy string
 829	if rid != "" {
 830		xonk := getxonk(userinfo.UserID, rid)
 831		if xonk != nil {
 832			if xonk.Public {
 833				honk.Audience = append(honk.Audience, xonk.Audience...)
 834			}
 835			convoy = xonk.Convoy
 836		} else {
 837			xonkaud, c := whosthere(rid)
 838			honk.Audience = append(honk.Audience, xonkaud...)
 839			convoy = c
 840		}
 841		for i, a := range honk.Audience {
 842			if a == thewholeworld {
 843				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 844				break
 845			}
 846		}
 847		honk.RID = rid
 848	} else {
 849		honk.Audience = []string{thewholeworld}
 850	}
 851	if noise != "" && noise[0] == '@' {
 852		honk.Audience = append(grapevine(noise), honk.Audience...)
 853	} else {
 854		honk.Audience = append(honk.Audience, grapevine(noise)...)
 855	}
 856	if convoy == "" {
 857		convoy = "data:,electrichonkytonk-" + xfiltrate()
 858	}
 859	butnottooloud(honk.Audience)
 860	honk.Audience = oneofakind(honk.Audience)
 861	if len(honk.Audience) == 0 {
 862		log.Printf("honk to nowhere")
 863		return
 864	}
 865	honk.Public = !keepitquiet(honk.Audience)
 866	noise = obfusbreak(noise)
 867	honk.Noise = noise
 868	honk.Convoy = convoy
 869
 870	file, filehdr, err := r.FormFile("donk")
 871	if err == nil {
 872		var buf bytes.Buffer
 873		io.Copy(&buf, file)
 874		file.Close()
 875		data := buf.Bytes()
 876		xid := xfiltrate()
 877		var media, name string
 878		img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
 879		if err == nil {
 880			data = img.Data
 881			format := img.Format
 882			media = "image/" + format
 883			if format == "jpeg" {
 884				format = "jpg"
 885			}
 886			name = xid + "." + format
 887			xid = name
 888		} else {
 889			maxsize := 100000
 890			if len(data) > maxsize {
 891				log.Printf("bad image: %s too much text: %d", err, len(data))
 892				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 893				return
 894			}
 895			for i := 0; i < len(data); i++ {
 896				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 897					log.Printf("bad image: %s not text: %d", err, data[i])
 898					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 899					return
 900				}
 901			}
 902			media = "text/plain"
 903			name = filehdr.Filename
 904			if name == "" {
 905				name = xid + ".txt"
 906			}
 907			xid += ".txt"
 908		}
 909		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 910		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 911		if err != nil {
 912			log.Printf("unable to save image: %s", err)
 913			return
 914		}
 915		var d Donk
 916		d.FileID, _ = res.LastInsertId()
 917		d.XID = name
 918		d.Name = name
 919		d.Media = media
 920		d.URL = url
 921		honk.Donks = append(honk.Donks, &d)
 922	}
 923	herd := herdofemus(honk.Noise)
 924	for _, e := range herd {
 925		donk := savedonk(e.ID, e.Name, "image/png", true)
 926		if donk != nil {
 927			donk.Name = e.Name
 928			honk.Donks = append(honk.Donks, donk)
 929		}
 930	}
 931	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 932
 933	aud := strings.Join(honk.Audience, " ")
 934	whofore := 2
 935	if !honk.Public {
 936		whofore = 3
 937	}
 938	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 939		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 940	if err != nil {
 941		log.Printf("error saving honk: %s", err)
 942		return
 943	}
 944	honk.ID, _ = res.LastInsertId()
 945	for _, d := range honk.Donks {
 946		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 947		if err != nil {
 948			log.Printf("err saving donk: %s", err)
 949			return
 950		}
 951	}
 952
 953	go honkworldwide(user, &honk)
 954
 955	http.Redirect(w, r, "/", http.StatusSeeOther)
 956}
 957
 958func showhonkers(w http.ResponseWriter, r *http.Request) {
 959	userinfo := login.GetUserInfo(r)
 960	templinfo := getInfo(r)
 961	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 962	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 963	err := readviews.Execute(w, "honkers.html", templinfo)
 964	if err != nil {
 965		log.Print(err)
 966	}
 967}
 968
 969func showcombos(w http.ResponseWriter, r *http.Request) {
 970	userinfo := login.GetUserInfo(r)
 971	templinfo := getInfo(r)
 972	honkers := gethonkers(userinfo.UserID)
 973	var combos []string
 974	for _, h := range honkers {
 975		combos = append(combos, h.Combos...)
 976	}
 977	for i, c := range combos {
 978		if c == "-" {
 979			combos[i] = ""
 980		}
 981	}
 982	combos = oneofakind(combos)
 983	sort.Strings(combos)
 984	templinfo["Combos"] = combos
 985	err := readviews.Execute(w, "combos.html", templinfo)
 986	if err != nil {
 987		log.Print(err)
 988	}
 989}
 990
 991func savehonker(w http.ResponseWriter, r *http.Request) {
 992	u := login.GetUserInfo(r)
 993	name := r.FormValue("name")
 994	url := r.FormValue("url")
 995	peep := r.FormValue("peep")
 996	combos := r.FormValue("combos")
 997	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 998
 999	if honkerid > 0 {
1000		goodbye := r.FormValue("goodbye")
1001		if goodbye == "F" {
1002			db := opendatabase()
1003			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1004				honkerid, u.UserID)
1005			var xid string
1006			err := row.Scan(&xid)
1007			if err != nil {
1008				log.Printf("can't get honker xid: %s", err)
1009				return
1010			}
1011			log.Printf("unsubscribing from %s", xid)
1012			user, _ := butwhatabout(u.Username)
1013			err = itakeitallback(user, xid)
1014			if err != nil {
1015				log.Printf("can't take it back: %s", err)
1016			} else {
1017				_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1018				if err != nil {
1019					log.Printf("error updating honker: %s", err)
1020					return
1021				}
1022			}
1023
1024			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1025			return
1026		}
1027		combos = " " + strings.TrimSpace(combos) + " "
1028		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1029		if err != nil {
1030			log.Printf("update honker err: %s", err)
1031			return
1032		}
1033		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1034	}
1035
1036	flavor := "presub"
1037	if peep == "peep" {
1038		flavor = "peep"
1039	}
1040	url = investigate(url)
1041	if url == "" {
1042		return
1043	}
1044	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1045	if err != nil {
1046		log.Print(err)
1047		return
1048	}
1049	if flavor == "presub" {
1050		user, _ := butwhatabout(u.Username)
1051		go subsub(user, url)
1052	}
1053	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1054}
1055
1056type Zonker struct {
1057	ID        int64
1058	Name      string
1059	Wherefore string
1060}
1061
1062func zonkzone(w http.ResponseWriter, r *http.Request) {
1063	db := opendatabase()
1064	userinfo := login.GetUserInfo(r)
1065	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1066	if err != nil {
1067		log.Printf("err: %s", err)
1068		return
1069	}
1070	var zonkers []Zonker
1071	for rows.Next() {
1072		var z Zonker
1073		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1074		zonkers = append(zonkers, z)
1075	}
1076	templinfo := getInfo(r)
1077	templinfo["Zonkers"] = zonkers
1078	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1079	err = readviews.Execute(w, "zonkers.html", templinfo)
1080	if err != nil {
1081		log.Print(err)
1082	}
1083}
1084
1085func zonkzonk(w http.ResponseWriter, r *http.Request) {
1086	userinfo := login.GetUserInfo(r)
1087	itsok := r.FormValue("itsok")
1088	if itsok == "iforgiveyou" {
1089		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1090		db := opendatabase()
1091		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1092			userinfo.UserID, zonkerid)
1093		bitethethumbs()
1094		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1095		return
1096	}
1097	wherefore := r.FormValue("wherefore")
1098	name := r.FormValue("name")
1099	if name == "" {
1100		return
1101	}
1102	switch wherefore {
1103	case "zonker":
1104	case "zurl":
1105	case "zonvoy":
1106	case "zword":
1107	default:
1108		return
1109	}
1110	db := opendatabase()
1111	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1112		userinfo.UserID, name, wherefore)
1113	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1114		bitethethumbs()
1115	}
1116
1117	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1118}
1119
1120func accountpage(w http.ResponseWriter, r *http.Request) {
1121	u := login.GetUserInfo(r)
1122	user, _ := butwhatabout(u.Username)
1123	templinfo := getInfo(r)
1124	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1125	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1126	templinfo["WhatAbout"] = user.About
1127	err := readviews.Execute(w, "account.html", templinfo)
1128	if err != nil {
1129		log.Print(err)
1130	}
1131}
1132
1133func dochpass(w http.ResponseWriter, r *http.Request) {
1134	err := login.ChangePassword(w, r)
1135	if err != nil {
1136		log.Printf("error changing password: %s", err)
1137	}
1138	http.Redirect(w, r, "/account", http.StatusSeeOther)
1139}
1140
1141func fingerlicker(w http.ResponseWriter, r *http.Request) {
1142	orig := r.FormValue("resource")
1143
1144	log.Printf("finger lick: %s", orig)
1145
1146	if strings.HasPrefix(orig, "acct:") {
1147		orig = orig[5:]
1148	}
1149
1150	name := orig
1151	idx := strings.LastIndexByte(name, '/')
1152	if idx != -1 {
1153		name = name[idx+1:]
1154		if "https://"+serverName+"/u/"+name != orig {
1155			log.Printf("foreign request rejected")
1156			name = ""
1157		}
1158	} else {
1159		idx = strings.IndexByte(name, '@')
1160		if idx != -1 {
1161			name = name[:idx]
1162			if name+"@"+serverName != orig {
1163				log.Printf("foreign request rejected")
1164				name = ""
1165			}
1166		}
1167	}
1168	user, err := butwhatabout(name)
1169	if err != nil {
1170		http.NotFound(w, r)
1171		return
1172	}
1173
1174	j := NewJunk()
1175	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1176	j["aliases"] = []string{user.URL}
1177	var links []map[string]interface{}
1178	l := NewJunk()
1179	l["rel"] = "self"
1180	l["type"] = `application/activity+json`
1181	l["href"] = user.URL
1182	links = append(links, l)
1183	j["links"] = links
1184
1185	w.Header().Set("Cache-Control", "max-age=3600")
1186	w.Header().Set("Content-Type", "application/jrd+json")
1187	WriteJunk(w, j)
1188}
1189
1190func somedays() string {
1191	secs := 432000 + notrand.Int63n(432000)
1192	return fmt.Sprintf("%d", secs)
1193}
1194
1195func avatate(w http.ResponseWriter, r *http.Request) {
1196	n := r.FormValue("a")
1197	a := avatar(n)
1198	w.Header().Set("Cache-Control", "max-age="+somedays())
1199	w.Write(a)
1200}
1201
1202func servecss(w http.ResponseWriter, r *http.Request) {
1203	w.Header().Set("Cache-Control", "max-age=7776000")
1204	http.ServeFile(w, r, "views"+r.URL.Path)
1205}
1206func servehtml(w http.ResponseWriter, r *http.Request) {
1207	templinfo := getInfo(r)
1208	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1209	if err != nil {
1210		log.Print(err)
1211	}
1212}
1213func serveemu(w http.ResponseWriter, r *http.Request) {
1214	xid := mux.Vars(r)["xid"]
1215	w.Header().Set("Cache-Control", "max-age="+somedays())
1216	http.ServeFile(w, r, "emus/"+xid)
1217}
1218func servememe(w http.ResponseWriter, r *http.Request) {
1219	xid := mux.Vars(r)["xid"]
1220	w.Header().Set("Cache-Control", "max-age="+somedays())
1221	http.ServeFile(w, r, "memes/"+xid)
1222}
1223
1224func servefile(w http.ResponseWriter, r *http.Request) {
1225	xid := mux.Vars(r)["xid"]
1226	row := stmtFileData.QueryRow(xid)
1227	var media string
1228	var data []byte
1229	err := row.Scan(&media, &data)
1230	if err != nil {
1231		log.Printf("error loading file: %s", err)
1232		http.NotFound(w, r)
1233		return
1234	}
1235	w.Header().Set("Content-Type", media)
1236	w.Header().Set("X-Content-Type-Options", "nosniff")
1237	w.Header().Set("Cache-Control", "max-age="+somedays())
1238	w.Write(data)
1239}
1240
1241func serve() {
1242	db := opendatabase()
1243	login.Init(db)
1244
1245	listener, err := openListener()
1246	if err != nil {
1247		log.Fatal(err)
1248	}
1249	go redeliverator()
1250
1251	debug := false
1252	getconfig("debug", &debug)
1253	readviews = templates.Load(debug,
1254		"views/honkpage.html",
1255		"views/honkers.html",
1256		"views/zonkers.html",
1257		"views/combos.html",
1258		"views/honkform.html",
1259		"views/honk.html",
1260		"views/account.html",
1261		"views/login.html",
1262		"views/header.html",
1263	)
1264	if !debug {
1265		s := "views/style.css"
1266		savedstyleparams[s] = getstyleparam(s)
1267		s = "views/local.css"
1268		savedstyleparams[s] = getstyleparam(s)
1269	}
1270
1271	bitethethumbs()
1272
1273	mux := mux.NewRouter()
1274	mux.Use(login.Checker)
1275
1276	posters := mux.Methods("POST").Subrouter()
1277	getters := mux.Methods("GET").Subrouter()
1278
1279	getters.HandleFunc("/", homepage)
1280	getters.HandleFunc("/rss", showrss)
1281	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1282	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1283	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1284	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1285	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1286	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1287	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1288	getters.HandleFunc("/a", avatate)
1289	getters.HandleFunc("/t", showconvoy)
1290	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1291	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1292	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1293	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1294
1295	getters.HandleFunc("/style.css", servecss)
1296	getters.HandleFunc("/local.css", servecss)
1297	getters.HandleFunc("/login", servehtml)
1298	posters.HandleFunc("/dologin", login.LoginFunc)
1299	getters.HandleFunc("/logout", login.LogoutFunc)
1300
1301	loggedin := mux.NewRoute().Subrouter()
1302	loggedin.Use(login.Required)
1303	loggedin.HandleFunc("/account", accountpage)
1304	loggedin.HandleFunc("/chpass", dochpass)
1305	loggedin.HandleFunc("/atme", homepage)
1306	loggedin.HandleFunc("/zonkzone", zonkzone)
1307	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1308	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1309	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1310	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1311	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1312	loggedin.HandleFunc("/honkers", showhonkers)
1313	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1314	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1315	loggedin.HandleFunc("/c", showcombos)
1316	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1317
1318	err = http.Serve(listener, mux)
1319	if err != nil {
1320		log.Fatal(err)
1321	}
1322}
1323
1324func cleanupdb() {
1325	db := opendatabase()
1326	rows, _ := db.Query("select userid, name from zonkers where wherefore = 'zonvoy'")
1327	deadthreads := make(map[int64][]string)
1328	for rows.Next() {
1329		var userid int64
1330		var name string
1331		rows.Scan(&userid, &name)
1332		deadthreads[userid] = append(deadthreads[userid], name)
1333	}
1334	rows.Close()
1335	for userid, threads := range deadthreads {
1336		for _, t := range threads {
1337			doordie(db, "delete from donks where honkid in (select honkid from honks where userid = ? and convoy = ?)", userid, t)
1338			doordie(db, "delete from honks where userid = ? and convoy = ?", userid, t)
1339		}
1340	}
1341	expdate := time.Now().UTC().Add(-30 * 24 * time.Hour).Format(dbtimeformat)
1342	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)
1343	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)
1344	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1345}
1346
1347var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1348var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1349var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1350var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1351var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1352var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1353var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1354var stmtGetXonker, stmtSaveXonker *sql.Stmt
1355
1356func preparetodie(db *sql.DB, s string) *sql.Stmt {
1357	stmt, err := db.Prepare(s)
1358	if err != nil {
1359		log.Fatalf("error %s: %s", err, s)
1360	}
1361	return stmt
1362}
1363
1364func prepareStatements(db *sql.DB) {
1365	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")
1366	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1367	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1368	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1369	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1370	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1371
1372	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 "
1373	limit := " order by honkid desc limit 250"
1374	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1375	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1376	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1377	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1378	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)
1379	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1380	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1381	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1382	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1383
1384	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1385	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1386	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1387	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1388	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1389	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1390	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1391	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1392	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1393	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1394	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1395	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1396	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1397	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1398	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1399	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1400	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1401	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1402}
1403
1404func ElaborateUnitTests() {
1405}
1406
1407func main() {
1408	cmd := "run"
1409	if len(os.Args) > 1 {
1410		cmd = os.Args[1]
1411	}
1412	switch cmd {
1413	case "init":
1414		initdb()
1415	case "upgrade":
1416		upgradedb()
1417	}
1418	db := opendatabase()
1419	dbversion := 0
1420	getconfig("dbversion", &dbversion)
1421	if dbversion != myVersion {
1422		log.Fatal("incorrect database version. run upgrade.")
1423	}
1424	getconfig("servername", &serverName)
1425	prepareStatements(db)
1426	switch cmd {
1427	case "adduser":
1428		adduser()
1429	case "cleanup":
1430		cleanupdb()
1431	case "ping":
1432		if len(os.Args) < 4 {
1433			fmt.Printf("usage: honk ping from to\n")
1434			return
1435		}
1436		name := os.Args[2]
1437		targ := os.Args[3]
1438		user, err := butwhatabout(name)
1439		if err != nil {
1440			log.Printf("unknown user")
1441			return
1442		}
1443		ping(user, targ)
1444	case "peep":
1445		peeppeep()
1446	case "run":
1447		serve()
1448	case "test":
1449		ElaborateUnitTests()
1450	default:
1451		log.Fatal("unknown command")
1452	}
1453}