all repos — honk @ f9fb2c73760045569f460dac473454ccfb4ea049

my fork of honk

honk.go (view raw)

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