all repos — honk @ ab793b88d0e934bf7b621cc82b4d347db98577b6

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