all repos — honk @ 9bc3913398f9f61f71a9f13aed34eabf9492865c

my fork of honk

web.go (view raw)

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