all repos — honk @ 73ad26f01d1da2b0c0d5745294b003ef9e2eac0d

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