all repos — honk @ 0c94d295a84efc76854b820b0e5313890872e8b9

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			stmtZonkDonks.Exec(xonk.ID)
 931			stmtZonkIt.Exec(userinfo.UserID, what)
 932			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 933				zonk := Honk{
 934					What:     "zonk",
 935					XID:      xonk.XID,
 936					Date:     time.Now().UTC(),
 937					Audience: oneofakind(xonk.Audience),
 938				}
 939				zonk.Public = !keepitquiet(zonk.Audience)
 940
 941				user, _ := butwhatabout(userinfo.Username)
 942				log.Printf("announcing deleted honk: %s", what)
 943				go honkworldwide(user, &zonk)
 944			}
 945		}
 946	}
 947	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 948	if err != nil {
 949		log.Printf("error saving zonker: %s", err)
 950		return
 951	}
 952}
 953
 954func savehonk(w http.ResponseWriter, r *http.Request) {
 955	rid := r.FormValue("rid")
 956	noise := r.FormValue("noise")
 957
 958	userinfo := login.GetUserInfo(r)
 959	user, _ := butwhatabout(userinfo.Username)
 960
 961	dt := time.Now().UTC()
 962	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
 963	what := "honk"
 964	if rid != "" {
 965		what = "tonk"
 966	}
 967	honk := Honk{
 968		UserID:   userinfo.UserID,
 969		Username: userinfo.Username,
 970		What:     "honk",
 971		Honker:   user.URL,
 972		XID:      xid,
 973		Date:     dt,
 974	}
 975	if strings.HasPrefix(noise, "DZ:") {
 976		idx := strings.Index(noise, "\n")
 977		if idx == -1 {
 978			honk.Precis = noise
 979			noise = ""
 980		} else {
 981			honk.Precis = noise[:idx]
 982			noise = noise[idx+1:]
 983		}
 984	}
 985	noise = hooterize(noise)
 986	noise = strings.TrimSpace(noise)
 987	honk.Precis = strings.TrimSpace(honk.Precis)
 988
 989	var convoy string
 990	if rid != "" {
 991		xonk := getxonk(userinfo.UserID, rid)
 992		if xonk != nil {
 993			if xonk.Public {
 994				honk.Audience = append(honk.Audience, xonk.Audience...)
 995			}
 996			convoy = xonk.Convoy
 997		} else {
 998			xonkaud, c := whosthere(rid)
 999			honk.Audience = append(honk.Audience, xonkaud...)
1000			convoy = c
1001		}
1002		for i, a := range honk.Audience {
1003			if a == thewholeworld {
1004				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1005				break
1006			}
1007		}
1008		honk.RID = rid
1009	} else {
1010		honk.Audience = []string{thewholeworld}
1011	}
1012	if noise != "" && noise[0] == '@' {
1013		honk.Audience = append(grapevine(noise), honk.Audience...)
1014	} else {
1015		honk.Audience = append(honk.Audience, grapevine(noise)...)
1016	}
1017	if convoy == "" {
1018		convoy = "data:,electrichonkytonk-" + xfiltrate()
1019	}
1020	butnottooloud(honk.Audience)
1021	honk.Audience = oneofakind(honk.Audience)
1022	if len(honk.Audience) == 0 {
1023		log.Printf("honk to nowhere")
1024		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1025		return
1026	}
1027	honk.Public = !keepitquiet(honk.Audience)
1028	noise = obfusbreak(noise)
1029	honk.Noise = noise
1030	honk.Convoy = convoy
1031
1032	donkxid := r.FormValue("donkxid")
1033	if donkxid == "" {
1034		file, filehdr, err := r.FormFile("donk")
1035		if err == nil {
1036			var buf bytes.Buffer
1037			io.Copy(&buf, file)
1038			file.Close()
1039			data := buf.Bytes()
1040			xid := xfiltrate()
1041			var media, name string
1042			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1043			if err == nil {
1044				data = img.Data
1045				format := img.Format
1046				media = "image/" + format
1047				if format == "jpeg" {
1048					format = "jpg"
1049				}
1050				name = xid + "." + format
1051				xid = name
1052			} else {
1053				maxsize := 100000
1054				if len(data) > maxsize {
1055					log.Printf("bad image: %s too much text: %d", err, len(data))
1056					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1057					return
1058				}
1059				for i := 0; i < len(data); i++ {
1060					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1061						log.Printf("bad image: %s not text: %d", err, data[i])
1062						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1063						return
1064					}
1065				}
1066				media = "text/plain"
1067				name = filehdr.Filename
1068				if name == "" {
1069					name = xid + ".txt"
1070				}
1071				xid += ".txt"
1072			}
1073			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1074			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1075			if err != nil {
1076				log.Printf("unable to save image: %s", err)
1077				return
1078			}
1079			var d Donk
1080			d.FileID, _ = res.LastInsertId()
1081			d.XID = name
1082			d.Name = name
1083			d.Media = media
1084			d.URL = url
1085			d.Local = true
1086			honk.Donks = append(honk.Donks, &d)
1087			donkxid = d.XID
1088		}
1089	} else {
1090		xid := donkxid
1091		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1092		var donk Donk
1093		row := stmtFindFile.QueryRow(url)
1094		err := row.Scan(&donk.FileID)
1095		if err == nil {
1096			donk.XID = xid
1097			donk.Local = true
1098			donk.URL = url
1099			honk.Donks = append(honk.Donks, &donk)
1100		} else {
1101			log.Printf("can't find file: %s", xid)
1102		}
1103	}
1104	herd := herdofemus(honk.Noise)
1105	for _, e := range herd {
1106		donk := savedonk(e.ID, e.Name, "image/png", true)
1107		if donk != nil {
1108			donk.Name = e.Name
1109			honk.Donks = append(honk.Donks, donk)
1110		}
1111	}
1112	memetize(&honk)
1113
1114	aud := strings.Join(honk.Audience, " ")
1115	whofore := 2
1116	if !honk.Public {
1117		whofore = 3
1118	}
1119	if r.FormValue("preview") == "preview" {
1120		honks := []*Honk{&honk}
1121		reverbolate(userinfo.UserID, honks)
1122		templinfo := getInfo(r)
1123		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1124		templinfo["Honks"] = honks
1125		templinfo["InReplyTo"] = r.FormValue("rid")
1126		templinfo["Noise"] = r.FormValue("noise")
1127		templinfo["SavedFile"] = donkxid
1128		templinfo["ServerMessage"] = "honk preview"
1129		err := readviews.Execute(w, "honkpage.html", templinfo)
1130		if err != nil {
1131			log.Print(err)
1132		}
1133		return
1134	}
1135	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1136		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1137	if err != nil {
1138		log.Printf("error saving honk: %s", err)
1139		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1140		return
1141	}
1142	honk.ID, _ = res.LastInsertId()
1143	for _, d := range honk.Donks {
1144		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1145		if err != nil {
1146			log.Printf("err saving donk: %s", err)
1147			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1148			return
1149		}
1150	}
1151
1152	if bloat_iscounter(&honk) {
1153		go bloat_counterannounce(user, &honk)
1154	} else {
1155		go honkworldwide(user, &honk)
1156	}
1157
1158	http.Redirect(w, r, xid, http.StatusSeeOther)
1159}
1160
1161func showhonkers(w http.ResponseWriter, r *http.Request) {
1162	userinfo := login.GetUserInfo(r)
1163	templinfo := getInfo(r)
1164	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1165	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1166	err := readviews.Execute(w, "honkers.html", templinfo)
1167	if err != nil {
1168		log.Print(err)
1169	}
1170}
1171
1172func showcombos(w http.ResponseWriter, r *http.Request) {
1173	userinfo := login.GetUserInfo(r)
1174	templinfo := getInfo(r)
1175	honkers := gethonkers(userinfo.UserID)
1176	var combos []string
1177	for _, h := range honkers {
1178		combos = append(combos, h.Combos...)
1179	}
1180	for i, c := range combos {
1181		if c == "-" {
1182			combos[i] = ""
1183		}
1184	}
1185	combos = oneofakind(combos)
1186	sort.Strings(combos)
1187	templinfo["Combos"] = combos
1188	err := readviews.Execute(w, "combos.html", templinfo)
1189	if err != nil {
1190		log.Print(err)
1191	}
1192}
1193
1194func savehonker(w http.ResponseWriter, r *http.Request) {
1195	u := login.GetUserInfo(r)
1196	name := r.FormValue("name")
1197	url := r.FormValue("url")
1198	peep := r.FormValue("peep")
1199	combos := r.FormValue("combos")
1200	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1201
1202	if honkerid > 0 {
1203		goodbye := r.FormValue("goodbye")
1204		if goodbye == "F" {
1205			db := opendatabase()
1206			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1207				honkerid, u.UserID)
1208			var xid string
1209			err := row.Scan(&xid)
1210			if err != nil {
1211				log.Printf("can't get honker xid: %s", err)
1212				return
1213			}
1214			log.Printf("unsubscribing from %s", xid)
1215			user, _ := butwhatabout(u.Username)
1216			go itakeitallback(user, xid)
1217			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1218			if err != nil {
1219				log.Printf("error updating honker: %s", err)
1220				return
1221			}
1222
1223			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1224			return
1225		}
1226		combos = " " + strings.TrimSpace(combos) + " "
1227		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1228		if err != nil {
1229			log.Printf("update honker err: %s", err)
1230			return
1231		}
1232		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1233	}
1234
1235	flavor := "presub"
1236	if peep == "peep" {
1237		flavor = "peep"
1238	}
1239	p := investigate(url)
1240	if p == nil {
1241		log.Printf("failed to investigate honker")
1242		return
1243	}
1244	url = p.XID
1245	if name == "" {
1246		name = p.Handle
1247	}
1248	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1249	if err != nil {
1250		log.Print(err)
1251		return
1252	}
1253	if flavor == "presub" {
1254		user, _ := butwhatabout(u.Username)
1255		go subsub(user, url)
1256	}
1257	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1258}
1259
1260type Zonker struct {
1261	ID        int64
1262	Name      string
1263	Wherefore string
1264}
1265
1266func zonkzone(w http.ResponseWriter, r *http.Request) {
1267	userinfo := login.GetUserInfo(r)
1268	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1269	if err != nil {
1270		log.Printf("err: %s", err)
1271		return
1272	}
1273	defer rows.Close()
1274	var zonkers []Zonker
1275	for rows.Next() {
1276		var z Zonker
1277		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1278		zonkers = append(zonkers, z)
1279	}
1280	sort.Slice(zonkers, func(i, j int) bool {
1281		w1 := zonkers[i].Wherefore
1282		w2 := zonkers[j].Wherefore
1283		if w1 == w2 {
1284			return zonkers[i].Name < zonkers[j].Name
1285		}
1286		if w1 == "zonvoy" {
1287			w1 = "zzzzzzz"
1288		}
1289		if w2 == "zonvoy" {
1290			w2 = "zzzzzzz"
1291		}
1292		return w1 < w2
1293	})
1294
1295	templinfo := getInfo(r)
1296	templinfo["Zonkers"] = zonkers
1297	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1298	err = readviews.Execute(w, "zonkers.html", templinfo)
1299	if err != nil {
1300		log.Print(err)
1301	}
1302}
1303
1304func zonkzonk(w http.ResponseWriter, r *http.Request) {
1305	userinfo := login.GetUserInfo(r)
1306	itsok := r.FormValue("itsok")
1307	if itsok == "iforgiveyou" {
1308		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1309		db := opendatabase()
1310		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1311			userinfo.UserID, zonkerid)
1312		bitethethumbs()
1313		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1314		return
1315	}
1316	wherefore := r.FormValue("wherefore")
1317	name := r.FormValue("name")
1318	if name == "" {
1319		return
1320	}
1321	switch wherefore {
1322	case "zonker":
1323	case "zomain":
1324	case "zonvoy":
1325	case "zord":
1326	case "zilence":
1327	default:
1328		return
1329	}
1330	db := opendatabase()
1331	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1332		userinfo.UserID, name, wherefore)
1333	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1334		bitethethumbs()
1335	}
1336
1337	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1338}
1339
1340func accountpage(w http.ResponseWriter, r *http.Request) {
1341	u := login.GetUserInfo(r)
1342	user, _ := butwhatabout(u.Username)
1343	templinfo := getInfo(r)
1344	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1345	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1346	templinfo["User"] = user
1347	err := readviews.Execute(w, "account.html", templinfo)
1348	if err != nil {
1349		log.Print(err)
1350	}
1351}
1352
1353func dochpass(w http.ResponseWriter, r *http.Request) {
1354	err := login.ChangePassword(w, r)
1355	if err != nil {
1356		log.Printf("error changing password: %s", err)
1357	}
1358	http.Redirect(w, r, "/account", http.StatusSeeOther)
1359}
1360
1361func fingerlicker(w http.ResponseWriter, r *http.Request) {
1362	orig := r.FormValue("resource")
1363
1364	log.Printf("finger lick: %s", orig)
1365
1366	if strings.HasPrefix(orig, "acct:") {
1367		orig = orig[5:]
1368	}
1369
1370	name := orig
1371	idx := strings.LastIndexByte(name, '/')
1372	if idx != -1 {
1373		name = name[idx+1:]
1374		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1375			log.Printf("foreign request rejected")
1376			name = ""
1377		}
1378	} else {
1379		idx = strings.IndexByte(name, '@')
1380		if idx != -1 {
1381			name = name[:idx]
1382			if name+"@"+serverName != orig {
1383				log.Printf("foreign request rejected")
1384				name = ""
1385			}
1386		}
1387	}
1388	user, err := butwhatabout(name)
1389	if err != nil {
1390		http.NotFound(w, r)
1391		return
1392	}
1393
1394	j := junk.New()
1395	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1396	j["aliases"] = []string{user.URL}
1397	var links []junk.Junk
1398	l := junk.New()
1399	l["rel"] = "self"
1400	l["type"] = `application/activity+json`
1401	l["href"] = user.URL
1402	links = append(links, l)
1403	j["links"] = links
1404
1405	w.Header().Set("Cache-Control", "max-age=3600")
1406	w.Header().Set("Content-Type", "application/jrd+json")
1407	j.Write(w)
1408}
1409
1410func somedays() string {
1411	secs := 432000 + notrand.Int63n(432000)
1412	return fmt.Sprintf("%d", secs)
1413}
1414
1415func avatate(w http.ResponseWriter, r *http.Request) {
1416	n := r.FormValue("a")
1417	a := avatar(n)
1418	w.Header().Set("Cache-Control", "max-age="+somedays())
1419	w.Write(a)
1420}
1421
1422func servecss(w http.ResponseWriter, r *http.Request) {
1423	w.Header().Set("Cache-Control", "max-age=7776000")
1424	http.ServeFile(w, r, "views"+r.URL.Path)
1425}
1426func servehtml(w http.ResponseWriter, r *http.Request) {
1427	templinfo := getInfo(r)
1428	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1429	if err != nil {
1430		log.Print(err)
1431	}
1432}
1433func serveemu(w http.ResponseWriter, r *http.Request) {
1434	xid := mux.Vars(r)["xid"]
1435	w.Header().Set("Cache-Control", "max-age="+somedays())
1436	http.ServeFile(w, r, "emus/"+xid)
1437}
1438func servememe(w http.ResponseWriter, r *http.Request) {
1439	xid := mux.Vars(r)["xid"]
1440	w.Header().Set("Cache-Control", "max-age="+somedays())
1441	http.ServeFile(w, r, "memes/"+xid)
1442}
1443
1444func servefile(w http.ResponseWriter, r *http.Request) {
1445	xid := mux.Vars(r)["xid"]
1446	row := stmtFileData.QueryRow(xid)
1447	var media string
1448	var data []byte
1449	err := row.Scan(&media, &data)
1450	if err != nil {
1451		log.Printf("error loading file: %s", err)
1452		http.NotFound(w, r)
1453		return
1454	}
1455	w.Header().Set("Content-Type", media)
1456	w.Header().Set("X-Content-Type-Options", "nosniff")
1457	w.Header().Set("Cache-Control", "max-age="+somedays())
1458	w.Write(data)
1459}
1460
1461func nomoroboto(w http.ResponseWriter, r *http.Request) {
1462	io.WriteString(w, "User-agent: *\n")
1463	io.WriteString(w, "Disallow: /a\n")
1464	io.WriteString(w, "Disallow: /d\n")
1465	io.WriteString(w, "Disallow: /meme\n")
1466	for _, u := range allusers() {
1467		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1468	}
1469}
1470
1471func serve() {
1472	db := opendatabase()
1473	login.Init(db)
1474
1475	listener, err := openListener()
1476	if err != nil {
1477		log.Fatal(err)
1478	}
1479	go redeliverator()
1480
1481	debug := false
1482	getconfig("debug", &debug)
1483	readviews = templates.Load(debug,
1484		"views/honkpage.html",
1485		"views/honkers.html",
1486		"views/zonkers.html",
1487		"views/combos.html",
1488		"views/honkform.html",
1489		"views/honk.html",
1490		"views/account.html",
1491		"views/about.html",
1492		"views/funzone.html",
1493		"views/login.html",
1494		"views/xzone.html",
1495		"views/header.html",
1496	)
1497	if !debug {
1498		s := "views/style.css"
1499		savedstyleparams[s] = getstyleparam(s)
1500		s = "views/local.css"
1501		savedstyleparams[s] = getstyleparam(s)
1502	}
1503
1504	bitethethumbs()
1505
1506	mux := mux.NewRouter()
1507	mux.Use(login.Checker)
1508
1509	posters := mux.Methods("POST").Subrouter()
1510	getters := mux.Methods("GET").Subrouter()
1511
1512	getters.HandleFunc("/", homepage)
1513	getters.HandleFunc("/front", homepage)
1514	getters.HandleFunc("/robots.txt", nomoroboto)
1515	getters.HandleFunc("/rss", showrss)
1516	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1517	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1518	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1519	posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1520	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1521	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1522	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1523	getters.HandleFunc("/a", avatate)
1524	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1525	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1526	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1527	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1528
1529	getters.HandleFunc("/style.css", servecss)
1530	getters.HandleFunc("/local.css", servecss)
1531	getters.HandleFunc("/about", servehtml)
1532	getters.HandleFunc("/login", servehtml)
1533	posters.HandleFunc("/dologin", login.LoginFunc)
1534	getters.HandleFunc("/logout", login.LogoutFunc)
1535
1536	loggedin := mux.NewRoute().Subrouter()
1537	loggedin.Use(login.Required)
1538	loggedin.HandleFunc("/account", accountpage)
1539	loggedin.HandleFunc("/funzone", showfunzone)
1540	loggedin.HandleFunc("/chpass", dochpass)
1541	loggedin.HandleFunc("/atme", homepage)
1542	loggedin.HandleFunc("/zonkzone", zonkzone)
1543	loggedin.HandleFunc("/xzone", xzone)
1544	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1545	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1546	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1547	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1548	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1549	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1550	loggedin.HandleFunc("/honkers", showhonkers)
1551	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1552	loggedin.HandleFunc("/h", showhonker)
1553	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1554	loggedin.HandleFunc("/c", showcombos)
1555	loggedin.HandleFunc("/t", showconvoy)
1556	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1557
1558	err = http.Serve(listener, mux)
1559	if err != nil {
1560		log.Fatal(err)
1561	}
1562}
1563
1564func cleanupdb(arg string) {
1565	db := opendatabase()
1566	days, err := strconv.Atoi(arg)
1567	if err != nil {
1568		honker := arg
1569		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1570		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1571		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1572	} else {
1573		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1574		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)
1575		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)
1576	}
1577	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1578	for _, u := range allusers() {
1579		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)
1580	}
1581}
1582
1583var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1584var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1585var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1586var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1587var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1588var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1589var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1590var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1591
1592func preparetodie(db *sql.DB, s string) *sql.Stmt {
1593	stmt, err := db.Prepare(s)
1594	if err != nil {
1595		log.Fatalf("error %s: %s", err, s)
1596	}
1597	return stmt
1598}
1599
1600func prepareStatements(db *sql.DB) {
1601	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")
1602	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1603	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1604	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1605	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1606	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1607
1608	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 "
1609	limit := " order by honkid desc limit 250"
1610	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1611	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1612	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1613	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1614	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)
1615	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1616	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1617	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1618	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1619	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1620
1621	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1622	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1623	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1624	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1625	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1626	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1627	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1628	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1629	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1630	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1631	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1632	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1633	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1634	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1635	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1636	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1637	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1638	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1639	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1640	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1641	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1642	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")
1643}
1644
1645func ElaborateUnitTests() {
1646	bloat_undocounter()
1647}
1648
1649func main() {
1650	cmd := "run"
1651	if len(os.Args) > 1 {
1652		cmd = os.Args[1]
1653	}
1654	switch cmd {
1655	case "init":
1656		initdb()
1657	case "upgrade":
1658		upgradedb()
1659	}
1660	db := opendatabase()
1661	dbversion := 0
1662	getconfig("dbversion", &dbversion)
1663	if dbversion != myVersion {
1664		log.Fatal("incorrect database version. run upgrade.")
1665	}
1666	getconfig("servermsg", &serverMsg)
1667	getconfig("servername", &serverName)
1668	getconfig("usersep", &userSep)
1669	getconfig("honksep", &honkSep)
1670	getconfig("dnf", &donotfedafterdark)
1671	prepareStatements(db)
1672	switch cmd {
1673	case "adduser":
1674		adduser()
1675	case "cleanup":
1676		arg := "30"
1677		if len(os.Args) > 2 {
1678			arg = os.Args[2]
1679		}
1680		cleanupdb(arg)
1681	case "ping":
1682		if len(os.Args) < 4 {
1683			fmt.Printf("usage: honk ping from to\n")
1684			return
1685		}
1686		name := os.Args[2]
1687		targ := os.Args[3]
1688		user, err := butwhatabout(name)
1689		if err != nil {
1690			log.Printf("unknown user")
1691			return
1692		}
1693		ping(user, targ)
1694	case "peep":
1695		peeppeep()
1696	case "run":
1697		serve()
1698	case "test":
1699		ElaborateUnitTests()
1700	default:
1701		log.Fatal("unknown command")
1702	}
1703}