all repos — honk @ v0.7.5

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