all repos — honk @ 9fd970ce7e1b1de5018279892bb6bbb3e9260a25

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