all repos — honk @ 3d4e359fcf2735ea6bedcfbe51dd480fed3c53a0

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