all repos — honk @ 625f4286a19223550b5cae699872f872c4519b27

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