all repos — honk @ 6c605142b3c6e6afd791d357909ea315a9934b08

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	"crypto/rsa"
  21	"database/sql"
  22	"fmt"
  23	"io"
  24	"log"
  25	notrand "math/rand"
  26	"net/http"
  27	"net/url"
  28	"os"
  29	"strings"
  30	"sync"
  31	"time"
  32
  33	"humungus.tedunangst.com/r/webs/htfilter"
  34	"humungus.tedunangst.com/r/webs/httpsig"
  35	"humungus.tedunangst.com/r/webs/image"
  36	"humungus.tedunangst.com/r/webs/junk"
  37)
  38
  39var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
  40var thefakename = `application/activity+json`
  41var falsenames = []string{
  42	`application/ld+json`,
  43	`application/activity+json`,
  44}
  45var itiswhatitis = "https://www.w3.org/ns/activitystreams"
  46var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
  47
  48func friendorfoe(ct string) bool {
  49	ct = strings.ToLower(ct)
  50	for _, at := range falsenames {
  51		if strings.HasPrefix(ct, at) {
  52			return true
  53		}
  54	}
  55	return false
  56}
  57
  58func PostJunk(keyname string, key *rsa.PrivateKey, url string, j junk.Junk) error {
  59	var buf bytes.Buffer
  60	j.Write(&buf)
  61	return PostMsg(keyname, key, url, buf.Bytes())
  62}
  63
  64func PostMsg(keyname string, key *rsa.PrivateKey, url string, msg []byte) error {
  65	client := http.DefaultClient
  66	req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
  67	if err != nil {
  68		return err
  69	}
  70	req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
  71	req.Header.Set("Content-Type", theonetruename)
  72	httpsig.SignRequest(keyname, key, req, msg)
  73	resp, err := client.Do(req)
  74	if err != nil {
  75		return err
  76	}
  77	resp.Body.Close()
  78	switch resp.StatusCode {
  79	case 200:
  80	case 201:
  81	case 202:
  82	default:
  83		return fmt.Errorf("http post status: %d", resp.StatusCode)
  84	}
  85	log.Printf("successful post: %s %d", url, resp.StatusCode)
  86	return nil
  87}
  88
  89func GetJunk(url string) (junk.Junk, error) {
  90	return GetJunkTimeout(url, 30*time.Second)
  91}
  92
  93func GetJunkFast(url string) (junk.Junk, error) {
  94	return GetJunkTimeout(url, 5*time.Second)
  95}
  96
  97func GetJunkHardMode(url string) (junk.Junk, error) {
  98	j, err := GetJunk(url)
  99	if err != nil {
 100		emsg := err.Error()
 101		if emsg == "http get status: 502" || strings.Contains(emsg, "timeout") {
 102			log.Printf("trying again after error: %s", emsg)
 103			time.Sleep(time.Duration(60+notrand.Int63n(60)) * time.Second)
 104			j, err = GetJunk(url)
 105			if err != nil {
 106				log.Printf("still couldn't get it")
 107			} else {
 108				log.Printf("retry success!")
 109			}
 110		}
 111	}
 112	return j, err
 113}
 114
 115func GetJunkTimeout(url string, timeout time.Duration) (junk.Junk, error) {
 116	at := thefakename
 117	if strings.Contains(url, ".well-known/webfinger?resource") {
 118		at = "application/jrd+json"
 119	}
 120	return junk.Get(url, junk.GetArgs{
 121		Accept:  at,
 122		Agent:   "honksnonk/5.0; " + serverName,
 123		Timeout: timeout,
 124	})
 125}
 126
 127func savedonk(url string, name, desc, media string, localize bool) *Donk {
 128	if url == "" {
 129		return nil
 130	}
 131	donk := finddonk(url)
 132	if donk != nil {
 133		return donk
 134	}
 135	donk = new(Donk)
 136	log.Printf("saving donk: %s", url)
 137	xid := xfiltrate()
 138	data := []byte{}
 139	if localize {
 140		resp, err := http.Get(url)
 141		if err != nil {
 142			log.Printf("error fetching %s: %s", url, err)
 143			localize = false
 144			goto saveit
 145		}
 146		defer resp.Body.Close()
 147		if resp.StatusCode != 200 {
 148			localize = false
 149			goto saveit
 150		}
 151		var buf bytes.Buffer
 152		limiter := io.LimitReader(resp.Body, 10*1024*1024)
 153		io.Copy(&buf, limiter)
 154
 155		data = buf.Bytes()
 156		if len(data) == 10*1024*1024 {
 157			log.Printf("truncation likely")
 158		}
 159		if strings.HasPrefix(media, "image") {
 160			img, err := image.Vacuum(&buf,
 161				image.Params{LimitSize: 4800 * 4800, MaxWidth: 2048, MaxHeight: 2048})
 162			if err != nil {
 163				log.Printf("unable to decode image: %s", err)
 164				localize = false
 165				data = []byte{}
 166				goto saveit
 167			}
 168			data = img.Data
 169			format := img.Format
 170			media = "image/" + format
 171			if format == "jpeg" {
 172				format = "jpg"
 173			}
 174			xid = xid + "." + format
 175		} else if len(data) > 100000 {
 176			log.Printf("not saving large attachment")
 177			localize = false
 178			data = []byte{}
 179		}
 180	}
 181saveit:
 182	fileid, err := savefile(xid, name, desc, url, media, localize, data)
 183	if err != nil {
 184		log.Printf("error saving file %s: %s", url, err)
 185		return nil
 186	}
 187	donk.FileID = fileid
 188	donk.XID = xid
 189	return donk
 190}
 191
 192func iszonked(userid int64, xid string) bool {
 193	row := stmtFindZonk.QueryRow(userid, xid)
 194	var id int64
 195	err := row.Scan(&id)
 196	if err == nil {
 197		return true
 198	}
 199	if err != sql.ErrNoRows {
 200		log.Printf("err querying zonk: %s", err)
 201	}
 202	return false
 203}
 204
 205func needxonk(user *WhatAbout, x *Honk) bool {
 206	if thoudostbitethythumb(user.ID, x.Audience, x.XID) {
 207		log.Printf("not saving thumb biter? %s via %s", x.XID, x.Honker)
 208		return false
 209	}
 210	return needxonkid(user, x.XID)
 211}
 212func needxonkid(user *WhatAbout, xid string) bool {
 213	if strings.HasPrefix(xid, user.URL+"/") {
 214		return false
 215	}
 216	if thoudostbitethythumb(user.ID, nil, xid) {
 217		log.Printf("don't need thumb biter? %s", xid)
 218		return false
 219	}
 220	if iszonked(user.ID, xid) {
 221		log.Printf("already zonked: %s", xid)
 222		return false
 223	}
 224	row := stmtFindXonk.QueryRow(user.ID, xid)
 225	var id int64
 226	err := row.Scan(&id)
 227	if err == nil {
 228		return false
 229	}
 230	if err != sql.ErrNoRows {
 231		log.Printf("err querying xonk: %s", err)
 232	}
 233	return true
 234}
 235
 236func eradicatexonk(userid int64, xid string) {
 237	xonk := getxonk(userid, xid)
 238	if xonk != nil {
 239		deletehonk(xonk.ID)
 240	}
 241	_, err := stmtSaveZonker.Exec(userid, xid, "zonk")
 242	if err != nil {
 243		log.Printf("error eradicating: %s", err)
 244	}
 245}
 246
 247func savexonk(x *Honk) {
 248	log.Printf("saving xonk: %s", x.XID)
 249	go prehandle(x.Honker)
 250	go prehandle(x.Oonker)
 251	savehonk(x)
 252}
 253
 254type Box struct {
 255	In     string
 256	Out    string
 257	Shared string
 258}
 259
 260var boxofboxes = make(map[string]*Box)
 261var boxlock sync.Mutex
 262var boxinglock sync.Mutex
 263
 264func getboxes(ident string) (*Box, error) {
 265	boxlock.Lock()
 266	b, ok := boxofboxes[ident]
 267	boxlock.Unlock()
 268	if ok {
 269		return b, nil
 270	}
 271
 272	boxinglock.Lock()
 273	defer boxinglock.Unlock()
 274
 275	boxlock.Lock()
 276	b, ok = boxofboxes[ident]
 277	boxlock.Unlock()
 278	if ok {
 279		return b, nil
 280	}
 281
 282	var info string
 283	row := stmtGetXonker.QueryRow(ident, "boxes")
 284	err := row.Scan(&info)
 285	if err != nil {
 286		j, err := GetJunk(ident)
 287		if err != nil {
 288			return nil, err
 289		}
 290		inbox, _ := j.GetString("inbox")
 291		outbox, _ := j.GetString("outbox")
 292		sbox, _ := j.FindString([]string{"endpoints", "sharedInbox"})
 293		b = &Box{In: inbox, Out: outbox, Shared: sbox}
 294		if inbox != "" {
 295			m := strings.Join([]string{inbox, outbox, sbox}, " ")
 296			_, err = stmtSaveXonker.Exec(ident, m, "boxes")
 297			if err != nil {
 298				log.Printf("error saving boxes: %s", err)
 299			}
 300		}
 301	} else {
 302		m := strings.Split(info, " ")
 303		b = &Box{In: m[0], Out: m[1], Shared: m[2]}
 304	}
 305
 306	boxlock.Lock()
 307	boxofboxes[ident] = b
 308	boxlock.Unlock()
 309	return b, nil
 310}
 311
 312func gimmexonks(user *WhatAbout, outbox string) {
 313	log.Printf("getting outbox: %s", outbox)
 314	j, err := GetJunk(outbox)
 315	if err != nil {
 316		log.Printf("error getting outbox: %s", err)
 317		return
 318	}
 319	t, _ := j.GetString("type")
 320	origin := originate(outbox)
 321	if t == "OrderedCollection" {
 322		items, _ := j.GetArray("orderedItems")
 323		if items == nil {
 324			obj, ok := j.GetMap("first")
 325			if ok {
 326				items, _ = obj.GetArray("orderedItems")
 327			} else {
 328				page1, _ := j.GetString("first")
 329				j, err = GetJunk(page1)
 330				if err != nil {
 331					log.Printf("error gettings page1: %s", err)
 332					return
 333				}
 334				items, _ = j.GetArray("orderedItems")
 335			}
 336		}
 337		if len(items) > 20 {
 338			items = items[0:20]
 339		}
 340		for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
 341			items[i], items[j] = items[j], items[i]
 342		}
 343		for _, item := range items {
 344			obj, ok := item.(junk.Junk)
 345			if !ok {
 346				continue
 347			}
 348			xonksaver(user, obj, origin)
 349		}
 350	}
 351}
 352
 353func peeppeep() {
 354	user, _ := butwhatabout("htest")
 355	honkers := gethonkers(user.ID)
 356	for _, f := range honkers {
 357		if f.Flavor != "peep" {
 358			continue
 359		}
 360		log.Printf("getting updates: %s", f.XID)
 361		box, err := getboxes(f.XID)
 362		if err != nil {
 363			log.Printf("error getting outbox: %s", err)
 364			continue
 365		}
 366		gimmexonks(user, box.Out)
 367	}
 368}
 369func whosthere(xid string) ([]string, string) {
 370	obj, err := GetJunk(xid)
 371	if err != nil {
 372		log.Printf("error getting remote xonk: %s", err)
 373		return nil, ""
 374	}
 375	convoy, _ := obj.GetString("context")
 376	if convoy == "" {
 377		convoy, _ = obj.GetString("conversation")
 378	}
 379	return newphone(nil, obj), convoy
 380}
 381
 382func newphone(a []string, obj junk.Junk) []string {
 383	for _, addr := range []string{"to", "cc", "attributedTo"} {
 384		who, _ := obj.GetString(addr)
 385		if who != "" {
 386			a = append(a, who)
 387		}
 388		whos, _ := obj.GetArray(addr)
 389		for _, w := range whos {
 390			who, _ := w.(string)
 391			if who != "" {
 392				a = append(a, who)
 393			}
 394		}
 395	}
 396	return a
 397}
 398
 399func extractattrto(obj junk.Junk) string {
 400	who, _ := obj.GetString("attributedTo")
 401	if who != "" {
 402		return who
 403	}
 404	o, ok := obj.GetMap("attributedTo")
 405	if ok {
 406		id, ok := o.GetString("id")
 407		if ok {
 408			return id
 409		}
 410	}
 411	arr, _ := obj.GetArray("attributedTo")
 412	for _, a := range arr {
 413		o, ok := a.(junk.Junk)
 414		if ok {
 415			t, _ := o.GetString("type")
 416			id, _ := o.GetString("id")
 417			if t == "Person" || t == "" {
 418				return id
 419			}
 420		}
 421	}
 422	return ""
 423}
 424
 425func xonksaver(user *WhatAbout, item junk.Junk, origin string) *Honk {
 426	depth := 0
 427	maxdepth := 10
 428	currenttid := ""
 429	goingup := 0
 430	var xonkxonkfn func(item junk.Junk, origin string) *Honk
 431
 432	saveonemore := func(xid string) {
 433		log.Printf("getting onemore: %s", xid)
 434		if depth >= maxdepth {
 435			log.Printf("in too deep")
 436			return
 437		}
 438		obj, err := GetJunkHardMode(xid)
 439		if err != nil {
 440			log.Printf("error getting onemore: %s: %s", xid, err)
 441			return
 442		}
 443		depth++
 444		xonkxonkfn(obj, originate(xid))
 445		depth--
 446	}
 447
 448	xonkxonkfn = func(item junk.Junk, origin string) *Honk {
 449		// id, _ := item.GetString( "id")
 450		what, _ := item.GetString("type")
 451		dt, _ := item.GetString("published")
 452
 453		var err error
 454		var xid, rid, url, content, precis, convoy string
 455		var replies []string
 456		var obj junk.Junk
 457		var ok bool
 458		isUpdate := false
 459		switch what {
 460		case "Delete":
 461			obj, ok = item.GetMap("object")
 462			if ok {
 463				xid, _ = obj.GetString("id")
 464			} else {
 465				xid, _ = item.GetString("object")
 466			}
 467			if xid == "" {
 468				return nil
 469			}
 470			if originate(xid) != origin {
 471				log.Printf("forged delete: %s", xid)
 472				return nil
 473			}
 474			log.Printf("eradicating %s", xid)
 475			eradicatexonk(user.ID, xid)
 476			return nil
 477		case "Tombstone":
 478			xid, _ = item.GetString("id")
 479			if xid == "" {
 480				return nil
 481			}
 482			if originate(xid) != origin {
 483				log.Printf("forged delete: %s", xid)
 484				return nil
 485			}
 486			log.Printf("eradicating %s", xid)
 487			eradicatexonk(user.ID, xid)
 488			return nil
 489		case "Announce":
 490			obj, ok = item.GetMap("object")
 491			if ok {
 492				xid, _ = obj.GetString("id")
 493			} else {
 494				xid, _ = item.GetString("object")
 495			}
 496			if !needxonkid(user, xid) {
 497				return nil
 498			}
 499			log.Printf("getting bonk: %s", xid)
 500			obj, err = GetJunkHardMode(xid)
 501			if err != nil {
 502				log.Printf("error getting bonk: %s: %s", xid, err)
 503			}
 504			origin = originate(xid)
 505			what = "bonk"
 506		case "Update":
 507			isUpdate = true
 508			fallthrough
 509		case "Create":
 510			obj, ok = item.GetMap("object")
 511			if !ok {
 512				xid, _ = item.GetString("object")
 513				log.Printf("getting created honk: %s", xid)
 514				obj, err = GetJunkHardMode(xid)
 515				if err != nil {
 516					log.Printf("error getting creation: %s", err)
 517				}
 518			}
 519			what = "honk"
 520		case "Read":
 521			xid, ok = item.GetString("object")
 522			if ok {
 523				if !needxonkid(user, xid) {
 524					log.Printf("don't need read obj: %s", xid)
 525					return nil
 526				}
 527				obj, err = GetJunkHardMode(xid)
 528				if err != nil {
 529					log.Printf("error getting read: %s", err)
 530					return nil
 531				}
 532				return xonkxonkfn(obj, originate(xid))
 533			}
 534			return nil
 535		case "Video":
 536			fallthrough
 537		case "Question":
 538			fallthrough
 539		case "Note":
 540			fallthrough
 541		case "Article":
 542			fallthrough
 543		case "Page":
 544			obj = item
 545			what = "honk"
 546		case "Event":
 547			obj = item
 548			what = "event"
 549		default:
 550			log.Printf("unknown activity: %s", what)
 551			fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 552			item.Write(fd)
 553			io.WriteString(fd, "\n")
 554			fd.Close()
 555			return nil
 556		}
 557
 558		var xonk Honk
 559		// early init
 560		xonk.UserID = user.ID
 561		xonk.Honker, _ = item.GetString("actor")
 562		if obj != nil {
 563			if xonk.Honker == "" {
 564				xonk.Honker = extractattrto(obj)
 565			}
 566			xonk.Oonker = extractattrto(obj)
 567			if xonk.Oonker == xonk.Honker {
 568				xonk.Oonker = ""
 569			}
 570			xonk.Audience = newphone(nil, obj)
 571		}
 572		xonk.Audience = append(xonk.Audience, xonk.Honker)
 573		xonk.Audience = oneofakind(xonk.Audience)
 574		for _, a := range xonk.Audience {
 575			if a == user.URL {
 576				xonk.Whofore = 1
 577			}
 578		}
 579
 580		if obj != nil {
 581			ot, _ := obj.GetString("type")
 582			url, _ = obj.GetString("url")
 583			dt2, ok := obj.GetString("published")
 584			if ok {
 585				dt = dt2
 586			}
 587			xid, _ = obj.GetString("id")
 588			precis, _ = obj.GetString("summary")
 589			if precis == "" {
 590				precis, _ = obj.GetString("name")
 591			}
 592			content, _ = obj.GetString("content")
 593			if !strings.HasPrefix(content, "<p>") {
 594				content = "<p>" + content
 595			}
 596			sens, _ := obj["sensitive"].(bool)
 597			if sens && precis == "" {
 598				precis = "unspecified horror"
 599			}
 600			rid, ok = obj.GetString("inReplyTo")
 601			if !ok {
 602				robj, ok := obj.GetMap("inReplyTo")
 603				if ok {
 604					rid, _ = robj.GetString("id")
 605				}
 606			}
 607			convoy, _ = obj.GetString("context")
 608			if strings.HasSuffix(convoy, "#context") &&
 609				originate(convoy) != originate(xid) {
 610				// friendica...
 611				convoy = ""
 612			}
 613			if convoy == "" {
 614				convoy, _ = obj.GetString("conversation")
 615			}
 616			if ot == "Question" {
 617				if what == "honk" {
 618					what = "qonk"
 619				}
 620				content += "<ul>"
 621				ans, _ := obj.GetArray("oneOf")
 622				for _, ai := range ans {
 623					a, ok := ai.(junk.Junk)
 624					if !ok {
 625						continue
 626					}
 627					as, _ := a.GetString("name")
 628					content += "<li>" + as
 629				}
 630				ans, _ = obj.GetArray("anyOf")
 631				for _, ai := range ans {
 632					a, ok := ai.(junk.Junk)
 633					if !ok {
 634						continue
 635					}
 636					as, _ := a.GetString("name")
 637					content += "<li>" + as
 638				}
 639				content += "</ul>"
 640			}
 641			if what == "honk" && rid != "" {
 642				what = "tonk"
 643			}
 644			atts, _ := obj.GetArray("attachment")
 645			for i, atti := range atts {
 646				att, ok := atti.(junk.Junk)
 647				if !ok {
 648					continue
 649				}
 650				at, _ := att.GetString("type")
 651				mt, _ := att.GetString("mediaType")
 652				u, _ := att.GetString("url")
 653				name, _ := att.GetString("name")
 654				desc, _ := att.GetString("summary")
 655				if desc == "" {
 656					desc = name
 657				}
 658				localize := false
 659				if i > 4 {
 660					log.Printf("excessive attachment: %s", at)
 661				} else if at == "Document" || at == "Image" {
 662					mt = strings.ToLower(mt)
 663					log.Printf("attachment: %s %s", mt, u)
 664					if mt == "text/plain" || strings.HasPrefix(mt, "image") {
 665						localize = true
 666					}
 667				} else {
 668					log.Printf("unknown attachment: %s", at)
 669				}
 670				if skipMedia(&xonk) {
 671					localize = false
 672				}
 673				donk := savedonk(u, name, desc, mt, localize)
 674				if donk != nil {
 675					xonk.Donks = append(xonk.Donks, donk)
 676				}
 677			}
 678			tags, _ := obj.GetArray("tag")
 679			for _, tagi := range tags {
 680				tag, ok := tagi.(junk.Junk)
 681				if !ok {
 682					continue
 683				}
 684				tt, _ := tag.GetString("type")
 685				name, _ := tag.GetString("name")
 686				desc, _ := tag.GetString("summary")
 687				if desc == "" {
 688					desc = name
 689				}
 690				if tt == "Emoji" {
 691					icon, _ := tag.GetMap("icon")
 692					mt, _ := icon.GetString("mediaType")
 693					if mt == "" {
 694						mt = "image/png"
 695					}
 696					u, _ := icon.GetString("url")
 697					donk := savedonk(u, name, desc, mt, true)
 698					if donk != nil {
 699						xonk.Donks = append(xonk.Donks, donk)
 700					}
 701				}
 702				if tt == "Hashtag" {
 703					if len(name) > 1 && name[0] == '#' {
 704						xonk.Onts = append(xonk.Onts, name)
 705					} else {
 706						log.Printf("bogus hashtag name: %s", name)
 707					}
 708				}
 709				if tt == "Place" {
 710					p := new(Place)
 711					p.Name = name
 712					p.Latitude, _ = tag["latitude"].(float64)
 713					p.Longitude, _ = tag["longitude"].(float64)
 714					p.Url, _ = tag.GetString("url")
 715					xonk.Place = p
 716				}
 717			}
 718			starttime, ok := obj.GetString("startTime")
 719			if ok {
 720				start, err := time.Parse(time.RFC3339, starttime)
 721				if err == nil {
 722					t := new(Time)
 723					t.StartTime = start
 724					dura, _ := obj.GetString("duration")
 725					if strings.HasPrefix(dura, "PT") {
 726						dura = strings.ToLower(dura[2:])
 727						d, _ := time.ParseDuration(dura)
 728						t.Duration = Duration(d)
 729					}
 730					xonk.Time = t
 731				}
 732			}
 733			loca, ok := obj.GetMap("location")
 734			if ok {
 735				tt, _ := loca.GetString("type")
 736				name, _ := loca.GetString("name")
 737				if tt == "Place" {
 738					p := new(Place)
 739					p.Name = name
 740					p.Latitude, _ = loca["latitude"].(float64)
 741					p.Longitude, _ = loca["longitude"].(float64)
 742					p.Url, _ = loca.GetString("url")
 743					xonk.Place = p
 744				}
 745			}
 746
 747			xonk.Onts = oneofakind(xonk.Onts)
 748			replyobj, ok := obj.GetMap("replies")
 749			if ok {
 750				items, ok := replyobj.GetArray("items")
 751				if !ok {
 752					first, ok := replyobj.GetMap("first")
 753					if ok {
 754						items, _ = first.GetArray("items")
 755					}
 756				}
 757				for _, repl := range items {
 758					s, ok := repl.(string)
 759					if ok {
 760						replies = append(replies, s)
 761					}
 762				}
 763			}
 764
 765		}
 766		if originate(xid) != origin {
 767			log.Printf("original sin: %s <> %s", xid, origin)
 768			item.Write(os.Stdout)
 769			return nil
 770		}
 771
 772		if currenttid == "" {
 773			currenttid = convoy
 774		}
 775
 776		if len(content) > 90001 {
 777			log.Printf("content too long. truncating")
 778			content = content[:90001]
 779		}
 780
 781		// grab any inline imgs
 782		imgfilt := htfilter.New()
 783		imgfilt.Imager = inlineimgs
 784		imgfilt.String(content)
 785
 786		// init xonk
 787		xonk.What = what
 788		xonk.XID = xid
 789		xonk.RID = rid
 790		xonk.Date, _ = time.Parse(time.RFC3339, dt)
 791		xonk.URL = url
 792		xonk.Noise = content
 793		xonk.Precis = precis
 794		xonk.Format = "html"
 795		xonk.Convoy = convoy
 796
 797		if isUpdate {
 798			log.Printf("something has changed! %s", xonk.XID)
 799			prev := getxonk(user.ID, xonk.XID)
 800			if prev == nil {
 801				log.Printf("didn't find old version for update: %s", xonk.XID)
 802				return nil
 803			}
 804			prev.Noise = xonk.Noise
 805			prev.Precis = xonk.Precis
 806			prev.Date = xonk.Date
 807			prev.Donks = xonk.Donks
 808			prev.Onts = xonk.Onts
 809			prev.Place = xonk.Place
 810			updatehonk(prev)
 811		} else if needxonk(user, &xonk) {
 812			if rid != "" {
 813				if needxonkid(user, rid) {
 814					goingup++
 815					saveonemore(rid)
 816					goingup--
 817				}
 818				if convoy == "" {
 819					xx := getxonk(user.ID, rid)
 820					if xx != nil {
 821						convoy = xx.Convoy
 822					}
 823				}
 824			}
 825			if convoy == "" {
 826				convoy = currenttid
 827			}
 828			if convoy == "" {
 829				convoy = "missing-" + xfiltrate()
 830				currenttid = convoy
 831			}
 832			xonk.Convoy = convoy
 833			savexonk(&xonk)
 834		}
 835		if goingup == 0 {
 836			for _, replid := range replies {
 837				if needxonkid(user, replid) {
 838					log.Printf("missing a reply: %s", replid)
 839					saveonemore(replid)
 840				}
 841			}
 842		}
 843		return &xonk
 844	}
 845
 846	return xonkxonkfn(item, origin)
 847}
 848
 849func rubadubdub(user *WhatAbout, req junk.Junk) {
 850	xid, _ := req.GetString("id")
 851	actor, _ := req.GetString("actor")
 852	j := junk.New()
 853	j["@context"] = itiswhatitis
 854	j["id"] = user.URL + "/dub/" + url.QueryEscape(xid)
 855	j["type"] = "Accept"
 856	j["actor"] = user.URL
 857	j["to"] = actor
 858	j["published"] = time.Now().UTC().Format(time.RFC3339)
 859	j["object"] = req
 860
 861	var buf bytes.Buffer
 862	j.Write(&buf)
 863	msg := buf.Bytes()
 864
 865	deliverate(0, user.Name, actor, msg)
 866}
 867
 868func itakeitallback(user *WhatAbout, xid string) {
 869	j := junk.New()
 870	j["@context"] = itiswhatitis
 871	j["id"] = user.URL + "/unsub/" + url.QueryEscape(xid)
 872	j["type"] = "Undo"
 873	j["actor"] = user.URL
 874	j["to"] = xid
 875	f := junk.New()
 876	f["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
 877	f["type"] = "Follow"
 878	f["actor"] = user.URL
 879	f["to"] = xid
 880	f["object"] = xid
 881	j["object"] = f
 882	j["published"] = time.Now().UTC().Format(time.RFC3339)
 883
 884	var buf bytes.Buffer
 885	j.Write(&buf)
 886	msg := buf.Bytes()
 887
 888	deliverate(0, user.Name, xid, msg)
 889}
 890
 891func subsub(user *WhatAbout, xid string) {
 892	j := junk.New()
 893	j["@context"] = itiswhatitis
 894	j["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
 895	j["type"] = "Follow"
 896	j["actor"] = user.URL
 897	j["to"] = xid
 898	j["object"] = xid
 899	j["published"] = time.Now().UTC().Format(time.RFC3339)
 900
 901	var buf bytes.Buffer
 902	j.Write(&buf)
 903	msg := buf.Bytes()
 904
 905	deliverate(0, user.Name, xid, msg)
 906}
 907
 908// returns activity, object
 909func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
 910	dt := h.Date.Format(time.RFC3339)
 911	var jo junk.Junk
 912	j := junk.New()
 913	j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
 914	j["actor"] = user.URL
 915	j["published"] = dt
 916	if h.Public {
 917		j["to"] = []string{h.Audience[0], user.URL + "/followers"}
 918	} else {
 919		j["to"] = h.Audience[0]
 920	}
 921	if len(h.Audience) > 1 {
 922		j["cc"] = h.Audience[1:]
 923	}
 924
 925	switch h.What {
 926	case "update":
 927		fallthrough
 928	case "tonk":
 929		fallthrough
 930	case "event":
 931		fallthrough
 932	case "honk":
 933		j["type"] = "Create"
 934		if h.What == "update" {
 935			j["type"] = "Update"
 936		}
 937
 938		jo = junk.New()
 939		jo["id"] = h.XID
 940		jo["type"] = "Note"
 941		if h.What == "event" {
 942			jo["type"] = "Event"
 943		}
 944		jo["published"] = dt
 945		jo["url"] = h.XID
 946		jo["attributedTo"] = user.URL
 947		if h.RID != "" {
 948			jo["inReplyTo"] = h.RID
 949		}
 950		if h.Convoy != "" {
 951			jo["context"] = h.Convoy
 952			jo["conversation"] = h.Convoy
 953		}
 954		jo["to"] = h.Audience[0]
 955		if len(h.Audience) > 1 {
 956			jo["cc"] = h.Audience[1:]
 957		}
 958		if !h.Public {
 959			jo["directMessage"] = true
 960		}
 961		translate(h)
 962		h.Noise = re_memes.ReplaceAllString(h.Noise, "")
 963		jo["summary"] = h.Precis
 964		jo["content"] = ontologize(mentionize(h.Noise))
 965		if strings.HasPrefix(h.Precis, "DZ:") {
 966			jo["sensitive"] = true
 967		}
 968
 969		var replies []string
 970		for _, reply := range h.Replies {
 971			replies = append(replies, reply.XID)
 972		}
 973		if len(replies) > 0 {
 974			jr := junk.New()
 975			jr["type"] = "Collection"
 976			jr["totalItems"] = len(replies)
 977			jr["items"] = replies
 978			jo["replies"] = jr
 979		}
 980
 981		var tags []junk.Junk
 982		g := bunchofgrapes(h.Noise)
 983		for _, m := range g {
 984			t := junk.New()
 985			t["type"] = "Mention"
 986			t["name"] = m.who
 987			t["href"] = m.where
 988			tags = append(tags, t)
 989		}
 990		for _, o := range h.Onts {
 991			t := junk.New()
 992			t["type"] = "Hashtag"
 993			o = strings.ToLower(o)
 994			t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
 995			t["name"] = o
 996			tags = append(tags, t)
 997		}
 998		herd := herdofemus(h.Noise)
 999		for _, e := range herd {
1000			t := junk.New()
1001			t["id"] = e.ID
1002			t["type"] = "Emoji"
1003			t["name"] = e.Name
1004			i := junk.New()
1005			i["type"] = "Image"
1006			i["mediaType"] = "image/png"
1007			i["url"] = e.ID
1008			t["icon"] = i
1009			tags = append(tags, t)
1010		}
1011		if p := h.Place; p != nil {
1012			t := junk.New()
1013			t["type"] = "Place"
1014			t["name"] = p.Name
1015			t["latitude"] = p.Latitude
1016			t["longitude"] = p.Longitude
1017			t["url"] = p.Url
1018			tags = append(tags, t)
1019		}
1020		if len(tags) > 0 {
1021			jo["tag"] = tags
1022		}
1023		if t := h.Time; t != nil {
1024			jo["startTime"] = t.StartTime.Format(time.RFC3339)
1025			if t.Duration != 0 {
1026				jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1027			}
1028		}
1029		var atts []junk.Junk
1030		for _, d := range h.Donks {
1031			if re_emus.MatchString(d.Name) {
1032				continue
1033			}
1034			jd := junk.New()
1035			jd["mediaType"] = d.Media
1036			jd["name"] = d.Name
1037			jd["summary"] = d.Desc
1038			jd["type"] = "Document"
1039			jd["url"] = d.URL
1040			atts = append(atts, jd)
1041		}
1042		if len(atts) > 0 {
1043			jo["attachment"] = atts
1044		}
1045		j["object"] = jo
1046	case "bonk":
1047		j["type"] = "Announce"
1048		if h.Convoy != "" {
1049			j["context"] = h.Convoy
1050		}
1051		j["object"] = h.XID
1052	case "unbonk":
1053		b := junk.New()
1054		b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1055		b["type"] = "Announce"
1056		b["actor"] = user.URL
1057		if h.Convoy != "" {
1058			b["context"] = h.Convoy
1059		}
1060		b["object"] = h.XID
1061		j["type"] = "Undo"
1062		j["object"] = b
1063	case "zonk":
1064		j["type"] = "Delete"
1065		j["object"] = h.XID
1066	case "ack":
1067		j["type"] = "Read"
1068		j["object"] = h.XID
1069	case "deack":
1070		b := junk.New()
1071		b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1072		b["type"] = "Read"
1073		b["actor"] = user.URL
1074		b["object"] = h.XID
1075		j["type"] = "Undo"
1076		j["object"] = b
1077	}
1078
1079	return j, jo
1080}
1081
1082func honkworldwide(user *WhatAbout, honk *Honk) {
1083	jonk, _ := jonkjonk(user, honk)
1084	jonk["@context"] = itiswhatitis
1085	var buf bytes.Buffer
1086	jonk.Write(&buf)
1087	msg := buf.Bytes()
1088
1089	rcpts := make(map[string]bool)
1090	for _, a := range honk.Audience {
1091		if a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1092			continue
1093		}
1094		box, _ := getboxes(a)
1095		if box != nil && honk.Public && box.Shared != "" {
1096			rcpts["%"+box.Shared] = true
1097		} else {
1098			rcpts[a] = true
1099		}
1100	}
1101	if honk.Public {
1102		for _, f := range getdubs(user.ID) {
1103			if f.XID == user.URL {
1104				continue
1105			}
1106			box, _ := getboxes(f.XID)
1107			if box != nil && box.Shared != "" {
1108				rcpts["%"+box.Shared] = true
1109			} else {
1110				rcpts[f.XID] = true
1111			}
1112		}
1113	}
1114	for a := range rcpts {
1115		go deliverate(0, user.Name, a, msg)
1116	}
1117}
1118
1119func asjonker(user *WhatAbout) junk.Junk {
1120	about := obfusbreak(user.About)
1121
1122	j := junk.New()
1123	j["@context"] = itiswhatitis
1124	j["id"] = user.URL
1125	j["type"] = "Person"
1126	j["inbox"] = user.URL + "/inbox"
1127	j["outbox"] = user.URL + "/outbox"
1128	j["followers"] = user.URL + "/followers"
1129	j["following"] = user.URL + "/following"
1130	j["name"] = user.Display
1131	j["preferredUsername"] = user.Name
1132	j["summary"] = about
1133	j["url"] = user.URL
1134	a := junk.New()
1135	a["type"] = "Image"
1136	a["mediaType"] = "image/png"
1137	a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
1138	j["icon"] = a
1139	k := junk.New()
1140	k["id"] = user.URL + "#key"
1141	k["owner"] = user.URL
1142	k["publicKeyPem"] = user.Key
1143	j["publicKey"] = k
1144
1145	return j
1146}
1147
1148var handfull = make(map[string]string)
1149var handlock sync.Mutex
1150
1151func gofish(name string) string {
1152	if name[0] == '@' {
1153		name = name[1:]
1154	}
1155	m := strings.Split(name, "@")
1156	if len(m) != 2 {
1157		log.Printf("bad fish name: %s", name)
1158		return ""
1159	}
1160	handlock.Lock()
1161	ref, ok := handfull[name]
1162	handlock.Unlock()
1163	if ok {
1164		return ref
1165	}
1166	row := stmtGetXonker.QueryRow(name, "fishname")
1167	var href string
1168	err := row.Scan(&href)
1169	if err == nil {
1170		handlock.Lock()
1171		handfull[name] = href
1172		handlock.Unlock()
1173		return href
1174	}
1175	log.Printf("fishing for %s", name)
1176	j, err := GetJunkFast(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1177	if err != nil {
1178		log.Printf("failed to go fish %s: %s", name, err)
1179		handlock.Lock()
1180		handfull[name] = ""
1181		handlock.Unlock()
1182		return ""
1183	}
1184	links, _ := j.GetArray("links")
1185	for _, li := range links {
1186		l, ok := li.(junk.Junk)
1187		if !ok {
1188			continue
1189		}
1190		href, _ := l.GetString("href")
1191		rel, _ := l.GetString("rel")
1192		t, _ := l.GetString("type")
1193		if rel == "self" && friendorfoe(t) {
1194			_, err := stmtSaveXonker.Exec(name, href, "fishname")
1195			if err != nil {
1196				log.Printf("error saving fishname: %s", err)
1197			}
1198			handlock.Lock()
1199			handfull[name] = href
1200			handlock.Unlock()
1201			return href
1202		}
1203	}
1204	handlock.Lock()
1205	handfull[name] = ""
1206	handlock.Unlock()
1207	return ""
1208}
1209
1210func isactor(t string) bool {
1211	switch t {
1212	case "Person":
1213	case "Organization":
1214	case "Application":
1215	case "Service":
1216	default:
1217		return false
1218	}
1219	return true
1220}
1221
1222func investigate(name string) (*Honker, error) {
1223	if name == "" {
1224		return nil, fmt.Errorf("no name")
1225	}
1226	if name[0] == '@' {
1227		name = gofish(name)
1228	}
1229	if name == "" {
1230		return nil, fmt.Errorf("no name")
1231	}
1232	log.Printf("digging up some info on %s", name)
1233	obj, err := GetJunkFast(name)
1234	if err != nil {
1235		log.Printf("error investigating honker: %s", err)
1236		return nil, err
1237	}
1238	t, _ := obj.GetString("type")
1239	if !isactor(t) {
1240		log.Printf("it's not a person! %s", name)
1241		return nil, err
1242	}
1243	xid, _ := obj.GetString("id")
1244	handle, _ := obj.GetString("preferredUsername")
1245	return &Honker{XID: xid, Handle: handle}, nil
1246}