all repos — honk @ 0b4f359cf26a5db3e2f052576668eade156dd74d

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