all repos — honk @ 7a480bfe227cf390f662f72b843eba3d4849ec3a

my fork of honk

web.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	"crypto/sha512"
  21	"database/sql"
  22	"fmt"
  23	"html/template"
  24	"io"
  25	notrand "math/rand"
  26	"mime/multipart"
  27	"net/http"
  28	"net/http/fcgi"
  29	"net/url"
  30	"os"
  31	"os/signal"
  32	"regexp"
  33	"sort"
  34	"strconv"
  35	"strings"
  36	"syscall"
  37	"time"
  38	"unicode/utf8"
  39
  40	"github.com/gorilla/mux"
  41	"humungus.tedunangst.com/r/gonix"
  42	"humungus.tedunangst.com/r/webs/cache"
  43	"humungus.tedunangst.com/r/webs/gencache"
  44	"humungus.tedunangst.com/r/webs/httpsig"
  45	"humungus.tedunangst.com/r/webs/junk"
  46	"humungus.tedunangst.com/r/webs/login"
  47	"humungus.tedunangst.com/r/webs/rss"
  48	"humungus.tedunangst.com/r/webs/templates"
  49)
  50
  51var readviews *templates.Template
  52
  53var userSep = "u"
  54var honkSep = "h"
  55
  56var develMode = false
  57
  58var allemus []Emu
  59
  60func getuserstyle(u *login.UserInfo) template.HTMLAttr {
  61	if u == nil {
  62		return ""
  63	}
  64	user, _ := butwhatabout(u.Username)
  65	class := template.HTMLAttr("")
  66	if user.Options.SkinnyCSS {
  67		class += `class="skinny"`
  68	}
  69	return class
  70}
  71
  72func getmaplink(u *login.UserInfo) string {
  73	if u == nil {
  74		return "osm"
  75	}
  76	user, _ := butwhatabout(u.Username)
  77	ml := user.Options.MapLink
  78	if ml == "" {
  79		ml = "osm"
  80	}
  81	return ml
  82}
  83
  84func getInfo(r *http.Request) map[string]interface{} {
  85	templinfo := make(map[string]interface{})
  86	templinfo["StyleParam"] = getassetparam(viewDir + "/views/style.css")
  87	templinfo["LocalStyleParam"] = getassetparam(dataDir + "/views/local.css")
  88	templinfo["JSParam"] = getassetparam(viewDir + "/views/honkpage.js")
  89	templinfo["MiscJSParam"] = getassetparam(viewDir + "/views/misc.js")
  90	templinfo["LocalJSParam"] = getassetparam(dataDir + "/views/local.js")
  91	templinfo["ServerName"] = serverName
  92	templinfo["IconName"] = iconName
  93	templinfo["UserSep"] = userSep
  94	if r == nil {
  95		return templinfo
  96	}
  97	if u := login.GetUserInfo(r); u != nil {
  98		templinfo["UserInfo"], _ = butwhatabout(u.Username)
  99		templinfo["UserStyle"] = getuserstyle(u)
 100		var combos []string
 101		combocache.Get(u.UserID, &combos)
 102		templinfo["Combos"] = combos
 103	}
 104	return templinfo
 105}
 106
 107var oldnews = gencache.New(gencache.Options[string, []byte]{
 108	Fill: func(url string) ([]byte, bool) {
 109		templinfo := getInfo(nil)
 110		var honks []*Honk
 111		var userid int64 = -1
 112
 113		templinfo["ServerMessage"] = serverMsg
 114		switch url {
 115		case "/events":
 116			honks = geteventhonks(userid)
 117			templinfo["ServerMessage"] = "some recent and upcoming events"
 118		default:
 119			templinfo["ShowRSS"] = true
 120			honks = getpublichonks()
 121		}
 122		reverbolate(userid, honks)
 123		templinfo["Honks"] = honks
 124		templinfo["MapLink"] = getmaplink(nil)
 125		var buf bytes.Buffer
 126		err := readviews.Execute(&buf, "honkpage.html", templinfo)
 127		if err != nil {
 128			elog.Print(err)
 129		}
 130		return buf.Bytes(), true
 131
 132	},
 133	Duration: 1 * time.Minute,
 134})
 135
 136func lonelypage(w http.ResponseWriter, r *http.Request) {
 137	page, _ := oldnews.Get(r.URL.Path)
 138	if !develMode {
 139		w.Header().Set("Cache-Control", "max-age=60")
 140	}
 141	w.Write(page)
 142}
 143
 144func homepage(w http.ResponseWriter, r *http.Request) {
 145	u := login.GetUserInfo(r)
 146	if u == nil {
 147		lonelypage(w, r)
 148		return
 149	}
 150	templinfo := getInfo(r)
 151	var honks []*Honk
 152	var userid int64 = -1
 153
 154	templinfo["ServerMessage"] = serverMsg
 155	if u == nil || r.URL.Path == "/front" {
 156		switch r.URL.Path {
 157		case "/events":
 158			honks = geteventhonks(userid)
 159			templinfo["ServerMessage"] = "some recent and upcoming events"
 160		default:
 161			templinfo["ShowRSS"] = true
 162			honks = getpublichonks()
 163		}
 164	} else {
 165		userid = u.UserID
 166		switch r.URL.Path {
 167		case "/atme":
 168			templinfo["ServerMessage"] = "at me!"
 169			templinfo["PageName"] = "atme"
 170			honks = gethonksforme(userid, 0)
 171			honks = osmosis(honks, userid, false)
 172			menewnone(userid)
 173			templinfo["UserInfo"], _ = butwhatabout(u.Username)
 174		case "/longago":
 175			templinfo["ServerMessage"] = "long ago and far away!"
 176			templinfo["PageName"] = "longago"
 177			honks = gethonksfromlongago(userid, 0)
 178			honks = osmosis(honks, userid, false)
 179		case "/events":
 180			templinfo["ServerMessage"] = "some recent and upcoming events"
 181			templinfo["PageName"] = "events"
 182			honks = geteventhonks(userid)
 183			honks = osmosis(honks, userid, true)
 184		case "/first":
 185			templinfo["PageName"] = "first"
 186			honks = gethonksforuserfirstclass(userid, 0)
 187			honks = osmosis(honks, userid, true)
 188		case "/saved":
 189			templinfo["ServerMessage"] = "saved honks"
 190			templinfo["PageName"] = "saved"
 191			honks = getsavedhonks(userid, 0)
 192		default:
 193			templinfo["PageName"] = "home"
 194			honks = gethonksforuser(userid, 0)
 195			honks = osmosis(honks, userid, true)
 196		}
 197		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 198	}
 199
 200	honkpage(w, u, honks, templinfo)
 201}
 202
 203func showemus(w http.ResponseWriter, r *http.Request) {
 204	templinfo := getInfo(r)
 205	templinfo["Emus"] = allemus
 206	err := readviews.Execute(w, "emus.html", templinfo)
 207	if err != nil {
 208		elog.Print(err)
 209	}
 210}
 211
 212func showfunzone(w http.ResponseWriter, r *http.Request) {
 213	var emunames, memenames []string
 214	emuext := make(map[string]string)
 215	dir, err := os.Open(dataDir + "/emus")
 216	if err == nil {
 217		emunames, _ = dir.Readdirnames(0)
 218		dir.Close()
 219	}
 220	for i, e := range emunames {
 221		if len(e) > 4 {
 222			emunames[i] = e[:len(e)-4]
 223			emuext[emunames[i]] = e[len(e)-4:]
 224		}
 225	}
 226	dir, err = os.Open(dataDir + "/memes")
 227	if err == nil {
 228		memenames, _ = dir.Readdirnames(0)
 229		dir.Close()
 230	}
 231	sort.Strings(emunames)
 232	sort.Strings(memenames)
 233	templinfo := getInfo(r)
 234	templinfo["Emus"] = emunames
 235	templinfo["Emuext"] = emuext
 236	templinfo["Memes"] = memenames
 237	err = readviews.Execute(w, "funzone.html", templinfo)
 238	if err != nil {
 239		elog.Print(err)
 240	}
 241}
 242
 243func showrss(w http.ResponseWriter, r *http.Request) {
 244	name := mux.Vars(r)["name"]
 245
 246	var honks []*Honk
 247	if name != "" {
 248		honks = gethonksbyuser(name, false, 0)
 249	} else {
 250		honks = getpublichonks()
 251	}
 252	reverbolate(-1, honks)
 253
 254	home := fmt.Sprintf("https://%s/", serverName)
 255	base := home
 256	if name != "" {
 257		home += "u/" + name
 258		name += " "
 259	}
 260	feed := rss.Feed{
 261		Title:       name + "honk",
 262		Link:        home,
 263		Description: name + "honk rss",
 264		Image: &rss.Image{
 265			URL:   base + "icon.png",
 266			Title: name + "honk rss",
 267			Link:  home,
 268		},
 269	}
 270	var modtime time.Time
 271	for _, honk := range honks {
 272		if !firstclass(honk) {
 273			continue
 274		}
 275		desc := string(honk.HTML)
 276		if t := honk.Time; t != nil {
 277			desc += fmt.Sprintf(`<p>Time: %s`, t.StartTime.Local().Format("03:04PM EDT Mon Jan 02"))
 278			if t.Duration != 0 {
 279				desc += fmt.Sprintf(`<br>Duration: %s`, t.Duration)
 280			}
 281		}
 282		if p := honk.Place; p != nil {
 283			desc += string(templates.Sprintf(`<p>Location: <a href="%s">%s</a> %f %f`,
 284				p.Url, p.Name, p.Latitude, p.Longitude))
 285		}
 286		for _, d := range honk.Donks {
 287			desc += string(templates.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 288				d.URL, d.Desc))
 289			if strings.HasPrefix(d.Media, "image") {
 290				desc += string(templates.Sprintf(`<img src="%s">`, d.URL))
 291			}
 292		}
 293
 294		feed.Items = append(feed.Items, &rss.Item{
 295			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 296			Description: rss.CData{Data: desc},
 297			Link:        honk.URL,
 298			PubDate:     honk.Date.Format(time.RFC1123),
 299			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 300		})
 301		if honk.Date.After(modtime) {
 302			modtime = honk.Date
 303		}
 304	}
 305	if !develMode {
 306		w.Header().Set("Cache-Control", "max-age=300")
 307		w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 308	}
 309
 310	err := feed.Write(w)
 311	if err != nil {
 312		elog.Printf("error writing rss: %s", err)
 313	}
 314}
 315
 316func crappola(j junk.Junk) bool {
 317	t, _ := j.GetString("type")
 318	a, _ := j.GetString("actor")
 319	o, _ := j.GetString("object")
 320	if t == "Delete" && a == o {
 321		dlog.Printf("crappola from %s", a)
 322		return true
 323	}
 324	return false
 325}
 326
 327func ping(user *WhatAbout, who string) {
 328	if targ := fullname(who, user.ID); targ != "" {
 329		who = targ
 330	}
 331	if !strings.HasPrefix(who, "https:") {
 332		who = gofish(who)
 333	}
 334	if who == "" {
 335		ilog.Printf("nobody to ping!")
 336		return
 337	}
 338	var box *Box
 339	ok := boxofboxes.Get(who, &box)
 340	if !ok {
 341		ilog.Printf("no inbox to ping %s", who)
 342		return
 343	}
 344	ilog.Printf("sending ping to %s", box.In)
 345	j := junk.New()
 346	j["@context"] = itiswhatitis
 347	j["type"] = "Ping"
 348	j["id"] = user.URL + "/ping/" + xfiltrate()
 349	j["actor"] = user.URL
 350	j["to"] = who
 351	ki := ziggy(user.ID)
 352	if ki == nil {
 353		return
 354	}
 355	err := PostJunk(ki.keyname, ki.seckey, box.In, j)
 356	if err != nil {
 357		elog.Printf("can't send ping: %s", err)
 358		return
 359	}
 360	ilog.Printf("sent ping to %s: %s", who, j["id"])
 361}
 362
 363func pong(user *WhatAbout, who string, obj string) {
 364	var box *Box
 365	ok := boxofboxes.Get(who, &box)
 366	if !ok {
 367		ilog.Printf("no inbox to pong %s", who)
 368		return
 369	}
 370	j := junk.New()
 371	j["@context"] = itiswhatitis
 372	j["type"] = "Pong"
 373	j["id"] = user.URL + "/pong/" + xfiltrate()
 374	j["actor"] = user.URL
 375	j["to"] = who
 376	j["object"] = obj
 377	ki := ziggy(user.ID)
 378	if ki == nil {
 379		return
 380	}
 381	err := PostJunk(ki.keyname, ki.seckey, box.In, j)
 382	if err != nil {
 383		elog.Printf("can't send pong: %s", err)
 384		return
 385	}
 386}
 387
 388func inbox(w http.ResponseWriter, r *http.Request) {
 389	name := mux.Vars(r)["name"]
 390	user, err := butwhatabout(name)
 391	if err != nil {
 392		http.NotFound(w, r)
 393		return
 394	}
 395	if stealthmode(user.ID, r) {
 396		http.NotFound(w, r)
 397		return
 398	}
 399	var buf bytes.Buffer
 400	limiter := io.LimitReader(r.Body, 1*1024*1024)
 401	io.Copy(&buf, limiter)
 402	payload := buf.Bytes()
 403	j, err := junk.FromBytes(payload)
 404	if err != nil {
 405		ilog.Printf("bad payload: %s", err)
 406		ilog.Writer().Write(payload)
 407		ilog.Writer().Write([]byte{'\n'})
 408		return
 409	}
 410
 411	if crappola(j) {
 412		return
 413	}
 414	what := firstofmany(j, "type")
 415	obj, _ := j.GetString("object")
 416	if what == "Like" || what == "Dislike" || (what == "EmojiReact" && originate(obj) != serverName) {
 417		return
 418	}
 419	who, _ := j.GetString("actor")
 420	if rejectactor(user.ID, who) {
 421		return
 422	}
 423
 424	keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
 425	if err != nil && keyname != "" {
 426		savingthrow(keyname)
 427		keyname, err = httpsig.VerifyRequest(r, payload, zaggy)
 428	}
 429	if err != nil {
 430		ilog.Printf("inbox message failed signature for %s from %s: %s", keyname, r.Header.Get("X-Forwarded-For"), err)
 431		if keyname != "" {
 432			ilog.Printf("bad signature from %s", keyname)
 433		}
 434		http.Error(w, "what did you call me?", http.StatusTeapot)
 435		return
 436	}
 437	origin := keymatch(keyname, who)
 438	if origin == "" {
 439		ilog.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 440		return
 441	}
 442
 443	switch what {
 444	case "Ping":
 445		id, _ := j.GetString("id")
 446		ilog.Printf("ping from %s: %s", who, id)
 447		pong(user, who, id)
 448	case "Pong":
 449		ilog.Printf("pong from %s: %s", who, obj)
 450	case "Follow":
 451		if obj != user.URL {
 452			ilog.Printf("can't follow %s", obj)
 453			return
 454		}
 455		followme(user, who, who, j)
 456	case "Accept":
 457		followyou2(user, j)
 458	case "Reject":
 459		nofollowyou2(user, j)
 460	case "Update":
 461		obj, ok := j.GetMap("object")
 462		if ok {
 463			what := firstofmany(obj, "type")
 464			switch what {
 465			case "Service":
 466				fallthrough
 467			case "Person":
 468				return
 469			case "Question":
 470				return
 471			}
 472		}
 473		go xonksaver(user, j, origin)
 474	case "Undo":
 475		obj, ok := j.GetMap("object")
 476		if !ok {
 477			folxid, ok := j.GetString("object")
 478			if ok && originate(folxid) == origin {
 479				unfollowme(user, "", "", j)
 480			}
 481			return
 482		}
 483		what := firstofmany(obj, "type")
 484		switch what {
 485		case "Follow":
 486			unfollowme(user, who, who, j)
 487		case "Announce":
 488			xid, _ := obj.GetString("object")
 489			dlog.Printf("undo announce: %s", xid)
 490		case "Like":
 491		default:
 492			ilog.Printf("unknown undo: %s", what)
 493		}
 494	case "EmojiReact":
 495		obj, ok := j.GetString("object")
 496		if ok {
 497			content, _ := j.GetString("content")
 498			addreaction(user, obj, who, content)
 499		}
 500	default:
 501		go saveandcheck(user, j, origin)
 502	}
 503}
 504
 505func saveandcheck(user *WhatAbout, j junk.Junk, origin string) {
 506	xonk := xonksaver(user, j, origin)
 507	if xonk == nil {
 508		return
 509	}
 510	if sname := shortname(user.ID, xonk.Honker); sname == "" {
 511		dlog.Printf("received unexpected activity from %s", xonk.Honker)
 512		if xonk.Whofore == 0 {
 513			dlog.Printf("it's not even for me!")
 514		}
 515	}
 516}
 517
 518func serverinbox(w http.ResponseWriter, r *http.Request) {
 519	user := getserveruser()
 520	if stealthmode(user.ID, r) {
 521		http.NotFound(w, r)
 522		return
 523	}
 524	var buf bytes.Buffer
 525	io.Copy(&buf, r.Body)
 526	payload := buf.Bytes()
 527	j, err := junk.FromBytes(payload)
 528	if err != nil {
 529		ilog.Printf("bad payload: %s", err)
 530		ilog.Writer().Write(payload)
 531		ilog.Writer().Write([]byte{'\n'})
 532		return
 533	}
 534	if crappola(j) {
 535		return
 536	}
 537	keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
 538	if err != nil && keyname != "" {
 539		savingthrow(keyname)
 540		keyname, err = httpsig.VerifyRequest(r, payload, zaggy)
 541	}
 542	if err != nil {
 543		ilog.Printf("inbox message failed signature for %s from %s: %s", keyname, r.Header.Get("X-Forwarded-For"), err)
 544		if keyname != "" {
 545			ilog.Printf("bad signature from %s", keyname)
 546		}
 547		http.Error(w, "what did you call me?", http.StatusTeapot)
 548		return
 549	}
 550	who, _ := j.GetString("actor")
 551	origin := keymatch(keyname, who)
 552	if origin == "" {
 553		ilog.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 554		return
 555	}
 556	if rejectactor(user.ID, who) {
 557		return
 558	}
 559	re_ont := regexp.MustCompile("https://" + serverName + "/o/([\\pL[:digit:]]+)")
 560	what := firstofmany(j, "type")
 561	dlog.Printf("server got a %s", what)
 562	switch what {
 563	case "Follow":
 564		obj, _ := j.GetString("object")
 565		if obj == user.URL {
 566			ilog.Printf("can't follow the server!")
 567			return
 568		}
 569		m := re_ont.FindStringSubmatch(obj)
 570		if len(m) != 2 {
 571			ilog.Printf("not sure how to handle this")
 572			return
 573		}
 574		ont := "#" + m[1]
 575
 576		followme(user, who, ont, j)
 577	case "Undo":
 578		obj, ok := j.GetMap("object")
 579		if !ok {
 580			ilog.Printf("unknown undo no object")
 581			return
 582		}
 583		what := firstofmany(obj, "type")
 584		if what != "Follow" {
 585			ilog.Printf("unknown undo: %s", what)
 586			return
 587		}
 588		targ, _ := obj.GetString("object")
 589		m := re_ont.FindStringSubmatch(targ)
 590		if len(m) != 2 {
 591			ilog.Printf("not sure how to handle this")
 592			return
 593		}
 594		ont := "#" + m[1]
 595		unfollowme(user, who, ont, j)
 596	default:
 597		ilog.Printf("unhandled server activity: %s", what)
 598		dumpactivity(j)
 599	}
 600}
 601
 602func serveractor(w http.ResponseWriter, r *http.Request) {
 603	user := getserveruser()
 604	if stealthmode(user.ID, r) {
 605		http.NotFound(w, r)
 606		return
 607	}
 608	j := junkuser(user)
 609	j.Write(w)
 610}
 611
 612func ximport(w http.ResponseWriter, r *http.Request) {
 613	u := login.GetUserInfo(r)
 614	xid := strings.TrimSpace(r.FormValue("q"))
 615	xonk := getxonk(u.UserID, xid)
 616	if xonk == nil {
 617		p, _ := investigate(xid)
 618		if p != nil {
 619			xid = p.XID
 620		}
 621		j, err := GetJunk(u.UserID, xid)
 622		if err != nil {
 623			http.Error(w, "error getting external object", http.StatusInternalServerError)
 624			ilog.Printf("error getting external object: %s", err)
 625			return
 626		}
 627		allinjest(originate(xid), j)
 628		dlog.Printf("importing %s", xid)
 629		user, _ := butwhatabout(u.Username)
 630
 631		info, _ := somethingabout(j)
 632		if info == nil {
 633			xonk = xonksaver(user, j, originate(xid))
 634		} else if info.What == SomeActor {
 635			outbox, _ := j.GetString("outbox")
 636			gimmexonks(user, outbox)
 637			http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
 638			return
 639		} else if info.What == SomeCollection {
 640			gimmexonks(user, xid)
 641			http.Redirect(w, r, "/xzone", http.StatusSeeOther)
 642			return
 643		}
 644	}
 645	convoy := ""
 646	if xonk != nil {
 647		convoy = xonk.Convoy
 648	}
 649	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 650}
 651
 652func xzone(w http.ResponseWriter, r *http.Request) {
 653	u := login.GetUserInfo(r)
 654	rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
 655	if err != nil {
 656		elog.Printf("query err: %s", err)
 657		return
 658	}
 659	defer rows.Close()
 660	var honkers []Honker
 661	for rows.Next() {
 662		var xid string
 663		rows.Scan(&xid)
 664		honkers = append(honkers, Honker{XID: xid})
 665	}
 666	rows.Close()
 667	for i := range honkers {
 668		_, honkers[i].Handle = handles(honkers[i].XID)
 669	}
 670	templinfo := getInfo(r)
 671	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 672	templinfo["Honkers"] = honkers
 673	err = readviews.Execute(w, "xzone.html", templinfo)
 674	if err != nil {
 675		elog.Print(err)
 676	}
 677}
 678
 679var oldoutbox = cache.New(cache.Options{Filler: func(name string) ([]byte, bool) {
 680	user, err := butwhatabout(name)
 681	if err != nil {
 682		return nil, false
 683	}
 684	honks := gethonksbyuser(name, false, 0)
 685	if len(honks) > 20 {
 686		honks = honks[0:20]
 687	}
 688
 689	var jonks []junk.Junk
 690	for _, h := range honks {
 691		j, _ := jonkjonk(user, h)
 692		jonks = append(jonks, j)
 693	}
 694
 695	j := junk.New()
 696	j["@context"] = itiswhatitis
 697	j["id"] = user.URL + "/outbox"
 698	j["attributedTo"] = user.URL
 699	j["type"] = "OrderedCollection"
 700	j["totalItems"] = len(jonks)
 701	j["orderedItems"] = jonks
 702
 703	return j.ToBytes(), true
 704}, Duration: 1 * time.Minute})
 705
 706func outbox(w http.ResponseWriter, r *http.Request) {
 707	name := mux.Vars(r)["name"]
 708	user, err := butwhatabout(name)
 709	if err != nil {
 710		http.NotFound(w, r)
 711		return
 712	}
 713	if stealthmode(user.ID, r) {
 714		http.NotFound(w, r)
 715		return
 716	}
 717	var j []byte
 718	ok := oldoutbox.Get(name, &j)
 719	if ok {
 720		w.Header().Set("Content-Type", theonetruename)
 721		w.Write(j)
 722	} else {
 723		http.NotFound(w, r)
 724	}
 725}
 726
 727var oldempties = cache.New(cache.Options{Filler: func(url string) ([]byte, bool) {
 728	colname := "/followers"
 729	if strings.HasSuffix(url, "/following") {
 730		colname = "/following"
 731	}
 732	user := fmt.Sprintf("https://%s%s", serverName, url[:len(url)-10])
 733	j := junk.New()
 734	j["@context"] = itiswhatitis
 735	j["id"] = user + colname
 736	j["attributedTo"] = user
 737	j["type"] = "OrderedCollection"
 738	j["totalItems"] = 0
 739	j["orderedItems"] = []junk.Junk{}
 740
 741	return j.ToBytes(), true
 742}})
 743
 744func emptiness(w http.ResponseWriter, r *http.Request) {
 745	name := mux.Vars(r)["name"]
 746	user, err := butwhatabout(name)
 747	if err != nil {
 748		http.NotFound(w, r)
 749		return
 750	}
 751	if stealthmode(user.ID, r) {
 752		http.NotFound(w, r)
 753		return
 754	}
 755	var j []byte
 756	ok := oldempties.Get(r.URL.Path, &j)
 757	if ok {
 758		w.Header().Set("Content-Type", theonetruename)
 759		w.Write(j)
 760	} else {
 761		http.NotFound(w, r)
 762	}
 763}
 764
 765func showuser(w http.ResponseWriter, r *http.Request) {
 766	name := mux.Vars(r)["name"]
 767	user, err := butwhatabout(name)
 768	if err != nil {
 769		ilog.Printf("user not found %s: %s", name, err)
 770		http.NotFound(w, r)
 771		return
 772	}
 773	if stealthmode(user.ID, r) {
 774		http.NotFound(w, r)
 775		return
 776	}
 777	if friendorfoe(r.Header.Get("Accept")) {
 778		j, ok := asjonker(name)
 779		if ok {
 780			w.Header().Set("Content-Type", theonetruename)
 781			w.Write(j)
 782		} else {
 783			http.NotFound(w, r)
 784		}
 785		return
 786	}
 787	u := login.GetUserInfo(r)
 788	if u != nil && u.Username != name {
 789		u = nil
 790	}
 791	honks := gethonksbyuser(name, u != nil, 0)
 792	templinfo := getInfo(r)
 793	templinfo["PageName"] = "user"
 794	templinfo["PageArg"] = name
 795	templinfo["Name"] = user.Name
 796	templinfo["WhatAbout"] = user.HTAbout
 797	templinfo["ServerMessage"] = ""
 798	templinfo["APAltLink"] = templates.Sprintf("<link href='%s' rel='alternate' type='application/activity+json'>", user.URL)
 799	if u != nil {
 800		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 801	}
 802	honkpage(w, u, honks, templinfo)
 803}
 804
 805func showhonker(w http.ResponseWriter, r *http.Request) {
 806	u := login.GetUserInfo(r)
 807	name := mux.Vars(r)["name"]
 808	var honks []*Honk
 809	if name == "" {
 810		name = r.FormValue("xid")
 811		honks = gethonksbyxonker(u.UserID, name, 0)
 812	} else {
 813		honks = gethonksbyhonker(u.UserID, name, 0)
 814	}
 815	miniform := templates.Sprintf(`<form action="/submithonker" method="POST">
 816<input type="hidden" name="CSRF" value="%s">
 817<input type="hidden" name="url" value="%s">
 818<button tabindex=1 name="add honker" value="add honker">add honker</button>
 819</form>`, login.GetCSRF("submithonker", r), name)
 820	msg := templates.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>%s`, name, name, miniform)
 821	templinfo := getInfo(r)
 822	templinfo["PageName"] = "honker"
 823	templinfo["PageArg"] = name
 824	templinfo["ServerMessage"] = msg
 825	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 826	honkpage(w, u, honks, templinfo)
 827}
 828
 829func showcombo(w http.ResponseWriter, r *http.Request) {
 830	name := mux.Vars(r)["name"]
 831	u := login.GetUserInfo(r)
 832	honks := gethonksbycombo(u.UserID, name, 0)
 833	honks = osmosis(honks, u.UserID, true)
 834	templinfo := getInfo(r)
 835	templinfo["PageName"] = "combo"
 836	templinfo["PageArg"] = name
 837	templinfo["ServerMessage"] = "honks by combo: " + name
 838	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 839	honkpage(w, u, honks, templinfo)
 840}
 841func showconvoy(w http.ResponseWriter, r *http.Request) {
 842	c := r.FormValue("c")
 843	u := login.GetUserInfo(r)
 844	honks := gethonksbyconvoy(u.UserID, c, 0)
 845	templinfo := getInfo(r)
 846	if len(honks) > 0 {
 847		templinfo["TopHID"] = honks[0].ID
 848	}
 849	honks = osmosis(honks, u.UserID, false)
 850	//reversehonks(honks)
 851	honks = threadsort(honks)
 852	templinfo["PageName"] = "convoy"
 853	templinfo["PageArg"] = c
 854	templinfo["ServerMessage"] = "honks in convoy: " + c
 855	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 856	honkpage(w, u, honks, templinfo)
 857}
 858func showsearch(w http.ResponseWriter, r *http.Request) {
 859	q := r.FormValue("q")
 860	if strings.HasPrefix(q, "https://") {
 861		ximport(w, r)
 862		return
 863	}
 864	u := login.GetUserInfo(r)
 865	honks := gethonksbysearch(u.UserID, q, 0)
 866	templinfo := getInfo(r)
 867	templinfo["PageName"] = "search"
 868	templinfo["PageArg"] = q
 869	templinfo["ServerMessage"] = "honks for search: " + q
 870	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 871	honkpage(w, u, honks, templinfo)
 872}
 873func showontology(w http.ResponseWriter, r *http.Request) {
 874	name := mux.Vars(r)["name"]
 875	u := login.GetUserInfo(r)
 876	var userid int64 = -1
 877	if u != nil {
 878		userid = u.UserID
 879	}
 880	honks := gethonksbyontology(userid, "#"+name, 0)
 881	if friendorfoe(r.Header.Get("Accept")) {
 882		if len(honks) > 40 {
 883			honks = honks[0:40]
 884		}
 885
 886		var xids []string
 887		for _, h := range honks {
 888			xids = append(xids, h.XID)
 889		}
 890
 891		user := getserveruser()
 892
 893		j := junk.New()
 894		j["@context"] = itiswhatitis
 895		j["id"] = fmt.Sprintf("https://%s/o/%s", serverName, name)
 896		j["name"] = "#" + name
 897		j["attributedTo"] = user.URL
 898		j["type"] = "OrderedCollection"
 899		j["totalItems"] = len(xids)
 900		j["orderedItems"] = xids
 901
 902		j.Write(w)
 903		return
 904	}
 905
 906	templinfo := getInfo(r)
 907	templinfo["ServerMessage"] = "honks by ontology: " + name
 908	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 909	honkpage(w, u, honks, templinfo)
 910}
 911
 912type Ont struct {
 913	Name  string
 914	Count int64
 915}
 916
 917func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) {
 918	u := login.GetUserInfo(r)
 919	var userid int64 = -1
 920	if u != nil {
 921		userid = u.UserID
 922	}
 923	rows, err := stmtAllOnts.Query(userid)
 924	if err != nil {
 925		elog.Printf("selection error: %s", err)
 926		return
 927	}
 928	defer rows.Close()
 929	var onts []Ont
 930	for rows.Next() {
 931		var o Ont
 932		err := rows.Scan(&o.Name, &o.Count)
 933		if err != nil {
 934			elog.Printf("error scanning ont: %s", err)
 935			continue
 936		}
 937		if utf8.RuneCountInString(o.Name) > 24 {
 938			continue
 939		}
 940		o.Name = o.Name[1:]
 941		onts = append(onts, o)
 942	}
 943	sort.Slice(onts, func(i, j int) bool {
 944		return onts[i].Name < onts[j].Name
 945	})
 946	if u == nil && !develMode {
 947		w.Header().Set("Cache-Control", "max-age=300")
 948	}
 949	templinfo := getInfo(r)
 950	templinfo["Onts"] = onts
 951	templinfo["FirstRune"] = func(s string) rune { r, _ := utf8.DecodeRuneInString(s); return r }
 952	err = readviews.Execute(w, "onts.html", templinfo)
 953	if err != nil {
 954		elog.Print(err)
 955	}
 956}
 957
 958type Track struct {
 959	xid string
 960	who string
 961}
 962
 963func getbacktracks(xid string) []string {
 964	c := make(chan bool)
 965	dumptracks <- c
 966	<-c
 967	row := stmtGetTracks.QueryRow(xid)
 968	var rawtracks string
 969	err := row.Scan(&rawtracks)
 970	if err != nil {
 971		if err != sql.ErrNoRows {
 972			elog.Printf("error scanning tracks: %s", err)
 973		}
 974		return nil
 975	}
 976	var rcpts []string
 977	for _, f := range strings.Split(rawtracks, " ") {
 978		idx := strings.LastIndexByte(f, '#')
 979		if idx != -1 {
 980			f = f[:idx]
 981		}
 982		if !strings.HasPrefix(f, "https://") {
 983			f = fmt.Sprintf("%%https://%s/inbox", f)
 984		}
 985		rcpts = append(rcpts, f)
 986	}
 987	return rcpts
 988}
 989
 990func savetracks(tracks map[string][]string) {
 991	db := opendatabase()
 992	tx, err := db.Begin()
 993	if err != nil {
 994		elog.Printf("savetracks begin error: %s", err)
 995		return
 996	}
 997	defer func() {
 998		err := tx.Commit()
 999		if err != nil {
1000			elog.Printf("savetracks commit error: %s", err)
1001		}
1002
1003	}()
1004	stmtGetTracks, err := tx.Prepare("select fetches from tracks where xid = ?")
1005	if err != nil {
1006		elog.Printf("savetracks error: %s", err)
1007		return
1008	}
1009	stmtNewTracks, err := tx.Prepare("insert into tracks (xid, fetches) values (?, ?)")
1010	if err != nil {
1011		elog.Printf("savetracks error: %s", err)
1012		return
1013	}
1014	stmtUpdateTracks, err := tx.Prepare("update tracks set fetches = ? where xid = ?")
1015	if err != nil {
1016		elog.Printf("savetracks error: %s", err)
1017		return
1018	}
1019	count := 0
1020	for xid, f := range tracks {
1021		count += len(f)
1022		var prev string
1023		row := stmtGetTracks.QueryRow(xid)
1024		err := row.Scan(&prev)
1025		if err == sql.ErrNoRows {
1026			f = oneofakind(f)
1027			stmtNewTracks.Exec(xid, strings.Join(f, " "))
1028		} else if err == nil {
1029			all := append(strings.Split(prev, " "), f...)
1030			all = oneofakind(all)
1031			stmtUpdateTracks.Exec(strings.Join(all, " "))
1032		} else {
1033			elog.Printf("savetracks error: %s", err)
1034		}
1035	}
1036	dlog.Printf("saved %d new fetches", count)
1037}
1038
1039var trackchan = make(chan Track)
1040var dumptracks = make(chan chan bool)
1041
1042func tracker() {
1043	timeout := 4 * time.Minute
1044	sleeper := time.NewTimer(timeout)
1045	tracks := make(map[string][]string)
1046	workinprogress++
1047	for {
1048		select {
1049		case track := <-trackchan:
1050			tracks[track.xid] = append(tracks[track.xid], track.who)
1051		case <-sleeper.C:
1052			if len(tracks) > 0 {
1053				go savetracks(tracks)
1054				tracks = make(map[string][]string)
1055			}
1056			sleeper.Reset(timeout)
1057		case c := <-dumptracks:
1058			if len(tracks) > 0 {
1059				savetracks(tracks)
1060			}
1061			c <- true
1062		case <-endoftheworld:
1063			if len(tracks) > 0 {
1064				savetracks(tracks)
1065			}
1066			readyalready <- true
1067			return
1068		}
1069	}
1070}
1071
1072var re_keyholder = regexp.MustCompile(`keyId="([^"]+)"`)
1073
1074func trackback(xid string, r *http.Request) {
1075	agent := r.UserAgent()
1076	who := originate(agent)
1077	sig := r.Header.Get("Signature")
1078	if sig != "" {
1079		m := re_keyholder.FindStringSubmatch(sig)
1080		if len(m) == 2 {
1081			who = m[1]
1082		}
1083	}
1084	if who != "" {
1085		trackchan <- Track{xid: xid, who: who}
1086	}
1087}
1088
1089func sameperson(h1, h2 *Honk) bool {
1090	n1, n2 := h1.Honker, h2.Honker
1091	if h1.Oonker != "" {
1092		n1 = h1.Oonker
1093	}
1094	if h2.Oonker != "" {
1095		n2 = h2.Oonker
1096	}
1097	return n1 == n2
1098}
1099
1100func threadsort(honks []*Honk) []*Honk {
1101	sort.Slice(honks, func(i, j int) bool {
1102		return honks[i].Date.Before(honks[j].Date)
1103	})
1104	honkx := make(map[string]*Honk)
1105	kids := make(map[string][]*Honk)
1106	for _, h := range honks {
1107		honkx[h.XID] = h
1108		rid := h.RID
1109		kids[rid] = append(kids[rid], h)
1110	}
1111	done := make(map[*Honk]bool)
1112	var thread []*Honk
1113	var nextlevel func(p *Honk)
1114	level := 0
1115	nextlevel = func(p *Honk) {
1116		levelup := level < 4
1117		if pp := honkx[p.RID]; p.RID == "" || (pp != nil && sameperson(p, pp)) {
1118			levelup = false
1119		}
1120		if level > 0 && len(kids[p.RID]) == 1 {
1121			if pp := honkx[p.RID]; pp != nil && len(kids[pp.RID]) == 1 {
1122				levelup = false
1123			}
1124		}
1125		if levelup {
1126			level++
1127		}
1128		p.Style += fmt.Sprintf(" level%d", level)
1129		childs := kids[p.XID]
1130		if false {
1131			sort.SliceStable(childs, func(i, j int) bool {
1132				return sameperson(childs[i], p) && !sameperson(childs[j], p)
1133			})
1134		}
1135		if true {
1136			sort.SliceStable(childs, func(i, j int) bool {
1137				return !sameperson(childs[i], p) && sameperson(childs[j], p)
1138			})
1139		}
1140		for _, h := range childs {
1141			if !done[h] {
1142				done[h] = true
1143				thread = append(thread, h)
1144				nextlevel(h)
1145			}
1146		}
1147		if levelup {
1148			level--
1149		}
1150	}
1151	for _, h := range honks {
1152		if !done[h] && h.RID == "" {
1153			done[h] = true
1154			thread = append(thread, h)
1155			nextlevel(h)
1156		}
1157	}
1158	for _, h := range honks {
1159		if !done[h] {
1160			done[h] = true
1161			thread = append(thread, h)
1162			nextlevel(h)
1163		}
1164	}
1165	return thread
1166}
1167
1168func honkology(honk *Honk) template.HTML {
1169	var user *WhatAbout
1170	ok := somenumberedusers.Get(honk.UserID, &user)
1171	if !ok {
1172		return ""
1173	}
1174	title := fmt.Sprintf("%s: %s", user.Display, honk.Precis)
1175	imgurl := avatarURL(user)
1176	for _, d := range honk.Donks {
1177		if d.Local && strings.HasPrefix(d.Media, "image") {
1178			imgurl = d.URL
1179			break
1180		}
1181	}
1182	short := honk.Noise
1183	if len(short) > 160 {
1184		short = short[0:160] + "..."
1185	}
1186	return templates.Sprintf(
1187		`<meta property="og:title" content="%s" />
1188<meta property="og:type" content="article" />
1189<meta property="article:author" content="%s" />
1190<meta property="og:url" content="%s" />
1191<meta property="og:image" content="%s" />
1192<meta property="og:description" content="%s" />`,
1193		title, user.URL, honk.XID, imgurl, short)
1194}
1195
1196func showonehonk(w http.ResponseWriter, r *http.Request) {
1197	name := mux.Vars(r)["name"]
1198	user, err := butwhatabout(name)
1199	if err != nil {
1200		http.NotFound(w, r)
1201		return
1202	}
1203	if stealthmode(user.ID, r) {
1204		http.NotFound(w, r)
1205		return
1206	}
1207	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
1208
1209	if friendorfoe(r.Header.Get("Accept")) {
1210		j, ok := gimmejonk(xid)
1211		if ok {
1212			trackback(xid, r)
1213			w.Header().Set("Content-Type", theonetruename)
1214			w.Write(j)
1215		} else {
1216			http.NotFound(w, r)
1217		}
1218		return
1219	}
1220	honk := getxonk(user.ID, xid)
1221	if honk == nil {
1222		http.NotFound(w, r)
1223		return
1224	}
1225	u := login.GetUserInfo(r)
1226	if u != nil && u.UserID != user.ID {
1227		u = nil
1228	}
1229	if !honk.Public {
1230		if u == nil {
1231			http.NotFound(w, r)
1232			return
1233
1234		}
1235		honks := []*Honk{honk}
1236		donksforhonks(honks)
1237		templinfo := getInfo(r)
1238		templinfo["ServerMessage"] = "one honk maybe more"
1239		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1240		honkpage(w, u, honks, templinfo)
1241		return
1242	}
1243
1244	templinfo := getInfo(r)
1245	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1246	//reversehonks(rawhonks)
1247	rawhonks = threadsort(rawhonks)
1248	var honks []*Honk
1249	for i, h := range rawhonks {
1250		if h.XID == xid {
1251			templinfo["Honkology"] = honkology(h)
1252			if i > 0 {
1253				h.Style += " glow"
1254			}
1255		}
1256		if h.Public && (h.Whofore == 2 || h.IsAcked()) {
1257			honks = append(honks, h)
1258		}
1259	}
1260
1261	templinfo["ServerMessage"] = "one honk maybe more"
1262	if u != nil {
1263		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1264	}
1265	templinfo["APAltLink"] = templates.Sprintf("<link href='%s' rel='alternate' type='application/activity+json'>", xid)
1266	honkpage(w, u, honks, templinfo)
1267}
1268
1269func honkpage(w http.ResponseWriter, u *login.UserInfo, honks []*Honk, templinfo map[string]interface{}) {
1270	var userid int64 = -1
1271	if u != nil {
1272		userid = u.UserID
1273		templinfo["User"], _ = butwhatabout(u.Username)
1274	}
1275	reverbolate(userid, honks)
1276	templinfo["Honks"] = honks
1277	templinfo["MapLink"] = getmaplink(u)
1278	if templinfo["TopHID"] == nil {
1279		if len(honks) > 0 {
1280			templinfo["TopHID"] = honks[0].ID
1281		} else {
1282			templinfo["TopHID"] = 0
1283		}
1284	}
1285	if u == nil && !develMode {
1286		w.Header().Set("Cache-Control", "max-age=60")
1287	}
1288	err := readviews.Execute(w, "honkpage.html", templinfo)
1289	if err != nil {
1290		elog.Print(err)
1291	}
1292}
1293
1294func saveuser(w http.ResponseWriter, r *http.Request) {
1295	whatabout := r.FormValue("whatabout")
1296	whatabout = strings.Replace(whatabout, "\r", "", -1)
1297	u := login.GetUserInfo(r)
1298	user, _ := butwhatabout(u.Username)
1299	db := opendatabase()
1300
1301	options := user.Options
1302	if r.FormValue("skinny") == "skinny" {
1303		options.SkinnyCSS = true
1304	} else {
1305		options.SkinnyCSS = false
1306	}
1307	if r.FormValue("omitimages") == "omitimages" {
1308		options.OmitImages = true
1309	} else {
1310		options.OmitImages = false
1311	}
1312	if r.FormValue("mentionall") == "mentionall" {
1313		options.MentionAll = true
1314	} else {
1315		options.MentionAll = false
1316	}
1317	if r.FormValue("inlineqts") == "inlineqts" {
1318		options.InlineQuotes = true
1319	} else {
1320		options.InlineQuotes = false
1321	}
1322	if r.FormValue("maps") == "apple" {
1323		options.MapLink = "apple"
1324	} else {
1325		options.MapLink = ""
1326	}
1327	options.Reaction = r.FormValue("reaction")
1328
1329	sendupdate := false
1330	ava := re_avatar.FindString(whatabout)
1331	if ava != "" {
1332		whatabout = re_avatar.ReplaceAllString(whatabout, "")
1333		ava = ava[7:]
1334		if ava[0] == ' ' {
1335			ava = ava[1:]
1336		}
1337		ava = fmt.Sprintf("https://%s/meme/%s", serverName, ava)
1338	}
1339	if ava != options.Avatar {
1340		options.Avatar = ava
1341		sendupdate = true
1342	}
1343	ban := re_banner.FindString(whatabout)
1344	if ban != "" {
1345		whatabout = re_banner.ReplaceAllString(whatabout, "")
1346		ban = ban[7:]
1347		if ban[0] == ' ' {
1348			ban = ban[1:]
1349		}
1350		ban = fmt.Sprintf("https://%s/meme/%s", serverName, ban)
1351	}
1352	if ban != options.Banner {
1353		options.Banner = ban
1354		sendupdate = true
1355	}
1356	whatabout = strings.TrimSpace(whatabout)
1357	if whatabout != user.About {
1358		sendupdate = true
1359	}
1360	j, err := jsonify(options)
1361	if err == nil {
1362		_, err = db.Exec("update users set about = ?, options = ? where username = ?", whatabout, j, u.Username)
1363	}
1364	if err != nil {
1365		elog.Printf("error bouting what: %s", err)
1366	}
1367	somenamedusers.Clear(u.Username)
1368	somenumberedusers.Clear(u.UserID)
1369	oldjonkers.Clear(u.Username)
1370
1371	if sendupdate {
1372		updateMe(u.Username)
1373	}
1374
1375	http.Redirect(w, r, "/account", http.StatusSeeOther)
1376}
1377
1378func bonkit(xid string, user *WhatAbout) {
1379	dlog.Printf("bonking %s", xid)
1380
1381	xonk := getxonk(user.ID, xid)
1382	if xonk == nil {
1383		return
1384	}
1385	if !xonk.Public {
1386		return
1387	}
1388	if xonk.IsBonked() {
1389		return
1390	}
1391	donksforhonks([]*Honk{xonk})
1392
1393	_, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
1394	if err != nil {
1395		elog.Printf("error acking bonk: %s", err)
1396	}
1397
1398	oonker := xonk.Oonker
1399	if oonker == "" {
1400		oonker = xonk.Honker
1401	}
1402	dt := time.Now().UTC()
1403	bonk := &Honk{
1404		UserID:   user.ID,
1405		Username: user.Name,
1406		What:     "bonk",
1407		Honker:   user.URL,
1408		Oonker:   oonker,
1409		XID:      xonk.XID,
1410		RID:      xonk.RID,
1411		Noise:    xonk.Noise,
1412		Precis:   xonk.Precis,
1413		URL:      xonk.URL,
1414		Date:     dt,
1415		Donks:    xonk.Donks,
1416		Whofore:  2,
1417		Convoy:   xonk.Convoy,
1418		Audience: []string{thewholeworld, oonker},
1419		Public:   true,
1420		Format:   xonk.Format,
1421		Place:    xonk.Place,
1422		Onts:     xonk.Onts,
1423		Time:     xonk.Time,
1424	}
1425
1426	err = savehonk(bonk)
1427	if err != nil {
1428		elog.Printf("uh oh")
1429		return
1430	}
1431
1432	go honkworldwide(user, bonk)
1433}
1434
1435func submitbonk(w http.ResponseWriter, r *http.Request) {
1436	xid := r.FormValue("xid")
1437	userinfo := login.GetUserInfo(r)
1438	user, _ := butwhatabout(userinfo.Username)
1439
1440	bonkit(xid, user)
1441
1442	if r.FormValue("js") != "1" {
1443		templinfo := getInfo(r)
1444		templinfo["ServerMessage"] = "Bonked!"
1445		err := readviews.Execute(w, "msg.html", templinfo)
1446		if err != nil {
1447			elog.Print(err)
1448		}
1449	}
1450}
1451
1452func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string, aux string) {
1453	zonk := &Honk{
1454		What:     what,
1455		XID:      xonk.XID,
1456		Date:     time.Now().UTC(),
1457		Audience: oneofakind(xonk.Audience),
1458		Noise:    aux,
1459	}
1460	zonk.Public = loudandproud(zonk.Audience)
1461
1462	dlog.Printf("announcing %sed honk: %s", what, xonk.XID)
1463	go honkworldwide(user, zonk)
1464}
1465
1466func zonkit(w http.ResponseWriter, r *http.Request) {
1467	wherefore := r.FormValue("wherefore")
1468	what := r.FormValue("what")
1469	userinfo := login.GetUserInfo(r)
1470	user, _ := butwhatabout(userinfo.Username)
1471
1472	if wherefore == "save" {
1473		xonk := getxonk(userinfo.UserID, what)
1474		if xonk != nil {
1475			_, err := stmtUpdateFlags.Exec(flagIsSaved, xonk.ID)
1476			if err != nil {
1477				elog.Printf("error saving: %s", err)
1478			}
1479		}
1480		return
1481	}
1482
1483	if wherefore == "unsave" {
1484		xonk := getxonk(userinfo.UserID, what)
1485		if xonk != nil {
1486			_, err := stmtClearFlags.Exec(flagIsSaved, xonk.ID)
1487			if err != nil {
1488				elog.Printf("error unsaving: %s", err)
1489			}
1490		}
1491		return
1492	}
1493
1494	if wherefore == "react" {
1495		reaction := user.Options.Reaction
1496		if r2 := r.FormValue("reaction"); r2 != "" {
1497			reaction = r2
1498		}
1499		if reaction == "none" {
1500			return
1501		}
1502		xonk := getxonk(userinfo.UserID, what)
1503		if xonk != nil {
1504			_, err := stmtUpdateFlags.Exec(flagIsReacted, xonk.ID)
1505			if err != nil {
1506				elog.Printf("error saving: %s", err)
1507			}
1508			sendzonkofsorts(xonk, user, "react", reaction)
1509		}
1510		return
1511	}
1512
1513	// my hammer is too big, oh well
1514	defer oldjonks.Flush()
1515
1516	if wherefore == "ack" {
1517		xonk := getxonk(userinfo.UserID, what)
1518		if xonk != nil && !xonk.IsAcked() {
1519			_, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1520			if err != nil {
1521				elog.Printf("error acking: %s", err)
1522			}
1523			sendzonkofsorts(xonk, user, "ack", "")
1524		}
1525		return
1526	}
1527
1528	if wherefore == "deack" {
1529		xonk := getxonk(userinfo.UserID, what)
1530		if xonk != nil && xonk.IsAcked() {
1531			_, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1532			if err != nil {
1533				elog.Printf("error deacking: %s", err)
1534			}
1535			sendzonkofsorts(xonk, user, "deack", "")
1536		}
1537		return
1538	}
1539
1540	if wherefore == "bonk" {
1541		user, _ := butwhatabout(userinfo.Username)
1542		bonkit(what, user)
1543		return
1544	}
1545
1546	if wherefore == "unbonk" {
1547		xonk := getbonk(userinfo.UserID, what)
1548		if xonk != nil {
1549			deletehonk(xonk.ID)
1550			xonk = getxonk(userinfo.UserID, what)
1551			_, err := stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1552			if err != nil {
1553				elog.Printf("error unbonking: %s", err)
1554			}
1555			sendzonkofsorts(xonk, user, "unbonk", "")
1556		}
1557		return
1558	}
1559
1560	if wherefore == "untag" {
1561		xonk := getxonk(userinfo.UserID, what)
1562		if xonk != nil {
1563			_, err := stmtUpdateFlags.Exec(flagIsUntagged, xonk.ID)
1564			if err != nil {
1565				elog.Printf("error untagging: %s", err)
1566			}
1567		}
1568		var badparents map[string]bool
1569		untagged.GetAndLock(userinfo.UserID, &badparents)
1570		badparents[what] = true
1571		untagged.Unlock()
1572		return
1573	}
1574
1575	ilog.Printf("zonking %s %s", wherefore, what)
1576	if wherefore == "zonk" {
1577		xonk := getxonk(userinfo.UserID, what)
1578		if xonk != nil {
1579			deletehonk(xonk.ID)
1580			if xonk.Whofore == 2 || xonk.Whofore == 3 {
1581				sendzonkofsorts(xonk, user, "zonk", "")
1582			}
1583		}
1584	}
1585	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1586	if err != nil {
1587		elog.Printf("error saving zonker: %s", err)
1588		return
1589	}
1590}
1591
1592func edithonkpage(w http.ResponseWriter, r *http.Request) {
1593	u := login.GetUserInfo(r)
1594	user, _ := butwhatabout(u.Username)
1595	xid := r.FormValue("xid")
1596	honk := getxonk(u.UserID, xid)
1597	if !canedithonk(user, honk) {
1598		http.Error(w, "no editing that please", http.StatusInternalServerError)
1599		return
1600	}
1601
1602	noise := honk.Noise
1603
1604	honks := []*Honk{honk}
1605	donksforhonks(honks)
1606	reverbolate(u.UserID, honks)
1607	templinfo := getInfo(r)
1608	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1609	templinfo["Honks"] = honks
1610	templinfo["MapLink"] = getmaplink(u)
1611	templinfo["Noise"] = noise
1612	templinfo["SavedPlace"] = honk.Place
1613	if tm := honk.Time; tm != nil {
1614		templinfo["ShowTime"] = " "
1615		templinfo["StartTime"] = tm.StartTime.Format("2006-01-02 15:04")
1616		if tm.Duration != 0 {
1617			templinfo["Duration"] = tm.Duration
1618		}
1619	}
1620	templinfo["ServerMessage"] = "honk edit"
1621	templinfo["IsPreview"] = true
1622	templinfo["UpdateXID"] = honk.XID
1623	if len(honk.Donks) > 0 {
1624		var savedfiles []string
1625		for _, d := range honk.Donks {
1626			savedfiles = append(savedfiles, fmt.Sprintf("%s:%d", d.XID, d.FileID))
1627		}
1628		templinfo["SavedFile"] = strings.Join(savedfiles, ",")
1629	}
1630	err := readviews.Execute(w, "honkpage.html", templinfo)
1631	if err != nil {
1632		elog.Print(err)
1633	}
1634}
1635
1636func newhonkpage(w http.ResponseWriter, r *http.Request) {
1637	u := login.GetUserInfo(r)
1638	rid := r.FormValue("rid")
1639	noise := ""
1640
1641	xonk := getxonk(u.UserID, rid)
1642	if xonk != nil {
1643		_, replto := handles(xonk.Honker)
1644		if replto != "" {
1645			noise = "@" + replto + " "
1646		}
1647	}
1648
1649	templinfo := getInfo(r)
1650	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1651	templinfo["InReplyTo"] = rid
1652	templinfo["Noise"] = noise
1653	templinfo["ServerMessage"] = "compose honk"
1654	templinfo["IsPreview"] = true
1655	err := readviews.Execute(w, "honkpage.html", templinfo)
1656	if err != nil {
1657		elog.Print(err)
1658	}
1659}
1660
1661func canedithonk(user *WhatAbout, honk *Honk) bool {
1662	if honk == nil || honk.Honker != user.URL || honk.What == "bonk" {
1663		return false
1664	}
1665	return true
1666}
1667
1668func submitdonk(w http.ResponseWriter, r *http.Request) ([]*Donk, error) {
1669	if !strings.HasPrefix(strings.ToLower(r.Header.Get("Content-Type")), "multipart/form-data") {
1670		return nil, nil
1671	}
1672	var donks []*Donk
1673	for i, hdr := range r.MultipartForm.File["donk"] {
1674		if i > 16 {
1675			break
1676		}
1677		donk, err := formtodonk(w, r, hdr)
1678		if err != nil {
1679			return nil, err
1680		}
1681		donks = append(donks, donk)
1682	}
1683	return donks, nil
1684}
1685
1686func formtodonk(w http.ResponseWriter, r *http.Request, filehdr *multipart.FileHeader) (*Donk, error) {
1687	file, err := filehdr.Open()
1688	if err != nil {
1689		if err == http.ErrMissingFile {
1690			return nil, nil
1691		}
1692		elog.Printf("error reading donk: %s", err)
1693		http.Error(w, "error reading donk", http.StatusUnsupportedMediaType)
1694		return nil, err
1695	}
1696	var buf bytes.Buffer
1697	io.Copy(&buf, file)
1698	file.Close()
1699	data := buf.Bytes()
1700	var media, name string
1701	img, err := bigshrink(data)
1702	if err == nil {
1703		data = img.Data
1704		format := img.Format
1705		media = "image/" + format
1706		if format == "jpeg" {
1707			format = "jpg"
1708		}
1709		if format == "svg+xml" {
1710			format = "svg"
1711		}
1712		name = xfiltrate() + "." + format
1713	} else {
1714		ct := http.DetectContentType(data)
1715		switch ct {
1716		case "application/pdf":
1717			maxsize := 10000000
1718			if len(data) > maxsize {
1719				ilog.Printf("bad image: %s too much pdf: %d", err, len(data))
1720				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1721				return nil, err
1722			}
1723			media = ct
1724			name = filehdr.Filename
1725			if name == "" {
1726				name = xfiltrate() + ".pdf"
1727			}
1728		default:
1729			maxsize := 100000
1730			if len(data) > maxsize {
1731				ilog.Printf("bad image: %s too much text: %d", err, len(data))
1732				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1733				return nil, err
1734			}
1735			for i := 0; i < len(data); i++ {
1736				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1737					ilog.Printf("bad image: %s not text: %d", err, data[i])
1738					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1739					return nil, err
1740				}
1741			}
1742			media = "text/plain"
1743			name = filehdr.Filename
1744			if name == "" {
1745				name = xfiltrate() + ".txt"
1746			}
1747		}
1748	}
1749	desc := strings.TrimSpace(r.FormValue("donkdesc"))
1750	if desc == "" {
1751		desc = name
1752	}
1753	fileid, xid, err := savefileandxid(name, desc, "", media, true, data)
1754	if err != nil {
1755		elog.Printf("unable to save image: %s", err)
1756		http.Error(w, "failed to save attachment", http.StatusUnsupportedMediaType)
1757		return nil, err
1758	}
1759	d := &Donk{
1760		FileID: fileid,
1761		XID:    xid,
1762		Desc:   desc,
1763		Local:  true,
1764	}
1765	return d, nil
1766}
1767
1768func websubmithonk(w http.ResponseWriter, r *http.Request) {
1769	h := submithonk(w, r)
1770	if h == nil {
1771		return
1772	}
1773	http.Redirect(w, r, h.XID[len(serverName)+8:], http.StatusSeeOther)
1774}
1775
1776// what a hot mess this function is
1777func submithonk(w http.ResponseWriter, r *http.Request) *Honk {
1778	rid := r.FormValue("rid")
1779	noise := r.FormValue("noise")
1780	format := r.FormValue("format")
1781	if format == "" {
1782		format = "markdown"
1783	}
1784	if !(format == "markdown" || format == "html") {
1785		http.Error(w, "unknown format", 500)
1786		return nil
1787	}
1788
1789	userinfo := login.GetUserInfo(r)
1790	user, _ := butwhatabout(userinfo.Username)
1791
1792	dt := time.Now().UTC()
1793	updatexid := r.FormValue("updatexid")
1794	var honk *Honk
1795	if updatexid != "" {
1796		honk = getxonk(userinfo.UserID, updatexid)
1797		if !canedithonk(user, honk) {
1798			http.Error(w, "no editing that please", http.StatusInternalServerError)
1799			return nil
1800		}
1801		honk.Date = dt
1802		honk.What = "update"
1803		honk.Format = format
1804	} else {
1805		xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1806		what := "honk"
1807		honk = &Honk{
1808			UserID:   userinfo.UserID,
1809			Username: userinfo.Username,
1810			What:     what,
1811			Honker:   user.URL,
1812			XID:      xid,
1813			Date:     dt,
1814			Format:   format,
1815		}
1816	}
1817
1818	var convoy string
1819	noise = strings.Replace(noise, "\r", "", -1)
1820	if updatexid == "" && rid == "" {
1821		noise = re_convoy.ReplaceAllStringFunc(noise, func(m string) string {
1822			convoy = m[7:]
1823			convoy = strings.TrimSpace(convoy)
1824			if !re_convalidate.MatchString(convoy) {
1825				convoy = ""
1826			}
1827			return ""
1828		})
1829	}
1830	noise = quickrename(noise, userinfo.UserID)
1831	noise = hooterize(noise)
1832	honk.Noise = noise
1833	precipitate(honk)
1834	noise = honk.Noise
1835	recategorize(honk)
1836	translate(honk)
1837
1838	if rid != "" {
1839		xonk := getxonk(userinfo.UserID, rid)
1840		if xonk == nil {
1841			http.Error(w, "replyto disappeared", http.StatusNotFound)
1842			return nil
1843		}
1844		if xonk.Public {
1845			honk.Audience = append(honk.Audience, xonk.Audience...)
1846		}
1847		convoy = xonk.Convoy
1848		for i, a := range honk.Audience {
1849			if a == thewholeworld {
1850				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1851				break
1852			}
1853		}
1854		honk.RID = rid
1855		if xonk.Precis != "" && honk.Precis == "" {
1856			honk.Precis = xonk.Precis
1857			if !re_dangerous.MatchString(honk.Precis) {
1858				honk.Precis = "re: " + honk.Precis
1859			}
1860		}
1861	} else if updatexid == "" {
1862		honk.Audience = []string{thewholeworld}
1863	}
1864	if honk.Noise != "" && honk.Noise[0] == '@' {
1865		honk.Audience = append(grapevine(honk.Mentions), honk.Audience...)
1866	} else {
1867		honk.Audience = append(honk.Audience, grapevine(honk.Mentions)...)
1868	}
1869
1870	if convoy == "" {
1871		convoy = "data:,electrichonkytonk-" + xfiltrate()
1872	}
1873	butnottooloud(honk.Audience)
1874	honk.Audience = oneofakind(honk.Audience)
1875	if len(honk.Audience) == 0 {
1876		ilog.Printf("honk to nowhere")
1877		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1878		return nil
1879	}
1880	honk.Public = loudandproud(honk.Audience)
1881	honk.Convoy = convoy
1882
1883	donkxid := strings.Join(r.Form["donkxid"], ",")
1884	if donkxid == "" {
1885		donks, err := submitdonk(w, r)
1886		if err != nil && err != http.ErrMissingFile {
1887			return nil
1888		}
1889		if len(donks) > 0 {
1890			honk.Donks = append(honk.Donks, donks...)
1891			var xids []string
1892			for _, d := range honk.Donks {
1893				xids = append(xids, fmt.Sprintf("%s:%d", d.XID, d.FileID))
1894			}
1895			donkxid = strings.Join(xids, ",")
1896		}
1897	} else {
1898		xids := strings.Split(donkxid, ",")
1899		for i, xid := range xids {
1900			if i > 16 {
1901				break
1902			}
1903			p := strings.Split(xid, ":")
1904			xid = p[0]
1905			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1906			var donk *Donk
1907			if len(p) > 1 {
1908				fileid, _ := strconv.ParseInt(p[1], 10, 0)
1909				donk = finddonkid(fileid, url)
1910			} else {
1911				donk = finddonk(url)
1912			}
1913			if donk != nil {
1914				honk.Donks = append(honk.Donks, donk)
1915			} else {
1916				ilog.Printf("can't find file: %s", xid)
1917			}
1918		}
1919	}
1920	memetize(honk)
1921	imaginate(honk)
1922
1923	placename := strings.TrimSpace(r.FormValue("placename"))
1924	placelat := strings.TrimSpace(r.FormValue("placelat"))
1925	placelong := strings.TrimSpace(r.FormValue("placelong"))
1926	placeurl := strings.TrimSpace(r.FormValue("placeurl"))
1927	if placename != "" || placelat != "" || placelong != "" || placeurl != "" {
1928		p := new(Place)
1929		p.Name = placename
1930		p.Latitude, _ = strconv.ParseFloat(placelat, 64)
1931		p.Longitude, _ = strconv.ParseFloat(placelong, 64)
1932		p.Url = placeurl
1933		honk.Place = p
1934	}
1935	timestart := strings.TrimSpace(r.FormValue("timestart"))
1936	if timestart != "" {
1937		t := new(Time)
1938		now := time.Now().Local()
1939		for _, layout := range []string{"2006-01-02 3:04pm", "2006-01-02 15:04", "3:04pm", "15:04"} {
1940			start, err := time.ParseInLocation(layout, timestart, now.Location())
1941			if err == nil {
1942				if start.Year() == 0 {
1943					start = time.Date(now.Year(), now.Month(), now.Day(), start.Hour(), start.Minute(), 0, 0, now.Location())
1944				}
1945				t.StartTime = start
1946				break
1947			}
1948		}
1949		timeend := r.FormValue("timeend")
1950		dur := parseDuration(timeend)
1951		if dur != 0 {
1952			t.Duration = Duration(dur)
1953		}
1954		if !t.StartTime.IsZero() {
1955			honk.What = "event"
1956			honk.Time = t
1957		}
1958	}
1959
1960	if honk.Public {
1961		honk.Whofore = 2
1962	} else {
1963		honk.Whofore = 3
1964	}
1965
1966	// back to markdown
1967	honk.Noise = noise
1968
1969	if r.FormValue("preview") == "preview" {
1970		honks := []*Honk{honk}
1971		reverbolate(userinfo.UserID, honks)
1972		templinfo := getInfo(r)
1973		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1974		templinfo["Honks"] = honks
1975		templinfo["MapLink"] = getmaplink(userinfo)
1976		templinfo["InReplyTo"] = r.FormValue("rid")
1977		templinfo["Noise"] = r.FormValue("noise")
1978		templinfo["SavedFile"] = donkxid
1979		if tm := honk.Time; tm != nil {
1980			templinfo["ShowTime"] = " "
1981			templinfo["StartTime"] = tm.StartTime.Format("2006-01-02 15:04")
1982			if tm.Duration != 0 {
1983				templinfo["Duration"] = tm.Duration
1984			}
1985		}
1986		templinfo["IsPreview"] = true
1987		templinfo["UpdateXID"] = updatexid
1988		templinfo["ServerMessage"] = "honk preview"
1989		err := readviews.Execute(w, "honkpage.html", templinfo)
1990		if err != nil {
1991			elog.Print(err)
1992		}
1993		return nil
1994	}
1995
1996	if updatexid != "" {
1997		updatehonk(honk)
1998		oldjonks.Clear(honk.XID)
1999	} else {
2000		err := savehonk(honk)
2001		if err != nil {
2002			elog.Printf("uh oh")
2003			return nil
2004		}
2005	}
2006
2007	// reload for consistency
2008	honk.Donks = nil
2009	donksforhonks([]*Honk{honk})
2010
2011	go honkworldwide(user, honk)
2012
2013	return honk
2014}
2015
2016func showhonkers(w http.ResponseWriter, r *http.Request) {
2017	userinfo := login.GetUserInfo(r)
2018	templinfo := getInfo(r)
2019	templinfo["Honkers"] = gethonkers(userinfo.UserID)
2020	templinfo["HonkerCSRF"] = login.GetCSRF("submithonker", r)
2021	err := readviews.Execute(w, "honkers.html", templinfo)
2022	if err != nil {
2023		elog.Print(err)
2024	}
2025}
2026
2027func showchatter(w http.ResponseWriter, r *http.Request) {
2028	u := login.GetUserInfo(r)
2029	chatnewnone(u.UserID)
2030	chatter := loadchatter(u.UserID)
2031	for _, chat := range chatter {
2032		for _, ch := range chat.Chonks {
2033			filterchonk(ch)
2034		}
2035	}
2036
2037	templinfo := getInfo(r)
2038	templinfo["Chatter"] = chatter
2039	templinfo["ChonkCSRF"] = login.GetCSRF("sendchonk", r)
2040	err := readviews.Execute(w, "chatter.html", templinfo)
2041	if err != nil {
2042		elog.Print(err)
2043	}
2044}
2045
2046func submitchonk(w http.ResponseWriter, r *http.Request) {
2047	u := login.GetUserInfo(r)
2048	user, _ := butwhatabout(u.Username)
2049	noise := r.FormValue("noise")
2050	target := r.FormValue("target")
2051	format := "markdown"
2052	dt := time.Now().UTC()
2053	xid := fmt.Sprintf("%s/%s/%s", user.URL, "chonk", xfiltrate())
2054
2055	if !strings.HasPrefix(target, "https://") {
2056		target = fullname(target, u.UserID)
2057	}
2058	if target == "" {
2059		http.Error(w, "who is that?", http.StatusInternalServerError)
2060		return
2061	}
2062	ch := Chonk{
2063		UserID: u.UserID,
2064		XID:    xid,
2065		Who:    user.URL,
2066		Target: target,
2067		Date:   dt,
2068		Noise:  noise,
2069		Format: format,
2070	}
2071	donks, err := submitdonk(w, r)
2072	if err != nil && err != http.ErrMissingFile {
2073		return
2074	}
2075	if len(donks) > 0 {
2076		ch.Donks = append(ch.Donks, donks...)
2077	}
2078
2079	translatechonk(&ch)
2080	savechonk(&ch)
2081	// reload for consistency
2082	ch.Donks = nil
2083	donksforchonks([]*Chonk{&ch})
2084	go sendchonk(user, &ch)
2085
2086	http.Redirect(w, r, "/chatter", http.StatusSeeOther)
2087}
2088
2089var combocache = cache.New(cache.Options{Filler: func(userid int64) ([]string, bool) {
2090	honkers := gethonkers(userid)
2091	var combos []string
2092	for _, h := range honkers {
2093		combos = append(combos, h.Combos...)
2094	}
2095	for i, c := range combos {
2096		if c == "-" {
2097			combos[i] = ""
2098		}
2099	}
2100	combos = oneofakind(combos)
2101	sort.Strings(combos)
2102	return combos, true
2103}, Invalidator: &honkerinvalidator})
2104
2105func showcombos(w http.ResponseWriter, r *http.Request) {
2106	userinfo := login.GetUserInfo(r)
2107	var combos []string
2108	combocache.Get(userinfo.UserID, &combos)
2109	templinfo := getInfo(r)
2110	err := readviews.Execute(w, "combos.html", templinfo)
2111	if err != nil {
2112		elog.Print(err)
2113	}
2114}
2115
2116func websubmithonker(w http.ResponseWriter, r *http.Request) {
2117	h := submithonker(w, r)
2118	if h == nil {
2119		return
2120	}
2121	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
2122}
2123
2124func submithonker(w http.ResponseWriter, r *http.Request) *Honker {
2125	u := login.GetUserInfo(r)
2126	user, _ := butwhatabout(u.Username)
2127	name := strings.TrimSpace(r.FormValue("name"))
2128	url := strings.TrimSpace(r.FormValue("url"))
2129	peep := r.FormValue("peep")
2130	combos := strings.TrimSpace(r.FormValue("combos"))
2131	combos = " " + combos + " "
2132	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
2133
2134	re_namecheck := regexp.MustCompile("^[\\pL[:digit:]_.-]+$")
2135	if name != "" && !re_namecheck.MatchString(name) {
2136		http.Error(w, "please use a plainer name", http.StatusInternalServerError)
2137		return nil
2138	}
2139
2140	var meta HonkerMeta
2141	meta.Notes = strings.TrimSpace(r.FormValue("notes"))
2142	mj, _ := jsonify(&meta)
2143
2144	defer honkerinvalidator.Clear(u.UserID)
2145
2146	// mostly dummy, fill in later...
2147	h := &Honker{
2148		ID: honkerid,
2149	}
2150
2151	if honkerid > 0 {
2152		if r.FormValue("delete") == "delete" {
2153			unfollowyou(user, honkerid, false)
2154			stmtDeleteHonker.Exec(honkerid)
2155			return h
2156		}
2157		if r.FormValue("unsub") == "unsub" {
2158			unfollowyou(user, honkerid, false)
2159		}
2160		if r.FormValue("sub") == "sub" {
2161			followyou(user, honkerid, false)
2162		}
2163		_, err := stmtUpdateHonker.Exec(name, combos, mj, honkerid, u.UserID)
2164		if err != nil {
2165			elog.Printf("update honker err: %s", err)
2166			return nil
2167		}
2168		return h
2169	}
2170
2171	if url == "" {
2172		http.Error(w, "subscribing to nothing?", http.StatusInternalServerError)
2173		return nil
2174	}
2175
2176	flavor := "presub"
2177	if peep == "peep" {
2178		flavor = "peep"
2179	}
2180
2181	var err error
2182	honkerid, err = savehonker(user, url, name, flavor, combos, mj)
2183	if err != nil {
2184		http.Error(w, "had some trouble with that: "+err.Error(), http.StatusInternalServerError)
2185		return nil
2186	}
2187	if flavor == "presub" {
2188		followyou(user, honkerid, false)
2189	}
2190	h.ID = honkerid
2191	return h
2192}
2193
2194func hfcspage(w http.ResponseWriter, r *http.Request) {
2195	userinfo := login.GetUserInfo(r)
2196
2197	filters := getfilters(userinfo.UserID, filtAny)
2198
2199	templinfo := getInfo(r)
2200	templinfo["Filters"] = filters
2201	templinfo["FilterCSRF"] = login.GetCSRF("filter", r)
2202	err := readviews.Execute(w, "hfcs.html", templinfo)
2203	if err != nil {
2204		elog.Print(err)
2205	}
2206}
2207
2208func savehfcs(w http.ResponseWriter, r *http.Request) {
2209	userinfo := login.GetUserInfo(r)
2210	itsok := r.FormValue("itsok")
2211	if itsok == "iforgiveyou" {
2212		hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0)
2213		_, err := stmtDeleteFilter.Exec(userinfo.UserID, hfcsid)
2214		if err != nil {
2215			elog.Printf("error deleting filter: %s", err)
2216		}
2217		filtInvalidator.Clear(userinfo.UserID)
2218		http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
2219		return
2220	}
2221
2222	filt := new(Filter)
2223	filt.Name = strings.TrimSpace(r.FormValue("name"))
2224	filt.Date = time.Now().UTC()
2225	filt.Actor = strings.TrimSpace(r.FormValue("actor"))
2226	filt.IncludeAudience = r.FormValue("incaud") == "yes"
2227	filt.Text = strings.TrimSpace(r.FormValue("filttext"))
2228	filt.IsReply = r.FormValue("isreply") == "yes"
2229	filt.IsAnnounce = r.FormValue("isannounce") == "yes"
2230	filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof"))
2231	filt.Reject = r.FormValue("doreject") == "yes"
2232	filt.SkipMedia = r.FormValue("doskipmedia") == "yes"
2233	filt.Hide = r.FormValue("dohide") == "yes"
2234	filt.Collapse = r.FormValue("docollapse") == "yes"
2235	filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite"))
2236	filt.Replace = strings.TrimSpace(r.FormValue("filtreplace"))
2237	if dur := parseDuration(r.FormValue("filtduration")); dur > 0 {
2238		filt.Expiration = time.Now().UTC().Add(dur)
2239	}
2240	filt.Notes = strings.TrimSpace(r.FormValue("filtnotes"))
2241
2242	if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce {
2243		ilog.Printf("blank filter")
2244		http.Error(w, "can't save a blank filter", http.StatusInternalServerError)
2245		return
2246	}
2247
2248	j, err := jsonify(filt)
2249	if err == nil {
2250		_, err = stmtSaveFilter.Exec(userinfo.UserID, j)
2251	}
2252	if err != nil {
2253		elog.Printf("error saving filter: %s", err)
2254	}
2255
2256	filtInvalidator.Clear(userinfo.UserID)
2257	http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
2258}
2259
2260func accountpage(w http.ResponseWriter, r *http.Request) {
2261	u := login.GetUserInfo(r)
2262	user, _ := butwhatabout(u.Username)
2263	templinfo := getInfo(r)
2264	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
2265	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
2266	templinfo["User"] = user
2267	about := user.About
2268	if ava := user.Options.Avatar; ava != "" {
2269		about += "\n\navatar: " + ava[strings.LastIndexByte(ava, '/')+1:]
2270	}
2271	if ban := user.Options.Banner; ban != "" {
2272		about += "\n\nbanner: " + ban[strings.LastIndexByte(ban, '/')+1:]
2273	}
2274	templinfo["WhatAbout"] = about
2275	err := readviews.Execute(w, "account.html", templinfo)
2276	if err != nil {
2277		elog.Print(err)
2278	}
2279}
2280
2281func dochpass(w http.ResponseWriter, r *http.Request) {
2282	err := login.ChangePassword(w, r)
2283	if err != nil {
2284		elog.Printf("error changing password: %s", err)
2285	}
2286	http.Redirect(w, r, "/account", http.StatusSeeOther)
2287}
2288
2289var oldfingers = cache.New(cache.Options{Filler: func(orig string) ([]byte, bool) {
2290	if strings.HasPrefix(orig, "acct:") {
2291		orig = orig[5:]
2292	}
2293	name := orig
2294	idx := strings.LastIndexByte(name, '/')
2295	if idx != -1 {
2296		name = name[idx+1:]
2297		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
2298			ilog.Printf("foreign request rejected")
2299			name = ""
2300		}
2301	} else {
2302		idx = strings.IndexByte(name, '@')
2303		if idx != -1 {
2304			name = name[:idx]
2305			if !(name+"@"+serverName == orig || name+"@"+masqName == orig) {
2306				ilog.Printf("foreign request rejected")
2307				name = ""
2308			}
2309		}
2310	}
2311	user, err := butwhatabout(name)
2312	if err != nil {
2313		return nil, false
2314	}
2315
2316	j := junk.New()
2317	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, masqName)
2318	j["aliases"] = []string{user.URL}
2319	l := junk.New()
2320	l["rel"] = "self"
2321	l["type"] = `application/activity+json`
2322	l["href"] = user.URL
2323	j["links"] = []junk.Junk{l}
2324	return j.ToBytes(), true
2325}})
2326
2327func fingerlicker(w http.ResponseWriter, r *http.Request) {
2328	orig := r.FormValue("resource")
2329
2330	dlog.Printf("finger lick: %s", orig)
2331
2332	var j []byte
2333	ok := oldfingers.Get(orig, &j)
2334	if ok {
2335		w.Header().Set("Content-Type", "application/jrd+json")
2336		w.Write(j)
2337	} else {
2338		http.NotFound(w, r)
2339	}
2340}
2341
2342func somedays() string {
2343	secs := 432000 + notrand.Int63n(432000)
2344	return fmt.Sprintf("%d", secs)
2345}
2346
2347func lookatme(ava string) string {
2348	if strings.Contains(ava, serverName+"/"+userSep) {
2349		idx := strings.LastIndexByte(ava, '/')
2350		if idx < len(ava) {
2351			name := ava[idx+1:]
2352			user, _ := butwhatabout(name)
2353			if user != nil && user.URL == ava {
2354				return user.Options.Avatar
2355			}
2356		}
2357	}
2358	return ""
2359}
2360
2361func avatate(w http.ResponseWriter, r *http.Request) {
2362	if develMode {
2363		loadAvatarColors()
2364	}
2365	n := r.FormValue("a")
2366	if redir := lookatme(n); redir != "" {
2367		http.Redirect(w, r, redir, http.StatusSeeOther)
2368		return
2369	}
2370	a := genAvatar(n)
2371	if !develMode {
2372		w.Header().Set("Cache-Control", "max-age="+somedays())
2373	}
2374	w.Write(a)
2375}
2376
2377func serveviewasset(w http.ResponseWriter, r *http.Request) {
2378	serveasset(w, r, viewDir)
2379}
2380func servedataasset(w http.ResponseWriter, r *http.Request) {
2381	if r.URL.Path == "/favicon.ico" {
2382		r.URL.Path = "/icon.png"
2383	}
2384	serveasset(w, r, dataDir)
2385}
2386
2387func serveasset(w http.ResponseWriter, r *http.Request, basedir string) {
2388	if !develMode {
2389		w.Header().Set("Cache-Control", "max-age=7776000")
2390	}
2391	http.ServeFile(w, r, basedir+"/views"+r.URL.Path)
2392}
2393func servehelp(w http.ResponseWriter, r *http.Request) {
2394	name := mux.Vars(r)["name"]
2395	if !develMode {
2396		w.Header().Set("Cache-Control", "max-age=3600")
2397	}
2398	http.ServeFile(w, r, viewDir+"/docs/"+name)
2399}
2400func servehtml(w http.ResponseWriter, r *http.Request) {
2401	u := login.GetUserInfo(r)
2402	templinfo := getInfo(r)
2403	templinfo["AboutMsg"] = aboutMsg
2404	templinfo["LoginMsg"] = loginMsg
2405	templinfo["HonkVersion"] = softwareVersion
2406	if r.URL.Path == "/about" {
2407		templinfo["Sensors"] = getSensors()
2408	}
2409	if u == nil && !develMode {
2410		w.Header().Set("Cache-Control", "max-age=60")
2411	}
2412	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
2413	if err != nil {
2414		elog.Print(err)
2415	}
2416}
2417func serveemu(w http.ResponseWriter, r *http.Request) {
2418	emu := mux.Vars(r)["emu"]
2419
2420	w.Header().Set("Cache-Control", "max-age="+somedays())
2421	http.ServeFile(w, r, dataDir+"/emus/"+emu)
2422}
2423func servememe(w http.ResponseWriter, r *http.Request) {
2424	meme := mux.Vars(r)["meme"]
2425
2426	w.Header().Set("Cache-Control", "max-age="+somedays())
2427	_, err := os.Stat(dataDir + "/memes/" + meme)
2428	if err == nil {
2429		http.ServeFile(w, r, dataDir+"/memes/"+meme)
2430	} else {
2431		mux.Vars(r)["xid"] = meme
2432		servefile(w, r)
2433	}
2434}
2435
2436func servefile(w http.ResponseWriter, r *http.Request) {
2437	xid := mux.Vars(r)["xid"]
2438	var media string
2439	var data []byte
2440	row := stmtGetFileData.QueryRow(xid)
2441	err := row.Scan(&media, &data)
2442	if err != nil {
2443		elog.Printf("error loading file: %s", err)
2444		http.NotFound(w, r)
2445		return
2446	}
2447	w.Header().Set("Content-Type", media)
2448	w.Header().Set("X-Content-Type-Options", "nosniff")
2449	w.Header().Set("Cache-Control", "max-age="+somedays())
2450	w.Write(data)
2451}
2452
2453func nomoroboto(w http.ResponseWriter, r *http.Request) {
2454	io.WriteString(w, "User-agent: *\n")
2455	io.WriteString(w, "Disallow: /a\n")
2456	io.WriteString(w, "Disallow: /d/\n")
2457	io.WriteString(w, "Disallow: /meme/\n")
2458	io.WriteString(w, "Disallow: /o\n")
2459	io.WriteString(w, "Disallow: /o/\n")
2460	io.WriteString(w, "Disallow: /help/\n")
2461	for _, u := range allusers() {
2462		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
2463	}
2464}
2465
2466type Hydration struct {
2467	Tophid    int64
2468	Srvmsg    template.HTML
2469	Honks     string
2470	MeCount   int64
2471	ChatCount int64
2472}
2473
2474func webhydra(w http.ResponseWriter, r *http.Request) {
2475	u := login.GetUserInfo(r)
2476	userid := u.UserID
2477	templinfo := getInfo(r)
2478	templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
2479	page := r.FormValue("page")
2480
2481	wanted, _ := strconv.ParseInt(r.FormValue("tophid"), 10, 0)
2482
2483	var hydra Hydration
2484
2485	var honks []*Honk
2486	switch page {
2487	case "atme":
2488		honks = gethonksforme(userid, wanted)
2489		honks = osmosis(honks, userid, false)
2490		menewnone(userid)
2491		hydra.Srvmsg = "at me!"
2492	case "longago":
2493		honks = gethonksfromlongago(userid, wanted)
2494		honks = osmosis(honks, userid, false)
2495		hydra.Srvmsg = "from long ago"
2496	case "home":
2497		honks = gethonksforuser(userid, wanted)
2498		honks = osmosis(honks, userid, true)
2499		hydra.Srvmsg = serverMsg
2500	case "first":
2501		honks = gethonksforuserfirstclass(userid, wanted)
2502		honks = osmosis(honks, userid, true)
2503		hydra.Srvmsg = "first class only"
2504	case "saved":
2505		honks = getsavedhonks(userid, wanted)
2506		templinfo["PageName"] = "saved"
2507		hydra.Srvmsg = "saved honks"
2508	case "combo":
2509		c := r.FormValue("c")
2510		honks = gethonksbycombo(userid, c, wanted)
2511		honks = osmosis(honks, userid, false)
2512		hydra.Srvmsg = templates.Sprintf("honks by combo: %s", c)
2513	case "convoy":
2514		c := r.FormValue("c")
2515		honks = gethonksbyconvoy(userid, c, wanted)
2516		honks = osmosis(honks, userid, false)
2517		honks = threadsort(honks)
2518		reversehonks(honks)
2519		hydra.Srvmsg = templates.Sprintf("honks in convoy: %s", c)
2520	case "honker":
2521		xid := r.FormValue("xid")
2522		honks = gethonksbyxonker(userid, xid, wanted)
2523		miniform := templates.Sprintf(`<form action="/submithonker" method="POST">
2524			<input type="hidden" name="CSRF" value="%s">
2525			<input type="hidden" name="url" value="%s">
2526			<button tabindex=1 name="add honker" value="add honker">add honker</button>
2527			</form>`, login.GetCSRF("submithonker", r), xid)
2528		msg := templates.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>%s`, xid, xid, miniform)
2529		hydra.Srvmsg = msg
2530	case "user":
2531		uname := r.FormValue("uname")
2532		honks = gethonksbyuser(uname, u != nil && u.Username == uname, wanted)
2533		hydra.Srvmsg = templates.Sprintf("honks by user: %s", uname)
2534	default:
2535		http.NotFound(w, r)
2536	}
2537
2538	if len(honks) > 0 {
2539		hydra.Tophid = honks[0].ID
2540	} else {
2541		hydra.Tophid = wanted
2542	}
2543	reverbolate(userid, honks)
2544
2545	user, _ := butwhatabout(u.Username)
2546
2547	var buf strings.Builder
2548	templinfo["Honks"] = honks
2549	templinfo["MapLink"] = getmaplink(u)
2550	templinfo["User"], _ = butwhatabout(u.Username)
2551	err := readviews.Execute(&buf, "honkfrags.html", templinfo)
2552	if err != nil {
2553		elog.Printf("frag error: %s", err)
2554		return
2555	}
2556	hydra.Honks = buf.String()
2557	hydra.MeCount = user.Options.MeCount
2558	hydra.ChatCount = user.Options.ChatCount
2559	w.Header().Set("Content-Type", "application/json")
2560	j, _ := jsonify(&hydra)
2561	io.WriteString(w, j)
2562}
2563
2564var honkline = make(chan bool)
2565
2566func honkhonkline() {
2567	for {
2568		select {
2569		case honkline <- true:
2570		default:
2571			return
2572		}
2573	}
2574}
2575
2576func apihandler(w http.ResponseWriter, r *http.Request) {
2577	u := login.GetUserInfo(r)
2578	userid := u.UserID
2579	action := r.FormValue("action")
2580	wait, _ := strconv.ParseInt(r.FormValue("wait"), 10, 0)
2581	dlog.Printf("api request '%s' on behalf of %s", action, u.Username)
2582	switch action {
2583	case "honk":
2584		h := submithonk(w, r)
2585		if h == nil {
2586			return
2587		}
2588		fmt.Fprintf(w, "%s", h.XID)
2589	case "donk":
2590		donks, err := submitdonk(w, r)
2591		if err != nil {
2592			http.Error(w, err.Error(), http.StatusBadRequest)
2593			return
2594		}
2595		if len(donks) == 0 {
2596			http.Error(w, "missing donk", http.StatusBadRequest)
2597			return
2598		}
2599		d := donks[0]
2600		donkxid := fmt.Sprintf("%s:%d", d.XID, d.FileID)
2601		w.Write([]byte(donkxid))
2602	case "zonkit":
2603		zonkit(w, r)
2604	case "gethonks":
2605		var honks []*Honk
2606		wanted, _ := strconv.ParseInt(r.FormValue("after"), 10, 0)
2607		page := r.FormValue("page")
2608		var waitchan <-chan time.Time
2609	requery:
2610		switch page {
2611		case "atme":
2612			honks = gethonksforme(userid, wanted)
2613			honks = osmosis(honks, userid, false)
2614			menewnone(userid)
2615		case "longago":
2616			honks = gethonksfromlongago(userid, wanted)
2617			honks = osmosis(honks, userid, false)
2618		case "home":
2619			honks = gethonksforuser(userid, wanted)
2620			honks = osmosis(honks, userid, true)
2621		case "myhonks":
2622			honks = gethonksbyuser(u.Username, true, wanted)
2623			honks = osmosis(honks, userid, true)
2624		default:
2625			http.Error(w, "unknown page", http.StatusNotFound)
2626			return
2627		}
2628		if len(honks) == 0 && wait > 0 {
2629			if waitchan == nil {
2630				waitchan = time.After(time.Duration(wait) * time.Second)
2631			}
2632			select {
2633			case <-honkline:
2634				goto requery
2635			case <-waitchan:
2636			}
2637		}
2638		reverbolate(userid, honks)
2639		j := junk.New()
2640		j["honks"] = honks
2641		j.Write(w)
2642	case "sendactivity":
2643		user, _ := butwhatabout(u.Username)
2644		public := r.FormValue("public") == "1"
2645		rcpts := boxuprcpts(user, r.Form["rcpt"], public)
2646		msg := []byte(r.FormValue("msg"))
2647		for rcpt := range rcpts {
2648			go deliverate(userid, rcpt, msg)
2649		}
2650	case "gethonkers":
2651		j := junk.New()
2652		j["honkers"] = gethonkers(u.UserID)
2653		j.Write(w)
2654	case "savehonker":
2655		h := submithonker(w, r)
2656		if h == nil {
2657			return
2658		}
2659		fmt.Fprintf(w, "%d", h.ID)
2660	default:
2661		http.Error(w, "unknown action", http.StatusNotFound)
2662		return
2663	}
2664}
2665
2666func fiveoh(w http.ResponseWriter, r *http.Request) {
2667	if !develMode {
2668		return
2669	}
2670	fd, err := os.OpenFile("violations.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
2671	if err != nil {
2672		elog.Printf("error opening violations! %s", err)
2673		return
2674	}
2675	defer fd.Close()
2676	io.Copy(fd, r.Body)
2677	fd.WriteString("\n")
2678}
2679
2680var endoftheworld = make(chan bool)
2681var readyalready = make(chan bool)
2682var workinprogress = 0
2683
2684func enditall() {
2685	sig := make(chan os.Signal, 1)
2686	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
2687	<-sig
2688	ilog.Printf("stopping...")
2689	for i := 0; i < workinprogress; i++ {
2690		endoftheworld <- true
2691	}
2692	ilog.Printf("waiting...")
2693	for i := 0; i < workinprogress; i++ {
2694		<-readyalready
2695	}
2696	ilog.Printf("apocalypse")
2697	os.Exit(0)
2698}
2699
2700func bgmonitor() {
2701	for {
2702		when := time.Now().Add(-3 * 24 * time.Hour).UTC().Format(dbtimeformat)
2703		_, err := stmtDeleteOldXonkers.Exec("pubkey", when)
2704		if err != nil {
2705			elog.Printf("error deleting old xonkers: %s", err)
2706		}
2707		zaggies.Flush()
2708		time.Sleep(50 * time.Minute)
2709	}
2710}
2711
2712func addcspheaders(next http.Handler) http.Handler {
2713	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2714		policy := "default-src 'none'; script-src 'self'; connect-src 'self'; style-src 'self'; img-src 'self'; media-src 'self'"
2715		if develMode {
2716			policy += "; report-uri /csp-violation"
2717		}
2718		w.Header().Set("Content-Security-Policy", policy)
2719		next.ServeHTTP(w, r)
2720	})
2721}
2722
2723func emuinit() {
2724	var emunames []string
2725	dir, err := os.Open(dataDir + "/emus")
2726	if err == nil {
2727		emunames, _ = dir.Readdirnames(0)
2728		dir.Close()
2729	}
2730	for _, e := range emunames {
2731		if len(e) <= 4 {
2732			continue
2733		}
2734		ext := e[len(e)-4:]
2735		emu := Emu{
2736			ID:   fmt.Sprintf("/emu/%s", e),
2737			Name: e[:len(e)-4],
2738			Type: "image/" + ext[1:],
2739		}
2740		allemus = append(allemus, emu)
2741	}
2742	sort.Slice(allemus, func(i, j int) bool {
2743		return allemus[i].Name < allemus[j].Name
2744	})
2745}
2746
2747var savedassetparams = make(map[string]string)
2748
2749func getassetparam(file string) string {
2750	if p, ok := savedassetparams[file]; ok {
2751		return p
2752	}
2753	data, err := os.ReadFile(file)
2754	if err != nil {
2755		return ""
2756	}
2757	hasher := sha512.New()
2758	hasher.Write(data)
2759
2760	return fmt.Sprintf("?v=%.8x", hasher.Sum(nil))
2761}
2762
2763func startWatcher() {
2764	watcher, err := gonix.NewWatcher()
2765	if err != nil {
2766		elog.Printf("can't watch: %s", err)
2767		return
2768	}
2769	go func() {
2770		s := dataDir + "/views/local.css"
2771		for {
2772			err := watcher.WatchFile(s)
2773			if err != nil {
2774				dlog.Printf("can't watch: %s", err)
2775				break
2776			}
2777			err = watcher.WaitForChange()
2778			if err != nil {
2779				dlog.Printf("can't wait: %s", err)
2780				break
2781			}
2782			dlog.Printf("local.css changed")
2783			delete(savedassetparams, s)
2784			savedassetparams[s] = getassetparam(s)
2785		}
2786	}()
2787}
2788
2789var usefcgi bool
2790
2791func serve() {
2792	db := opendatabase()
2793	login.Init(login.InitArgs{Db: db, Logger: ilog, Insecure: develMode, SameSiteStrict: !develMode})
2794
2795	listener, err := openListener()
2796	if err != nil {
2797		elog.Fatal(err)
2798	}
2799	runBackendServer()
2800	go enditall()
2801	go redeliverator()
2802	go tracker()
2803	go bgmonitor()
2804	go qotd()
2805	loadLingo()
2806	emuinit()
2807
2808	readviews = templates.Load(develMode,
2809		viewDir+"/views/honkpage.html",
2810		viewDir+"/views/honkfrags.html",
2811		viewDir+"/views/honkers.html",
2812		viewDir+"/views/chatter.html",
2813		viewDir+"/views/hfcs.html",
2814		viewDir+"/views/combos.html",
2815		viewDir+"/views/honkform.html",
2816		viewDir+"/views/honk.html",
2817		viewDir+"/views/account.html",
2818		viewDir+"/views/about.html",
2819		viewDir+"/views/funzone.html",
2820		viewDir+"/views/login.html",
2821		viewDir+"/views/xzone.html",
2822		viewDir+"/views/msg.html",
2823		viewDir+"/views/header.html",
2824		viewDir+"/views/onts.html",
2825		viewDir+"/views/emus.html",
2826		viewDir+"/views/honkpage.js",
2827	)
2828	if !develMode {
2829		assets := []string{
2830			viewDir + "/views/style.css",
2831			dataDir + "/views/local.css",
2832			viewDir + "/views/honkpage.js",
2833			viewDir + "/views/misc.js",
2834			dataDir + "/views/local.js",
2835		}
2836		for _, s := range assets {
2837			savedassetparams[s] = getassetparam(s)
2838		}
2839		loadAvatarColors()
2840	}
2841	startWatcher()
2842
2843	securitizeweb()
2844
2845	mux := mux.NewRouter()
2846	mux.Use(addcspheaders)
2847	mux.Use(login.Checker)
2848
2849	mux.Handle("/api", login.TokenRequired(http.HandlerFunc(apihandler)))
2850
2851	posters := mux.Methods("POST").Subrouter()
2852	getters := mux.Methods("GET").Subrouter()
2853
2854	getters.HandleFunc("/", homepage)
2855	getters.HandleFunc("/home", homepage)
2856	getters.HandleFunc("/front", homepage)
2857	getters.HandleFunc("/events", homepage)
2858	getters.HandleFunc("/robots.txt", nomoroboto)
2859	getters.HandleFunc("/rss", showrss)
2860	getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}", showuser)
2861	getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/"+honkSep+"/{xid:[\\pL[:digit:]]+}", showonehonk)
2862	getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/rss", showrss)
2863	posters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/inbox", inbox)
2864	getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/outbox", outbox)
2865	getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/followers", emptiness)
2866	getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/following", emptiness)
2867	getters.HandleFunc("/a", avatate)
2868	getters.HandleFunc("/o", thelistingoftheontologies)
2869	getters.HandleFunc("/o/{name:.+}", showontology)
2870	getters.HandleFunc("/d/{xid:[\\pL[:digit:].]+}", servefile)
2871	getters.HandleFunc("/emu/{emu:[^.]*[^/]+}", serveemu)
2872	getters.HandleFunc("/meme/{meme:[^.]*[^/]+}", servememe)
2873	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
2874
2875	getters.HandleFunc("/flag/{code:.+}", showflag)
2876
2877	getters.HandleFunc("/server", serveractor)
2878	posters.HandleFunc("/server/inbox", serverinbox)
2879	posters.HandleFunc("/inbox", serverinbox)
2880
2881	posters.HandleFunc("/csp-violation", fiveoh)
2882
2883	getters.HandleFunc("/style.css", serveviewasset)
2884	getters.HandleFunc("/honkpage.js", serveviewasset)
2885	getters.HandleFunc("/misc.js", serveviewasset)
2886	getters.HandleFunc("/local.css", servedataasset)
2887	getters.HandleFunc("/local.js", servedataasset)
2888	getters.HandleFunc("/icon.png", servedataasset)
2889	getters.HandleFunc("/favicon.ico", servedataasset)
2890
2891	getters.HandleFunc("/about", servehtml)
2892	getters.HandleFunc("/login", servehtml)
2893	posters.HandleFunc("/dologin", login.LoginFunc)
2894	getters.HandleFunc("/logout", login.LogoutFunc)
2895	getters.HandleFunc("/help/{name:[\\pL[:digit:]_.-]+}", servehelp)
2896
2897	loggedin := mux.NewRoute().Subrouter()
2898	loggedin.Use(login.Required)
2899	loggedin.HandleFunc("/first", homepage)
2900	loggedin.HandleFunc("/chatter", showchatter)
2901	loggedin.Handle("/sendchonk", login.CSRFWrap("sendchonk", http.HandlerFunc(submitchonk)))
2902	loggedin.HandleFunc("/saved", homepage)
2903	loggedin.HandleFunc("/account", accountpage)
2904	loggedin.HandleFunc("/funzone", showfunzone)
2905	loggedin.HandleFunc("/chpass", dochpass)
2906	loggedin.HandleFunc("/atme", homepage)
2907	loggedin.HandleFunc("/longago", homepage)
2908	loggedin.HandleFunc("/hfcs", hfcspage)
2909	loggedin.HandleFunc("/xzone", xzone)
2910	loggedin.HandleFunc("/newhonk", newhonkpage)
2911	loggedin.HandleFunc("/edit", edithonkpage)
2912	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(websubmithonk)))
2913	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(submitbonk)))
2914	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
2915	loggedin.Handle("/savehfcs", login.CSRFWrap("filter", http.HandlerFunc(savehfcs)))
2916	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
2917	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
2918	loggedin.HandleFunc("/honkers", showhonkers)
2919	loggedin.HandleFunc("/h/{name:[\\pL[:digit:]_.-]+}", showhonker)
2920	loggedin.HandleFunc("/h", showhonker)
2921	loggedin.HandleFunc("/c/{name:[\\pL[:digit:]_.-]+}", showcombo)
2922	loggedin.HandleFunc("/c", showcombos)
2923	loggedin.HandleFunc("/t", showconvoy)
2924	loggedin.HandleFunc("/q", showsearch)
2925	loggedin.HandleFunc("/hydra", webhydra)
2926	loggedin.HandleFunc("/emus", showemus)
2927	loggedin.Handle("/submithonker", login.CSRFWrap("submithonker", http.HandlerFunc(websubmithonker)))
2928
2929	if usefcgi {
2930		err = fcgi.Serve(listener, mux)
2931	} else {
2932		err = http.Serve(listener, mux)
2933	}
2934
2935	err = http.Serve(listener, mux)
2936	if err != nil {
2937		elog.Fatal(err)
2938	}
2939}