activity.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "bytes"
20 "compress/gzip"
21 "crypto/rsa"
22 "database/sql"
23 "encoding/json"
24 "fmt"
25 "io"
26 "log"
27 "net/http"
28 "net/url"
29 "os"
30 "strconv"
31 "strings"
32 "sync"
33 "time"
34)
35
36func NewJunk() map[string]interface{} {
37 return make(map[string]interface{})
38}
39
40func WriteJunk(w io.Writer, j map[string]interface{}) error {
41 e := json.NewEncoder(w)
42 e.SetEscapeHTML(false)
43 e.SetIndent("", " ")
44 err := e.Encode(j)
45 return err
46}
47
48func ReadJunk(r io.Reader) (map[string]interface{}, error) {
49 decoder := json.NewDecoder(r)
50 var j map[string]interface{}
51 err := decoder.Decode(&j)
52 if err != nil {
53 return nil, err
54 }
55 return j, nil
56}
57
58var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
59var falsenames = []string{
60 `application/ld+json`,
61 `application/activity+json`,
62}
63var itiswhatitis = "https://www.w3.org/ns/activitystreams"
64var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
65
66func friendorfoe(ct string) bool {
67 ct = strings.ToLower(ct)
68 for _, at := range falsenames {
69 if strings.HasPrefix(ct, at) {
70 return true
71 }
72 }
73 return false
74}
75
76func PostJunk(keyname string, key *rsa.PrivateKey, url string, j map[string]interface{}) error {
77 var buf bytes.Buffer
78 WriteJunk(&buf, j)
79 return PostMsg(keyname, key, url, buf.Bytes())
80}
81
82func PostMsg(keyname string, key *rsa.PrivateKey, url string, msg []byte) error {
83 client := http.DefaultClient
84 req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
85 if err != nil {
86 return err
87 }
88 req.Header.Set("Content-Type", theonetruename)
89 zig(keyname, key, req, msg)
90 resp, err := client.Do(req)
91 if err != nil {
92 return err
93 }
94 if resp.StatusCode != 200 && resp.StatusCode != 202 {
95 resp.Body.Close()
96 return fmt.Errorf("http post status: %d", resp.StatusCode)
97 }
98 log.Printf("successful post: %s %d", url, resp.StatusCode)
99 return nil
100}
101
102type gzCloser struct {
103 r *gzip.Reader
104 under io.ReadCloser
105}
106
107func (gz *gzCloser) Read(p []byte) (int, error) {
108 return gz.r.Read(p)
109}
110
111func (gz *gzCloser) Close() error {
112 defer gz.under.Close()
113 return gz.r.Close()
114}
115
116func GetJunk(url string) (map[string]interface{}, error) {
117 client := http.DefaultClient
118 req, err := http.NewRequest("GET", url, nil)
119 if err != nil {
120 return nil, err
121 }
122 req.Header.Set("Accept", theonetruename)
123 req.Header.Set("Accept-Encoding", "gzip")
124 resp, err := client.Do(req)
125 if err != nil {
126 return nil, err
127 }
128 if resp.StatusCode != 200 {
129 resp.Body.Close()
130 return nil, fmt.Errorf("http get status: %d", resp.StatusCode)
131 }
132 if strings.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") {
133 gz, err := gzip.NewReader(resp.Body)
134 if err != nil {
135 resp.Body.Close()
136 return nil, err
137 }
138 resp.Body = &gzCloser{r: gz, under: resp.Body}
139 }
140 defer resp.Body.Close()
141 j, err := ReadJunk(resp.Body)
142 return j, err
143}
144
145func jsonfindinterface(ii interface{}, keys []string) interface{} {
146 for _, key := range keys {
147 idx, err := strconv.Atoi(key)
148 if err == nil {
149 m := ii.([]interface{})
150 if idx >= len(m) {
151 return nil
152 }
153 ii = m[idx]
154 } else {
155 m := ii.(map[string]interface{})
156 ii = m[key]
157 if ii == nil {
158 return nil
159 }
160 }
161 }
162 return ii
163}
164func jsonfindstring(j interface{}, keys []string) (string, bool) {
165 s, ok := jsonfindinterface(j, keys).(string)
166 return s, ok
167}
168func jsonfindarray(j interface{}, keys []string) ([]interface{}, bool) {
169 a, ok := jsonfindinterface(j, keys).([]interface{})
170 return a, ok
171}
172func jsonfindmap(j interface{}, keys []string) (map[string]interface{}, bool) {
173 m, ok := jsonfindinterface(j, keys).(map[string]interface{})
174 return m, ok
175}
176func jsongetstring(j interface{}, key string) (string, bool) {
177 return jsonfindstring(j, []string{key})
178}
179func jsongetarray(j interface{}, key string) ([]interface{}, bool) {
180 return jsonfindarray(j, []string{key})
181}
182func jsongetmap(j interface{}, key string) (map[string]interface{}, bool) {
183 return jsonfindmap(j, []string{key})
184}
185
186func savedonk(url string, name, media string) *Donk {
187 log.Printf("saving donk: %s", url)
188 var donk Donk
189 row := stmtFindFile.QueryRow(url)
190 err := row.Scan(&donk.FileID)
191 if err == nil {
192 return &donk
193 }
194 if err != nil && err != sql.ErrNoRows {
195 log.Printf("err querying: %s", err)
196 }
197 resp, err := http.Get(url)
198 if err != nil {
199 log.Printf("errer fetching %s: %s", url, err)
200 return nil
201 }
202 defer resp.Body.Close()
203 if resp.StatusCode != 200 {
204 return nil
205 }
206 var buf bytes.Buffer
207 io.Copy(&buf, resp.Body)
208
209 xid := xfiltrate()
210
211 res, err := stmtSaveFile.Exec(xid, name, url, media, buf.Bytes())
212 if err != nil {
213 log.Printf("error saving file %s: %s", url, err)
214 return nil
215 }
216 donk.FileID, _ = res.LastInsertId()
217 return &donk
218}
219
220func needxonk(user *WhatAbout, x *Honk) bool {
221 if strings.HasPrefix(x.XID, user.URL + "/h/") {
222 return false
223 }
224 row := stmtFindXonk.QueryRow(user.ID, x.XID)
225 err := row.Scan(&x.ID)
226 if err == nil {
227 return false
228 }
229 if err != sql.ErrNoRows {
230 log.Printf("err querying xonk: %s", err)
231 }
232 return true
233}
234
235func savexonk(x *Honk) {
236 if x.What == "eradicate" {
237 log.Printf("eradicating %s by %s", x.RID, x.Honker)
238 _, err := stmtDeleteHonk.Exec(x.RID, x.Honker)
239 if err != nil {
240 log.Printf("error eradicating: %s", err)
241 }
242 return
243 }
244 dt := x.Date.UTC().Format(dbtimeformat)
245 aud := strings.Join(x.Audience, " ")
246 res, err := stmtSaveHonk.Exec(x.UserID, x.What, x.Honker, x.XID, x.RID, dt, x.URL, aud, x.Noise)
247 if err != nil {
248 log.Printf("err saving xonk: %s", err)
249 return
250 }
251 x.ID, _ = res.LastInsertId()
252 for _, d := range x.Donks {
253 _, err = stmtSaveDonk.Exec(x.ID, d.FileID)
254 if err != nil {
255 log.Printf("err saving donk: %s", err)
256 return
257 }
258 }
259}
260
261var boxofboxes = make(map[string]string)
262var boxlock sync.Mutex
263
264func getboxes(ident string) (string, string, error) {
265 boxlock.Lock()
266 b, ok := boxofboxes[ident]
267 boxlock.Unlock()
268 if ok {
269 m := strings.Split(b, "\n")
270 return m[0], m[1], nil
271 }
272 j, err := GetJunk(ident)
273 if err != nil {
274 return "", "", err
275 }
276 inbox, _ := jsongetstring(j, "inbox")
277 outbox, _ := jsongetstring(j, "outbox")
278 boxlock.Lock()
279 boxofboxes[ident] = inbox + "\n" + outbox
280 boxlock.Unlock()
281 return inbox, outbox, err
282}
283
284func peeppeep() {
285 user, _ := butwhatabout("")
286 honkers := gethonkers(user.ID)
287 for _, f := range honkers {
288 if f.Flavor != "peep" {
289 continue
290 }
291 log.Printf("getting updates: %s", f.XID)
292 _, outbox, err := getboxes(f.XID)
293 if err != nil {
294 log.Printf("error getting outbox: %s", err)
295 continue
296 }
297 log.Printf("getting outbox")
298 j, err := GetJunk(outbox)
299 if err != nil {
300 log.Printf("err: %s", err)
301 continue
302 }
303 t, _ := jsongetstring(j, "type")
304 if t == "OrderedCollection" {
305 items, _ := jsongetarray(j, "orderedItems")
306 if items == nil {
307 page1, _ := jsongetstring(j, "first")
308 j, err = GetJunk(page1)
309 if err != nil {
310 log.Printf("err: %s", err)
311 continue
312 }
313 items, _ = jsongetarray(j, "orderedItems")
314 }
315
316 for _, item := range items {
317 xonk := xonkxonk(item)
318 if xonk != nil && needxonk(user, xonk) {
319 xonk.UserID = user.ID
320 savexonk(xonk)
321 }
322 }
323 }
324 }
325}
326
327func whosthere(xid string) []string {
328 obj, err := GetJunk(xid)
329 if err != nil {
330 log.Printf("error getting remote xonk: %s", err)
331 return nil
332 }
333 return newphone(nil, obj)
334}
335
336func newphone(a []string, obj map[string]interface{}) []string {
337 for _, addr := range []string{"to", "cc", "attributedTo"} {
338 who, _ := jsongetstring(obj, addr)
339 if who != "" {
340 a = append(a, who)
341 }
342 whos, _ := jsongetarray(obj, addr)
343 for _, w := range whos {
344 who, _ := w.(string)
345 if who != "" {
346 a = append(a, who)
347 }
348 }
349 }
350 return a
351}
352
353func xonkxonk(item interface{}) *Honk {
354 // id, _ := jsongetstring(item, "id")
355 what, _ := jsongetstring(item, "type")
356 dt, _ := jsongetstring(item, "published")
357
358 var audience []string
359 var err error
360 var xid, rid, url, content string
361 var obj map[string]interface{}
362 switch what {
363 case "Announce":
364 xid, _ = jsongetstring(item, "object")
365 log.Printf("getting bonk: %s", xid)
366 obj, err = GetJunk(xid)
367 if err != nil {
368 log.Printf("error regetting: %s", err)
369 }
370 what = "bonk"
371 case "Create":
372 obj, _ = jsongetmap(item, "object")
373 what = "honk"
374 case "Delete":
375 obj, _ = jsongetmap(item, "object")
376 what = "eradicate"
377 default:
378 log.Printf("unknown activity: %s", what)
379 return nil
380 }
381 who, _ := jsongetstring(item, "actor")
382
383 var xonk Honk
384 if obj != nil {
385 ot, _ := jsongetstring(obj, "type")
386 url, _ = jsongetstring(obj, "url")
387 if ot == "Note" || ot == "Article" {
388 audience = newphone(audience, obj)
389 xid, _ = jsongetstring(obj, "id")
390 content, _ = jsongetstring(obj, "content")
391 summary, _ := jsongetstring(obj, "summary")
392 if summary != "" {
393 content = "<p>summary: " + summary + content
394 }
395 rid, _ = jsongetstring(obj, "inReplyTo")
396 if what == "honk" && rid != "" {
397 what = "tonk"
398 }
399 }
400 if ot == "Tombstone" {
401 rid, _ = jsongetstring(obj, "id")
402 }
403 atts, _ := jsongetarray(obj, "attachment")
404 for _, att := range atts {
405 at, _ := jsongetstring(att, "type")
406 mt, _ := jsongetstring(att, "mediaType")
407 u, _ := jsongetstring(att, "url")
408 name, _ := jsongetstring(att, "name")
409 if at == "Document" {
410 mt = strings.ToLower(mt)
411 log.Printf("attachment: %s %s", mt, u)
412 if mt == "image/jpeg" || mt == "image/png" ||
413 mt == "image/gif" {
414 donk := savedonk(u, name, mt)
415 if donk != nil {
416 xonk.Donks = append(xonk.Donks, donk)
417 }
418 }
419 }
420 }
421 tags, _ := jsongetarray(obj, "tag")
422 for _, tag := range tags {
423 tt, _ := jsongetstring(tag, "type")
424 name, _ := jsongetstring(tag, "name")
425 if tt == "Emoji" {
426 icon, _ := jsongetmap(tag, "icon")
427 mt, _ := jsongetstring(icon, "mediaType")
428 u, _ := jsongetstring(icon, "url")
429 donk := savedonk(u, name, mt)
430 if donk != nil {
431 xonk.Donks = append(xonk.Donks, donk)
432 }
433 }
434 }
435 }
436 audience = append(audience, who)
437
438 audience = oneofakind(audience)
439
440 xonk.What = what
441 xonk.Honker = who
442 xonk.XID = xid
443 xonk.RID = rid
444 xonk.Date, _ = time.Parse(time.RFC3339, dt)
445 xonk.URL = url
446 xonk.Noise = content
447 xonk.Audience = audience
448
449 return &xonk
450}
451
452func rubadubdub(user *WhatAbout, req map[string]interface{}) {
453 xid, _ := jsongetstring(req, "id")
454 reqactor, _ := jsongetstring(req, "actor")
455 j := NewJunk()
456 j["@context"] = itiswhatitis
457 j["id"] = user.URL + "/dub/" + xid
458 j["type"] = "Accept"
459 j["actor"] = user.URL
460 j["to"] = reqactor
461 j["published"] = time.Now().UTC().Format(time.RFC3339)
462 j["object"] = req
463
464 WriteJunk(os.Stdout, j)
465
466 actor, _ := jsongetstring(req, "actor")
467 inbox, _, err := getboxes(actor)
468 if err != nil {
469 log.Printf("can't get dub box: %s", err)
470 return
471 }
472 keyname, key := ziggy(user.Name)
473 err = PostJunk(keyname, key, inbox, j)
474 if err != nil {
475 log.Printf("can't rub a dub: %s", err)
476 return
477 }
478 stmtSaveDub.Exec(user.ID, actor, actor, "dub")
479}
480
481func subsub(user *WhatAbout, xid string) {
482 j := NewJunk()
483 j["@context"] = itiswhatitis
484 j["id"] = user.URL + "/sub/" + xid
485 j["type"] = "Follow"
486 j["actor"] = user.URL
487 j["to"] = xid
488 j["object"] = xid
489 j["published"] = time.Now().UTC().Format(time.RFC3339)
490
491 inbox, _, err := getboxes(xid)
492 if err != nil {
493 log.Printf("can't send follow: %s", err)
494 return
495 }
496 WriteJunk(os.Stdout, j)
497 keyname, key := ziggy(user.Name)
498 err = PostJunk(keyname, key, inbox, j)
499 if err != nil {
500 log.Printf("failed to subsub: %s", err)
501 }
502}
503
504func jonkjonk(user *WhatAbout, h *Honk) (map[string]interface{}, map[string]interface{}) {
505 dt := h.Date.Format(time.RFC3339)
506 var jo map[string]interface{}
507 j := NewJunk()
508 j["id"] = user.URL + "/" + h.What + "/" + h.XID
509 j["actor"] = user.URL
510 j["published"] = dt
511 j["to"] = h.Audience[0]
512 if len(h.Audience) > 1 {
513 j["cc"] = h.Audience[1:]
514 }
515
516 switch h.What {
517 case "tonk":
518 fallthrough
519 case "honk":
520 j["type"] = "Create"
521 jo = NewJunk()
522 jo["id"] = user.URL + "/h/" + h.XID
523 jo["type"] = "Note"
524 jo["published"] = dt
525 jo["url"] = user.URL + "/h/" + h.XID
526 jo["attributedTo"] = user.URL
527 if h.RID != "" {
528 jo["inReplyTo"] = h.RID
529 }
530 jo["to"] = h.Audience[0]
531 if len(h.Audience) > 1 {
532 jo["cc"] = h.Audience[1:]
533 }
534 jo["content"] = h.Noise
535 var tags []interface{}
536 g := bunchofgrapes(h.Noise)
537 for _, m := range g {
538 t := NewJunk()
539 t["type"] = "Mention"
540 t["name"] = m.who
541 t["href"] = m.where
542 tags = append(tags, t)
543 }
544 herd := herdofemus(h.Noise)
545 for _, e := range herd {
546 t := NewJunk()
547 t["id"] = e.ID
548 t["type"] = "Emoji"
549 t["name"] = e.Name
550 i := NewJunk()
551 i["type"] = "Image"
552 i["mediaType"] = "image/png"
553 i["url"] = e.ID
554 t["icon"] = i
555 tags = append(tags, t)
556 }
557 if len(tags) > 0 {
558 jo["tag"] = tags
559 }
560 var atts []interface{}
561 for _, d := range h.Donks {
562 if re_emus.MatchString(d.Name) {
563 continue
564 }
565 jd := NewJunk()
566 jd["mediaType"] = d.Media
567 jd["name"] = d.Name
568 jd["type"] = "Document"
569 jd["url"] = d.URL
570 atts = append(atts, jd)
571 }
572 if len(atts) > 0 {
573 jo["attachment"] = atts
574 }
575 j["object"] = jo
576 case "bonk":
577 j["type"] = "Announce"
578 j["object"] = h.XID
579 }
580
581 return j, jo
582}
583
584func deliverate(username string, rcpt string, msg []byte) {
585 keyname, key := ziggy(username)
586 inbox, _, err := getboxes(rcpt)
587 if err != nil {
588 log.Printf("error getting inbox %s: %s", rcpt, err)
589 return
590 }
591 err = PostMsg(keyname, key, inbox, msg)
592 if err != nil {
593 log.Printf("failed to post json to %s: %s", inbox, err)
594 }
595}
596
597func honkworldwide(user *WhatAbout, honk *Honk) {
598 rcpts := make(map[string]bool)
599 for _, a := range honk.Audience {
600 if a != thewholeworld && a != user.URL {
601 rcpts[a] = true
602 }
603 }
604 jonk, _ := jonkjonk(user, honk)
605 jonk["@context"] = itiswhatitis
606 var buf bytes.Buffer
607 WriteJunk(&buf, jonk)
608 msg := buf.Bytes()
609 for _, f := range getdubs(user.ID) {
610 deliverate(user.Name, f.XID, msg)
611 delete(rcpts, f.XID)
612 }
613 for a := range rcpts {
614 if !strings.HasSuffix(a, "/followers") {
615 deliverate(user.Name, a, msg)
616 }
617 }
618}
619
620func asjonker(user *WhatAbout) map[string]interface{} {
621 about := obfusbreak(user.About)
622
623 j := NewJunk()
624 j["@context"] = itiswhatitis
625 j["id"] = user.URL
626 j["type"] = "Person"
627 j["inbox"] = user.URL + "/inbox"
628 j["outbox"] = user.URL + "/outbox"
629 j["name"] = user.Display
630 j["preferredUsername"] = user.Name
631 j["summary"] = about
632 j["url"] = user.URL
633 a := NewJunk()
634 a["type"] = "icon"
635 a["mediaType"] = "image/png"
636 a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
637 j["icon"] = a
638 k := NewJunk()
639 k["id"] = user.URL + "#key"
640 k["owner"] = user.URL
641 k["publicKeyPem"] = user.Key
642 j["publicKey"] = k
643
644 return j
645}