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