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 "database/sql"
21 "errors"
22 "fmt"
23 "html"
24 "io"
25 "log"
26 notrand "math/rand"
27 "net/http"
28 "net/url"
29 "os"
30 "regexp"
31 "strings"
32 "time"
33
34 "humungus.tedunangst.com/r/webs/cache"
35 "humungus.tedunangst.com/r/webs/gate"
36 "humungus.tedunangst.com/r/webs/httpsig"
37 "humungus.tedunangst.com/r/webs/junk"
38 "humungus.tedunangst.com/r/webs/templates"
39)
40
41var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
42var thefakename = `application/activity+json`
43var falsenames = []string{
44 `application/ld+json`,
45 `application/activity+json`,
46}
47var itiswhatitis = "https://www.w3.org/ns/activitystreams"
48var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
49
50func friendorfoe(ct string) bool {
51 ct = strings.ToLower(ct)
52 for _, at := range falsenames {
53 if strings.HasPrefix(ct, at) {
54 return true
55 }
56 }
57 return false
58}
59
60func PostJunk(keyname string, key httpsig.PrivateKey, url string, j junk.Junk) error {
61 return PostMsg(keyname, key, url, j.ToBytes())
62}
63
64func PostMsg(keyname string, key httpsig.PrivateKey, url string, msg []byte) error {
65 client := http.DefaultClient
66 req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
67 if err != nil {
68 return err
69 }
70 req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
71 req.Header.Set("Content-Type", theonetruename)
72 httpsig.SignRequest(keyname, key, req, msg)
73 resp, err := client.Do(req)
74 if err != nil {
75 return err
76 }
77 resp.Body.Close()
78 switch resp.StatusCode {
79 case 200:
80 case 201:
81 case 202:
82 default:
83 return fmt.Errorf("http post status: %d", resp.StatusCode)
84 }
85 log.Printf("successful post: %s %d", url, resp.StatusCode)
86 return nil
87}
88
89type JunkError struct {
90 Junk junk.Junk
91 Err error
92}
93
94func GetJunk(url string) (junk.Junk, error) {
95 return GetJunkTimeout(url, 30*time.Second)
96}
97
98func GetJunkFast(url string) (junk.Junk, error) {
99 return GetJunkTimeout(url, 5*time.Second)
100}
101
102func GetJunkHardMode(url string) (junk.Junk, error) {
103 j, err := GetJunk(url)
104 if err != nil {
105 emsg := err.Error()
106 if emsg == "http get status: 502" || strings.Contains(emsg, "timeout") {
107 log.Printf("trying again after error: %s", emsg)
108 time.Sleep(time.Duration(60+notrand.Int63n(60)) * time.Second)
109 j, err = GetJunk(url)
110 if err != nil {
111 log.Printf("still couldn't get it")
112 } else {
113 log.Printf("retry success!")
114 }
115 }
116 }
117 return j, err
118}
119
120var flightdeck = gate.NewSerializer()
121
122func GetJunkTimeout(url string, timeout time.Duration) (junk.Junk, error) {
123
124 fn := func() (interface{}, error) {
125 at := thefakename
126 if strings.Contains(url, ".well-known/webfinger?resource") {
127 at = "application/jrd+json"
128 }
129 j, err := junk.Get(url, junk.GetArgs{
130 Accept: at,
131 Agent: "honksnonk/5.0; " + serverName,
132 Timeout: timeout,
133 })
134 return j, err
135 }
136
137 ji, err := flightdeck.Call(url, fn)
138 if err != nil {
139 return nil, err
140 }
141 j := ji.(junk.Junk)
142 return j, nil
143}
144
145func fetchsome(url string) ([]byte, error) {
146 resp, err := http.Get(url)
147 if err != nil {
148 log.Printf("error fetching %s: %s", url, err)
149 return nil, err
150 }
151 defer resp.Body.Close()
152 if resp.StatusCode != 200 {
153 return nil, errors.New("not 200")
154 }
155 var buf bytes.Buffer
156 limiter := io.LimitReader(resp.Body, 10*1024*1024)
157 io.Copy(&buf, limiter)
158 return buf.Bytes(), nil
159}
160
161func savedonk(url string, name, desc, media string, localize bool) *Donk {
162 if url == "" {
163 return nil
164 }
165 donk := finddonk(url)
166 if donk != nil {
167 return donk
168 }
169 donk = new(Donk)
170 log.Printf("saving donk: %s", url)
171 xid := xfiltrate()
172 data := []byte{}
173 if localize {
174 fn := func() (interface{}, error) {
175 return fetchsome(url)
176 }
177 ii, err := flightdeck.Call(url, fn)
178 if err != nil {
179 log.Printf("error fetching donk: %s", err)
180 localize = false
181 goto saveit
182 }
183 data = ii.([]byte)
184
185 if len(data) == 10*1024*1024 {
186 log.Printf("truncation likely")
187 }
188 if strings.HasPrefix(media, "image") {
189 img, err := shrinkit(data)
190 if err != nil {
191 log.Printf("unable to decode image: %s", err)
192 localize = false
193 data = []byte{}
194 goto saveit
195 }
196 data = img.Data
197 format := img.Format
198 media = "image/" + format
199 if format == "jpeg" {
200 format = "jpg"
201 }
202 xid = xid + "." + format
203 } else if media == "application/pdf" {
204 if len(data) > 1000000 {
205 log.Printf("not saving large pdf")
206 localize = false
207 data = []byte{}
208 }
209 } else if len(data) > 100000 {
210 log.Printf("not saving large attachment")
211 localize = false
212 data = []byte{}
213 }
214 }
215saveit:
216 fileid, err := savefile(xid, name, desc, url, media, localize, data)
217 if err != nil {
218 log.Printf("error saving file %s: %s", url, err)
219 return nil
220 }
221 donk.FileID = fileid
222 donk.XID = xid
223 return donk
224}
225
226func iszonked(userid int64, xid string) bool {
227 var id int64
228 row := stmtFindZonk.QueryRow(userid, xid)
229 err := row.Scan(&id)
230 if err == nil {
231 return true
232 }
233 if err != sql.ErrNoRows {
234 log.Printf("error querying zonk: %s", err)
235 }
236 return false
237}
238
239func needxonk(user *WhatAbout, x *Honk) bool {
240 if rejectxonk(x) {
241 return false
242 }
243 return needxonkid(user, x.XID)
244}
245func needbonkid(user *WhatAbout, xid string) bool {
246 return needxonkidX(user, xid, true)
247}
248func needxonkid(user *WhatAbout, xid string) bool {
249 return needxonkidX(user, xid, false)
250}
251func needxonkidX(user *WhatAbout, xid string, isannounce bool) bool {
252 if !strings.HasPrefix(xid, "https://") {
253 return false
254 }
255 if strings.HasPrefix(xid, user.URL+"/") {
256 return false
257 }
258 if rejectorigin(user.ID, xid, isannounce) {
259 log.Printf("rejecting origin: %s", xid)
260 return false
261 }
262 if iszonked(user.ID, xid) {
263 log.Printf("already zonked: %s", xid)
264 return false
265 }
266 var id int64
267 row := stmtFindXonk.QueryRow(user.ID, xid)
268 err := row.Scan(&id)
269 if err == nil {
270 return false
271 }
272 if err != sql.ErrNoRows {
273 log.Printf("error querying xonk: %s", err)
274 }
275 return true
276}
277
278func eradicatexonk(userid int64, xid string) {
279 xonk := getxonk(userid, xid)
280 if xonk != nil {
281 deletehonk(xonk.ID)
282 }
283 _, err := stmtSaveZonker.Exec(userid, xid, "zonk")
284 if err != nil {
285 log.Printf("error eradicating: %s", err)
286 }
287}
288
289func savexonk(x *Honk) {
290 log.Printf("saving xonk: %s", x.XID)
291 go handles(x.Honker)
292 go handles(x.Oonker)
293 savehonk(x)
294}
295
296type Box struct {
297 In string
298 Out string
299 Shared string
300}
301
302var boxofboxes = cache.New(cache.Options{Filler: func(ident string) (*Box, bool) {
303 var info string
304 row := stmtGetXonker.QueryRow(ident, "boxes")
305 err := row.Scan(&info)
306 if err != nil {
307 log.Printf("need to get boxes for %s", ident)
308 var j junk.Junk
309 j, err = GetJunk(ident)
310 if err != nil {
311 log.Printf("error getting boxes: %s", err)
312 return nil, false
313 }
314 allinjest(originate(ident), j)
315 row = stmtGetXonker.QueryRow(ident, "boxes")
316 err = row.Scan(&info)
317 }
318 if err == nil {
319 m := strings.Split(info, " ")
320 b := &Box{In: m[0], Out: m[1], Shared: m[2]}
321 return b, true
322 }
323 return nil, false
324}})
325
326func gimmexonks(user *WhatAbout, outbox string) {
327 log.Printf("getting outbox: %s", outbox)
328 j, err := GetJunk(outbox)
329 if err != nil {
330 log.Printf("error getting outbox: %s", err)
331 return
332 }
333 t, _ := j.GetString("type")
334 origin := originate(outbox)
335 if t == "OrderedCollection" {
336 items, _ := j.GetArray("orderedItems")
337 if items == nil {
338 items, _ = j.GetArray("items")
339 }
340 if items == nil {
341 obj, ok := j.GetMap("first")
342 if ok {
343 items, _ = obj.GetArray("orderedItems")
344 } else {
345 page1, ok := j.GetString("first")
346 if ok {
347 j, err = GetJunk(page1)
348 if err != nil {
349 log.Printf("error gettings page1: %s", err)
350 return
351 }
352 items, _ = j.GetArray("orderedItems")
353 }
354 }
355 }
356 if len(items) > 20 {
357 items = items[0:20]
358 }
359 for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
360 items[i], items[j] = items[j], items[i]
361 }
362 for _, item := range items {
363 obj, ok := item.(junk.Junk)
364 if ok {
365 xonksaver(user, obj, origin)
366 continue
367 }
368 xid, ok := item.(string)
369 if ok {
370 if !needxonkid(user, xid) {
371 continue
372 }
373 obj, err = GetJunk(xid)
374 if err != nil {
375 log.Printf("error getting item: %s", err)
376 continue
377 }
378 xonksaver(user, obj, originate(xid))
379 }
380 }
381 }
382}
383
384func whosthere(xid string) ([]string, string) {
385 obj, err := GetJunk(xid)
386 if err != nil {
387 log.Printf("error getting remote xonk: %s", err)
388 return nil, ""
389 }
390 convoy, _ := obj.GetString("context")
391 if convoy == "" {
392 convoy, _ = obj.GetString("conversation")
393 }
394 return newphone(nil, obj), convoy
395}
396
397func newphone(a []string, obj junk.Junk) []string {
398 for _, addr := range []string{"to", "cc", "attributedTo"} {
399 who, _ := obj.GetString(addr)
400 if who != "" {
401 a = append(a, who)
402 }
403 whos, _ := obj.GetArray(addr)
404 for _, w := range whos {
405 who, _ := w.(string)
406 if who != "" {
407 a = append(a, who)
408 }
409 }
410 }
411 return a
412}
413
414func extractattrto(obj junk.Junk) string {
415 who, _ := obj.GetString("attributedTo")
416 if who != "" {
417 return who
418 }
419 o, ok := obj.GetMap("attributedTo")
420 if ok {
421 id, ok := o.GetString("id")
422 if ok {
423 return id
424 }
425 }
426 arr, _ := obj.GetArray("attributedTo")
427 for _, a := range arr {
428 o, ok := a.(junk.Junk)
429 if ok {
430 t, _ := o.GetString("type")
431 id, _ := o.GetString("id")
432 if t == "Person" || t == "" {
433 return id
434 }
435 }
436 s, ok := a.(string)
437 if ok {
438 return s
439 }
440 }
441 return ""
442}
443
444func xonksaver(user *WhatAbout, item junk.Junk, origin string) *Honk {
445 depth := 0
446 maxdepth := 10
447 currenttid := ""
448 goingup := 0
449 var xonkxonkfn func(item junk.Junk, origin string) *Honk
450
451 saveonemore := func(xid string) {
452 log.Printf("getting onemore: %s", xid)
453 if depth >= maxdepth {
454 log.Printf("in too deep")
455 return
456 }
457 obj, err := GetJunkHardMode(xid)
458 if err != nil {
459 log.Printf("error getting onemore: %s: %s", xid, err)
460 return
461 }
462 depth++
463 xonkxonkfn(obj, originate(xid))
464 depth--
465 }
466
467 xonkxonkfn = func(item junk.Junk, origin string) *Honk {
468 // id, _ := item.GetString( "id")
469 what, _ := item.GetString("type")
470 dt, ok := item.GetString("published")
471 if !ok {
472 dt = time.Now().Format(time.RFC3339)
473 }
474
475 var err error
476 var xid, rid, url, content, precis, convoy string
477 var replies []string
478 var obj junk.Junk
479 isUpdate := false
480 switch what {
481 case "Delete":
482 obj, ok = item.GetMap("object")
483 if ok {
484 xid, _ = obj.GetString("id")
485 } else {
486 xid, _ = item.GetString("object")
487 }
488 if xid == "" {
489 return nil
490 }
491 if originate(xid) != origin {
492 log.Printf("forged delete: %s", xid)
493 return nil
494 }
495 log.Printf("eradicating %s", xid)
496 eradicatexonk(user.ID, xid)
497 return nil
498 case "Tombstone":
499 xid, _ = item.GetString("id")
500 if xid == "" {
501 return nil
502 }
503 if originate(xid) != origin {
504 log.Printf("forged delete: %s", xid)
505 return nil
506 }
507 log.Printf("eradicating %s", xid)
508 eradicatexonk(user.ID, xid)
509 return nil
510 case "Announce":
511 obj, ok = item.GetMap("object")
512 if ok {
513 xid, _ = obj.GetString("id")
514 } else {
515 xid, _ = item.GetString("object")
516 }
517 if !needbonkid(user, xid) {
518 return nil
519 }
520 log.Printf("getting bonk: %s", xid)
521 obj, err = GetJunkHardMode(xid)
522 if err != nil {
523 log.Printf("error getting bonk: %s: %s", xid, err)
524 }
525 origin = originate(xid)
526 what = "bonk"
527 case "Update":
528 isUpdate = true
529 fallthrough
530 case "Create":
531 obj, ok = item.GetMap("object")
532 if !ok {
533 xid, _ = item.GetString("object")
534 log.Printf("getting created honk: %s", xid)
535 obj, err = GetJunkHardMode(xid)
536 if err != nil {
537 log.Printf("error getting creation: %s", err)
538 }
539 }
540 what = "honk"
541 if obj != nil {
542 t, _ := obj.GetString("type")
543 switch t {
544 case "Event":
545 what = "event"
546 }
547 }
548 case "Read":
549 xid, ok = item.GetString("object")
550 if ok {
551 if !needxonkid(user, xid) {
552 log.Printf("don't need read obj: %s", xid)
553 return nil
554 }
555 obj, err = GetJunkHardMode(xid)
556 if err != nil {
557 log.Printf("error getting read: %s", err)
558 return nil
559 }
560 return xonkxonkfn(obj, originate(xid))
561 }
562 return nil
563 case "Add":
564 xid, ok = item.GetString("object")
565 if ok {
566 // check target...
567 if !needxonkid(user, xid) {
568 log.Printf("don't need added obj: %s", xid)
569 return nil
570 }
571 obj, err = GetJunkHardMode(xid)
572 if err != nil {
573 log.Printf("error getting add: %s", err)
574 return nil
575 }
576 return xonkxonkfn(obj, originate(xid))
577 }
578 return nil
579 case "Move":
580 obj = item
581 what = "move"
582 case "Audio":
583 fallthrough
584 case "Image":
585 fallthrough
586 case "Video":
587 fallthrough
588 case "Question":
589 fallthrough
590 case "Note":
591 fallthrough
592 case "Article":
593 fallthrough
594 case "Page":
595 obj = item
596 what = "honk"
597 case "Event":
598 obj = item
599 what = "event"
600 default:
601 log.Printf("unknown activity: %s", what)
602 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
603 item.Write(fd)
604 io.WriteString(fd, "\n")
605 fd.Close()
606 return nil
607 }
608
609 if obj != nil {
610 if _, ok := obj.GetString("diaspora:guid"); ok {
611 // friendica does the silliest bonks
612 if c, ok := obj.GetString("source", "content"); ok {
613 re_link := regexp.MustCompile(`link='([^']*)'`)
614 if m := re_link.FindStringSubmatch(c); len(m) > 1 {
615 xid = m[1]
616 log.Printf("getting friendica flavored bonk: %s", xid)
617 if !needxonkid(user, xid) {
618 return nil
619 }
620 if newobj, err := GetJunkHardMode(xid); err == nil {
621 obj = newobj
622 origin = originate(xid)
623 what = "bonk"
624 } else {
625 log.Printf("error getting bonk: %s: %s", xid, err)
626 }
627 }
628 }
629 }
630 xid, _ = obj.GetString("id")
631 }
632
633 if xid == "" {
634 log.Printf("don't know what xid is")
635 item.Write(os.Stdout)
636 return nil
637 }
638 if originate(xid) != origin {
639 log.Printf("original sin: %s <> %s", xid, origin)
640 item.Write(os.Stdout)
641 return nil
642 }
643
644 var xonk Honk
645 // early init
646 xonk.XID = xid
647 xonk.UserID = user.ID
648 xonk.Honker, _ = item.GetString("actor")
649 if xonk.Honker == "" {
650 xonk.Honker, _ = item.GetString("attributedTo")
651 }
652 if obj != nil {
653 if xonk.Honker == "" {
654 xonk.Honker = extractattrto(obj)
655 }
656 xonk.Oonker = extractattrto(obj)
657 if xonk.Oonker == xonk.Honker {
658 xonk.Oonker = ""
659 }
660 xonk.Audience = newphone(nil, obj)
661 }
662 xonk.Audience = append(xonk.Audience, xonk.Honker)
663 xonk.Audience = oneofakind(xonk.Audience)
664
665 var mentions []Mention
666 if obj != nil {
667 ot, _ := obj.GetString("type")
668 url, _ = obj.GetString("url")
669 if dt2, ok := obj.GetString("published"); ok {
670 dt = dt2
671 }
672 content, _ = obj.GetString("content")
673 if !strings.HasPrefix(content, "<p>") {
674 content = "<p>" + content
675 }
676 precis, _ = obj.GetString("summary")
677 if name, ok := obj.GetString("name"); ok {
678 if precis != "" {
679 content = precis + "<p>" + content
680 }
681 precis = html.EscapeString(name)
682 }
683 if sens, _ := obj["sensitive"].(bool); sens && precis == "" {
684 precis = "unspecified horror"
685 }
686 rid, ok = obj.GetString("inReplyTo")
687 if !ok {
688 if robj, ok := obj.GetMap("inReplyTo"); ok {
689 rid, _ = robj.GetString("id")
690 }
691 }
692 convoy, _ = obj.GetString("context")
693 if convoy == "" {
694 convoy, _ = obj.GetString("conversation")
695 }
696 if ot == "Question" {
697 if what == "honk" {
698 what = "qonk"
699 }
700 content += "<ul>"
701 ans, _ := obj.GetArray("oneOf")
702 for _, ai := range ans {
703 a, ok := ai.(junk.Junk)
704 if !ok {
705 continue
706 }
707 as, _ := a.GetString("name")
708 content += "<li>" + as
709 }
710 ans, _ = obj.GetArray("anyOf")
711 for _, ai := range ans {
712 a, ok := ai.(junk.Junk)
713 if !ok {
714 continue
715 }
716 as, _ := a.GetString("name")
717 content += "<li>" + as
718 }
719 content += "</ul>"
720 }
721 if ot == "Move" {
722 targ, _ := obj.GetString("target")
723 content += string(templates.Sprintf(`<p>Moved to <a href="%s">%s</a>`, targ, targ))
724 }
725 if what == "honk" && rid != "" {
726 what = "tonk"
727 }
728 atts, _ := obj.GetArray("attachment")
729 for i, atti := range atts {
730 if rejectxonk(&xonk) {
731 log.Printf("skipping rejected attachment: %s", xid)
732 continue
733 }
734 att, ok := atti.(junk.Junk)
735 if !ok {
736 continue
737 }
738 at, _ := att.GetString("type")
739 mt, _ := att.GetString("mediaType")
740 u, _ := att.GetString("url")
741 name, _ := att.GetString("name")
742 desc, _ := att.GetString("summary")
743 if desc == "" {
744 desc = name
745 }
746 localize := false
747 if i > 4 {
748 log.Printf("excessive attachment: %s", at)
749 } else if at == "Document" || at == "Image" {
750 mt = strings.ToLower(mt)
751 log.Printf("attachment: %s %s", mt, u)
752 if mt == "text/plain" || mt == "application/pdf" ||
753 strings.HasPrefix(mt, "image") {
754 localize = true
755 }
756 } else {
757 log.Printf("unknown attachment: %s", at)
758 }
759 if skipMedia(&xonk) {
760 localize = false
761 }
762 donk := savedonk(u, name, desc, mt, localize)
763 if donk != nil {
764 xonk.Donks = append(xonk.Donks, donk)
765 }
766 }
767 tags, _ := obj.GetArray("tag")
768 for _, tagi := range tags {
769 if rejectxonk(&xonk) {
770 log.Printf("skipping rejected attachment: %s", xid)
771 continue
772 }
773 tag, ok := tagi.(junk.Junk)
774 if !ok {
775 continue
776 }
777 tt, _ := tag.GetString("type")
778 name, _ := tag.GetString("name")
779 desc, _ := tag.GetString("summary")
780 if desc == "" {
781 desc = name
782 }
783 if tt == "Emoji" {
784 icon, _ := tag.GetMap("icon")
785 mt, _ := icon.GetString("mediaType")
786 if mt == "" {
787 mt = "image/png"
788 }
789 u, _ := icon.GetString("url")
790 donk := savedonk(u, name, desc, mt, true)
791 if donk != nil {
792 xonk.Donks = append(xonk.Donks, donk)
793 }
794 }
795 if tt == "Hashtag" {
796 if name == "" || name == "#" {
797 // skip it
798 } else {
799 if name[0] != '#' {
800 name = "#" + name
801 }
802 xonk.Onts = append(xonk.Onts, name)
803 }
804 }
805 if tt == "Place" {
806 p := new(Place)
807 p.Name = name
808 p.Latitude, _ = tag["latitude"].(float64)
809 p.Longitude, _ = tag["longitude"].(float64)
810 p.Url, _ = tag.GetString("url")
811 xonk.Place = p
812 }
813 if tt == "Mention" {
814 var m Mention
815 m.Who, _ = tag.GetString("name")
816 m.Where, _ = tag.GetString("href")
817 mentions = append(mentions, m)
818 }
819 }
820 if starttime, ok := obj.GetString("startTime"); ok {
821 if start, err := time.Parse(time.RFC3339, starttime); err == nil {
822 t := new(Time)
823 t.StartTime = start
824 endtime, _ := obj.GetString("endTime")
825 t.EndTime, _ = time.Parse(time.RFC3339, endtime)
826 dura, _ := obj.GetString("duration")
827 if strings.HasPrefix(dura, "PT") {
828 dura = strings.ToLower(dura[2:])
829 d, _ := time.ParseDuration(dura)
830 t.Duration = Duration(d)
831 }
832 xonk.Time = t
833 }
834 }
835 if loca, ok := obj.GetMap("location"); ok {
836 if tt, _ := loca.GetString("type"); tt == "Place" {
837 p := new(Place)
838 p.Name, _ = loca.GetString("name")
839 p.Latitude, _ = loca["latitude"].(float64)
840 p.Longitude, _ = loca["longitude"].(float64)
841 p.Url, _ = loca.GetString("url")
842 xonk.Place = p
843 }
844 }
845
846 xonk.Onts = oneofakind(xonk.Onts)
847 replyobj, ok := obj.GetMap("replies")
848 if ok {
849 items, ok := replyobj.GetArray("items")
850 if !ok {
851 first, ok := replyobj.GetMap("first")
852 if ok {
853 items, _ = first.GetArray("items")
854 }
855 }
856 for _, repl := range items {
857 s, ok := repl.(string)
858 if ok {
859 replies = append(replies, s)
860 }
861 }
862 }
863
864 }
865
866 if currenttid == "" {
867 currenttid = convoy
868 }
869
870 if len(content) > 90001 {
871 log.Printf("content too long. truncating")
872 content = content[:90001]
873 }
874
875 // init xonk
876 xonk.What = what
877 xonk.RID = rid
878 xonk.Date, _ = time.Parse(time.RFC3339, dt)
879 xonk.URL = url
880 xonk.Noise = content
881 xonk.Precis = precis
882 xonk.Format = "html"
883 xonk.Convoy = convoy
884 xonk.Mentions = mentions
885 for _, m := range mentions {
886 if m.Where == user.URL {
887 xonk.Whofore = 1
888 }
889 }
890 imaginate(&xonk)
891
892 if isUpdate {
893 log.Printf("something has changed! %s", xonk.XID)
894 prev := getxonk(user.ID, xonk.XID)
895 if prev == nil {
896 log.Printf("didn't find old version for update: %s", xonk.XID)
897 isUpdate = false
898 } else {
899 xonk.ID = prev.ID
900 updatehonk(&xonk)
901 }
902 }
903 if !isUpdate && needxonk(user, &xonk) {
904 if strings.HasSuffix(convoy, "#context") {
905 // friendica...
906 if rid != "" {
907 convoy = ""
908 } else {
909 convoy = url
910 }
911 }
912 if rid != "" {
913 if needxonkid(user, rid) {
914 goingup++
915 saveonemore(rid)
916 goingup--
917 }
918 if convoy == "" {
919 xx := getxonk(user.ID, rid)
920 if xx != nil {
921 convoy = xx.Convoy
922 }
923 }
924 }
925 if convoy == "" {
926 convoy = currenttid
927 }
928 if convoy == "" {
929 convoy = "data:,missing-" + xfiltrate()
930 currenttid = convoy
931 }
932 xonk.Convoy = convoy
933 savexonk(&xonk)
934 }
935 if goingup == 0 {
936 for _, replid := range replies {
937 if needxonkid(user, replid) {
938 log.Printf("missing a reply: %s", replid)
939 saveonemore(replid)
940 }
941 }
942 }
943 return &xonk
944 }
945
946 return xonkxonkfn(item, origin)
947}
948
949func rubadubdub(user *WhatAbout, req junk.Junk) {
950 xid, _ := req.GetString("id")
951 actor, _ := req.GetString("actor")
952 j := junk.New()
953 j["@context"] = itiswhatitis
954 j["id"] = user.URL + "/dub/" + url.QueryEscape(xid)
955 j["type"] = "Accept"
956 j["actor"] = user.URL
957 j["to"] = actor
958 j["published"] = time.Now().UTC().Format(time.RFC3339)
959 j["object"] = req
960
961 deliverate(0, user.ID, actor, j.ToBytes())
962}
963
964func itakeitallback(user *WhatAbout, xid string) {
965 j := junk.New()
966 j["@context"] = itiswhatitis
967 j["id"] = user.URL + "/unsub/" + url.QueryEscape(xid)
968 j["type"] = "Undo"
969 j["actor"] = user.URL
970 j["to"] = xid
971 f := junk.New()
972 f["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
973 f["type"] = "Follow"
974 f["actor"] = user.URL
975 f["to"] = xid
976 f["object"] = xid
977 j["object"] = f
978 j["published"] = time.Now().UTC().Format(time.RFC3339)
979
980 deliverate(0, user.ID, xid, j.ToBytes())
981}
982
983func subsub(user *WhatAbout, xid string, owner string) {
984 if xid == "" {
985 log.Printf("can't subscribe to empty")
986 return
987 }
988 j := junk.New()
989 j["@context"] = itiswhatitis
990 j["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
991 j["type"] = "Follow"
992 j["actor"] = user.URL
993 j["to"] = owner
994 j["object"] = xid
995 j["published"] = time.Now().UTC().Format(time.RFC3339)
996
997 deliverate(0, user.ID, owner, j.ToBytes())
998}
999
1000// returns activity, object
1001func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
1002 dt := h.Date.Format(time.RFC3339)
1003 var jo junk.Junk
1004 j := junk.New()
1005 j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
1006 j["actor"] = user.URL
1007 j["published"] = dt
1008 if h.Public {
1009 j["to"] = []string{h.Audience[0], user.URL + "/followers"}
1010 } else {
1011 j["to"] = h.Audience[0]
1012 }
1013 if len(h.Audience) > 1 {
1014 j["cc"] = h.Audience[1:]
1015 }
1016
1017 switch h.What {
1018 case "update":
1019 fallthrough
1020 case "tonk":
1021 fallthrough
1022 case "event":
1023 fallthrough
1024 case "honk":
1025 j["type"] = "Create"
1026 if h.What == "update" {
1027 j["type"] = "Update"
1028 }
1029
1030 jo = junk.New()
1031 jo["id"] = h.XID
1032 jo["type"] = "Note"
1033 if h.What == "event" {
1034 jo["type"] = "Event"
1035 }
1036 jo["published"] = dt
1037 jo["url"] = h.XID
1038 jo["attributedTo"] = user.URL
1039 if h.RID != "" {
1040 jo["inReplyTo"] = h.RID
1041 }
1042 if h.Convoy != "" {
1043 jo["context"] = h.Convoy
1044 jo["conversation"] = h.Convoy
1045 }
1046 jo["to"] = h.Audience[0]
1047 if len(h.Audience) > 1 {
1048 jo["cc"] = h.Audience[1:]
1049 }
1050 if !h.Public {
1051 jo["directMessage"] = true
1052 }
1053 mentions := bunchofgrapes(h.Noise)
1054 translate(h)
1055 redoimages(h)
1056 jo["summary"] = html.EscapeString(h.Precis)
1057 jo["content"] = h.Noise
1058 if h.Precis != "" {
1059 jo["sensitive"] = true
1060 }
1061
1062 var replies []string
1063 for _, reply := range h.Replies {
1064 replies = append(replies, reply.XID)
1065 }
1066 if len(replies) > 0 {
1067 jr := junk.New()
1068 jr["type"] = "Collection"
1069 jr["totalItems"] = len(replies)
1070 jr["items"] = replies
1071 jo["replies"] = jr
1072 }
1073
1074 var tags []junk.Junk
1075 for _, m := range mentions {
1076 t := junk.New()
1077 t["type"] = "Mention"
1078 t["name"] = m.Who
1079 t["href"] = m.Where
1080 tags = append(tags, t)
1081 }
1082 for _, o := range h.Onts {
1083 t := junk.New()
1084 t["type"] = "Hashtag"
1085 o = strings.ToLower(o)
1086 t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1087 t["name"] = o
1088 tags = append(tags, t)
1089 }
1090 for _, e := range herdofemus(h.Noise) {
1091 t := junk.New()
1092 t["id"] = e.ID
1093 t["type"] = "Emoji"
1094 t["name"] = e.Name
1095 i := junk.New()
1096 i["type"] = "Image"
1097 i["mediaType"] = "image/png"
1098 i["url"] = e.ID
1099 t["icon"] = i
1100 tags = append(tags, t)
1101 }
1102 if len(tags) > 0 {
1103 jo["tag"] = tags
1104 }
1105 if p := h.Place; p != nil {
1106 t := junk.New()
1107 t["type"] = "Place"
1108 if p.Name != "" {
1109 t["name"] = p.Name
1110 }
1111 if p.Latitude != 0 {
1112 t["latitude"] = p.Latitude
1113 }
1114 if p.Longitude != 0 {
1115 t["longitude"] = p.Longitude
1116 }
1117 if p.Url != "" {
1118 t["url"] = p.Url
1119 }
1120 jo["location"] = t
1121 }
1122 if t := h.Time; t != nil {
1123 jo["startTime"] = t.StartTime.Format(time.RFC3339)
1124 if t.Duration != 0 {
1125 jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1126 }
1127 }
1128 var atts []junk.Junk
1129 for _, d := range h.Donks {
1130 if re_emus.MatchString(d.Name) {
1131 continue
1132 }
1133 jd := junk.New()
1134 jd["mediaType"] = d.Media
1135 jd["name"] = d.Name
1136 jd["summary"] = html.EscapeString(d.Desc)
1137 jd["type"] = "Document"
1138 jd["url"] = d.URL
1139 atts = append(atts, jd)
1140 }
1141 if len(atts) > 0 {
1142 jo["attachment"] = atts
1143 }
1144 j["object"] = jo
1145 case "bonk":
1146 j["type"] = "Announce"
1147 if h.Convoy != "" {
1148 j["context"] = h.Convoy
1149 }
1150 j["object"] = h.XID
1151 case "unbonk":
1152 b := junk.New()
1153 b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1154 b["type"] = "Announce"
1155 b["actor"] = user.URL
1156 if h.Convoy != "" {
1157 b["context"] = h.Convoy
1158 }
1159 b["object"] = h.XID
1160 j["type"] = "Undo"
1161 j["object"] = b
1162 case "zonk":
1163 j["type"] = "Delete"
1164 j["object"] = h.XID
1165 case "ack":
1166 j["type"] = "Read"
1167 j["object"] = h.XID
1168 if h.Convoy != "" {
1169 j["context"] = h.Convoy
1170 }
1171 case "react":
1172 j["type"] = "EmojiReaction"
1173 j["object"] = h.XID
1174 if h.Convoy != "" {
1175 j["context"] = h.Convoy
1176 }
1177 j["content"] = user.Options.Reaction
1178 case "deack":
1179 b := junk.New()
1180 b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1181 b["type"] = "Read"
1182 b["actor"] = user.URL
1183 b["object"] = h.XID
1184 if h.Convoy != "" {
1185 b["context"] = h.Convoy
1186 }
1187 j["type"] = "Undo"
1188 j["object"] = b
1189 }
1190
1191 return j, jo
1192}
1193
1194var oldjonks = cache.New(cache.Options{Filler: func(xid string) ([]byte, bool) {
1195 row := stmtAnyXonk.QueryRow(xid)
1196 honk := scanhonk(row)
1197 if honk == nil || !honk.Public {
1198 return nil, true
1199 }
1200 user, _ := butwhatabout(honk.Username)
1201 rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1202 reversehonks(rawhonks)
1203 for _, h := range rawhonks {
1204 if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
1205 honk.Replies = append(honk.Replies, h)
1206 }
1207 }
1208 donksforhonks([]*Honk{honk})
1209 _, j := jonkjonk(user, honk)
1210 j["@context"] = itiswhatitis
1211
1212 return j.ToBytes(), true
1213}, Limit: 128})
1214
1215func gimmejonk(xid string) ([]byte, bool) {
1216 var j []byte
1217 ok := oldjonks.Get(xid, &j)
1218 return j, ok
1219}
1220
1221func boxuprcpts(user *WhatAbout, addresses []string, useshared bool) map[string]bool {
1222 rcpts := make(map[string]bool)
1223 for _, a := range addresses {
1224 if a == "" || a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1225 continue
1226 }
1227 if a[0] == '%' {
1228 rcpts[a] = true
1229 continue
1230 }
1231 var box *Box
1232 ok := boxofboxes.Get(a, &box)
1233 if ok && useshared && box.Shared != "" {
1234 rcpts["%"+box.Shared] = true
1235 } else {
1236 rcpts[a] = true
1237 }
1238 }
1239 return rcpts
1240}
1241
1242func honkworldwide(user *WhatAbout, honk *Honk) {
1243 jonk, _ := jonkjonk(user, honk)
1244 jonk["@context"] = itiswhatitis
1245 msg := jonk.ToBytes()
1246
1247 rcpts := boxuprcpts(user, honk.Audience, honk.Public)
1248
1249 if honk.Public {
1250 for _, h := range getdubs(user.ID) {
1251 if h.XID == user.URL {
1252 continue
1253 }
1254 var box *Box
1255 ok := boxofboxes.Get(h.XID, &box)
1256 if ok && box.Shared != "" {
1257 rcpts["%"+box.Shared] = true
1258 } else {
1259 rcpts[h.XID] = true
1260 }
1261 }
1262 for _, f := range getbacktracks(honk.XID) {
1263 rcpts[f] = true
1264 }
1265 }
1266 for a := range rcpts {
1267 go deliverate(0, user.ID, a, msg)
1268 }
1269 if honk.Public && len(honk.Onts) > 0 {
1270 collectiveaction(honk)
1271 }
1272}
1273
1274func collectiveaction(honk *Honk) {
1275 user := getserveruser()
1276 for _, ont := range honk.Onts {
1277 dubs := getnameddubs(serverUID, ont)
1278 if len(dubs) == 0 {
1279 continue
1280 }
1281 j := junk.New()
1282 j["@context"] = itiswhatitis
1283 j["type"] = "Add"
1284 j["id"] = user.URL + "/add/" + shortxid(ont+honk.XID)
1285 j["actor"] = user.URL
1286 j["object"] = honk.XID
1287 j["target"] = fmt.Sprintf("https://%s/o/%s", serverName, ont[1:])
1288 rcpts := make(map[string]bool)
1289 for _, dub := range dubs {
1290 var box *Box
1291 ok := boxofboxes.Get(dub.XID, &box)
1292 if ok && box.Shared != "" {
1293 rcpts["%"+box.Shared] = true
1294 } else {
1295 rcpts[dub.XID] = true
1296 }
1297 }
1298 msg := j.ToBytes()
1299 for a := range rcpts {
1300 go deliverate(0, user.ID, a, msg)
1301 }
1302 }
1303}
1304
1305func junkuser(user *WhatAbout) []byte {
1306 about := markitzero(user.About)
1307
1308 j := junk.New()
1309 j["@context"] = itiswhatitis
1310 j["id"] = user.URL
1311 j["inbox"] = user.URL + "/inbox"
1312 j["outbox"] = user.URL + "/outbox"
1313 j["name"] = user.Display
1314 j["preferredUsername"] = user.Name
1315 j["summary"] = about
1316 if user.ID > 0 {
1317 j["type"] = "Person"
1318 j["url"] = user.URL
1319 j["followers"] = user.URL + "/followers"
1320 j["following"] = user.URL + "/following"
1321 a := junk.New()
1322 a["type"] = "Image"
1323 a["mediaType"] = "image/png"
1324 if ava := user.Options.Avatar; ava != "" {
1325 a["url"] = ava
1326 } else {
1327 a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
1328 }
1329 j["icon"] = a
1330 } else {
1331 j["type"] = "Service"
1332 }
1333 k := junk.New()
1334 k["id"] = user.URL + "#key"
1335 k["owner"] = user.URL
1336 k["publicKeyPem"] = user.Key
1337 j["publicKey"] = k
1338
1339 return j.ToBytes()
1340}
1341
1342var oldjonkers = cache.New(cache.Options{Filler: func(name string) ([]byte, bool) {
1343 user, err := butwhatabout(name)
1344 if err != nil {
1345 return nil, false
1346 }
1347 return junkuser(user), true
1348}, Duration: 1 * time.Minute})
1349
1350func asjonker(name string) ([]byte, bool) {
1351 var j []byte
1352 ok := oldjonkers.Get(name, &j)
1353 return j, ok
1354}
1355
1356var handfull = cache.New(cache.Options{Filler: func(name string) (string, bool) {
1357 m := strings.Split(name, "@")
1358 if len(m) != 2 {
1359 log.Printf("bad fish name: %s", name)
1360 return "", true
1361 }
1362 var href string
1363 row := stmtGetXonker.QueryRow(name, "fishname")
1364 err := row.Scan(&href)
1365 if err == nil {
1366 return href, true
1367 }
1368 log.Printf("fishing for %s", name)
1369 j, err := GetJunkFast(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1370 if err != nil {
1371 log.Printf("failed to go fish %s: %s", name, err)
1372 return "", true
1373 }
1374 links, _ := j.GetArray("links")
1375 for _, li := range links {
1376 l, ok := li.(junk.Junk)
1377 if !ok {
1378 continue
1379 }
1380 href, _ := l.GetString("href")
1381 rel, _ := l.GetString("rel")
1382 t, _ := l.GetString("type")
1383 if rel == "self" && friendorfoe(t) {
1384 when := time.Now().UTC().Format(dbtimeformat)
1385 _, err := stmtSaveXonker.Exec(name, href, "fishname", when)
1386 if err != nil {
1387 log.Printf("error saving fishname: %s", err)
1388 }
1389 return href, true
1390 }
1391 }
1392 return href, true
1393}})
1394
1395func gofish(name string) string {
1396 if name[0] == '@' {
1397 name = name[1:]
1398 }
1399 var href string
1400 handfull.Get(name, &href)
1401 return href
1402}
1403
1404func investigate(name string) (*SomeThing, error) {
1405 if name == "" {
1406 return nil, fmt.Errorf("no name")
1407 }
1408 if name[0] == '@' {
1409 name = gofish(name)
1410 }
1411 if name == "" {
1412 return nil, fmt.Errorf("no name")
1413 }
1414 obj, err := GetJunkFast(name)
1415 if err != nil {
1416 return nil, err
1417 }
1418 allinjest(originate(name), obj)
1419 return somethingabout(obj)
1420}
1421
1422func somethingabout(obj junk.Junk) (*SomeThing, error) {
1423 info := new(SomeThing)
1424 t, _ := obj.GetString("type")
1425 switch t {
1426 case "Person":
1427 fallthrough
1428 case "Organization":
1429 fallthrough
1430 case "Application":
1431 fallthrough
1432 case "Service":
1433 info.What = SomeActor
1434 case "OrderedCollection":
1435 fallthrough
1436 case "Collection":
1437 info.What = SomeCollection
1438 default:
1439 return nil, fmt.Errorf("unknown object type")
1440 }
1441 info.XID, _ = obj.GetString("id")
1442 info.Name, _ = obj.GetString("preferredUsername")
1443 if info.Name == "" {
1444 info.Name, _ = obj.GetString("name")
1445 }
1446 info.Owner, _ = obj.GetString("attributedTo")
1447 if info.Owner == "" {
1448 info.Owner = info.XID
1449 }
1450 return info, nil
1451}
1452
1453func allinjest(origin string, obj junk.Junk) {
1454 keyobj, ok := obj.GetMap("publicKey")
1455 if ok {
1456 ingestpubkey(origin, keyobj)
1457 }
1458 ingestboxes(origin, obj)
1459 ingesthandle(origin, obj)
1460}
1461
1462func ingestpubkey(origin string, obj junk.Junk) {
1463 keyobj, ok := obj.GetMap("publicKey")
1464 if ok {
1465 obj = keyobj
1466 }
1467 keyname, ok := obj.GetString("id")
1468 var data string
1469 row := stmtGetXonker.QueryRow(keyname, "pubkey")
1470 err := row.Scan(&data)
1471 if err == nil {
1472 return
1473 }
1474 if !ok || origin != originate(keyname) {
1475 log.Printf("bad key origin %s <> %s", origin, keyname)
1476 return
1477 }
1478 log.Printf("ingesting a needed pubkey: %s", keyname)
1479 owner, ok := obj.GetString("owner")
1480 if !ok {
1481 log.Printf("error finding %s pubkey owner", keyname)
1482 return
1483 }
1484 data, ok = obj.GetString("publicKeyPem")
1485 if !ok {
1486 log.Printf("error finding %s pubkey", keyname)
1487 return
1488 }
1489 if originate(owner) != origin {
1490 log.Printf("bad key owner: %s <> %s", owner, origin)
1491 return
1492 }
1493 _, _, err = httpsig.DecodeKey(data)
1494 if err != nil {
1495 log.Printf("error decoding %s pubkey: %s", keyname, err)
1496 return
1497 }
1498 when := time.Now().UTC().Format(dbtimeformat)
1499 _, err = stmtSaveXonker.Exec(keyname, data, "pubkey", when)
1500 if err != nil {
1501 log.Printf("error saving key: %s", err)
1502 }
1503}
1504
1505func ingestboxes(origin string, obj junk.Junk) {
1506 ident, _ := obj.GetString("id")
1507 if ident == "" {
1508 return
1509 }
1510 if originate(ident) != origin {
1511 return
1512 }
1513 var info string
1514 row := stmtGetXonker.QueryRow(ident, "boxes")
1515 err := row.Scan(&info)
1516 if err == nil {
1517 return
1518 }
1519 log.Printf("ingesting boxes: %s", ident)
1520 inbox, _ := obj.GetString("inbox")
1521 outbox, _ := obj.GetString("outbox")
1522 sbox, _ := obj.GetString("endpoints", "sharedInbox")
1523 if inbox != "" {
1524 when := time.Now().UTC().Format(dbtimeformat)
1525 m := strings.Join([]string{inbox, outbox, sbox}, " ")
1526 _, err = stmtSaveXonker.Exec(ident, m, "boxes", when)
1527 if err != nil {
1528 log.Printf("error saving boxes: %s", err)
1529 }
1530 }
1531}
1532
1533func ingesthandle(origin string, obj junk.Junk) {
1534 xid, _ := obj.GetString("id")
1535 if xid == "" {
1536 return
1537 }
1538 if originate(xid) != origin {
1539 return
1540 }
1541 var handle string
1542 row := stmtGetXonker.QueryRow(xid, "handle")
1543 err := row.Scan(&handle)
1544 if err == nil {
1545 return
1546 }
1547 handle, _ = obj.GetString("preferredUsername")
1548 if handle != "" {
1549 when := time.Now().UTC().Format(dbtimeformat)
1550 _, err = stmtSaveXonker.Exec(xid, handle, "handle", when)
1551 if err != nil {
1552 log.Printf("error saving handle: %s", err)
1553 }
1554 }
1555}