all repos — honk @ 1eb3f5b2dc7f16cecd88dd303276ffbe1aa3bddc

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