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 "encoding/json"
24 "fmt"
25 "image"
26 "io"
27 "log"
28 "net/http"
29 "net/url"
30 "os"
31 "strconv"
32 "strings"
33 "sync"
34 "time"
35)
36
37func NewJunk() map[string]interface{} {
38 return make(map[string]interface{})
39}
40
41func WriteJunk(w io.Writer, j map[string]interface{}) error {
42 e := json.NewEncoder(w)
43 e.SetEscapeHTML(false)
44 e.SetIndent("", " ")
45 err := e.Encode(j)
46 return err
47}
48
49func ReadJunk(r io.Reader) (map[string]interface{}, error) {
50 decoder := json.NewDecoder(r)
51 var j map[string]interface{}
52 err := decoder.Decode(&j)
53 if err != nil {
54 return nil, err
55 }
56 return j, nil
57}
58
59var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
60var thefakename = `application/activity+json`
61var falsenames = []string{
62 `application/ld+json`,
63 `application/activity+json`,
64}
65var itiswhatitis = "https://www.w3.org/ns/activitystreams"
66var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
67
68func friendorfoe(ct string) bool {
69 ct = strings.ToLower(ct)
70 for _, at := range falsenames {
71 if strings.HasPrefix(ct, at) {
72 return true
73 }
74 }
75 return false
76}
77
78func PostJunk(keyname string, key *rsa.PrivateKey, url string, j map[string]interface{}) error {
79 var buf bytes.Buffer
80 WriteJunk(&buf, j)
81 return PostMsg(keyname, key, url, buf.Bytes())
82}
83
84func PostMsg(keyname string, key *rsa.PrivateKey, url string, msg []byte) error {
85 client := http.DefaultClient
86 req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
87 if err != nil {
88 return err
89 }
90 req.Header.Set("Content-Type", theonetruename)
91 zig(keyname, key, req, msg)
92 resp, err := client.Do(req)
93 if err != nil {
94 return err
95 }
96 resp.Body.Close()
97 switch resp.StatusCode {
98 case 200:
99 case 201:
100 case 202:
101 default:
102 return fmt.Errorf("http post status: %d", resp.StatusCode)
103 }
104 log.Printf("successful post: %s %d", url, resp.StatusCode)
105 return nil
106}
107
108type gzCloser struct {
109 r *gzip.Reader
110 under io.ReadCloser
111}
112
113func (gz *gzCloser) Read(p []byte) (int, error) {
114 return gz.r.Read(p)
115}
116
117func (gz *gzCloser) Close() error {
118 defer gz.under.Close()
119 return gz.r.Close()
120}
121
122func GetJunk(url string) (map[string]interface{}, error) {
123 client := http.DefaultClient
124 req, err := http.NewRequest("GET", url, nil)
125 if err != nil {
126 return nil, err
127 }
128 at := thefakename
129 if strings.Contains(url, ".well-known/webfinger?resource") {
130 at = "application/jrd+json"
131 }
132 req.Header.Set("Accept", at)
133 req.Header.Set("Accept-Encoding", "gzip")
134 resp, err := client.Do(req)
135 if err != nil {
136 return nil, err
137 }
138 if resp.StatusCode != 200 {
139 resp.Body.Close()
140 return nil, fmt.Errorf("http get status: %d", resp.StatusCode)
141 }
142 if strings.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") {
143 gz, err := gzip.NewReader(resp.Body)
144 if err != nil {
145 resp.Body.Close()
146 return nil, err
147 }
148 resp.Body = &gzCloser{r: gz, under: resp.Body}
149 }
150 defer resp.Body.Close()
151 j, err := ReadJunk(resp.Body)
152 return j, err
153}
154
155func jsonfindinterface(ii interface{}, keys []string) interface{} {
156 for _, key := range keys {
157 idx, err := strconv.Atoi(key)
158 if err == nil {
159 m := ii.([]interface{})
160 if idx >= len(m) {
161 return nil
162 }
163 ii = m[idx]
164 } else {
165 m := ii.(map[string]interface{})
166 ii = m[key]
167 if ii == nil {
168 return nil
169 }
170 }
171 }
172 return ii
173}
174func jsonfindstring(j interface{}, keys []string) (string, bool) {
175 s, ok := jsonfindinterface(j, keys).(string)
176 return s, ok
177}
178func jsonfindarray(j interface{}, keys []string) ([]interface{}, bool) {
179 a, ok := jsonfindinterface(j, keys).([]interface{})
180 return a, ok
181}
182func jsonfindmap(j interface{}, keys []string) (map[string]interface{}, bool) {
183 m, ok := jsonfindinterface(j, keys).(map[string]interface{})
184 return m, ok
185}
186func jsongetstring(j interface{}, key string) (string, bool) {
187 return jsonfindstring(j, []string{key})
188}
189func jsongetarray(j interface{}, key string) ([]interface{}, bool) {
190 return jsonfindarray(j, []string{key})
191}
192func jsongetmap(j interface{}, key string) (map[string]interface{}, bool) {
193 return jsonfindmap(j, []string{key})
194}
195
196func savedonk(url string, name, media string) *Donk {
197 var donk Donk
198 row := stmtFindFile.QueryRow(url)
199 err := row.Scan(&donk.FileID)
200 if err == nil {
201 return &donk
202 }
203 log.Printf("saving donk: %s", url)
204 if err != nil && err != sql.ErrNoRows {
205 log.Printf("error querying: %s", err)
206 }
207 resp, err := http.Get(url)
208 if err != nil {
209 log.Printf("error fetching %s: %s", url, err)
210 return nil
211 }
212 defer resp.Body.Close()
213 if resp.StatusCode != 200 {
214 return nil
215 }
216 var buf bytes.Buffer
217 io.Copy(&buf, resp.Body)
218
219 xid := xfiltrate()
220
221 data := buf.Bytes()
222 if strings.HasPrefix(media, "image") {
223 img, format, err := image.Decode(&buf)
224 if err != nil {
225 log.Printf("unable to decode image: %s", err)
226 return nil
227 }
228 data, format, err = vacuumwrap(img, format)
229 media = "image/" + format
230 }
231 res, err := stmtSaveFile.Exec(xid, name, url, media, data)
232 if err != nil {
233 log.Printf("error saving file %s: %s", url, err)
234 return nil
235 }
236 donk.FileID, _ = res.LastInsertId()
237 return &donk
238}
239
240func needxonk(user *WhatAbout, x *Honk) bool {
241 if x == nil {
242 return false
243 }
244 if x.What == "eradicate" {
245 return true
246 }
247 if thoudostbitethythumb(user.ID, x.Audience, x.XID) {
248 log.Printf("not saving thumb biter? %s", x.Honker)
249 return false
250 }
251 return needxonkid(user, x.XID)
252}
253func needxonkid(user *WhatAbout, xid string) bool {
254 if strings.HasPrefix(xid, user.URL+"/h/") {
255 return false
256 }
257 row := stmtFindXonk.QueryRow(user.ID, xid)
258 var id int64
259 err := row.Scan(&id)
260 if err == nil {
261 return false
262 }
263 if err != sql.ErrNoRows {
264 log.Printf("err querying xonk: %s", err)
265 }
266 return true
267}
268
269func savexonk(user *WhatAbout, x *Honk) {
270 if x.What == "eradicate" {
271 log.Printf("eradicating %s by %s", x.RID, x.Honker)
272 _, err := stmtDeleteHonk.Exec(x.RID, x.Honker, user.ID)
273 if err != nil {
274 log.Printf("error eradicating: %s", err)
275 }
276 return
277 }
278 dt := x.Date.UTC().Format(dbtimeformat)
279 aud := strings.Join(x.Audience, " ")
280 whofore := 0
281 if strings.Contains(aud, user.URL) {
282 whofore = 1
283 }
284 res, err := stmtSaveHonk.Exec(x.UserID, x.What, x.Honker, x.XID, x.RID, dt, x.URL, aud,
285 x.Noise, x.Convoy, whofore)
286 if err != nil {
287 log.Printf("err saving xonk: %s", err)
288 return
289 }
290 x.ID, _ = res.LastInsertId()
291 for _, d := range x.Donks {
292 _, err = stmtSaveDonk.Exec(x.ID, d.FileID)
293 if err != nil {
294 log.Printf("err saving donk: %s", err)
295 return
296 }
297 }
298}
299
300type Box struct {
301 In string
302 Out string
303 Shared string
304}
305
306var boxofboxes = make(map[string]*Box)
307var boxlock sync.Mutex
308var boxinglock sync.Mutex
309
310func getboxes(ident string) (*Box, error) {
311 boxlock.Lock()
312 b, ok := boxofboxes[ident]
313 boxlock.Unlock()
314 if ok {
315 return b, nil
316 }
317
318 boxinglock.Lock()
319 defer boxinglock.Unlock()
320
321 boxlock.Lock()
322 b, ok = boxofboxes[ident]
323 boxlock.Unlock()
324 if ok {
325 return b, nil
326 }
327
328 db := opendatabase()
329
330 row := db.QueryRow("select ibox, obox, sbox from xonkers where xid = ?", ident)
331 b = &Box{}
332 err := row.Scan(&b.In, &b.Out, &b.Shared)
333 if err != nil {
334 j, err := GetJunk(ident)
335 if err != nil {
336 return nil, err
337 }
338 inbox, _ := jsongetstring(j, "inbox")
339 outbox, _ := jsongetstring(j, "outbox")
340 sbox, _ := jsonfindstring(j, []string{"endpoints", "sharedInbox"})
341 b = &Box{In: inbox, Out: outbox, Shared: sbox}
342 if inbox != "" {
343 db.Exec("insert into xonkers (xid, ibox, obox, sbox, pubkey) values (?, ?, ?, ?, ?)",
344 ident, inbox, outbox, sbox, "")
345 }
346 }
347 boxlock.Lock()
348 boxofboxes[ident] = b
349 boxlock.Unlock()
350 return b, nil
351}
352
353func peeppeep() {
354 user, _ := butwhatabout("htest")
355 honkers := gethonkers(user.ID)
356 for _, f := range honkers {
357 if f.Flavor != "peep" {
358 continue
359 }
360 log.Printf("getting updates: %s", f.XID)
361 box, err := getboxes(f.XID)
362 if err != nil {
363 log.Printf("error getting outbox: %s", err)
364 continue
365 }
366 log.Printf("getting outbox")
367 j, err := GetJunk(box.Out)
368 if err != nil {
369 log.Printf("err: %s", err)
370 continue
371 }
372 t, _ := jsongetstring(j, "type")
373 if t == "OrderedCollection" {
374 items, _ := jsongetarray(j, "orderedItems")
375 if items == nil {
376 page1, _ := jsongetstring(j, "first")
377 j, err = GetJunk(page1)
378 if err != nil {
379 log.Printf("err: %s", err)
380 continue
381 }
382 items, _ = jsongetarray(j, "orderedItems")
383 }
384
385 for _, item := range items {
386 xonk := xonkxonk(user, item)
387 if needxonk(user, xonk) {
388 savexonk(user, xonk)
389 }
390 }
391 }
392 }
393}
394
395func whosthere(xid string) ([]string, string) {
396 obj, err := GetJunk(xid)
397 if err != nil {
398 log.Printf("error getting remote xonk: %s", err)
399 return nil, ""
400 }
401 convoy, _ := jsongetstring(obj, "context")
402 if convoy == "" {
403 convoy, _ = jsongetstring(obj, "conversation")
404 }
405 return newphone(nil, obj), convoy
406}
407
408func newphone(a []string, obj map[string]interface{}) []string {
409 for _, addr := range []string{"to", "cc", "attributedTo"} {
410 who, _ := jsongetstring(obj, addr)
411 if who != "" {
412 a = append(a, who)
413 }
414 whos, _ := jsongetarray(obj, addr)
415 for _, w := range whos {
416 who, _ := w.(string)
417 if who != "" {
418 a = append(a, who)
419 }
420 }
421 }
422 return a
423}
424
425func xonkxonk(user *WhatAbout, item interface{}) *Honk {
426 depth := 0
427 maxdepth := 4
428 var xonkxonkfn func(item interface{}) *Honk
429
430 saveoneup := func(xid string) {
431 log.Printf("getting oneup: %s", xid)
432 if depth >= maxdepth {
433 log.Printf("in too deep")
434 return
435 }
436 obj, err := GetJunk(xid)
437 if err != nil {
438 log.Printf("error getting oneup: %s", err)
439 return
440 }
441 depth++
442 xonk := xonkxonkfn(obj)
443 if needxonk(user, xonk) {
444 xonk.UserID = user.ID
445 savexonk(user, xonk)
446 }
447 depth--
448 }
449
450 xonkxonkfn = func(item interface{}) *Honk {
451 // id, _ := jsongetstring(item, "id")
452 what, _ := jsongetstring(item, "type")
453 dt, _ := jsongetstring(item, "published")
454
455 var audience []string
456 var err error
457 var xid, rid, url, content, convoy string
458 var obj map[string]interface{}
459 var ok bool
460 switch what {
461 case "Announce":
462 xid, ok = jsongetstring(item, "object")
463 if ok {
464 if !needxonkid(user, xid) {
465 return nil
466 }
467 log.Printf("getting bonk: %s", xid)
468 obj, err = GetJunk(xid)
469 if err != nil {
470 log.Printf("error regetting: %s", err)
471 }
472 } else {
473 obj, _ = jsongetmap(item, "object")
474 }
475 what = "bonk"
476 case "Create":
477 obj, _ = jsongetmap(item, "object")
478 what = "honk"
479 case "Note":
480 obj = item.(map[string]interface{})
481 what = "honk"
482 case "Delete":
483 obj, _ = jsongetmap(item, "object")
484 rid, _ = jsongetstring(item, "object")
485 what = "eradicate"
486 default:
487 log.Printf("unknown activity: %s", what)
488 return nil
489 }
490
491 var xonk Honk
492 who, _ := jsongetstring(item, "actor")
493 if obj != nil {
494 if who == "" {
495 who, _ = jsongetstring(obj, "attributedTo")
496 }
497 ot, _ := jsongetstring(obj, "type")
498 url, _ = jsongetstring(obj, "url")
499 if ot == "Note" || ot == "Article" {
500 audience = newphone(audience, obj)
501 xid, _ = jsongetstring(obj, "id")
502 content, _ = jsongetstring(obj, "content")
503 summary, _ := jsongetstring(obj, "summary")
504 if !strings.HasPrefix(content, "<p>") {
505 content = "<p>" + content
506 }
507 if summary != "" {
508 content = "<p>summary: " + summary + content
509 }
510 rid, _ = jsongetstring(obj, "inReplyTo")
511 convoy, _ = jsongetstring(obj, "context")
512 if convoy == "" {
513 convoy, _ = jsongetstring(obj, "conversation")
514 }
515 if what == "honk" && rid != "" {
516 what = "tonk"
517 if needxonkid(user, rid) {
518 saveoneup(rid)
519 }
520 }
521 }
522 if ot == "Tombstone" {
523 rid, _ = jsongetstring(obj, "id")
524 }
525 atts, _ := jsongetarray(obj, "attachment")
526 for _, att := range atts {
527 at, _ := jsongetstring(att, "type")
528 mt, _ := jsongetstring(att, "mediaType")
529 u, _ := jsongetstring(att, "url")
530 name, _ := jsongetstring(att, "name")
531 if at == "Document" {
532 mt = strings.ToLower(mt)
533 log.Printf("attachment: %s %s", mt, u)
534 if mt == "image/jpeg" || mt == "image/png" ||
535 mt == "text/plain" {
536 donk := savedonk(u, name, mt)
537 if donk != nil {
538 xonk.Donks = append(xonk.Donks, donk)
539 }
540 }
541 }
542 }
543 tags, _ := jsongetarray(obj, "tag")
544 for _, tag := range tags {
545 tt, _ := jsongetstring(tag, "type")
546 name, _ := jsongetstring(tag, "name")
547 if tt == "Emoji" {
548 icon, _ := jsongetmap(tag, "icon")
549 mt, _ := jsongetstring(icon, "mediaType")
550 if mt == "" {
551 mt = "image/png"
552 }
553 u, _ := jsongetstring(icon, "url")
554 donk := savedonk(u, name, mt)
555 if donk != nil {
556 xonk.Donks = append(xonk.Donks, donk)
557 }
558 }
559 }
560 }
561 audience = append(audience, who)
562
563 audience = oneofakind(audience)
564
565 xonk.UserID = user.ID
566 xonk.What = what
567 xonk.Honker = who
568 xonk.XID = xid
569 xonk.RID = rid
570 xonk.Date, _ = time.Parse(time.RFC3339, dt)
571 xonk.URL = url
572 xonk.Noise = content
573 xonk.Audience = audience
574 xonk.Convoy = convoy
575
576 return &xonk
577 }
578
579 return xonkxonkfn(item)
580}
581
582func rubadubdub(user *WhatAbout, req map[string]interface{}) {
583 xid, _ := jsongetstring(req, "id")
584 reqactor, _ := jsongetstring(req, "actor")
585 j := NewJunk()
586 j["@context"] = itiswhatitis
587 j["id"] = user.URL + "/dub/" + xid
588 j["type"] = "Accept"
589 j["actor"] = user.URL
590 j["to"] = reqactor
591 j["published"] = time.Now().UTC().Format(time.RFC3339)
592 j["object"] = req
593
594 WriteJunk(os.Stdout, j)
595
596 actor, _ := jsongetstring(req, "actor")
597 box, err := getboxes(actor)
598 if err != nil {
599 log.Printf("can't get dub box: %s", err)
600 return
601 }
602 keyname, key := ziggy(user.Name)
603 err = PostJunk(keyname, key, box.In, j)
604 if err != nil {
605 log.Printf("can't rub a dub: %s", err)
606 return
607 }
608 stmtSaveDub.Exec(user.ID, actor, actor, "dub")
609}
610
611func itakeitallback(user *WhatAbout, xid string) error {
612 j := NewJunk()
613 j["@context"] = itiswhatitis
614 j["id"] = user.URL + "/unsub/" + xid
615 j["type"] = "Undo"
616 j["actor"] = user.URL
617 j["to"] = xid
618 f := NewJunk()
619 f["id"] = user.URL + "/sub/" + xid
620 f["type"] = "Follow"
621 f["actor"] = user.URL
622 f["to"] = xid
623 j["object"] = f
624 j["published"] = time.Now().UTC().Format(time.RFC3339)
625
626 box, err := getboxes(xid)
627 if err != nil {
628 return err
629 }
630 keyname, key := ziggy(user.Name)
631 err = PostJunk(keyname, key, box.In, j)
632 if err != nil {
633 return err
634 }
635 return nil
636}
637
638func subsub(user *WhatAbout, xid string) {
639 j := NewJunk()
640 j["@context"] = itiswhatitis
641 j["id"] = user.URL + "/sub/" + xid
642 j["type"] = "Follow"
643 j["actor"] = user.URL
644 j["to"] = xid
645 j["object"] = xid
646 j["published"] = time.Now().UTC().Format(time.RFC3339)
647
648 box, err := getboxes(xid)
649 if err != nil {
650 log.Printf("can't send follow: %s", err)
651 return
652 }
653 WriteJunk(os.Stdout, j)
654 keyname, key := ziggy(user.Name)
655 err = PostJunk(keyname, key, box.In, j)
656 if err != nil {
657 log.Printf("failed to subsub: %s", err)
658 }
659}
660
661func jonkjonk(user *WhatAbout, h *Honk) (map[string]interface{}, map[string]interface{}) {
662 dt := h.Date.Format(time.RFC3339)
663 var jo map[string]interface{}
664 j := NewJunk()
665 j["id"] = user.URL + "/" + h.What + "/" + h.XID
666 j["actor"] = user.URL
667 j["published"] = dt
668 j["to"] = h.Audience[0]
669 if len(h.Audience) > 1 {
670 j["cc"] = h.Audience[1:]
671 }
672
673 switch h.What {
674 case "zonk":
675 fallthrough
676 case "tonk":
677 fallthrough
678 case "honk":
679 j["type"] = "Create"
680 if h.What == "zonk" {
681 j["type"] = "Delete"
682 }
683
684 jo = NewJunk()
685 jo["id"] = user.URL + "/h/" + h.XID
686 jo["type"] = "Note"
687 if h.What == "zonk" {
688 jo["type"] = "Tombstone"
689 }
690 jo["published"] = dt
691 jo["url"] = user.URL + "/h/" + h.XID
692 jo["attributedTo"] = user.URL
693 if h.RID != "" {
694 jo["inReplyTo"] = h.RID
695 }
696 if h.Convoy != "" {
697 jo["context"] = h.Convoy
698 jo["conversation"] = h.Convoy
699 }
700 jo["to"] = h.Audience[0]
701 if len(h.Audience) > 1 {
702 jo["cc"] = h.Audience[1:]
703 }
704 jo["content"] = mentionize(h.Noise)
705 jo["summary"] = nil
706 var tags []interface{}
707 g := bunchofgrapes(h.Noise)
708 for _, m := range g {
709 t := NewJunk()
710 t["type"] = "Mention"
711 t["name"] = m.who
712 t["href"] = m.where
713 tags = append(tags, t)
714 }
715 herd := herdofemus(h.Noise)
716 for _, e := range herd {
717 t := NewJunk()
718 t["id"] = e.ID
719 t["type"] = "Emoji"
720 t["name"] = e.Name
721 i := NewJunk()
722 i["type"] = "Image"
723 i["mediaType"] = "image/png"
724 i["url"] = e.ID
725 t["icon"] = i
726 tags = append(tags, t)
727 }
728 if len(tags) > 0 {
729 jo["tag"] = tags
730 }
731 var atts []interface{}
732 for _, d := range h.Donks {
733 if re_emus.MatchString(d.Name) {
734 continue
735 }
736 jd := NewJunk()
737 jd["mediaType"] = d.Media
738 jd["name"] = d.Name
739 jd["type"] = "Document"
740 jd["url"] = d.URL
741 atts = append(atts, jd)
742 }
743 if len(atts) > 0 {
744 jo["attachment"] = atts
745 }
746 j["object"] = jo
747 case "bonk":
748 j["type"] = "Announce"
749 j["object"] = h.XID
750 }
751
752 return j, jo
753}
754
755func honkworldwide(user *WhatAbout, honk *Honk) {
756 jonk, _ := jonkjonk(user, honk)
757 jonk["@context"] = itiswhatitis
758 var buf bytes.Buffer
759 WriteJunk(&buf, jonk)
760 msg := buf.Bytes()
761
762 rcpts := make(map[string]bool)
763 for _, a := range honk.Audience {
764 if a != thewholeworld && a != user.URL && !strings.HasSuffix(a, "/followers") {
765 box, _ := getboxes(a)
766 if box != nil && box.Shared != "" {
767 rcpts["%"+box.Shared] = true
768 } else {
769 rcpts[a] = true
770 }
771 }
772 }
773 for _, f := range getdubs(user.ID) {
774 box, _ := getboxes(f.XID)
775 if box != nil && box.Shared != "" {
776 rcpts["%"+box.Shared] = true
777 } else {
778 rcpts[f.XID] = true
779 }
780 }
781 for a := range rcpts {
782 go deliverate(0, user.Name, a, msg)
783 }
784}
785
786func asjonker(user *WhatAbout) map[string]interface{} {
787 about := obfusbreak(user.About)
788
789 j := NewJunk()
790 j["@context"] = itiswhatitis
791 j["id"] = user.URL
792 j["type"] = "Person"
793 j["inbox"] = user.URL + "/inbox"
794 j["outbox"] = user.URL + "/outbox"
795 j["followers"] = user.URL + "/followers"
796 j["following"] = user.URL + "/following"
797 j["name"] = user.Display
798 j["preferredUsername"] = user.Name
799 j["summary"] = about
800 j["url"] = user.URL
801 a := NewJunk()
802 a["type"] = "icon"
803 a["mediaType"] = "image/png"
804 a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
805 j["icon"] = a
806 k := NewJunk()
807 k["id"] = user.URL + "#key"
808 k["owner"] = user.URL
809 k["publicKeyPem"] = user.Key
810 j["publicKey"] = k
811
812 return j
813}