all repos — honk @ f592ea42a831899ec563d9d45d41b6915a90513b

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