all repos — honk @ 44b5f459edcb22b22460c9968ea31c11cd1e8b7a

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