all repos — honk @ e8daca31ac4af4da80f77ca12d84149555db9915

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"
  96
  97var readviews *templates.Template
  98
  99func getInfo(r *http.Request) map[string]interface{} {
 100	templinfo := make(map[string]interface{})
 101	templinfo["StyleParam"] = getstyleparam("views/style.css")
 102	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 103	templinfo["ServerName"] = serverName
 104	templinfo["IconName"] = iconName
 105	templinfo["UserInfo"] = login.GetUserInfo(r)
 106	return templinfo
 107}
 108
 109func homepage(w http.ResponseWriter, r *http.Request) {
 110	templinfo := getInfo(r)
 111	u := login.GetUserInfo(r)
 112	var honks []*Honk
 113	if u != nil {
 114		if r.URL.Path == "/atme" {
 115			honks = gethonksforme(u.UserID)
 116		} else {
 117			honks = gethonksforuser(u.UserID)
 118			honks = osmosis(honks, u.UserID)
 119		}
 120		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 121	} else {
 122		honks = getpublichonks()
 123	}
 124
 125	var modtime time.Time
 126	if len(honks) > 0 {
 127		modtime = honks[0].Date
 128	}
 129	debug := false
 130	getconfig("debug", &debug)
 131	imh := r.Header.Get("If-Modified-Since")
 132	if !debug && imh != "" && !modtime.IsZero() {
 133		ifmod, err := time.Parse(http.TimeFormat, imh)
 134		if err == nil && !modtime.After(ifmod) {
 135			w.WriteHeader(http.StatusNotModified)
 136			return
 137		}
 138	}
 139	reverbolate(honks)
 140
 141	msg := "Things happen."
 142	getconfig("servermsg", &msg)
 143	templinfo["Honks"] = honks
 144	templinfo["ShowRSS"] = true
 145	templinfo["ServerMessage"] = msg
 146	if u == nil {
 147		w.Header().Set("Cache-Control", "max-age=60")
 148	} else {
 149		w.Header().Set("Cache-Control", "max-age=0")
 150	}
 151	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 152	err := readviews.Execute(w, "honkpage.html", templinfo)
 153	if err != nil {
 154		log.Print(err)
 155	}
 156}
 157
 158func showrss(w http.ResponseWriter, r *http.Request) {
 159	name := mux.Vars(r)["name"]
 160
 161	var honks []*Honk
 162	if name != "" {
 163		honks = gethonksbyuser(name, false)
 164	} else {
 165		honks = getpublichonks()
 166	}
 167	reverbolate(honks)
 168
 169	home := fmt.Sprintf("https://%s/", serverName)
 170	base := home
 171	if name != "" {
 172		home += "u/" + name
 173		name += " "
 174	}
 175	feed := rss.Feed{
 176		Title:       name + "honk",
 177		Link:        home,
 178		Description: name + "honk rss",
 179		Image: &rss.Image{
 180			URL:   base + "icon.png",
 181			Title: name + "honk rss",
 182			Link:  home,
 183		},
 184	}
 185	var modtime time.Time
 186	for _, honk := range honks {
 187		desc := string(honk.HTML)
 188		for _, d := range honk.Donks {
 189			desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 190				d.URL, html.EscapeString(d.Name))
 191		}
 192
 193		feed.Items = append(feed.Items, &rss.Item{
 194			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 195			Description: rss.CData{desc},
 196			Link:        honk.URL,
 197			PubDate:     honk.Date.Format(time.RFC1123),
 198			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 199		})
 200		if honk.Date.After(modtime) {
 201			modtime = honk.Date
 202		}
 203	}
 204	w.Header().Set("Cache-Control", "max-age=300")
 205	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 206
 207	err := feed.Write(w)
 208	if err != nil {
 209		log.Printf("error writing rss: %s", err)
 210	}
 211}
 212
 213func butwhatabout(name string) (*WhatAbout, error) {
 214	row := stmtWhatAbout.QueryRow(name)
 215	var user WhatAbout
 216	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key)
 217	user.URL = fmt.Sprintf("https://%s/u/%s", serverName, user.Name)
 218	return &user, err
 219}
 220
 221func crappola(j junk.Junk) bool {
 222	t, _ := j.GetString("type")
 223	a, _ := j.GetString("actor")
 224	o, _ := j.GetString("object")
 225	if t == "Delete" && a == o {
 226		log.Printf("crappola from %s", a)
 227		return true
 228	}
 229	return false
 230}
 231
 232func ping(user *WhatAbout, who string) {
 233	box, err := getboxes(who)
 234	if err != nil {
 235		log.Printf("no inbox for ping: %s", err)
 236		return
 237	}
 238	j := junk.New()
 239	j["@context"] = itiswhatitis
 240	j["type"] = "Ping"
 241	j["id"] = user.URL + "/ping/" + xfiltrate()
 242	j["actor"] = user.URL
 243	j["to"] = who
 244	keyname, key := ziggy(user.Name)
 245	err = PostJunk(keyname, key, box.In, j)
 246	if err != nil {
 247		log.Printf("can't send ping: %s", err)
 248		return
 249	}
 250	log.Printf("sent ping to %s: %s", who, j["id"])
 251}
 252
 253func pong(user *WhatAbout, who string, obj string) {
 254	box, err := getboxes(who)
 255	if err != nil {
 256		log.Printf("no inbox for pong %s : %s", who, err)
 257		return
 258	}
 259	j := junk.New()
 260	j["@context"] = itiswhatitis
 261	j["type"] = "Pong"
 262	j["id"] = user.URL + "/pong/" + xfiltrate()
 263	j["actor"] = user.URL
 264	j["to"] = who
 265	j["object"] = obj
 266	keyname, key := ziggy(user.Name)
 267	err = PostJunk(keyname, key, box.In, j)
 268	if err != nil {
 269		log.Printf("can't send pong: %s", err)
 270		return
 271	}
 272}
 273
 274func inbox(w http.ResponseWriter, r *http.Request) {
 275	name := mux.Vars(r)["name"]
 276	user, err := butwhatabout(name)
 277	if err != nil {
 278		http.NotFound(w, r)
 279		return
 280	}
 281	var buf bytes.Buffer
 282	io.Copy(&buf, r.Body)
 283	payload := buf.Bytes()
 284	j, err := junk.Read(bytes.NewReader(payload))
 285	if err != nil {
 286		log.Printf("bad payload: %s", err)
 287		io.WriteString(os.Stdout, "bad payload\n")
 288		os.Stdout.Write(payload)
 289		io.WriteString(os.Stdout, "\n")
 290		return
 291	}
 292	if crappola(j) {
 293		return
 294	}
 295	keyname, err := zag(r, payload)
 296	if err != nil {
 297		log.Printf("inbox message failed signature: %s", err)
 298		if keyname != "" {
 299			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 300		}
 301		if err != nil {
 302			return
 303		}
 304	}
 305	what, _ := j.GetString("type")
 306	if what == "Like" {
 307		return
 308	}
 309	who, _ := j.GetString("actor")
 310	origin := keymatch(keyname, who)
 311	if origin == "" {
 312		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 313		return
 314	}
 315	objid, _ := j.GetString("id")
 316	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 317		log.Printf("ignoring thumb sucker %s", who)
 318		return
 319	}
 320	switch what {
 321	case "Ping":
 322		obj, _ := j.GetString("id")
 323		log.Printf("ping from %s: %s", who, obj)
 324		pong(user, who, obj)
 325	case "Pong":
 326		obj, _ := j.GetString("object")
 327		log.Printf("pong from %s: %s", who, obj)
 328	case "Follow":
 329		obj, _ := j.GetString("object")
 330		if obj == user.URL {
 331			log.Printf("updating honker follow: %s", who)
 332			stmtSaveDub.Exec(user.ID, who, who, "dub")
 333			go rubadubdub(user, j)
 334		} else {
 335			log.Printf("can't follow %s", obj)
 336		}
 337	case "Accept":
 338		log.Printf("updating honker accept: %s", who)
 339		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 340		if err != nil {
 341			log.Printf("error updating honker: %s", err)
 342			return
 343		}
 344	case "Update":
 345		obj, ok := j.GetMap("object")
 346		if ok {
 347			what, _ := obj.GetString("type")
 348			switch what {
 349			case "Person":
 350				return
 351			}
 352		}
 353		log.Printf("unknown Update activity")
 354		fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 355		j.Write(fd)
 356		io.WriteString(fd, "\n")
 357		fd.Close()
 358
 359	case "Undo":
 360		obj, ok := j.GetMap("object")
 361		if !ok {
 362			log.Printf("unknown undo no object")
 363		} else {
 364			what, _ := obj.GetString("type")
 365			switch what {
 366			case "Follow":
 367				log.Printf("updating honker undo: %s", who)
 368				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 369				if err != nil {
 370					log.Printf("error updating honker: %s", err)
 371					return
 372				}
 373			case "Like":
 374			case "Announce":
 375			default:
 376				log.Printf("unknown undo: %s", what)
 377			}
 378		}
 379	default:
 380		go consumeactivity(user, j, origin)
 381	}
 382}
 383
 384func ximport(w http.ResponseWriter, r *http.Request) {
 385	xid := r.FormValue("xid")
 386	j, err := GetJunk(xid)
 387	if err != nil {
 388		log.Printf("error getting external object: %s", err)
 389		return
 390	}
 391	u := login.GetUserInfo(r)
 392	user, _ := butwhatabout(u.Username)
 393	xonk := xonkxonk(user, j, originate(xid))
 394	convoy := ""
 395	if xonk != nil {
 396		convoy = xonk.Convoy
 397		savexonk(user, xonk)
 398	}
 399	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 400}
 401
 402func xzone(w http.ResponseWriter, r *http.Request) {
 403	templinfo := getInfo(r)
 404	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 405	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
 406	if err != nil {
 407		log.Print(err)
 408	}
 409}
 410func outbox(w http.ResponseWriter, r *http.Request) {
 411	name := mux.Vars(r)["name"]
 412	user, err := butwhatabout(name)
 413	if err != nil {
 414		http.NotFound(w, r)
 415		return
 416	}
 417	honks := gethonksbyuser(name, false)
 418
 419	var jonks []map[string]interface{}
 420	for _, h := range honks {
 421		j, _ := jonkjonk(user, h)
 422		jonks = append(jonks, j)
 423	}
 424
 425	j := junk.New()
 426	j["@context"] = itiswhatitis
 427	j["id"] = user.URL + "/outbox"
 428	j["type"] = "OrderedCollection"
 429	j["totalItems"] = len(jonks)
 430	j["orderedItems"] = jonks
 431
 432	w.Header().Set("Cache-Control", "max-age=60")
 433	w.Header().Set("Content-Type", theonetruename)
 434	j.Write(w)
 435}
 436
 437func emptiness(w http.ResponseWriter, r *http.Request) {
 438	name := mux.Vars(r)["name"]
 439	user, err := butwhatabout(name)
 440	if err != nil {
 441		http.NotFound(w, r)
 442		return
 443	}
 444	colname := "/followers"
 445	if strings.HasSuffix(r.URL.Path, "/following") {
 446		colname = "/following"
 447	}
 448	j := junk.New()
 449	j["@context"] = itiswhatitis
 450	j["id"] = user.URL + colname
 451	j["type"] = "OrderedCollection"
 452	j["totalItems"] = 0
 453	j["orderedItems"] = []interface{}{}
 454
 455	w.Header().Set("Cache-Control", "max-age=60")
 456	w.Header().Set("Content-Type", theonetruename)
 457	j.Write(w)
 458}
 459
 460func showuser(w http.ResponseWriter, r *http.Request) {
 461	name := mux.Vars(r)["name"]
 462	user, err := butwhatabout(name)
 463	if err != nil {
 464		http.NotFound(w, r)
 465		return
 466	}
 467	if friendorfoe(r.Header.Get("Accept")) {
 468		j := asjonker(user)
 469		w.Header().Set("Cache-Control", "max-age=600")
 470		w.Header().Set("Content-Type", theonetruename)
 471		j.Write(w)
 472		return
 473	}
 474	u := login.GetUserInfo(r)
 475	honks := gethonksbyuser(name, u != nil && u.Username == name)
 476	honkpage(w, r, u, user, honks, "")
 477}
 478
 479func showhonker(w http.ResponseWriter, r *http.Request) {
 480	name := mux.Vars(r)["name"]
 481	u := login.GetUserInfo(r)
 482	honks := gethonksbyhonker(u.UserID, name)
 483	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 484}
 485
 486func showcombo(w http.ResponseWriter, r *http.Request) {
 487	name := mux.Vars(r)["name"]
 488	u := login.GetUserInfo(r)
 489	honks := gethonksbycombo(u.UserID, name)
 490	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 491}
 492func showconvoy(w http.ResponseWriter, r *http.Request) {
 493	c := r.FormValue("c")
 494	var userid int64 = -1
 495	u := login.GetUserInfo(r)
 496	if u != nil {
 497		userid = u.UserID
 498	}
 499	honks := gethonksbyconvoy(userid, c)
 500	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 501}
 502
 503func showhonk(w http.ResponseWriter, r *http.Request) {
 504	name := mux.Vars(r)["name"]
 505	user, err := butwhatabout(name)
 506	if err != nil {
 507		http.NotFound(w, r)
 508		return
 509	}
 510	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 511	h := getxonk(user.ID, xid)
 512	if h == nil || !h.Public {
 513		http.NotFound(w, r)
 514		return
 515	}
 516	if friendorfoe(r.Header.Get("Accept")) {
 517		donksforhonks([]*Honk{h})
 518		_, j := jonkjonk(user, h)
 519		j["@context"] = itiswhatitis
 520		w.Header().Set("Cache-Control", "max-age=3600")
 521		w.Header().Set("Content-Type", theonetruename)
 522		j.Write(w)
 523		return
 524	}
 525	honks := gethonksbyconvoy(-1, h.Convoy)
 526	u := login.GetUserInfo(r)
 527	honkpage(w, r, u, nil, honks, "one honk maybe more")
 528}
 529
 530func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 531	honks []*Honk, infomsg string) {
 532	reverbolate(honks)
 533	templinfo := getInfo(r)
 534	if u != nil {
 535		honks = osmosis(honks, u.UserID)
 536		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 537	}
 538	if u == nil {
 539		w.Header().Set("Cache-Control", "max-age=60")
 540	}
 541	if user != nil {
 542		filt := htfilter.New()
 543		templinfo["Name"] = user.Name
 544		whatabout := user.About
 545		whatabout = obfusbreak(user.About)
 546		templinfo["WhatAbout"], _ = filt.String(whatabout)
 547	}
 548	templinfo["Honks"] = honks
 549	templinfo["ServerMessage"] = infomsg
 550	err := readviews.Execute(w, "honkpage.html", templinfo)
 551	if err != nil {
 552		log.Print(err)
 553	}
 554}
 555
 556func saveuser(w http.ResponseWriter, r *http.Request) {
 557	whatabout := r.FormValue("whatabout")
 558	u := login.GetUserInfo(r)
 559	db := opendatabase()
 560	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 561	if err != nil {
 562		log.Printf("error bouting what: %s", err)
 563	}
 564
 565	http.Redirect(w, r, "/account", http.StatusSeeOther)
 566}
 567
 568func gethonkers(userid int64) []*Honker {
 569	rows, err := stmtHonkers.Query(userid)
 570	if err != nil {
 571		log.Printf("error querying honkers: %s", err)
 572		return nil
 573	}
 574	defer rows.Close()
 575	var honkers []*Honker
 576	for rows.Next() {
 577		var f Honker
 578		var combos string
 579		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 580		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 581		if err != nil {
 582			log.Printf("error scanning honker: %s", err)
 583			return nil
 584		}
 585		honkers = append(honkers, &f)
 586	}
 587	return honkers
 588}
 589
 590func getdubs(userid int64) []*Honker {
 591	rows, err := stmtDubbers.Query(userid)
 592	if err != nil {
 593		log.Printf("error querying dubs: %s", err)
 594		return nil
 595	}
 596	defer rows.Close()
 597	var honkers []*Honker
 598	for rows.Next() {
 599		var f Honker
 600		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 601		if err != nil {
 602			log.Printf("error scanning honker: %s", err)
 603			return nil
 604		}
 605		honkers = append(honkers, &f)
 606	}
 607	return honkers
 608}
 609
 610func allusers() []login.UserInfo {
 611	var users []login.UserInfo
 612	rows, _ := opendatabase().Query("select userid, username from users")
 613	defer rows.Close()
 614	for rows.Next() {
 615		var u login.UserInfo
 616		rows.Scan(&u.UserID, &u.Username)
 617		users = append(users, u)
 618	}
 619	return users
 620}
 621
 622func getxonk(userid int64, xid string) *Honk {
 623	h := new(Honk)
 624	var dt, aud string
 625	row := stmtOneXonk.QueryRow(userid, xid)
 626	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 627		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 628	if err != nil {
 629		if err != sql.ErrNoRows {
 630			log.Printf("error scanning xonk: %s", err)
 631		}
 632		return nil
 633	}
 634	h.Date, _ = time.Parse(dbtimeformat, dt)
 635	h.Audience = strings.Split(aud, " ")
 636	h.Public = !keepitquiet(h.Audience)
 637	return h
 638}
 639
 640func getpublichonks() []*Honk {
 641	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 642	rows, err := stmtPublicHonks.Query(dt)
 643	return getsomehonks(rows, err)
 644}
 645func gethonksbyuser(name string, includeprivate bool) []*Honk {
 646	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 647	whofore := 2
 648	if includeprivate {
 649		whofore = 3
 650	}
 651	rows, err := stmtUserHonks.Query(whofore, name, dt)
 652	return getsomehonks(rows, err)
 653}
 654func gethonksforuser(userid int64) []*Honk {
 655	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 656	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 657	return getsomehonks(rows, err)
 658}
 659func gethonksforme(userid int64) []*Honk {
 660	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 661	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 662	return getsomehonks(rows, err)
 663}
 664func gethonksbyhonker(userid int64, honker string) []*Honk {
 665	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 666	return getsomehonks(rows, err)
 667}
 668func gethonksbycombo(userid int64, combo string) []*Honk {
 669	combo = "% " + combo + " %"
 670	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 671	return getsomehonks(rows, err)
 672}
 673func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 674	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 675	honks := getsomehonks(rows, err)
 676	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 677		honks[i], honks[j] = honks[j], honks[i]
 678	}
 679	return honks
 680}
 681
 682func getsomehonks(rows *sql.Rows, err error) []*Honk {
 683	if err != nil {
 684		log.Printf("error querying honks: %s", err)
 685		return nil
 686	}
 687	defer rows.Close()
 688	var honks []*Honk
 689	for rows.Next() {
 690		var h Honk
 691		var dt, aud string
 692		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 693			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 694		if err != nil {
 695			log.Printf("error scanning honks: %s", err)
 696			return nil
 697		}
 698		h.Date, _ = time.Parse(dbtimeformat, dt)
 699		h.Audience = strings.Split(aud, " ")
 700		h.Public = !keepitquiet(h.Audience)
 701		honks = append(honks, &h)
 702	}
 703	rows.Close()
 704	donksforhonks(honks)
 705	return honks
 706}
 707
 708func donksforhonks(honks []*Honk) {
 709	db := opendatabase()
 710	var ids []string
 711	hmap := make(map[int64]*Honk)
 712	for _, h := range honks {
 713		ids = append(ids, fmt.Sprintf("%d", h.ID))
 714		hmap[h.ID] = h
 715	}
 716	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, ","))
 717	rows, err := db.Query(q)
 718	if err != nil {
 719		log.Printf("error querying donks: %s", err)
 720		return
 721	}
 722	defer rows.Close()
 723	for rows.Next() {
 724		var hid int64
 725		var d Donk
 726		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 727		if err != nil {
 728			log.Printf("error scanning donk: %s", err)
 729			continue
 730		}
 731		h := hmap[hid]
 732		h.Donks = append(h.Donks, &d)
 733	}
 734}
 735
 736func savebonk(w http.ResponseWriter, r *http.Request) {
 737	xid := r.FormValue("xid")
 738	userinfo := login.GetUserInfo(r)
 739	user, _ := butwhatabout(userinfo.Username)
 740
 741	log.Printf("bonking %s", xid)
 742
 743	xonk := getxonk(userinfo.UserID, xid)
 744	if xonk == nil {
 745		return
 746	}
 747	if !xonk.Public {
 748		return
 749	}
 750	donksforhonks([]*Honk{xonk})
 751
 752	dt := time.Now().UTC()
 753	bonk := Honk{
 754		UserID:   userinfo.UserID,
 755		Username: userinfo.Username,
 756		What:     "bonk",
 757		Honker:   user.URL,
 758		XID:      xonk.XID,
 759		Date:     dt,
 760		Donks:    xonk.Donks,
 761		Audience: []string{thewholeworld},
 762		Public:   true,
 763	}
 764
 765	oonker := xonk.Oonker
 766	if oonker == "" {
 767		oonker = xonk.Honker
 768	}
 769	aud := strings.Join(bonk.Audience, " ")
 770	whofore := 2
 771	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 772		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 773		xonk.Precis, oonker)
 774	if err != nil {
 775		log.Printf("error saving bonk: %s", err)
 776		return
 777	}
 778	bonk.ID, _ = res.LastInsertId()
 779	for _, d := range bonk.Donks {
 780		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 781		if err != nil {
 782			log.Printf("err saving donk: %s", err)
 783			return
 784		}
 785	}
 786
 787	go honkworldwide(user, &bonk)
 788}
 789
 790func zonkit(w http.ResponseWriter, r *http.Request) {
 791	wherefore := r.FormValue("wherefore")
 792	var what string
 793	switch wherefore {
 794	case "this honk":
 795		what = r.FormValue("honk")
 796		wherefore = "zonk"
 797	case "this honker":
 798		what = r.FormValue("honker")
 799		wherefore = "zonker"
 800	case "this convoy":
 801		what = r.FormValue("convoy")
 802		wherefore = "zonvoy"
 803	}
 804	if what == "" {
 805		return
 806	}
 807
 808	log.Printf("zonking %s %s", wherefore, what)
 809	userinfo := login.GetUserInfo(r)
 810	if wherefore == "zonk" {
 811		xonk := getxonk(userinfo.UserID, what)
 812		if xonk != nil {
 813			stmtZonkDonks.Exec(xonk.ID)
 814			stmtZonkIt.Exec(userinfo.UserID, what)
 815			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 816				zonk := Honk{
 817					What:     "zonk",
 818					XID:      xonk.XID,
 819					Date:     time.Now().UTC(),
 820					Audience: oneofakind(xonk.Audience),
 821				}
 822
 823				user, _ := butwhatabout(userinfo.Username)
 824				log.Printf("announcing deleted honk: %s", what)
 825				go honkworldwide(user, &zonk)
 826			}
 827		}
 828	} else {
 829		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 830		if err != nil {
 831			log.Printf("error saving zonker: %s", err)
 832			return
 833		}
 834	}
 835}
 836
 837func savehonk(w http.ResponseWriter, r *http.Request) {
 838	rid := r.FormValue("rid")
 839	noise := r.FormValue("noise")
 840
 841	userinfo := login.GetUserInfo(r)
 842	user, _ := butwhatabout(userinfo.Username)
 843
 844	dt := time.Now().UTC()
 845	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 846	what := "honk"
 847	if rid != "" {
 848		what = "tonk"
 849	}
 850	honk := Honk{
 851		UserID:   userinfo.UserID,
 852		Username: userinfo.Username,
 853		What:     "honk",
 854		Honker:   user.URL,
 855		XID:      xid,
 856		Date:     dt,
 857	}
 858	if strings.HasPrefix(noise, "DZ:") {
 859		idx := strings.Index(noise, "\n")
 860		if idx == -1 {
 861			honk.Precis = noise
 862			noise = ""
 863		} else {
 864			honk.Precis = noise[:idx]
 865			noise = noise[idx+1:]
 866		}
 867	}
 868	noise = hooterize(noise)
 869	noise = strings.TrimSpace(noise)
 870	honk.Precis = strings.TrimSpace(honk.Precis)
 871
 872	var convoy string
 873	if rid != "" {
 874		xonk := getxonk(userinfo.UserID, rid)
 875		if xonk != nil {
 876			if xonk.Public {
 877				honk.Audience = append(honk.Audience, xonk.Audience...)
 878			}
 879			convoy = xonk.Convoy
 880		} else {
 881			xonkaud, c := whosthere(rid)
 882			honk.Audience = append(honk.Audience, xonkaud...)
 883			convoy = c
 884		}
 885		for i, a := range honk.Audience {
 886			if a == thewholeworld {
 887				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 888				break
 889			}
 890		}
 891		honk.RID = rid
 892	} else {
 893		honk.Audience = []string{thewholeworld}
 894	}
 895	if noise != "" && noise[0] == '@' {
 896		honk.Audience = append(grapevine(noise), honk.Audience...)
 897	} else {
 898		honk.Audience = append(honk.Audience, grapevine(noise)...)
 899	}
 900	if convoy == "" {
 901		convoy = "data:,electrichonkytonk-" + xfiltrate()
 902	}
 903	butnottooloud(honk.Audience)
 904	honk.Audience = oneofakind(honk.Audience)
 905	if len(honk.Audience) == 0 {
 906		log.Printf("honk to nowhere")
 907		return
 908	}
 909	honk.Public = !keepitquiet(honk.Audience)
 910	noise = obfusbreak(noise)
 911	honk.Noise = noise
 912	honk.Convoy = convoy
 913
 914	file, filehdr, err := r.FormFile("donk")
 915	if err == nil {
 916		var buf bytes.Buffer
 917		io.Copy(&buf, file)
 918		file.Close()
 919		data := buf.Bytes()
 920		xid := xfiltrate()
 921		var media, name string
 922		img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
 923		if err == nil {
 924			data = img.Data
 925			format := img.Format
 926			media = "image/" + format
 927			if format == "jpeg" {
 928				format = "jpg"
 929			}
 930			name = xid + "." + format
 931			xid = name
 932		} else {
 933			maxsize := 100000
 934			if len(data) > maxsize {
 935				log.Printf("bad image: %s too much text: %d", err, len(data))
 936				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 937				return
 938			}
 939			for i := 0; i < len(data); i++ {
 940				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 941					log.Printf("bad image: %s not text: %d", err, data[i])
 942					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 943					return
 944				}
 945			}
 946			media = "text/plain"
 947			name = filehdr.Filename
 948			if name == "" {
 949				name = xid + ".txt"
 950			}
 951			xid += ".txt"
 952		}
 953		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 954		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 955		if err != nil {
 956			log.Printf("unable to save image: %s", err)
 957			return
 958		}
 959		var d Donk
 960		d.FileID, _ = res.LastInsertId()
 961		d.XID = name
 962		d.Name = name
 963		d.Media = media
 964		d.URL = url
 965		honk.Donks = append(honk.Donks, &d)
 966	}
 967	herd := herdofemus(honk.Noise)
 968	for _, e := range herd {
 969		donk := savedonk(e.ID, e.Name, "image/png", true)
 970		if donk != nil {
 971			donk.Name = e.Name
 972			honk.Donks = append(honk.Donks, donk)
 973		}
 974	}
 975	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 976
 977	aud := strings.Join(honk.Audience, " ")
 978	whofore := 2
 979	if !honk.Public {
 980		whofore = 3
 981	}
 982	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 983		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 984	if err != nil {
 985		log.Printf("error saving honk: %s", err)
 986		return
 987	}
 988	honk.ID, _ = res.LastInsertId()
 989	for _, d := range honk.Donks {
 990		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 991		if err != nil {
 992			log.Printf("err saving donk: %s", err)
 993			return
 994		}
 995	}
 996
 997	go honkworldwide(user, &honk)
 998
 999	http.Redirect(w, r, "/", http.StatusSeeOther)
