all repos — honk @ 039a5c36fa004f3096599b3b6f45a72e2ea3ed9d

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