all repos — honk @ 48a2c70ffc89605ed78ca836fe6abab31470f9b1

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