all repos — honk @ ef338d73e2266849afc566b398a892e1c4a3f980

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