all repos — honk @ 595a894f90cc59a027e119c0dd057ab8b61e007d

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