all repos — honk @ d60c51960fa83b20ff4432cdaa5ff0c5a1110676

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