all repos — honk @ 3aa99097ca8c102e83f9f6a12d50dbb4d1400fe7

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