all repos — honk @ abae6a4d371a28920edf52d1286cb0df5491a913

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