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