all repos — honk @ 3564ac20788b84ec98931bc6db96103a3b2839bb

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	origin := keymatch(keyname, who)
 308	if origin == "" {
 309		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 310		return
 311	}
 312	objid, _ := jsongetstring(j, "id")
 313	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 314		log.Printf("ignoring thumb sucker %s", who)
 315		return
 316	}
 317	switch what {
 318	case "Ping":
 319		obj, _ := jsongetstring(j, "id")
 320		log.Printf("ping from %s: %s", who, obj)
 321		pong(user, who, obj)
 322	case "Pong":
 323		obj, _ := jsongetstring(j, "object")
 324		log.Printf("pong from %s: %s", who, obj)
 325	case "Follow":
 326		obj, _ := jsongetstring(j, "object")
 327		if obj == user.URL {
 328			log.Printf("updating honker follow: %s", who)
 329			rubadubdub(user, j)
 330		} else {
 331			log.Printf("can't follow %s", obj)
 332		}
 333	case "Accept":
 334		log.Printf("updating honker accept: %s", who)
 335		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 336		if err != nil {
 337			log.Printf("error updating honker: %s", err)
 338			return
 339		}
 340	case "Undo":
 341		obj, ok := jsongetmap(j, "object")
 342		if !ok {
 343			log.Printf("unknown undo no object")
 344		} else {
 345			what, _ := jsongetstring(obj, "type")
 346			switch what {
 347			case "Follow":
 348				log.Printf("updating honker undo: %s", who)
 349				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 350				if err != nil {
 351					log.Printf("error updating honker: %s", err)
 352					return
 353				}
 354			case "Like":
 355			case "Announce":
 356			default:
 357				log.Printf("unknown undo: %s", what)
 358			}
 359		}
 360	default:
 361		go consumeactivity(user, j, origin)
 362	}
 363}
 364
 365func outbox(w http.ResponseWriter, r *http.Request) {
 366	name := mux.Vars(r)["name"]
 367	user, err := butwhatabout(name)
 368	if err != nil {
 369		http.NotFound(w, r)
 370		return
 371	}
 372	honks := gethonksbyuser(name)
 373
 374	var jonks []map[string]interface{}
 375	for _, h := range honks {
 376		j, _ := jonkjonk(user, h)
 377		jonks = append(jonks, j)
 378	}
 379
 380	j := NewJunk()
 381	j["@context"] = itiswhatitis
 382	j["id"] = user.URL + "/outbox"
 383	j["type"] = "OrderedCollection"
 384	j["totalItems"] = len(jonks)
 385	j["orderedItems"] = jonks
 386
 387	w.Header().Set("Cache-Control", "max-age=60")
 388	w.Header().Set("Content-Type", theonetruename)
 389	WriteJunk(w, j)
 390}
 391
 392func emptiness(w http.ResponseWriter, r *http.Request) {
 393	name := mux.Vars(r)["name"]
 394	user, err := butwhatabout(name)
 395	if err != nil {
 396		http.NotFound(w, r)
 397		return
 398	}
 399	colname := "/followers"
 400	if strings.HasSuffix(r.URL.Path, "/following") {
 401		colname = "/following"
 402	}
 403	j := NewJunk()
 404	j["@context"] = itiswhatitis
 405	j["id"] = user.URL + colname
 406	j["type"] = "OrderedCollection"
 407	j["totalItems"] = 0
 408	j["orderedItems"] = []interface{}{}
 409
 410	w.Header().Set("Cache-Control", "max-age=60")
 411	w.Header().Set("Content-Type", theonetruename)
 412	WriteJunk(w, j)
 413}
 414
 415func showuser(w http.ResponseWriter, r *http.Request) {
 416	name := mux.Vars(r)["name"]
 417	user, err := butwhatabout(name)
 418	if err != nil {
 419		http.NotFound(w, r)
 420		return
 421	}
 422	if friendorfoe(r.Header.Get("Accept")) {
 423		j := asjonker(user)
 424		w.Header().Set("Cache-Control", "max-age=600")
 425		w.Header().Set("Content-Type", theonetruename)
 426		WriteJunk(w, j)
 427		return
 428	}
 429	honks := gethonksbyuser(name)
 430	u := login.GetUserInfo(r)
 431	honkpage(w, r, u, user, honks, "")
 432}
 433
 434func showhonker(w http.ResponseWriter, r *http.Request) {
 435	name := mux.Vars(r)["name"]
 436	u := login.GetUserInfo(r)
 437	honks := gethonksbyhonker(u.UserID, name)
 438	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 439}
 440
 441func showcombo(w http.ResponseWriter, r *http.Request) {
 442	name := mux.Vars(r)["name"]
 443	u := login.GetUserInfo(r)
 444	honks := gethonksbycombo(u.UserID, name)
 445	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 446}
 447func showconvoy(w http.ResponseWriter, r *http.Request) {
 448	c := r.FormValue("c")
 449	var userid int64 = -1
 450	u := login.GetUserInfo(r)
 451	if u != nil {
 452		userid = u.UserID
 453	}
 454	honks := gethonksbyconvoy(userid, c)
 455	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 456}
 457
 458func showhonk(w http.ResponseWriter, r *http.Request) {
 459	name := mux.Vars(r)["name"]
 460	user, err := butwhatabout(name)
 461	if err != nil {
 462		http.NotFound(w, r)
 463		return
 464	}
 465	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 466	h := getxonk(user.ID, xid)
 467	if h == nil {
 468		http.NotFound(w, r)
 469		return
 470	}
 471	if friendorfoe(r.Header.Get("Accept")) {
 472		donksforhonks([]*Honk{h})
 473		_, j := jonkjonk(user, h)
 474		j["@context"] = itiswhatitis
 475		w.Header().Set("Cache-Control", "max-age=3600")
 476		w.Header().Set("Content-Type", theonetruename)
 477		WriteJunk(w, j)
 478		return
 479	}
 480	honks := gethonksbyconvoy(-1, h.Convoy)
 481	for _, hh := range honks {
 482		if hh.XID != h.XID {
 483			hh.Privacy = "limited"
 484		}
 485	}
 486	u := login.GetUserInfo(r)
 487	honkpage(w, r, u, nil, honks, "one honk maybe more")
 488}
 489
 490func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 491	honks []*Honk, infomsg string) {
 492	reverbolate(honks)
 493	templinfo := getInfo(r)
 494	if u != nil {
 495		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 496	}
 497	if u == nil {
 498		w.Header().Set("Cache-Control", "max-age=60")
 499	}
 500	if user != nil {
 501		filt := htfilter.New()
 502		templinfo["Name"] = user.Name
 503		whatabout := user.About
 504		whatabout = obfusbreak(user.About)
 505		templinfo["WhatAbout"], _ = filt.String(whatabout)
 506	}
 507	templinfo["Honks"] = honks
 508	templinfo["ServerMessage"] = infomsg
 509	err := readviews.Execute(w, "honkpage.html", templinfo)
 510	if err != nil {
 511		log.Print(err)
 512	}
 513}
 514
 515func saveuser(w http.ResponseWriter, r *http.Request) {
 516	whatabout := r.FormValue("whatabout")
 517	u := login.GetUserInfo(r)
 518	db := opendatabase()
 519	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 520	if err != nil {
 521		log.Printf("error bouting what: %s", err)
 522	}
 523
 524	http.Redirect(w, r, "/account", http.StatusSeeOther)
 525}
 526
 527func gethonkers(userid int64) []*Honker {
 528	rows, err := stmtHonkers.Query(userid)
 529	if err != nil {
 530		log.Printf("error querying honkers: %s", err)
 531		return nil
 532	}
 533	defer rows.Close()
 534	var honkers []*Honker
 535	for rows.Next() {
 536		var f Honker
 537		var combos string
 538		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 539		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 540		if err != nil {
 541			log.Printf("error scanning honker: %s", err)
 542			return nil
 543		}
 544		honkers = append(honkers, &f)
 545	}
 546	return honkers
 547}
 548
 549func getdubs(userid int64) []*Honker {
 550	rows, err := stmtDubbers.Query(userid)
 551	if err != nil {
 552		log.Printf("error querying dubs: %s", err)
 553		return nil
 554	}
 555	defer rows.Close()
 556	var honkers []*Honker
 557	for rows.Next() {
 558		var f Honker
 559		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 560		if err != nil {
 561			log.Printf("error scanning honker: %s", err)
 562			return nil
 563		}
 564		honkers = append(honkers, &f)
 565	}
 566	return honkers
 567}
 568
 569func allusers() []login.UserInfo {
 570	var users []login.UserInfo
 571	rows, _ := opendatabase().Query("select userid, username from users")
 572	defer rows.Close()
 573	for rows.Next() {
 574		var u login.UserInfo
 575		rows.Scan(&u.UserID, &u.Username)
 576		users = append(users, u)
 577	}
 578	return users
 579}
 580
 581func getxonk(userid int64, xid string) *Honk {
 582	h := new(Honk)
 583	var dt, aud string
 584	row := stmtOneXonk.QueryRow(userid, xid)
 585	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 586		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 587	if err != nil {
 588		if err != sql.ErrNoRows {
 589			log.Printf("error scanning xonk: %s", err)
 590		}
 591		return nil
 592	}
 593	h.Date, _ = time.Parse(dbtimeformat, dt)
 594	h.Audience = strings.Split(aud, " ")
 595	return h
 596}
 597
 598func getpublichonks() []*Honk {
 599	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 600	rows, err := stmtPublicHonks.Query(dt)
 601	return getsomehonks(rows, err)
 602}
 603func gethonksbyuser(name string) []*Honk {
 604	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 605	rows, err := stmtUserHonks.Query(name, dt)
 606	return getsomehonks(rows, err)
 607}
 608func gethonksforuser(userid int64) []*Honk {
 609	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 610	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 611	return getsomehonks(rows, err)
 612}
 613func gethonksforme(userid int64) []*Honk {
 614	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 615	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 616	return getsomehonks(rows, err)
 617}
 618func gethonksbyhonker(userid int64, honker string) []*Honk {
 619	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 620	return getsomehonks(rows, err)
 621}
 622func gethonksbycombo(userid int64, combo string) []*Honk {
 623	combo = "% " + combo + " %"
 624	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 625	return getsomehonks(rows, err)
 626}
 627func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 628	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 629	honks := getsomehonks(rows, err)
 630	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 631		honks[i], honks[j] = honks[j], honks[i]
 632	}
 633	return honks
 634}
 635
 636func getsomehonks(rows *sql.Rows, err error) []*Honk {
 637	if err != nil {
 638		log.Printf("error querying honks: %s", err)
 639		return nil
 640	}
 641	defer rows.Close()
 642	var honks []*Honk
 643	for rows.Next() {
 644		var h Honk
 645		var dt, aud string
 646		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 647			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 648		if err != nil {
 649			log.Printf("error scanning honks: %s", err)
 650			return nil
 651		}
 652		h.Date, _ = time.Parse(dbtimeformat, dt)
 653		h.Audience = strings.Split(aud, " ")
 654		h.Privacy = "limited"
 655		for _, a := range h.Audience {
 656			if a == thewholeworld {
 657				h.Privacy = ""
 658				break
 659			}
 660		}
 661		honks = append(honks, &h)
 662	}
 663	rows.Close()
 664	donksforhonks(honks)
 665	return honks
 666}
 667
 668func donksforhonks(honks []*Honk) {
 669	db := opendatabase()
 670	var ids []string
 671	hmap := make(map[int64]*Honk)
 672	for _, h := range honks {
 673		ids = append(ids, fmt.Sprintf("%d", h.ID))
 674		hmap[h.ID] = h
 675	}
 676	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, ","))
 677	rows, err := db.Query(q)
 678	if err != nil {
 679		log.Printf("error querying donks: %s", err)
 680		return
 681	}
 682	defer rows.Close()
 683	for rows.Next() {
 684		var hid int64
 685		var d Donk
 686		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
 687		if err != nil {
 688			log.Printf("error scanning donk: %s", err)
 689			continue
 690		}
 691		h := hmap[hid]
 692		h.Donks = append(h.Donks, &d)
 693	}
 694}
 695
 696func savebonk(w http.ResponseWriter, r *http.Request) {
 697	xid := r.FormValue("xid")
 698	userinfo := login.GetUserInfo(r)
 699	user, _ := butwhatabout(userinfo.Username)
 700
 701	log.Printf("bonking %s", xid)
 702
 703	xonk := getxonk(userinfo.UserID, xid)
 704	if xonk == nil {
 705		return
 706	}
 707	donksforhonks([]*Honk{xonk})
 708
 709	dt := time.Now().UTC()
 710	bonk := Honk{
 711		UserID:   userinfo.UserID,
 712		Username: userinfo.Username,
 713		What:     "bonk",
 714		Honker:   user.URL,
 715		XID:      xonk.XID,
 716		Date:     dt,
 717		Donks:    xonk.Donks,
 718		Audience: []string{thewholeworld},
 719	}
 720
 721	oonker := xonk.Oonker
 722	if oonker == "" {
 723		oonker = xonk.Honker
 724	}
 725	aud := strings.Join(bonk.Audience, " ")
 726	whofore := 2
 727	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 728		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 729		xonk.Precis, oonker)
 730	if err != nil {
 731		log.Printf("error saving bonk: %s", err)
 732		return
 733	}
 734	bonk.ID, _ = res.LastInsertId()
 735	for _, d := range bonk.Donks {
 736		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 737		if err != nil {
 738			log.Printf("err saving donk: %s", err)
 739			return
 740		}
 741	}
 742
 743	go honkworldwide(user, &bonk)
 744}
 745
 746func zonkit(w http.ResponseWriter, r *http.Request) {
 747	wherefore := r.FormValue("wherefore")
 748	var what string
 749	switch wherefore {
 750	case "this honk":
 751		what = r.FormValue("honk")
 752		wherefore = "zonk"
 753	case "this honker":
 754		what = r.FormValue("honker")
 755		wherefore = "zonker"
 756	case "this convoy":
 757		what = r.FormValue("convoy")
 758		wherefore = "zonvoy"
 759	}
 760	if what == "" {
 761		return
 762	}
 763
 764	log.Printf("zonking %s %s", wherefore, what)
 765	userinfo := login.GetUserInfo(r)
 766	if wherefore == "zonk" {
 767		xonk := getxonk(userinfo.UserID, what)
 768		if xonk != nil {
 769			stmtZonkDonks.Exec(xonk.ID)
 770			stmtZonkIt.Exec(userinfo.UserID, what)
 771			if xonk.Whofore == 2 {
 772				zonk := Honk{
 773					What:     "zonk",
 774					XID:      xonk.XID,
 775					Date:     time.Now().UTC(),
 776					Audience: oneofakind(xonk.Audience),
 777				}
 778
 779				user, _ := butwhatabout(userinfo.Username)
 780				log.Printf("announcing deleted honk: %s", what)
 781				go honkworldwide(user, &zonk)
 782			}
 783		}
 784	} else {
 785		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 786		if err != nil {
 787			log.Printf("error saving zonker: %s", err)
 788			return
 789		}
 790	}
 791}
 792
 793func savehonk(w http.ResponseWriter, r *http.Request) {
 794	rid := r.FormValue("rid")
 795	noise := r.FormValue("noise")
 796
 797	userinfo := login.GetUserInfo(r)
 798	user, _ := butwhatabout(userinfo.Username)
 799
 800	dt := time.Now().UTC()
 801	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 802	what := "honk"
 803	if rid != "" {
 804		what = "tonk"
 805	}
 806	honk := Honk{
 807		UserID:   userinfo.UserID,
 808		Username: userinfo.Username,
 809		What:     "honk",
 810		Honker:   user.URL,
 811		XID:      xid,
 812		Date:     dt,
 813	}
 814	if strings.HasPrefix(noise, "DZ:") {
 815		idx := strings.Index(noise, "\n")
 816		if idx == -1 {
 817			honk.Precis = noise
 818			noise = ""
 819		} else {
 820			honk.Precis = noise[:idx]
 821			noise = noise[idx+1:]
 822		}
 823	}
 824	noise = strings.TrimSpace(noise)
 825	honk.Precis = strings.TrimSpace(honk.Precis)
 826
 827	if noise != "" && noise[0] == '@' {
 828		honk.Audience = append(grapevine(noise), thewholeworld)
 829	} else {
 830		honk.Audience = prepend(thewholeworld, grapevine(noise))
 831	}
 832	var convoy string
 833	if rid != "" {
 834		xonk := getxonk(userinfo.UserID, rid)
 835		if xonk != nil {
 836			honk.Audience = append(honk.Audience, xonk.Audience...)
 837			convoy = xonk.Convoy
 838		} else {
 839			xonkaud, c := whosthere(rid)
 840			honk.Audience = append(honk.Audience, xonkaud...)
 841			convoy = c
 842		}
 843		honk.RID = rid
 844	}
 845	if convoy == "" {
 846		convoy = "data:,electrichonkytonk-" + xfiltrate()
 847	}
 848	butnottooloud(honk.Audience)
 849	honk.Audience = oneofakind(honk.Audience)
 850	noise = obfusbreak(noise)
 851	honk.Noise = noise
 852	honk.Convoy = convoy
 853
 854	file, filehdr, err := r.FormFile("donk")
 855	if err == nil {
 856		var buf bytes.Buffer
 857		io.Copy(&buf, file)
 858		file.Close()
 859		data := buf.Bytes()
 860		xid := xfiltrate()
 861		var media, name string
 862		img, err := image.Vacuum(&buf)
 863		if err == nil {
 864			data = img.Data
 865			format := img.Format
 866			media = "image/" + format
 867			if format == "jpeg" {
 868				format = "jpg"
 869			}
 870			name = xid + "." + format
 871			xid = name
 872		} else {
 873			maxsize := 100000
 874			if len(data) > maxsize {
 875				log.Printf("bad image: %s too much text: %d", err, len(data))
 876				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 877				return
 878			}
 879			for i := 0; i < len(data); i++ {
 880				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 881					log.Printf("bad image: %s not text: %d", err, data[i])
 882					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 883					return
 884				}
 885			}
 886			media = "text/plain"
 887			name = filehdr.Filename
 888			if name == "" {
 889				name = xid + ".txt"
 890			}
 891			xid += ".txt"
 892		}
 893		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 894		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 895		if err != nil {
 896			log.Printf("unable to save image: %s", err)
 897			return
 898		}
 899		var d Donk
 900		d.FileID, _ = res.LastInsertId()
 901		d.XID = name
 902		d.Name = name
 903		d.Media = media
 904		d.URL = url
 905		honk.Donks = append(honk.Donks, &d)
 906	}
 907	herd := herdofemus(honk.Noise)
 908	for _, e := range herd {
 909		donk := savedonk(e.ID, e.Name, "image/png")
 910		if donk != nil {
 911			donk.Name = e.Name
 912			honk.Donks = append(honk.Donks, donk)
 913		}
 914	}
 915
 916	aud := strings.Join(honk.Audience, " ")
 917	whofore := 2
 918	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 919		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 920	if err != nil {
 921		log.Printf("error saving honk: %s", err)
 922		return
 923	}
 924	honk.ID, _ = res.LastInsertId()
 925	for _, d := range honk.Donks {
 926		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 927		if err != nil {
 928			log.Printf("err saving donk: %s", err)
 929			return
 930		}
 931	}
 932
 933	go honkworldwide(user, &honk)
 934
 935	http.Redirect(w, r, "/", http.StatusSeeOther)
 936}
 937
 938func showhonkers(w http.ResponseWriter, r *http.Request) {
 939	userinfo := login.GetUserInfo(r)
 940	templinfo := getInfo(r)
 941	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 942	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 943	err := readviews.Execute(w, "honkers.html", templinfo)
 944	if err != nil {
 945		log.Print(err)
 946	}
 947}
 948
 949func showcombos(w http.ResponseWriter, r *http.Request) {
 950	userinfo := login.GetUserInfo(r)
 951	templinfo := getInfo(r)
 952	honkers := gethonkers(userinfo.UserID)
 953	var combos []string
 954	for _, h := range honkers {
 955		combos = append(combos, h.Combos...)
 956	}
 957	for i, c := range combos {
 958		if c == "-" {
 959			combos[i] = ""
 960		}
 961	}
 962	combos = oneofakind(combos)
 963	sort.Strings(combos)
 964	templinfo["Combos"] = combos
 965	err := readviews.Execute(w, "combos.html", templinfo)
 966	if err != nil {
 967		log.Print(err)
 968	}
 969}
 970
 971func savehonker(w http.ResponseWriter, r *http.Request) {
 972	u := login.GetUserInfo(r)
 973	name := r.FormValue("name")
 974	url := r.FormValue("url")
 975	peep := r.FormValue("peep")
 976	combos := r.FormValue("combos")
 977	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 978
 979	if honkerid > 0 {
 980		goodbye := r.FormValue("goodbye")
 981		if goodbye == "goodbye" {
 982			db := opendatabase()
 983			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
 984				honkerid, u.UserID)
 985			var xid string
 986			err := row.Scan(&xid)
 987			if err != nil {
 988				log.Printf("can't get honker xid: %s", err)
 989				return
 990			}
 991			log.Printf("unsubscribing from %s", xid)
 992			user, _ := butwhatabout(u.Username)
 993			err = itakeitallback(user, xid)
 994			if err != nil {
 995				log.Printf("can't take it back: %s", err)
 996			} else {
 997				_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
 998				if err != nil {
 999					log.Printf("error updating honker: %s", err)
1000					return
1001				}
1002			}
1003
1004			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1005			return
1006		}
1007		combos = " " + strings.TrimSpace(combos) + " "
1008		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1009		if err != nil {
1010			log.Printf("update honker err: %s", err)
1011			return
1012		}
1013		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1014	}
1015
1016	flavor := "presub"
1017	if peep == "peep" {
1018		flavor = "peep"
1019	}
1020	if url == "" {
1021		return
1022	}
1023	if url[0] == '@' {
1024		url = gofish(url)
1025	}
1026	if url == "" {
1027		return
1028	}
1029	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1030	if err != nil {
1031		log.Print(err)
1032		return
1033	}
1034	if flavor == "presub" {
1035		user, _ := butwhatabout(u.Username)
1036		go subsub(user, url)
1037	}
1038	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1039}
1040
1041type Zonker struct {
1042	ID        int64
1043	Name      string
1044	Wherefore string
1045}
1046
1047func killzone(w http.ResponseWriter, r *http.Request) {
1048	db := opendatabase()
1049	userinfo := login.GetUserInfo(r)
1050	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1051	if err != nil {
1052		log.Printf("err: %s", err)
1053		return
1054	}
1055	var zonkers []Zonker
1056	for rows.Next() {
1057		var z Zonker
1058		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1059		zonkers = append(zonkers, z)
1060	}
1061	templinfo := getInfo(r)
1062	templinfo["Zonkers"] = zonkers
1063	templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r)
1064	err = readviews.Execute(w, "zonkers.html", templinfo)
1065	if err != nil {
1066		log.Print(err)
1067	}
1068}
1069
1070func killitwithfire(w http.ResponseWriter, r *http.Request) {
1071	userinfo := login.GetUserInfo(r)
1072	itsok := r.FormValue("itsok")
1073	if itsok == "iforgiveyou" {
1074		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1075		db := opendatabase()
1076		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1077			userinfo.UserID, zonkerid)
1078		bitethethumbs()
1079		http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1080		return
1081	}
1082	wherefore := r.FormValue("wherefore")
1083	name := r.FormValue("name")
1084	if name == "" {
1085		return
1086	}
1087	switch wherefore {
1088	case "zonker":
1089	case "zurl":
1090	case "zonvoy":
1091	default:
1092		return
1093	}
1094	db := opendatabase()
1095	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1096		userinfo.UserID, name, wherefore)
1097	if wherefore == "zonker" || wherefore == "zurl" {
1098		bitethethumbs()
1099	}
1100
1101	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1102}
1103
1104func accountpage(w http.ResponseWriter, r *http.Request) {
1105	u := login.GetUserInfo(r)
1106	user, _ := butwhatabout(u.Username)
1107	templinfo := getInfo(r)
1108	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1109	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1110	templinfo["WhatAbout"] = user.About
1111	err := readviews.Execute(w, "account.html", templinfo)
1112	if err != nil {
1113		log.Print(err)
1114	}
1115}
1116
1117func dochpass(w http.ResponseWriter, r *http.Request) {
1118	err := login.ChangePassword(w, r)
1119	if err != nil {
1120		log.Printf("error changing password: %s", err)
1121	}
1122	http.Redirect(w, r, "/account", http.StatusSeeOther)
1123}
1124
1125func fingerlicker(w http.ResponseWriter, r *http.Request) {
1126	orig := r.FormValue("resource")
1127
1128	log.Printf("finger lick: %s", orig)
1129
1130	if strings.HasPrefix(orig, "acct:") {
1131		orig = orig[5:]
1132	}
1133
1134	name := orig
1135	idx := strings.LastIndexByte(name, '/')
1136	if idx != -1 {
1137		name = name[idx+1:]
1138		if "https://"+serverName+"/u/"+name != orig {
1139			log.Printf("foreign request rejected")
1140			name = ""
1141		}
1142	} else {
1143		idx = strings.IndexByte(name, '@')
1144		if idx != -1 {
1145			name = name[:idx]
1146			if name+"@"+serverName != orig {
1147				log.Printf("foreign request rejected")
1148				name = ""
1149			}
1150		}
1151	}
1152	user, err := butwhatabout(name)
1153	if err != nil {
1154		http.NotFound(w, r)
1155		return
1156	}
1157
1158	j := NewJunk()
1159	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1160	j["aliases"] = []string{user.URL}
1161	var links []map[string]interface{}
1162	l := NewJunk()
1163	l["rel"] = "self"
1164	l["type"] = `application/activity+json`
1165	l["href"] = user.URL
1166	links = append(links, l)
1167	j["links"] = links
1168
1169	w.Header().Set("Cache-Control", "max-age=3600")
1170	w.Header().Set("Content-Type", "application/jrd+json")
1171	WriteJunk(w, j)
1172}
1173
1174func somedays() string {
1175	secs := 432000 + notrand.Int63n(432000)
1176	return fmt.Sprintf("%d", secs)
1177}
1178
1179func avatate(w http.ResponseWriter, r *http.Request) {
1180	n := r.FormValue("a")
1181	a := avatar(n)
1182	w.Header().Set("Cache-Control", "max-age="+somedays())
1183	w.Write(a)
1184}
1185
1186func servecss(w http.ResponseWriter, r *http.Request) {
1187	w.Header().Set("Cache-Control", "max-age=7776000")
1188	http.ServeFile(w, r, "views"+r.URL.Path)
1189}
1190func servehtml(w http.ResponseWriter, r *http.Request) {
1191	templinfo := getInfo(r)
1192	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1193	if err != nil {
1194		log.Print(err)
1195	}
1196}
1197func serveemu(w http.ResponseWriter, r *http.Request) {
1198	xid := mux.Vars(r)["xid"]
1199	w.Header().Set("Cache-Control", "max-age="+somedays())
1200	http.ServeFile(w, r, "emus/"+xid)
1201}
1202
1203func servefile(w http.ResponseWriter, r *http.Request) {
1204	xid := mux.Vars(r)["xid"]
1205	row := stmtFileData.QueryRow(xid)
1206	var media string
1207	var data []byte
1208	err := row.Scan(&media, &data)
1209	if err != nil {
1210		log.Printf("error loading file: %s", err)
1211		http.NotFound(w, r)
1212		return
1213	}
1214	w.Header().Set("Content-Type", media)
1215	w.Header().Set("X-Content-Type-Options", "nosniff")
1216	w.Header().Set("Cache-Control", "max-age="+somedays())
1217	w.Write(data)
1218}
1219
1220func serve() {
1221	db := opendatabase()
1222	login.Init(db)
1223
1224	listener, err := openListener()
1225	if err != nil {
1226		log.Fatal(err)
1227	}
1228	go redeliverator()
1229
1230	debug := false
1231	getconfig("debug", &debug)
1232	readviews = templates.Load(debug,
1233		"views/honkpage.html",
1234		"views/honkers.html",
1235		"views/zonkers.html",
1236		"views/combos.html",
1237		"views/honkform.html",
1238		"views/honk.html",
1239		"views/account.html",
1240		"views/login.html",
1241		"views/header.html",
1242	)
1243	if !debug {
1244		s := "views/style.css"
1245		savedstyleparams[s] = getstyleparam(s)
1246		s = "views/local.css"
1247		savedstyleparams[s] = getstyleparam(s)
1248	}
1249
1250	bitethethumbs()
1251
1252	mux := mux.NewRouter()
1253	mux.Use(login.Checker)
1254
1255	posters := mux.Methods("POST").Subrouter()
1256	getters := mux.Methods("GET").Subrouter()
1257
1258	getters.HandleFunc("/", homepage)
1259	getters.HandleFunc("/rss", showrss)
1260	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1261	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1262	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1263	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1264	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1265	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1266	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1267	getters.HandleFunc("/a", avatate)
1268	getters.HandleFunc("/t", showconvoy)
1269	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1270	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1271	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1272
1273	getters.HandleFunc("/style.css", servecss)
1274	getters.HandleFunc("/local.css", servecss)
1275	getters.HandleFunc("/login", servehtml)
1276	posters.HandleFunc("/dologin", login.LoginFunc)
1277	getters.HandleFunc("/logout", login.LogoutFunc)
1278
1279	loggedin := mux.NewRoute().Subrouter()
1280	loggedin.Use(login.Required)
1281	loggedin.HandleFunc("/account", accountpage)
1282	loggedin.HandleFunc("/chpass", dochpass)
1283	loggedin.HandleFunc("/atme", homepage)
1284	loggedin.HandleFunc("/killzone", killzone)
1285	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1286	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1287	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1288	loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1289	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1290	loggedin.HandleFunc("/honkers", showhonkers)
1291	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1292	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1293	loggedin.HandleFunc("/c", showcombos)
1294	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1295
1296	err = http.Serve(listener, mux)
1297	if err != nil {
1298		log.Fatal(err)
1299	}
1300}
1301
1302func cleanupdb() {
1303	db := opendatabase()
1304	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1305}
1306
1307var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1308var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1309var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1310var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1311var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1312var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1313var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1314var stmtGetBoxes, stmtSaveBoxes *sql.Stmt
1315
1316func preparetodie(db *sql.DB, s string) *sql.Stmt {
1317	stmt, err := db.Prepare(s)
1318	if err != nil {
1319		log.Fatalf("error %s: %s", err, s)
1320	}
1321	return stmt
1322}
1323
1324func prepareStatements(db *sql.DB) {
1325	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")
1326	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1327	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1328	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1329	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1330	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1331
1332	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 "
1333	limit := " order by honkid desc limit 250"
1334	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1335	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1336	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1337	stmtUserHonks = preparetodie(db, selecthonks+"where whofore = 2 and username = ? and dt > ?"+limit)
1338	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)
1339	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1340	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1341	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1342	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1343
1344	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1345	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1346	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1347	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1348	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1349	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1350	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1351	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1352	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1353	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1354	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1355	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1356	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1357	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1358	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl')")
1359	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1360	stmtGetBoxes = preparetodie(db, "select ibox, obox, sbox from xonkers where xid = ?")
1361	stmtSaveBoxes = preparetodie(db, "insert into xonkers (xid, ibox, obox, sbox, pubkey) values (?, ?, ?, ?, ?)")
1362}
1363
1364func ElaborateUnitTests() {
1365}
1366
1367func finishusersetup() error {
1368	db := opendatabase()
1369	k, err := rsa.GenerateKey(rand.Reader, 2048)
1370	if err != nil {
1371		return err
1372	}
1373	pubkey, err := zem(&k.PublicKey)
1374	if err != nil {
1375		return err
1376	}
1377	seckey, err := zem(k)
1378	if err != nil {
1379		return err
1380	}
1381	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1382	if err != nil {
1383		return err
1384	}
1385	return nil
1386}
1387
1388func main() {
1389	cmd := "run"
1390	if len(os.Args) > 1 {
1391		cmd = os.Args[1]
1392	}
1393	switch cmd {
1394	case "init":
1395		initdb()
1396	case "upgrade":
1397		upgradedb()
1398	}
1399	db := opendatabase()
1400	dbversion := 0
1401	getconfig("dbversion", &dbversion)
1402	if dbversion != myVersion {
1403		log.Fatal("incorrect database version. run upgrade.")
1404	}
1405	getconfig("servername", &serverName)
1406	prepareStatements(db)
1407	switch cmd {
1408	case "cleanup":
1409		cleanupdb()
1410	case "ping":
1411		if len(os.Args) < 4 {
1412			fmt.Printf("usage: honk ping from to\n")
1413			return
1414		}
1415		name := os.Args[2]
1416		targ := os.Args[3]
1417		user, err := butwhatabout(name)
1418		if err != nil {
1419			log.Printf("unknown user")
1420			return
1421		}
1422		ping(user, targ)
1423	case "peep":
1424		peeppeep()
1425	case "run":
1426		serve()
1427	case "test":
1428		ElaborateUnitTests()
1429	default:
1430		log.Fatal("unknown command")
1431	}
1432}