all repos — honk @ df3bfebfee550ce8f851fc03c53c72c0da3b2176

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