all repos — honk @ 433e1d53bc2d34e78aa9c5917cdf48d1c9998f7f

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	"net/http"
 26	"net/url"
 27	"os"
 28	"strings"
 29	"sync"
 30	"time"
 31
 32	"humungus.tedunangst.com/r/webs/httpsig"
 33	"humungus.tedunangst.com/r/webs/image"
 34	"humungus.tedunangst.com/r/webs/junk"
 35)
 36
 37var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
 38var thefakename = `application/activity+json`
 39var falsenames = []string{
 40	`application/ld+json`,
 41	`application/activity+json`,
 42}
 43var itiswhatitis = "https://www.w3.org/ns/activitystreams"
 44var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
 45
 46func friendorfoe(ct string) bool {
 47	ct = strings.ToLower(ct)
 48	for _, at := range falsenames {
 49		if strings.HasPrefix(ct, at) {
 50			return true
 51		}
 52	}
 53	return false
 54}
 55
 56func PostJunk(keyname string, key *rsa.PrivateKey, url string, j junk.Junk) error {
 57	var buf bytes.Buffer
 58	j.Write(&buf)
 59	return PostMsg(keyname, key, url, buf.Bytes())
 60}
 61
 62func PostMsg(keyname string, key *rsa.PrivateKey, url string, msg []byte) error {
 63	client := http.DefaultClient
 64	req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
 65	if err != nil {
 66		return err
 67	}
 68	req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
 69	req.Header.Set("Content-Type", theonetruename)
 70	httpsig.SignRequest(keyname, key, req, msg)
 71	resp, err := client.Do(req)
 72	if err != nil {
 73		return err
 74	}
 75	resp.Body.Close()
 76	switch resp.StatusCode {
 77	case 200:
 78	case 201:
 79	case 202:
 80	default:
 81		return fmt.Errorf("http post status: %d", resp.StatusCode)
 82	}
 83	log.Printf("successful post: %s %d", url, resp.StatusCode)
 84	return nil
 85}
 86
 87func GetJunk(url string) (junk.Junk, error) {
 88	return GetJunkTimeout(url, 0)
 89}
 90
 91func GetJunkFast(url string) (junk.Junk, error) {
 92	return GetJunkTimeout(url, 5*time.Second)
 93}
 94
 95func GetJunkTimeout(url string, timeout time.Duration) (junk.Junk, error) {
 96	at := thefakename
 97	if strings.Contains(url, ".well-known/webfinger?resource") {
 98		at = "application/jrd+json"
 99	}
100	return junk.Get(url, junk.GetArgs{
101		Accept:  at,
102		Agent:   "honksnonk/5.0; " + serverName,
103		Timeout: timeout,
104	})
105}
106
107func savedonk(url string, name, media string, localize bool) *Donk {
108	if url == "" {
109		return nil
110	}
111	var donk Donk
112	row := stmtFindFile.QueryRow(url)
113	err := row.Scan(&donk.FileID)
114	if err == nil {
115		return &donk
116	}
117	log.Printf("saving donk: %s", url)
118	if err != nil && err != sql.ErrNoRows {
119		log.Printf("error querying: %s", err)
120	}
121	xid := xfiltrate()
122	data := []byte{}
123	if localize {
124		resp, err := http.Get(url)
125		if err != nil {
126			log.Printf("error fetching %s: %s", url, err)
127			localize = false
128			goto saveit
129		}
130		defer resp.Body.Close()
131		if resp.StatusCode != 200 {
132			localize = false
133			goto saveit
134		}
135		var buf bytes.Buffer
136		io.Copy(&buf, resp.Body)
137
138		data = buf.Bytes()
139		if strings.HasPrefix(media, "image") {
140			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
141			if err != nil {
142				log.Printf("unable to decode image: %s", err)
143				localize = false
144				data = []byte{}
145				goto saveit
146			}
147			data = img.Data
148			media = "image/" + img.Format
149		} else if len(data) > 100000 {
150			log.Printf("not saving large attachment")
151			localize = false
152			data = []byte{}
153		}
154	}
155saveit:
156	res, err := stmtSaveFile.Exec(xid, name, url, media, localize, data)
157	if err != nil {
158		log.Printf("error saving file %s: %s", url, err)
159		return nil
160	}
161	donk.FileID, _ = res.LastInsertId()
162	return &donk
163}
164
165func iszonked(userid int64, xid string) bool {
166	row := stmtFindZonk.QueryRow(userid, xid)
167	var id int64
168	err := row.Scan(&id)
169	if err == nil {
170		return true
171	}
172	if err != sql.ErrNoRows {
173		log.Printf("err querying zonk: %s", err)
174	}
175	return false
176}
177
178func needxonk(user *WhatAbout, x *Honk) bool {
179	if x.What == "eradicate" {
180		return true
181	}
182	if thoudostbitethythumb(user.ID, x.Audience, x.XID) {
183		log.Printf("not saving thumb biter? %s via %s", x.XID, x.Honker)
184		return false
185	}
186	return needxonkid(user, x.XID)
187}
188func needxonkid(user *WhatAbout, xid string) bool {
189	if strings.HasPrefix(xid, user.URL+"/") {
190		return false
191	}
192	if iszonked(user.ID, xid) {
193		log.Printf("already zonked: %s", xid)
194		return false
195	}
196	row := stmtFindXonk.QueryRow(user.ID, xid)
197	var id int64
198	err := row.Scan(&id)
199	if err == nil {
200		return false
201	}
202	if err != sql.ErrNoRows {
203		log.Printf("err querying xonk: %s", err)
204	}
205	return true
206}
207
208func savexonk(user *WhatAbout, x *Honk) {
209	if x.What == "eradicate" {
210		log.Printf("eradicating %s by %s", x.XID, x.Honker)
211		xonk := getxonk(user.ID, x.XID)
212		if xonk != nil {
213			stmtZonkDonks.Exec(xonk.ID)
214			_, err := stmtZonkIt.Exec(user.ID, x.XID)
215			if err != nil {
216				log.Printf("error eradicating: %s", err)
217			}
218		}
219		stmtSaveZonker.Exec(user.ID, x.XID, "zonk")
220		return
221	}
222	log.Printf("saving xonk: %s", x.XID)
223	dt := x.Date.UTC().Format(dbtimeformat)
224	aud := strings.Join(x.Audience, " ")
225	whofore := 0
226	if strings.Contains(aud, user.URL) {
227		whofore = 1
228	}
229	res, err := stmtSaveHonk.Exec(x.UserID, x.What, x.Honker, x.XID, x.RID, dt, x.URL, aud,
230		x.Noise, x.Convoy, whofore, "html", x.Precis, x.Oonker)
231	if err != nil {
232		log.Printf("err saving xonk: %s", err)
233		return
234	}
235	x.ID, _ = res.LastInsertId()
236	for _, d := range x.Donks {
237		_, err = stmtSaveDonk.Exec(x.ID, d.FileID)
238		if err != nil {
239			log.Printf("err saving donk: %s", err)
240			return
241		}
242	}
243}
244
245type Box struct {
246	In     string
247	Out    string
248	Shared string
249}
250
251var boxofboxes = make(map[string]*Box)
252var boxlock sync.Mutex
253var boxinglock sync.Mutex
254
255func getboxes(ident string) (*Box, error) {
256	boxlock.Lock()
257	b, ok := boxofboxes[ident]
258	boxlock.Unlock()
259	if ok {
260		return b, nil
261	}
262
263	boxinglock.Lock()
264	defer boxinglock.Unlock()
265
266	boxlock.Lock()
267	b, ok = boxofboxes[ident]
268	boxlock.Unlock()
269	if ok {
270		return b, nil
271	}
272
273	var info string
274	row := stmtGetXonker.QueryRow(ident, "boxes")
275	err := row.Scan(&info)
276	if err != nil {
277		j, err := GetJunk(ident)
278		if err != nil {
279			return nil, err
280		}
281		inbox, _ := j.GetString("inbox")
282		outbox, _ := j.GetString("outbox")
283		sbox, _ := j.FindString([]string{"endpoints", "sharedInbox"})
284		b = &Box{In: inbox, Out: outbox, Shared: sbox}
285		if inbox != "" {
286			m := strings.Join([]string{inbox, outbox, sbox}, " ")
287			_, err = stmtSaveXonker.Exec(ident, m, "boxes")
288			if err != nil {
289				log.Printf("error saving boxes: %s", err)
290			}
291		}
292	} else {
293		m := strings.Split(info, " ")
294		b = &Box{In: m[0], Out: m[1], Shared: m[2]}
295	}
296
297	boxlock.Lock()
298	boxofboxes[ident] = b
299	boxlock.Unlock()
300	return b, nil
301}
302
303func gimmexonks(user *WhatAbout, outbox string) {
304	log.Printf("getting outbox: %s", outbox)
305	j, err := GetJunk(outbox)
306	if err != nil {
307		log.Printf("error getting outbox: %s", err)
308		return
309	}
310	t, _ := j.GetString("type")
311	origin := originate(outbox)
312	if t == "OrderedCollection" {
313		items, _ := j.GetArray("orderedItems")
314		if items == nil {
315			obj, ok := j.GetMap("first")
316			if ok {
317				items, _ = obj.GetArray("orderedItems")
318			} else {
319				page1, _ := j.GetString("first")
320				j, err = GetJunk(page1)
321				if err != nil {
322					log.Printf("error gettings page1: %s", err)
323					return
324				}
325				items, _ = j.GetArray("orderedItems")
326			}
327		}
328		if len(items) > 20 {
329			items = items[0:20]
330		}
331		for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
332			items[i], items[j] = items[j], items[i]
333		}
334		for _, item := range items {
335			obj, ok := item.(junk.Junk)
336			if !ok {
337				continue
338			}
339			xonk := xonkxonk(user, obj, origin)
340			if xonk != nil {
341				savexonk(user, xonk)
342			}
343		}
344	}
345}
346
347func peeppeep() {
348	user, _ := butwhatabout("htest")
349	honkers := gethonkers(user.ID)
350	for _, f := range honkers {
351		if f.Flavor != "peep" {
352			continue
353		}
354		log.Printf("getting updates: %s", f.XID)
355		box, err := getboxes(f.XID)
356		if err != nil {
357			log.Printf("error getting outbox: %s", err)
358			continue
359		}
360		gimmexonks(user, box.Out)
361	}
362}
363func whosthere(xid string) ([]string, string) {
364	obj, err := GetJunk(xid)
365	if err != nil {
366		log.Printf("error getting remote xonk: %s", err)
367		return nil, ""
368	}
369	convoy, _ := obj.GetString("context")
370	if convoy == "" {
371		convoy, _ = obj.GetString("conversation")
372	}
373	return newphone(nil, obj), convoy
374}
375
376func newphone(a []string, obj junk.Junk) []string {
377	for _, addr := range []string{"to", "cc", "attributedTo"} {
378		who, _ := obj.GetString(addr)
379		if who != "" {
380			a = append(a, who)
381		}
382		whos, _ := obj.GetArray(addr)
383		for _, w := range whos {
384			who, _ := w.(string)
385			if who != "" {
386				a = append(a, who)
387			}
388		}
389	}
390	return a
391}
392
393func extractattrto(obj junk.Junk) string {
394	who, _ := obj.GetString("attributedTo")
395	if who != "" {
396		return who
397	}
398	o, ok := obj.GetMap("attributedTo")
399	if ok {
400		id, ok := o.GetString("id")
401		if ok {
402			return id
403		}
404	}
405	arr, _ := obj.GetArray("attributedTo")
406	for _, a := range arr {
407		o, ok := a.(junk.Junk)
408		if ok {
409			t, _ := o.GetString("type")
410			id, _ := o.GetString("id")
411			if t == "Person" || t == "" {
412				return id
413			}
414		}
415	}
416	return ""
417}
418
419func consumeactivity(user *WhatAbout, j junk.Junk, origin string) {
420	xonk := xonkxonk(user, j, origin)
421	if xonk != nil {
422		savexonk(user, xonk)
423	}
424}
425
426func xonkxonk(user *WhatAbout, item junk.Junk, origin string) *Honk {
427	depth := 0
428	maxdepth := 10
429	currenttid := ""
430	var xonkxonkfn func(item junk.Junk, origin string) *Honk
431
432	saveoneup := func(xid string) {
433		log.Printf("getting oneup: %s", xid)
434		if depth >= maxdepth {
435			log.Printf("in too deep")
436			return
437		}
438		obj, err := GetJunk(xid)
439		if err != nil {
440			log.Printf("error getting oneup: %s", err)
441			return
442		}
443		depth++
444		xonk := xonkxonkfn(obj, originate(xid))
445		if xonk != nil {
446			savexonk(user, xonk)
447		}
448		depth--
449	}
450
451	xonkxonkfn = func(item junk.Junk, origin string) *Honk {
452		// id, _ := item.GetString( "id")
453		what, _ := item.GetString("type")
454		dt, _ := item.GetString("published")
455
456		var audience []string
457		var err error
458		var xid, rid, url, content, precis, convoy, oonker string
459		var obj junk.Junk
460		var ok bool
461		switch what {
462		case "Announce":
463			obj, ok = item.GetMap("object")
464			if ok {
465				xid, _ = obj.GetString("id")
466			} else {
467				xid, _ = item.GetString("object")
468			}
469			if !needxonkid(user, xid) {
470				return nil
471			}
472			log.Printf("getting bonk: %s", xid)
473			obj, err = GetJunk(xid)
474			if err != nil {
475				log.Printf("error regetting: %s", err)
476			}
477			origin = originate(xid)
478			what = "bonk"
479		case "Create":
480			obj, ok = item.GetMap("object")
481			if !ok {
482				xid, _ = obj.GetString("object")
483				log.Printf("getting created honk: %s", xid)
484				obj, err = GetJunk(xid)
485				if err != nil {
486					log.Printf("error getting creation: %s", err)
487				}
488			}
489			what = "honk"
490		case "Delete":
491			obj, _ = item.GetMap("object")
492			xid, _ = item.GetString("object")
493			what = "eradicate"
494		case "Video":
495			fallthrough
496		case "Question":
497			fallthrough
498		case "Note":
499			fallthrough
500		case "Article":
501			fallthrough
502		case "Page":
503			obj = item
504			what = "honk"
505		default:
506			log.Printf("unknown activity: %s", what)
507			fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
508			item.Write(fd)
509			io.WriteString(fd, "\n")
510			fd.Close()
511			return nil
512		}
513
514		var xonk Honk
515		who, _ := item.GetString("actor")
516		if obj != nil {
517			if who == "" {
518				who = extractattrto(obj)
519			}
520			oonker = extractattrto(obj)
521			ot, _ := obj.GetString("type")
522			url, _ = obj.GetString("url")
523			if ot == "Tombstone" {
524				xid, _ = obj.GetString("id")
525			} else {
526				audience = newphone(audience, obj)
527				xid, _ = obj.GetString("id")
528				precis, _ = obj.GetString("summary")
529				if precis == "" {
530					precis, _ = obj.GetString("name")
531				}
532				content, _ = obj.GetString("content")
533				if !strings.HasPrefix(content, "<p>") {
534					content = "<p>" + content
535				}
536				sens, _ := obj["sensitive"].(bool)
537				if sens && precis == "" {
538					precis = "unspecified horror"
539				}
540				rid, ok = obj.GetString("inReplyTo")
541				if !ok {
542					robj, ok := obj.GetMap("inReplyTo")
543					if ok {
544						rid, _ = robj.GetString("id")
545					}
546				}
547				convoy, _ = obj.GetString("context")
548				if convoy == "" {
549					convoy, _ = obj.GetString("conversation")
550				}
551				if ot == "Question" {
552					if what == "honk" {
553						what = "qonk"
554					}
555					content += "<ul>"
556					ans, _ := obj.GetArray("oneOf")
557					for _, ai := range ans {
558						a, ok := ai.(junk.Junk)
559						if !ok {
560							continue
561						}
562						as, _ := a.GetString("name")
563						content += "<li>" + as
564					}
565					ans, _ = obj.GetArray("anyOf")
566					for _, ai := range ans {
567						a, ok := ai.(junk.Junk)
568						if !ok {
569							continue
570						}
571						as, _ := a.GetString("name")
572						content += "<li>" + as
573					}
574					content += "</ul>"
575				}
576				if what == "honk" && rid != "" {
577					what = "tonk"
578				}
579			}
580			atts, _ := obj.GetArray("attachment")
581			for i, atti := range atts {
582				att, ok := atti.(junk.Junk)
583				if !ok {
584					continue
585				}
586				at, _ := att.GetString("type")
587				mt, _ := att.GetString("mediaType")
588				u, _ := att.GetString("url")
589				name, _ := att.GetString("name")
590				localize := false
591				if i > 4 {
592					log.Printf("excessive attachment: %s", at)
593				} else if at == "Document" || at == "Image" {
594					mt = strings.ToLower(mt)
595					log.Printf("attachment: %s %s", mt, u)
596					if mt == "text/plain" || strings.HasPrefix(mt, "image") {
597						localize = true
598					}
599				} else {
600					log.Printf("unknown attachment: %s", at)
601				}
602				donk := savedonk(u, name, mt, localize)
603				if donk != nil {
604					xonk.Donks = append(xonk.Donks, donk)
605				}
606			}
607			tags, _ := obj.GetArray("tag")
608			for _, tagi := range tags {
609				tag, ok := tagi.(junk.Junk)
610				if !ok {
611					continue
612				}
613				tt, _ := tag.GetString("type")
614				name, _ := tag.GetString("name")
615				if tt == "Emoji" {
616					icon, _ := tag.GetMap("icon")
617					mt, _ := icon.GetString("mediaType")
618					if mt == "" {
619						mt = "image/png"
620					}
621					u, _ := icon.GetString("url")
622					donk := savedonk(u, name, mt, true)
623					if donk != nil {
624						xonk.Donks = append(xonk.Donks, donk)
625					}
626				}
627			}
628		}
629		if originate(xid) != origin {
630			log.Printf("original sin: %s <> %s", xid, origin)
631			item.Write(os.Stdout)
632			return nil
633		}
634		audience = append(audience, who)
635
636		audience = oneofakind(audience)
637
638		if currenttid == "" {
639			currenttid = convoy
640		}
641
642		if oonker == who {
643			oonker = ""
644		}
645		xonk.UserID = user.ID
646		xonk.What = what
647		xonk.Honker = who
648		xonk.XID = xid
649		xonk.RID = rid
650		xonk.Date, _ = time.Parse(time.RFC3339, dt)
651		xonk.URL = url
652		xonk.Noise = content
653		xonk.Precis = precis
654		xonk.Audience = audience
655		xonk.Oonker = oonker
656
657		if needxonk(user, &xonk) {
658			if rid != "" {
659				if needxonkid(user, rid) {
660					saveoneup(rid)
661				}
662			}
663			if convoy == "" {
664				convoy = currenttid
665			}
666			xonk.Convoy = convoy
667			return &xonk
668		}
669		return nil
670	}
671
672	return xonkxonkfn(item, origin)
673}
674
675func rubadubdub(user *WhatAbout, req junk.Junk) {
676	xid, _ := req.GetString("id")
677	actor, _ := req.GetString("actor")
678	j := junk.New()
679	j["@context"] = itiswhatitis
680	j["id"] = user.URL + "/dub/" + url.QueryEscape(xid)
681	j["type"] = "Accept"
682	j["actor"] = user.URL
683	j["to"] = actor
684	j["published"] = time.Now().UTC().Format(time.RFC3339)
685	j["object"] = req
686
687	var buf bytes.Buffer
688	j.Write(&buf)
689	msg := buf.Bytes()
690
691	deliverate(0, user.Name, actor, msg)
692}
693
694func itakeitallback(user *WhatAbout, xid string) {
695	j := junk.New()
696	j["@context"] = itiswhatitis
697	j["id"] = user.URL + "/unsub/" + url.QueryEscape(xid)
698	j["type"] = "Undo"
699	j["actor"] = user.URL
700	j["to"] = xid
701	f := junk.New()
702	f["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
703	f["type"] = "Follow"
704	f["actor"] = user.URL
705	f["to"] = xid
706	f["object"] = xid
707	j["object"] = f
708	j["published"] = time.Now().UTC().Format(time.RFC3339)
709
710	var buf bytes.Buffer
711	j.Write(&buf)
712	msg := buf.Bytes()
713
714	deliverate(0, user.Name, xid, msg)
715}
716
717func subsub(user *WhatAbout, xid string) {
718	j := junk.New()
719	j["@context"] = itiswhatitis
720	j["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
721	j["type"] = "Follow"
722	j["actor"] = user.URL
723	j["to"] = xid
724	j["object"] = xid
725	j["published"] = time.Now().UTC().Format(time.RFC3339)
726
727	var buf bytes.Buffer
728	j.Write(&buf)
729	msg := buf.Bytes()
730
731	deliverate(0, user.Name, xid, msg)
732}
733
734func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
735	dt := h.Date.Format(time.RFC3339)
736	var jo junk.Junk
737	j := junk.New()
738	j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
739	j["actor"] = user.URL
740	j["published"] = dt
741	if h.Public {
742		j["to"] = []string{h.Audience[0], user.URL + "/followers"}
743	} else {
744		j["to"] = h.Audience[0]
745	}
746	if len(h.Audience) > 1 {
747		j["cc"] = h.Audience[1:]
748	}
749
750	switch h.What {
751	case "tonk":
752		fallthrough
753	case "honk":
754		j["type"] = "Create"
755
756		jo = junk.New()
757		jo["id"] = h.XID
758		jo["type"] = "Note"
759		jo["published"] = dt
760		jo["url"] = h.XID
761		jo["attributedTo"] = user.URL
762		if h.RID != "" {
763			jo["inReplyTo"] = h.RID
764		}
765		if h.Convoy != "" {
766			jo["context"] = h.Convoy
767			jo["conversation"] = h.Convoy
768		}
769		jo["to"] = h.Audience[0]
770		if len(h.Audience) > 1 {
771			jo["cc"] = h.Audience[1:]
772		}
773		if !h.Public {
774			jo["directMessage"] = true
775		}
776		jo["summary"] = h.Precis
777		jo["content"] = mentionize(h.Noise)
778		if strings.HasPrefix(h.Precis, "DZ:") {
779			jo["sensitive"] = true
780		}
781
782		var tags []junk.Junk
783		g := bunchofgrapes(h.Noise)
784		for _, m := range g {
785			t := junk.New()
786			t["type"] = "Mention"
787			t["name"] = m.who
788			t["href"] = m.where
789			tags = append(tags, t)
790		}
791		ooo := ontologies(h.Noise)
792		for _, o := range ooo {
793			t := junk.New()
794			t["type"] = "Hashtag"
795			t["name"] = o
796			tags = append(tags, t)
797		}
798		herd := herdofemus(h.Noise)
799		for _, e := range herd {
800			t := junk.New()
801			t["id"] = e.ID
802			t["type"] = "Emoji"
803			t["name"] = e.Name
804			i := junk.New()
805			i["type"] = "Image"
806			i["mediaType"] = "image/png"
807			i["url"] = e.ID
808			t["icon"] = i
809			tags = append(tags, t)
810		}
811		if len(tags) > 0 {
812			jo["tag"] = tags
813		}
814		var atts []junk.Junk
815		for _, d := range h.Donks {
816			if re_emus.MatchString(d.Name) {
817				continue
818			}
819			jd := junk.New()
820			jd["mediaType"] = d.Media
821			jd["name"] = d.Name
822			jd["type"] = "Document"
823			jd["url"] = d.URL
824			atts = append(atts, jd)
825		}
826		if len(atts) > 0 {
827			jo["attachment"] = atts
828		}
829		j["object"] = jo
830	case "bonk":
831		j["type"] = "Announce"
832		if h.Convoy != "" {
833			j["context"] = h.Convoy
834		}
835		j["object"] = h.XID
836	case "zonk":
837		j["type"] = "Delete"
838		j["object"] = h.XID
839	}
840
841	return j, jo
842}
843
844func honkworldwide(user *WhatAbout, honk *Honk) {
845	jonk, _ := jonkjonk(user, honk)
846	jonk["@context"] = itiswhatitis
847	var buf bytes.Buffer
848	jonk.Write(&buf)
849	msg := buf.Bytes()
850
851	rcpts := make(map[string]bool)
852	for _, a := range honk.Audience {
853		if a != thewholeworld && a != user.URL && !strings.HasSuffix(a, "/followers") {
854			box, _ := getboxes(a)
855			if box != nil && honk.Public && box.Shared != "" {
856				rcpts["%"+box.Shared] = true
857			} else {
858				rcpts[a] = true
859			}
860		}
861	}
862	if honk.Public {
863		for _, f := range getdubs(user.ID) {
864			box, _ := getboxes(f.XID)
865			if box != nil && box.Shared != "" {
866				rcpts["%"+box.Shared] = true
867			} else {
868				rcpts[f.XID] = true
869			}
870		}
871	}
872	for a := range rcpts {
873		go deliverate(0, user.Name, a, msg)
874	}
875}
876
877func asjonker(user *WhatAbout) junk.Junk {
878	about := obfusbreak(user.About)
879
880	j := junk.New()
881	j["@context"] = itiswhatitis
882	j["id"] = user.URL
883	j["type"] = "Person"
884	j["inbox"] = user.URL + "/inbox"
885	j["outbox"] = user.URL + "/outbox"
886	j["followers"] = user.URL + "/followers"
887	j["following"] = user.URL + "/following"
888	j["name"] = user.Display
889	j["preferredUsername"] = user.Name
890	j["summary"] = about
891	j["url"] = user.URL
892	a := junk.New()
893	a["type"] = "Image"
894	a["mediaType"] = "image/png"
895	a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
896	j["icon"] = a
897	k := junk.New()
898	k["id"] = user.URL + "#key"
899	k["owner"] = user.URL
900	k["publicKeyPem"] = user.Key
901	j["publicKey"] = k
902
903	return j
904}
905
906var handfull = make(map[string]string)
907var handlock sync.Mutex
908
909func gofish(name string) string {
910	if name[0] == '@' {
911		name = name[1:]
912	}
913	m := strings.Split(name, "@")
914	if len(m) != 2 {
915		log.Printf("bad fish name: %s", name)
916		return ""
917	}
918	handlock.Lock()
919	ref, ok := handfull[name]
920	handlock.Unlock()
921	if ok {
922		return ref
923	}
924	row := stmtGetXonker.QueryRow(name, "fishname")
925	var href string
926	err := row.Scan(&href)
927	if err == nil {
928		handlock.Lock()
929		handfull[name] = href
930		handlock.Unlock()
931		return href
932	}
933	log.Printf("fishing for %s", name)
934	j, err := GetJunkFast(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
935	if err != nil {
936		log.Printf("failed to go fish %s: %s", name, err)
937		handlock.Lock()
938		handfull[name] = ""
939		handlock.Unlock()
940		return ""
941	}
942	links, _ := j.GetArray("links")
943	for _, li := range links {
944		l, ok := li.(junk.Junk)
945		if !ok {
946			continue
947		}
948		href, _ := l.GetString("href")
949		rel, _ := l.GetString("rel")
950		t, _ := l.GetString("type")
951		if rel == "self" && friendorfoe(t) {
952			stmtSaveXonker.Exec(name, href, "fishname")
953			handlock.Lock()
954			handfull[name] = href
955			handlock.Unlock()
956			return href
957		}
958	}
959	handlock.Lock()
960	handfull[name] = ""
961	handlock.Unlock()
962	return ""
963}
964
965func isactor(t string) bool {
966	switch t {
967	case "Person":
968	case "Application":
969	case "Service":
970	default:
971		return false
972	}
973	return true
974}
975
976func investigate(name string) string {
977	if name == "" {
978		return ""
979	}
980	if name[0] == '@' {
981		name = gofish(name)
982	}
983	if name == "" {
984		return ""
985	}
986	obj, err := GetJunkFast(name)
987	if err != nil {
988		log.Printf("error investigating honker: %s", err)
989		return ""
990	}
991	t, _ := obj.GetString("type")
992	if !isactor(t) {
993		log.Printf("it's not a person! %s", name)
994		return ""
995	}
996	id, _ := obj.GetString("id")
997	return id
998}