all repos — honk @ e36bbfba4a31da0cf3abfde4acffbcfc6dee3b9b

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