all repos — honk @ 58bf6f26077efe66d531e99222cbc5a26b775832

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