all repos — honk @ 0a5cfa3e6ba65a37f466f206cd04b35191f574fd

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