all repos — honk @ d61c72d0643cb9eb62a047b367bda78d8854ac30

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