all repos — honk @ 162663bf800442ac3bf3946eb3de755f990589ed

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	memetize(&honk)
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, honk.Noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1073	if err != nil {
1074		log.Printf("error saving honk: %s", err)
1075		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1076		return
1077	}
1078	honk.ID, _ = res.LastInsertId()
1079	for _, d := range honk.Donks {
1080		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1081		if err != nil {
1082			log.Printf("err saving donk: %s", err)
1083			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1084			return
1085		}
1086	}
1087
1088	go honkworldwide(user, &honk)
1089
1090	http.Redirect(w, r, xid, http.StatusSeeOther)
1091}
1092
1093func showhonkers(w http.ResponseWriter, r *http.Request) {
1094	userinfo := login.GetUserInfo(r)
1095	templinfo := getInfo(r)
1096	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1097	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1098	err := readviews.Execute(w, "honkers.html", templinfo)
1099	if err != nil {
1100		log.Print(err)
1101	}
1102}
1103
1104func showcombos(w http.ResponseWriter, r *http.Request) {
1105	userinfo := login.GetUserInfo(r)
1106	templinfo := getInfo(r)
1107	honkers := gethonkers(userinfo.UserID)
1108	var combos []string
1109	for _, h := range honkers {
1110		combos = append(combos, h.Combos...)
1111	}
1112	for i, c := range combos {
1113		if c == "-" {
1114			combos[i] = ""
1115		}
1116	}
1117	combos = oneofakind(combos)
1118	sort.Strings(combos)
1119	templinfo["Combos"] = combos
1120	err := readviews.Execute(w, "combos.html", templinfo)
1121	if err != nil {
1122		log.Print(err)
1123	}
1124}
1125
1126func savehonker(w http.ResponseWriter, r *http.Request) {
1127	u := login.GetUserInfo(r)
1128	name := r.FormValue("name")
1129	url := r.FormValue("url")
1130	peep := r.FormValue("peep")
1131	combos := r.FormValue("combos")
1132	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1133
1134	if honkerid > 0 {
1135		goodbye := r.FormValue("goodbye")
1136		if goodbye == "F" {
1137			db := opendatabase()
1138			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1139				honkerid, u.UserID)
1140			var xid string
1141			err := row.Scan(&xid)
1142			if err != nil {
1143				log.Printf("can't get honker xid: %s", err)
1144				return
1145			}
1146			log.Printf("unsubscribing from %s", xid)
1147			user, _ := butwhatabout(u.Username)
1148			go itakeitallback(user, xid)
1149			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1150			if err != nil {
1151				log.Printf("error updating honker: %s", err)
1152				return
1153			}
1154
1155			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1156			return
1157		}
1158		combos = " " + strings.TrimSpace(combos) + " "
1159		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1160		if err != nil {
1161			log.Printf("update honker err: %s", err)
1162			return
1163		}
1164		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1165	}
1166
1167	flavor := "presub"
1168	if peep == "peep" {
1169		flavor = "peep"
1170	}
1171	url = investigate(url)
1172	if url == "" {
1173		return
1174	}
1175	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1176	if err != nil {
1177		log.Print(err)
1178		return
1179	}
1180	if flavor == "presub" {
1181		user, _ := butwhatabout(u.Username)
1182		go subsub(user, url)
1183	}
1184	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1185}
1186
1187type Zonker struct {
1188	ID        int64
1189	Name      string
1190	Wherefore string
1191}
1192
1193func zonkzone(w http.ResponseWriter, r *http.Request) {
1194	userinfo := login.GetUserInfo(r)
1195	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1196	if err != nil {
1197		log.Printf("err: %s", err)
1198		return
1199	}
1200	defer rows.Close()
1201	var zonkers []Zonker
1202	for rows.Next() {
1203		var z Zonker
1204		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1205		zonkers = append(zonkers, z)
1206	}
1207	sort.Slice(zonkers, func(i, j int) bool {
1208		w1 := zonkers[i].Wherefore
1209		w2 := zonkers[j].Wherefore
1210		if w1 == w2 {
1211			return zonkers[i].Name < zonkers[j].Name
1212		}
1213		if w1 == "zonvoy" {
1214			w1 = "zzzzzzz"
1215		}
1216		if w2 == "zonvoy" {
1217			w2 = "zzzzzzz"
1218		}
1219		return w1 < w2
1220	})
1221
1222	templinfo := getInfo(r)
1223	templinfo["Zonkers"] = zonkers
1224	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1225	err = readviews.Execute(w, "zonkers.html", templinfo)
1226	if err != nil {
1227		log.Print(err)
1228	}
1229}
1230
1231func zonkzonk(w http.ResponseWriter, r *http.Request) {
1232	userinfo := login.GetUserInfo(r)
1233	itsok := r.FormValue("itsok")
1234	if itsok == "iforgiveyou" {
1235		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1236		db := opendatabase()
1237		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1238			userinfo.UserID, zonkerid)
1239		bitethethumbs()
1240		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1241		return
1242	}
1243	wherefore := r.FormValue("wherefore")
1244	name := r.FormValue("name")
1245	if name == "" {
1246		return
1247	}
1248	switch wherefore {
1249	case "zonker":
1250	case "zomain":
1251	case "zonvoy":
1252	case "zord":
1253	default:
1254		return
1255	}
1256	db := opendatabase()
1257	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1258		userinfo.UserID, name, wherefore)
1259	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" {
1260		bitethethumbs()
1261	}
1262
1263	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1264}
1265
1266func accountpage(w http.ResponseWriter, r *http.Request) {
1267	u := login.GetUserInfo(r)
1268	user, _ := butwhatabout(u.Username)
1269	templinfo := getInfo(r)
1270	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1271	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1272	templinfo["User"] = user
1273	err := readviews.Execute(w, "account.html", templinfo)
1274	if err != nil {
1275		log.Print(err)
1276	}
1277}
1278
1279func dochpass(w http.ResponseWriter, r *http.Request) {
1280	err := login.ChangePassword(w, r)
1281	if err != nil {
1282		log.Printf("error changing password: %s", err)
1283	}
1284	http.Redirect(w, r, "/account", http.StatusSeeOther)
1285}
1286
1287func fingerlicker(w http.ResponseWriter, r *http.Request) {
1288	orig := r.FormValue("resource")
1289
1290	log.Printf("finger lick: %s", orig)
1291
1292	if strings.HasPrefix(orig, "acct:") {
1293		orig = orig[5:]
1294	}
1295
1296	name := orig
1297	idx := strings.LastIndexByte(name, '/')
1298	if idx != -1 {
1299		name = name[idx+1:]
1300		if "https://"+serverName+"/u/"+name != orig {
1301			log.Printf("foreign request rejected")
1302			name = ""
1303		}
1304	} else {
1305		idx = strings.IndexByte(name, '@')
1306		if idx != -1 {
1307			name = name[:idx]
1308			if name+"@"+serverName != orig {
1309				log.Printf("foreign request rejected")
1310				name = ""
1311			}
1312		}
1313	}
1314	user, err := butwhatabout(name)
1315	if err != nil {
1316		http.NotFound(w, r)
1317		return
1318	}
1319
1320	j := junk.New()
1321	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1322	j["aliases"] = []string{user.URL}
1323	var links []map[string]interface{}
1324	l := junk.New()
1325	l["rel"] = "self"
1326	l["type"] = `application/activity+json`
1327	l["href"] = user.URL
1328	links = append(links, l)
1329	j["links"] = links
1330
1331	w.Header().Set("Cache-Control", "max-age=3600")
1332	w.Header().Set("Content-Type", "application/jrd+json")
1333	j.Write(w)
1334}
1335
1336func somedays() string {
1337	secs := 432000 + notrand.Int63n(432000)
1338	return fmt.Sprintf("%d", secs)
1339}
1340
1341func avatate(w http.ResponseWriter, r *http.Request) {
1342	n := r.FormValue("a")
1343	a := avatar(n)
1344	w.Header().Set("Cache-Control", "max-age="+somedays())
1345	w.Write(a)
1346}
1347
1348func servecss(w http.ResponseWriter, r *http.Request) {
1349	w.Header().Set("Cache-Control", "max-age=7776000")
1350	http.ServeFile(w, r, "views"+r.URL.Path)
1351}
1352func servehtml(w http.ResponseWriter, r *http.Request) {
1353	templinfo := getInfo(r)
1354	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1355	if err != nil {
1356		log.Print(err)
1357	}
1358}
1359func serveemu(w http.ResponseWriter, r *http.Request) {
1360	xid := mux.Vars(r)["xid"]
1361	w.Header().Set("Cache-Control", "max-age="+somedays())
1362	http.ServeFile(w, r, "emus/"+xid)
1363}
1364func servememe(w http.ResponseWriter, r *http.Request) {
1365	xid := mux.Vars(r)["xid"]
1366	w.Header().Set("Cache-Control", "max-age="+somedays())
1367	http.ServeFile(w, r, "memes/"+xid)
1368}
1369
1370func servefile(w http.ResponseWriter, r *http.Request) {
1371	xid := mux.Vars(r)["xid"]
1372	row := stmtFileData.QueryRow(xid)
1373	var media string
1374	var data []byte
1375	err := row.Scan(&media, &data)
1376	if err != nil {
1377		log.Printf("error loading file: %s", err)
1378		http.NotFound(w, r)
1379		return
1380	}
1381	w.Header().Set("Content-Type", media)
1382	w.Header().Set("X-Content-Type-Options", "nosniff")
1383	w.Header().Set("Cache-Control", "max-age="+somedays())
1384	w.Write(data)
1385}
1386
1387func nomoroboto(w http.ResponseWriter, r *http.Request) {
1388	io.WriteString(w, "User-agent: *\n")
1389	io.WriteString(w, "Disallow: /t\n")
1390	for _, u := range allusers() {
1391		fmt.Fprintf(w, "Disallow: /u/%s/h/\n", u.Username)
1392	}
1393}
1394
1395func serve() {
1396	db := opendatabase()
1397	login.Init(db)
1398
1399	listener, err := openListener()
1400	if err != nil {
1401		log.Fatal(err)
1402	}
1403	go redeliverator()
1404
1405	debug := false
1406	getconfig("debug", &debug)
1407	readviews = templates.Load(debug,
1408		"views/honkpage.html",
1409		"views/honkers.html",
1410		"views/zonkers.html",
1411		"views/combos.html",
1412		"views/honkform.html",
1413		"views/honk.html",
1414		"views/account.html",
1415		"views/about.html",
1416		"views/funzone.html",
1417		"views/login.html",
1418		"views/xzone.html",
1419		"views/header.html",
1420	)
1421	if !debug {
1422		s := "views/style.css"
1423		savedstyleparams[s] = getstyleparam(s)
1424		s = "views/local.css"
1425		savedstyleparams[s] = getstyleparam(s)
1426	}
1427
1428	bitethethumbs()
1429
1430	mux := mux.NewRouter()
1431	mux.Use(login.Checker)
1432
1433	posters := mux.Methods("POST").Subrouter()
1434	getters := mux.Methods("GET").Subrouter()
1435
1436	getters.HandleFunc("/", homepage)
1437	getters.HandleFunc("/front", homepage)
1438	getters.HandleFunc("/robots.txt", nomoroboto)
1439	getters.HandleFunc("/rss", showrss)
1440	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1441	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1442	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1443	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1444	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1445	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1446	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1447	getters.HandleFunc("/a", avatate)
1448	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1449	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1450	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1451	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1452
1453	getters.HandleFunc("/style.css", servecss)
1454	getters.HandleFunc("/local.css", servecss)
1455	getters.HandleFunc("/about", servehtml)
1456	getters.HandleFunc("/login", servehtml)
1457	posters.HandleFunc("/dologin", login.LoginFunc)
1458	getters.HandleFunc("/logout", login.LogoutFunc)
1459
1460	loggedin := mux.NewRoute().Subrouter()
1461	loggedin.Use(login.Required)
1462	loggedin.HandleFunc("/account", accountpage)
1463	loggedin.HandleFunc("/funzone", showfunzone)
1464	loggedin.HandleFunc("/chpass", dochpass)
1465	loggedin.HandleFunc("/atme", homepage)
1466	loggedin.HandleFunc("/zonkzone", zonkzone)
1467	loggedin.HandleFunc("/xzone", xzone)
1468	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1469	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1470	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1471	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1472	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1473	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1474	loggedin.HandleFunc("/honkers", showhonkers)
1475	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1476	loggedin.HandleFunc("/h", showhonker)
1477	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1478	loggedin.HandleFunc("/c", showcombos)
1479	loggedin.HandleFunc("/t", showconvoy)
1480	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1481
1482	err = http.Serve(listener, mux)
1483	if err != nil {
1484		log.Fatal(err)
1485	}
1486}
1487
1488func cleanupdb(days int) {
1489	db := opendatabase()
1490	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1491	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)
1492	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)
1493	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1494}
1495
1496func reducedb(honker string) {
1497	db := opendatabase()
1498	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1499	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1500	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1501	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1502}
1503
1504var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1505var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1506var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1507var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1508var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1509var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1510var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1511var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1512
1513func preparetodie(db *sql.DB, s string) *sql.Stmt {
1514	stmt, err := db.Prepare(s)
1515	if err != nil {
1516		log.Fatalf("error %s: %s", err, s)
1517	}
1518	return stmt
1519}
1520
1521func prepareStatements(db *sql.DB) {
1522	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")
1523	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1524	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1525	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1526	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1527	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1528
1529	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 "
1530	limit := " order by honkid desc limit 250"
1531	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1532	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1533	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1534	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1535	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)
1536	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1537	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1538	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and honker = ?"+butnotthose+limit)
1539	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1540	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1541
1542	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1543	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1544	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1545	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1546	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1547	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1548	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1549	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1550	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1551	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1552	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1553	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1554	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1555	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1556	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord')")
1557	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1558	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1559	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1560	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1561	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1562	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1563	stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1564}
1565
1566func ElaborateUnitTests() {
1567}
1568
1569func main() {
1570	var err error
1571	cmd := "run"
1572	if len(os.Args) > 1 {
1573		cmd = os.Args[1]
1574	}
1575	switch cmd {
1576	case "init":
1577		initdb()
1578	case "upgrade":
1579		upgradedb()
1580	}
1581	db := opendatabase()
1582	dbversion := 0
1583	getconfig("dbversion", &dbversion)
1584	if dbversion != myVersion {
1585		log.Fatal("incorrect database version. run upgrade.")
1586	}
1587	getconfig("servermsg", &serverMsg)
1588	getconfig("servername", &serverName)
1589	getconfig("dnf", &donotfedafterdark)
1590	prepareStatements(db)
1591	switch cmd {
1592	case "adduser":
1593		adduser()
1594	case "cleanup":
1595		days := 30
1596		if len(os.Args) > 2 {
1597			days, err = strconv.Atoi(os.Args[2])
1598			if err != nil {
1599				log.Fatal(err)
1600			}
1601		}
1602		cleanupdb(days)
1603	case "reduce":
1604		if len(os.Args) < 3 {
1605			log.Fatal("need a honker name")
1606		}
1607		reducedb(os.Args[2])
1608	case "ping":
1609		if len(os.Args) < 4 {
1610			fmt.Printf("usage: honk ping from to\n")
1611			return
1612		}
1613		name := os.Args[2]
1614		targ := os.Args[3]
1615		user, err := butwhatabout(name)
1616		if err != nil {
1617			log.Printf("unknown user")
1618			return
1619		}
1620		ping(user, targ)
1621	case "peep":
1622		peeppeep()
1623	case "run":
1624		serve()
1625	case "test":
1626		ElaborateUnitTests()
1627	default:
1628		log.Fatal("unknown command")
1629	}
1630}