all repos — honk @ 46df8b5ced26c13c8e746f3812d05b7b516126a7

my fork of honk

honk.go (view raw)

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