all repos — honk @ 40cbaec63a2409a3d187c3746a859ab0e8e21e98

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