all repos — honk @ 64b71f4cb4d76c577973af89a0df384d652c3ee9

my fork of honk

honk.go (view raw)

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