all repos — honk @ 9bc0903f98725d5301134ce488119fb44a1aee2e

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