all repos — honk @ aab77a5021bdc107fa264eb2ad054b3b58273dce

my fork of honk

activity.go (view raw)

   1//
   2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
   3//
   4// Permission to use, copy, modify, and distribute this software for any
   5// purpose with or without fee is hereby granted, provided that the above
   6// copyright notice and this permission notice appear in all copies.
   7//
   8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15
  16package main
  17
  18import (
  19	"bytes"
  20	"context"
  21	"crypto/tls"
  22	"database/sql"
  23	"fmt"
  24	"html"
  25	"io"
  26	notrand "math/rand"
  27	"net/http"
  28	"os"
  29	"regexp"
  30	"strings"
  31	"sync"
  32	"time"
  33
  34	"humungus.tedunangst.com/r/webs/gate"
  35	"humungus.tedunangst.com/r/webs/gencache"
  36	"humungus.tedunangst.com/r/webs/httpsig"
  37	"humungus.tedunangst.com/r/webs/junk"
  38	"humungus.tedunangst.com/r/webs/templates"
  39)
  40
  41var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
  42var thefakename = `application/activity+json`
  43var falsenames = []string{
  44	`application/ld+json`,
  45	`application/activity+json`,
  46}
  47
  48const itiswhatitis = "https://www.w3.org/ns/activitystreams"
  49const thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
  50const tinyworld = "as:Public"
  51const chatKeyProp = "chatKeyV0"
  52
  53var fastTimeout time.Duration = 5
  54var slowTimeout time.Duration = 30
  55
  56func friendorfoe(ct string) bool {
  57	ct = strings.ToLower(ct)
  58	for _, at := range falsenames {
  59		if strings.HasPrefix(ct, at) {
  60			return true
  61		}
  62	}
  63	return false
  64}
  65
  66var honkTransport = http.Transport{
  67	MaxIdleConns:    120,
  68	MaxConnsPerHost: 4,
  69}
  70
  71var honkClient = http.Client{
  72	Transport: &honkTransport,
  73}
  74
  75func gogglesDoNothing() {
  76	honkTransport.TLSClientConfig = &tls.Config{
  77		InsecureSkipVerify: true,
  78	}
  79}
  80
  81func PostJunk(keyname string, key httpsig.PrivateKey, url string, j junk.Junk) error {
  82	return PostMsg(keyname, key, url, j.ToBytes())
  83}
  84
  85func PostMsg(keyname string, key httpsig.PrivateKey, url string, msg []byte) error {
  86	req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
  87	if err != nil {
  88		return err
  89	}
  90	req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
  91	req.Header.Set("Content-Type", theonetruename)
  92	httpsig.SignRequest(keyname, key, req, msg)
  93	ctx, cancel := context.WithTimeout(context.Background(), 2*slowTimeout*time.Second)
  94	defer cancel()
  95	req = req.WithContext(ctx)
  96	resp, err := honkClient.Do(req)
  97	if err != nil {
  98		return err
  99	}
 100	defer resp.Body.Close()
 101	switch resp.StatusCode {
 102	case 200:
 103	case 201:
 104	case 202:
 105	default:
 106		var buf [4096]byte
 107		n, _ := resp.Body.Read(buf[:])
 108		dlog.Printf("post failure message: %s", buf[:n])
 109		return fmt.Errorf("http post status: %d", resp.StatusCode)
 110	}
 111	ilog.Printf("successful post: %s %d", url, resp.StatusCode)
 112	return nil
 113}
 114
 115func GetJunk(userid int64, url string) (junk.Junk, error) {
 116	return GetJunkTimeout(userid, url, slowTimeout*time.Second, nil)
 117}
 118
 119func GetJunkFast(userid int64, url string) (junk.Junk, error) {
 120	return GetJunkTimeout(userid, url, fastTimeout*time.Second, nil)
 121}
 122
 123func GetJunkHardMode(userid int64, url string) (junk.Junk, error) {
 124	j, err := GetJunk(userid, url)
 125	if err != nil {
 126		emsg := err.Error()
 127		if emsg == "http get status: 429" || emsg == "http get status: 502" ||
 128			strings.Contains(emsg, "timeout") {
 129			ilog.Printf("trying %s again after error: %s", url, emsg)
 130			time.Sleep(time.Duration(60+notrand.Int63n(60)) * time.Second)
 131			j, err = GetJunk(userid, url)
 132			if err != nil {
 133				ilog.Printf("still couldn't get %s", url)
 134			} else {
 135				ilog.Printf("retry success!")
 136			}
 137		}
 138	}
 139	return j, err
 140}
 141
 142var flightdeck = gate.NewSerializer()
 143
 144func GetJunkTimeout(userid int64, url string, timeout time.Duration, final *string) (junk.Junk, error) {
 145	if rejectorigin(userid, url, false) {
 146		return nil, fmt.Errorf("rejected origin: %s", url)
 147	}
 148	if final != nil {
 149		*final = url
 150	}
 151	sign := func(req *http.Request) error {
 152		ki := ziggy(userid)
 153		if ki != nil {
 154			httpsig.SignRequest(ki.keyname, ki.seckey, req, nil)
 155		}
 156		return nil
 157	}
 158	if develMode {
 159		sign = nil
 160	}
 161	client := honkClient
 162	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
 163		if len(via) >= 5 {
 164			return fmt.Errorf("stopped after 5 redirects")
 165		}
 166		if final != nil {
 167			*final = req.URL.String()
 168		}
 169		if sign != nil {
 170			sign(req)
 171		}
 172		return nil
 173	}
 174	fn := func() (interface{}, error) {
 175		at := theonetruename
 176		if strings.Contains(url, ".well-known/webfinger?resource") {
 177			at = "application/jrd+json"
 178		}
 179		j, err := junk.Get(url, junk.GetArgs{
 180			Accept:  at,
 181			Agent:   "honksnonk/5.0; " + serverName,
 182			Timeout: timeout,
 183			Client:  &client,
 184			Fixup:   sign,
 185		})
 186		return j, err
 187	}
 188
 189	ji, err := flightdeck.Call(url, fn)
 190	if err != nil {
 191		return nil, err
 192	}
 193	j := ji.(junk.Junk)
 194	return j, nil
 195}
 196
 197func fetchsome(url string) ([]byte, error) {
 198	req, err := http.NewRequest("GET", url, nil)
 199	if err != nil {
 200		ilog.Printf("error fetching %s: %s", url, err)
 201		return nil, err
 202	}
 203	req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
 204	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
 205	defer cancel()
 206	req = req.WithContext(ctx)
 207	resp, err := honkClient.Do(req)
 208	if err != nil {
 209		ilog.Printf("error fetching %s: %s", url, err)
 210		return nil, err
 211	}
 212	defer resp.Body.Close()
 213	switch resp.StatusCode {
 214	case 200:
 215	case 201:
 216	case 202:
 217	default:
 218		return nil, fmt.Errorf("http get not 200: %d %s", resp.StatusCode, url)
 219	}
 220	var buf bytes.Buffer
 221	limiter := io.LimitReader(resp.Body, 10*1024*1024)
 222	io.Copy(&buf, limiter)
 223	return buf.Bytes(), nil
 224}
 225
 226func savedonk(url string, name, desc, media string, localize bool) *Donk {
 227	if url == "" {
 228		return nil
 229	}
 230	if donk := finddonk(url); donk != nil {
 231		return donk
 232	}
 233	ilog.Printf("saving donk: %s", url)
 234	data := []byte{}
 235	if localize {
 236		fn := func() (interface{}, error) {
 237			return fetchsome(url)
 238		}
 239		ii, err := flightdeck.Call(url, fn)
 240		if err != nil {
 241			ilog.Printf("error fetching donk: %s", err)
 242			localize = false
 243			goto saveit
 244		}
 245		data = ii.([]byte)
 246
 247		if len(data) == 10*1024*1024 {
 248			ilog.Printf("truncation likely")
 249		}
 250		if strings.HasPrefix(media, "image") {
 251			img, err := shrinkit(data)
 252			if err != nil {
 253				ilog.Printf("unable to decode image: %s", err)
 254				localize = false
 255				data = []byte{}
 256				goto saveit
 257			}
 258			data = img.Data
 259			media = "image/" + img.Format
 260		} else if media == "application/pdf" {
 261			if len(data) > 1000000 {
 262				ilog.Printf("not saving large pdf")
 263				localize = false
 264				data = []byte{}
 265			}
 266		} else if len(data) > 100000 {
 267			ilog.Printf("not saving large attachment")
 268			localize = false
 269			data = []byte{}
 270		}
 271	}
 272saveit:
 273	fileid, err := savefile(name, desc, url, media, localize, data)
 274	if err != nil {
 275		elog.Printf("error saving file %s: %s", url, err)
 276		return nil
 277	}
 278	donk := new(Donk)
 279	donk.FileID = fileid
 280	return donk
 281}
 282
 283func iszonked(userid int64, xid string) bool {
 284	var id int64
 285	row := stmtFindZonk.QueryRow(userid, xid)
 286	err := row.Scan(&id)
 287	if err == nil {
 288		return true
 289	}
 290	if err != sql.ErrNoRows {
 291		ilog.Printf("error querying zonk: %s", err)
 292	}
 293	return false
 294}
 295
 296func needxonk(user *WhatAbout, x *Honk) bool {
 297	if rejectxonk(x) {
 298		return false
 299	}
 300	return needxonkid(user, x.XID)
 301}
 302func needbonkid(user *WhatAbout, xid string) bool {
 303	return needxonkidX(user, xid, true)
 304}
 305func needxonkid(user *WhatAbout, xid string) bool {
 306	return needxonkidX(user, xid, false)
 307}
 308func needxonkidX(user *WhatAbout, xid string, isannounce bool) bool {
 309	if !strings.HasPrefix(xid, "https://") {
 310		return false
 311	}
 312	if strings.HasPrefix(xid, user.URL+"/") {
 313		return false
 314	}
 315	if rejectorigin(user.ID, xid, isannounce) {
 316		ilog.Printf("rejecting origin: %s", xid)
 317		return false
 318	}
 319	if iszonked(user.ID, xid) {
 320		ilog.Printf("already zonked: %s", xid)
 321		return false
 322	}
 323	var id int64
 324	row := stmtFindXonk.QueryRow(user.ID, xid)
 325	err := row.Scan(&id)
 326	if err == nil {
 327		return false
 328	}
 329	if err != sql.ErrNoRows {
 330		ilog.Printf("error querying xonk: %s", err)
 331	}
 332	return true
 333}
 334
 335func eradicatexonk(userid int64, xid string) {
 336	xonk := getxonk(userid, xid)
 337	if xonk != nil {
 338		deletehonk(xonk.ID)
 339		_, err := stmtSaveZonker.Exec(userid, xid, "zonk")
 340		if err != nil {
 341			elog.Printf("error eradicating: %s", err)
 342		}
 343	}
 344}
 345
 346func savexonk(x *Honk) {
 347	ilog.Printf("saving xonk: %s", x.XID)
 348	go handles(x.Honker)
 349	go handles(x.Oonker)
 350	savehonk(x)
 351}
 352
 353type Box struct {
 354	In     string
 355	Out    string
 356	Shared string
 357}
 358
 359var boxofboxes = gencache.New(gencache.Options[string, *Box]{Fill: func(ident string) (*Box, bool) {
 360	var info string
 361	row := stmtGetXonker.QueryRow(ident, "boxes")
 362	err := row.Scan(&info)
 363	if err != nil {
 364		dlog.Printf("need to get boxes for %s", ident)
 365		var j junk.Junk
 366		j, err = GetJunk(readyLuserOne, ident)
 367		if err != nil {
 368			dlog.Printf("error getting boxes: %s", err)
 369			return nil, false
 370		}
 371		allinjest(originate(ident), j)
 372		row = stmtGetXonker.QueryRow(ident, "boxes")
 373		err = row.Scan(&info)
 374	}
 375	if err == nil {
 376		m := strings.Split(info, " ")
 377		b := &Box{In: m[0], Out: m[1], Shared: m[2]}
 378		return b, true
 379	}
 380	return nil, false
 381}})
 382
 383var gettergate = gate.NewLimiter(1)
 384
 385func getsomemore(user *WhatAbout, page string) {
 386	time.Sleep(5 * time.Second)
 387	gettergate.Start()
 388	defer gettergate.Finish()
 389	gimmexonks(user, page)
 390}
 391
 392func gimmexonks(user *WhatAbout, outbox string) {
 393	dlog.Printf("getting outbox: %s", outbox)
 394	j, err := GetJunk(user.ID, outbox)
 395	if err != nil {
 396		ilog.Printf("error getting outbox: %s", err)
 397		return
 398	}
 399	t, _ := j.GetString("type")
 400	origin := originate(outbox)
 401	if t == "OrderedCollection" || t == "CollectionPage" {
 402		items, _ := j.GetArray("orderedItems")
 403		if items == nil {
 404			items, _ = j.GetArray("items")
 405		}
 406		if items == nil {
 407			obj, ok := j.GetMap("first")
 408			if ok {
 409				items, _ = obj.GetArray("orderedItems")
 410			} else {
 411				page1, ok := j.GetString("first")
 412				if ok {
 413					j, err = GetJunk(user.ID, page1)
 414					if err != nil {
 415						ilog.Printf("error getting page1: %s", err)
 416						return
 417					}
 418					items, _ = j.GetArray("orderedItems")
 419				}
 420			}
 421		}
 422		if len(items) > 20 {
 423			items = items[0:20]
 424		}
 425		for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
 426			items[i], items[j] = items[j], items[i]
 427		}
 428		for _, item := range items {
 429			obj, ok := item.(junk.Junk)
 430			if ok {
 431				xonksaver(user, obj, origin)
 432				continue
 433			}
 434			xid, ok := item.(string)
 435			if ok {
 436				if !needxonkid(user, xid) {
 437					continue
 438				}
 439				obj, err = GetJunk(user.ID, xid)
 440				if err != nil {
 441					ilog.Printf("error getting item: %s", err)
 442					continue
 443				}
 444				xonksaver(user, obj, originate(xid))
 445			}
 446		}
 447	}
 448}
 449
 450func newphone(a []string, obj junk.Junk) []string {
 451	for _, addr := range []string{"to", "cc", "attributedTo"} {
 452		who, _ := obj.GetString(addr)
 453		if who != "" {
 454			a = append(a, who)
 455		}
 456		whos, _ := obj.GetArray(addr)
 457		for _, w := range whos {
 458			who, _ := w.(string)
 459			if who != "" {
 460				a = append(a, who)
 461			}
 462		}
 463	}
 464	return a
 465}
 466
 467func extractattrto(obj junk.Junk) string {
 468	arr := oneforall(obj, "attributedTo")
 469	for _, a := range arr {
 470		s, ok := a.(string)
 471		if ok {
 472			return s
 473		}
 474		o, ok := a.(junk.Junk)
 475		if ok {
 476			t, _ := o.GetString("type")
 477			id, _ := o.GetString("id")
 478			if t == "Person" || t == "" {
 479				return id
 480			}
 481		}
 482	}
 483	return ""
 484}
 485
 486func oneforall(obj junk.Junk, key string) []interface{} {
 487	if val, ok := obj.GetMap(key); ok {
 488		return []interface{}{val}
 489	}
 490	if str, ok := obj.GetString(key); ok {
 491		return []interface{}{str}
 492	}
 493	arr, _ := obj.GetArray(key)
 494	return arr
 495}
 496
 497func firstofmany(obj junk.Junk, key string) string {
 498	if val, _ := obj.GetString(key); val != "" {
 499		return val
 500	}
 501	if arr, _ := obj.GetArray(key); len(arr) > 0 {
 502		val, ok := arr[0].(string)
 503		if ok {
 504			return val
 505		}
 506	}
 507	return ""
 508}
 509
 510var re_mast0link = regexp.MustCompile(`https://[[:alnum:].]+/users/[[:alnum:]]+/statuses/[[:digit:]]+`)
 511var re_masto1ink = regexp.MustCompile(`https://([[:alnum:].]+)/@([[:alnum:]]+)(@[[:alnum:].]+)?/([[:digit:]]+)`)
 512var re_misslink = regexp.MustCompile(`https://[[:alnum:].]+/notes/[[:alnum:]]+`)
 513var re_honklink = regexp.MustCompile(`https://[[:alnum:].]+/u/[[:alnum:]]+/h/[[:alnum:]]+`)
 514var re_r0malink = regexp.MustCompile(`https://[[:alnum:].]+/objects/[[:alnum:]-]+`)
 515var re_roma1ink = regexp.MustCompile(`https://[[:alnum:].]+/notice/[[:alnum:]]+`)
 516var re_qtlinks = regexp.MustCompile(`>https://[^\s<]+<`)
 517
 518func xonksaver(user *WhatAbout, item junk.Junk, origin string) *Honk {
 519	depth := 0
 520	maxdepth := 10
 521	currenttid := ""
 522	goingup := 0
 523	var xonkxonkfn func(junk.Junk, string, bool, string) *Honk
 524
 525	qutify := func(user *WhatAbout, qurl, content string) string {
 526		if depth >= maxdepth {
 527			ilog.Printf("in too deep")
 528			return content
 529		}
 530		// well this is gross
 531		malcontent := strings.ReplaceAll(content, `</span><span class="ellipsis">`, "")
 532		malcontent = strings.ReplaceAll(malcontent, `</span><span class="invisible">`, "")
 533		mlinks := re_qtlinks.FindAllString(malcontent, -1)
 534		if qurl != "" {
 535			mlinks = append(mlinks, ">"+qurl+"<")
 536		}
 537		mlinks = oneofakind(mlinks)
 538		for _, m := range mlinks {
 539			tryit := false
 540			m = m[1 : len(m)-1]
 541			if re_mast0link.MatchString(m) || re_misslink.MatchString(m) ||
 542				re_honklink.MatchString(m) || re_r0malink.MatchString(m) ||
 543				re_roma1ink.MatchString(m) {
 544				tryit = true
 545			} else if re_masto1ink.MatchString(m) {
 546				tryit = true
 547			}
 548			if tryit {
 549				var prefix string
 550				if m == qurl {
 551					prefix += fmt.Sprintf("<p><a href=\"%s\">%s</a>", m, m)
 552				}
 553				var final string
 554				if x := getxonk(user.ID, m); x != nil {
 555					content = fmt.Sprintf("%s%s<blockquote>%s</blockquote>", content, prefix, x.Noise)
 556				} else if j, err := GetJunkTimeout(user.ID, m, fastTimeout*time.Second, &final); err == nil {
 557					q, ok := j.GetString("content")
 558					if ok {
 559						content = fmt.Sprintf("%s%s<blockquote>%s</blockquote>", content, prefix, q)
 560					}
 561					prevdepth := depth
 562					depth = maxdepth
 563					xonkxonkfn(j, originate(final), false, "")
 564					depth = prevdepth
 565				}
 566			}
 567		}
 568		return content
 569	}
 570
 571	saveonemore := func(xid string) {
 572		dlog.Printf("getting onemore: %s", xid)
 573		if depth >= maxdepth {
 574			ilog.Printf("in too deep")
 575			return
 576		}
 577		obj, err := GetJunkHardMode(user.ID, xid)
 578		if err != nil {
 579			ilog.Printf("error getting onemore: %s: %s", xid, err)
 580			return
 581		}
 582		xonkxonkfn(obj, originate(xid), false, "")
 583	}
 584
 585	xonkxonkfn = func(item junk.Junk, origin string, isUpdate bool, bonker string) *Honk {
 586		id, _ := item.GetString("id")
 587		what := firstofmany(item, "type")
 588		dt, ok := item.GetString("published")
 589		if !ok {
 590			dt = time.Now().Format(time.RFC3339)
 591		}
 592		if depth >= maxdepth+5 {
 593			ilog.Printf("went too deep in xonkxonk")
 594			return nil
 595		}
 596		depth++
 597		defer func() { depth-- }()
 598
 599		var err error
 600		var xid, rid, url, convoy string
 601		var replies []string
 602		var obj junk.Junk
 603		waspage := false
 604		preferorig := false
 605		switch what {
 606		case "Delete":
 607			obj, ok = item.GetMap("object")
 608			if ok {
 609				xid, _ = obj.GetString("id")
 610			} else {
 611				xid, _ = item.GetString("object")
 612			}
 613			if xid == "" {
 614				return nil
 615			}
 616			if originate(xid) != origin {
 617				ilog.Printf("forged delete: %s", xid)
 618				return nil
 619			}
 620			ilog.Printf("eradicating %s", xid)
 621			eradicatexonk(user.ID, xid)
 622			return nil
 623		case "Remove":
 624			xid, _ = item.GetString("object")
 625			targ, _ := obj.GetString("target")
 626			ilog.Printf("remove %s from %s", obj, targ)
 627			return nil
 628		case "Tombstone":
 629			xid, _ = item.GetString("id")
 630			if xid == "" {
 631				return nil
 632			}
 633			if originate(xid) != origin {
 634				ilog.Printf("forged delete: %s", xid)
 635				return nil
 636			}
 637			ilog.Printf("eradicating %s", xid)
 638			eradicatexonk(user.ID, xid)
 639			return nil
 640		case "Announce":
 641			obj, ok = item.GetMap("object")
 642			if ok {
 643				// peek ahead some
 644				what := firstofmany(obj, "type")
 645				if what == "Create" || what == "Update" {
 646					if what == "Update" {
 647						isUpdate = true
 648					}
 649					inner, ok := obj.GetMap("object")
 650					if ok {
 651						obj = inner
 652					} else {
 653						xid, _ = obj.GetString("object")
 654					}
 655				}
 656				if xid == "" {
 657					xid, _ = obj.GetString("id")
 658				}
 659			} else {
 660				xid, _ = item.GetString("object")
 661			}
 662			if !isUpdate && !needbonkid(user, xid) {
 663				return nil
 664			}
 665			bonker, _ = item.GetString("actor")
 666			if originate(bonker) != origin {
 667				ilog.Printf("out of bounds actor in bonk: %s not from %s", bonker, origin)
 668				item.Write(ilog.Writer())
 669				return nil
 670			}
 671			origin = originate(xid)
 672			if ok && originate(id) == origin {
 673				dlog.Printf("using object in announce for %s", xid)
 674			} else {
 675				dlog.Printf("getting bonk: %s", xid)
 676				obj, err = GetJunkHardMode(user.ID, xid)
 677				if err != nil {
 678					ilog.Printf("error getting bonk: %s: %s", xid, err)
 679					return nil
 680				}
 681			}
 682			return xonkxonkfn(obj, origin, isUpdate, bonker)
 683		case "Update":
 684			isUpdate = true
 685			fallthrough
 686		case "Create":
 687			obj, ok = item.GetMap("object")
 688			if !ok {
 689				xid, _ = item.GetString("object")
 690				dlog.Printf("getting created honk: %s", xid)
 691				if originate(xid) != origin {
 692					ilog.Printf("out of bounds object in create: %s not from %s", xid, origin)
 693					return nil
 694				}
 695				obj, err = GetJunkHardMode(user.ID, xid)
 696				if err != nil {
 697					ilog.Printf("error getting creation: %s", err)
 698				}
 699			}
 700			if obj == nil {
 701				ilog.Printf("no object for creation %s", id)
 702				return nil
 703			}
 704			return xonkxonkfn(obj, origin, isUpdate, bonker)
 705		case "Read":
 706			xid, ok = item.GetString("object")
 707			if ok {
 708				if !needxonkid(user, xid) {
 709					dlog.Printf("don't need read obj: %s", xid)
 710					return nil
 711				}
 712				obj, err = GetJunkHardMode(user.ID, xid)
 713				if err != nil {
 714					ilog.Printf("error getting read: %s", err)
 715					return nil
 716				}
 717				return xonkxonkfn(obj, originate(xid), false, "")
 718			}
 719			return nil
 720		case "Add":
 721			xid, ok = item.GetString("object")
 722			if ok {
 723				// check target...
 724				if !needxonkid(user, xid) {
 725					dlog.Printf("don't need added obj: %s", xid)
 726					return nil
 727				}
 728				obj, err = GetJunkHardMode(user.ID, xid)
 729				if err != nil {
 730					ilog.Printf("error getting add: %s", err)
 731					return nil
 732				}
 733				return xonkxonkfn(obj, originate(xid), false, "")
 734			}
 735			return nil
 736		case "Move":
 737			obj = item
 738			what = "move"
 739		case "Page":
 740			waspage = true
 741			fallthrough
 742		case "Audio":
 743			fallthrough
 744		case "Image":
 745			if what == "Image" {
 746				preferorig = true
 747			}
 748			fallthrough
 749		case "Video":
 750			fallthrough
 751		case "Question":
 752			fallthrough
 753		case "Note":
 754			fallthrough
 755		case "Article":
 756			obj = item
 757			what = "honk"
 758		case "Event":
 759			obj = item
 760			what = "event"
 761		case "ChatMessage":
 762			bonker = ""
 763			obj = item
 764			what = "chonk"
 765		default:
 766			ilog.Printf("unknown activity: %s", what)
 767			dumpactivity(item)
 768			return nil
 769		}
 770		if bonker != "" {
 771			what = "bonk"
 772		}
 773
 774		if obj != nil {
 775			xid, _ = obj.GetString("id")
 776		}
 777
 778		if xid == "" {
 779			ilog.Printf("don't know what xid is")
 780			item.Write(ilog.Writer())
 781			return nil
 782		}
 783		if originate(xid) != origin {
 784			if !develMode && origin != "" {
 785				ilog.Printf("original sin: %s not from %s", xid, origin)
 786				item.Write(ilog.Writer())
 787				return nil
 788			}
 789		}
 790
 791		var xonk Honk
 792		// early init
 793		xonk.XID = xid
 794		xonk.UserID = user.ID
 795		xonk.Honker, _ = item.GetString("actor")
 796		if xonk.Honker == "" {
 797			xonk.Honker, _ = item.GetString("attributedTo")
 798		}
 799		if originate(xonk.Honker) != origin {
 800			ilog.Printf("out of bounds honker %s from %s", xonk.Honker, origin)
 801			item.Write(ilog.Writer())
 802			return nil
 803		}
 804		if obj != nil {
 805			if xonk.Honker == "" {
 806				xonk.Honker = extractattrto(obj)
 807			}
 808			if bonker != "" {
 809				xonk.Honker, xonk.Oonker = bonker, xonk.Honker
 810			}
 811			if xonk.Oonker == xonk.Honker {
 812				xonk.Oonker = ""
 813			}
 814			xonk.Audience = newphone(nil, obj)
 815		}
 816		xonk.Audience = append(xonk.Audience, xonk.Honker)
 817		xonk.Audience = oneofakind(xonk.Audience)
 818		for i, a := range xonk.Audience {
 819			if a == tinyworld {
 820				xonk.Audience[i] = thewholeworld
 821			}
 822		}
 823		xonk.Public = loudandproud(xonk.Audience)
 824
 825		var mentions []Mention
 826		if obj != nil {
 827			ot := firstofmany(obj, "type")
 828			url, _ = obj.GetString("url")
 829			if dt2, ok := obj.GetString("published"); ok {
 830				dt = dt2
 831			}
 832			content, _ := obj.GetString("content")
 833			if !strings.HasPrefix(content, "<p>") {
 834				content = "<p>" + content
 835			}
 836			precis, _ := obj.GetString("summary")
 837			if name, ok := obj.GetString("name"); ok {
 838				if precis != "" {
 839					content = precis + "<p>" + content
 840				}
 841				precis = html.EscapeString(name)
 842			}
 843			if sens, _ := obj["sensitive"].(bool); sens && precis == "" {
 844				precis = "unspecified horror"
 845			}
 846			if waspage {
 847				content += fmt.Sprintf(`<p><a href="%s">%s</a>`, url, url)
 848				url = xid
 849			}
 850			if user.Options.InlineQuotes {
 851				qurl, _ := obj.GetString("quoteUrl")
 852				content = qutify(user, qurl, content)
 853			}
 854			rid, ok = obj.GetString("inReplyTo")
 855			if !ok {
 856				if robj, ok := obj.GetMap("inReplyTo"); ok {
 857					rid, _ = robj.GetString("id")
 858				}
 859			}
 860			convoy, _ = obj.GetString("context")
 861			if convoy == "" {
 862				convoy, _ = obj.GetString("conversation")
 863			}
 864			if ot == "Question" {
 865				if what == "honk" {
 866					what = "qonk"
 867				}
 868				content += "<ul>"
 869				ans, _ := obj.GetArray("oneOf")
 870				for _, ai := range ans {
 871					a, ok := ai.(junk.Junk)
 872					if !ok {
 873						continue
 874					}
 875					as, _ := a.GetString("name")
 876					content += "<li>" + as
 877				}
 878				ans, _ = obj.GetArray("anyOf")
 879				for _, ai := range ans {
 880					a, ok := ai.(junk.Junk)
 881					if !ok {
 882						continue
 883					}
 884					as, _ := a.GetString("name")
 885					content += "<li>" + as
 886				}
 887				content += "</ul>"
 888			}
 889			if ot == "Move" {
 890				targ, _ := obj.GetString("target")
 891				content += string(templates.Sprintf(`<p>Moved to <a href="%s">%s</a>`, targ, targ))
 892			}
 893			if len(content) > 90001 {
 894				ilog.Printf("content too long. truncating")
 895				content = content[:90001]
 896			}
 897
 898			xonk.Noise = content
 899			xonk.Precis = precis
 900			if rejectxonk(&xonk) {
 901				dlog.Printf("fast reject: %s", xid)
 902				return nil
 903			}
 904
 905			numatts := 0
 906			var donkfns []func() *Donk
 907			procatt := func(att junk.Junk) {
 908				at, _ := att.GetString("type")
 909				mt, _ := att.GetString("mediaType")
 910				if mt == "" {
 911					mt = "image"
 912				}
 913				u, ok := att.GetString("url")
 914				if !ok {
 915					u, ok = att.GetString("href")
 916				}
 917				if !ok {
 918					if ua, ok := att.GetArray("url"); ok && len(ua) > 0 {
 919						u, ok = ua[0].(string)
 920						if !ok {
 921							if uu, ok := ua[0].(junk.Junk); ok {
 922								u, _ = uu.GetString("href")
 923								if mt == "" {
 924									mt, _ = uu.GetString("mediaType")
 925								}
 926							}
 927						}
 928					} else if uu, ok := att.GetMap("url"); ok {
 929						u, _ = uu.GetString("href")
 930						if mt == "" {
 931							mt, _ = uu.GetString("mediaType")
 932						}
 933					}
 934				}
 935				name, _ := att.GetString("name")
 936				desc, _ := att.GetString("summary")
 937				desc = html.UnescapeString(desc)
 938				if desc == "" {
 939					desc = name
 940				}
 941				localize := false
 942				if at == "Document" || at == "Image" {
 943					mt = strings.ToLower(mt)
 944					dlog.Printf("attachment: %s %s", mt, u)
 945					if mt == "text/plain" || mt == "application/pdf" ||
 946						strings.HasPrefix(mt, "image") {
 947						if numatts > 4 {
 948							ilog.Printf("excessive attachment: %s", at)
 949						} else {
 950							localize = true
 951						}
 952					}
 953				} else if at == "Link" {
 954					if waspage {
 955						xonk.Noise += fmt.Sprintf(`<p><a href="%s">%s</a>`, u, u)
 956						return
 957					}
 958					if name == "" {
 959						name = u
 960					}
 961				} else {
 962					ilog.Printf("unknown attachment: %s", at)
 963				}
 964				if skipMedia(&xonk) {
 965					localize = false
 966				}
 967				if preferorig && !localize {
 968					return
 969				}
 970				numatts++
 971				donkfns = append(donkfns, func() *Donk {
 972					return savedonk(u, name, desc, mt, localize)
 973				})
 974			}
 975			if img, ok := obj.GetMap("image"); ok {
 976				procatt(img)
 977			}
 978			if preferorig {
 979				atts, _ := obj.GetArray("url")
 980				for _, atti := range atts {
 981					att, ok := atti.(junk.Junk)
 982					if !ok {
 983						ilog.Printf("attachment that wasn't map?")
 984						continue
 985					}
 986					procatt(att)
 987				}
 988				if numatts == 0 {
 989					preferorig = false
 990				}
 991			}
 992			if !preferorig {
 993				atts := oneforall(obj, "attachment")
 994				for _, atti := range atts {
 995					att, ok := atti.(junk.Junk)
 996					if !ok {
 997						ilog.Printf("attachment that wasn't map?")
 998						continue
 999					}
1000					procatt(att)
1001				}
1002			}
1003			for _, donk := range multiRun(donkfns) {
1004				if donk != nil {
1005					xonk.Donks = append(xonk.Donks, donk)
1006				}
1007			}
1008			proctag := func(tag junk.Junk) {
1009				tt, _ := tag.GetString("type")
1010				name, _ := tag.GetString("name")
1011				desc, _ := tag.GetString("summary")
1012				desc = html.UnescapeString(desc)
1013				if desc == "" {
1014					desc = name
1015				}
1016				if tt == "Emoji" {
1017					icon, _ := tag.GetMap("icon")
1018					mt, _ := icon.GetString("mediaType")
1019					if mt == "" {
1020						mt = "image/png"
1021					}
1022					u, _ := icon.GetString("url")
1023					donk := savedonk(u, name, desc, mt, true)
1024					if donk != nil {
1025						xonk.Donks = append(xonk.Donks, donk)
1026					}
1027				}
1028				if tt == "Hashtag" {
1029					if name == "" || name == "#" {
1030						// skip it
1031					} else {
1032						if name[0] != '#' {
1033							name = "#" + name
1034						}
1035						xonk.Onts = append(xonk.Onts, name)
1036					}
1037				}
1038				if tt == "Place" {
1039					p := new(Place)
1040					p.Name = name
1041					p.Latitude, _ = tag.GetNumber("latitude")
1042					p.Longitude, _ = tag.GetNumber("longitude")
1043					p.Url, _ = tag.GetString("url")
1044					xonk.Place = p
1045				}
1046				if tt == "Mention" {
1047					var m Mention
1048					m.Who, _ = tag.GetString("name")
1049					m.Where, _ = tag.GetString("href")
1050					mentions = append(mentions, m)
1051				}
1052			}
1053			tags := oneforall(obj, "tag")
1054			for _, tagi := range tags {
1055				tag, ok := tagi.(junk.Junk)
1056				if !ok {
1057					continue
1058				}
1059				proctag(tag)
1060			}
1061			if starttime, ok := obj.GetString("startTime"); ok {
1062				if start, err := time.Parse(time.RFC3339, starttime); err == nil {
1063					t := new(Time)
1064					t.StartTime = start
1065					endtime, _ := obj.GetString("endTime")
1066					t.EndTime, _ = time.Parse(time.RFC3339, endtime)
1067					dura, _ := obj.GetString("duration")
1068					if strings.HasPrefix(dura, "PT") {
1069						dura = strings.ToLower(dura[2:])
1070						d, _ := time.ParseDuration(dura)
1071						t.Duration = Duration(d)
1072					}
1073					xonk.Time = t
1074				}
1075			}
1076			if loca, ok := obj.GetMap("location"); ok {
1077				if tt, _ := loca.GetString("type"); tt == "Place" {
1078					p := new(Place)
1079					p.Name, _ = loca.GetString("name")
1080					p.Latitude, _ = loca.GetNumber("latitude")
1081					p.Longitude, _ = loca.GetNumber("longitude")
1082					p.Url, _ = loca.GetString("url")
1083					xonk.Place = p
1084				}
1085			}
1086
1087			xonk.Onts = oneofakind(xonk.Onts)
1088			replyobj, ok := obj.GetMap("replies")
1089			if ok {
1090				items, ok := replyobj.GetArray("items")
1091				if !ok {
1092					first, ok := replyobj.GetMap("first")
1093					if ok {
1094						items, _ = first.GetArray("items")
1095					}
1096				}
1097				for _, repl := range items {
1098					s, ok := repl.(string)
1099					if ok {
1100						replies = append(replies, s)
1101					}
1102				}
1103			}
1104
1105		}
1106
1107		if currenttid == "" {
1108			currenttid = convoy
1109		}
1110
1111		// init xonk
1112		xonk.What = what
1113		xonk.RID = rid
1114		xonk.Date, _ = time.Parse(time.RFC3339, dt)
1115		xonk.URL = url
1116		xonk.Format = "html"
1117		xonk.Convoy = convoy
1118		xonk.Mentions = mentions
1119		for _, m := range mentions {
1120			if m.Where == user.URL {
1121				xonk.Whofore = 1
1122			}
1123		}
1124		imaginate(&xonk)
1125
1126		if what == "chonk" {
1127			// undo damage above
1128			xonk.Noise = strings.TrimPrefix(xonk.Noise, "<p>")
1129			target := firstofmany(obj, "to")
1130			if target == user.URL {
1131				target = xonk.Honker
1132			}
1133			enc, _ := obj.GetString(chatKeyProp)
1134			if enc != "" {
1135				var dec string
1136				if pubkey, ok := getchatkey(xonk.Honker); ok {
1137					dec, err = decryptString(xonk.Noise, user.ChatSecKey, pubkey)
1138					if err != nil {
1139						ilog.Printf("failed to decrypt chonk")
1140					}
1141				}
1142				if err == nil {
1143					dlog.Printf("successful decrypt from %s", xonk.Honker)
1144					xonk.Noise = dec
1145				}
1146			}
1147			ch := Chonk{
1148				UserID: xonk.UserID,
1149				XID:    xid,
1150				Who:    xonk.Honker,
1151				Target: target,
1152				Date:   xonk.Date,
1153				Noise:  xonk.Noise,
1154				Format: xonk.Format,
1155				Donks:  xonk.Donks,
1156			}
1157			savechonk(&ch)
1158			return nil
1159		}
1160
1161		if isUpdate {
1162			dlog.Printf("something has changed! %s", xonk.XID)
1163			prev := getxonk(user.ID, xonk.XID)
1164			if prev == nil {
1165				ilog.Printf("didn't find old version for update: %s", xonk.XID)
1166				isUpdate = false
1167			} else {
1168				xonk.ID = prev.ID
1169				updatehonk(&xonk)
1170			}
1171		}
1172		if !isUpdate && needxonk(user, &xonk) {
1173			if rid != "" && xonk.Public {
1174				if needxonkid(user, rid) {
1175					goingup++
1176					saveonemore(rid)
1177					goingup--
1178				}
1179				if convoy == "" {
1180					xx := getxonk(user.ID, rid)
1181					if xx != nil {
1182						convoy = xx.Convoy
1183					}
1184				}
1185			}
1186			if convoy == "" {
1187				convoy = currenttid
1188			}
1189			if convoy == "" {
1190				convoy = xonk.XID
1191				currenttid = convoy
1192			}
1193			xonk.Convoy = convoy
1194			savexonk(&xonk)
1195		}
1196		if goingup == 0 {
1197			for _, replid := range replies {
1198				if needxonkid(user, replid) {
1199					dlog.Printf("missing a reply: %s", replid)
1200					saveonemore(replid)
1201				}
1202			}
1203		}
1204		return &xonk
1205	}
1206
1207	return xonkxonkfn(item, origin, false, "")
1208}
1209
1210func dumpactivity(item junk.Junk) {
1211	fd, err := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
1212	if err != nil {
1213		elog.Printf("error opening inbox! %s", err)
1214		return
1215	}
1216	defer fd.Close()
1217	item.Write(fd)
1218	io.WriteString(fd, "\n")
1219}
1220
1221func rubadubdub(user *WhatAbout, req junk.Junk) {
1222	actor, _ := req.GetString("actor")
1223	j := junk.New()
1224	j["@context"] = itiswhatitis
1225	j["id"] = user.URL + "/dub/" + xfiltrate()
1226	j["type"] = "Accept"
1227	j["actor"] = user.URL
1228	j["to"] = actor
1229	j["published"] = time.Now().UTC().Format(time.RFC3339)
1230	j["object"] = req
1231
1232	deliverate(user.ID, actor, j.ToBytes())
1233}
1234
1235func itakeitallback(user *WhatAbout, xid string, owner string, folxid string) {
1236	j := junk.New()
1237	j["@context"] = itiswhatitis
1238	j["id"] = user.URL + "/unsub/" + folxid
1239	j["type"] = "Undo"
1240	j["actor"] = user.URL
1241	j["to"] = owner
1242	f := junk.New()
1243	f["id"] = user.URL + "/sub/" + folxid
1244	f["type"] = "Follow"
1245	f["actor"] = user.URL
1246	f["to"] = owner
1247	f["object"] = xid
1248	j["object"] = f
1249	j["published"] = time.Now().UTC().Format(time.RFC3339)
1250
1251	deliverate(user.ID, owner, j.ToBytes())
1252}
1253
1254func subsub(user *WhatAbout, xid string, owner string, folxid string) {
1255	if xid == "" {
1256		ilog.Printf("can't subscribe to empty")
1257		return
1258	}
1259	j := junk.New()
1260	j["@context"] = itiswhatitis
1261	j["id"] = user.URL + "/sub/" + folxid
1262	j["type"] = "Follow"
1263	j["actor"] = user.URL
1264	j["to"] = owner
1265	j["object"] = xid
1266	j["published"] = time.Now().UTC().Format(time.RFC3339)
1267
1268	deliverate(user.ID, owner, j.ToBytes())
1269}
1270
1271func activatedonks(donks []*Donk) []junk.Junk {
1272	var atts []junk.Junk
1273	for _, d := range donks {
1274		if re_emus.MatchString(d.Name) {
1275			continue
1276		}
1277		jd := junk.New()
1278		jd["mediaType"] = d.Media
1279		jd["name"] = d.Name
1280		jd["summary"] = html.EscapeString(d.Desc)
1281		jd["type"] = "Document"
1282		jd["url"] = d.URL
1283		atts = append(atts, jd)
1284	}
1285	return atts
1286}
1287
1288// returns activity, object
1289func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
1290	dt := h.Date.Format(time.RFC3339)
1291	var jo junk.Junk
1292	j := junk.New()
1293	j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
1294	j["actor"] = user.URL
1295	j["published"] = dt
1296	j["to"] = h.Audience[0]
1297	if len(h.Audience) > 1 {
1298		j["cc"] = h.Audience[1:]
1299	}
1300
1301	switch h.What {
1302	case "update":
1303		fallthrough
1304	case "event":
1305		fallthrough
1306	case "honk":
1307		j["type"] = "Create"
1308		jo = junk.New()
1309		jo["id"] = h.XID
1310		jo["type"] = "Note"
1311		if h.What == "event" {
1312			jo["type"] = "Event"
1313		}
1314		if h.What == "update" {
1315			j["type"] = "Update"
1316			jo["updated"] = dt
1317		}
1318		jo["published"] = dt
1319		jo["url"] = h.XID
1320		jo["attributedTo"] = user.URL
1321		if h.RID != "" {
1322			jo["inReplyTo"] = h.RID
1323		}
1324		if h.Convoy != "" {
1325			jo["context"] = h.Convoy
1326			jo["conversation"] = h.Convoy
1327		}
1328		jo["to"] = h.Audience[0]
1329		if len(h.Audience) > 1 {
1330			jo["cc"] = h.Audience[1:]
1331		}
1332		if !h.Public {
1333			jo["directMessage"] = true
1334		}
1335		h.Noise = re_retag.ReplaceAllString(h.Noise, "")
1336		translate(h)
1337		redoimages(h)
1338		if h.Precis != "" {
1339			jo["sensitive"] = true
1340		}
1341
1342		var replies []string
1343		for _, reply := range h.Replies {
1344			replies = append(replies, reply.XID)
1345		}
1346		if len(replies) > 0 {
1347			jr := junk.New()
1348			jr["type"] = "Collection"
1349			jr["totalItems"] = len(replies)
1350			jr["items"] = replies
1351			jo["replies"] = jr
1352		}
1353
1354		var tags []junk.Junk
1355		for _, m := range h.Mentions {
1356			t := junk.New()
1357			t["type"] = "Mention"
1358			t["name"] = m.Who
1359			t["href"] = m.Where
1360			tags = append(tags, t)
1361		}
1362		for _, o := range h.Onts {
1363			t := junk.New()
1364			t["type"] = "Hashtag"
1365			o = strings.ToLower(o)
1366			t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1367			t["name"] = o
1368			tags = append(tags, t)
1369		}
1370		for _, e := range herdofemus(h.Noise) {
1371			t := junk.New()
1372			t["id"] = e.ID
1373			t["type"] = "Emoji"
1374			t["name"] = e.Name
1375			i := junk.New()
1376			i["type"] = "Image"
1377			i["mediaType"] = e.Type
1378			i["url"] = e.ID
1379			t["icon"] = i
1380			tags = append(tags, t)
1381		}
1382		for _, e := range fixupflags(h) {
1383			t := junk.New()
1384			t["id"] = e.ID
1385			t["type"] = "Emoji"
1386			t["name"] = e.Name
1387			i := junk.New()
1388			i["type"] = "Image"
1389			i["mediaType"] = "image/png"
1390			i["url"] = e.ID
1391			t["icon"] = i
1392			tags = append(tags, t)
1393		}
1394		if len(tags) > 0 {
1395			jo["tag"] = tags
1396		}
1397		if p := h.Place; p != nil {
1398			t := junk.New()
1399			t["type"] = "Place"
1400			if p.Name != "" {
1401				t["name"] = p.Name
1402			}
1403			if p.Latitude != 0 {
1404				t["latitude"] = p.Latitude
1405			}
1406			if p.Longitude != 0 {
1407				t["longitude"] = p.Longitude
1408			}
1409			if p.Url != "" {
1410				t["url"] = p.Url
1411			}
1412			jo["location"] = t
1413		}
1414		if t := h.Time; t != nil {
1415			jo["startTime"] = t.StartTime.Format(time.RFC3339)
1416			if t.Duration != 0 {
1417				jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1418			}
1419		}
1420		atts := activatedonks(h.Donks)
1421		if len(atts) > 0 {
1422			jo["attachment"] = atts
1423		}
1424		jo["summary"] = h.Precis
1425		jo["content"] = h.Noise
1426		j["object"] = jo
1427	case "bonk":
1428		j["type"] = "Announce"
1429		if h.Convoy != "" {
1430			j["context"] = h.Convoy
1431		}
1432		j["object"] = h.XID
1433	case "unbonk":
1434		b := junk.New()
1435		b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1436		b["type"] = "Announce"
1437		b["actor"] = user.URL
1438		if h.Convoy != "" {
1439			b["context"] = h.Convoy
1440		}
1441		b["object"] = h.XID
1442		j["type"] = "Undo"
1443		j["object"] = b
1444	case "zonk":
1445		j["type"] = "Delete"
1446		j["object"] = h.XID
1447	case "ack":
1448		j["type"] = "Read"
1449		j["object"] = h.XID
1450		if h.Convoy != "" {
1451			j["context"] = h.Convoy
1452		}
1453	case "react":
1454		j["type"] = "EmojiReact"
1455		j["object"] = h.XID
1456		if h.Convoy != "" {
1457			j["context"] = h.Convoy
1458		}
1459		j["content"] = h.Noise
1460	case "deack":
1461		b := junk.New()
1462		b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1463		b["type"] = "Read"
1464		b["actor"] = user.URL
1465		b["object"] = h.XID
1466		if h.Convoy != "" {
1467			b["context"] = h.Convoy
1468		}
1469		j["type"] = "Undo"
1470		j["object"] = b
1471	}
1472
1473	return j, jo
1474}
1475
1476var oldjonks = gencache.New(gencache.Options[string, []byte]{Fill: func(xid string) ([]byte, bool) {
1477	row := stmtAnyXonk.QueryRow(xid)
1478	honk := scanhonk(row)
1479	if honk == nil || !honk.Public {
1480		return nil, true
1481	}
1482	user, _ := butwhatabout(honk.Username)
1483	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1484	reversehonks(rawhonks)
1485	for _, h := range rawhonks {
1486		if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
1487			honk.Replies = append(honk.Replies, h)
1488		}
1489	}
1490	donksforhonks([]*Honk{honk})
1491	_, j := jonkjonk(user, honk)
1492	if j == nil {
1493		elog.Fatalf("what just happened? %v", honk)
1494	}
1495	j["@context"] = itiswhatitis
1496
1497	return j.ToBytes(), true
1498}, Limit: 128})
1499
1500func gimmejonk(xid string) ([]byte, bool) {
1501	j, ok := oldjonks.Get(xid)
1502	return j, ok
1503}
1504
1505func boxuprcpts(user *WhatAbout, addresses []string, useshared bool) map[string]bool {
1506	rcpts := make(map[string]bool)
1507	for _, a := range addresses {
1508		if a == "" || a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1509			continue
1510		}
1511		if a[0] == '%' {
1512			rcpts[a] = true
1513			continue
1514		}
1515		box, ok := boxofboxes.Get(a)
1516		if ok && useshared && box.Shared != "" {
1517			rcpts["%"+box.Shared] = true
1518		} else {
1519			rcpts[a] = true
1520		}
1521	}
1522	return rcpts
1523}
1524
1525func chonkifymsg(user *WhatAbout, rcpt string, ch *Chonk) []byte {
1526	dt := ch.Date.Format(time.RFC3339)
1527	aud := []string{ch.Target}
1528
1529	jo := junk.New()
1530	jo["id"] = ch.XID
1531	jo["type"] = "ChatMessage"
1532	jo["published"] = dt
1533	jo["attributedTo"] = user.URL
1534	jo["to"] = aud
1535	content := string(ch.HTML)
1536	if user.ChatSecKey.key != nil {
1537		if pubkey, ok := getchatkey(rcpt); ok {
1538			var err error
1539			content, err = encryptString(content, user.ChatSecKey, pubkey)
1540			if err != nil {
1541				ilog.Printf("failure encrypting chonk: %s", err)
1542			}
1543			jo[chatKeyProp] = user.Options.ChatPubKey
1544		}
1545	}
1546	jo["content"] = content
1547
1548	atts := activatedonks(ch.Donks)
1549	if len(atts) > 0 {
1550		jo["attachment"] = atts
1551	}
1552	var tags []junk.Junk
1553	for _, e := range herdofemus(ch.Noise) {
1554		t := junk.New()
1555		t["id"] = e.ID
1556		t["type"] = "Emoji"
1557		t["name"] = e.Name
1558		i := junk.New()
1559		i["type"] = "Image"
1560		i["mediaType"] = e.Type
1561		i["url"] = e.ID
1562		t["icon"] = i
1563		tags = append(tags, t)
1564	}
1565	if len(tags) > 0 {
1566		jo["tag"] = tags
1567	}
1568
1569	j := junk.New()
1570	j["@context"] = itiswhatitis
1571	j["id"] = user.URL + "/" + "honk" + "/" + shortxid(ch.XID)
1572	j["type"] = "Create"
1573	j["actor"] = user.URL
1574	j["published"] = dt
1575	j["to"] = aud
1576	j["object"] = jo
1577
1578	return j.ToBytes()
1579}
1580
1581func sendchonk(user *WhatAbout, ch *Chonk) {
1582	rcpts := make(map[string]bool)
1583	rcpts[ch.Target] = true
1584	for a := range rcpts {
1585		msg := chonkifymsg(user, a, ch)
1586		go deliverate(user.ID, a, msg)
1587	}
1588}
1589
1590func honkworldwide(user *WhatAbout, honk *Honk) {
1591	jonk, _ := jonkjonk(user, honk)
1592	jonk["@context"] = itiswhatitis
1593	msg := jonk.ToBytes()
1594
1595	rcpts := boxuprcpts(user, honk.Audience, honk.Public)
1596
1597	if honk.Public {
1598		for _, h := range getdubs(user.ID) {
1599			if h.XID == user.URL {
1600				continue
1601			}
1602			box, ok := boxofboxes.Get(h.XID)
1603			if ok && box.Shared != "" {
1604				rcpts["%"+box.Shared] = true
1605			} else {
1606				rcpts[h.XID] = true
1607			}
1608		}
1609		for _, f := range getbacktracks(honk.XID) {
1610			if f[0] == '%' {
1611				rcpts[f] = true
1612			} else {
1613				box, ok := boxofboxes.Get(f)
1614				if ok && box.Shared != "" {
1615					rcpts["%"+box.Shared] = true
1616				} else {
1617					rcpts[f] = true
1618				}
1619			}
1620		}
1621	}
1622	for a := range rcpts {
1623		go deliverate(user.ID, a, msg)
1624	}
1625	if honk.Public && len(honk.Onts) > 0 {
1626		collectiveaction(honk)
1627	}
1628}
1629
1630func collectiveaction(honk *Honk) {
1631	user := getserveruser()
1632	for _, ont := range honk.Onts {
1633		dubs := getnameddubs(readyLuserOne, ont)
1634		if len(dubs) == 0 {
1635			continue
1636		}
1637		j := junk.New()
1638		j["@context"] = itiswhatitis
1639		j["type"] = "Add"
1640		j["id"] = user.URL + "/add/" + shortxid(ont+honk.XID)
1641		j["actor"] = user.URL
1642		j["object"] = honk.XID
1643		j["target"] = fmt.Sprintf("https://%s/o/%s", serverName, ont[1:])
1644		rcpts := make(map[string]bool)
1645		for _, dub := range dubs {
1646			box, ok := boxofboxes.Get(dub.XID)
1647			if ok && box.Shared != "" {
1648				rcpts["%"+box.Shared] = true
1649			} else {
1650				rcpts[dub.XID] = true
1651			}
1652		}
1653		msg := j.ToBytes()
1654		for a := range rcpts {
1655			go deliverate(user.ID, a, msg)
1656		}
1657	}
1658}
1659
1660func junkuser(user *WhatAbout) junk.Junk {
1661	j := junk.New()
1662	j["@context"] = itiswhatitis
1663	j["id"] = user.URL
1664	j["inbox"] = user.URL + "/inbox"
1665	j["outbox"] = user.URL + "/outbox"
1666	j["name"] = user.Display
1667	j["preferredUsername"] = user.Name
1668	j["summary"] = user.HTAbout
1669	var tags []junk.Junk
1670	for _, o := range user.Onts {
1671		t := junk.New()
1672		t["type"] = "Hashtag"
1673		o = strings.ToLower(o)
1674		t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1675		t["name"] = o
1676		tags = append(tags, t)
1677	}
1678	if len(tags) > 0 {
1679		j["tag"] = tags
1680	}
1681
1682	if user.ID > 0 {
1683		j["type"] = "Person"
1684		j["url"] = user.URL
1685		j["followers"] = user.URL + "/followers"
1686		j["following"] = user.URL + "/following"
1687		a := junk.New()
1688		a["type"] = "Image"
1689		a["mediaType"] = "image/png"
1690		a["url"] = avatarURL(user)
1691		j["icon"] = a
1692		if ban := user.Options.Banner; ban != "" {
1693			a := junk.New()
1694			a["type"] = "Image"
1695			a["mediaType"] = "image/jpg"
1696			a["url"] = ban
1697			j["image"] = a
1698		}
1699	} else {
1700		j["type"] = "Service"
1701	}
1702	k := junk.New()
1703	k["id"] = user.URL + "#key"
1704	k["owner"] = user.URL
1705	k["publicKeyPem"] = user.Key
1706	j["publicKey"] = k
1707	j[chatKeyProp] = user.Options.ChatPubKey
1708
1709	return j
1710}
1711
1712var oldjonkers = gencache.New(gencache.Options[string, []byte]{Fill: func(name string) ([]byte, bool) {
1713	user, err := butwhatabout(name)
1714	if err != nil {
1715		return nil, false
1716	}
1717	j := junkuser(user)
1718	return j.ToBytes(), true
1719}, Duration: 1 * time.Minute})
1720
1721func asjonker(name string) ([]byte, bool) {
1722	j, ok := oldjonkers.Get(name)
1723	return j, ok
1724}
1725
1726var handfull = gencache.New(gencache.Options[string, string]{Fill: func(name string) (string, bool) {
1727	m := strings.Split(name, "@")
1728	if len(m) != 2 {
1729		dlog.Printf("bad fish name: %s", name)
1730		return "", true
1731	}
1732	var href string
1733	row := stmtGetXonker.QueryRow(name, "fishname")
1734	err := row.Scan(&href)
1735	if err == nil {
1736		return href, true
1737	}
1738	dlog.Printf("fishing for %s", name)
1739	j, err := GetJunkFast(readyLuserOne, fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1740	if err != nil {
1741		ilog.Printf("failed to go fish %s: %s", name, err)
1742		return "", true
1743	}
1744	links, _ := j.GetArray("links")
1745	for _, li := range links {
1746		l, ok := li.(junk.Junk)
1747		if !ok {
1748			continue
1749		}
1750		href, _ := l.GetString("href")
1751		rel, _ := l.GetString("rel")
1752		t, _ := l.GetString("type")
1753		if rel == "self" && friendorfoe(t) {
1754			when := time.Now().UTC().Format(dbtimeformat)
1755			_, err := stmtSaveXonker.Exec(name, href, "fishname", when)
1756			if err != nil {
1757				elog.Printf("error saving fishname: %s", err)
1758			}
1759			return href, true
1760		}
1761	}
1762	return href, true
1763}, Duration: 1 * time.Minute})
1764
1765func gofish(name string) string {
1766	if name[0] == '@' {
1767		name = name[1:]
1768	}
1769	href, _ := handfull.Get(name)
1770	return href
1771}
1772
1773func investigate(name string) (*SomeThing, error) {
1774	if name == "" {
1775		return nil, fmt.Errorf("no name")
1776	}
1777	if name[0] == '@' {
1778		name = gofish(name)
1779	}
1780	if name == "" {
1781		return nil, fmt.Errorf("no name")
1782	}
1783	obj, err := GetJunkFast(readyLuserOne, name)
1784	if err != nil {
1785		return nil, err
1786	}
1787	allinjest(originate(name), obj)
1788	return somethingabout(obj)
1789}
1790
1791func somethingabout(obj junk.Junk) (*SomeThing, error) {
1792	info := new(SomeThing)
1793	t, _ := obj.GetString("type")
1794	isowned := false
1795	switch t {
1796	case "Person":
1797		fallthrough
1798	case "Group":
1799		fallthrough
1800	case "Organization":
1801		fallthrough
1802	case "Application":
1803		fallthrough
1804	case "Service":
1805		info.What = SomeActor
1806	case "OrderedCollection":
1807		isowned = true
1808		fallthrough
1809	case "Collection":
1810		info.What = SomeCollection
1811	default:
1812		return nil, fmt.Errorf("unknown object type")
1813	}
1814	info.XID, _ = obj.GetString("id")
1815	info.Name, _ = obj.GetString("preferredUsername")
1816	if info.Name == "" {
1817		info.Name, _ = obj.GetString("name")
1818	}
1819	if isowned {
1820		info.Owner, _ = obj.GetString("attributedTo")
1821	}
1822	if info.Owner == "" {
1823		info.Owner = info.XID
1824	}
1825	return info, nil
1826}
1827
1828func allinjest(origin string, obj junk.Junk) {
1829	ident, _ := obj.GetString("id")
1830	if ident == "" {
1831		return
1832	}
1833	if originate(ident) != origin {
1834		return
1835	}
1836	keyobj, ok := obj.GetMap("publicKey")
1837	if ok {
1838		ingestpubkey(origin, keyobj)
1839	}
1840	ingestboxes(origin, obj)
1841	ingesthandle(origin, obj)
1842	chatkey, ok := obj.GetString(chatKeyProp)
1843	if ok {
1844		when := time.Now().UTC().Format(dbtimeformat)
1845		_, err := stmtSaveXonker.Exec(ident, chatkey, chatKeyProp, when)
1846		if err != nil {
1847			elog.Printf("error saving chatkey: %s", err)
1848		}
1849	}
1850}
1851
1852func ingestpubkey(origin string, obj junk.Junk) {
1853	keyobj, ok := obj.GetMap("publicKey")
1854	if ok {
1855		obj = keyobj
1856	}
1857	keyname, ok := obj.GetString("id")
1858	var data string
1859	row := stmtGetXonker.QueryRow(keyname, "pubkey")
1860	err := row.Scan(&data)
1861	if err == nil {
1862		return
1863	}
1864	if !ok || origin != originate(keyname) {
1865		ilog.Printf("bad key origin %s <> %s", origin, keyname)
1866		return
1867	}
1868	dlog.Printf("ingesting a needed pubkey: %s", keyname)
1869	owner, ok := obj.GetString("owner")
1870	if !ok {
1871		ilog.Printf("error finding %s pubkey owner", keyname)
1872		return
1873	}
1874	data, ok = obj.GetString("publicKeyPem")
1875	if !ok {
1876		ilog.Printf("error finding %s pubkey", keyname)
1877		return
1878	}
1879	if originate(owner) != origin {
1880		ilog.Printf("bad key owner: %s <> %s", owner, origin)
1881		return
1882	}
1883	_, _, err = httpsig.DecodeKey(data)
1884	if err != nil {
1885		ilog.Printf("error decoding %s pubkey: %s", keyname, err)
1886		return
1887	}
1888	when := time.Now().UTC().Format(dbtimeformat)
1889	_, err = stmtSaveXonker.Exec(keyname, data, "pubkey", when)
1890	if err != nil {
1891		elog.Printf("error saving key: %s", err)
1892	}
1893}
1894
1895func ingestboxes(origin string, obj junk.Junk) {
1896	ident, _ := obj.GetString("id")
1897	if ident == "" {
1898		return
1899	}
1900	if originate(ident) != origin {
1901		return
1902	}
1903	var info string
1904	row := stmtGetXonker.QueryRow(ident, "boxes")
1905	err := row.Scan(&info)
1906	if err == nil {
1907		return
1908	}
1909	dlog.Printf("ingesting boxes: %s", ident)
1910	inbox, _ := obj.GetString("inbox")
1911	outbox, _ := obj.GetString("outbox")
1912	sbox, _ := obj.GetString("endpoints", "sharedInbox")
1913	if inbox != "" {
1914		when := time.Now().UTC().Format(dbtimeformat)
1915		m := strings.Join([]string{inbox, outbox, sbox}, " ")
1916		_, err = stmtSaveXonker.Exec(ident, m, "boxes", when)
1917		if err != nil {
1918			elog.Printf("error saving boxes: %s", err)
1919		}
1920	}
1921}
1922
1923func ingesthandle(origin string, obj junk.Junk) {
1924	xid, _ := obj.GetString("id")
1925	if xid == "" {
1926		return
1927	}
1928	if originate(xid) != origin {
1929		return
1930	}
1931	var handle string
1932	row := stmtGetXonker.QueryRow(xid, "handle")
1933	err := row.Scan(&handle)
1934	if err == nil {
1935		return
1936	}
1937	handle, _ = obj.GetString("preferredUsername")
1938	if handle != "" {
1939		when := time.Now().UTC().Format(dbtimeformat)
1940		_, err = stmtSaveXonker.Exec(xid, handle, "handle", when)
1941		if err != nil {
1942			elog.Printf("error saving handle: %s", err)
1943		}
1944	}
1945}
1946
1947func updateMe(username string) {
1948	user, _ := somenamedusers.Get(username)
1949	dt := time.Now().UTC().Format(time.RFC3339)
1950	j := junk.New()
1951	j["@context"] = itiswhatitis
1952	j["id"] = fmt.Sprintf("%s/upme/%s/%d", user.URL, user.Name, time.Now().Unix())
1953	j["actor"] = user.URL
1954	j["published"] = dt
1955	j["to"] = thewholeworld
1956	j["type"] = "Update"
1957	j["object"] = junkuser(user)
1958
1959	msg := j.ToBytes()
1960
1961	rcpts := make(map[string]bool)
1962	for _, f := range getdubs(user.ID) {
1963		if f.XID == user.URL {
1964			continue
1965		}
1966		box, ok := boxofboxes.Get(f.XID)
1967		if ok && box.Shared != "" {
1968			rcpts["%"+box.Shared] = true
1969		} else {
1970			rcpts[f.XID] = true
1971		}
1972	}
1973	for a := range rcpts {
1974		go deliverate(user.ID, a, msg)
1975	}
1976}
1977
1978func followme(user *WhatAbout, who string, name string, j junk.Junk) {
1979	folxid, _ := j.GetString("id")
1980
1981	ilog.Printf("updating honker follow: %s %s", who, folxid)
1982
1983	var x string
1984	db := opendatabase()
1985	row := db.QueryRow("select xid from honkers where name = ? and xid = ? and userid = ? and flavor in ('dub', 'undub')", name, who, user.ID)
1986	err := row.Scan(&x)
1987	if err != sql.ErrNoRows {
1988		ilog.Printf("duplicate follow request: %s", who)
1989		_, err = stmtUpdateFlavor.Exec("dub", folxid, user.ID, name, who, "undub")
1990		if err != nil {
1991			elog.Printf("error updating honker: %s", err)
1992		}
1993	} else {
1994		stmtSaveDub.Exec(user.ID, name, who, "dub", folxid)
1995	}
1996	go rubadubdub(user, j)
1997}
1998
1999func unfollowme(user *WhatAbout, who string, name string, j junk.Junk) {
2000	var folxid string
2001	if who == "" {
2002		folxid, _ = j.GetString("object")
2003
2004		db := opendatabase()
2005		row := db.QueryRow("select xid, name from honkers where userid = ? and folxid = ? and flavor in ('dub', 'undub')", user.ID, folxid)
2006		err := row.Scan(&who, &name)
2007		if err != nil {
2008			if err != sql.ErrNoRows {
2009				elog.Printf("error scanning honker: %s", err)
2010			}
2011			return
2012		}
2013	}
2014
2015	ilog.Printf("updating honker undo: %s %s", who, folxid)
2016	_, err := stmtUpdateFlavor.Exec("undub", folxid, user.ID, name, who, "dub")
2017	if err != nil {
2018		elog.Printf("error updating honker: %s", err)
2019		return
2020	}
2021}
2022
2023func followyou(user *WhatAbout, honkerid int64, sync bool) {
2024	var url, owner string
2025	db := opendatabase()
2026	row := db.QueryRow("select xid, owner from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')",
2027		honkerid, user.ID)
2028	err := row.Scan(&url, &owner)
2029	if err != nil {
2030		elog.Printf("can't get honker xid: %s", err)
2031		return
2032	}
2033	folxid := xfiltrate()
2034	ilog.Printf("subscribing to %s", url)
2035	_, err = db.Exec("update honkers set flavor = ?, folxid = ? where honkerid = ?", "presub", folxid, honkerid)
2036	if err != nil {
2037		elog.Printf("error updating honker: %s", err)
2038		return
2039	}
2040	if sync {
2041		subsub(user, url, owner, folxid)
2042	} else {
2043		go subsub(user, url, owner, folxid)
2044	}
2045
2046}
2047func unfollowyou(user *WhatAbout, honkerid int64, sync bool) {
2048	db := opendatabase()
2049	row := db.QueryRow("select xid, owner, folxid, flavor from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')",
2050		honkerid, user.ID)
2051	var url, owner, folxid, flavor string
2052	err := row.Scan(&url, &owner, &folxid, &flavor)
2053	if err != nil {
2054		elog.Printf("can't get honker xid: %s", err)
2055		return
2056	}
2057	if flavor == "peep" {
2058		return
2059	}
2060	ilog.Printf("unsubscribing from %s", url)
2061	_, err = db.Exec("update honkers set flavor = ? where honkerid = ?", "unsub", honkerid)
2062	if err != nil {
2063		elog.Printf("error updating honker: %s", err)
2064		return
2065	}
2066	if sync {
2067		itakeitallback(user, url, owner, folxid)
2068	} else {
2069		go itakeitallback(user, url, owner, folxid)
2070	}
2071}
2072
2073func followyou2(user *WhatAbout, j junk.Junk) {
2074	who, _ := j.GetString("actor")
2075
2076	ilog.Printf("updating honker accept: %s", who)
2077	db := opendatabase()
2078	row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')",
2079		user.ID, who)
2080	var name, folxid string
2081	err := row.Scan(&name, &folxid)
2082	if err != nil {
2083		elog.Printf("can't get honker name: %s", err)
2084		return
2085	}
2086	_, err = stmtUpdateFlavor.Exec("sub", folxid, user.ID, name, who, "presub")
2087	if err != nil {
2088		elog.Printf("error updating honker: %s", err)
2089		return
2090	}
2091}
2092
2093func nofollowyou2(user *WhatAbout, j junk.Junk) {
2094	who, _ := j.GetString("actor")
2095
2096	ilog.Printf("updating honker reject: %s", who)
2097	db := opendatabase()
2098	row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')",
2099		user.ID, who)
2100	var name, folxid string
2101	err := row.Scan(&name, &folxid)
2102	if err != nil {
2103		elog.Printf("can't get honker name: %s", err)
2104		return
2105	}
2106	_, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "presub")
2107	_, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "sub")
2108	if err != nil {
2109		elog.Printf("error updating honker: %s", err)
2110		return
2111	}
2112}
2113
2114func multiRun[R any](fns []func() R) []R {
2115	results := make([]R, len(fns))
2116	var wg sync.WaitGroup
2117	for i, fn := range fns {
2118		wg.Add(1)
2119		go func(i int, fn func() R) {
2120			results[i] = fn()
2121			wg.Done()
2122		}(i, fn)
2123	}
2124	wg.Wait()
2125	return results
2126}