all repos — honk @ cf2a581cdc7d90e1bda06095ad7efa1981269780

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