all repos — honk @ 0da9c0ed21bcb0d5d8cbb5b0f59a5842ae701968

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