capturing errors
[akkoma] / test / web / activity_pub / transmogrifier_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
6 use Pleroma.DataCase
7 alias Pleroma.Activity
8 alias Pleroma.Object
9 alias Pleroma.Object.Fetcher
10 alias Pleroma.Repo
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Transmogrifier
14 alias Pleroma.Web.OStatus
15 alias Pleroma.Web.Websub.WebsubClientSubscription
16
17 import Pleroma.Factory
18 import ExUnit.CaptureLog
19 alias Pleroma.Web.CommonAPI
20
21 setup_all do
22 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
23 :ok
24 end
25
26 describe "handle_incoming" do
27 test "it ignores an incoming notice if we already have it" do
28 activity = insert(:note_activity)
29
30 data =
31 File.read!("test/fixtures/mastodon-post-activity.json")
32 |> Poison.decode!()
33 |> Map.put("object", activity.data["object"])
34
35 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
36
37 assert activity == returned_activity
38 end
39
40 test "it fetches replied-to activities if we don't have them" do
41 data =
42 File.read!("test/fixtures/mastodon-post-activity.json")
43 |> Poison.decode!()
44
45 object =
46 data["object"]
47 |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
48
49 data =
50 data
51 |> Map.put("object", object)
52
53 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
54 returned_object = Object.normalize(returned_activity.data["object"])
55
56 assert activity =
57 Activity.get_create_by_object_ap_id(
58 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
59 )
60
61 assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
62 end
63
64 test "it does not crash if the object in inReplyTo can't be fetched" do
65 data =
66 File.read!("test/fixtures/mastodon-post-activity.json")
67 |> Poison.decode!()
68
69 object =
70 data["object"]
71 |> Map.put("inReplyTo", "https://404.site/whatever")
72
73 data =
74 data
75 |> Map.put("object", object)
76
77 assert capture_log(fn ->
78 {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
79 end) =~ "[error] Couldn't fetch \"\"https://404.site/whatever\"\", error: nil"
80 end
81
82 test "it works for incoming notices" do
83 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
84
85 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
86
87 assert data["id"] ==
88 "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
89
90 assert data["context"] ==
91 "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
92
93 assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
94
95 assert data["cc"] == [
96 "http://mastodon.example.org/users/admin/followers",
97 "http://localtesting.pleroma.lol/users/lain"
98 ]
99
100 assert data["actor"] == "http://mastodon.example.org/users/admin"
101
102 object = Object.normalize(data["object"]).data
103 assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822"
104
105 assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
106
107 assert object["cc"] == [
108 "http://mastodon.example.org/users/admin/followers",
109 "http://localtesting.pleroma.lol/users/lain"
110 ]
111
112 assert object["actor"] == "http://mastodon.example.org/users/admin"
113 assert object["attributedTo"] == "http://mastodon.example.org/users/admin"
114
115 assert object["context"] ==
116 "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
117
118 assert object["sensitive"] == true
119
120 user = User.get_cached_by_ap_id(object["actor"])
121
122 assert user.info.note_count == 1
123 end
124
125 test "it works for incoming notices with hashtags" do
126 data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
127
128 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
129 object = Object.normalize(data["object"])
130
131 assert Enum.at(object.data["tag"], 2) == "moo"
132 end
133
134 test "it works for incoming questions" do
135 data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
136
137 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
138
139 object = Object.normalize(activity)
140
141 assert Enum.all?(object.data["oneOf"], fn choice ->
142 choice["name"] in [
143 "Dunno",
144 "Everyone knows that!",
145 "25 char limit is dumb",
146 "I can't even fit a funny"
147 ]
148 end)
149 end
150
151 test "it rewrites Note votes to Answers and increments vote counters on question activities" do
152 user = insert(:user)
153
154 {:ok, activity} =
155 CommonAPI.post(user, %{
156 "status" => "suya...",
157 "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
158 })
159
160 object = Object.normalize(activity)
161
162 data =
163 File.read!("test/fixtures/mastodon-vote.json")
164 |> Poison.decode!()
165 |> Kernel.put_in(["to"], user.ap_id)
166 |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
167 |> Kernel.put_in(["object", "to"], user.ap_id)
168
169 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
170 answer_object = Object.normalize(activity)
171 assert answer_object.data["type"] == "Answer"
172 object = Object.get_by_ap_id(object.data["id"])
173
174 assert Enum.any?(
175 object.data["oneOf"],
176 fn
177 %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
178 _ -> false
179 end
180 )
181 end
182
183 test "it works for incoming notices with contentMap" do
184 data =
185 File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
186
187 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
188 object = Object.normalize(data["object"])
189
190 assert object.data["content"] ==
191 "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
192 end
193
194 test "it works for incoming notices with to/cc not being an array (kroeg)" do
195 data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!()
196
197 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
198 object = Object.normalize(data["object"])
199
200 assert object.data["content"] ==
201 "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
202 end
203
204 test "it works for incoming announces with actor being inlined (kroeg)" do
205 data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!()
206
207 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
208
209 assert data["actor"] == "https://puckipedia.com/"
210 end
211
212 test "it works for incoming notices with tag not being an array (kroeg)" do
213 data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!()
214
215 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
216 object = Object.normalize(data["object"])
217
218 assert object.data["emoji"] == %{
219 "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
220 }
221
222 data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!()
223
224 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
225 object = Object.normalize(data["object"])
226
227 assert "test" in object.data["tag"]
228 end
229
230 test "it works for incoming notices with url not being a string (prismo)" do
231 data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!()
232
233 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
234 object = Object.normalize(data["object"])
235
236 assert object.data["url"] == "https://prismo.news/posts/83"
237 end
238
239 test "it cleans up incoming notices which are not really DMs" do
240 user = insert(:user)
241 other_user = insert(:user)
242
243 to = [user.ap_id, other_user.ap_id]
244
245 data =
246 File.read!("test/fixtures/mastodon-post-activity.json")
247 |> Poison.decode!()
248 |> Map.put("to", to)
249 |> Map.put("cc", [])
250
251 object =
252 data["object"]
253 |> Map.put("to", to)
254 |> Map.put("cc", [])
255
256 data = Map.put(data, "object", object)
257
258 {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
259
260 assert data["to"] == []
261 assert data["cc"] == to
262
263 object_data = Object.normalize(activity).data
264
265 assert object_data["to"] == []
266 assert object_data["cc"] == to
267 end
268
269 test "it works for incoming likes" do
270 user = insert(:user)
271 {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
272
273 data =
274 File.read!("test/fixtures/mastodon-like.json")
275 |> Poison.decode!()
276 |> Map.put("object", activity.data["object"])
277
278 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
279
280 assert data["actor"] == "http://mastodon.example.org/users/admin"
281 assert data["type"] == "Like"
282 assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
283 assert data["object"] == activity.data["object"]
284 end
285
286 test "it returns an error for incoming unlikes wihout a like activity" do
287 user = insert(:user)
288 {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
289
290 data =
291 File.read!("test/fixtures/mastodon-undo-like.json")
292 |> Poison.decode!()
293 |> Map.put("object", activity.data["object"])
294
295 assert Transmogrifier.handle_incoming(data) == :error
296 end
297
298 test "it works for incoming unlikes with an existing like activity" do
299 user = insert(:user)
300 {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
301
302 like_data =
303 File.read!("test/fixtures/mastodon-like.json")
304 |> Poison.decode!()
305 |> Map.put("object", activity.data["object"])
306
307 {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
308
309 data =
310 File.read!("test/fixtures/mastodon-undo-like.json")
311 |> Poison.decode!()
312 |> Map.put("object", like_data)
313 |> Map.put("actor", like_data["actor"])
314
315 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
316
317 assert data["actor"] == "http://mastodon.example.org/users/admin"
318 assert data["type"] == "Undo"
319 assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
320 assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
321 end
322
323 test "it works for incoming announces" do
324 data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
325
326 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
327
328 assert data["actor"] == "http://mastodon.example.org/users/admin"
329 assert data["type"] == "Announce"
330
331 assert data["id"] ==
332 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
333
334 assert data["object"] ==
335 "http://mastodon.example.org/users/admin/statuses/99541947525187367"
336
337 assert Activity.get_create_by_object_ap_id(data["object"])
338 end
339
340 test "it works for incoming announces with an existing activity" do
341 user = insert(:user)
342 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
343
344 data =
345 File.read!("test/fixtures/mastodon-announce.json")
346 |> Poison.decode!()
347 |> Map.put("object", activity.data["object"])
348
349 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
350
351 assert data["actor"] == "http://mastodon.example.org/users/admin"
352 assert data["type"] == "Announce"
353
354 assert data["id"] ==
355 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
356
357 assert data["object"] == activity.data["object"]
358
359 assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
360 end
361
362 test "it does not clobber the addressing on announce activities" do
363 user = insert(:user)
364 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
365
366 data =
367 File.read!("test/fixtures/mastodon-announce.json")
368 |> Poison.decode!()
369 |> Map.put("object", Object.normalize(activity).data["id"])
370 |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"])
371 |> Map.put("cc", [])
372
373 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
374
375 assert data["to"] == ["http://mastodon.example.org/users/admin/followers"]
376 end
377
378 test "it ensures that as:Public activities make it to their followers collection" do
379 user = insert(:user)
380
381 data =
382 File.read!("test/fixtures/mastodon-post-activity.json")
383 |> Poison.decode!()
384 |> Map.put("actor", user.ap_id)
385 |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
386 |> Map.put("cc", [])
387
388 object =
389 data["object"]
390 |> Map.put("attributedTo", user.ap_id)
391 |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
392 |> Map.put("cc", [])
393
394 data = Map.put(data, "object", object)
395
396 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
397
398 assert data["cc"] == [User.ap_followers(user)]
399 end
400
401 test "it ensures that address fields become lists" do
402 user = insert(:user)
403
404 data =
405 File.read!("test/fixtures/mastodon-post-activity.json")
406 |> Poison.decode!()
407 |> Map.put("actor", user.ap_id)
408 |> Map.put("to", nil)
409 |> Map.put("cc", nil)
410
411 object =
412 data["object"]
413 |> Map.put("attributedTo", user.ap_id)
414 |> Map.put("to", nil)
415 |> Map.put("cc", nil)
416
417 data = Map.put(data, "object", object)
418
419 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
420
421 assert !is_nil(data["to"])
422 assert !is_nil(data["cc"])
423 end
424
425 test "it works for incoming update activities" do
426 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
427
428 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
429 update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
430
431 object =
432 update_data["object"]
433 |> Map.put("actor", data["actor"])
434 |> Map.put("id", data["actor"])
435
436 update_data =
437 update_data
438 |> Map.put("actor", data["actor"])
439 |> Map.put("object", object)
440
441 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
442
443 user = User.get_cached_by_ap_id(data["actor"])
444 assert user.name == "gargle"
445
446 assert user.avatar["url"] == [
447 %{
448 "href" =>
449 "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
450 }
451 ]
452
453 assert user.info.banner["url"] == [
454 %{
455 "href" =>
456 "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
457 }
458 ]
459
460 assert user.bio == "<p>Some bio</p>"
461 end
462
463 test "it works for incoming update activities which lock the account" do
464 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
465
466 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
467 update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
468
469 object =
470 update_data["object"]
471 |> Map.put("actor", data["actor"])
472 |> Map.put("id", data["actor"])
473 |> Map.put("manuallyApprovesFollowers", true)
474
475 update_data =
476 update_data
477 |> Map.put("actor", data["actor"])
478 |> Map.put("object", object)
479
480 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
481
482 user = User.get_cached_by_ap_id(data["actor"])
483 assert user.info.locked == true
484 end
485
486 test "it works for incoming deletes" do
487 activity = insert(:note_activity)
488
489 data =
490 File.read!("test/fixtures/mastodon-delete.json")
491 |> Poison.decode!()
492
493 object =
494 data["object"]
495 |> Map.put("id", activity.data["object"])
496
497 data =
498 data
499 |> Map.put("object", object)
500 |> Map.put("actor", activity.data["actor"])
501
502 {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
503
504 refute Activity.get_by_id(activity.id)
505 end
506
507 test "it fails for incoming deletes with spoofed origin" do
508 activity = insert(:note_activity)
509
510 data =
511 File.read!("test/fixtures/mastodon-delete.json")
512 |> Poison.decode!()
513
514 object =
515 data["object"]
516 |> Map.put("id", activity.data["object"])
517
518 data =
519 data
520 |> Map.put("object", object)
521
522 assert capture_log(fn ->
523 :error = Transmogrifier.handle_incoming(data)
524 end) =~
525 "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, {:error, :nxdomain}}"
526
527 assert Activity.get_by_id(activity.id)
528 end
529
530 test "it works for incoming unannounces with an existing notice" do
531 user = insert(:user)
532 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
533
534 announce_data =
535 File.read!("test/fixtures/mastodon-announce.json")
536 |> Poison.decode!()
537 |> Map.put("object", activity.data["object"])
538
539 {:ok, %Activity{data: announce_data, local: false}} =
540 Transmogrifier.handle_incoming(announce_data)
541
542 data =
543 File.read!("test/fixtures/mastodon-undo-announce.json")
544 |> Poison.decode!()
545 |> Map.put("object", announce_data)
546 |> Map.put("actor", announce_data["actor"])
547
548 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
549
550 assert data["type"] == "Undo"
551 assert data["object"]["type"] == "Announce"
552 assert data["object"]["object"] == activity.data["object"]
553
554 assert data["object"]["id"] ==
555 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
556 end
557
558 test "it works for incomming unfollows with an existing follow" do
559 user = insert(:user)
560
561 follow_data =
562 File.read!("test/fixtures/mastodon-follow-activity.json")
563 |> Poison.decode!()
564 |> Map.put("object", user.ap_id)
565
566 {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
567
568 data =
569 File.read!("test/fixtures/mastodon-unfollow-activity.json")
570 |> Poison.decode!()
571 |> Map.put("object", follow_data)
572
573 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
574
575 assert data["type"] == "Undo"
576 assert data["object"]["type"] == "Follow"
577 assert data["object"]["object"] == user.ap_id
578 assert data["actor"] == "http://mastodon.example.org/users/admin"
579
580 refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
581 end
582
583 test "it works for incoming blocks" do
584 user = insert(:user)
585
586 data =
587 File.read!("test/fixtures/mastodon-block-activity.json")
588 |> Poison.decode!()
589 |> Map.put("object", user.ap_id)
590
591 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
592
593 assert data["type"] == "Block"
594 assert data["object"] == user.ap_id
595 assert data["actor"] == "http://mastodon.example.org/users/admin"
596
597 blocker = User.get_cached_by_ap_id(data["actor"])
598
599 assert User.blocks?(blocker, user)
600 end
601
602 test "incoming blocks successfully tear down any follow relationship" do
603 blocker = insert(:user)
604 blocked = insert(:user)
605
606 data =
607 File.read!("test/fixtures/mastodon-block-activity.json")
608 |> Poison.decode!()
609 |> Map.put("object", blocked.ap_id)
610 |> Map.put("actor", blocker.ap_id)
611
612 {:ok, blocker} = User.follow(blocker, blocked)
613 {:ok, blocked} = User.follow(blocked, blocker)
614
615 assert User.following?(blocker, blocked)
616 assert User.following?(blocked, blocker)
617
618 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
619
620 assert data["type"] == "Block"
621 assert data["object"] == blocked.ap_id
622 assert data["actor"] == blocker.ap_id
623
624 blocker = User.get_cached_by_ap_id(data["actor"])
625 blocked = User.get_cached_by_ap_id(data["object"])
626
627 assert User.blocks?(blocker, blocked)
628
629 refute User.following?(blocker, blocked)
630 refute User.following?(blocked, blocker)
631 end
632
633 test "it works for incoming unblocks with an existing block" do
634 user = insert(:user)
635
636 block_data =
637 File.read!("test/fixtures/mastodon-block-activity.json")
638 |> Poison.decode!()
639 |> Map.put("object", user.ap_id)
640
641 {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
642
643 data =
644 File.read!("test/fixtures/mastodon-unblock-activity.json")
645 |> Poison.decode!()
646 |> Map.put("object", block_data)
647
648 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
649 assert data["type"] == "Undo"
650 assert data["object"]["type"] == "Block"
651 assert data["object"]["object"] == user.ap_id
652 assert data["actor"] == "http://mastodon.example.org/users/admin"
653
654 blocker = User.get_cached_by_ap_id(data["actor"])
655
656 refute User.blocks?(blocker, user)
657 end
658
659 test "it works for incoming accepts which were pre-accepted" do
660 follower = insert(:user)
661 followed = insert(:user)
662
663 {:ok, follower} = User.follow(follower, followed)
664 assert User.following?(follower, followed) == true
665
666 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
667
668 accept_data =
669 File.read!("test/fixtures/mastodon-accept-activity.json")
670 |> Poison.decode!()
671 |> Map.put("actor", followed.ap_id)
672
673 object =
674 accept_data["object"]
675 |> Map.put("actor", follower.ap_id)
676 |> Map.put("id", follow_activity.data["id"])
677
678 accept_data = Map.put(accept_data, "object", object)
679
680 {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
681 refute activity.local
682
683 assert activity.data["object"] == follow_activity.data["id"]
684
685 follower = User.get_cached_by_id(follower.id)
686
687 assert User.following?(follower, followed) == true
688 end
689
690 test "it works for incoming accepts which were orphaned" do
691 follower = insert(:user)
692 followed = insert(:user, %{info: %User.Info{locked: true}})
693
694 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
695
696 accept_data =
697 File.read!("test/fixtures/mastodon-accept-activity.json")
698 |> Poison.decode!()
699 |> Map.put("actor", followed.ap_id)
700
701 accept_data =
702 Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
703
704 {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
705 assert activity.data["object"] == follow_activity.data["id"]
706
707 follower = User.get_cached_by_id(follower.id)
708
709 assert User.following?(follower, followed) == true
710 end
711
712 test "it works for incoming accepts which are referenced by IRI only" do
713 follower = insert(:user)
714 followed = insert(:user, %{info: %User.Info{locked: true}})
715
716 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
717
718 accept_data =
719 File.read!("test/fixtures/mastodon-accept-activity.json")
720 |> Poison.decode!()
721 |> Map.put("actor", followed.ap_id)
722 |> Map.put("object", follow_activity.data["id"])
723
724 {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
725 assert activity.data["object"] == follow_activity.data["id"]
726
727 follower = User.get_cached_by_id(follower.id)
728
729 assert User.following?(follower, followed) == true
730 end
731
732 test "it fails for incoming accepts which cannot be correlated" do
733 follower = insert(:user)
734 followed = insert(:user, %{info: %User.Info{locked: true}})
735
736 accept_data =
737 File.read!("test/fixtures/mastodon-accept-activity.json")
738 |> Poison.decode!()
739 |> Map.put("actor", followed.ap_id)
740
741 accept_data =
742 Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
743
744 :error = Transmogrifier.handle_incoming(accept_data)
745
746 follower = User.get_cached_by_id(follower.id)
747
748 refute User.following?(follower, followed) == true
749 end
750
751 test "it fails for incoming rejects which cannot be correlated" do
752 follower = insert(:user)
753 followed = insert(:user, %{info: %User.Info{locked: true}})
754
755 accept_data =
756 File.read!("test/fixtures/mastodon-reject-activity.json")
757 |> Poison.decode!()
758 |> Map.put("actor", followed.ap_id)
759
760 accept_data =
761 Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
762
763 :error = Transmogrifier.handle_incoming(accept_data)
764
765 follower = User.get_cached_by_id(follower.id)
766
767 refute User.following?(follower, followed) == true
768 end
769
770 test "it works for incoming rejects which are orphaned" do
771 follower = insert(:user)
772 followed = insert(:user, %{info: %User.Info{locked: true}})
773
774 {:ok, follower} = User.follow(follower, followed)
775 {:ok, _follow_activity} = ActivityPub.follow(follower, followed)
776
777 assert User.following?(follower, followed) == true
778
779 reject_data =
780 File.read!("test/fixtures/mastodon-reject-activity.json")
781 |> Poison.decode!()
782 |> Map.put("actor", followed.ap_id)
783
784 reject_data =
785 Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id))
786
787 {:ok, activity} = Transmogrifier.handle_incoming(reject_data)
788 refute activity.local
789
790 follower = User.get_cached_by_id(follower.id)
791
792 assert User.following?(follower, followed) == false
793 end
794
795 test "it works for incoming rejects which are referenced by IRI only" do
796 follower = insert(:user)
797 followed = insert(:user, %{info: %User.Info{locked: true}})
798
799 {:ok, follower} = User.follow(follower, followed)
800 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
801
802 assert User.following?(follower, followed) == true
803
804 reject_data =
805 File.read!("test/fixtures/mastodon-reject-activity.json")
806 |> Poison.decode!()
807 |> Map.put("actor", followed.ap_id)
808 |> Map.put("object", follow_activity.data["id"])
809
810 {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
811
812 follower = User.get_cached_by_id(follower.id)
813
814 assert User.following?(follower, followed) == false
815 end
816
817 test "it rejects activities without a valid ID" do
818 user = insert(:user)
819
820 data =
821 File.read!("test/fixtures/mastodon-follow-activity.json")
822 |> Poison.decode!()
823 |> Map.put("object", user.ap_id)
824 |> Map.put("id", "")
825
826 :error = Transmogrifier.handle_incoming(data)
827 end
828
829 test "it remaps video URLs as attachments if necessary" do
830 {:ok, object} =
831 Fetcher.fetch_object_from_id(
832 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
833 )
834
835 attachment = %{
836 "type" => "Link",
837 "mediaType" => "video/mp4",
838 "href" =>
839 "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
840 "mimeType" => "video/mp4",
841 "size" => 5_015_880,
842 "url" => [
843 %{
844 "href" =>
845 "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
846 "mediaType" => "video/mp4",
847 "type" => "Link"
848 }
849 ],
850 "width" => 480
851 }
852
853 assert object.data["url"] ==
854 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
855
856 assert object.data["attachment"] == [attachment]
857 end
858
859 test "it accepts Flag activities" do
860 user = insert(:user)
861 other_user = insert(:user)
862
863 {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
864 object = Object.normalize(activity.data["object"])
865
866 message = %{
867 "@context" => "https://www.w3.org/ns/activitystreams",
868 "cc" => [user.ap_id],
869 "object" => [user.ap_id, object.data["id"]],
870 "type" => "Flag",
871 "content" => "blocked AND reported!!!",
872 "actor" => other_user.ap_id
873 }
874
875 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
876
877 assert activity.data["object"] == [user.ap_id, object.data["id"]]
878 assert activity.data["content"] == "blocked AND reported!!!"
879 assert activity.data["actor"] == other_user.ap_id
880 assert activity.data["cc"] == [user.ap_id]
881 end
882 end
883
884 describe "prepare outgoing" do
885 test "it turns mentions into tags" do
886 user = insert(:user)
887 other_user = insert(:user)
888
889 {:ok, activity} =
890 CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
891
892 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
893 object = modified["object"]
894
895 expected_mention = %{
896 "href" => other_user.ap_id,
897 "name" => "@#{other_user.nickname}",
898 "type" => "Mention"
899 }
900
901 expected_tag = %{
902 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
903 "type" => "Hashtag",
904 "name" => "#2hu"
905 }
906
907 assert Enum.member?(object["tag"], expected_tag)
908 assert Enum.member?(object["tag"], expected_mention)
909 end
910
911 test "it adds the sensitive property" do
912 user = insert(:user)
913
914 {:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"})
915 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
916
917 assert modified["object"]["sensitive"]
918 end
919
920 test "it adds the json-ld context and the conversation property" do
921 user = insert(:user)
922
923 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
924 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
925
926 assert modified["@context"] ==
927 Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"]
928
929 assert modified["object"]["conversation"] == modified["context"]
930 end
931
932 test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
933 user = insert(:user)
934
935 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
936 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
937
938 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
939 end
940
941 test "it translates ostatus IDs to external URLs" do
942 incoming = File.read!("test/fixtures/incoming_note_activity.xml")
943 {:ok, [referent_activity]} = OStatus.handle_incoming(incoming)
944
945 user = insert(:user)
946
947 {:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user)
948 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
949
950 assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29"
951 end
952
953 test "it translates ostatus reply_to IDs to external URLs" do
954 incoming = File.read!("test/fixtures/incoming_note_activity.xml")
955 {:ok, [referred_activity]} = OStatus.handle_incoming(incoming)
956
957 user = insert(:user)
958
959 {:ok, activity} =
960 CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id})
961
962 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
963
964 assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29"
965 end
966
967 test "it strips internal hashtag data" do
968 user = insert(:user)
969
970 {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu"})
971
972 expected_tag = %{
973 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
974 "type" => "Hashtag",
975 "name" => "#2hu"
976 }
977
978 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
979
980 assert modified["object"]["tag"] == [expected_tag]
981 end
982
983 test "it strips internal fields" do
984 user = insert(:user)
985
986 {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu :firefox:"})
987
988 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
989
990 assert length(modified["object"]["tag"]) == 2
991
992 assert is_nil(modified["object"]["emoji"])
993 assert is_nil(modified["object"]["like_count"])
994 assert is_nil(modified["object"]["announcements"])
995 assert is_nil(modified["object"]["announcement_count"])
996 assert is_nil(modified["object"]["context_id"])
997 end
998
999 test "it strips internal fields of article" do
1000 activity = insert(:article_activity)
1001
1002 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1003
1004 assert length(modified["object"]["tag"]) == 2
1005
1006 assert is_nil(modified["object"]["emoji"])
1007 assert is_nil(modified["object"]["like_count"])
1008 assert is_nil(modified["object"]["announcements"])
1009 assert is_nil(modified["object"]["announcement_count"])
1010 assert is_nil(modified["object"]["context_id"])
1011 end
1012
1013 test "it adds like collection to object" do
1014 activity = insert(:note_activity)
1015 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1016
1017 assert modified["object"]["likes"]["type"] == "OrderedCollection"
1018 assert modified["object"]["likes"]["totalItems"] == 0
1019 end
1020
1021 test "the directMessage flag is present" do
1022 user = insert(:user)
1023 other_user = insert(:user)
1024
1025 {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu :moominmamma:"})
1026
1027 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1028
1029 assert modified["directMessage"] == false
1030
1031 {:ok, activity} =
1032 CommonAPI.post(user, %{"status" => "@#{other_user.nickname} :moominmamma:"})
1033
1034 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1035
1036 assert modified["directMessage"] == false
1037
1038 {:ok, activity} =
1039 CommonAPI.post(user, %{
1040 "status" => "@#{other_user.nickname} :moominmamma:",
1041 "visibility" => "direct"
1042 })
1043
1044 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1045
1046 assert modified["directMessage"] == true
1047 end
1048 end
1049
1050 describe "user upgrade" do
1051 test "it upgrades a user to activitypub" do
1052 user =
1053 insert(:user, %{
1054 nickname: "rye@niu.moe",
1055 local: false,
1056 ap_id: "https://niu.moe/users/rye",
1057 follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
1058 })
1059
1060 user_two = insert(:user, %{following: [user.follower_address]})
1061
1062 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1063 {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
1064 assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
1065
1066 user = User.get_cached_by_id(user.id)
1067 assert user.info.note_count == 1
1068
1069 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
1070 assert user.info.ap_enabled
1071 assert user.info.note_count == 1
1072 assert user.follower_address == "https://niu.moe/users/rye/followers"
1073
1074 user = User.get_cached_by_id(user.id)
1075 assert user.info.note_count == 1
1076
1077 activity = Activity.get_by_id(activity.id)
1078 assert user.follower_address in activity.recipients
1079
1080 assert %{
1081 "url" => [
1082 %{
1083 "href" =>
1084 "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
1085 }
1086 ]
1087 } = user.avatar
1088
1089 assert %{
1090 "url" => [
1091 %{
1092 "href" =>
1093 "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
1094 }
1095 ]
1096 } = user.info.banner
1097
1098 refute "..." in activity.recipients
1099
1100 unrelated_activity = Activity.get_by_id(unrelated_activity.id)
1101 refute user.follower_address in unrelated_activity.recipients
1102
1103 user_two = User.get_cached_by_id(user_two.id)
1104 assert user.follower_address in user_two.following
1105 refute "..." in user_two.following
1106 end
1107 end
1108
1109 describe "maybe_retire_websub" do
1110 test "it deletes all websub client subscripitions with the user as topic" do
1111 subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"}
1112 {:ok, ws} = Repo.insert(subscription)
1113
1114 subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"}
1115 {:ok, ws2} = Repo.insert(subscription)
1116
1117 Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye")
1118
1119 refute Repo.get(WebsubClientSubscription, ws.id)
1120 assert Repo.get(WebsubClientSubscription, ws2.id)
1121 end
1122 end
1123
1124 describe "actor rewriting" do
1125 test "it fixes the actor URL property to be a proper URI" do
1126 data = %{
1127 "url" => %{"href" => "http://example.com"}
1128 }
1129
1130 rewritten = Transmogrifier.maybe_fix_user_object(data)
1131 assert rewritten["url"] == "http://example.com"
1132 end
1133 end
1134
1135 describe "actor origin containment" do
1136 test "it rejects activities which reference objects with bogus origins" do
1137 data = %{
1138 "@context" => "https://www.w3.org/ns/activitystreams",
1139 "id" => "http://mastodon.example.org/users/admin/activities/1234",
1140 "actor" => "http://mastodon.example.org/users/admin",
1141 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1142 "object" => "https://info.pleroma.site/activity.json",
1143 "type" => "Announce"
1144 }
1145
1146 :error = Transmogrifier.handle_incoming(data)
1147 end
1148
1149 test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
1150 data = %{
1151 "@context" => "https://www.w3.org/ns/activitystreams",
1152 "id" => "http://mastodon.example.org/users/admin/activities/1234",
1153 "actor" => "http://mastodon.example.org/users/admin",
1154 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1155 "object" => "https://info.pleroma.site/activity2.json",
1156 "type" => "Announce"
1157 }
1158
1159 :error = Transmogrifier.handle_incoming(data)
1160 end
1161
1162 test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
1163 data = %{
1164 "@context" => "https://www.w3.org/ns/activitystreams",
1165 "id" => "http://mastodon.example.org/users/admin/activities/1234",
1166 "actor" => "http://mastodon.example.org/users/admin",
1167 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1168 "object" => "https://info.pleroma.site/activity3.json",
1169 "type" => "Announce"
1170 }
1171
1172 :error = Transmogrifier.handle_incoming(data)
1173 end
1174 end
1175
1176 describe "reserialization" do
1177 test "successfully reserializes a message with inReplyTo == nil" do
1178 user = insert(:user)
1179
1180 message = %{
1181 "@context" => "https://www.w3.org/ns/activitystreams",
1182 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1183 "cc" => [],
1184 "type" => "Create",
1185 "object" => %{
1186 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1187 "cc" => [],
1188 "type" => "Note",
1189 "content" => "Hi",
1190 "inReplyTo" => nil,
1191 "attributedTo" => user.ap_id
1192 },
1193 "actor" => user.ap_id
1194 }
1195
1196 {:ok, activity} = Transmogrifier.handle_incoming(message)
1197
1198 {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
1199 end
1200
1201 test "successfully reserializes a message with AS2 objects in IR" do
1202 user = insert(:user)
1203
1204 message = %{
1205 "@context" => "https://www.w3.org/ns/activitystreams",
1206 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1207 "cc" => [],
1208 "type" => "Create",
1209 "object" => %{
1210 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1211 "cc" => [],
1212 "type" => "Note",
1213 "content" => "Hi",
1214 "inReplyTo" => nil,
1215 "attributedTo" => user.ap_id,
1216 "tag" => [
1217 %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
1218 %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
1219 ]
1220 },
1221 "actor" => user.ap_id
1222 }
1223
1224 {:ok, activity} = Transmogrifier.handle_incoming(message)
1225
1226 {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
1227 end
1228 end
1229
1230 test "Rewrites Answers to Notes" do
1231 user = insert(:user)
1232
1233 {:ok, poll_activity} =
1234 CommonAPI.post(user, %{
1235 "status" => "suya...",
1236 "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
1237 })
1238
1239 poll_object = Object.normalize(poll_activity)
1240 # TODO: Replace with CommonAPI vote creation when implemented
1241 data =
1242 File.read!("test/fixtures/mastodon-vote.json")
1243 |> Poison.decode!()
1244 |> Kernel.put_in(["to"], user.ap_id)
1245 |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
1246 |> Kernel.put_in(["object", "to"], user.ap_id)
1247
1248 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
1249 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
1250
1251 assert data["object"]["type"] == "Note"
1252 end
1253
1254 describe "fix_explicit_addressing" do
1255 setup do
1256 user = insert(:user)
1257 [user: user]
1258 end
1259
1260 test "moves non-explicitly mentioned actors to cc", %{user: user} do
1261 explicitly_mentioned_actors = [
1262 "https://pleroma.gold/users/user1",
1263 "https://pleroma.gold/user2"
1264 ]
1265
1266 object = %{
1267 "actor" => user.ap_id,
1268 "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
1269 "cc" => [],
1270 "tag" =>
1271 Enum.map(explicitly_mentioned_actors, fn href ->
1272 %{"type" => "Mention", "href" => href}
1273 end)
1274 }
1275
1276 fixed_object = Transmogrifier.fix_explicit_addressing(object)
1277 assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
1278 refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
1279 assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
1280 end
1281
1282 test "does not move actor's follower collection to cc", %{user: user} do
1283 object = %{
1284 "actor" => user.ap_id,
1285 "to" => [user.follower_address],
1286 "cc" => []
1287 }
1288
1289 fixed_object = Transmogrifier.fix_explicit_addressing(object)
1290 assert user.follower_address in fixed_object["to"]
1291 refute user.follower_address in fixed_object["cc"]
1292 end
1293
1294 test "removes recipient's follower collection from cc", %{user: user} do
1295 recipient = insert(:user)
1296
1297 object = %{
1298 "actor" => user.ap_id,
1299 "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
1300 "cc" => [user.follower_address, recipient.follower_address]
1301 }
1302
1303 fixed_object = Transmogrifier.fix_explicit_addressing(object)
1304
1305 assert user.follower_address in fixed_object["cc"]
1306 refute recipient.follower_address in fixed_object["cc"]
1307 refute recipient.follower_address in fixed_object["to"]
1308 end
1309 end
1310 end