all repos — honk @ 34e2004eac07b14b178bc52f099f03f15919ae3e

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