all repos — honk @ 6f60bf1709d4a48631977d3838cd24804a078571

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			savexonk(&xonk)
 829		}
 830		if goingup == 0 {
 831			for _, replid := range replies {
 832				if needxonkid(user, replid) {
 833					log.Printf("missing a reply: %s", replid)
 834					saveonemore(replid)
 835				}
 836			}
 837		}
 838		return &xonk
 839	}
 840
 841	return xonkxonkfn(item, origin)
 842}
 843
 844func rubadubdub(user *WhatAbout, req junk.Junk) {
 845	xid, _ := req.GetString("id")
 846	actor, _ := req.GetString("actor")
 847	j := junk.New()
 848	j["@context"] = itiswhatitis
 849	j["id"] = user.URL + "/dub/" + url.QueryEscape(xid)
 850	j["type"] = "Accept"
 851	j["actor"] = user.URL
 852	j["to"] = actor
 853	j["published"] = time.Now().UTC().Format(time.RFC3339)
 854	j["object"] = req
 855
 856	var buf bytes.Buffer
 857	j.Write(&buf)
 858	msg := buf.Bytes()
 859
 860	deliverate(0, user.Name, actor, msg)
 861}
 862
 863func itakeitallback(user *WhatAbout, xid string) {
 864	j := junk.New()
 865	j["@context"] = itiswhatitis
 866	j["id"] = user.URL + "/unsub/" + url.QueryEscape(xid)
 867	j["type"] = "Undo"
 868	j["actor"] = user.URL
 869	j["to"] = xid
 870	f := junk.New()
 871	f["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
 872	f["type"] = "Follow"
 873	f["actor"] = user.URL
 874	f["to"] = xid
 875	f["object"] = xid
 876	j["object"] = f
 877	j["published"] = time.Now().UTC().Format(time.RFC3339)
 878
 879	var buf bytes.Buffer
 880	j.Write(&buf)
 881	msg := buf.Bytes()
 882
 883	deliverate(0, user.Name, xid, msg)
 884}
 885
 886func subsub(user *WhatAbout, xid string) {
 887	j := junk.New()
 888	j["@context"] = itiswhatitis
 889	j["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
 890	j["type"] = "Follow"
 891	j["actor"] = user.URL
 892	j["to"] = xid
 893	j["object"] = xid
 894	j["published"] = time.Now().UTC().Format(time.RFC3339)
 895
 896	var buf bytes.Buffer
 897	j.Write(&buf)
 898	msg := buf.Bytes()
 899
 900	deliverate(0, user.Name, xid, msg)
 901}
 902
 903// returns activity, object
 904func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
 905	dt := h.Date.Format(time.RFC3339)
 906	var jo junk.Junk
 907	j := junk.New()
 908	j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
 909	j["actor"] = user.URL
 910	j["published"] = dt
 911	if h.Public {
 912		j["to"] = []string{h.Audience[0], user.URL + "/followers"}
 913	} else {
 914		j["to"] = h.Audience[0]
 915	}
 916	if len(h.Audience) > 1 {
 917		j["cc"] = h.Audience[1:]
 918	}
 919
 920	switch h.What {
 921	case "update":
 922		fallthrough
 923	case "tonk":
 924		fallthrough
 925	case "event":
 926		fallthrough
 927	case "honk":
 928		j["type"] = "Create"
 929		if h.What == "update" {
 930			j["type"] = "Update"
 931		}
 932
 933		jo = junk.New()
 934		jo["id"] = h.XID
 935		jo["type"] = "Note"
 936		if h.What == "event" {
 937			jo["type"] = "Event"
 938		}
 939		jo["published"] = dt
 940		jo["url"] = h.XID
 941		jo["attributedTo"] = user.URL
 942		if h.RID != "" {
 943			jo["inReplyTo"] = h.RID
 944		}
 945		if h.Convoy != "" {
 946			jo["context"] = h.Convoy
 947			jo["conversation"] = h.Convoy
 948		}
 949		jo["to"] = h.Audience[0]
 950		if len(h.Audience) > 1 {
 951			jo["cc"] = h.Audience[1:]
 952		}
 953		if !h.Public {
 954			jo["directMessage"] = true
 955		}
 956		translate(h)
 957		h.Noise = re_memes.ReplaceAllString(h.Noise, "")
 958		jo["summary"] = h.Precis
 959		jo["content"] = ontologize(mentionize(h.Noise))
 960		if strings.HasPrefix(h.Precis, "DZ:") {
 961			jo["sensitive"] = true
 962		}
 963
 964		var replies []string
 965		for _, reply := range h.Replies {
 966			replies = append(replies, reply.XID)
 967		}
 968		if len(replies) > 0 {
 969			jr := junk.New()
 970			jr["type"] = "Collection"
 971			jr["totalItems"] = len(replies)
 972			jr["items"] = replies
 973			jo["replies"] = jr
 974		}
 975
 976		var tags []junk.Junk
 977		g := bunchofgrapes(h.Noise)
 978		for _, m := range g {
 979			t := junk.New()
 980			t["type"] = "Mention"
 981			t["name"] = m.who
 982			t["href"] = m.where
 983			tags = append(tags, t)
 984		}
 985		for _, o := range h.Onts {
 986			t := junk.New()
 987			t["type"] = "Hashtag"
 988			o = strings.ToLower(o)
 989			t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
 990			t["name"] = o
 991			tags = append(tags, t)
 992		}
 993		herd := herdofemus(h.Noise)
 994		for _, e := range herd {
 995			t := junk.New()
 996			t["id"] = e.ID
 997			t["type"] = "Emoji"
 998			t["name"] = e.Name
 999			i := junk.New()
1000			i["type"] = "Image"
1001			i["mediaType"] = "image/png"
1002			i["url"] = e.ID
1003			t["icon"] = i
1004			tags = append(tags, t)
1005		}
1006		if p := h.Place; p != nil {
1007			t := junk.New()
1008			t["type"] = "Place"
1009			t["name"] = p.Name
1010			t["latitude"] = p.Latitude
1011			t["longitude"] = p.Longitude
1012			t["url"] = p.Url
1013			tags = append(tags, t)
1014		}
1015		if len(tags) > 0 {
1016			jo["tag"] = tags
1017		}
1018		if t := h.Time; t != nil {
1019			jo["startTime"] = t.StartTime.Format(time.RFC3339)
1020			if t.Duration != 0 {
1021				jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1022			}
1023		}
1024		var atts []junk.Junk
1025		for _, d := range h.Donks {
1026			if re_emus.MatchString(d.Name) {
1027				continue
1028			}
1029			jd := junk.New()
1030			jd["mediaType"] = d.Media
1031			jd["name"] = d.Name
1032			jd["summary"] = d.Desc
1033			jd["type"] = "Document"
1034			jd["url"] = d.URL
1035			atts = append(atts, jd)
1036		}
1037		if len(atts) > 0 {
1038			jo["attachment"] = atts
1039		}
1040		j["object"] = jo
1041	case "bonk":
1042		j["type"] = "Announce"
1043		if h.Convoy != "" {
1044			j["context"] = h.Convoy
1045		}
1046		j["object"] = h.XID
1047	case "unbonk":
1048		b := junk.New()
1049		b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1050		b["type"] = "Announce"
1051		b["actor"] = user.URL
1052		if h.Convoy != "" {
1053			b["context"] = h.Convoy
1054		}
1055		b["object"] = h.XID
1056		j["type"] = "Undo"
1057		j["object"] = b
1058	case "zonk":
1059		j["type"] = "Delete"
1060		j["object"] = h.XID
1061	case "ack":
1062		j["type"] = "Read"
1063		j["object"] = h.XID
1064	case "deack":
1065		b := junk.New()
1066		b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1067		b["type"] = "Read"
1068		b["actor"] = user.URL
1069		b["object"] = h.XID
1070		j["type"] = "Undo"
1071		j["object"] = b
1072	}
1073
1074	return j, jo
1075}
1076
1077func honkworldwide(user *WhatAbout, honk *Honk) {
1078	jonk, _ := jonkjonk(user, honk)
1079	jonk["@context"] = itiswhatitis
1080	var buf bytes.Buffer
1081	jonk.Write(&buf)
1082	msg := buf.Bytes()
1083
1084	rcpts := make(map[string]bool)
1085	for _, a := range honk.Audience {
1086		if a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1087			continue
1088		}
1089		box, _ := getboxes(a)
1090		if box != nil && honk.Public && box.Shared != "" {
1091			rcpts["%"+box.Shared] = true
1092		} else {
1093			rcpts[a] = true
1094		}
1095	}
1096	if honk.Public {
1097		for _, f := range getdubs(user.ID) {
1098			if f.XID == user.URL {
1099				continue
1100			}
1101			box, _ := getboxes(f.XID)
1102			if box != nil && box.Shared != "" {
1103				rcpts["%"+box.Shared] = true
1104			} else {
1105				rcpts[f.XID] = true
1106			}
1107		}
1108	}
1109	for a := range rcpts {
1110		go deliverate(0, user.Name, a, msg)
1111	}
1112}
1113
1114func asjonker(user *WhatAbout) junk.Junk {
1115	about := obfusbreak(user.About)
1116
1117	j := junk.New()
1118	j["@context"] = itiswhatitis
1119	j["id"] = user.URL
1120	j["type"] = "Person"
1121	j["inbox"] = user.URL + "/inbox"
1122	j["outbox"] = user.URL + "/outbox"
1123	j["followers"] = user.URL + "/followers"
1124	j["following"] = user.URL + "/following"
1125	j["name"] = user.Display
1126	j["preferredUsername"] = user.Name
1127	j["summary"] = about
1128	j["url"] = user.URL
1129	a := junk.New()
1130	a["type"] = "Image"
1131	a["mediaType"] = "image/png"
1132	a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
1133	j["icon"] = a
1134	k := junk.New()
1135	k["id"] = user.URL + "#key"
1136	k["owner"] = user.URL
1137	k["publicKeyPem"] = user.Key
1138	j["publicKey"] = k
1139
1140	return j
1141}
1142
1143var handfull = make(map[string]string)
1144var handlock sync.Mutex
1145
1146func gofish(name string) string {
1147	if name[0] == '@' {
1148		name = name[1:]
1149	}
1150	m := strings.Split(name, "@")
1151	if len(m) != 2 {
1152		log.Printf("bad fish name: %s", name)
1153		return ""
1154	}
1155	handlock.Lock()
1156	ref, ok := handfull[name]
1157	handlock.Unlock()
1158	if ok {
1159		return ref
1160	}
1161	row := stmtGetXonker.QueryRow(name, "fishname")
1162	var href string
1163	err := row.Scan(&href)
1164	if err == nil {
1165		handlock.Lock()
1166		handfull[name] = href
1167		handlock.Unlock()
1168		return href
1169	}
1170	log.Printf("fishing for %s", name)
1171	j, err := GetJunkFast(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1172	if err != nil {
1173		log.Printf("failed to go fish %s: %s", name, err)
1174		handlock.Lock()
1175		handfull[name] = ""
1176		handlock.Unlock()
1177		return ""
1178	}
1179	links, _ := j.GetArray("links")
1180	for _, li := range links {
1181		l, ok := li.(junk.Junk)
1182		if !ok {
1183			continue
1184		}
1185		href, _ := l.GetString("href")
1186		rel, _ := l.GetString("rel")
1187		t, _ := l.GetString("type")
1188		if rel == "self" && friendorfoe(t) {
1189			_, err := stmtSaveXonker.Exec(name, href, "fishname")
1190			if err != nil {
1191				log.Printf("error saving fishname: %s", err)
1192			}
1193			handlock.Lock()
1194			handfull[name] = href
1195			handlock.Unlock()
1196			return href
1197		}
1198	}
1199	handlock.Lock()
1200	handfull[name] = ""
1201	handlock.Unlock()
1202	return ""
1203}
1204
1205func isactor(t string) bool {
1206	switch t {
1207	case "Person":
1208	case "Organization":
1209	case "Application":
1210	case "Service":
1211	default:
1212		return false
1213	}
1214	return true
1215}
1216
1217func investigate(name string) (*Honker, error) {
1218	if name == "" {
1219		return nil, fmt.Errorf("no name")
1220	}
1221	if name[0] == '@' {
1222		name = gofish(name)
1223	}
1224	if name == "" {
1225		return nil, fmt.Errorf("no name")
1226	}
1227	log.Printf("digging up some info on %s", name)
1228	obj, err := GetJunkFast(name)
1229	if err != nil {
1230		log.Printf("error investigating honker: %s", err)
1231		return nil, err
1232	}
1233	t, _ := obj.GetString("type")
1234	if !isactor(t) {
1235		log.Printf("it's not a person! %s", name)
1236		return nil, err
1237	}
1238	xid, _ := obj.GetString("id")
1239	handle, _ := obj.GetString("preferredUsername")
1240	return &Honker{XID: xid, Handle: handle}, nil
1241}