all repos — honk @ 7f9cf15fdcd40c6c9cfe0c3865f7dbe2745c0bc1

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