all repos — honk @ 32f5b6b4ebda692969ce1a233beed19aee663911

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