all repos — honk @ 555041b498c239a731c861436e3820c48b86c0ed

my fork of honk

honk.go (view raw)

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