all repos — honk @ fc566c12e724f0270061f8601d5b14592cfcc938

my fork of honk

honk.go (view raw)

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