all repos — honk @ 0dec695e683f3cd0e9a7d4d459f91ecec2fd87d9

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