all repos — honk @ e981a55ce775534c92d2ad3837c2015d04ddc87d

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