all repos — honk @ 04c12c4c221ca70ed9e197966cb0b3f3ef9b9403

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		d.Local = true
 950		honk.Donks = append(honk.Donks, &d)
 951	}
 952	herd := herdofemus(honk.Noise)
 953	for _, e := range herd {
 954		donk := savedonk(e.ID, e.Name, "image/png", true)
 955		if donk != nil {
 956			donk.Name = e.Name
 957			honk.Donks = append(honk.Donks, donk)
 958		}
 959	}
 960	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 961
 962	aud := strings.Join(honk.Audience, " ")
 963	whofore := 2
 964	if !honk.Public {
 965		whofore = 3
 966	}
 967	if r.FormValue("preview") == "preview" {
 968		honks := []*Honk{ &honk }
 969		reverbolate(honks)
 970		templinfo := getInfo(r)
 971		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 972		templinfo["Honks"] = honks
 973		templinfo["Noise"] = r.FormValue("noise")
 974		templinfo["ServerMessage"] = "honk preview"
 975		err := readviews.Execute(w, "honkpage.html", templinfo)
 976		if err != nil {
 977			log.Print(err)
 978		}
 979		return
 980	}
 981	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 982		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 983	if err != nil {
 984		log.Printf("error saving honk: %s", err)
 985		return
 986	}
 987	honk.ID, _ = res.LastInsertId()
 988	for _, d := range honk.Donks {
 989		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 990		if err != nil {
 991			log.Printf("err saving donk: %s", err)
 992			return
 993		}
 994	}
 995
 996	go honkworldwide(user, &honk)
 997
 998	http.Redirect(w, r, "/", http.StatusSeeOther)
 999}
