all repos — honk @ b2c76dfd770be7cdbe175c6e080a9d641c93ee2e

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				zonk.Public = !keepitquiet(zonk.Audience)
 917
 918				user, _ := butwhatabout(userinfo.Username)
 919				log.Printf("announcing deleted honk: %s", what)
 920				go honkworldwide(user, &zonk)
 921			}
 922		}
 923	}
 924	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 925	if err != nil {
 926		log.Printf("error saving zonker: %s", err)
 927		return
 928	}
 929}
 930
 931func savehonk(w http.ResponseWriter, r *http.Request) {
 932	rid := r.FormValue("rid")
 933	noise := r.FormValue("noise")
 934
 935	userinfo := login.GetUserInfo(r)
 936	user, _ := butwhatabout(userinfo.Username)
 937
 938	dt := time.Now().UTC()
 939	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 940	what := "honk"
 941	if rid != "" {
 942		what = "tonk"
 943	}
 944	honk := Honk{
 945		UserID:   userinfo.UserID,
 946		Username: userinfo.Username,
 947		What:     "honk",
 948		Honker:   user.URL,
 949		XID:      xid,
 950		Date:     dt,
 951	}
 952	if strings.HasPrefix(noise, "DZ:") {
 953		idx := strings.Index(noise, "\n")
 954		if idx == -1 {
 955			honk.Precis = noise
 956			noise = ""
 957		} else {
 958			honk.Precis = noise[:idx]
 959			noise = noise[idx+1:]
 960		}
 961	}
 962	noise = hooterize(noise)
 963	noise = strings.TrimSpace(noise)
 964	honk.Precis = strings.TrimSpace(honk.Precis)
 965
 966	var convoy string
 967	if rid != "" {
 968		xonk := getxonk(userinfo.UserID, rid)
 969		if xonk != nil {
 970			if xonk.Public {
 971				honk.Audience = append(honk.Audience, xonk.Audience...)
 972			}
 973			convoy = xonk.Convoy
 974		} else {
 975			xonkaud, c := whosthere(rid)
 976			honk.Audience = append(honk.Audience, xonkaud...)
 977			convoy = c
 978		}
 979		for i, a := range honk.Audience {
 980			if a == thewholeworld {
 981				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 982				break
 983			}
 984		}
 985		honk.RID = rid
 986	} else {
 987		honk.Audience = []string{thewholeworld}
 988	}
 989	if noise != "" && noise[0] == '@' {
 990		honk.Audience = append(grapevine(noise), honk.Audience...)
 991	} else {
 992		honk.Audience = append(honk.Audience, grapevine(noise)...)
 993	}
 994	if convoy == "" {
 995		convoy = "data:,electrichonkytonk-" + xfiltrate()
 996	}
 997	butnottooloud(honk.Audience)
 998	honk.Audience = oneofakind(honk.Audience)
 999	if len(honk.Audience) == 0 {
1000		log.Printf("honk to nowhere")
1001		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1002		return
1003	}
1004	honk.Public = !keepitquiet(honk.Audience)
1005	noise = obfusbreak(noise)
1006	honk.Noise = noise
1007	honk.Convoy = convoy
1008
1009	file, filehdr, err := r.FormFile("donk")
1010	if err == nil {
1011		var buf bytes.Buffer
1012		io.Copy(&buf, file)
1013		file.Close()
1014		data := buf.Bytes()
1015		xid := xfiltrate()
1016		var media, name string
1017		img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1018		if err == nil {
1019			data = img.Data
1020			format := img.Format
1021			media = "image/" + format
1022			if format == "jpeg" {
1023				format = "jpg"
1024			}
1025			name = xid + "." + format
1026			xid = name
1027		} else {
1028			maxsize := 100000
1029			if len(data) > maxsize {
1030				log.Printf("bad image: %s too much text: %d", err, len(data))
1031				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1032				return
1033			}
1034			for i := 0; i < len(data); i++ {
1035				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1036					log.Printf("bad image: %s not text: %d", err, data[i])
1037					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1038					return
1039				}
1040			}
1041			media = "text/plain"
1042			name = filehdr.Filename
1043			if name == "" {
1044				name = xid + ".txt"
1045			}
1046			xid += ".txt"
1047		}
1048		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1049		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1050		if err != nil {
1051			log.Printf("unable to save image: %s", err)
1052			return
1053		}
1054		var d Donk
1055		d.FileID, _ = res.LastInsertId()
1056		d.XID = name
1057		d.Name = name
1058		d.Media = media
1059		d.URL = url
1060		d.Local = true
1061		honk.Donks = append(honk.Donks, &d)
1062	}
1063	herd := herdofemus(honk.Noise)
1064	for _, e := range herd {
1065		donk := savedonk(e.ID, e.Name, "image/png", true)
1066		if donk != nil {
1067			donk.Name = e.Name
1068			honk.Donks = append(honk.Donks, donk)
1069		}
1070	}
1071	memetize(&honk)
1072
1073	aud := strings.Join(honk.Audience, " ")
1074	whofore := 2
1075	if !honk.Public {
1076		whofore = 3
1077	}
1078	if r.FormValue("preview") == "preview" {
1079		honks := []*Honk{&honk}
1080		reverbolate(userinfo.UserID, honks)
1081		templinfo := getInfo(r)
1082		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1083		templinfo["Honks"] = honks
1084		templinfo["Noise"] = r.FormValue("noise")
1085		templinfo["ServerMessage"] = "honk preview"
1086		err := readviews.Execute(w, "honkpage.html", templinfo)
1087		if err != nil {
1088			log.Print(err)
1089		}
1090		return
1091	}
1092	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1093		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1094	if err != nil {
1095		log.Printf("error saving honk: %s", err)
1096		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1097		return
1098	}
1099	honk.ID, _ = res.LastInsertId()
1100	for _, d := range honk.Donks {
1101		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1102		if err != nil {
1103			log.Printf("err saving donk: %s", err)
1104			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1105			return
1106		}
1107	}
1108
1109	go honkworldwide(user, &honk)
1110
1111	http.Redirect(w, r, xid, http.StatusSeeOther)
1112}
1113
1114func showhonkers(w http.ResponseWriter, r *http.Request) {
1115	userinfo := login.GetUserInfo(r)
1116	templinfo := getInfo(r)
1117	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1118	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1119	err := readviews.Execute(w, "honkers.html", templinfo)
1120	if err != nil {
1121		log.Print(err)
1122	}
1123}
1124
1125func showcombos(w http.ResponseWriter, r *http.Request) {
1126	userinfo := login.GetUserInfo(r)
1127	templinfo := getInfo(r)
1128	honkers := gethonkers(userinfo.UserID)
1129	var combos []string
1130	for _, h := range honkers {
1131		combos = append(combos, h.Combos...)
1132	}
1133	for i, c := range combos {
1134		if c == "-" {
1135			combos[i] = ""
1136		}
1137	}
1138	combos = oneofakind(combos)
1139	sort.Strings(combos)
1140	templinfo["Combos"] = combos
1141	err := readviews.Execute(w, "combos.html", templinfo)
1142	if err != nil {
1143		log.Print(err)
1144	}
1145}
1146
1147func savehonker(w http.ResponseWriter, r *http.Request) {
1148	u := login.GetUserInfo(r)
1149	name := r.FormValue("name")
1150	url := r.FormValue("url")
1151	peep := r.FormValue("peep")
1152	combos := r.FormValue("combos")
1153	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1154
1155	if honkerid > 0 {
1156		goodbye := r.FormValue("goodbye")
1157		if goodbye == "F" {
1158			db := opendatabase()
1159			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1160				honkerid, u.UserID)
1161			var xid string
1162			err := row.Scan(&xid)
1163			if err != nil {
1164				log.Printf("can't get honker xid: %s", err)
1165				return
1166			}
1167			log.Printf("unsubscribing from %s", xid)
1168			user, _ := butwhatabout(u.Username)
1169			go itakeitallback(user, xid)
1170			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1171			if err != nil {
1172				log.Printf("error updating honker: %s", err)
1173				return
1174			}
1175
1176			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1177			return
1178		}
1179		combos = " " + strings.TrimSpace(combos) + " "
1180		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1181		if err != nil {
1182			log.Printf("update honker err: %s", err)
1183			return
1184		}
1185		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1186	}
1187
1188	flavor := "presub"
1189	if peep == "peep" {
1190		flavor = "peep"
1191	}
1192	url = investigate(url)
1193	if url == "" {
1194		return
1195	}
1196	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1197	if err != nil {
1198		log.Print(err)
1199		return
1200	}
1201	if flavor == "presub" {
1202		user, _ := butwhatabout(u.Username)
1203		go subsub(user, url)
1204	}
1205	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1206}
1207
1208type Zonker struct {
1209	ID        int64
1210	Name      string
1211	Wherefore string
1212}
1213
1214func zonkzone(w http.ResponseWriter, r *http.Request) {
1215	userinfo := login.GetUserInfo(r)
1216	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1217	if err != nil {
1218		log.Printf("err: %s", err)
1219		return
1220	}
1221	defer rows.Close()
1222	var zonkers []Zonker
1223	for rows.Next() {
1224		var z Zonker
1225		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1226		zonkers = append(zonkers, z)
1227	}
1228	sort.Slice(zonkers, func(i, j int) bool {
1229		w1 := zonkers[i].Wherefore
1230		w2 := zonkers[j].Wherefore
1231		if w1 == w2 {
1232			return zonkers[i].Name < zonkers[j].Name
1233		}
1234		if w1 == "zonvoy" {
1235			w1 = "zzzzzzz"
1236		}
1237		if w2 == "zonvoy" {
1238			w2 = "zzzzzzz"
1239		}
1240		return w1 < w2
1241	})
1242
1243	templinfo := getInfo(r)
1244	templinfo["Zonkers"] = zonkers
1245	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1246	err = readviews.Execute(w, "zonkers.html", templinfo)
1247	if err != nil {
1248		log.Print(err)
1249	}
1250}
1251
1252func zonkzonk(w http.ResponseWriter, r *http.Request) {
1253	userinfo := login.GetUserInfo(r)
1254	itsok := r.FormValue("itsok")
1255	if itsok == "iforgiveyou" {
1256		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1257		db := opendatabase()
1258		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1259			userinfo.UserID, zonkerid)
1260		bitethethumbs()
1261		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1262		return
1263	}
1264	wherefore := r.FormValue("wherefore")
1265	name := r.FormValue("name")
1266	if name == "" {
1267		return
1268	}
1269	switch wherefore {
1270	case "zonker":
1271	case "zomain":
1272	case "zonvoy":
1273	case "zord":
1274	case "zilence":
1275	default:
1276		return
1277	}
1278	db := opendatabase()
1279	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1280		userinfo.UserID, name, wherefore)
1281	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1282		bitethethumbs()
1283	}
1284
1285	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1286}
1287
1288func accountpage(w http.ResponseWriter, r *http.Request) {
1289	u := login.GetUserInfo(r)
1290	user, _ := butwhatabout(u.Username)
1291	templinfo := getInfo(r)
1292	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1293	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1294	templinfo["User"] = user
1295	err := readviews.Execute(w, "account.html", templinfo)
1296	if err != nil {
1297		log.Print(err)
1298	}
1299}
1300
1301func dochpass(w http.ResponseWriter, r *http.Request) {
1302	err := login.ChangePassword(w, r)
1303	if err != nil {
1304		log.Printf("error changing password: %s", err)
1305	}
1306	http.Redirect(w, r, "/account", http.StatusSeeOther)
1307}
1308
1309func fingerlicker(w http.ResponseWriter, r *http.Request) {
1310	orig := r.FormValue("resource")
1311
1312	log.Printf("finger lick: %s", orig)
1313
1314	if strings.HasPrefix(orig, "acct:") {
1315		orig = orig[5:]
1316	}
1317
1318	name := orig
1319	idx := strings.LastIndexByte(name, '/')
1320	if idx != -1 {
1321		name = name[idx+1:]
1322		if "https://"+serverName+"/u/"+name != orig {
1323			log.Printf("foreign request rejected")
1324			name = ""
1325		}
1326	} else {
1327		idx = strings.IndexByte(name, '@')
1328		if idx != -1 {
1329			name = name[:idx]
1330			if name+"@"+serverName != orig {
1331				log.Printf("foreign request rejected")
1332				name = ""
1333			}
1334		}
1335	}
1336	user, err := butwhatabout(name)
1337	if err != nil {
1338		http.NotFound(w, r)
1339		return
1340	}
1341
1342	j := junk.New()
1343	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1344	j["aliases"] = []string{user.URL}
1345	var links []map[string]interface{}
1346	l := junk.New()
1347	l["rel"] = "self"
1348	l["type"] = `application/activity+json`
1349	l["href"] = user.URL
1350	links = append(links, l)
1351	j["links"] = links
1352
1353	w.Header().Set("Cache-Control", "max-age=3600")
1354	w.Header().Set("Content-Type", "application/jrd+json")
1355	j.Write(w)
1356}
1357
1358func somedays() string {
1359	secs := 432000 + notrand.Int63n(432000)
1360	return fmt.Sprintf("%d", secs)
1361}
1362
1363func avatate(w http.ResponseWriter, r *http.Request) {
1364	n := r.FormValue("a")
1365	a := avatar(n)
1366	w.Header().Set("Cache-Control", "max-age="+somedays())
1367	w.Write(a)
1368}
1369
1370func servecss(w http.ResponseWriter, r *http.Request) {
1371	w.Header().Set("Cache-Control", "max-age=7776000")
1372	http.ServeFile(w, r, "views"+r.URL.Path)
1373}
1374func servehtml(w http.ResponseWriter, r *http.Request) {
1375	templinfo := getInfo(r)
1376	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1377	if err != nil {
1378		log.Print(err)
1379	}
1380}
1381func serveemu(w http.ResponseWriter, r *http.Request) {
1382	xid := mux.Vars(r)["xid"]
1383	w.Header().Set("Cache-Control", "max-age="+somedays())
1384	http.ServeFile(w, r, "emus/"+xid)
1385}
1386func servememe(w http.ResponseWriter, r *http.Request) {
1387	xid := mux.Vars(r)["xid"]
1388	w.Header().Set("Cache-Control", "max-age="+somedays())
1389	http.ServeFile(w, r, "memes/"+xid)
1390}
1391
1392func servefile(w http.ResponseWriter, r *http.Request) {
1393	xid := mux.Vars(r)["xid"]
1394	row := stmtFileData.QueryRow(xid)
1395	var media string
1396	var data []byte
1397	err := row.Scan(&media, &data)
1398	if err != nil {
1399		log.Printf("error loading file: %s", err)
1400		http.NotFound(w, r)
1401		return
1402	}
1403	w.Header().Set("Content-Type", media)
1404	w.Header().Set("X-Content-Type-Options", "nosniff")
1405	w.Header().Set("Cache-Control", "max-age="+somedays())
1406	w.Write(data)
1407}
1408
1409func nomoroboto(w http.ResponseWriter, r *http.Request) {
1410	io.WriteString(w, "User-agent: *\n")
1411	io.WriteString(w, "Disallow: /a\n")
1412	io.WriteString(w, "Disallow: /d\n")
1413	io.WriteString(w, "Disallow: /meme\n")
1414	for _, u := range allusers() {
1415		fmt.Fprintf(w, "Disallow: /u/%s/h/\n", u.Username)
1416	}
1417}
1418
1419func serve() {
1420	db := opendatabase()
1421	login.Init(db)
1422
1423	listener, err := openListener()
1424	if err != nil {
1425		log.Fatal(err)
1426	}
1427	go redeliverator()
1428
1429	debug := false
1430	getconfig("debug", &debug)
1431	readviews = templates.Load(debug,
1432		"views/honkpage.html",
1433		"views/honkers.html",
1434		"views/zonkers.html",
1435		"views/combos.html",
1436		"views/honkform.html",
1437		"views/honk.html",
1438		"views/account.html",
1439		"views/about.html",
1440		"views/funzone.html",
1441		"views/login.html",
1442		"views/xzone.html",
1443		"views/header.html",
1444	)
1445	if !debug {
1446		s := "views/style.css"
1447		savedstyleparams[s] = getstyleparam(s)
1448		s = "views/local.css"
1449		savedstyleparams[s] = getstyleparam(s)
1450	}
1451
1452	bitethethumbs()
1453
1454	mux := mux.NewRouter()
1455	mux.Use(login.Checker)
1456
1457	posters := mux.Methods("POST").Subrouter()
1458	getters := mux.Methods("GET").Subrouter()
1459
1460	getters.HandleFunc("/", homepage)
1461	getters.HandleFunc("/front", homepage)
1462	getters.HandleFunc("/robots.txt", nomoroboto)
1463	getters.HandleFunc("/rss", showrss)
1464	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1465	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1466	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1467	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1468	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1469	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1470	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1471	getters.HandleFunc("/a", avatate)
1472	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1473	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1474	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1475	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1476
1477	getters.HandleFunc("/style.css", servecss)
1478	getters.HandleFunc("/local.css", servecss)
1479	getters.HandleFunc("/about", servehtml)
1480	getters.HandleFunc("/login", servehtml)
1481	posters.HandleFunc("/dologin", login.LoginFunc)
1482	getters.HandleFunc("/logout", login.LogoutFunc)
1483
1484	loggedin := mux.NewRoute().Subrouter()
1485	loggedin.Use(login.Required)
1486	loggedin.HandleFunc("/account", accountpage)
1487	loggedin.HandleFunc("/funzone", showfunzone)
1488	loggedin.HandleFunc("/chpass", dochpass)
1489	loggedin.HandleFunc("/atme", homepage)
1490	loggedin.HandleFunc("/zonkzone", zonkzone)
1491	loggedin.HandleFunc("/xzone", xzone)
1492	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1493	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1494	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1495	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1496	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1497	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1498	loggedin.HandleFunc("/honkers", showhonkers)
1499	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1500	loggedin.HandleFunc("/h", showhonker)
1501	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1502	loggedin.HandleFunc("/c", showcombos)
1503	loggedin.HandleFunc("/t", showconvoy)
1504	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1505
1506	err = http.Serve(listener, mux)
1507	if err != nil {
1508		log.Fatal(err)
1509	}
1510}
1511
1512func cleanupdb(arg string) {
1513	db := opendatabase()
1514	days, err := strconv.Atoi(arg)
1515	if err != nil {
1516		honker := arg
1517		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1518		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1519		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1520	} else {
1521		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1522		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)
1523		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)
1524	}
1525	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1526}
1527
1528var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1529var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1530var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1531var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1532var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1533var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1534var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1535var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1536
1537func preparetodie(db *sql.DB, s string) *sql.Stmt {
1538	stmt, err := db.Prepare(s)
1539	if err != nil {
1540		log.Fatalf("error %s: %s", err, s)
1541	}
1542	return stmt
1543}
1544
1545func prepareStatements(db *sql.DB) {
1546	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")
1547	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1548	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1549	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1550	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1551	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1552
1553	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 "
1554	limit := " order by honkid desc limit 250"
1555	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1556	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1557	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1558	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1559	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)
1560	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1561	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1562	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1563	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1564	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1565
1566	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1567	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1568	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1569	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1570	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1571	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1572	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1573	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1574	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1575	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1576	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1577	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1578	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1579	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1580	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1581	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1582	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1583	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1584	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1585	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1586	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1587	stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1588}
1589
1590func ElaborateUnitTests() {
1591}
1592
1593func main() {
1594	cmd := "run"
1595	if len(os.Args) > 1 {
1596		cmd = os.Args[1]
1597	}
1598	switch cmd {
1599	case "init":
1600		initdb()
1601	case "upgrade":
1602		upgradedb()
1603	}
1604	db := opendatabase()
1605	dbversion := 0
1606	getconfig("dbversion", &dbversion)
1607	if dbversion != myVersion {
1608		log.Fatal("incorrect database version. run upgrade.")
1609	}
1610	getconfig("servermsg", &serverMsg)
1611	getconfig("servername", &serverName)
1612	getconfig("dnf", &donotfedafterdark)
1613	prepareStatements(db)
1614	switch cmd {
1615	case "adduser":
1616		adduser()
1617	case "cleanup":
1618		arg := "30"
1619		if len(os.Args) > 2 {
1620			arg = os.Args[2]
1621		}
1622		cleanupdb(arg)
1623	case "ping":
1624		if len(os.Args) < 4 {
1625			fmt.Printf("usage: honk ping from to\n")
1626			return
1627		}
1628		name := os.Args[2]
1629		targ := os.Args[3]
1630		user, err := butwhatabout(name)
1631		if err != nil {
1632			log.Printf("unknown user")
1633			return
1634		}
1635		ping(user, targ)
1636	case "peep":
1637		peeppeep()
1638	case "run":
1639		serve()
1640	case "test":
1641		ElaborateUnitTests()
1642	default:
1643		log.Fatal("unknown command")
1644	}
1645}