all repos — honk @ 0d1335deb0f126d81d2bc2a0b2d69b4c39b087db

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