all repos — honk @ fe478b7ebe3994b48eeebe5416a4ea1cab7da4f5

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