all repos — honk @ ee7d7b5b5232c051e3b711a3260ac7892d892c20

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