all repos — honk @ a9dfd9f24c316952dc6de514b24e722d1f093875

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