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