all repos — honk @ a1d71144c7a596ca58abc230330058a8eb963170

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	j, err := GetJunk(xid)
 427	if err != nil {
 428		log.Printf("error getting external object: %s", err)
 429		return
 430	}
 431	log.Printf("importing %s", xid)
 432	u := login.GetUserInfo(r)
 433	user, _ := butwhatabout(u.Username)
 434
 435	what, _ := j.GetString("type")
 436	if what == "Person" {
 437		outbox, _ := j.GetString("outbox")
 438		gimmexonks(user, outbox)
 439		http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
 440		return
 441	}
 442	xonk := xonkxonk(user, j, originate(xid))
 443	convoy := ""
 444	if xonk != nil {
 445		convoy = xonk.Convoy
 446		savexonk(user, xonk)
 447	}
 448	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 449}
 450
 451func xzone(w http.ResponseWriter, r *http.Request) {
 452	u := login.GetUserInfo(r)
 453	var honkers []string
 454	rows, err := stmtRecentHonkers.Query(u.UserID)
 455	if err != nil {
 456		log.Printf("query err: %s", err)
 457		return
 458	}
 459	defer rows.Close()
 460	for rows.Next() {
 461		var s string
 462		rows.Scan(&s)
 463		honkers = append(honkers, s)
 464	}
 465
 466	templinfo := getInfo(r)
 467	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 468	templinfo["Honkers"] = honkers
 469	err = readviews.Execute(w, "xzone.html", templinfo)
 470	if err != nil {
 471		log.Print(err)
 472	}
 473}
 474
 475func outbox(w http.ResponseWriter, r *http.Request) {
 476	name := mux.Vars(r)["name"]
 477	user, err := butwhatabout(name)
 478	if err != nil {
 479		http.NotFound(w, r)
 480		return
 481	}
 482	if stealthed(r) {
 483		http.NotFound(w, r)
 484		return
 485	}
 486
 487	honks := gethonksbyuser(name, false)
 488
 489	var jonks []map[string]interface{}
 490	for _, h := range honks {
 491		j, _ := jonkjonk(user, h)
 492		jonks = append(jonks, j)
 493	}
 494
 495	j := junk.New()
 496	j["@context"] = itiswhatitis
 497	j["id"] = user.URL + "/outbox"
 498	j["type"] = "OrderedCollection"
 499	j["totalItems"] = len(jonks)
 500	j["orderedItems"] = jonks
 501
 502	w.Header().Set("Content-Type", theonetruename)
 503	j.Write(w)
 504}
 505
 506func emptiness(w http.ResponseWriter, r *http.Request) {
 507	name := mux.Vars(r)["name"]
 508	user, err := butwhatabout(name)
 509	if err != nil {
 510		http.NotFound(w, r)
 511		return
 512	}
 513	colname := "/followers"
 514	if strings.HasSuffix(r.URL.Path, "/following") {
 515		colname = "/following"
 516	}
 517	j := junk.New()
 518	j["@context"] = itiswhatitis
 519	j["id"] = user.URL + colname
 520	j["type"] = "OrderedCollection"
 521	j["totalItems"] = 0
 522	j["orderedItems"] = []interface{}{}
 523
 524	w.Header().Set("Content-Type", theonetruename)
 525	j.Write(w)
 526}
 527
 528func showuser(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	if friendorfoe(r.Header.Get("Accept")) {
 536		j := asjonker(user)
 537		w.Header().Set("Content-Type", theonetruename)
 538		j.Write(w)
 539		return
 540	}
 541	u := login.GetUserInfo(r)
 542	honks := gethonksbyuser(name, u != nil && u.Username == name)
 543	honkpage(w, r, u, user, honks, "")
 544}
 545
 546func showhonker(w http.ResponseWriter, r *http.Request) {
 547	u := login.GetUserInfo(r)
 548	name := mux.Vars(r)["name"]
 549	var honks []*Honk
 550	if name == "" {
 551		name = r.FormValue("xid")
 552		honks = gethonksbyxonker(u.UserID, name)
 553	} else {
 554		honks = gethonksbyhonker(u.UserID, name)
 555	}
 556	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 557}
 558
 559func showcombo(w http.ResponseWriter, r *http.Request) {
 560	name := mux.Vars(r)["name"]
 561	u := login.GetUserInfo(r)
 562	honks := gethonksbycombo(u.UserID, name)
 563	honks = osmosis(honks, u.UserID)
 564	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 565}
 566func showconvoy(w http.ResponseWriter, r *http.Request) {
 567	c := r.FormValue("c")
 568	var userid int64 = -1
 569	u := login.GetUserInfo(r)
 570	if u != nil {
 571		userid = u.UserID
 572	}
 573	honks := gethonksbyconvoy(userid, c)
 574	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 575}
 576
 577func showhonk(w http.ResponseWriter, r *http.Request) {
 578	name := mux.Vars(r)["name"]
 579	user, err := butwhatabout(name)
 580	if err != nil {
 581		http.NotFound(w, r)
 582		return
 583	}
 584	if stealthed(r) {
 585		http.NotFound(w, r)
 586		return
 587	}
 588
 589	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 590	h := getxonk(user.ID, xid)
 591	if h == nil {
 592		http.NotFound(w, r)
 593		return
 594	}
 595	u := login.GetUserInfo(r)
 596	if !h.Public {
 597		if u == nil || u.UserID != h.UserID {
 598			http.NotFound(w, r)
 599			return
 600
 601		}
 602		honkpage(w, r, u, nil, []*Honk{h}, "one honk maybe more")
 603		return
 604	}
 605	if friendorfoe(r.Header.Get("Accept")) {
 606		donksforhonks([]*Honk{h})
 607		_, j := jonkjonk(user, h)
 608		j["@context"] = itiswhatitis
 609		w.Header().Set("Content-Type", theonetruename)
 610		j.Write(w)
 611		return
 612	}
 613	honks := gethonksbyconvoy(-1, h.Convoy)
 614	honkpage(w, r, u, nil, honks, "one honk maybe more")
 615}
 616
 617func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 618	honks []*Honk, infomsg string) {
 619	reverbolate(honks)
 620	templinfo := getInfo(r)
 621	if u != nil {
 622		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 623	}
 624	if u == nil {
 625		w.Header().Set("Cache-Control", "max-age=60")
 626	}
 627	if user != nil {
 628		filt := htfilter.New()
 629		templinfo["Name"] = user.Name
 630		whatabout := user.About
 631		whatabout = obfusbreak(user.About)
 632		templinfo["WhatAbout"], _ = filt.String(whatabout)
 633	}
 634	templinfo["Honks"] = honks
 635	templinfo["ServerMessage"] = infomsg
 636	err := readviews.Execute(w, "honkpage.html", templinfo)
 637	if err != nil {
 638		log.Print(err)
 639	}
 640}
 641
 642func saveuser(w http.ResponseWriter, r *http.Request) {
 643	whatabout := r.FormValue("whatabout")
 644	u := login.GetUserInfo(r)
 645	db := opendatabase()
 646	options := ""
 647	if r.FormValue("skinny") == "skinny" {
 648		options += " skinny "
 649	}
 650	_, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
 651	if err != nil {
 652		log.Printf("error bouting what: %s", err)
 653	}
 654
 655	http.Redirect(w, r, "/account", http.StatusSeeOther)
 656}
 657
 658func gethonkers(userid int64) []*Honker {
 659	rows, err := stmtHonkers.Query(userid)
 660	if err != nil {
 661		log.Printf("error querying honkers: %s", err)
 662		return nil
 663	}
 664	defer rows.Close()
 665	var honkers []*Honker
 666	for rows.Next() {
 667		var f Honker
 668		var combos string
 669		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 670		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 671		if err != nil {
 672			log.Printf("error scanning honker: %s", err)
 673			return nil
 674		}
 675		honkers = append(honkers, &f)
 676	}
 677	return honkers
 678}
 679
 680func getdubs(userid int64) []*Honker {
 681	rows, err := stmtDubbers.Query(userid)
 682	if err != nil {
 683		log.Printf("error querying dubs: %s", err)
 684		return nil
 685	}
 686	defer rows.Close()
 687	var honkers []*Honker
 688	for rows.Next() {
 689		var f Honker
 690		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 691		if err != nil {
 692			log.Printf("error scanning honker: %s", err)
 693			return nil
 694		}
 695		honkers = append(honkers, &f)
 696	}
 697	return honkers
 698}
 699
 700func allusers() []login.UserInfo {
 701	var users []login.UserInfo
 702	rows, _ := opendatabase().Query("select userid, username from users")
 703	defer rows.Close()
 704	for rows.Next() {
 705		var u login.UserInfo
 706		rows.Scan(&u.UserID, &u.Username)
 707		users = append(users, u)
 708	}
 709	return users
 710}
 711
 712func getxonk(userid int64, xid string) *Honk {
 713	h := new(Honk)
 714	var dt, aud string
 715	row := stmtOneXonk.QueryRow(userid, xid)
 716	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 717		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 718	if err != nil {
 719		if err != sql.ErrNoRows {
 720			log.Printf("error scanning xonk: %s", err)
 721		}
 722		return nil
 723	}
 724	h.Date, _ = time.Parse(dbtimeformat, dt)
 725	h.Audience = strings.Split(aud, " ")
 726	h.Public = !keepitquiet(h.Audience)
 727	return h
 728}
 729
 730func getpublichonks() []*Honk {
 731	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 732	rows, err := stmtPublicHonks.Query(dt)
 733	return getsomehonks(rows, err)
 734}
 735func gethonksbyuser(name string, includeprivate bool) []*Honk {
 736	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 737	whofore := 2
 738	if includeprivate {
 739		whofore = 3
 740	}
 741	rows, err := stmtUserHonks.Query(whofore, name, dt)
 742	return getsomehonks(rows, err)
 743}
 744func gethonksforuser(userid int64) []*Honk {
 745	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 746	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 747	return getsomehonks(rows, err)
 748}
 749func gethonksforme(userid int64) []*Honk {
 750	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 751	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 752	return getsomehonks(rows, err)
 753}
 754func gethonksbyhonker(userid int64, honker string) []*Honk {
 755	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 756	return getsomehonks(rows, err)
 757}
 758func gethonksbyxonker(userid int64, xonker string) []*Honk {
 759	rows, err := stmtHonksByXonker.Query(userid, xonker, userid)
 760	return getsomehonks(rows, err)
 761}
 762func gethonksbycombo(userid int64, combo string) []*Honk {
 763	combo = "% " + combo + " %"
 764	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 765	return getsomehonks(rows, err)
 766}
 767func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 768	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 769	honks := getsomehonks(rows, err)
 770	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 771		honks[i], honks[j] = honks[j], honks[i]
 772	}
 773	return honks
 774}
 775
 776func getsomehonks(rows *sql.Rows, err error) []*Honk {
 777	if err != nil {
 778		log.Printf("error querying honks: %s", err)
 779		return nil
 780	}
 781	defer rows.Close()
 782	var honks []*Honk
 783	for rows.Next() {
 784		var h Honk
 785		var dt, aud string
 786		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 787			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 788		if err != nil {
 789			log.Printf("error scanning honks: %s", err)
 790			return nil
 791		}
 792		h.Date, _ = time.Parse(dbtimeformat, dt)
 793		h.Audience = strings.Split(aud, " ")
 794		h.Public = !keepitquiet(h.Audience)
 795		honks = append(honks, &h)
 796	}
 797	rows.Close()
 798	donksforhonks(honks)
 799	return honks
 800}
 801
 802func donksforhonks(honks []*Honk) {
 803	db := opendatabase()
 804	var ids []string
 805	hmap := make(map[int64]*Honk)
 806	for _, h := range honks {
 807		ids = append(ids, fmt.Sprintf("%d", h.ID))
 808		hmap[h.ID] = h
 809	}
 810	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, ","))
 811	rows, err := db.Query(q)
 812	if err != nil {
 813		log.Printf("error querying donks: %s", err)
 814		return
 815	}
 816	defer rows.Close()
 817	for rows.Next() {
 818		var hid int64
 819		var d Donk
 820		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 821		if err != nil {
 822			log.Printf("error scanning donk: %s", err)
 823			continue
 824		}
 825		h := hmap[hid]
 826		h.Donks = append(h.Donks, &d)
 827	}
 828}
 829
 830func savebonk(w http.ResponseWriter, r *http.Request) {
 831	xid := r.FormValue("xid")
 832	userinfo := login.GetUserInfo(r)
 833	user, _ := butwhatabout(userinfo.Username)
 834
 835	log.Printf("bonking %s", xid)
 836
 837	xonk := getxonk(userinfo.UserID, xid)
 838	if xonk == nil {
 839		return
 840	}
 841	if !xonk.Public {
 842		return
 843	}
 844	donksforhonks([]*Honk{xonk})
 845
 846	oonker := xonk.Oonker
 847	if oonker == "" {
 848		oonker = xonk.Honker
 849	}
 850	dt := time.Now().UTC()
 851	bonk := Honk{
 852		UserID:   userinfo.UserID,
 853		Username: userinfo.Username,
 854		What:     "bonk",
 855		Honker:   user.URL,
 856		XID:      xonk.XID,
 857		Date:     dt,
 858		Donks:    xonk.Donks,
 859		Convoy:   xonk.Convoy,
 860		Audience: []string{oonker, thewholeworld},
 861		Public:   true,
 862	}
 863
 864	aud := strings.Join(bonk.Audience, " ")
 865	whofore := 2
 866	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 867		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 868		xonk.Precis, oonker)
 869	if err != nil {
 870		log.Printf("error saving bonk: %s", err)
 871		return
 872	}
 873	bonk.ID, _ = res.LastInsertId()
 874	for _, d := range bonk.Donks {
 875		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 876		if err != nil {
 877			log.Printf("err saving donk: %s", err)
 878			return
 879		}
 880	}
 881
 882	go honkworldwide(user, &bonk)
 883}
 884
 885func zonkit(w http.ResponseWriter, r *http.Request) {
 886	wherefore := r.FormValue("wherefore")
 887	what := r.FormValue("what")
 888	switch wherefore {
 889	case "zonk":
 890	case "zonvoy":
 891	}
 892
 893	log.Printf("zonking %s %s", wherefore, what)
 894	userinfo := login.GetUserInfo(r)
 895	if wherefore == "zonk" {
 896		xonk := getxonk(userinfo.UserID, what)
 897		if xonk != nil {
 898			stmtZonkDonks.Exec(xonk.ID)
 899			stmtZonkIt.Exec(userinfo.UserID, what)
 900			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 901				zonk := Honk{
 902					What:     "zonk",
 903					XID:      xonk.XID,
 904					Date:     time.Now().UTC(),
 905					Audience: oneofakind(xonk.Audience),
 906				}
 907
 908				user, _ := butwhatabout(userinfo.Username)
 909				log.Printf("announcing deleted honk: %s", what)
 910				go honkworldwide(user, &zonk)
 911			}
 912		}
 913	}
 914	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 915	if err != nil {
 916		log.Printf("error saving zonker: %s", err)
 917		return
 918	}
 919}
 920
 921func savehonk(w http.ResponseWriter, r *http.Request) {
 922	rid := r.FormValue("rid")
 923	noise := r.FormValue("noise")
 924
 925	userinfo := login.GetUserInfo(r)
 926	user, _ := butwhatabout(userinfo.Username)
 927
 928	dt := time.Now().UTC()
 929	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 930	what := "honk"
 931	if rid != "" {
 932		what = "tonk"
 933	}
 934	honk := Honk{
 935		UserID:   userinfo.UserID,
 936		Username: userinfo.Username,
 937		What:     "honk",
 938		Honker:   user.URL,
 939		XID:      xid,
 940		Date:     dt,
 941	}
 942	if strings.HasPrefix(noise, "DZ:") {
 943		idx := strings.Index(noise, "\n")
 944		if idx == -1 {
 945			honk.Precis = noise
 946			noise = ""
 947		} else {
 948			honk.Precis = noise[:idx]
 949			noise = noise[idx+1:]
 950		}
 951	}
 952	noise = hooterize(noise)
 953	noise = strings.TrimSpace(noise)
 954	honk.Precis = strings.TrimSpace(honk.Precis)
 955
 956	var convoy string
 957	if rid != "" {
 958		xonk := getxonk(userinfo.UserID, rid)
 959		if xonk != nil {
 960			if xonk.Public {
 961				honk.Audience = append(honk.Audience, xonk.Audience...)
 962			}
 963			convoy = xonk.Convoy
 964		} else {
 965			xonkaud, c := whosthere(rid)
 966			honk.Audience = append(honk.Audience, xonkaud...)
 967			convoy = c
 968		}
 969		for i, a := range honk.Audience {
 970			if a == thewholeworld {
 971				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 972				break
 973			}
 974		}
 975		honk.RID = rid
 976	} else {
 977		honk.Audience = []string{thewholeworld}
 978	}
 979	if noise != "" && noise[0] == '@' {
 980		honk.Audience = append(grapevine(noise), honk.Audience...)
 981	} else {
 982		honk.Audience = append(honk.Audience, grapevine(noise)...)
 983	}
 984	if convoy == "" {
 985		convoy = "data:,electrichonkytonk-" + xfiltrate()
 986	}
 987	butnottooloud(honk.Audience)
 988	honk.Audience = oneofakind(honk.Audience)
 989	if len(honk.Audience) == 0 {
 990		log.Printf("honk to nowhere")
 991		http.Error(w, "honk to nowhere...", http.StatusNotFound)
 992		return
 993	}
 994	honk.Public = !keepitquiet(honk.Audience)
 995	noise = obfusbreak(noise)
 996	honk.Noise = noise
 997	honk.Convoy = convoy
 998
 999	file, filehdr, err := r.FormFile("donk")
1000	if err == nil {
1001		var buf bytes.Buffer
1002		io.Copy(&buf, file)
1003		file.Close()
1004		data := buf.Bytes()
1005		xid := xfiltrate()
1006		var media, name string
1007		img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1008		if err == nil {
1009			data = img.Data
1010			format := img.Format
1011			media = "image/" + format
1012			if format == "jpeg" {
1013				format = "jpg"
1014			}
1015			name = xid + "." + format
1016			xid = name
1017		} else {
1018			maxsize := 100000
1019			if len(data) > maxsize {
1020				log.Printf("bad image: %s too much text: %d", err, len(data))
1021				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1022				return
1023			}
1024			for i := 0; i < len(data); i++ {
1025				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1026					log.Printf("bad image: %s not text: %d", err, data[i])
1027					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1028					return
1029				}
1030			}
1031			media = "text/plain"
1032			name = filehdr.Filename
1033			if name == "" {
1034				name = xid + ".txt"
1035			}
1036			xid += ".txt"
1037		}
1038		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1039		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1040		if err != nil {
1041			log.Printf("unable to save image: %s", err)
1042			return
1043		}
1044		var d Donk
1045		d.FileID, _ = res.LastInsertId()
1046		d.XID = name
1047		d.Name = name
1048		d.Media = media
1049		d.URL = url
1050		d.Local = true
1051		honk.Donks = append(honk.Donks, &d)
1052	}
1053	herd := herdofemus(honk.Noise)
1054	for _, e := range herd {
1055		donk := savedonk(e.ID, e.Name, "image/png", true)
1056		if donk != nil {
1057			donk.Name = e.Name
1058			honk.Donks = append(honk.Donks, donk)
1059		}
1060	}
1061	memetize(&honk)
1062
1063	aud := strings.Join(honk.Audience, " ")
1064	whofore := 2
1065	if !honk.Public {
1066		whofore = 3
1067	}
1068	if r.FormValue("preview") == "preview" {
1069		honks := []*Honk{&honk}
1070		reverbolate(honks)
1071		templinfo := getInfo(r)
1072		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1073		templinfo["Honks"] = honks
1074		templinfo["Noise"] = r.FormValue("noise")
1075		templinfo["ServerMessage"] = "honk preview"
1076		err := readviews.Execute(w, "honkpage.html", templinfo)
1077		if err != nil {
1078			log.Print(err)
1079		}
1080		return
1081	}
1082	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1083		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1084	if err != nil {
1085		log.Printf("error saving honk: %s", err)
1086		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1087		return
1088	}
1089	honk.ID, _ = res.LastInsertId()
1090	for _, d := range honk.Donks {
1091		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1092		if err != nil {
1093			log.Printf("err saving donk: %s", err)
1094			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1095			return
1096		}
1097	}
1098
1099	go honkworldwide(user, &honk)
1100
1101	http.Redirect(w, r, xid, http.StatusSeeOther)
1102}
1103
1104func showhonkers(w http.ResponseWriter, r *http.Request) {
1105	userinfo := login.GetUserInfo(r)
1106	templinfo := getInfo(r)
1107	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1108	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1109	err := readviews.Execute(w, "honkers.html", templinfo)
1110	if err != nil {
1111		log.Print(err)
1112	}
1113}
1114
1115func showcombos(w http.ResponseWriter, r *http.Request) {
1116	userinfo := login.GetUserInfo(r)
1117	templinfo := getInfo(r)
1118	honkers := gethonkers(userinfo.UserID)
1119	var combos []string
1120	for _, h := range honkers {
1121		combos = append(combos, h.Combos...)
1122	}
1123	for i, c := range combos {
1124		if c == "-" {
1125			combos[i] = ""
1126		}
1127	}
1128	combos = oneofakind(combos)
1129	sort.Strings(combos)
1130	templinfo["Combos"] = combos
1131	err := readviews.Execute(w, "combos.html", templinfo)
1132	if err != nil {
1133		log.Print(err)
1134	}
1135}
1136
1137func savehonker(w http.ResponseWriter, r *http.Request) {
1138	u := login.GetUserInfo(r)
1139	name := r.FormValue("name")
1140	url := r.FormValue("url")
1141	peep := r.FormValue("peep")
1142	combos := r.FormValue("combos")
1143	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1144
1145	if honkerid > 0 {
1146		goodbye := r.FormValue("goodbye")
1147		if goodbye == "F" {
1148			db := opendatabase()
1149			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1150				honkerid, u.UserID)
1151			var xid string
1152			err := row.Scan(&xid)
1153			if err != nil {
1154				log.Printf("can't get honker xid: %s", err)
1155				return
1156			}
1157			log.Printf("unsubscribing from %s", xid)
1158			user, _ := butwhatabout(u.Username)
1159			go itakeitallback(user, xid)
1160			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1161			if err != nil {
1162				log.Printf("error updating honker: %s", err)
1163				return
1164			}
1165
1166			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1167			return
1168		}
1169		combos = " " + strings.TrimSpace(combos) + " "
1170		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1171		if err != nil {
1172			log.Printf("update honker err: %s", err)
1173			return
1174		}
1175		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1176	}
1177
1178	flavor := "presub"
1179	if peep == "peep" {
1180		flavor = "peep"
1181	}
1182	url = investigate(url)
1183	if url == "" {
1184		return
1185	}
1186	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1187	if err != nil {
1188		log.Print(err)
1189		return
1190	}
1191	if flavor == "presub" {
1192		user, _ := butwhatabout(u.Username)
1193		go subsub(user, url)
1194	}
1195	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1196}
1197
1198type Zonker struct {
1199	ID        int64
1200	Name      string
1201	Wherefore string
1202}
1203
1204func zonkzone(w http.ResponseWriter, r *http.Request) {
1205	userinfo := login.GetUserInfo(r)
1206	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1207	if err != nil {
1208		log.Printf("err: %s", err)
1209		return
1210	}
1211	defer rows.Close()
1212	var zonkers []Zonker
1213	for rows.Next() {
1214		var z Zonker
1215		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1216		zonkers = append(zonkers, z)
1217	}
1218	sort.Slice(zonkers, func(i, j int) bool {
1219		w1 := zonkers[i].Wherefore
1220		w2 := zonkers[j].Wherefore
1221		if w1 == w2 {
1222			return zonkers[i].Name < zonkers[j].Name
1223		}
1224		if w1 == "zonvoy" {
1225			w1 = "zzzzzzz"
1226		}
1227		if w2 == "zonvoy" {
1228			w2 = "zzzzzzz"
1229		}
1230		return w1 < w2
1231	})
1232
1233	templinfo := getInfo(r)
1234	templinfo["Zonkers"] = zonkers
1235	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1236	err = readviews.Execute(w, "zonkers.html", templinfo)
1237	if err != nil {
1238		log.Print(err)
1239	}
1240}
1241
1242func zonkzonk(w http.ResponseWriter, r *http.Request) {
1243	userinfo := login.GetUserInfo(r)
1244	itsok := r.FormValue("itsok")
1245	if itsok == "iforgiveyou" {
1246		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1247		db := opendatabase()
1248		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1249			userinfo.UserID, zonkerid)
1250		bitethethumbs()
1251		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1252		return
1253	}
1254	wherefore := r.FormValue("wherefore")
1255	name := r.FormValue("name")
1256	if name == "" {
1257		return
1258	}
1259	switch wherefore {
1260	case "zonker":
1261	case "zomain":
1262	case "zonvoy":
1263	case "zord":
1264	default:
1265		return
1266	}
1267	db := opendatabase()
1268	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1269		userinfo.UserID, name, wherefore)
1270	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" {
1271		bitethethumbs()
1272	}
1273
1274	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1275}
1276
1277func accountpage(w http.ResponseWriter, r *http.Request) {
1278	u := login.GetUserInfo(r)
1279	user, _ := butwhatabout(u.Username)
1280	templinfo := getInfo(r)
1281	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1282	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1283	templinfo["User"] = user
1284	err := readviews.Execute(w, "account.html", templinfo)
1285	if err != nil {
1286		log.Print(err)
1287	}
1288}
1289
1290func dochpass(w http.ResponseWriter, r *http.Request) {
1291	err := login.ChangePassword(w, r)
1292	if err != nil {
1293		log.Printf("error changing password: %s", err)
1294	}
1295	http.Redirect(w, r, "/account", http.StatusSeeOther)
1296}
1297
1298func fingerlicker(w http.ResponseWriter, r *http.Request) {
1299	orig := r.FormValue("resource")
1300
1301	log.Printf("finger lick: %s", orig)
1302
1303	if strings.HasPrefix(orig, "acct:") {
1304		orig = orig[5:]
1305	}
1306
1307	name := orig
1308	idx := strings.LastIndexByte(name, '/')
1309	if idx != -1 {
1310		name = name[idx+1:]
1311		if "https://"+serverName+"/u/"+name != orig {
1312			log.Printf("foreign request rejected")
1313			name = ""
1314		}
1315	} else {
1316		idx = strings.IndexByte(name, '@')
1317		if idx != -1 {
1318			name = name[:idx]
1319			if name+"@"+serverName != orig {
1320				log.Printf("foreign request rejected")
1321				name = ""
1322			}
1323		}
1324	}
1325	user, err := butwhatabout(name)
1326	if err != nil {
1327		http.NotFound(w, r)
1328		return
1329	}
1330
1331	j := junk.New()
1332	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1333	j["aliases"] = []string{user.URL}
1334	var links []map[string]interface{}
1335	l := junk.New()
1336	l["rel"] = "self"
1337	l["type"] = `application/activity+json`
1338	l["href"] = user.URL
1339	links = append(links, l)
1340	j["links"] = links
1341
1342	w.Header().Set("Cache-Control", "max-age=3600")
1343	w.Header().Set("Content-Type", "application/jrd+json")
1344	j.Write(w)
1345}
1346
1347func somedays() string {
1348	secs := 432000 + notrand.Int63n(432000)
1349	return fmt.Sprintf("%d", secs)
1350}
1351
1352func avatate(w http.ResponseWriter, r *http.Request) {
1353	n := r.FormValue("a")
1354	a := avatar(n)
1355	w.Header().Set("Cache-Control", "max-age="+somedays())
1356	w.Write(a)
1357}
1358
1359func servecss(w http.ResponseWriter, r *http.Request) {
1360	w.Header().Set("Cache-Control", "max-age=7776000")
1361	http.ServeFile(w, r, "views"+r.URL.Path)
1362}
1363func servehtml(w http.ResponseWriter, r *http.Request) {
1364	templinfo := getInfo(r)
1365	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1366	if err != nil {
1367		log.Print(err)
1368	}
1369}
1370func serveemu(w http.ResponseWriter, r *http.Request) {
1371	xid := mux.Vars(r)["xid"]
1372	w.Header().Set("Cache-Control", "max-age="+somedays())
1373	http.ServeFile(w, r, "emus/"+xid)
1374}
1375func servememe(w http.ResponseWriter, r *http.Request) {
1376	xid := mux.Vars(r)["xid"]
1377	w.Header().Set("Cache-Control", "max-age="+somedays())
1378	http.ServeFile(w, r, "memes/"+xid)
1379}
1380
1381func servefile(w http.ResponseWriter, r *http.Request) {
1382	xid := mux.Vars(r)["xid"]
1383	row := stmtFileData.QueryRow(xid)
1384	var media string
1385	var data []byte
1386	err := row.Scan(&media, &data)
1387	if err != nil {
1388		log.Printf("error loading file: %s", err)
1389		http.NotFound(w, r)
1390		return
1391	}
1392	w.Header().Set("Content-Type", media)
1393	w.Header().Set("X-Content-Type-Options", "nosniff")
1394	w.Header().Set("Cache-Control", "max-age="+somedays())
1395	w.Write(data)
1396}
1397
1398func nomoroboto(w http.ResponseWriter, r *http.Request) {
1399	io.WriteString(w, "User-agent: *\n")
1400	io.WriteString(w, "Disallow: /t\n")
1401	for _, u := range allusers() {
1402		fmt.Fprintf(w, "Disallow: /u/%s/h/\n", u.Username)
1403	}
1404}
1405
1406func serve() {
1407	db := opendatabase()
1408	login.Init(db)
1409
1410	listener, err := openListener()
1411	if err != nil {
1412		log.Fatal(err)
1413	}
1414	go redeliverator()
1415
1416	debug := false
1417	getconfig("debug", &debug)
1418	readviews = templates.Load(debug,
1419		"views/honkpage.html",
1420		"views/honkers.html",
1421		"views/zonkers.html",
1422		"views/combos.html",
1423		"views/honkform.html",
1424		"views/honk.html",
1425		"views/account.html",
1426		"views/about.html",
1427		"views/funzone.html",
1428		"views/login.html",
1429		"views/xzone.html",
1430		"views/header.html",
1431	)
1432	if !debug {
1433		s := "views/style.css"
1434		savedstyleparams[s] = getstyleparam(s)
1435		s = "views/local.css"
1436		savedstyleparams[s] = getstyleparam(s)
1437	}
1438
1439	bitethethumbs()
1440
1441	mux := mux.NewRouter()
1442	mux.Use(login.Checker)
1443
1444	posters := mux.Methods("POST").Subrouter()
1445	getters := mux.Methods("GET").Subrouter()
1446
1447	getters.HandleFunc("/", homepage)
1448	getters.HandleFunc("/front", homepage)
1449	getters.HandleFunc("/robots.txt", nomoroboto)
1450	getters.HandleFunc("/rss", showrss)
1451	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1452	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1453	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1454	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1455	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1456	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1457	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1458	getters.HandleFunc("/a", avatate)
1459	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1460	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1461	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1462	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1463
1464	getters.HandleFunc("/style.css", servecss)
1465	getters.HandleFunc("/local.css", servecss)
1466	getters.HandleFunc("/about", servehtml)
1467	getters.HandleFunc("/login", servehtml)
1468	posters.HandleFunc("/dologin", login.LoginFunc)
1469	getters.HandleFunc("/logout", login.LogoutFunc)
1470
1471	loggedin := mux.NewRoute().Subrouter()
1472	loggedin.Use(login.Required)
1473	loggedin.HandleFunc("/account", accountpage)
1474	loggedin.HandleFunc("/funzone", showfunzone)
1475	loggedin.HandleFunc("/chpass", dochpass)
1476	loggedin.HandleFunc("/atme", homepage)
1477	loggedin.HandleFunc("/zonkzone", zonkzone)
1478	loggedin.HandleFunc("/xzone", xzone)
1479	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1480	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1481	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1482	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1483	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1484	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1485	loggedin.HandleFunc("/honkers", showhonkers)
1486	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1487	loggedin.HandleFunc("/h", showhonker)
1488	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1489	loggedin.HandleFunc("/c", showcombos)
1490	loggedin.HandleFunc("/t", showconvoy)
1491	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1492
1493	err = http.Serve(listener, mux)
1494	if err != nil {
1495		log.Fatal(err)
1496	}
1497}
1498
1499func cleanupdb(days int) {
1500	db := opendatabase()
1501	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1502	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)
1503	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)
1504	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1505}
1506
1507func reducedb(honker string) {
1508	db := opendatabase()
1509	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1510	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1511	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1512	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1513}
1514
1515var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1516var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1517var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1518var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1519var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1520var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1521var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1522var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1523
1524func preparetodie(db *sql.DB, s string) *sql.Stmt {
1525	stmt, err := db.Prepare(s)
1526	if err != nil {
1527		log.Fatalf("error %s: %s", err, s)
1528	}
1529	return stmt
1530}
1531
1532func prepareStatements(db *sql.DB) {
1533	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")
1534	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1535	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1536	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1537	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1538	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1539
1540	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 "
1541	limit := " order by honkid desc limit 250"
1542	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1543	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1544	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1545	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1546	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)
1547	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1548	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1549	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and honker = ?"+butnotthose+limit)
1550	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1551	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1552
1553	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1554	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1555	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1556	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1557	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1558	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1559	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1560	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1561	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1562	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1563	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1564	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1565	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1566	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1567	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord')")
1568	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1569	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1570	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1571	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1572	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1573	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1574	stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1575}
1576
1577func ElaborateUnitTests() {
1578}
1579
1580func main() {
1581	var err error
1582	cmd := "run"
1583	if len(os.Args) > 1 {
1584		cmd = os.Args[1]
1585	}
1586	switch cmd {
1587	case "init":
1588		initdb()
1589	case "upgrade":
1590		upgradedb()
1591	}
1592	db := opendatabase()
1593	dbversion := 0
1594	getconfig("dbversion", &dbversion)
1595	if dbversion != myVersion {
1596		log.Fatal("incorrect database version. run upgrade.")
1597	}
1598	getconfig("servermsg", &serverMsg)
1599	getconfig("servername", &serverName)
1600	getconfig("dnf", &donotfedafterdark)
1601	prepareStatements(db)
1602	switch cmd {
1603	case "adduser":
1604		adduser()
1605	case "cleanup":
1606		days := 30
1607		if len(os.Args) > 2 {
1608			days, err = strconv.Atoi(os.Args[2])
1609			if err != nil {
1610				log.Fatal(err)
1611			}
1612		}
1613		cleanupdb(days)
1614	case "reduce":
1615		if len(os.Args) < 3 {
1616			log.Fatal("need a honker name")
1617		}
1618		reducedb(os.Args[2])
1619	case "ping":
1620		if len(os.Args) < 4 {
1621			fmt.Printf("usage: honk ping from to\n")
1622			return
1623		}
1624		name := os.Args[2]
1625		targ := os.Args[3]
1626		user, err := butwhatabout(name)
1627		if err != nil {
1628			log.Printf("unknown user")
1629			return
1630		}
1631		ping(user, targ)
1632	case "peep":
1633		peeppeep()
1634	case "run":
1635		serve()
1636	case "test":
1637		ElaborateUnitTests()
1638	default:
1639		log.Fatal("unknown command")
1640	}
1641}