all repos — honk @ 9cf56cbd6be181e6bfed2509d4bb7e140550f460

my fork of honk

honk.go (view raw)

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