all repos — honk @ dc6446f0525e8301df2a03496f71f9c909d9edfa

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