all repos — honk @ a8d8566bcbd8a2accf8c847caab661f9968c6da4

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 r.URL.Path == "/front" || u == nil {
 115		honks = getpublichonks()
 116	} else {
 117		if r.URL.Path == "/atme" {
 118			honks = gethonksforme(u.UserID)
 119		} else {
 120			honks = gethonksforuser(u.UserID)
 121			honks = osmosis(honks, u.UserID)
 122		}
 123		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 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	honks = osmosis(honks, u.UserID)
 505	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 506}
 507func showconvoy(w http.ResponseWriter, r *http.Request) {
 508	c := r.FormValue("c")
 509	var userid int64 = -1
 510	u := login.GetUserInfo(r)
 511	if u != nil {
 512		userid = u.UserID
 513	}
 514	honks := gethonksbyconvoy(userid, c)
 515	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 516}
 517
 518func showhonk(w http.ResponseWriter, r *http.Request) {
 519	name := mux.Vars(r)["name"]
 520	user, err := butwhatabout(name)
 521	if err != nil {
 522		http.NotFound(w, r)
 523		return
 524	}
 525	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 526	h := getxonk(user.ID, xid)
 527	if h == nil || !h.Public {
 528		http.NotFound(w, r)
 529		return
 530	}
 531	if friendorfoe(r.Header.Get("Accept")) {
 532		donksforhonks([]*Honk{h})
 533		_, j := jonkjonk(user, h)
 534		j["@context"] = itiswhatitis
 535		w.Header().Set("Cache-Control", "max-age=3600")
 536		w.Header().Set("Content-Type", theonetruename)
 537		j.Write(w)
 538		return
 539	}
 540	honks := gethonksbyconvoy(-1, h.Convoy)
 541	u := login.GetUserInfo(r)
 542	honkpage(w, r, u, nil, honks, "one honk maybe more")
 543}
 544
 545func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 546	honks []*Honk, infomsg string) {
 547	reverbolate(honks)
 548	templinfo := getInfo(r)
 549	if u != nil {
 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 "zomain":
1182	case "zonvoy":
1183	case "zord":
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 == "zomain" || wherefore == "zord" {
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 nomoroboto(w http.ResponseWriter, r *http.Request) {
1319	io.WriteString(w, "User-agent: *\n")
1320	io.WriteString(w, "Disallow: /t\n")
1321	for _, u := range allusers() {
1322		fmt.Fprintf(w, "Disallow: /u/%s/h/\n", u.Username)
1323	}
1324}
1325
1326func serve() {
1327	db := opendatabase()
1328	login.Init(db)
1329
1330	listener, err := openListener()
1331	if err != nil {
1332		log.Fatal(err)
1333	}
1334	go redeliverator()
1335
1336	debug := false
1337	getconfig("debug", &debug)
1338	readviews = templates.Load(debug,
1339		"views/honkpage.html",
1340		"views/honkers.html",
1341		"views/zonkers.html",
1342		"views/combos.html",
1343		"views/honkform.html",
1344		"views/honk.html",
1345		"views/account.html",
1346		"views/about.html",
1347		"views/login.html",
1348		"views/xzone.html",
1349		"views/header.html",
1350	)
1351	if !debug {
1352		s := "views/style.css"
1353		savedstyleparams[s] = getstyleparam(s)
1354		s = "views/local.css"
1355		savedstyleparams[s] = getstyleparam(s)
1356	}
1357
1358	bitethethumbs()
1359
1360	mux := mux.NewRouter()
1361	mux.Use(login.Checker)
1362
1363	posters := mux.Methods("POST").Subrouter()
1364	getters := mux.Methods("GET").Subrouter()
1365
1366	getters.HandleFunc("/", homepage)
1367	getters.HandleFunc("/front", homepage)
1368	getters.HandleFunc("/robots.txt", nomoroboto)
1369	getters.HandleFunc("/rss", showrss)
1370	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1371	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1372	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1373	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1374	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1375	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1376	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1377	getters.HandleFunc("/a", avatate)
1378	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1379	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1380	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1381	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1382
1383	getters.HandleFunc("/style.css", servecss)
1384	getters.HandleFunc("/local.css", servecss)
1385	getters.HandleFunc("/about", servehtml)
1386	getters.HandleFunc("/login", servehtml)
1387	posters.HandleFunc("/dologin", login.LoginFunc)
1388	getters.HandleFunc("/logout", login.LogoutFunc)
1389
1390	loggedin := mux.NewRoute().Subrouter()
1391	loggedin.Use(login.Required)
1392	loggedin.HandleFunc("/account", accountpage)
1393	loggedin.HandleFunc("/chpass", dochpass)
1394	loggedin.HandleFunc("/atme", homepage)
1395	loggedin.HandleFunc("/zonkzone", zonkzone)
1396	loggedin.HandleFunc("/xzone", xzone)
1397	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1398	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1399	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1400	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1401	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1402	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1403	loggedin.HandleFunc("/honkers", showhonkers)
1404	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1405	loggedin.HandleFunc("/h", showhonker)
1406	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1407	loggedin.HandleFunc("/c", showcombos)
1408	loggedin.HandleFunc("/t", showconvoy)
1409	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1410
1411	err = http.Serve(listener, mux)
1412	if err != nil {
1413		log.Fatal(err)
1414	}
1415}
1416
1417func cleanupdb(days int) {
1418	db := opendatabase()
1419	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1420	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)
1421	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)
1422	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1423}
1424
1425func reducedb(honker string) {
1426	db := opendatabase()
1427	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1428	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1429	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1430	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1431}
1432
1433var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1434var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1435var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1436var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1437var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1438var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1439var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1440var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker *sql.Stmt
1441
1442func preparetodie(db *sql.DB, s string) *sql.Stmt {
1443	stmt, err := db.Prepare(s)
1444	if err != nil {
1445		log.Fatalf("error %s: %s", err, s)
1446	}
1447	return stmt
1448}
1449
1450func prepareStatements(db *sql.DB) {
1451	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")
1452	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1453	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1454	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1455	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1456	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1457
1458	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 "
1459	limit := " order by honkid desc limit 250"
1460	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1461	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1462	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1463	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1464	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)
1465	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1466	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1467	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and honker = ?"+butnotthose+limit)
1468	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1469	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1470
1471	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1472	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1473	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1474	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1475	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1476	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1477	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1478	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1479	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1480	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1481	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1482	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1483	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1484	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1485	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord')")
1486	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1487	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
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}