all repos — honk @ 7c7290b1d463d015e736724a7994408053b35cb5

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