all repos — honk @ a1adf0135fcbaab6b0fb5ee53f534fcd08619eb4

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