all repos — honk @ bb603ba4649e31b248833c897bba77ea693ed957

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