1000}
1001
1002func showhonkers(w http.ResponseWriter, r *http.Request) {
1003	userinfo := login.GetUserInfo(r)
1004	templinfo := getInfo(r)
1005	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1006	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1007	err := readviews.Execute(w, "honkers.html", templinfo)
1008	if err != nil {
1009		log.Print(err)
1010	}
1011}
1012
1013func showcombos(w http.ResponseWriter, r *http.Request) {
1014	userinfo := login.GetUserInfo(r)
1015	templinfo := getInfo(r)
1016	honkers := gethonkers(userinfo.UserID)
1017	var combos []string
1018	for _, h := range honkers {
1019		combos = append(combos, h.Combos...)
1020	}
1021	for i, c := range combos {
1022		if c == "-" {
1023			combos[i] = ""
1024		}
1025	}
1026	combos = oneofakind(combos)
1027	sort.Strings(combos)
1028	templinfo["Combos"] = combos
1029	err := readviews.Execute(w, "combos.html", templinfo)
1030	if err != nil {
1031		log.Print(err)
1032	}
1033}
1034
1035func savehonker(w http.ResponseWriter, r *http.Request) {
1036	u := login.GetUserInfo(r)
1037	name := r.FormValue("name")
1038	url := r.FormValue("url")
1039	peep := r.FormValue("peep")
1040	combos := r.FormValue("combos")
1041	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1042
1043	if honkerid > 0 {
1044		goodbye := r.FormValue("goodbye")
1045		if goodbye == "F" {
1046			db := opendatabase()
1047			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1048				honkerid, u.UserID)
1049			var xid string
1050			err := row.Scan(&xid)
1051			if err != nil {
1052				log.Printf("can't get honker xid: %s", err)
1053				return
1054			}
1055			log.Printf("unsubscribing from %s", xid)
1056			user, _ := butwhatabout(u.Username)
1057			go itakeitallback(user, xid)
1058			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1059			if err != nil {
1060				log.Printf("error updating honker: %s", err)
1061				return
1062			}
1063
1064			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1065			return
1066		}
1067		combos = " " + strings.TrimSpace(combos) + " "
1068		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1069		if err != nil {
1070			log.Printf("update honker err: %s", err)
1071			return
1072		}
1073		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1074	}
1075
1076	flavor := "presub"
1077	if peep == "peep" {
1078		flavor = "peep"
1079	}
1080	url = investigate(url)
1081	if url == "" {
1082		return
1083	}
1084	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1085	if err != nil {
1086		log.Print(err)
1087		return
1088	}
1089	if flavor == "presub" {
1090		user, _ := butwhatabout(u.Username)
1091		go subsub(user, url)
1092	}
1093	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1094}
1095
1096type Zonker struct {
1097	ID        int64
1098	Name      string
1099	Wherefore string
1100}
1101
1102func zonkzone(w http.ResponseWriter, r *http.Request) {
1103	db := opendatabase()
1104	userinfo := login.GetUserInfo(r)
1105	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1106	if err != nil {
1107		log.Printf("err: %s", err)
1108		return
1109	}
1110	var zonkers []Zonker
1111	for rows.Next() {
1112		var z Zonker
1113		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1114		zonkers = append(zonkers, z)
1115	}
1116	templinfo := getInfo(r)
1117	templinfo["Zonkers"] = zonkers
1118	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1119	err = readviews.Execute(w, "zonkers.html", templinfo)
1120	if err != nil {
1121		log.Print(err)
1122	}
1123}
1124
1125func zonkzonk(w http.ResponseWriter, r *http.Request) {
1126	userinfo := login.GetUserInfo(r)
1127	itsok := r.FormValue("itsok")
1128	if itsok == "iforgiveyou" {
1129		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1130		db := opendatabase()
1131		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1132			userinfo.UserID, zonkerid)
1133		bitethethumbs()
1134		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1135		return
1136	}
1137	wherefore := r.FormValue("wherefore")
1138	name := r.FormValue("name")
1139	if name == "" {
1140		return
1141	}
1142	switch wherefore {
1143	case "zonker":
1144	case "zurl":
1145	case "zonvoy":
1146	case "zword":
1147	default:
1148		return
1149	}
1150	db := opendatabase()
1151	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1152		userinfo.UserID, name, wherefore)
1153	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1154		bitethethumbs()
1155	}
1156
1157	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1158}
1159
1160func accountpage(w http.ResponseWriter, r *http.Request) {
1161	u := login.GetUserInfo(r)
1162	user, _ := butwhatabout(u.Username)
1163	templinfo := getInfo(r)
1164	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1165	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1166	templinfo["WhatAbout"] = user.About
1167	err := readviews.Execute(w, "account.html", templinfo)
1168	if err != nil {
1169		log.Print(err)
1170	}
1171}
1172
1173func dochpass(w http.ResponseWriter, r *http.Request) {
1174	err := login.ChangePassword(w, r)
1175	if err != nil {
1176		log.Printf("error changing password: %s", err)
1177	}
1178	http.Redirect(w, r, "/account", http.StatusSeeOther)
1179}
1180
1181func fingerlicker(w http.ResponseWriter, r *http.Request) {
1182	orig := r.FormValue("resource")
1183
1184	log.Printf("finger lick: %s", orig)
1185
1186	if strings.HasPrefix(orig, "acct:") {
1187		orig = orig[5:]
1188	}
1189
1190	name := orig
1191	idx := strings.LastIndexByte(name, '/')
1192	if idx != -1 {
1193		name = name[idx+1:]
1194		if "https://"+serverName+"/u/"+name != orig {
1195			log.Printf("foreign request rejected")
1196			name = ""
1197		}
1198	} else {
1199		idx = strings.IndexByte(name, '@')
1200		if idx != -1 {
1201			name = name[:idx]
1202			if name+"@"+serverName != orig {
1203				log.Printf("foreign request rejected")
1204				name = ""
1205			}
1206		}
1207	}
1208	user, err := butwhatabout(name)
1209	if err != nil {
1210		http.NotFound(w, r)
1211		return
1212	}
1213
1214	j := junk.New()
1215	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1216	j["aliases"] = []string{user.URL}
1217	var links []map[string]interface{}
1218	l := junk.New()
1219	l["rel"] = "self"
1220	l["type"] = `application/activity+json`
1221	l["href"] = user.URL
1222	links = append(links, l)
1223	j["links"] = links
1224
1225	w.Header().Set("Cache-Control", "max-age=3600")
1226	w.Header().Set("Content-Type", "application/jrd+json")
1227	j.Write(w)
1228}
1229
1230func somedays() string {
1231	secs := 432000 + notrand.Int63n(432000)
1232	return fmt.Sprintf("%d", secs)
1233}
1234
1235func avatate(w http.ResponseWriter, r *http.Request) {
1236	n := r.FormValue("a")
1237	a := avatar(n)
1238	w.Header().Set("Cache-Control", "max-age="+somedays())
1239	w.Write(a)
1240}
1241
1242func servecss(w http.ResponseWriter, r *http.Request) {
1243	w.Header().Set("Cache-Control", "max-age=7776000")
1244	http.ServeFile(w, r, "views"+r.URL.Path)
1245}
1246func servehtml(w http.ResponseWriter, r *http.Request) {
1247	templinfo := getInfo(r)
1248	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1249	if err != nil {
1250		log.Print(err)
1251	}
1252}
1253func serveemu(w http.ResponseWriter, r *http.Request) {
1254	xid := mux.Vars(r)["xid"]
1255	w.Header().Set("Cache-Control", "max-age="+somedays())
1256	http.ServeFile(w, r, "emus/"+xid)
1257}
1258func servememe(w http.ResponseWriter, r *http.Request) {
1259	xid := mux.Vars(r)["xid"]
1260	w.Header().Set("Cache-Control", "max-age="+somedays())
1261	http.ServeFile(w, r, "memes/"+xid)
1262}
1263
1264func servefile(w http.ResponseWriter, r *http.Request) {
1265	xid := mux.Vars(r)["xid"]
1266	row := stmtFileData.QueryRow(xid)
1267	var media string
1268	var data []byte
1269	err := row.Scan(&media, &data)
1270	if err != nil {
1271		log.Printf("error loading file: %s", err)
1272		http.NotFound(w, r)
1273		return
1274	}
1275	w.Header().Set("Content-Type", media)
1276	w.Header().Set("X-Content-Type-Options", "nosniff")
1277	w.Header().Set("Cache-Control", "max-age="+somedays())
1278	w.Write(data)
1279}
1280
1281func serve() {
1282	db := opendatabase()
1283	login.Init(db)
1284
1285	listener, err := openListener()
1286	if err != nil {
1287		log.Fatal(err)
1288	}
1289	go redeliverator()
1290
1291	debug := false
1292	getconfig("debug", &debug)
1293	readviews = templates.Load(debug,
1294		"views/honkpage.html",
1295		"views/honkers.html",
1296		"views/zonkers.html",
1297		"views/combos.html",
1298		"views/honkform.html",
1299		"views/honk.html",
1300		"views/account.html",
1301		"views/about.html",
1302		"views/login.html",
1303		"views/xzone.html",
1304		"views/header.html",
1305	)
1306	if !debug {
1307		s := "views/style.css"
1308		savedstyleparams[s] = getstyleparam(s)
1309		s = "views/local.css"
1310		savedstyleparams[s] = getstyleparam(s)
1311	}
1312
1313	bitethethumbs()
1314
1315	mux := mux.NewRouter()
1316	mux.Use(login.Checker)
1317
1318	posters := mux.Methods("POST").Subrouter()
1319	getters := mux.Methods("GET").Subrouter()
1320
1321	getters.HandleFunc("/", homepage)
1322	getters.HandleFunc("/rss", showrss)
1323	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1324	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1325	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1326	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1327	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1328	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1329	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1330	getters.HandleFunc("/a", avatate)
1331	getters.HandleFunc("/t", showconvoy)
1332	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1333	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1334	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1335	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1336
1337	getters.HandleFunc("/style.css", servecss)
1338	getters.HandleFunc("/local.css", servecss)
1339	getters.HandleFunc("/about", servehtml)
1340	getters.HandleFunc("/login", servehtml)
1341	posters.HandleFunc("/dologin", login.LoginFunc)
1342	getters.HandleFunc("/logout", login.LogoutFunc)
1343
1344	loggedin := mux.NewRoute().Subrouter()
1345	loggedin.Use(login.Required)
1346	loggedin.HandleFunc("/account", accountpage)
1347	loggedin.HandleFunc("/chpass", dochpass)
1348	loggedin.HandleFunc("/atme", homepage)
1349	loggedin.HandleFunc("/zonkzone", zonkzone)
1350	loggedin.HandleFunc("/xzone", xzone)
1351	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1352	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1353	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1354	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1355	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1356	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1357	loggedin.HandleFunc("/honkers", showhonkers)
1358	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1359	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1360	loggedin.HandleFunc("/c", showcombos)
1361	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1362
1363	err = http.Serve(listener, mux)
1364	if err != nil {
1365		log.Fatal(err)
1366	}
1367}
1368
1369func cleanupdb(days int) {
1370	db := opendatabase()
1371	rows, _ := db.Query("select userid, name from zonkers where wherefore = 'zonvoy'")
1372	deadthreads := make(map[int64][]string)
1373	for rows.Next() {
1374		var userid int64
1375		var name string
1376		rows.Scan(&userid, &name)
1377		deadthreads[userid] = append(deadthreads[userid], name)
1378	}
1379	rows.Close()
1380	for userid, threads := range deadthreads {
1381		for _, t := range threads {
1382			doordie(db, "delete from donks where honkid in (select honkid from honks where userid = ? and convoy = ?)", userid, t)
1383			doordie(db, "delete from honks where userid = ? and convoy = ?", userid, t)
1384		}
1385	}
1386	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1387	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)
1388	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)
1389	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1390}
1391
1392func reducedb(honker string) {
1393	db := opendatabase()
1394	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1395	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1396	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1397	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1398}
1399
1400var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1401var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1402var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1403var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1404var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1405var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1406var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1407var stmtGetXonker, stmtSaveXonker *sql.Stmt
1408
1409func preparetodie(db *sql.DB, s string) *sql.Stmt {
1410	stmt, err := db.Prepare(s)
1411	if err != nil {
1412		log.Fatalf("error %s: %s", err, s)
1413	}
1414	return stmt
1415}
1416
1417func prepareStatements(db *sql.DB) {
1418	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")
1419	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1420	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1421	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1422	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1423	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1424
1425	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 "
1426	limit := " order by honkid desc limit 250"
1427	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1428	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1429	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1430	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1431	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)
1432	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1433	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1434	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1435	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1436
1437	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1438	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1439	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1440	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1441	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1442	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1443	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1444	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1445	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1446	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1447	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1448	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1449	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1450	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1451	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1452	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1453	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1454	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1455}
1456
1457func ElaborateUnitTests() {
1458}
1459
1460func main() {
1461	var err error
1462	cmd := "run"
1463	if len(os.Args) > 1 {
1464		cmd = os.Args[1]
1465	}
1466	switch cmd {
1467	case "init":
1468		initdb()
1469	case "upgrade":
1470		upgradedb()
1471	}
1472	db := opendatabase()
1473	dbversion := 0
1474	getconfig("dbversion", &dbversion)
1475	if dbversion != myVersion {
1476		log.Fatal("incorrect database version. run upgrade.")
1477	}
1478	getconfig("servername", &serverName)
1479	prepareStatements(db)
1480	switch cmd {
1481	case "adduser":
1482		adduser()
1483	case "cleanup":
1484		days := 30
1485		if len(os.Args) > 2 {
1486			days, err = strconv.Atoi(os.Args[2])
1487			if err != nil {
1488				log.Fatal(err)
1489			}
1490		}
1491		cleanupdb(days)
1492	case "reduce":
1493		if len(os.Args) < 3 {
1494			log.Fatal("need a honker name")
1495		}
1496		reducedb(os.Args[2])
1497	case "ping":
1498		if len(os.Args) < 4 {
1499			fmt.Printf("usage: honk ping from to\n")
1500			return
1501		}
1502		name := os.Args[2]
1503		targ := os.Args[3]
1504		user, err := butwhatabout(name)
1505		if err != nil {
1506			log.Printf("unknown user")
1507			return
1508		}
1509		ping(user, targ)
1510	case "peep":
1511		peeppeep()
1512	case "run":
1513		serve()
1514	case "test":
1515		ElaborateUnitTests()
1516	default:
1517		log.Fatal("unknown command")
1518	}
1519}