all repos — honk @ 6c47ffec6b71064bdb4f1ff80488cb7df69d2495

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