all repos — honk @ c1d2649f1754d7ce90965d3745f3556422b5b111

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