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