all repos — honk @ 64176ea8e361e9a672884d51ed94d28499185b16

my fork of honk

honk.go (view raw)

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