1000
1001func showhonkers(w http.ResponseWriter, r *http.Request) {
1002	userinfo := login.GetUserInfo(r)
1003	templinfo := getInfo(r)
1004	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1005	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1006	err := readviews.Execute(w, "honkers.html", templinfo)
1007	if err != nil {
1008		log.Print(err)
1009	}
1010}
1011
1012func showcombos(w http.ResponseWriter, r *http.Request) {
1013	userinfo := login.GetUserInfo(r)
1014	templinfo := getInfo(r)
1015	honkers := gethonkers(userinfo.UserID)
1016	var combos []string
1017	for _, h := range honkers {
1018		combos = append(combos, h.Combos...)
1019	}
1020	for i, c := range combos {
1021		if c == "-" {
1022			combos[i] = ""
1023		}
1024	}
1025	combos = oneofakind(combos)
1026	sort.Strings(combos)
1027	templinfo["Combos"] = combos
1028	err := readviews.Execute(w, "combos.html", templinfo)
1029	if err != nil {
1030		log.Print(err)
1031	}
1032}
1033
1034func savehonker(w http.ResponseWriter, r *http.Request) {
1035	u := login.GetUserInfo(r)
1036	name := r.FormValue("name")
1037	url := r.FormValue("url")
1038	peep := r.FormValue("peep")
1039	combos := r.FormValue("combos")
1040	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1041
1042	if honkerid > 0 {
1043		goodbye := r.FormValue("goodbye")
1044		if goodbye == "F" {
1045			db := opendatabase()
1046			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1047				honkerid, u.UserID)
1048			var xid string
1049			err := row.Scan(&xid)
1050			if err != nil {
1051				log.Printf("can't get honker xid: %s", err)
1052				return
1053			}
1054			log.Printf("unsubscribing from %s", xid)
1055			user, _ := butwhatabout(u.Username)
1056			go itakeitallback(user, xid)
1057			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1058			if err != nil {
1059				log.Printf("error updating honker: %s", err)
1060				return
1061			}
1062
1063			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1064			return
1065		}
1066		combos = " " + strings.TrimSpace(combos) + " "
1067		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1068		if err != nil {
1069			log.Printf("update honker err: %s", err)
1070			return
1071		}
1072		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1073	}
1074
1075	flavor := "presub"
1076	if peep == "peep" {
1077		flavor = "peep"
1078	}
1079	url = investigate(url)
1080	if url == "" {
1081		return
1082	}
1083	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1084	if err != nil {
1085		log.Print(err)
1086		return
1087	}
1088	if flavor == "presub" {
1089		user, _ := butwhatabout(u.Username)
1090		go subsub(user, url)
1091	}
1092	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1093}
1094
1095type Zonker struct {
1096	ID        int64
1097	Name      string
1098	Wherefore string
1099}
1100
1101func zonkzone(w http.ResponseWriter, r *http.Request) {
1102	db := opendatabase()
1103	userinfo := login.GetUserInfo(r)
1104	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1105	if err != nil {
1106		log.Printf("err: %s", err)
1107		return
1108	}
1109	var zonkers []Zonker
1110	for rows.Next() {
1111		var z Zonker
1112		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1113		zonkers = append(zonkers, z)
1114	}
1115	sort.Slice(zonkers, func (i, j int) bool {
1116		w1 := zonkers[i].Wherefore
1117		w2 := zonkers[j].Wherefore
1118		if w1 == w2 {
1119			return zonkers[i].Name < zonkers[j].Name
1120		}
1121		if w1 == "zonvoy" {
1122			w1 = "zzzzzzz"
1123		}
1124		if w2 == "zonvoy" {
1125			w2 = "zzzzzzz"
1126		}
1127		return w1 < w2
1128	})
1129
1130	templinfo := getInfo(r)
1131	templinfo["Zonkers"] = zonkers
1132	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1133	err = readviews.Execute(w, "zonkers.html", templinfo)
1134	if err != nil {
1135		log.Print(err)
1136	}
1137}
1138
1139func zonkzonk(w http.ResponseWriter, r *http.Request) {
1140	userinfo := login.GetUserInfo(r)
1141	itsok := r.FormValue("itsok")
1142	if itsok == "iforgiveyou" {
1143		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1144		db := opendatabase()
1145		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1146			userinfo.UserID, zonkerid)
1147		bitethethumbs()
1148		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1149		return
1150	}
1151	wherefore := r.FormValue("wherefore")
1152	name := r.FormValue("name")
1153	if name == "" {
1154		return
1155	}
1156	switch wherefore {
1157	case "zonker":
1158	case "zurl":
1159	case "zonvoy":
1160	case "zword":
1161	default:
1162		return
1163	}
1164	db := opendatabase()
1165	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1166		userinfo.UserID, name, wherefore)
1167	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1168		bitethethumbs()
1169	}
1170
1171	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1172}
1173
1174func accountpage(w http.ResponseWriter, r *http.Request) {
1175	u := login.GetUserInfo(r)
1176	user, _ := butwhatabout(u.Username)
1177	templinfo := getInfo(r)
1178	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1179	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1180	templinfo["WhatAbout"] = user.About
1181	err := readviews.Execute(w, "account.html", templinfo)
1182	if err != nil {
1183		log.Print(err)
1184	}
1185}
1186
1187func dochpass(w http.ResponseWriter, r *http.Request) {
1188	err := login.ChangePassword(w, r)
1189	if err != nil {
1190		log.Printf("error changing password: %s", err)
1191	}
1192	http.Redirect(w, r, "/account", http.StatusSeeOther)
1193}
1194
1195func fingerlicker(w http.ResponseWriter, r *http.Request) {
1196	orig := r.FormValue("resource")
1197
1198	log.Printf("finger lick: %s", orig)
1199
1200	if strings.HasPrefix(orig, "acct:") {
1201		orig = orig[5:]
1202	}
1203
1204	name := orig
1205	idx := strings.LastIndexByte(name, '/')
1206	if idx != -1 {
1207		name = name[idx+1:]
1208		if "https://"+serverName+"/u/"+name != orig {
1209			log.Printf("foreign request rejected")
1210			name = ""
1211		}
1212	} else {
1213		idx = strings.IndexByte(name, '@')
1214		if idx != -1 {
1215			name = name[:idx]
1216			if name+"@"+serverName != orig {
1217				log.Printf("foreign request rejected")
1218				name = ""
1219			}
1220		}
1221	}
1222	user, err := butwhatabout(name)
1223	if err != nil {
1224		http.NotFound(w, r)
1225		return
1226	}
1227
1228	j := junk.New()
1229	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1230	j["aliases"] = []string{user.URL}
1231	var links []map[string]interface{}
1232	l := junk.New()
1233	l["rel"] = "self"
1234	l["type"] = `application/activity+json`
1235	l["href"] = user.URL
1236	links = append(links, l)
1237	j["links"] = links
1238
1239	w.Header().Set("Cache-Control", "max-age=3600")
1240	w.Header().Set("Content-Type", "application/jrd+json")
1241	j.Write(w)
1242}
1243
1244func somedays() string {
1245	secs := 432000 + notrand.Int63n(432000)
1246	return fmt.Sprintf("%d", secs)
1247}
1248
1249func avatate(w http.ResponseWriter, r *http.Request) {
1250	n := r.FormValue("a")
1251	a := avatar(n)
1252	w.Header().Set("Cache-Control", "max-age="+somedays())
1253	w.Write(a)
1254}
1255
1256func servecss(w http.ResponseWriter, r *http.Request) {
1257	w.Header().Set("Cache-Control", "max-age=7776000")
1258	http.ServeFile(w, r, "views"+r.URL.Path)
1259}
1260func servehtml(w http.ResponseWriter, r *http.Request) {
1261	templinfo := getInfo(r)
1262	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1263	if err != nil {
1264		log.Print(err)
1265	}
1266}
1267func serveemu(w http.ResponseWriter, r *http.Request) {
1268	xid := mux.Vars(r)["xid"]
1269	w.Header().Set("Cache-Control", "max-age="+somedays())
1270	http.ServeFile(w, r, "emus/"+xid)
1271}
1272func servememe(w http.ResponseWriter, r *http.Request) {
1273	xid := mux.Vars(r)["xid"]
1274	w.Header().Set("Cache-Control", "max-age="+somedays())
1275	http.ServeFile(w, r, "memes/"+xid)
1276}
1277
1278func servefile(w http.ResponseWriter, r *http.Request) {
1279	xid := mux.Vars(r)["xid"]
1280	row := stmtFileData.QueryRow(xid)
1281	var media string
1282	var data []byte
1283	err := row.Scan(&media, &data)
1284	if err != nil {
1285		log.Printf("error loading file: %s", err)
1286		http.NotFound(w, r)
1287		return
1288	}
1289	w.Header().Set("Content-Type", media)
1290	w.Header().Set("X-Content-Type-Options", "nosniff")
1291	w.Header().Set("Cache-Control", "max-age="+somedays())
1292	w.Write(data)
1293}
1294
1295func serve() {
1296	db := opendatabase()
1297	login.Init(db)
1298
1299	listener, err := openListener()
1300	if err != nil {
1301		log.Fatal(err)
1302	}
1303	go redeliverator()
1304
1305	debug := false
1306	getconfig("debug", &debug)
1307	readviews = templates.Load(debug,
1308		"views/honkpage.html",
1309		"views/honkers.html",
1310		"views/zonkers.html",
1311		"views/combos.html",
1312		"views/honkform.html",
1313		"views/honk.html",
1314		"views/account.html",
1315		"views/about.html",
1316		"views/login.html",
1317		"views/xzone.html",
1318		"views/header.html",
1319	)
1320	if !debug {
1321		s := "views/style.css"
1322		savedstyleparams[s] = getstyleparam(s)
1323		s = "views/local.css"
1324		savedstyleparams[s] = getstyleparam(s)
1325	}
1326
1327	bitethethumbs()
1328
1329	mux := mux.NewRouter()
1330	mux.Use(login.Checker)
1331
1332	posters := mux.Methods("POST").Subrouter()
1333	getters := mux.Methods("GET").Subrouter()
1334
1335	getters.HandleFunc("/", homepage)
1336	getters.HandleFunc("/rss", showrss)
1337	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1338	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1339	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1340	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1341	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1342	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1343	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1344	getters.HandleFunc("/a", avatate)
1345	getters.HandleFunc("/t", showconvoy)
1346	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1347	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1348	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1349	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1350
1351	getters.HandleFunc("/style.css", servecss)
1352	getters.HandleFunc("/local.css", servecss)
1353	getters.HandleFunc("/about", servehtml)
1354	getters.HandleFunc("/login", servehtml)
1355	posters.HandleFunc("/dologin", login.LoginFunc)
1356	getters.HandleFunc("/logout", login.LogoutFunc)
1357
1358	loggedin := mux.NewRoute().Subrouter()
1359	loggedin.Use(login.Required)
1360	loggedin.HandleFunc("/account", accountpage)
1361	loggedin.HandleFunc("/chpass", dochpass)
1362	loggedin.HandleFunc("/atme", homepage)
1363	loggedin.HandleFunc("/zonkzone", zonkzone)
1364	loggedin.HandleFunc("/xzone", xzone)
1365	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1366	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1367	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1368	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1369	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1370	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1371	loggedin.HandleFunc("/honkers", showhonkers)
1372	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1373	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1374	loggedin.HandleFunc("/c", showcombos)
1375	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1376
1377	err = http.Serve(listener, mux)
1378	if err != nil {
1379		log.Fatal(err)
1380	}
1381}
1382
1383func cleanupdb(days int) {
1384	db := opendatabase()
1385	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1386	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)
1387	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)
1388	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1389}
1390
1391func reducedb(honker string) {
1392	db := opendatabase()
1393	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1394	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1395	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1396	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1397}
1398
1399var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1400var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1401var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1402var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1403var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1404var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1405var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1406var stmtGetXonker, stmtSaveXonker *sql.Stmt
1407
1408func preparetodie(db *sql.DB, s string) *sql.Stmt {
1409	stmt, err := db.Prepare(s)
1410	if err != nil {
1411		log.Fatalf("error %s: %s", err, s)
1412	}
1413	return stmt
1414}
1415
1416func prepareStatements(db *sql.DB) {
1417	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")
1418	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1419	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1420	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1421	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1422	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1423
1424	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 "
1425	limit := " order by honkid desc limit 250"
1426	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1427	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1428	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1429	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1430	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)
1431	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1432	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1433	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1434	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1435
1436	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1437	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1438	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1439	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1440	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1441	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1442	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1443	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1444	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1445	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1446	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1447	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1448	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1449	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1450	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1451	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1452	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1453	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1454}
1455
1456func ElaborateUnitTests() {
1457}
1458
1459func main() {
1460	var err error
1461	cmd := "run"
1462	if len(os.Args) > 1 {
1463		cmd = os.Args[1]
1464	}
1465	switch cmd {
1466	case "init":
1467		initdb()
1468	case "upgrade":
1469		upgradedb()
1470	}
1471	db := opendatabase()
1472	dbversion := 0
1473	getconfig("dbversion", &dbversion)
1474	if dbversion != myVersion {
1475		log.Fatal("incorrect database version. run upgrade.")
1476	}
1477	getconfig("servermsg", &serverMsg)
1478	getconfig("servername", &serverName)
1479	prepareStatements(db)
1480	switch cmd {
1481	case "adduser":
1482		adduser()
1483	case "cleanup":
1484		days := 30
1485		if len(os.Args) > 2 {
1486			days, err = strconv.Atoi(os.Args[2])
1487			if err != nil {
1488				log.Fatal(err)
1489			}
1490		}
1491		cleanupdb(days)
1492	case "reduce":
1493		if len(os.Args) < 3 {
1494			log.Fatal("need a honker name")
1495		}
1496		reducedb(os.Args[2])
1497	case "ping":
1498		if len(os.Args) < 4 {
1499			fmt.Printf("usage: honk ping from to\n")
1500			return
1501		}
1502		name := os.Args[2]
1503		targ := os.Args[3]
1504		user, err := butwhatabout(name)
1505		if err != nil {
1506			log.Printf("unknown user")
1507			return
1508		}
1509		ping(user, targ)
1510	case "peep":
1511		peeppeep()
1512	case "run":
1513		serve()
1514	case "test":
1515		ElaborateUnitTests()
1516	default:
1517		log.Fatal("unknown command")
1518	}
1519}