all repos — honk @ dc14d410ad0ab39164c650175ac31c1cb4d51694

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 "Undo":
 342		obj, ok := jsongetmap(j, "object")
 343		if !ok {
 344			log.Printf("unknown undo no object")
 345		} else {
 346			what, _ := jsongetstring(obj, "type")
 347			switch what {
 348			case "Follow":
 349				log.Printf("updating honker undo: %s", who)
 350				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 351				if err != nil {
 352					log.Printf("error updating honker: %s", err)
 353					return
 354				}
 355			case "Like":
 356			case "Announce":
 357			default:
 358				log.Printf("unknown undo: %s", what)
 359			}
 360		}
 361	default:
 362		go consumeactivity(user, j, origin)
 363	}
 364}
 365
 366func outbox(w http.ResponseWriter, r *http.Request) {
 367	name := mux.Vars(r)["name"]
 368	user, err := butwhatabout(name)
 369	if err != nil {
 370		http.NotFound(w, r)
 371		return
 372	}
 373	honks := gethonksbyuser(name, false)
 374
 375	var jonks []map[string]interface{}
 376	for _, h := range honks {
 377		j, _ := jonkjonk(user, h)
 378		jonks = append(jonks, j)
 379	}
 380
 381	j := NewJunk()
 382	j["@context"] = itiswhatitis
 383	j["id"] = user.URL + "/outbox"
 384	j["type"] = "OrderedCollection"
 385	j["totalItems"] = len(jonks)
 386	j["orderedItems"] = jonks
 387
 388	w.Header().Set("Cache-Control", "max-age=60")
 389	w.Header().Set("Content-Type", theonetruename)
 390	WriteJunk(w, j)
 391}
 392
 393func emptiness(w http.ResponseWriter, r *http.Request) {
 394	name := mux.Vars(r)["name"]
 395	user, err := butwhatabout(name)
 396	if err != nil {
 397		http.NotFound(w, r)
 398		return
 399	}
 400	colname := "/followers"
 401	if strings.HasSuffix(r.URL.Path, "/following") {
 402		colname = "/following"
 403	}
 404	j := NewJunk()
 405	j["@context"] = itiswhatitis
 406	j["id"] = user.URL + colname
 407	j["type"] = "OrderedCollection"
 408	j["totalItems"] = 0
 409	j["orderedItems"] = []interface{}{}
 410
 411	w.Header().Set("Cache-Control", "max-age=60")
 412	w.Header().Set("Content-Type", theonetruename)
 413	WriteJunk(w, j)
 414}
 415
 416func showuser(w http.ResponseWriter, r *http.Request) {
 417	name := mux.Vars(r)["name"]
 418	user, err := butwhatabout(name)
 419	if err != nil {
 420		http.NotFound(w, r)
 421		return
 422	}
 423	if friendorfoe(r.Header.Get("Accept")) {
 424		j := asjonker(user)
 425		w.Header().Set("Cache-Control", "max-age=600")
 426		w.Header().Set("Content-Type", theonetruename)
 427		WriteJunk(w, j)
 428		return
 429	}
 430	u := login.GetUserInfo(r)
 431	honks := gethonksbyuser(name, u != nil && u.Username == name)
 432	honkpage(w, r, u, user, honks, "")
 433}
 434
 435func showhonker(w http.ResponseWriter, r *http.Request) {
 436	name := mux.Vars(r)["name"]
 437	u := login.GetUserInfo(r)
 438	honks := gethonksbyhonker(u.UserID, name)
 439	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 440}
 441
 442func showcombo(w http.ResponseWriter, r *http.Request) {
 443	name := mux.Vars(r)["name"]
 444	u := login.GetUserInfo(r)
 445	honks := gethonksbycombo(u.UserID, name)
 446	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 447}
 448func showconvoy(w http.ResponseWriter, r *http.Request) {
 449	c := r.FormValue("c")
 450	var userid int64 = -1
 451	u := login.GetUserInfo(r)
 452	if u != nil {
 453		userid = u.UserID
 454	}
 455	honks := gethonksbyconvoy(userid, c)
 456	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 457}
 458
 459func showhonk(w http.ResponseWriter, r *http.Request) {
 460	name := mux.Vars(r)["name"]
 461	user, err := butwhatabout(name)
 462	if err != nil {
 463		http.NotFound(w, r)
 464		return
 465	}
 466	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 467	h := getxonk(user.ID, xid)
 468	if h == nil || !h.Public {
 469		http.NotFound(w, r)
 470		return
 471	}
 472	if friendorfoe(r.Header.Get("Accept")) {
 473		donksforhonks([]*Honk{h})
 474		_, j := jonkjonk(user, h)
 475		j["@context"] = itiswhatitis
 476		w.Header().Set("Cache-Control", "max-age=3600")
 477		w.Header().Set("Content-Type", theonetruename)
 478		WriteJunk(w, j)
 479		return
 480	}
 481	honks := gethonksbyconvoy(-1, h.Convoy)
 482	u := login.GetUserInfo(r)
 483	honkpage(w, r, u, nil, honks, "one honk maybe more")
 484}
 485
 486func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 487	honks []*Honk, infomsg string) {
 488	reverbolate(honks)
 489	templinfo := getInfo(r)
 490	if u != nil {
 491		honks = osmosis(honks, u.UserID)
 492		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 493	}
 494	if u == nil {
 495		w.Header().Set("Cache-Control", "max-age=60")
 496	}
 497	if user != nil {
 498		filt := htfilter.New()
 499		templinfo["Name"] = user.Name
 500		whatabout := user.About
 501		whatabout = obfusbreak(user.About)
 502		templinfo["WhatAbout"], _ = filt.String(whatabout)
 503	}
 504	templinfo["Honks"] = honks
 505	templinfo["ServerMessage"] = infomsg
 506	err := readviews.Execute(w, "honkpage.html", templinfo)
 507	if err != nil {
 508		log.Print(err)
 509	}
 510}
 511
 512func saveuser(w http.ResponseWriter, r *http.Request) {
 513	whatabout := r.FormValue("whatabout")
 514	u := login.GetUserInfo(r)
 515	db := opendatabase()
 516	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 517	if err != nil {
 518		log.Printf("error bouting what: %s", err)
 519	}
 520
 521	http.Redirect(w, r, "/account", http.StatusSeeOther)
 522}
 523
 524func gethonkers(userid int64) []*Honker {
 525	rows, err := stmtHonkers.Query(userid)
 526	if err != nil {
 527		log.Printf("error querying honkers: %s", err)
 528		return nil
 529	}
 530	defer rows.Close()
 531	var honkers []*Honker
 532	for rows.Next() {
 533		var f Honker
 534		var combos string
 535		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 536		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 537		if err != nil {
 538			log.Printf("error scanning honker: %s", err)
 539			return nil
 540		}
 541		honkers = append(honkers, &f)
 542	}
 543	return honkers
 544}
 545
 546func getdubs(userid int64) []*Honker {
 547	rows, err := stmtDubbers.Query(userid)
 548	if err != nil {
 549		log.Printf("error querying dubs: %s", err)
 550		return nil
 551	}
 552	defer rows.Close()
 553	var honkers []*Honker
 554	for rows.Next() {
 555		var f Honker
 556		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 557		if err != nil {
 558			log.Printf("error scanning honker: %s", err)
 559			return nil
 560		}
 561		honkers = append(honkers, &f)
 562	}
 563	return honkers
 564}
 565
 566func allusers() []login.UserInfo {
 567	var users []login.UserInfo
 568	rows, _ := opendatabase().Query("select userid, username from users")
 569	defer rows.Close()
 570	for rows.Next() {
 571		var u login.UserInfo
 572		rows.Scan(&u.UserID, &u.Username)
 573		users = append(users, u)
 574	}
 575	return users
 576}
 577
 578func getxonk(userid int64, xid string) *Honk {
 579	h := new(Honk)
 580	var dt, aud string
 581	row := stmtOneXonk.QueryRow(userid, xid)
 582	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 583		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 584	if err != nil {
 585		if err != sql.ErrNoRows {
 586			log.Printf("error scanning xonk: %s", err)
 587		}
 588		return nil
 589	}
 590	h.Date, _ = time.Parse(dbtimeformat, dt)
 591	h.Audience = strings.Split(aud, " ")
 592	h.Public = !keepitquiet(h.Audience)
 593	return h
 594}
 595
 596func getpublichonks() []*Honk {
 597	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 598	rows, err := stmtPublicHonks.Query(dt)
 599	return getsomehonks(rows, err)
 600}
 601func gethonksbyuser(name string, includeprivate bool) []*Honk {
 602	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 603	whofore := 2
 604	if includeprivate {
 605		whofore = 3
 606	}
 607	rows, err := stmtUserHonks.Query(whofore, name, dt)
 608	return getsomehonks(rows, err)
 609}
 610func gethonksforuser(userid int64) []*Honk {
 611	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 612	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 613	return getsomehonks(rows, err)
 614}
 615func gethonksforme(userid int64) []*Honk {
 616	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 617	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 618	return getsomehonks(rows, err)
 619}
 620func gethonksbyhonker(userid int64, honker string) []*Honk {
 621	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 622	return getsomehonks(rows, err)
 623}
 624func gethonksbycombo(userid int64, combo string) []*Honk {
 625	combo = "% " + combo + " %"
 626	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 627	return getsomehonks(rows, err)
 628}
 629func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 630	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 631	honks := getsomehonks(rows, err)
 632	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 633		honks[i], honks[j] = honks[j], honks[i]
 634	}
 635	return honks
 636}
 637
 638func getsomehonks(rows *sql.Rows, err error) []*Honk {
 639	if err != nil {
 640		log.Printf("error querying honks: %s", err)
 641		return nil
 642	}
 643	defer rows.Close()
 644	var honks []*Honk
 645	for rows.Next() {
 646		var h Honk
 647		var dt, aud string
 648		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 649			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 650		if err != nil {
 651			log.Printf("error scanning honks: %s", err)
 652			return nil
 653		}
 654		h.Date, _ = time.Parse(dbtimeformat, dt)
 655		h.Audience = strings.Split(aud, " ")
 656		h.Public = !keepitquiet(h.Audience)
 657		honks = append(honks, &h)
 658	}
 659	rows.Close()
 660	donksforhonks(honks)
 661	return honks
 662}
 663
 664func donksforhonks(honks []*Honk) {
 665	db := opendatabase()
 666	var ids []string
 667	hmap := make(map[int64]*Honk)
 668	for _, h := range honks {
 669		ids = append(ids, fmt.Sprintf("%d", h.ID))
 670		hmap[h.ID] = h
 671	}
 672	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, ","))
 673	rows, err := db.Query(q)
 674	if err != nil {
 675		log.Printf("error querying donks: %s", err)
 676		return
 677	}
 678	defer rows.Close()
 679	for rows.Next() {
 680		var hid int64
 681		var d Donk
 682		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 683		if err != nil {
 684			log.Printf("error scanning donk: %s", err)
 685			continue
 686		}
 687		h := hmap[hid]
 688		h.Donks = append(h.Donks, &d)
 689	}
 690}
 691
 692func savebonk(w http.ResponseWriter, r *http.Request) {
 693	xid := r.FormValue("xid")
 694	userinfo := login.GetUserInfo(r)
 695	user, _ := butwhatabout(userinfo.Username)
 696
 697	log.Printf("bonking %s", xid)
 698
 699	xonk := getxonk(userinfo.UserID, xid)
 700	if xonk == nil {
 701		return
 702	}
 703	if !xonk.Public {
 704		return
 705	}
 706	donksforhonks([]*Honk{xonk})
 707
 708	dt := time.Now().UTC()
 709	bonk := Honk{
 710		UserID:   userinfo.UserID,
 711		Username: userinfo.Username,
 712		What:     "bonk",
 713		Honker:   user.URL,
 714		XID:      xonk.XID,
 715		Date:     dt,
 716		Donks:    xonk.Donks,
 717		Audience: []string{thewholeworld},
 718		Public:   true,
 719	}
 720
 721	oonker := xonk.Oonker
 722	if oonker == "" {
 723		oonker = xonk.Honker
 724	}
 725	aud := strings.Join(bonk.Audience, " ")
 726	whofore := 2
 727	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 728		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 729		xonk.Precis, oonker)
 730	if err != nil {
 731		log.Printf("error saving bonk: %s", err)
 732		return
 733	}
 734	bonk.ID, _ = res.LastInsertId()
 735	for _, d := range bonk.Donks {
 736		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 737		if err != nil {
 738			log.Printf("err saving donk: %s", err)
 739			return
 740		}
 741	}
 742
 743	go honkworldwide(user, &bonk)
 744}
 745
 746func zonkit(w http.ResponseWriter, r *http.Request) {
 747	wherefore := r.FormValue("wherefore")
 748	var what string
 749	switch wherefore {
 750	case "this honk":
 751		what = r.FormValue("honk")
 752		wherefore = "zonk"
 753	case "this honker":
 754		what = r.FormValue("honker")
 755		wherefore = "zonker"
 756	case "this convoy":
 757		what = r.FormValue("convoy")
 758		wherefore = "zonvoy"
 759	}
 760	if what == "" {
 761		return
 762	}
 763
 764	log.Printf("zonking %s %s", wherefore, what)
 765	userinfo := login.GetUserInfo(r)
 766	if wherefore == "zonk" {
 767		xonk := getxonk(userinfo.UserID, what)
 768		if xonk != nil {
 769			stmtZonkDonks.Exec(xonk.ID)
 770			stmtZonkIt.Exec(userinfo.UserID, what)
 771			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 772				zonk := Honk{
 773					What:     "zonk",
 774					XID:      xonk.XID,
 775					Date:     time.Now().UTC(),
 776					Audience: oneofakind(xonk.Audience),
 777				}
 778
 779				user, _ := butwhatabout(userinfo.Username)
 780				log.Printf("announcing deleted honk: %s", what)
 781				go honkworldwide(user, &zonk)
 782			}
 783		}
 784	} else {
 785		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 786		if err != nil {
 787			log.Printf("error saving zonker: %s", err)
 788			return
 789		}
 790	}
 791}
 792
 793func savehonk(w http.ResponseWriter, r *http.Request) {
 794	rid := r.FormValue("rid")
 795	noise := r.FormValue("noise")
 796
 797	userinfo := login.GetUserInfo(r)
 798	user, _ := butwhatabout(userinfo.Username)
 799
 800	dt := time.Now().UTC()
 801	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 802	what := "honk"
 803	if rid != "" {
 804		what = "tonk"
 805	}
 806	honk := Honk{
 807		UserID:   userinfo.UserID,
 808		Username: userinfo.Username,
 809		What:     "honk",
 810		Honker:   user.URL,
 811		XID:      xid,
 812		Date:     dt,
 813	}
 814	if strings.HasPrefix(noise, "DZ:") {
 815		idx := strings.Index(noise, "\n")
 816		if idx == -1 {
 817			honk.Precis = noise
 818			noise = ""
 819		} else {
 820			honk.Precis = noise[:idx]
 821			noise = noise[idx+1:]
 822		}
 823	}
 824	noise = strings.TrimSpace(noise)
 825	honk.Precis = strings.TrimSpace(honk.Precis)
 826
 827	var convoy string
 828	if rid != "" {
 829		xonk := getxonk(userinfo.UserID, rid)
 830		if xonk != nil {
 831			if xonk.Public {
 832				honk.Audience = append(honk.Audience, xonk.Audience...)
 833			}
 834			convoy = xonk.Convoy
 835		} else {
 836			xonkaud, c := whosthere(rid)
 837			honk.Audience = append(honk.Audience, xonkaud...)
 838			convoy = c
 839		}
 840		for i, a := range honk.Audience {
 841			if a == thewholeworld {
 842				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 843				break
 844			}
 845		}
 846		honk.RID = rid
 847	} else {
 848		honk.Audience = []string{thewholeworld}
 849	}
 850	if noise != "" && noise[0] == '@' {
 851		honk.Audience = append(grapevine(noise), honk.Audience...)
 852	} else {
 853		honk.Audience = append(honk.Audience, grapevine(noise)...)
 854	}
 855	if convoy == "" {
 856		convoy = "data:,electrichonkytonk-" + xfiltrate()
 857	}
 858	butnottooloud(honk.Audience)
 859	honk.Audience = oneofakind(honk.Audience)
 860	if len(honk.Audience) == 0 {
 861		log.Printf("honk to nowhere")
 862		return
 863	}
 864	honk.Public = !keepitquiet(honk.Audience)
 865	noise = obfusbreak(noise)
 866	honk.Noise = noise
 867	honk.Convoy = convoy
 868
 869	file, filehdr, err := r.FormFile("donk")
 870	if err == nil {
 871		var buf bytes.Buffer
 872		io.Copy(&buf, file)
 873		file.Close()
 874		data := buf.Bytes()
 875		xid := xfiltrate()
 876		var media, name string
 877		img, err := image.Vacuum(&buf)
 878		if err == nil {
 879			data = img.Data
 880			format := img.Format
 881			media = "image/" + format
 882			if format == "jpeg" {
 883				format = "jpg"
 884			}
 885			name = xid + "." + format
 886			xid = name
 887		} else {
 888			maxsize := 100000
 889			if len(data) > maxsize {
 890				log.Printf("bad image: %s too much text: %d", err, len(data))
 891				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 892				return
 893			}
 894			for i := 0; i < len(data); i++ {
 895				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 896					log.Printf("bad image: %s not text: %d", err, data[i])
 897					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 898					return
 899				}
 900			}
 901			media = "text/plain"
 902			name = filehdr.Filename
 903			if name == "" {
 904				name = xid + ".txt"
 905			}
 906			xid += ".txt"
 907		}
 908		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 909		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 910		if err != nil {
 911			log.Printf("unable to save image: %s", err)
 912			return
 913		}
 914		var d Donk
 915		d.FileID, _ = res.LastInsertId()
 916		d.XID = name
 917		d.Name = name
 918		d.Media = media
 919		d.URL = url
 920		honk.Donks = append(honk.Donks, &d)
 921	}
 922	herd := herdofemus(honk.Noise)
 923	for _, e := range herd {
 924		donk := savedonk(e.ID, e.Name, "image/png", true)
 925		if donk != nil {
 926			donk.Name = e.Name
 927			honk.Donks = append(honk.Donks, donk)
 928		}
 929	}
 930	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 931
 932	aud := strings.Join(honk.Audience, " ")
 933	whofore := 2
 934	if !honk.Public {
 935		whofore = 3
 936	}
 937	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 938		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 939	if err != nil {
 940		log.Printf("error saving honk: %s", err)
 941		return
 942	}
 943	honk.ID, _ = res.LastInsertId()
 944	for _, d := range honk.Donks {
 945		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 946		if err != nil {
 947			log.Printf("err saving donk: %s", err)
 948			return
 949		}
 950	}
 951
 952	go honkworldwide(user, &honk)
 953
 954	http.Redirect(w, r, "/", http.StatusSeeOther)
 955}
 956
 957func showhonkers(w http.ResponseWriter, r *http.Request) {
 958	userinfo := login.GetUserInfo(r)
 959	templinfo := getInfo(r)
 960	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 961	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 962	err := readviews.Execute(w, "honkers.html", templinfo)
 963	if err != nil {
 964		log.Print(err)
 965	}
 966}
 967
 968func showcombos(w http.ResponseWriter, r *http.Request) {
 969	userinfo := login.GetUserInfo(r)
 970	templinfo := getInfo(r)
 971	honkers := gethonkers(userinfo.UserID)
 972	var combos []string
 973	for _, h := range honkers {
 974		combos = append(combos, h.Combos...)
 975	}
 976	for i, c := range combos {
 977		if c == "-" {
 978			combos[i] = ""
 979		}
 980	}
 981	combos = oneofakind(combos)
 982	sort.Strings(combos)
 983	templinfo["Combos"] = combos
 984	err := readviews.Execute(w, "combos.html", templinfo)
 985	if err != nil {
 986		log.Print(err)
 987	}
 988}
 989
 990func savehonker(w http.ResponseWriter, r *http.Request) {
 991	u := login.GetUserInfo(r)
 992	name := r.FormValue("name")
 993	url := r.FormValue("url")
 994	peep := r.FormValue("peep")
 995	combos := r.FormValue("combos")
 996	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 997
 998	if honkerid > 0 {
 999		goodbye := r.FormValue("goodbye")
1000		if goodbye == "F" {
1001			db := opendatabase()
1002			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1003				honkerid, u.UserID)
1004			var xid string
1005			err := row.Scan(&xid)
1006			if err != nil {
1007				log.Printf("can't get honker xid: %s", err)
1008				return
1009			}
1010			log.Printf("unsubscribing from %s", xid)
1011			user, _ := butwhatabout(u.Username)
1012			err = itakeitallback(user, xid)
1013			if err != nil {
1014				log.Printf("can't take it back: %s", err)
1015			} else {
1016				_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1017				if err != nil {
1018					log.Printf("error updating honker: %s", err)
1019					return
1020				}
1021			}
1022
1023			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1024			return
1025		}
1026		combos = " " + strings.TrimSpace(combos) + " "
1027		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1028		if err != nil {
1029			log.Printf("update honker err: %s", err)
1030			return
1031		}
1032		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1033	}
1034
1035	flavor := "presub"
1036	if peep == "peep" {
1037		flavor = "peep"
1038	}
1039	url = investigate(url)
1040	if url == "" {
1041		return
1042	}
1043	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1044	if err != nil {
1045		log.Print(err)
1046		return
1047	}
1048	if flavor == "presub" {
1049		user, _ := butwhatabout(u.Username)
1050		go subsub(user, url)
1051	}
1052	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1053}
1054
1055type Zonker struct {
1056	ID        int64
1057	Name      string
1058	Wherefore string
1059}
1060
1061func zonkzone(w http.ResponseWriter, r *http.Request) {
1062	db := opendatabase()
1063	userinfo := login.GetUserInfo(r)
1064	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1065	if err != nil {
1066		log.Printf("err: %s", err)
1067		return
1068	}
1069	var zonkers []Zonker
1070	for rows.Next() {
1071		var z Zonker
1072		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1073		zonkers = append(zonkers, z)
1074	}
1075	templinfo := getInfo(r)
1076	templinfo["Zonkers"] = zonkers
1077	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1078	err = readviews.Execute(w, "zonkers.html", templinfo)
1079	if err != nil {
1080		log.Print(err)
1081	}
1082}
1083
1084func zonkzonk(w http.ResponseWriter, r *http.Request) {
1085	userinfo := login.GetUserInfo(r)
1086	itsok := r.FormValue("itsok")
1087	if itsok == "iforgiveyou" {
1088		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1089		db := opendatabase()
1090		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1091			userinfo.UserID, zonkerid)
1092		bitethethumbs()
1093		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1094		return
1095	}
1096	wherefore := r.FormValue("wherefore")
1097	name := r.FormValue("name")
1098	if name == "" {
1099		return
1100	}
1101	switch wherefore {
1102	case "zonker":
1103	case "zurl":
1104	case "zonvoy":
1105	case "zword":
1106	default:
1107		return
1108	}
1109	db := opendatabase()
1110	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1111		userinfo.UserID, name, wherefore)
1112	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1113		bitethethumbs()
1114	}
1115
1116	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1117}
1118
1119func accountpage(w http.ResponseWriter, r *http.Request) {
1120	u := login.GetUserInfo(r)
1121	user, _ := butwhatabout(u.Username)
1122	templinfo := getInfo(r)
1123	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1124	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1125	templinfo["WhatAbout"] = user.About
1126	err := readviews.Execute(w, "account.html", templinfo)
1127	if err != nil {
1128		log.Print(err)
1129	}
1130}
1131
1132func dochpass(w http.ResponseWriter, r *http.Request) {
1133	err := login.ChangePassword(w, r)
1134	if err != nil {
1135		log.Printf("error changing password: %s", err)
1136	}
1137	http.Redirect(w, r, "/account", http.StatusSeeOther)
1138}
1139
1140func fingerlicker(w http.ResponseWriter, r *http.Request) {
1141	orig := r.FormValue("resource")
1142
1143	log.Printf("finger lick: %s", orig)
1144
1145	if strings.HasPrefix(orig, "acct:") {
1146		orig = orig[5:]
1147	}
1148
1149	name := orig
1150	idx := strings.LastIndexByte(name, '/')
1151	if idx != -1 {
1152		name = name[idx+1:]
1153		if "https://"+serverName+"/u/"+name != orig {
1154			log.Printf("foreign request rejected")
1155			name = ""
1156		}
1157	} else {
1158		idx = strings.IndexByte(name, '@')
1159		if idx != -1 {
1160			name = name[:idx]
1161			if name+"@"+serverName != orig {
1162				log.Printf("foreign request rejected")
1163				name = ""
1164			}
1165		}
1166	}
1167	user, err := butwhatabout(name)
1168	if err != nil {
1169		http.NotFound(w, r)
1170		return
1171	}
1172
1173	j := NewJunk()
1174	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1175	j["aliases"] = []string{user.URL}
1176	var links []map[string]interface{}
1177	l := NewJunk()
1178	l["rel"] = "self"
1179	l["type"] = `application/activity+json`
1180	l["href"] = user.URL
1181	links = append(links, l)
1182	j["links"] = links
1183
1184	w.Header().Set("Cache-Control", "max-age=3600")
1185	w.Header().Set("Content-Type", "application/jrd+json")
1186	WriteJunk(w, j)
1187}
1188
1189func somedays() string {
1190	secs := 432000 + notrand.Int63n(432000)
1191	return fmt.Sprintf("%d", secs)
1192}
1193
1194func avatate(w http.ResponseWriter, r *http.Request) {
1195	n := r.FormValue("a")
1196	a := avatar(n)
1197	w.Header().Set("Cache-Control", "max-age="+somedays())
1198	w.Write(a)
1199}
1200
1201func servecss(w http.ResponseWriter, r *http.Request) {
1202	w.Header().Set("Cache-Control", "max-age=7776000")
1203	http.ServeFile(w, r, "views"+r.URL.Path)
1204}
1205func servehtml(w http.ResponseWriter, r *http.Request) {
1206	templinfo := getInfo(r)
1207	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1208	if err != nil {
1209		log.Print(err)
1210	}
1211}
1212func serveemu(w http.ResponseWriter, r *http.Request) {
1213	xid := mux.Vars(r)["xid"]
1214	w.Header().Set("Cache-Control", "max-age="+somedays())
1215	http.ServeFile(w, r, "emus/"+xid)
1216}
1217func servememe(w http.ResponseWriter, r *http.Request) {
1218	xid := mux.Vars(r)["xid"]
1219	w.Header().Set("Cache-Control", "max-age="+somedays())
1220	http.ServeFile(w, r, "memes/"+xid)
1221}
1222
1223func servefile(w http.ResponseWriter, r *http.Request) {
1224	xid := mux.Vars(r)["xid"]
1225	row := stmtFileData.QueryRow(xid)
1226	var media string
1227	var data []byte
1228	err := row.Scan(&media, &data)
1229	if err != nil {
1230		log.Printf("error loading file: %s", err)
1231		http.NotFound(w, r)
1232		return
1233	}
1234	w.Header().Set("Content-Type", media)
1235	w.Header().Set("X-Content-Type-Options", "nosniff")
1236	w.Header().Set("Cache-Control", "max-age="+somedays())
1237	w.Write(data)
1238}
1239
1240func serve() {
1241	db := opendatabase()
1242	login.Init(db)
1243
1244	listener, err := openListener()
1245	if err != nil {
1246		log.Fatal(err)
1247	}
1248	go redeliverator()
1249
1250	debug := false
1251	getconfig("debug", &debug)
1252	readviews = templates.Load(debug,
1253		"views/honkpage.html",
1254		"views/honkers.html",
1255		"views/zonkers.html",
1256		"views/combos.html",
1257		"views/honkform.html",
1258		"views/honk.html",
1259		"views/account.html",
1260		"views/login.html",
1261		"views/header.html",
1262	)
1263	if !debug {
1264		s := "views/style.css"
1265		savedstyleparams[s] = getstyleparam(s)
1266		s = "views/local.css"
1267		savedstyleparams[s] = getstyleparam(s)
1268	}
1269
1270	bitethethumbs()
1271
1272	mux := mux.NewRouter()
1273	mux.Use(login.Checker)
1274
1275	posters := mux.Methods("POST").Subrouter()
1276	getters := mux.Methods("GET").Subrouter()
1277
1278	getters.HandleFunc("/", homepage)
1279	getters.HandleFunc("/rss", showrss)
1280	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1281	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1282	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1283	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1284	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1285	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1286	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1287	getters.HandleFunc("/a", avatate)
1288	getters.HandleFunc("/t", showconvoy)
1289	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1290	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1291	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1292	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1293
1294	getters.HandleFunc("/style.css", servecss)
1295	getters.HandleFunc("/local.css", servecss)
1296	getters.HandleFunc("/login", servehtml)
1297	posters.HandleFunc("/dologin", login.LoginFunc)
1298	getters.HandleFunc("/logout", login.LogoutFunc)
1299
1300	loggedin := mux.NewRoute().Subrouter()
1301	loggedin.Use(login.Required)
1302	loggedin.HandleFunc("/account", accountpage)
1303	loggedin.HandleFunc("/chpass", dochpass)
1304	loggedin.HandleFunc("/atme", homepage)
1305	loggedin.HandleFunc("/zonkzone", zonkzone)
1306	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1307	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1308	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1309	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1310	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1311	loggedin.HandleFunc("/honkers", showhonkers)
1312	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1313	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1314	loggedin.HandleFunc("/c", showcombos)
1315	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1316
1317	err = http.Serve(listener, mux)
1318	if err != nil {
1319		log.Fatal(err)
1320	}
1321}
1322
1323func cleanupdb() {
1324	db := opendatabase()
1325	rows, _ := db.Query("select userid, name from zonkers where wherefore = 'zonvoy'")
1326	deadthreads := make(map[int64][]string)
1327	for rows.Next() {
1328		var userid int64
1329		var name string
1330		rows.Scan(&userid, &name)
1331		deadthreads[userid] = append(deadthreads[userid], name)
1332	}
1333	rows.Close()
1334	for userid, threads := range deadthreads {
1335		for _, t := range threads {
1336			doordie(db, "delete from donks where honkid in (select honkid from honks where userid = ? and convoy = ?)", userid, t)
1337			doordie(db, "delete from honks where userid = ? and convoy = ?", userid, t)
1338		}
1339	}
1340	expdate := time.Now().UTC().Add(-30 * 24 * time.Hour).Format(dbtimeformat)
1341	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)
1342	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)
1343	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1344}
1345
1346var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1347var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1348var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1349var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1350var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1351var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1352var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1353var stmtGetXonker, stmtSaveXonker *sql.Stmt
1354
1355func preparetodie(db *sql.DB, s string) *sql.Stmt {
1356	stmt, err := db.Prepare(s)
1357	if err != nil {
1358		log.Fatalf("error %s: %s", err, s)
1359	}
1360	return stmt
1361}
1362
1363func prepareStatements(db *sql.DB) {
1364	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")
1365	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1366	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1367	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1368	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1369	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1370
1371	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 "
1372	limit := " order by honkid desc limit 250"
1373	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1374	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1375	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1376	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1377	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)
1378	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1379	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1380	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1381	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1382
1383	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1384	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1385	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1386	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1387	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1388	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1389	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1390	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1391	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1392	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1393	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1394	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1395	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1396	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1397	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1398	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1399	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1400	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1401}
1402
1403func ElaborateUnitTests() {
1404}
1405
1406func main() {
1407	cmd := "run"
1408	if len(os.Args) > 1 {
1409		cmd = os.Args[1]
1410	}
1411	switch cmd {
1412	case "init":
1413		initdb()
1414	case "upgrade":
1415		upgradedb()
1416	}
1417	db := opendatabase()
1418	dbversion := 0
1419	getconfig("dbversion", &dbversion)
1420	if dbversion != myVersion {
1421		log.Fatal("incorrect database version. run upgrade.")
1422	}
1423	getconfig("servername", &serverName)
1424	prepareStatements(db)
1425	switch cmd {
1426	case "adduser":
1427		adduser()
1428	case "cleanup":
1429		cleanupdb()
1430	case "ping":
1431		if len(os.Args) < 4 {
1432			fmt.Printf("usage: honk ping from to\n")
1433			return
1434		}
1435		name := os.Args[2]
1436		targ := os.Args[3]
1437		user, err := butwhatabout(name)
1438		if err != nil {
1439			log.Printf("unknown user")
1440			return
1441		}
1442		ping(user, targ)
1443	case "peep":
1444		peeppeep()
1445	case "run":
1446		serve()
1447	case "test":
1448		ElaborateUnitTests()
1449	default:
1450		log.Fatal("unknown command")
1451	}
1452}