all repos — honk @ cd9c23bb088fd58514101a83eb9144d79cc0f7b5

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		mlinks = oneofakind(mlinks)
 536		for _, m := range mlinks {
 537			tryit := false
 538			m = m[1 : len(m)-1]
 539			if re_mast0link.MatchString(m) || re_misslink.MatchString(m) ||
 540				re_honklink.MatchString(m) || re_r0malink.MatchString(m) ||
 541				re_roma1ink.MatchString(m) {
 542				tryit = true
 543			} else if re_masto1ink.MatchString(m) {
 544				tryit = true
 545			}
 546			if tryit {
 547				var prefix string
 548				if m == qurl {
 549					prefix += fmt.Sprintf("<p><a href=\"%s\">%s</a>", m, m)
 550				}
 551				var final string
 552				if x := getxonk(user.ID, m); x != nil {
 553					content = fmt.Sprintf("%s%s<blockquote>%s</blockquote>", content, prefix, x.Noise)
 554				} else if j, err := GetJunkTimeout(user.ID, m, fastTimeout*time.Second, &final); err == nil {
 555					q, ok := j.GetString("content")
 556					if ok {
 557						content = fmt.Sprintf("%s%s<blockquote>%s</blockquote>", content, prefix, q)
 558					}
 559					prevdepth := depth
 560					depth = maxdepth
 561					xonkxonkfn(j, originate(final), false, "")
 562					depth = prevdepth
 563				}
 564			}
 565		}
 566		return content
 567	}
 568
 569	saveonemore := func(xid string) {
 570		dlog.Printf("getting onemore: %s", xid)
 571		if depth >= maxdepth {
 572			ilog.Printf("in too deep")
 573			return
 574		}
 575		obj, err := GetJunkHardMode(user.ID, xid)
 576		if err != nil {
 577			ilog.Printf("error getting onemore: %s: %s", xid, err)
 578			return
 579		}
 580		xonkxonkfn(obj, originate(xid), false, "")
 581	}
 582
 583	xonkxonkfn = func(item junk.Junk, origin string, isUpdate bool, bonker string) *Honk {
 584		id, _ := item.GetString("id")
 585		what := firstofmany(item, "type")
 586		dt, ok := item.GetString("published")
 587		if !ok {
 588			dt = time.Now().Format(time.RFC3339)
 589		}
 590		if depth >= maxdepth+5 {
 591			ilog.Printf("went too deep in xonkxonk")
 592			return nil
 593		}
 594		depth++
 595		defer func() { depth-- }()
 596
 597		var err error
 598		var xid, rid, url, convoy string
 599		var replies []string
 600		var obj junk.Junk
 601		waspage := false
 602		preferorig := false
 603		switch what {
 604		case "Delete":
 605			obj, ok = item.GetMap("object")
 606			if ok {
 607				xid, _ = obj.GetString("id")
 608			} else {
 609				xid, _ = item.GetString("object")
 610			}
 611			if xid == "" {
 612				return nil
 613			}
 614			if originate(xid) != origin {
 615				ilog.Printf("forged delete: %s", xid)
 616				return nil
 617			}
 618			ilog.Printf("eradicating %s", xid)
 619			eradicatexonk(user.ID, xid)
 620			return nil
 621		case "Remove":
 622			xid, _ = item.GetString("object")
 623			targ, _ := obj.GetString("target")
 624			ilog.Printf("remove %s from %s", obj, targ)
 625			return nil
 626		case "Tombstone":
 627			xid, _ = item.GetString("id")
 628			if xid == "" {
 629				return nil
 630			}
 631			if originate(xid) != origin {
 632				ilog.Printf("forged delete: %s", xid)
 633				return nil
 634			}
 635			ilog.Printf("eradicating %s", xid)
 636			eradicatexonk(user.ID, xid)
 637			return nil
 638		case "Announce":
 639			obj, ok = item.GetMap("object")
 640			if ok {
 641				// peek ahead some
 642				what := firstofmany(obj, "type")
 643				if what == "Create" || what == "Update" {
 644					if what == "Update" {
 645						isUpdate = true
 646					}
 647					inner, ok := obj.GetMap("object")
 648					if ok {
 649						obj = inner
 650					} else {
 651						xid, _ = obj.GetString("object")
 652					}
 653				}
 654				if xid == "" {
 655					xid, _ = obj.GetString("id")
 656				}
 657			} else {
 658				xid, _ = item.GetString("object")
 659			}
 660			if !isUpdate && !needbonkid(user, xid) {
 661				return nil
 662			}
 663			bonker, _ = item.GetString("actor")
 664			origin = originate(xid)
 665			if ok && originate(id) == origin {
 666				dlog.Printf("using object in announce for %s", xid)
 667			} else {
 668				dlog.Printf("getting bonk: %s", xid)
 669				obj, err = GetJunkHardMode(user.ID, xid)
 670				if err != nil {
 671					ilog.Printf("error getting bonk: %s: %s", xid, err)
 672					return nil
 673				}
 674			}
 675			return xonkxonkfn(obj, origin, isUpdate, bonker)
 676		case "Update":
 677			isUpdate = true
 678			fallthrough
 679		case "Create":
 680			obj, ok = item.GetMap("object")
 681			if !ok {
 682				xid, _ = item.GetString("object")
 683				dlog.Printf("getting created honk: %s", xid)
 684				if originate(xid) != origin {
 685					ilog.Printf("out of bounds %s not from %s", xid, origin)
 686					return nil
 687				}
 688				obj, err = GetJunkHardMode(user.ID, xid)
 689				if err != nil {
 690					ilog.Printf("error getting creation: %s", err)
 691				}
 692			}
 693			if obj == nil {
 694				ilog.Printf("no object for creation %s", id)
 695				return nil
 696			}
 697			return xonkxonkfn(obj, origin, isUpdate, bonker)
 698		case "Read":
 699			xid, ok = item.GetString("object")
 700			if ok {
 701				if !needxonkid(user, xid) {
 702					dlog.Printf("don't need read obj: %s", xid)
 703					return nil
 704				}
 705				obj, err = GetJunkHardMode(user.ID, xid)
 706				if err != nil {
 707					ilog.Printf("error getting read: %s", err)
 708					return nil
 709				}
 710				return xonkxonkfn(obj, originate(xid), false, "")
 711			}
 712			return nil
 713		case "Add":
 714			xid, ok = item.GetString("object")
 715			if ok {
 716				// check target...
 717				if !needxonkid(user, xid) {
 718					dlog.Printf("don't need added obj: %s", xid)
 719					return nil
 720				}
 721				obj, err = GetJunkHardMode(user.ID, xid)
 722				if err != nil {
 723					ilog.Printf("error getting add: %s", err)
 724					return nil
 725				}
 726				return xonkxonkfn(obj, originate(xid), false, "")
 727			}
 728			return nil
 729		case "Move":
 730			obj = item
 731			what = "move"
 732		case "Page":
 733			waspage = true
 734			fallthrough
 735		case "Audio":
 736			fallthrough
 737		case "Image":
 738			if what == "Image" {
 739				preferorig = true
 740			}
 741			fallthrough
 742		case "Video":
 743			fallthrough
 744		case "Question":
 745			fallthrough
 746		case "Note":
 747			fallthrough
 748		case "Article":
 749			obj = item
 750			what = "honk"
 751		case "Event":
 752			obj = item
 753			what = "event"
 754		case "ChatMessage":
 755			bonker = ""
 756			obj = item
 757			what = "chonk"
 758		default:
 759			ilog.Printf("unknown activity: %s", what)
 760			dumpactivity(item)
 761			return nil
 762		}
 763		if bonker != "" {
 764			what = "bonk"
 765		}
 766
 767		if obj != nil {
 768			xid, _ = obj.GetString("id")
 769		}
 770
 771		if xid == "" {
 772			ilog.Printf("don't know what xid is")
 773			item.Write(ilog.Writer())
 774			return nil
 775		}
 776		if originate(xid) != origin {
 777			if !develMode && origin != "" {
 778				ilog.Printf("original sin: %s not from %s", xid, origin)
 779				item.Write(ilog.Writer())
 780				return nil
 781			}
 782		}
 783
 784		var xonk Honk
 785		// early init
 786		xonk.XID = xid
 787		xonk.UserID = user.ID
 788		xonk.Honker, _ = item.GetString("actor")
 789		if xonk.Honker == "" {
 790			xonk.Honker, _ = item.GetString("attributedTo")
 791		}
 792		if obj != nil {
 793			if xonk.Honker == "" {
 794				xonk.Honker = extractattrto(obj)
 795			}
 796			if bonker != "" {
 797				xonk.Honker, xonk.Oonker = bonker, xonk.Honker
 798			}
 799			if xonk.Oonker == xonk.Honker {
 800				xonk.Oonker = ""
 801			}
 802			xonk.Audience = newphone(nil, obj)
 803		}
 804		xonk.Audience = append(xonk.Audience, xonk.Honker)
 805		xonk.Audience = oneofakind(xonk.Audience)
 806		for i, a := range xonk.Audience {
 807			if a == tinyworld {
 808				xonk.Audience[i] = thewholeworld
 809			}
 810		}
 811		xonk.Public = loudandproud(xonk.Audience)
 812
 813		var mentions []Mention
 814		if obj != nil {
 815			ot := firstofmany(obj, "type")
 816			url, _ = obj.GetString("url")
 817			if dt2, ok := obj.GetString("published"); ok {
 818				dt = dt2
 819			}
 820			content, _ := obj.GetString("content")
 821			if !strings.HasPrefix(content, "<p>") {
 822				content = "<p>" + content
 823			}
 824			precis, _ := obj.GetString("summary")
 825			if name, ok := obj.GetString("name"); ok {
 826				if precis != "" {
 827					content = precis + "<p>" + content
 828				}
 829				precis = html.EscapeString(name)
 830			}
 831			if sens, _ := obj["sensitive"].(bool); sens && precis == "" {
 832				precis = "unspecified horror"
 833			}
 834			if waspage {
 835				content += fmt.Sprintf(`<p><a href="%s">%s</a>`, url, url)
 836				url = xid
 837			}
 838			if user.Options.InlineQuotes {
 839				qurl, _ := obj.GetString("quoteUrl")
 840				content = qutify(user, qurl, content)
 841			}
 842			rid, ok = obj.GetString("inReplyTo")
 843			if !ok {
 844				if robj, ok := obj.GetMap("inReplyTo"); ok {
 845					rid, _ = robj.GetString("id")
 846				}
 847			}
 848			convoy, _ = obj.GetString("context")
 849			if convoy == "" {
 850				convoy, _ = obj.GetString("conversation")
 851			}
 852			if ot == "Question" {
 853				if what == "honk" {
 854					what = "qonk"
 855				}
 856				content += "<ul>"
 857				ans, _ := obj.GetArray("oneOf")
 858				for _, ai := range ans {
 859					a, ok := ai.(junk.Junk)
 860					if !ok {
 861						continue
 862					}
 863					as, _ := a.GetString("name")
 864					content += "<li>" + as
 865				}
 866				ans, _ = obj.GetArray("anyOf")
 867				for _, ai := range ans {
 868					a, ok := ai.(junk.Junk)
 869					if !ok {
 870						continue
 871					}
 872					as, _ := a.GetString("name")
 873					content += "<li>" + as
 874				}
 875				content += "</ul>"
 876			}
 877			if ot == "Move" {
 878				targ, _ := obj.GetString("target")
 879				content += string(templates.Sprintf(`<p>Moved to <a href="%s">%s</a>`, targ, targ))
 880			}
 881			if len(content) > 90001 {
 882				ilog.Printf("content too long. truncating")
 883				content = content[:90001]
 884			}
 885
 886			xonk.Noise = content
 887			xonk.Precis = precis
 888			if rejectxonk(&xonk) {
 889				dlog.Printf("fast reject: %s", xid)
 890				return nil
 891			}
 892
 893			numatts := 0
 894			procatt := func(att junk.Junk) {
 895				at, _ := att.GetString("type")
 896				mt, _ := att.GetString("mediaType")
 897				if mt == "" {
 898					mt = "image"
 899				}
 900				u, ok := att.GetString("url")
 901				if !ok {
 902					u, ok = att.GetString("href")
 903				}
 904				if !ok {
 905					if ua, ok := att.GetArray("url"); ok && len(ua) > 0 {
 906						u, ok = ua[0].(string)
 907						if !ok {
 908							if uu, ok := ua[0].(junk.Junk); ok {
 909								u, _ = uu.GetString("href")
 910								if mt == "" {
 911									mt, _ = uu.GetString("mediaType")
 912								}
 913							}
 914						}
 915					} else if uu, ok := att.GetMap("url"); ok {
 916						u, _ = uu.GetString("href")
 917						if mt == "" {
 918							mt, _ = uu.GetString("mediaType")
 919						}
 920					}
 921				}
 922				name, _ := att.GetString("name")
 923				desc, _ := att.GetString("summary")
 924				desc = html.UnescapeString(desc)
 925				if desc == "" {
 926					desc = name
 927				}
 928				localize := false
 929				if at == "Document" || at == "Image" {
 930					mt = strings.ToLower(mt)
 931					dlog.Printf("attachment: %s %s", mt, u)
 932					if mt == "text/plain" || mt == "application/pdf" ||
 933						strings.HasPrefix(mt, "image") {
 934						if numatts > 4 {
 935							ilog.Printf("excessive attachment: %s", at)
 936						} else {
 937							localize = true
 938						}
 939					}
 940				} else if at == "Link" {
 941					if waspage {
 942						xonk.Noise += fmt.Sprintf(`<p><a href="%s">%s</a>`, u, u)
 943						return
 944					}
 945					if name == "" {
 946						name = u
 947					}
 948				} else {
 949					ilog.Printf("unknown attachment: %s", at)
 950				}
 951				if skipMedia(&xonk) {
 952					localize = false
 953				}
 954				if preferorig && !localize {
 955					return
 956				}
 957				donk := savedonk(u, name, desc, mt, localize)
 958				if donk != nil {
 959					xonk.Donks = append(xonk.Donks, donk)
 960				}
 961				numatts++
 962			}
 963			if img, ok := obj.GetMap("image"); ok {
 964				procatt(img)
 965			}
 966			if preferorig {
 967				atts, _ := obj.GetArray("url")
 968				for _, atti := range atts {
 969					att, ok := atti.(junk.Junk)
 970					if !ok {
 971						ilog.Printf("attachment that wasn't map?")
 972						continue
 973					}
 974					procatt(att)
 975				}
 976				if numatts == 0 {
 977					preferorig = false
 978				}
 979			}
 980			if !preferorig {
 981				atts := oneforall(obj, "attachment")
 982				for _, atti := range atts {
 983					att, ok := atti.(junk.Junk)
 984					if !ok {
 985						ilog.Printf("attachment that wasn't map?")
 986						continue
 987					}
 988					procatt(att)
 989				}
 990			}
 991			proctag := func(tag junk.Junk) {
 992				tt, _ := tag.GetString("type")
 993				name, _ := tag.GetString("name")
 994				desc, _ := tag.GetString("summary")
 995				desc = html.UnescapeString(desc)
 996				if desc == "" {
 997					desc = name
 998				}
 999				if tt == "Emoji" {
1000					icon, _ := tag.GetMap("icon")
1001					mt, _ := icon.GetString("mediaType")
1002					if mt == "" {
1003						mt = "image/png"
1004					}
1005					u, _ := icon.GetString("url")
1006					donk := savedonk(u, name, desc, mt, true)
1007					if donk != nil {
1008						xonk.Donks = append(xonk.Donks, donk)
1009					}
1010				}
1011				if tt == "Hashtag" {
1012					if name == "" || name == "#" {
1013						// skip it
1014					} else {
1015						if name[0] != '#' {
1016							name = "#" + name
1017						}
1018						xonk.Onts = append(xonk.Onts, name)
1019					}
1020				}
1021				if tt == "Place" {
1022					p := new(Place)
1023					p.Name = name
1024					p.Latitude, _ = tag.GetNumber("latitude")
1025					p.Longitude, _ = tag.GetNumber("longitude")
1026					p.Url, _ = tag.GetString("url")
1027					xonk.Place = p
1028				}
1029				if tt == "Mention" {
1030					var m Mention
1031					m.Who, _ = tag.GetString("name")
1032					m.Where, _ = tag.GetString("href")
1033					mentions = append(mentions, m)
1034				}
1035			}
1036			tags := oneforall(obj, "tag")
1037			for _, tagi := range tags {
1038				tag, ok := tagi.(junk.Junk)
1039				if !ok {
1040					continue
1041				}
1042				proctag(tag)
1043			}
1044			if starttime, ok := obj.GetString("startTime"); ok {
1045				if start, err := time.Parse(time.RFC3339, starttime); err == nil {
1046					t := new(Time)
1047					t.StartTime = start
1048					endtime, _ := obj.GetString("endTime")
1049					t.EndTime, _ = time.Parse(time.RFC3339, endtime)
1050					dura, _ := obj.GetString("duration")
1051					if strings.HasPrefix(dura, "PT") {
1052						dura = strings.ToLower(dura[2:])
1053						d, _ := time.ParseDuration(dura)
1054						t.Duration = Duration(d)
1055					}
1056					xonk.Time = t
1057				}
1058			}
1059			if loca, ok := obj.GetMap("location"); ok {
1060				if tt, _ := loca.GetString("type"); tt == "Place" {
1061					p := new(Place)
1062					p.Name, _ = loca.GetString("name")
1063					p.Latitude, _ = loca.GetNumber("latitude")
1064					p.Longitude, _ = loca.GetNumber("longitude")
1065					p.Url, _ = loca.GetString("url")
1066					xonk.Place = p
1067				}
1068			}
1069
1070			xonk.Onts = oneofakind(xonk.Onts)
1071			replyobj, ok := obj.GetMap("replies")
1072			if ok {
1073				items, ok := replyobj.GetArray("items")
1074				if !ok {
1075					first, ok := replyobj.GetMap("first")
1076					if ok {
1077						items, _ = first.GetArray("items")
1078					}
1079				}
1080				for _, repl := range items {
1081					s, ok := repl.(string)
1082					if ok {
1083						replies = append(replies, s)
1084					}
1085				}
1086			}
1087
1088		}
1089
1090		if currenttid == "" {
1091			currenttid = convoy
1092		}
1093
1094		// init xonk
1095		xonk.What = what
1096		xonk.RID = rid
1097		xonk.Date, _ = time.Parse(time.RFC3339, dt)
1098		xonk.URL = url
1099		xonk.Format = "html"
1100		xonk.Convoy = convoy
1101		xonk.Mentions = mentions
1102		for _, m := range mentions {
1103			if m.Where == user.URL {
1104				xonk.Whofore = 1
1105			}
1106		}
1107		imaginate(&xonk)
1108
1109		if what == "chonk" {
1110			target, _ := obj.GetString("to")
1111			if target == user.URL {
1112				target = xonk.Honker
1113			}
1114			ch := Chonk{
1115				UserID: xonk.UserID,
1116				XID:    xid,
1117				Who:    xonk.Honker,
1118				Target: target,
1119				Date:   xonk.Date,
1120				Noise:  xonk.Noise,
1121				Format: xonk.Format,
1122				Donks:  xonk.Donks,
1123			}
1124			savechonk(&ch)
1125			return nil
1126		}
1127
1128		if isUpdate {
1129			dlog.Printf("something has changed! %s", xonk.XID)
1130			prev := getxonk(user.ID, xonk.XID)
1131			if prev == nil {
1132				ilog.Printf("didn't find old version for update: %s", xonk.XID)
1133				isUpdate = false
1134			} else {
1135				xonk.ID = prev.ID
1136				updatehonk(&xonk)
1137			}
1138		}
1139		if !isUpdate && needxonk(user, &xonk) {
1140			if rid != "" && xonk.Public {
1141				if needxonkid(user, rid) {
1142					goingup++
1143					saveonemore(rid)
1144					goingup--
1145				}
1146				if convoy == "" {
1147					xx := getxonk(user.ID, rid)
1148					if xx != nil {
1149						convoy = xx.Convoy
1150					}
1151				}
1152			}
1153			if convoy == "" {
1154				convoy = currenttid
1155			}
1156			if convoy == "" {
1157				convoy = xonk.XID
1158				currenttid = convoy
1159			}
1160			xonk.Convoy = convoy
1161			savexonk(&xonk)
1162		}
1163		if goingup == 0 {
1164			for _, replid := range replies {
1165				if needxonkid(user, replid) {
1166					dlog.Printf("missing a reply: %s", replid)
1167					saveonemore(replid)
1168				}
1169			}
1170		}
1171		return &xonk
1172	}
1173
1174	return xonkxonkfn(item, origin, false, "")
1175}
1176
1177func dumpactivity(item junk.Junk) {
1178	fd, err := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
1179	if err != nil {
1180		elog.Printf("error opening inbox! %s", err)
1181		return
1182	}
1183	defer fd.Close()
1184	item.Write(fd)
1185	io.WriteString(fd, "\n")
1186}
1187
1188func rubadubdub(user *WhatAbout, req junk.Junk) {
1189	actor, _ := req.GetString("actor")
1190	j := junk.New()
1191	j["@context"] = itiswhatitis
1192	j["id"] = user.URL + "/dub/" + xfiltrate()
1193	j["type"] = "Accept"
1194	j["actor"] = user.URL
1195	j["to"] = actor
1196	j["published"] = time.Now().UTC().Format(time.RFC3339)
1197	j["object"] = req
1198
1199	deliverate(user.ID, actor, j.ToBytes())
1200}
1201
1202func itakeitallback(user *WhatAbout, xid string, owner string, folxid string) {
1203	j := junk.New()
1204	j["@context"] = itiswhatitis
1205	j["id"] = user.URL + "/unsub/" + folxid
1206	j["type"] = "Undo"
1207	j["actor"] = user.URL
1208	j["to"] = owner
1209	f := junk.New()
1210	f["id"] = user.URL + "/sub/" + folxid
1211	f["type"] = "Follow"
1212	f["actor"] = user.URL
1213	f["to"] = owner
1214	f["object"] = xid
1215	j["object"] = f
1216	j["published"] = time.Now().UTC().Format(time.RFC3339)
1217
1218	deliverate(user.ID, owner, j.ToBytes())
1219}
1220
1221func subsub(user *WhatAbout, xid string, owner string, folxid string) {
1222	if xid == "" {
1223		ilog.Printf("can't subscribe to empty")
1224		return
1225	}
1226	j := junk.New()
1227	j["@context"] = itiswhatitis
1228	j["id"] = user.URL + "/sub/" + folxid
1229	j["type"] = "Follow"
1230	j["actor"] = user.URL
1231	j["to"] = owner
1232	j["object"] = xid
1233	j["published"] = time.Now().UTC().Format(time.RFC3339)
1234
1235	deliverate(user.ID, owner, j.ToBytes())
1236}
1237
1238func activatedonks(donks []*Donk) []junk.Junk {
1239	var atts []junk.Junk
1240	for _, d := range donks {
1241		if re_emus.MatchString(d.Name) {
1242			continue
1243		}
1244		jd := junk.New()
1245		jd["mediaType"] = d.Media
1246		jd["name"] = d.Name
1247		jd["summary"] = html.EscapeString(d.Desc)
1248		jd["type"] = "Document"
1249		jd["url"] = d.URL
1250		atts = append(atts, jd)
1251	}
1252	return atts
1253}
1254
1255// returns activity, object
1256func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
1257	dt := h.Date.Format(time.RFC3339)
1258	var jo junk.Junk
1259	j := junk.New()
1260	j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
1261	j["actor"] = user.URL
1262	j["published"] = dt
1263	j["to"] = h.Audience[0]
1264	if len(h.Audience) > 1 {
1265		j["cc"] = h.Audience[1:]
1266	}
1267
1268	switch h.What {
1269	case "update":
1270		fallthrough
1271	case "event":
1272		fallthrough
1273	case "honk":
1274		j["type"] = "Create"
1275		jo = junk.New()
1276		jo["id"] = h.XID
1277		jo["type"] = "Note"
1278		if h.What == "event" {
1279			jo["type"] = "Event"
1280		}
1281		if h.What == "update" {
1282			j["type"] = "Update"
1283			jo["updated"] = dt
1284		}
1285		jo["published"] = dt
1286		jo["url"] = h.XID
1287		jo["attributedTo"] = user.URL
1288		if h.RID != "" {
1289			jo["inReplyTo"] = h.RID
1290		}
1291		if h.Convoy != "" {
1292			jo["context"] = h.Convoy
1293			jo["conversation"] = h.Convoy
1294		}
1295		jo["to"] = h.Audience[0]
1296		if len(h.Audience) > 1 {
1297			jo["cc"] = h.Audience[1:]
1298		}
1299		if !h.Public {
1300			jo["directMessage"] = true
1301		}
1302		h.Noise = re_retag.ReplaceAllString(h.Noise, "")
1303		translate(h)
1304		redoimages(h)
1305		if h.Precis != "" {
1306			jo["sensitive"] = true
1307		}
1308
1309		var replies []string
1310		for _, reply := range h.Replies {
1311			replies = append(replies, reply.XID)
1312		}
1313		if len(replies) > 0 {
1314			jr := junk.New()
1315			jr["type"] = "Collection"
1316			jr["totalItems"] = len(replies)
1317			jr["items"] = replies
1318			jo["replies"] = jr
1319		}
1320
1321		var tags []junk.Junk
1322		for _, m := range h.Mentions {
1323			t := junk.New()
1324			t["type"] = "Mention"
1325			t["name"] = m.Who
1326			t["href"] = m.Where
1327			tags = append(tags, t)
1328		}
1329		for _, o := range h.Onts {
1330			t := junk.New()
1331			t["type"] = "Hashtag"
1332			o = strings.ToLower(o)
1333			t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1334			t["name"] = o
1335			tags = append(tags, t)
1336		}
1337		for _, e := range herdofemus(h.Noise) {
1338			t := junk.New()
1339			t["id"] = e.ID
1340			t["type"] = "Emoji"
1341			t["name"] = e.Name
1342			i := junk.New()
1343			i["type"] = "Image"
1344			i["mediaType"] = e.Type
1345			i["url"] = e.ID
1346			t["icon"] = i
1347			tags = append(tags, t)
1348		}
1349		for _, e := range fixupflags(h) {
1350			t := junk.New()
1351			t["id"] = e.ID
1352			t["type"] = "Emoji"
1353			t["name"] = e.Name
1354			i := junk.New()
1355			i["type"] = "Image"
1356			i["mediaType"] = "image/png"
1357			i["url"] = e.ID
1358			t["icon"] = i
1359			tags = append(tags, t)
1360		}
1361		if len(tags) > 0 {
1362			jo["tag"] = tags
1363		}
1364		if p := h.Place; p != nil {
1365			t := junk.New()
1366			t["type"] = "Place"
1367			if p.Name != "" {
1368				t["name"] = p.Name
1369			}
1370			if p.Latitude != 0 {
1371				t["latitude"] = p.Latitude
1372			}
1373			if p.Longitude != 0 {
1374				t["longitude"] = p.Longitude
1375			}
1376			if p.Url != "" {
1377				t["url"] = p.Url
1378			}
1379			jo["location"] = t
1380		}
1381		if t := h.Time; t != nil {
1382			jo["startTime"] = t.StartTime.Format(time.RFC3339)
1383			if t.Duration != 0 {
1384				jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1385			}
1386		}
1387		atts := activatedonks(h.Donks)
1388		if len(atts) > 0 {
1389			jo["attachment"] = atts
1390		}
1391		jo["summary"] = h.Precis
1392		jo["content"] = h.Noise
1393		j["object"] = jo
1394	case "bonk":
1395		j["type"] = "Announce"
1396		if h.Convoy != "" {
1397			j["context"] = h.Convoy
1398		}
1399		j["object"] = h.XID
1400	case "unbonk":
1401		b := junk.New()
1402		b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1403		b["type"] = "Announce"
1404		b["actor"] = user.URL
1405		if h.Convoy != "" {
1406			b["context"] = h.Convoy
1407		}
1408		b["object"] = h.XID
1409		j["type"] = "Undo"
1410		j["object"] = b
1411	case "zonk":
1412		j["type"] = "Delete"
1413		j["object"] = h.XID
1414	case "ack":
1415		j["type"] = "Read"
1416		j["object"] = h.XID
1417		if h.Convoy != "" {
1418			j["context"] = h.Convoy
1419		}
1420	case "react":
1421		j["type"] = "EmojiReact"
1422		j["object"] = h.XID
1423		if h.Convoy != "" {
1424			j["context"] = h.Convoy
1425		}
1426		j["content"] = h.Noise
1427	case "deack":
1428		b := junk.New()
1429		b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1430		b["type"] = "Read"
1431		b["actor"] = user.URL
1432		b["object"] = h.XID
1433		if h.Convoy != "" {
1434			b["context"] = h.Convoy
1435		}
1436		j["type"] = "Undo"
1437		j["object"] = b
1438	}
1439
1440	return j, jo
1441}
1442
1443var oldjonks = gencache.New(gencache.Options[string, []byte]{Fill: func(xid string) ([]byte, bool) {
1444	row := stmtAnyXonk.QueryRow(xid)
1445	honk := scanhonk(row)
1446	if honk == nil || !honk.Public {
1447		return nil, true
1448	}
1449	user, _ := butwhatabout(honk.Username)
1450	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1451	reversehonks(rawhonks)
1452	for _, h := range rawhonks {
1453		if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
1454			honk.Replies = append(honk.Replies, h)
1455		}
1456	}
1457	donksforhonks([]*Honk{honk})
1458	_, j := jonkjonk(user, honk)
1459	if j == nil {
1460		elog.Fatalf("what just happened? %v", honk)
1461	}
1462	j["@context"] = itiswhatitis
1463
1464	return j.ToBytes(), true
1465}, Limit: 128})
1466
1467func gimmejonk(xid string) ([]byte, bool) {
1468	j, ok := oldjonks.Get(xid)
1469	return j, ok
1470}
1471
1472func boxuprcpts(user *WhatAbout, addresses []string, useshared bool) map[string]bool {
1473	rcpts := make(map[string]bool)
1474	for _, a := range addresses {
1475		if a == "" || a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1476			continue
1477		}
1478		if a[0] == '%' {
1479			rcpts[a] = true
1480			continue
1481		}
1482		box, ok := boxofboxes.Get(a)
1483		if ok && useshared && box.Shared != "" {
1484			rcpts["%"+box.Shared] = true
1485		} else {
1486			rcpts[a] = true
1487		}
1488	}
1489	return rcpts
1490}
1491
1492func chonkifymsg(user *WhatAbout, ch *Chonk) []byte {
1493	dt := ch.Date.Format(time.RFC3339)
1494	aud := []string{ch.Target}
1495
1496	jo := junk.New()
1497	jo["id"] = ch.XID
1498	jo["type"] = "ChatMessage"
1499	jo["published"] = dt
1500	jo["attributedTo"] = user.URL
1501	jo["to"] = aud
1502	jo["content"] = ch.HTML
1503	atts := activatedonks(ch.Donks)
1504	if len(atts) > 0 {
1505		jo["attachment"] = atts
1506	}
1507	var tags []junk.Junk
1508	for _, e := range herdofemus(ch.Noise) {
1509		t := junk.New()
1510		t["id"] = e.ID
1511		t["type"] = "Emoji"
1512		t["name"] = e.Name
1513		i := junk.New()
1514		i["type"] = "Image"
1515		i["mediaType"] = e.Type
1516		i["url"] = e.ID
1517		t["icon"] = i
1518		tags = append(tags, t)
1519	}
1520	if len(tags) > 0 {
1521		jo["tag"] = tags
1522	}
1523
1524	j := junk.New()
1525	j["@context"] = itiswhatitis
1526	j["id"] = user.URL + "/" + "honk" + "/" + shortxid(ch.XID)
1527	j["type"] = "Create"
1528	j["actor"] = user.URL
1529	j["published"] = dt
1530	j["to"] = aud
1531	j["object"] = jo
1532
1533	return j.ToBytes()
1534}
1535
1536func sendchonk(user *WhatAbout, ch *Chonk) {
1537	msg := chonkifymsg(user, ch)
1538
1539	rcpts := make(map[string]bool)
1540	rcpts[ch.Target] = true
1541	for a := range rcpts {
1542		go deliverate(user.ID, a, msg)
1543	}
1544}
1545
1546func honkworldwide(user *WhatAbout, honk *Honk) {
1547	jonk, _ := jonkjonk(user, honk)
1548	jonk["@context"] = itiswhatitis
1549	msg := jonk.ToBytes()
1550
1551	rcpts := boxuprcpts(user, honk.Audience, honk.Public)
1552
1553	if honk.Public {
1554		for _, h := range getdubs(user.ID) {
1555			if h.XID == user.URL {
1556				continue
1557			}
1558			box, ok := boxofboxes.Get(h.XID)
1559			if ok && box.Shared != "" {
1560				rcpts["%"+box.Shared] = true
1561			} else {
1562				rcpts[h.XID] = true
1563			}
1564		}
1565		for _, f := range getbacktracks(honk.XID) {
1566			if f[0] == '%' {
1567				rcpts[f] = true
1568			} else {
1569				box, ok := boxofboxes.Get(f)
1570				if ok && box.Shared != "" {
1571					rcpts["%"+box.Shared] = true
1572				} else {
1573					rcpts[f] = true
1574				}
1575			}
1576		}
1577	}
1578	for a := range rcpts {
1579		go deliverate(user.ID, a, msg)
1580	}
1581	if honk.Public && len(honk.Onts) > 0 {
1582		collectiveaction(honk)
1583	}
1584}
1585
1586func collectiveaction(honk *Honk) {
1587	user := getserveruser()
1588	for _, ont := range honk.Onts {
1589		dubs := getnameddubs(readyLuserOne, ont)
1590		if len(dubs) == 0 {
1591			continue
1592		}
1593		j := junk.New()
1594		j["@context"] = itiswhatitis
1595		j["type"] = "Add"
1596		j["id"] = user.URL + "/add/" + shortxid(ont+honk.XID)
1597		j["actor"] = user.URL
1598		j["object"] = honk.XID
1599		j["target"] = fmt.Sprintf("https://%s/o/%s", serverName, ont[1:])
1600		rcpts := make(map[string]bool)
1601		for _, dub := range dubs {
1602			box, ok := boxofboxes.Get(dub.XID)
1603			if ok && box.Shared != "" {
1604				rcpts["%"+box.Shared] = true
1605			} else {
1606				rcpts[dub.XID] = true
1607			}
1608		}
1609		msg := j.ToBytes()
1610		for a := range rcpts {
1611			go deliverate(user.ID, a, msg)
1612		}
1613	}
1614}
1615
1616func junkuser(user *WhatAbout) junk.Junk {
1617	j := junk.New()
1618	j["@context"] = itiswhatitis
1619	j["id"] = user.URL
1620	j["inbox"] = user.URL + "/inbox"
1621	j["outbox"] = user.URL + "/outbox"
1622	j["name"] = user.Display
1623	j["preferredUsername"] = user.Name
1624	j["summary"] = user.HTAbout
1625	var tags []junk.Junk
1626	for _, o := range user.Onts {
1627		t := junk.New()
1628		t["type"] = "Hashtag"
1629		o = strings.ToLower(o)
1630		t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1631		t["name"] = o
1632		tags = append(tags, t)
1633	}
1634	if len(tags) > 0 {
1635		j["tag"] = tags
1636	}
1637
1638	if user.ID > 0 {
1639		j["type"] = "Person"
1640		j["url"] = user.URL
1641		j["followers"] = user.URL + "/followers"
1642		j["following"] = user.URL + "/following"
1643		a := junk.New()
1644		a["type"] = "Image"
1645		a["mediaType"] = "image/png"
1646		a["url"] = avatarURL(user)
1647		j["icon"] = a
1648		if ban := user.Options.Banner; ban != "" {
1649			a := junk.New()
1650			a["type"] = "Image"
1651			a["mediaType"] = "image/jpg"
1652			a["url"] = ban
1653			j["image"] = a
1654		}
1655	} else {
1656		j["type"] = "Service"
1657	}
1658	k := junk.New()
1659	k["id"] = user.URL + "#key"
1660	k["owner"] = user.URL
1661	k["publicKeyPem"] = user.Key
1662	j["publicKey"] = k
1663
1664	return j
1665}
1666
1667var oldjonkers = gencache.New(gencache.Options[string, []byte]{Fill: func(name string) ([]byte, bool) {
1668	user, err := butwhatabout(name)
1669	if err != nil {
1670		return nil, false
1671	}
1672	j := junkuser(user)
1673	return j.ToBytes(), true
1674}, Duration: 1 * time.Minute})
1675
1676func asjonker(name string) ([]byte, bool) {
1677	j, ok := oldjonkers.Get(name)
1678	return j, ok
1679}
1680
1681var handfull = gencache.New(gencache.Options[string, string]{Fill: func(name string) (string, bool) {
1682	m := strings.Split(name, "@")
1683	if len(m) != 2 {
1684		dlog.Printf("bad fish name: %s", name)
1685		return "", true
1686	}
1687	var href string
1688	row := stmtGetXonker.QueryRow(name, "fishname")
1689	err := row.Scan(&href)
1690	if err == nil {
1691		return href, true
1692	}
1693	dlog.Printf("fishing for %s", name)
1694	j, err := GetJunkFast(readyLuserOne, fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1695	if err != nil {
1696		ilog.Printf("failed to go fish %s: %s", name, err)
1697		return "", true
1698	}
1699	links, _ := j.GetArray("links")
1700	for _, li := range links {
1701		l, ok := li.(junk.Junk)
1702		if !ok {
1703			continue
1704		}
1705		href, _ := l.GetString("href")
1706		rel, _ := l.GetString("rel")
1707		t, _ := l.GetString("type")
1708		if rel == "self" && friendorfoe(t) {
1709			when := time.Now().UTC().Format(dbtimeformat)
1710			_, err := stmtSaveXonker.Exec(name, href, "fishname", when)
1711			if err != nil {
1712				elog.Printf("error saving fishname: %s", err)
1713			}
1714			return href, true
1715		}
1716	}
1717	return href, true
1718}, Duration: 1 * time.Minute})
1719
1720func gofish(name string) string {
1721	if name[0] == '@' {
1722		name = name[1:]
1723	}
1724	href, _ := handfull.Get(name)
1725	return href
1726}
1727
1728func investigate(name string) (*SomeThing, error) {
1729	if name == "" {
1730		return nil, fmt.Errorf("no name")
1731	}
1732	if name[0] == '@' {
1733		name = gofish(name)
1734	}
1735	if name == "" {
1736		return nil, fmt.Errorf("no name")
1737	}
1738	obj, err := GetJunkFast(readyLuserOne, name)
1739	if err != nil {
1740		return nil, err
1741	}
1742	allinjest(originate(name), obj)
1743	return somethingabout(obj)
1744}
1745
1746func somethingabout(obj junk.Junk) (*SomeThing, error) {
1747	info := new(SomeThing)
1748	t, _ := obj.GetString("type")
1749	isowned := false
1750	switch t {
1751	case "Person":
1752		fallthrough
1753	case "Group":
1754		fallthrough
1755	case "Organization":
1756		fallthrough
1757	case "Application":
1758		fallthrough
1759	case "Service":
1760		info.What = SomeActor
1761	case "OrderedCollection":
1762		isowned = true
1763		fallthrough
1764	case "Collection":
1765		info.What = SomeCollection
1766	default:
1767		return nil, fmt.Errorf("unknown object type")
1768	}
1769	info.XID, _ = obj.GetString("id")
1770	info.Name, _ = obj.GetString("preferredUsername")
1771	if info.Name == "" {
1772		info.Name, _ = obj.GetString("name")
1773	}
1774	if isowned {
1775		info.Owner, _ = obj.GetString("attributedTo")
1776	}
1777	if info.Owner == "" {
1778		info.Owner = info.XID
1779	}
1780	return info, nil
1781}
1782
1783func allinjest(origin string, obj junk.Junk) {
1784	keyobj, ok := obj.GetMap("publicKey")
1785	if ok {
1786		ingestpubkey(origin, keyobj)
1787	}
1788	ingestboxes(origin, obj)
1789	ingesthandle(origin, obj)
1790}
1791
1792func ingestpubkey(origin string, obj junk.Junk) {
1793	keyobj, ok := obj.GetMap("publicKey")
1794	if ok {
1795		obj = keyobj
1796	}
1797	keyname, ok := obj.GetString("id")
1798	var data string
1799	row := stmtGetXonker.QueryRow(keyname, "pubkey")
1800	err := row.Scan(&data)
1801	if err == nil {
1802		return
1803	}
1804	if !ok || origin != originate(keyname) {
1805		ilog.Printf("bad key origin %s <> %s", origin, keyname)
1806		return
1807	}
1808	dlog.Printf("ingesting a needed pubkey: %s", keyname)
1809	owner, ok := obj.GetString("owner")
1810	if !ok {
1811		ilog.Printf("error finding %s pubkey owner", keyname)
1812		return
1813	}
1814	data, ok = obj.GetString("publicKeyPem")
1815	if !ok {
1816		ilog.Printf("error finding %s pubkey", keyname)
1817		return
1818	}
1819	if originate(owner) != origin {
1820		ilog.Printf("bad key owner: %s <> %s", owner, origin)
1821		return
1822	}
1823	_, _, err = httpsig.DecodeKey(data)
1824	if err != nil {
1825		ilog.Printf("error decoding %s pubkey: %s", keyname, err)
1826		return
1827	}
1828	when := time.Now().UTC().Format(dbtimeformat)
1829	_, err = stmtSaveXonker.Exec(keyname, data, "pubkey", when)
1830	if err != nil {
1831		elog.Printf("error saving key: %s", err)
1832	}
1833}
1834
1835func ingestboxes(origin string, obj junk.Junk) {
1836	ident, _ := obj.GetString("id")
1837	if ident == "" {
1838		return
1839	}
1840	if originate(ident) != origin {
1841		return
1842	}
1843	var info string
1844	row := stmtGetXonker.QueryRow(ident, "boxes")
1845	err := row.Scan(&info)
1846	if err == nil {
1847		return
1848	}
1849	dlog.Printf("ingesting boxes: %s", ident)
1850	inbox, _ := obj.GetString("inbox")
1851	outbox, _ := obj.GetString("outbox")
1852	sbox, _ := obj.GetString("endpoints", "sharedInbox")
1853	if inbox != "" {
1854		when := time.Now().UTC().Format(dbtimeformat)
1855		m := strings.Join([]string{inbox, outbox, sbox}, " ")
1856		_, err = stmtSaveXonker.Exec(ident, m, "boxes", when)
1857		if err != nil {
1858			elog.Printf("error saving boxes: %s", err)
1859		}
1860	}
1861}
1862
1863func ingesthandle(origin string, obj junk.Junk) {
1864	xid, _ := obj.GetString("id")
1865	if xid == "" {
1866		return
1867	}
1868	if originate(xid) != origin {
1869		return
1870	}
1871	var handle string
1872	row := stmtGetXonker.QueryRow(xid, "handle")
1873	err := row.Scan(&handle)
1874	if err == nil {
1875		return
1876	}
1877	handle, _ = obj.GetString("preferredUsername")
1878	if handle != "" {
1879		when := time.Now().UTC().Format(dbtimeformat)
1880		_, err = stmtSaveXonker.Exec(xid, handle, "handle", when)
1881		if err != nil {
1882			elog.Printf("error saving handle: %s", err)
1883		}
1884	}
1885}
1886
1887func updateMe(username string) {
1888	user, _ := somenamedusers.Get(username)
1889	dt := time.Now().UTC().Format(time.RFC3339)
1890	j := junk.New()
1891	j["@context"] = itiswhatitis
1892	j["id"] = fmt.Sprintf("%s/upme/%s/%d", user.URL, user.Name, time.Now().Unix())
1893	j["actor"] = user.URL
1894	j["published"] = dt
1895	j["to"] = thewholeworld
1896	j["type"] = "Update"
1897	j["object"] = junkuser(user)
1898
1899	msg := j.ToBytes()
1900
1901	rcpts := make(map[string]bool)
1902	for _, f := range getdubs(user.ID) {
1903		if f.XID == user.URL {
1904			continue
1905		}
1906		box, ok := boxofboxes.Get(f.XID)
1907		if ok && box.Shared != "" {
1908			rcpts["%"+box.Shared] = true
1909		} else {
1910			rcpts[f.XID] = true
1911		}
1912	}
1913	for a := range rcpts {
1914		go deliverate(user.ID, a, msg)
1915	}
1916}
1917
1918func followme(user *WhatAbout, who string, name string, j junk.Junk) {
1919	folxid, _ := j.GetString("id")
1920
1921	ilog.Printf("updating honker follow: %s %s", who, folxid)
1922
1923	var x string
1924	db := opendatabase()
1925	row := db.QueryRow("select xid from honkers where name = ? and xid = ? and userid = ? and flavor in ('dub', 'undub')", name, who, user.ID)
1926	err := row.Scan(&x)
1927	if err != sql.ErrNoRows {
1928		ilog.Printf("duplicate follow request: %s", who)
1929		_, err = stmtUpdateFlavor.Exec("dub", folxid, user.ID, name, who, "undub")
1930		if err != nil {
1931			elog.Printf("error updating honker: %s", err)
1932		}
1933	} else {
1934		stmtSaveDub.Exec(user.ID, name, who, "dub", folxid)
1935	}
1936	go rubadubdub(user, j)
1937}
1938
1939func unfollowme(user *WhatAbout, who string, name string, j junk.Junk) {
1940	var folxid string
1941	if who == "" {
1942		folxid, _ = j.GetString("object")
1943
1944		db := opendatabase()
1945		row := db.QueryRow("select xid, name from honkers where userid = ? and folxid = ? and flavor in ('dub', 'undub')", user.ID, folxid)
1946		err := row.Scan(&who, &name)
1947		if err != nil {
1948			if err != sql.ErrNoRows {
1949				elog.Printf("error scanning honker: %s", err)
1950			}
1951			return
1952		}
1953	}
1954
1955	ilog.Printf("updating honker undo: %s %s", who, folxid)
1956	_, err := stmtUpdateFlavor.Exec("undub", folxid, user.ID, name, who, "dub")
1957	if err != nil {
1958		elog.Printf("error updating honker: %s", err)
1959		return
1960	}
1961}
1962
1963func followyou(user *WhatAbout, honkerid int64, sync bool) {
1964	var url, owner string
1965	db := opendatabase()
1966	row := db.QueryRow("select xid, owner from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')",
1967		honkerid, user.ID)
1968	err := row.Scan(&url, &owner)
1969	if err != nil {
1970		elog.Printf("can't get honker xid: %s", err)
1971		return
1972	}
1973	folxid := xfiltrate()
1974	ilog.Printf("subscribing to %s", url)
1975	_, err = db.Exec("update honkers set flavor = ?, folxid = ? where honkerid = ?", "presub", folxid, honkerid)
1976	if err != nil {
1977		elog.Printf("error updating honker: %s", err)
1978		return
1979	}
1980	if sync {
1981		subsub(user, url, owner, folxid)
1982	} else {
1983		go subsub(user, url, owner, folxid)
1984	}
1985
1986}
1987func unfollowyou(user *WhatAbout, honkerid int64, sync bool) {
1988	db := opendatabase()
1989	row := db.QueryRow("select xid, owner, folxid, flavor from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')",
1990		honkerid, user.ID)
1991	var url, owner, folxid, flavor string
1992	err := row.Scan(&url, &owner, &folxid, &flavor)
1993	if err != nil {
1994		elog.Printf("can't get honker xid: %s", err)
1995		return
1996	}
1997	if flavor == "peep" {
1998		return
1999	}
2000	ilog.Printf("unsubscribing from %s", url)
2001	_, err = db.Exec("update honkers set flavor = ? where honkerid = ?", "unsub", honkerid)
2002	if err != nil {
2003		elog.Printf("error updating honker: %s", err)
2004		return
2005	}
2006	if sync {
2007		itakeitallback(user, url, owner, folxid)
2008	} else {
2009		go itakeitallback(user, url, owner, folxid)
2010	}
2011}
2012
2013func followyou2(user *WhatAbout, j junk.Junk) {
2014	who, _ := j.GetString("actor")
2015
2016	ilog.Printf("updating honker accept: %s", who)
2017	db := opendatabase()
2018	row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')",
2019		user.ID, who)
2020	var name, folxid string
2021	err := row.Scan(&name, &folxid)
2022	if err != nil {
2023		elog.Printf("can't get honker name: %s", err)
2024		return
2025	}
2026	_, err = stmtUpdateFlavor.Exec("sub", folxid, user.ID, name, who, "presub")
2027	if err != nil {
2028		elog.Printf("error updating honker: %s", err)
2029		return
2030	}
2031}
2032
2033func nofollowyou2(user *WhatAbout, j junk.Junk) {
2034	who, _ := j.GetString("actor")
2035
2036	ilog.Printf("updating honker reject: %s", who)
2037	db := opendatabase()
2038	row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')",
2039		user.ID, who)
2040	var name, folxid string
2041	err := row.Scan(&name, &folxid)
2042	if err != nil {
2043		elog.Printf("can't get honker name: %s", err)
2044		return
2045	}
2046	_, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "presub")
2047	_, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "sub")
2048	if err != nil {
2049		elog.Printf("error updating honker: %s", err)
2050		return
2051	}
2052}