all repos — honk @ f37b30f081397e6e0ad92b7e23861e70870e0f87

my fork of honk

honk.go (view raw)

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