all repos — honk @ v1.2.1

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, 10*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, _ = item.GetString("attributedTo")
 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	j["to"] = h.Audience[0]
1291	if len(h.Audience) > 1 {
1292		j["cc"] = h.Audience[1:]
1293	}
1294
1295	switch h.What {
1296	case "update":
1297		fallthrough
1298	case "event":
1299		fallthrough
1300	case "honk":
1301		j["type"] = "Create"
1302		jo = junk.New()
1303		jo["id"] = h.XID
1304		jo["type"] = "Note"
1305		if h.What == "event" {
1306			jo["type"] = "Event"
1307		}
1308		if h.What == "update" {
1309			j["type"] = "Update"
1310			jo["updated"] = dt
1311		}
1312		jo["published"] = dt
1313		jo["url"] = h.XID
1314		jo["attributedTo"] = user.URL
1315		if h.RID != "" {
1316			jo["inReplyTo"] = h.RID
1317		}
1318		if h.Convoy != "" {
1319			jo["context"] = h.Convoy
1320			jo["conversation"] = h.Convoy
1321		}
1322		jo["to"] = h.Audience[0]
1323		if len(h.Audience) > 1 {
1324			jo["cc"] = h.Audience[1:]
1325		}
1326		if !h.Public {
1327			jo["directMessage"] = true
1328		}
1329		h.Noise = re_retag.ReplaceAllString(h.Noise, "")
1330		translate(h)
1331		redoimages(h)
1332		if h.Precis != "" {
1333			jo["sensitive"] = true
1334		}
1335
1336		var replies []string
1337		for _, reply := range h.Replies {
1338			replies = append(replies, reply.XID)
1339		}
1340		if len(replies) > 0 {
1341			jr := junk.New()
1342			jr["type"] = "Collection"
1343			jr["totalItems"] = len(replies)
1344			jr["items"] = replies
1345			jo["replies"] = jr
1346		}
1347
1348		var tags []junk.Junk
1349		for _, m := range h.Mentions {
1350			t := junk.New()
1351			t["type"] = "Mention"
1352			t["name"] = m.Who
1353			t["href"] = m.Where
1354			tags = append(tags, t)
1355		}
1356		for _, o := range h.Onts {
1357			t := junk.New()
1358			t["type"] = "Hashtag"
1359			o = strings.ToLower(o)
1360			t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1361			t["name"] = o
1362			tags = append(tags, t)
1363		}
1364		for _, e := range herdofemus(h.Noise) {
1365			t := junk.New()
1366			t["id"] = e.ID
1367			t["type"] = "Emoji"
1368			t["name"] = e.Name
1369			i := junk.New()
1370			i["type"] = "Image"
1371			i["mediaType"] = e.Type
1372			i["url"] = e.ID
1373			t["icon"] = i
1374			tags = append(tags, t)
1375		}
1376		for _, e := range fixupflags(h) {
1377			t := junk.New()
1378			t["id"] = e.ID
1379			t["type"] = "Emoji"
1380			t["name"] = e.Name
1381			i := junk.New()
1382			i["type"] = "Image"
1383			i["mediaType"] = "image/png"
1384			i["url"] = e.ID
1385			t["icon"] = i
1386			tags = append(tags, t)
1387		}
1388		if len(tags) > 0 {
1389			jo["tag"] = tags
1390		}
1391		if p := h.Place; p != nil {
1392			t := junk.New()
1393			t["type"] = "Place"
1394			if p.Name != "" {
1395				t["name"] = p.Name
1396			}
1397			if p.Latitude != 0 {
1398				t["latitude"] = p.Latitude
1399			}
1400			if p.Longitude != 0 {
1401				t["longitude"] = p.Longitude
1402			}
1403			if p.Url != "" {
1404				t["url"] = p.Url
1405			}
1406			jo["location"] = t
1407		}
1408		if t := h.Time; t != nil {
1409			jo["startTime"] = t.StartTime.Format(time.RFC3339)
1410			if t.Duration != 0 {
1411				jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1412			}
1413		}
1414		atts := activatedonks(h.Donks)
1415		if len(atts) > 0 {
1416			jo["attachment"] = atts
1417		}
1418		jo["summary"] = h.Precis
1419		jo["content"] = h.Noise
1420		j["object"] = jo
1421	case "bonk":
1422		j["type"] = "Announce"
1423		if h.Convoy != "" {
1424			j["context"] = h.Convoy
1425		}
1426		j["object"] = h.XID
1427	case "unbonk":
1428		b := junk.New()
1429		b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1430		b["type"] = "Announce"
1431		b["actor"] = user.URL
1432		if h.Convoy != "" {
1433			b["context"] = h.Convoy
1434		}
1435		b["object"] = h.XID
1436		j["type"] = "Undo"
1437		j["object"] = b
1438	case "zonk":
1439		j["type"] = "Delete"
1440		j["object"] = h.XID
1441	case "ack":
1442		j["type"] = "Read"
1443		j["object"] = h.XID
1444		if h.Convoy != "" {
1445			j["context"] = h.Convoy
1446		}
1447	case "react":
1448		j["type"] = "EmojiReact"
1449		j["object"] = h.XID
1450		if h.Convoy != "" {
1451			j["context"] = h.Convoy
1452		}
1453		j["content"] = h.Noise
1454	case "deack":
1455		b := junk.New()
1456		b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1457		b["type"] = "Read"
1458		b["actor"] = user.URL
1459		b["object"] = h.XID
1460		if h.Convoy != "" {
1461			b["context"] = h.Convoy
1462		}
1463		j["type"] = "Undo"
1464		j["object"] = b
1465	}
1466
1467	return j, jo
1468}
1469
1470var oldjonks = gencache.New(gencache.Options[string, []byte]{Fill: func(xid string) ([]byte, bool) {
1471	row := stmtAnyXonk.QueryRow(xid)
1472	honk := scanhonk(row)
1473	if honk == nil || !honk.Public {
1474		return nil, true
1475	}
1476	user, _ := butwhatabout(honk.Username)
1477	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1478	reversehonks(rawhonks)
1479	for _, h := range rawhonks {
1480		if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
1481			honk.Replies = append(honk.Replies, h)
1482		}
1483	}
1484	donksforhonks([]*Honk{honk})
1485	_, j := jonkjonk(user, honk)
1486	if j == nil {
1487		elog.Fatalf("what just happened? %v", honk)
1488	}
1489	j["@context"] = itiswhatitis
1490
1491	return j.ToBytes(), true
1492}, Limit: 128})
1493
1494func gimmejonk(xid string) ([]byte, bool) {
1495	j, ok := oldjonks.Get(xid)
1496	return j, ok
1497}
1498
1499func boxuprcpts(user *WhatAbout, addresses []string, useshared bool) map[string]bool {
1500	rcpts := make(map[string]bool)
1501	for _, a := range addresses {
1502		if a == "" || a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1503			continue
1504		}
1505		if a[0] == '%' {
1506			rcpts[a] = true
1507			continue
1508		}
1509		box, ok := boxofboxes.Get(a)
1510		if ok && useshared && box.Shared != "" {
1511			rcpts["%"+box.Shared] = true
1512		} else {
1513			rcpts[a] = true
1514		}
1515	}
1516	return rcpts
1517}
1518
1519func chonkifymsg(user *WhatAbout, rcpt string, ch *Chonk) []byte {
1520	dt := ch.Date.Format(time.RFC3339)
1521	aud := []string{ch.Target}
1522
1523	jo := junk.New()
1524	jo["id"] = ch.XID
1525	jo["type"] = "ChatMessage"
1526	jo["published"] = dt
1527	jo["attributedTo"] = user.URL
1528	jo["to"] = aud
1529	content := string(ch.HTML)
1530	if user.ChatSecKey.key != nil {
1531		if pubkey, ok := getchatkey(rcpt); ok {
1532			var err error
1533			content, err = encryptString(content, user.ChatSecKey, pubkey)
1534			if err != nil {
1535				ilog.Printf("failure encrypting chonk: %s", err)
1536			}
1537			jo[chatKeyProp] = user.Options.ChatPubKey
1538		}
1539	}
1540	jo["content"] = content
1541
1542	atts := activatedonks(ch.Donks)
1543	if len(atts) > 0 {
1544		jo["attachment"] = atts
1545	}
1546	var tags []junk.Junk
1547	for _, e := range herdofemus(ch.Noise) {
1548		t := junk.New()
1549		t["id"] = e.ID
1550		t["type"] = "Emoji"
1551		t["name"] = e.Name
1552		i := junk.New()
1553		i["type"] = "Image"
1554		i["mediaType"] = e.Type
1555		i["url"] = e.ID
1556		t["icon"] = i
1557		tags = append(tags, t)
1558	}
1559	if len(tags) > 0 {
1560		jo["tag"] = tags
1561	}
1562
1563	j := junk.New()
1564	j["@context"] = itiswhatitis
1565	j["id"] = user.URL + "/" + "honk" + "/" + shortxid(ch.XID)
1566	j["type"] = "Create"
1567	j["actor"] = user.URL
1568	j["published"] = dt
1569	j["to"] = aud
1570	j["object"] = jo
1571
1572	return j.ToBytes()
1573}
1574
1575func sendchonk(user *WhatAbout, ch *Chonk) {
1576	rcpts := make(map[string]bool)
1577	rcpts[ch.Target] = true
1578	for a := range rcpts {
1579		msg := chonkifymsg(user, a, ch)
1580		go deliverate(user.ID, a, msg)
1581	}
1582}
1583
1584func honkworldwide(user *WhatAbout, honk *Honk) {
1585	jonk, _ := jonkjonk(user, honk)
1586	jonk["@context"] = itiswhatitis
1587	msg := jonk.ToBytes()
1588
1589	rcpts := boxuprcpts(user, honk.Audience, honk.Public)
1590
1591	if honk.Public {
1592		for _, h := range getdubs(user.ID) {
1593			if h.XID == user.URL {
1594				continue
1595			}
1596			box, ok := boxofboxes.Get(h.XID)
1597			if ok && box.Shared != "" {
1598				rcpts["%"+box.Shared] = true
1599			} else {
1600				rcpts[h.XID] = true
1601			}
1602		}
1603		for _, f := range getbacktracks(honk.XID) {
1604			if f[0] == '%' {
1605				rcpts[f] = true
1606			} else {
1607				box, ok := boxofboxes.Get(f)
1608				if ok && box.Shared != "" {
1609					rcpts["%"+box.Shared] = true
1610				} else {
1611					rcpts[f] = true
1612				}
1613			}
1614		}
1615	}
1616	for a := range rcpts {
1617		go deliverate(user.ID, a, msg)
1618	}
1619	if honk.Public && len(honk.Onts) > 0 {
1620		collectiveaction(honk)
1621	}
1622}
1623
1624func collectiveaction(honk *Honk) {
1625	user := getserveruser()
1626	for _, ont := range honk.Onts {
1627		dubs := getnameddubs(readyLuserOne, ont)
1628		if len(dubs) == 0 {
1629			continue
1630		}
1631		j := junk.New()
1632		j["@context"] = itiswhatitis
1633		j["type"] = "Add"
1634		j["id"] = user.URL + "/add/" + shortxid(ont+honk.XID)
1635		j["actor"] = user.URL
1636		j["object"] = honk.XID
1637		j["target"] = fmt.Sprintf("https://%s/o/%s", serverName, ont[1:])
1638		rcpts := make(map[string]bool)
1639		for _, dub := range dubs {
1640			box, ok := boxofboxes.Get(dub.XID)
1641			if ok && box.Shared != "" {
1642				rcpts["%"+box.Shared] = true
1643			} else {
1644				rcpts[dub.XID] = true
1645			}
1646		}
1647		msg := j.ToBytes()
1648		for a := range rcpts {
1649			go deliverate(user.ID, a, msg)
1650		}
1651	}
1652}
1653
1654func junkuser(user *WhatAbout) junk.Junk {
1655	j := junk.New()
1656	j["@context"] = itiswhatitis
1657	j["id"] = user.URL
1658	j["inbox"] = user.URL + "/inbox"
1659	j["outbox"] = user.URL + "/outbox"
1660	j["name"] = user.Display
1661	j["preferredUsername"] = user.Name
1662	j["summary"] = user.HTAbout
1663	var tags []junk.Junk
1664	for _, o := range user.Onts {
1665		t := junk.New()
1666		t["type"] = "Hashtag"
1667		o = strings.ToLower(o)
1668		t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1669		t["name"] = o
1670		tags = append(tags, t)
1671	}
1672	if len(tags) > 0 {
1673		j["tag"] = tags
1674	}
1675
1676	if user.ID > 0 {
1677		j["type"] = "Person"
1678		j["url"] = user.URL
1679		j["followers"] = user.URL + "/followers"
1680		j["following"] = user.URL + "/following"
1681		a := junk.New()
1682		a["type"] = "Image"
1683		a["mediaType"] = "image/png"
1684		a["url"] = avatarURL(user)
1685		j["icon"] = a
1686		if ban := user.Options.Banner; ban != "" {
1687			a := junk.New()
1688			a["type"] = "Image"
1689			a["mediaType"] = "image/jpg"
1690			a["url"] = ban
1691			j["image"] = a
1692		}
1693	} else {
1694		j["type"] = "Service"
1695	}
1696	k := junk.New()
1697	k["id"] = user.URL + "#key"
1698	k["owner"] = user.URL
1699	k["publicKeyPem"] = user.Key
1700	j["publicKey"] = k
1701	j[chatKeyProp] = user.Options.ChatPubKey
1702
1703	return j
1704}
1705
1706var oldjonkers = gencache.New(gencache.Options[string, []byte]{Fill: func(name string) ([]byte, bool) {
1707	user, err := butwhatabout(name)
1708	if err != nil {
1709		return nil, false
1710	}
1711	j := junkuser(user)
1712	return j.ToBytes(), true
1713}, Duration: 1 * time.Minute})
1714
1715func asjonker(name string) ([]byte, bool) {
1716	j, ok := oldjonkers.Get(name)
1717	return j, ok
1718}
1719
1720var handfull = gencache.New(gencache.Options[string, string]{Fill: func(name string) (string, bool) {
1721	m := strings.Split(name, "@")
1722	if len(m) != 2 {
1723		dlog.Printf("bad fish name: %s", name)
1724		return "", true
1725	}
1726	var href string
1727	row := stmtGetXonker.QueryRow(name, "fishname")
1728	err := row.Scan(&href)
1729	if err == nil {
1730		return href, true
1731	}
1732	dlog.Printf("fishing for %s", name)
1733	j, err := GetJunkFast(readyLuserOne, fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1734	if err != nil {
1735		ilog.Printf("failed to go fish %s: %s", name, err)
1736		return "", true
1737	}
1738	links, _ := j.GetArray("links")
1739	for _, li := range links {
1740		l, ok := li.(junk.Junk)
1741		if !ok {
1742			continue
1743		}
1744		href, _ := l.GetString("href")
1745		rel, _ := l.GetString("rel")
1746		t, _ := l.GetString("type")
1747		if rel == "self" && friendorfoe(t) {
1748			when := time.Now().UTC().Format(dbtimeformat)
1749			_, err := stmtSaveXonker.Exec(name, href, "fishname", when)
1750			if err != nil {
1751				elog.Printf("error saving fishname: %s", err)
1752			}
1753			return href, true
1754		}
1755	}
1756	return href, true
1757}, Duration: 1 * time.Minute})
1758
1759func gofish(name string) string {
1760	if name[0] == '@' {
1761		name = name[1:]
1762	}
1763	href, _ := handfull.Get(name)
1764	return href
1765}
1766
1767func investigate(name string) (*SomeThing, error) {
1768	if name == "" {
1769		return nil, fmt.Errorf("no name")
1770	}
1771	if name[0] == '@' {
1772		name = gofish(name)
1773	}
1774	if name == "" {
1775		return nil, fmt.Errorf("no name")
1776	}
1777	obj, err := GetJunkFast(readyLuserOne, name)
1778	if err != nil {
1779		return nil, err
1780	}
1781	allinjest(originate(name), obj)
1782	return somethingabout(obj)
1783}
1784
1785func somethingabout(obj junk.Junk) (*SomeThing, error) {
1786	info := new(SomeThing)
1787	t, _ := obj.GetString("type")
1788	isowned := false
1789	switch t {
1790	case "Person":
1791		fallthrough
1792	case "Group":
1793		fallthrough
1794	case "Organization":
1795		fallthrough
1796	case "Application":
1797		fallthrough
1798	case "Service":
1799		info.What = SomeActor
1800	case "OrderedCollection":
1801		isowned = true
1802		fallthrough
1803	case "Collection":
1804		info.What = SomeCollection
1805	default:
1806		return nil, fmt.Errorf("unknown object type")
1807	}
1808	info.XID, _ = obj.GetString("id")
1809	info.Name, _ = obj.GetString("preferredUsername")
1810	if info.Name == "" {
1811		info.Name, _ = obj.GetString("name")
1812	}
1813	if isowned {
1814		info.Owner, _ = obj.GetString("attributedTo")
1815	}
1816	if info.Owner == "" {
1817		info.Owner = info.XID
1818	}
1819	return info, nil
1820}
1821
1822func allinjest(origin string, obj junk.Junk) {
1823	ident, _ := obj.GetString("id")
1824	if ident == "" {
1825		return
1826	}
1827	if originate(ident) != origin {
1828		return
1829	}
1830	keyobj, ok := obj.GetMap("publicKey")
1831	if ok {
1832		ingestpubkey(origin, keyobj)
1833	}
1834	ingestboxes(origin, obj)
1835	ingesthandle(origin, obj)
1836	chatkey, ok := obj.GetString(chatKeyProp)
1837	if ok {
1838		when := time.Now().UTC().Format(dbtimeformat)
1839		_, err := stmtSaveXonker.Exec(ident, chatkey, chatKeyProp, when)
1840		if err != nil {
1841			elog.Printf("error saving chatkey: %s", err)
1842		}
1843	}
1844}
1845
1846func ingestpubkey(origin string, obj junk.Junk) {
1847	keyobj, ok := obj.GetMap("publicKey")
1848	if ok {
1849		obj = keyobj
1850	}
1851	keyname, ok := obj.GetString("id")
1852	var data string
1853	row := stmtGetXonker.QueryRow(keyname, "pubkey")
1854	err := row.Scan(&data)
1855	if err == nil {
1856		return
1857	}
1858	if !ok || origin != originate(keyname) {
1859		ilog.Printf("bad key origin %s <> %s", origin, keyname)
1860		return
1861	}
1862	dlog.Printf("ingesting a needed pubkey: %s", keyname)
1863	owner, ok := obj.GetString("owner")
1864	if !ok {
1865		ilog.Printf("error finding %s pubkey owner", keyname)
1866		return
1867	}
1868	data, ok = obj.GetString("publicKeyPem")
1869	if !ok {
1870		ilog.Printf("error finding %s pubkey", keyname)
1871		return
1872	}
1873	if originate(owner) != origin {
1874		ilog.Printf("bad key owner: %s <> %s", owner, origin)
1875		return
1876	}
1877	_, _, err = httpsig.DecodeKey(data)
1878	if err != nil {
1879		ilog.Printf("error decoding %s pubkey: %s", keyname, err)
1880		return
1881	}
1882	when := time.Now().UTC().Format(dbtimeformat)
1883	_, err = stmtSaveXonker.Exec(keyname, data, "pubkey", when)
1884	if err != nil {
1885		elog.Printf("error saving key: %s", err)
1886	}
1887}
1888
1889func ingestboxes(origin string, obj junk.Junk) {
1890	ident, _ := obj.GetString("id")
1891	if ident == "" {
1892		return
1893	}
1894	if originate(ident) != origin {
1895		return
1896	}
1897	var info string
1898	row := stmtGetXonker.QueryRow(ident, "boxes")
1899	err := row.Scan(&info)
1900	if err == nil {
1901		return
1902	}
1903	dlog.Printf("ingesting boxes: %s", ident)
1904	inbox, _ := obj.GetString("inbox")
1905	outbox, _ := obj.GetString("outbox")
1906	sbox, _ := obj.GetString("endpoints", "sharedInbox")
1907	if inbox != "" {
1908		when := time.Now().UTC().Format(dbtimeformat)
1909		m := strings.Join([]string{inbox, outbox, sbox}, " ")
1910		_, err = stmtSaveXonker.Exec(ident, m, "boxes", when)
1911		if err != nil {
1912			elog.Printf("error saving boxes: %s", err)
1913		}
1914	}
1915}
1916
1917func ingesthandle(origin string, obj junk.Junk) {
1918	xid, _ := obj.GetString("id")
1919	if xid == "" {
1920		return
1921	}
1922	if originate(xid) != origin {
1923		return
1924	}
1925	var handle string
1926	row := stmtGetXonker.QueryRow(xid, "handle")
1927	err := row.Scan(&handle)
1928	if err == nil {
1929		return
1930	}
1931	handle, _ = obj.GetString("preferredUsername")
1932	if handle != "" {
1933		when := time.Now().UTC().Format(dbtimeformat)
1934		_, err = stmtSaveXonker.Exec(xid, handle, "handle", when)
1935		if err != nil {
1936			elog.Printf("error saving handle: %s", err)
1937		}
1938	}
1939}
1940
1941func updateMe(username string) {
1942	user, _ := somenamedusers.Get(username)
1943	dt := time.Now().UTC().Format(time.RFC3339)
1944	j := junk.New()
1945	j["@context"] = itiswhatitis
1946	j["id"] = fmt.Sprintf("%s/upme/%s/%d", user.URL, user.Name, time.Now().Unix())
1947	j["actor"] = user.URL
1948	j["published"] = dt
1949	j["to"] = thewholeworld
1950	j["type"] = "Update"
1951	j["object"] = junkuser(user)
1952
1953	msg := j.ToBytes()
1954
1955	rcpts := make(map[string]bool)
1956	for _, f := range getdubs(user.ID) {
1957		if f.XID == user.URL {
1958			continue
1959		}
1960		box, ok := boxofboxes.Get(f.XID)
1961		if ok && box.Shared != "" {
1962			rcpts["%"+box.Shared] = true
1963		} else {
1964			rcpts[f.XID] = true
1965		}
1966	}
1967	for a := range rcpts {
1968		go deliverate(user.ID, a, msg)
1969	}
1970}
1971
1972func followme(user *WhatAbout, who string, name string, j junk.Junk) {
1973	folxid, _ := j.GetString("id")
1974
1975	ilog.Printf("updating honker follow: %s %s", who, folxid)
1976
1977	var x string
1978	db := opendatabase()
1979	row := db.QueryRow("select xid from honkers where name = ? and xid = ? and userid = ? and flavor in ('dub', 'undub')", name, who, user.ID)
1980	err := row.Scan(&x)
1981	if err != sql.ErrNoRows {
1982		ilog.Printf("duplicate follow request: %s", who)
1983		_, err = stmtUpdateFlavor.Exec("dub", folxid, user.ID, name, who, "undub")
1984		if err != nil {
1985			elog.Printf("error updating honker: %s", err)
1986		}
1987	} else {
1988		stmtSaveDub.Exec(user.ID, name, who, "dub", folxid)
1989	}
1990	go rubadubdub(user, j)
1991}
1992
1993func unfollowme(user *WhatAbout, who string, name string, j junk.Junk) {
1994	var folxid string
1995	if who == "" {
1996		folxid, _ = j.GetString("object")
1997
1998		db := opendatabase()
1999		row := db.QueryRow("select xid, name from honkers where userid = ? and folxid = ? and flavor in ('dub', 'undub')", user.ID, folxid)
2000		err := row.Scan(&who, &name)
2001		if err != nil {
2002			if err != sql.ErrNoRows {
2003				elog.Printf("error scanning honker: %s", err)
2004			}
2005			return
2006		}
2007	}
2008
2009	ilog.Printf("updating honker undo: %s %s", who, folxid)
2010	_, err := stmtUpdateFlavor.Exec("undub", folxid, user.ID, name, who, "dub")
2011	if err != nil {
2012		elog.Printf("error updating honker: %s", err)
2013		return
2014	}
2015}
2016
2017func followyou(user *WhatAbout, honkerid int64, sync bool) {
2018	var url, owner string
2019	db := opendatabase()
2020	row := db.QueryRow("select xid, owner from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')",
2021		honkerid, user.ID)
2022	err := row.Scan(&url, &owner)
2023	if err != nil {
2024		elog.Printf("can't get honker xid: %s", err)
2025		return
2026	}
2027	folxid := xfiltrate()
2028	ilog.Printf("subscribing to %s", url)
2029	_, err = db.Exec("update honkers set flavor = ?, folxid = ? where honkerid = ?", "presub", folxid, honkerid)
2030	if err != nil {
2031		elog.Printf("error updating honker: %s", err)
2032		return
2033	}
2034	if sync {
2035		subsub(user, url, owner, folxid)
2036	} else {
2037		go subsub(user, url, owner, folxid)
2038	}
2039
2040}
2041func unfollowyou(user *WhatAbout, honkerid int64, sync bool) {
2042	db := opendatabase()
2043	row := db.QueryRow("select xid, owner, folxid, flavor from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')",
2044		honkerid, user.ID)
2045	var url, owner, folxid, flavor string
2046	err := row.Scan(&url, &owner, &folxid, &flavor)
2047	if err != nil {
2048		elog.Printf("can't get honker xid: %s", err)
2049		return
2050	}
2051	if flavor == "peep" {
2052		return
2053	}
2054	ilog.Printf("unsubscribing from %s", url)
2055	_, err = db.Exec("update honkers set flavor = ? where honkerid = ?", "unsub", honkerid)
2056	if err != nil {
2057		elog.Printf("error updating honker: %s", err)
2058		return
2059	}
2060	if sync {
2061		itakeitallback(user, url, owner, folxid)
2062	} else {
2063		go itakeitallback(user, url, owner, folxid)
2064	}
2065}
2066
2067func followyou2(user *WhatAbout, j junk.Junk) {
2068	who, _ := j.GetString("actor")
2069
2070	ilog.Printf("updating honker accept: %s", who)
2071	db := opendatabase()
2072	row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')",
2073		user.ID, who)
2074	var name, folxid string
2075	err := row.Scan(&name, &folxid)
2076	if err != nil {
2077		elog.Printf("can't get honker name: %s", err)
2078		return
2079	}
2080	_, err = stmtUpdateFlavor.Exec("sub", folxid, user.ID, name, who, "presub")
2081	if err != nil {
2082		elog.Printf("error updating honker: %s", err)
2083		return
2084	}
2085}
2086
2087func nofollowyou2(user *WhatAbout, j junk.Junk) {
2088	who, _ := j.GetString("actor")
2089
2090	ilog.Printf("updating honker reject: %s", who)
2091	db := opendatabase()
2092	row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')",
2093		user.ID, who)
2094	var name, folxid string
2095	err := row.Scan(&name, &folxid)
2096	if err != nil {
2097		elog.Printf("can't get honker name: %s", err)
2098		return
2099	}
2100	_, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "presub")
2101	_, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "sub")
2102	if err != nil {
2103		elog.Printf("error updating honker: %s", err)
2104		return
2105	}
2106}