e39af1dfc1d6b022f181a89b5ec6b7ce6c603a68
[akkoma] / test / pleroma / web / activity_pub / transmogrifier_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
6 use Oban.Testing, repo: Pleroma.Repo
7 use Pleroma.DataCase
8
9 alias Pleroma.Activity
10 alias Pleroma.Object
11 alias Pleroma.Tests.ObanHelpers
12 alias Pleroma.User
13 alias Pleroma.Web.ActivityPub.Transmogrifier
14 alias Pleroma.Web.AdminAPI.AccountView
15 alias Pleroma.Web.CommonAPI
16
17 import Mock
18 import Pleroma.Factory
19 import ExUnit.CaptureLog
20
21 setup_all do
22 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
23 :ok
24 end
25
26 setup do: clear_config([:instance, :max_remote_account_fields])
27
28 describe "handle_incoming" do
29 test "it works for incoming notices with tag not being an array (kroeg)" do
30 data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!()
31
32 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
33 object = Object.normalize(data["object"])
34
35 assert object.data["emoji"] == %{
36 "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
37 }
38
39 data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!()
40
41 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
42 object = Object.normalize(data["object"])
43
44 assert "test" in object.data["tag"]
45 end
46
47 test "it cleans up incoming notices which are not really DMs" do
48 user = insert(:user)
49 other_user = insert(:user)
50
51 to = [user.ap_id, other_user.ap_id]
52
53 data =
54 File.read!("test/fixtures/mastodon-post-activity.json")
55 |> Poison.decode!()
56 |> Map.put("to", to)
57 |> Map.put("cc", [])
58
59 object =
60 data["object"]
61 |> Map.put("to", to)
62 |> Map.put("cc", [])
63
64 data = Map.put(data, "object", object)
65
66 {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
67
68 assert data["to"] == []
69 assert data["cc"] == to
70
71 object_data = Object.normalize(activity).data
72
73 assert object_data["to"] == []
74 assert object_data["cc"] == to
75 end
76
77 test "it ignores an incoming notice if we already have it" do
78 activity = insert(:note_activity)
79
80 data =
81 File.read!("test/fixtures/mastodon-post-activity.json")
82 |> Poison.decode!()
83 |> Map.put("object", Object.normalize(activity).data)
84
85 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
86
87 assert activity == returned_activity
88 end
89
90 @tag capture_log: true
91 test "it fetches reply-to activities if we don't have them" do
92 data =
93 File.read!("test/fixtures/mastodon-post-activity.json")
94 |> Poison.decode!()
95
96 object =
97 data["object"]
98 |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394")
99
100 data = Map.put(data, "object", object)
101 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
102 returned_object = Object.normalize(returned_activity, false)
103
104 assert %Activity{} =
105 Activity.get_create_by_object_ap_id(
106 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
107 )
108
109 assert returned_object.data["inReplyTo"] ==
110 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
111 end
112
113 test "it does not fetch reply-to activities beyond max replies depth limit" do
114 data =
115 File.read!("test/fixtures/mastodon-post-activity.json")
116 |> Poison.decode!()
117
118 object =
119 data["object"]
120 |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
121
122 data = Map.put(data, "object", object)
123
124 with_mock Pleroma.Web.Federator,
125 allowed_thread_distance?: fn _ -> false end do
126 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
127
128 returned_object = Object.normalize(returned_activity, false)
129
130 refute Activity.get_create_by_object_ap_id(
131 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
132 )
133
134 assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873"
135 end
136 end
137
138 test "it does not crash if the object in inReplyTo can't be fetched" do
139 data =
140 File.read!("test/fixtures/mastodon-post-activity.json")
141 |> Poison.decode!()
142
143 object =
144 data["object"]
145 |> Map.put("inReplyTo", "https://404.site/whatever")
146
147 data =
148 data
149 |> Map.put("object", object)
150
151 assert capture_log(fn ->
152 {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
153 end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil"
154 end
155
156 test "it does not work for deactivated users" do
157 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
158
159 insert(:user, ap_id: data["actor"], deactivated: true)
160
161 assert {:error, _} = Transmogrifier.handle_incoming(data)
162 end
163
164 test "it works for incoming notices" do
165 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
166
167 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
168
169 assert data["id"] ==
170 "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
171
172 assert data["context"] ==
173 "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
174
175 assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
176
177 assert data["cc"] == [
178 "http://mastodon.example.org/users/admin/followers",
179 "http://localtesting.pleroma.lol/users/lain"
180 ]
181
182 assert data["actor"] == "http://mastodon.example.org/users/admin"
183
184 object_data = Object.normalize(data["object"]).data
185
186 assert object_data["id"] ==
187 "http://mastodon.example.org/users/admin/statuses/99512778738411822"
188
189 assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
190
191 assert object_data["cc"] == [
192 "http://mastodon.example.org/users/admin/followers",
193 "http://localtesting.pleroma.lol/users/lain"
194 ]
195
196 assert object_data["actor"] == "http://mastodon.example.org/users/admin"
197 assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin"
198
199 assert object_data["context"] ==
200 "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
201
202 assert object_data["sensitive"] == true
203
204 user = User.get_cached_by_ap_id(object_data["actor"])
205
206 assert user.note_count == 1
207 end
208
209 test "it works for incoming notices without the sensitive property but an nsfw hashtag" do
210 data = File.read!("test/fixtures/mastodon-post-activity-nsfw.json") |> Poison.decode!()
211
212 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
213
214 object_data = Object.normalize(data["object"], false).data
215
216 assert object_data["sensitive"] == true
217 end
218
219 test "it works for incoming notices with hashtags" do
220 data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
221
222 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
223 object = Object.normalize(data["object"])
224
225 assert Enum.at(object.data["tag"], 2) == "moo"
226 end
227
228 test "it works for incoming notices with contentMap" do
229 data =
230 File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
231
232 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
233 object = Object.normalize(data["object"])
234
235 assert object.data["content"] ==
236 "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
237 end
238
239 test "it works for incoming notices with to/cc not being an array (kroeg)" do
240 data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!()
241
242 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
243 object = Object.normalize(data["object"])
244
245 assert object.data["content"] ==
246 "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
247 end
248
249 test "it ensures that as:Public activities make it to their followers collection" do
250 user = insert(:user)
251
252 data =
253 File.read!("test/fixtures/mastodon-post-activity.json")
254 |> Poison.decode!()
255 |> Map.put("actor", user.ap_id)
256 |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
257 |> Map.put("cc", [])
258
259 object =
260 data["object"]
261 |> Map.put("attributedTo", user.ap_id)
262 |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
263 |> Map.put("cc", [])
264 |> Map.put("id", user.ap_id <> "/activities/12345678")
265
266 data = Map.put(data, "object", object)
267
268 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
269
270 assert data["cc"] == [User.ap_followers(user)]
271 end
272
273 test "it ensures that address fields become lists" do
274 user = insert(:user)
275
276 data =
277 File.read!("test/fixtures/mastodon-post-activity.json")
278 |> Poison.decode!()
279 |> Map.put("actor", user.ap_id)
280 |> Map.put("to", nil)
281 |> Map.put("cc", nil)
282
283 object =
284 data["object"]
285 |> Map.put("attributedTo", user.ap_id)
286 |> Map.put("to", nil)
287 |> Map.put("cc", nil)
288 |> Map.put("id", user.ap_id <> "/activities/12345678")
289
290 data = Map.put(data, "object", object)
291
292 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
293
294 assert !is_nil(data["to"])
295 assert !is_nil(data["cc"])
296 end
297
298 test "it strips internal likes" do
299 data =
300 File.read!("test/fixtures/mastodon-post-activity.json")
301 |> Poison.decode!()
302
303 likes = %{
304 "first" =>
305 "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
306 "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
307 "totalItems" => 3,
308 "type" => "OrderedCollection"
309 }
310
311 object = Map.put(data["object"], "likes", likes)
312 data = Map.put(data, "object", object)
313
314 {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data)
315
316 refute Map.has_key?(object.data, "likes")
317 end
318
319 test "it strips internal reactions" do
320 user = insert(:user)
321 {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
322 {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
323
324 %{object: object} = Activity.get_by_id_with_object(activity.id)
325 assert Map.has_key?(object.data, "reactions")
326 assert Map.has_key?(object.data, "reaction_count")
327
328 object_data = Transmogrifier.strip_internal_fields(object.data)
329 refute Map.has_key?(object_data, "reactions")
330 refute Map.has_key?(object_data, "reaction_count")
331 end
332
333 test "it works for incoming unfollows with an existing follow" do
334 user = insert(:user)
335
336 follow_data =
337 File.read!("test/fixtures/mastodon-follow-activity.json")
338 |> Poison.decode!()
339 |> Map.put("object", user.ap_id)
340
341 {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
342
343 data =
344 File.read!("test/fixtures/mastodon-unfollow-activity.json")
345 |> Poison.decode!()
346 |> Map.put("object", follow_data)
347
348 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
349
350 assert data["type"] == "Undo"
351 assert data["object"]["type"] == "Follow"
352 assert data["object"]["object"] == user.ap_id
353 assert data["actor"] == "http://mastodon.example.org/users/admin"
354
355 refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
356 end
357
358 test "it accepts Flag activities" do
359 user = insert(:user)
360 other_user = insert(:user)
361
362 {:ok, activity} = CommonAPI.post(user, %{status: "test post"})
363 object = Object.normalize(activity)
364
365 note_obj = %{
366 "type" => "Note",
367 "id" => activity.data["id"],
368 "content" => "test post",
369 "published" => object.data["published"],
370 "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true})
371 }
372
373 message = %{
374 "@context" => "https://www.w3.org/ns/activitystreams",
375 "cc" => [user.ap_id],
376 "object" => [user.ap_id, activity.data["id"]],
377 "type" => "Flag",
378 "content" => "blocked AND reported!!!",
379 "actor" => other_user.ap_id
380 }
381
382 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
383
384 assert activity.data["object"] == [user.ap_id, note_obj]
385 assert activity.data["content"] == "blocked AND reported!!!"
386 assert activity.data["actor"] == other_user.ap_id
387 assert activity.data["cc"] == [user.ap_id]
388 end
389
390 test "it correctly processes messages with non-array to field" do
391 user = insert(:user)
392
393 message = %{
394 "@context" => "https://www.w3.org/ns/activitystreams",
395 "to" => "https://www.w3.org/ns/activitystreams#Public",
396 "type" => "Create",
397 "object" => %{
398 "content" => "blah blah blah",
399 "type" => "Note",
400 "attributedTo" => user.ap_id,
401 "inReplyTo" => nil
402 },
403 "actor" => user.ap_id
404 }
405
406 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
407
408 assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
409 end
410
411 test "it correctly processes messages with non-array cc field" do
412 user = insert(:user)
413
414 message = %{
415 "@context" => "https://www.w3.org/ns/activitystreams",
416 "to" => user.follower_address,
417 "cc" => "https://www.w3.org/ns/activitystreams#Public",
418 "type" => "Create",
419 "object" => %{
420 "content" => "blah blah blah",
421 "type" => "Note",
422 "attributedTo" => user.ap_id,
423 "inReplyTo" => nil
424 },
425 "actor" => user.ap_id
426 }
427
428 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
429
430 assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
431 assert [user.follower_address] == activity.data["to"]
432 end
433
434 test "it correctly processes messages with weirdness in address fields" do
435 user = insert(:user)
436
437 message = %{
438 "@context" => "https://www.w3.org/ns/activitystreams",
439 "to" => [nil, user.follower_address],
440 "cc" => ["https://www.w3.org/ns/activitystreams#Public", ["¿"]],
441 "type" => "Create",
442 "object" => %{
443 "content" => "…",
444 "type" => "Note",
445 "attributedTo" => user.ap_id,
446 "inReplyTo" => nil
447 },
448 "actor" => user.ap_id
449 }
450
451 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
452
453 assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
454 assert [user.follower_address] == activity.data["to"]
455 end
456
457 test "it accepts Move activities" do
458 old_user = insert(:user)
459 new_user = insert(:user)
460
461 message = %{
462 "@context" => "https://www.w3.org/ns/activitystreams",
463 "type" => "Move",
464 "actor" => old_user.ap_id,
465 "object" => old_user.ap_id,
466 "target" => new_user.ap_id
467 }
468
469 assert :error = Transmogrifier.handle_incoming(message)
470
471 {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
472
473 assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
474 assert activity.actor == old_user.ap_id
475 assert activity.data["actor"] == old_user.ap_id
476 assert activity.data["object"] == old_user.ap_id
477 assert activity.data["target"] == new_user.ap_id
478 assert activity.data["type"] == "Move"
479 end
480 end
481
482 describe "`handle_incoming/2`, Mastodon format `replies` handling" do
483 setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
484 setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
485
486 setup do
487 data =
488 "test/fixtures/mastodon-post-activity.json"
489 |> File.read!()
490 |> Poison.decode!()
491
492 items = get_in(data, ["object", "replies", "first", "items"])
493 assert length(items) > 0
494
495 %{data: data, items: items}
496 end
497
498 test "schedules background fetching of `replies` items if max thread depth limit allows", %{
499 data: data,
500 items: items
501 } do
502 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
503
504 {:ok, _activity} = Transmogrifier.handle_incoming(data)
505
506 for id <- items do
507 job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
508 assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
509 end
510 end
511
512 test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
513 %{data: data} do
514 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
515
516 {:ok, _activity} = Transmogrifier.handle_incoming(data)
517
518 assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
519 end
520 end
521
522 describe "`handle_incoming/2`, Pleroma format `replies` handling" do
523 setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
524 setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
525
526 setup do
527 user = insert(:user)
528
529 {:ok, activity} = CommonAPI.post(user, %{status: "post1"})
530
531 {:ok, reply1} =
532 CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id})
533
534 {:ok, reply2} =
535 CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: activity.id})
536
537 replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end)
538
539 {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data)
540
541 Repo.delete(activity.object)
542 Repo.delete(activity)
543
544 %{federation_output: federation_output, replies_uris: replies_uris}
545 end
546
547 test "schedules background fetching of `replies` items if max thread depth limit allows", %{
548 federation_output: federation_output,
549 replies_uris: replies_uris
550 } do
551 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1)
552
553 {:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
554
555 for id <- replies_uris do
556 job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
557 assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
558 end
559 end
560
561 test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
562 %{federation_output: federation_output} do
563 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
564
565 {:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
566
567 assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
568 end
569 end
570
571 describe "prepare outgoing" do
572 test "it inlines private announced objects" do
573 user = insert(:user)
574
575 {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"})
576
577 {:ok, announce_activity} = CommonAPI.repeat(activity.id, user)
578
579 {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
580
581 assert modified["object"]["content"] == "hey"
582 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
583 end
584
585 test "it turns mentions into tags" do
586 user = insert(:user)
587 other_user = insert(:user)
588
589 {:ok, activity} =
590 CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"})
591
592 with_mock Pleroma.Notification,
593 get_notified_from_activity: fn _, _ -> [] end do
594 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
595
596 object = modified["object"]
597
598 expected_mention = %{
599 "href" => other_user.ap_id,
600 "name" => "@#{other_user.nickname}",
601 "type" => "Mention"
602 }
603
604 expected_tag = %{
605 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
606 "type" => "Hashtag",
607 "name" => "#2hu"
608 }
609
610 refute called(Pleroma.Notification.get_notified_from_activity(:_, :_))
611 assert Enum.member?(object["tag"], expected_tag)
612 assert Enum.member?(object["tag"], expected_mention)
613 end
614 end
615
616 test "it adds the sensitive property" do
617 user = insert(:user)
618
619 {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"})
620 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
621
622 assert modified["object"]["sensitive"]
623 end
624
625 test "it adds the json-ld context and the conversation property" do
626 user = insert(:user)
627
628 {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
629 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
630
631 assert modified["@context"] ==
632 Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"]
633
634 assert modified["object"]["conversation"] == modified["context"]
635 end
636
637 test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
638 user = insert(:user)
639
640 {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
641 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
642
643 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
644 end
645
646 test "it strips internal hashtag data" do
647 user = insert(:user)
648
649 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"})
650
651 expected_tag = %{
652 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
653 "type" => "Hashtag",
654 "name" => "#2hu"
655 }
656
657 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
658
659 assert modified["object"]["tag"] == [expected_tag]
660 end
661
662 test "it strips internal fields" do
663 user = insert(:user)
664
665 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu :firefox:"})
666
667 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
668
669 assert length(modified["object"]["tag"]) == 2
670
671 assert is_nil(modified["object"]["emoji"])
672 assert is_nil(modified["object"]["like_count"])
673 assert is_nil(modified["object"]["announcements"])
674 assert is_nil(modified["object"]["announcement_count"])
675 assert is_nil(modified["object"]["context_id"])
676 end
677
678 test "it strips internal fields of article" do
679 activity = insert(:article_activity)
680
681 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
682
683 assert length(modified["object"]["tag"]) == 2
684
685 assert is_nil(modified["object"]["emoji"])
686 assert is_nil(modified["object"]["like_count"])
687 assert is_nil(modified["object"]["announcements"])
688 assert is_nil(modified["object"]["announcement_count"])
689 assert is_nil(modified["object"]["context_id"])
690 assert is_nil(modified["object"]["likes"])
691 end
692
693 test "the directMessage flag is present" do
694 user = insert(:user)
695 other_user = insert(:user)
696
697 {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"})
698
699 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
700
701 assert modified["directMessage"] == false
702
703 {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"})
704
705 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
706
707 assert modified["directMessage"] == false
708
709 {:ok, activity} =
710 CommonAPI.post(user, %{
711 status: "@#{other_user.nickname} :moominmamma:",
712 visibility: "direct"
713 })
714
715 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
716
717 assert modified["directMessage"] == true
718 end
719
720 test "it strips BCC field" do
721 user = insert(:user)
722 {:ok, list} = Pleroma.List.create("foo", user)
723
724 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
725
726 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
727
728 assert is_nil(modified["bcc"])
729 end
730
731 test "it can handle Listen activities" do
732 listen_activity = insert(:listen)
733
734 {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
735
736 assert modified["type"] == "Listen"
737
738 user = insert(:user)
739
740 {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
741
742 {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
743 end
744 end
745
746 describe "user upgrade" do
747 test "it upgrades a user to activitypub" do
748 user =
749 insert(:user, %{
750 nickname: "rye@niu.moe",
751 local: false,
752 ap_id: "https://niu.moe/users/rye",
753 follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
754 })
755
756 user_two = insert(:user)
757 Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
758
759 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
760 {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"})
761 assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
762
763 user = User.get_cached_by_id(user.id)
764 assert user.note_count == 1
765
766 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
767 ObanHelpers.perform_all()
768
769 assert user.ap_enabled
770 assert user.note_count == 1
771 assert user.follower_address == "https://niu.moe/users/rye/followers"
772 assert user.following_address == "https://niu.moe/users/rye/following"
773
774 user = User.get_cached_by_id(user.id)
775 assert user.note_count == 1
776
777 activity = Activity.get_by_id(activity.id)
778 assert user.follower_address in activity.recipients
779
780 assert %{
781 "url" => [
782 %{
783 "href" =>
784 "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
785 }
786 ]
787 } = user.avatar
788
789 assert %{
790 "url" => [
791 %{
792 "href" =>
793 "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
794 }
795 ]
796 } = user.banner
797
798 refute "..." in activity.recipients
799
800 unrelated_activity = Activity.get_by_id(unrelated_activity.id)
801 refute user.follower_address in unrelated_activity.recipients
802
803 user_two = User.get_cached_by_id(user_two.id)
804 assert User.following?(user_two, user)
805 refute "..." in User.following(user_two)
806 end
807 end
808
809 describe "actor rewriting" do
810 test "it fixes the actor URL property to be a proper URI" do
811 data = %{
812 "url" => %{"href" => "http://example.com"}
813 }
814
815 rewritten = Transmogrifier.maybe_fix_user_object(data)
816 assert rewritten["url"] == "http://example.com"
817 end
818 end
819
820 describe "actor origin containment" do
821 test "it rejects activities which reference objects with bogus origins" do
822 data = %{
823 "@context" => "https://www.w3.org/ns/activitystreams",
824 "id" => "http://mastodon.example.org/users/admin/activities/1234",
825 "actor" => "http://mastodon.example.org/users/admin",
826 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
827 "object" => "https://info.pleroma.site/activity.json",
828 "type" => "Announce"
829 }
830
831 assert capture_log(fn ->
832 {:error, _} = Transmogrifier.handle_incoming(data)
833 end) =~ "Object containment failed"
834 end
835
836 test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
837 data = %{
838 "@context" => "https://www.w3.org/ns/activitystreams",
839 "id" => "http://mastodon.example.org/users/admin/activities/1234",
840 "actor" => "http://mastodon.example.org/users/admin",
841 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
842 "object" => "https://info.pleroma.site/activity2.json",
843 "type" => "Announce"
844 }
845
846 assert capture_log(fn ->
847 {:error, _} = Transmogrifier.handle_incoming(data)
848 end) =~ "Object containment failed"
849 end
850
851 test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
852 data = %{
853 "@context" => "https://www.w3.org/ns/activitystreams",
854 "id" => "http://mastodon.example.org/users/admin/activities/1234",
855 "actor" => "http://mastodon.example.org/users/admin",
856 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
857 "object" => "https://info.pleroma.site/activity3.json",
858 "type" => "Announce"
859 }
860
861 assert capture_log(fn ->
862 {:error, _} = Transmogrifier.handle_incoming(data)
863 end) =~ "Object containment failed"
864 end
865 end
866
867 describe "reserialization" do
868 test "successfully reserializes a message with inReplyTo == nil" do
869 user = insert(:user)
870
871 message = %{
872 "@context" => "https://www.w3.org/ns/activitystreams",
873 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
874 "cc" => [],
875 "type" => "Create",
876 "object" => %{
877 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
878 "cc" => [],
879 "type" => "Note",
880 "content" => "Hi",
881 "inReplyTo" => nil,
882 "attributedTo" => user.ap_id
883 },
884 "actor" => user.ap_id
885 }
886
887 {:ok, activity} = Transmogrifier.handle_incoming(message)
888
889 {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
890 end
891
892 test "successfully reserializes a message with AS2 objects in IR" do
893 user = insert(:user)
894
895 message = %{
896 "@context" => "https://www.w3.org/ns/activitystreams",
897 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
898 "cc" => [],
899 "type" => "Create",
900 "object" => %{
901 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
902 "cc" => [],
903 "type" => "Note",
904 "content" => "Hi",
905 "inReplyTo" => nil,
906 "attributedTo" => user.ap_id,
907 "tag" => [
908 %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
909 %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
910 ]
911 },
912 "actor" => user.ap_id
913 }
914
915 {:ok, activity} = Transmogrifier.handle_incoming(message)
916
917 {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
918 end
919 end
920
921 describe "fix_explicit_addressing" do
922 setup do
923 user = insert(:user)
924 [user: user]
925 end
926
927 test "moves non-explicitly mentioned actors to cc", %{user: user} do
928 explicitly_mentioned_actors = [
929 "https://pleroma.gold/users/user1",
930 "https://pleroma.gold/user2"
931 ]
932
933 object = %{
934 "actor" => user.ap_id,
935 "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
936 "cc" => [],
937 "tag" =>
938 Enum.map(explicitly_mentioned_actors, fn href ->
939 %{"type" => "Mention", "href" => href}
940 end)
941 }
942
943 fixed_object = Transmogrifier.fix_explicit_addressing(object)
944 assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
945 refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
946 assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
947 end
948
949 test "does not move actor's follower collection to cc", %{user: user} do
950 object = %{
951 "actor" => user.ap_id,
952 "to" => [user.follower_address],
953 "cc" => []
954 }
955
956 fixed_object = Transmogrifier.fix_explicit_addressing(object)
957 assert user.follower_address in fixed_object["to"]
958 refute user.follower_address in fixed_object["cc"]
959 end
960
961 test "removes recipient's follower collection from cc", %{user: user} do
962 recipient = insert(:user)
963
964 object = %{
965 "actor" => user.ap_id,
966 "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
967 "cc" => [user.follower_address, recipient.follower_address]
968 }
969
970 fixed_object = Transmogrifier.fix_explicit_addressing(object)
971
972 assert user.follower_address in fixed_object["cc"]
973 refute recipient.follower_address in fixed_object["cc"]
974 refute recipient.follower_address in fixed_object["to"]
975 end
976 end
977
978 describe "fix_summary/1" do
979 test "returns fixed object" do
980 assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
981 assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
982 assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
983 end
984 end
985
986 describe "fix_in_reply_to/2" do
987 setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
988
989 setup do
990 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
991 [data: data]
992 end
993
994 test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
995 assert Transmogrifier.fix_in_reply_to(data) == data
996 end
997
998 test "returns object with inReplyTo when denied incoming reply", %{data: data} do
999 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
1000
1001 object_with_reply =
1002 Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
1003
1004 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1005 assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
1006
1007 object_with_reply =
1008 Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
1009
1010 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1011 assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
1012
1013 object_with_reply =
1014 Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
1015
1016 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1017 assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
1018
1019 object_with_reply = Map.put(data["object"], "inReplyTo", [])
1020 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1021 assert modified_object["inReplyTo"] == []
1022 end
1023
1024 @tag capture_log: true
1025 test "returns modified object when allowed incoming reply", %{data: data} do
1026 object_with_reply =
1027 Map.put(
1028 data["object"],
1029 "inReplyTo",
1030 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
1031 )
1032
1033 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
1034 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1035
1036 assert modified_object["inReplyTo"] ==
1037 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
1038
1039 assert modified_object["context"] ==
1040 "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4"
1041 end
1042 end
1043
1044 describe "fix_url/1" do
1045 test "fixes data for object when url is map" do
1046 object = %{
1047 "url" => %{
1048 "type" => "Link",
1049 "mimeType" => "video/mp4",
1050 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
1051 }
1052 }
1053
1054 assert Transmogrifier.fix_url(object) == %{
1055 "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
1056 }
1057 end
1058
1059 test "returns non-modified object" do
1060 assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
1061 end
1062 end
1063
1064 describe "get_obj_helper/2" do
1065 test "returns nil when cannot normalize object" do
1066 assert capture_log(fn ->
1067 refute Transmogrifier.get_obj_helper("test-obj-id")
1068 end) =~ "Unsupported URI scheme"
1069 end
1070
1071 @tag capture_log: true
1072 test "returns {:ok, %Object{}} for success case" do
1073 assert {:ok, %Object{}} =
1074 Transmogrifier.get_obj_helper(
1075 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
1076 )
1077 end
1078 end
1079
1080 describe "fix_attachments/1" do
1081 test "returns not modified object" do
1082 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
1083 assert Transmogrifier.fix_attachments(data) == data
1084 end
1085
1086 test "returns modified object when attachment is map" do
1087 assert Transmogrifier.fix_attachments(%{
1088 "attachment" => %{
1089 "mediaType" => "video/mp4",
1090 "url" => "https://peertube.moe/stat-480.mp4"
1091 }
1092 }) == %{
1093 "attachment" => [
1094 %{
1095 "mediaType" => "video/mp4",
1096 "type" => "Document",
1097 "url" => [
1098 %{
1099 "href" => "https://peertube.moe/stat-480.mp4",
1100 "mediaType" => "video/mp4",
1101 "type" => "Link"
1102 }
1103 ]
1104 }
1105 ]
1106 }
1107 end
1108
1109 test "returns modified object when attachment is list" do
1110 assert Transmogrifier.fix_attachments(%{
1111 "attachment" => [
1112 %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
1113 %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
1114 ]
1115 }) == %{
1116 "attachment" => [
1117 %{
1118 "mediaType" => "video/mp4",
1119 "type" => "Document",
1120 "url" => [
1121 %{
1122 "href" => "https://pe.er/stat-480.mp4",
1123 "mediaType" => "video/mp4",
1124 "type" => "Link"
1125 }
1126 ]
1127 },
1128 %{
1129 "mediaType" => "video/mp4",
1130 "type" => "Document",
1131 "url" => [
1132 %{
1133 "href" => "https://pe.er/stat-480.mp4",
1134 "mediaType" => "video/mp4",
1135 "type" => "Link"
1136 }
1137 ]
1138 }
1139 ]
1140 }
1141 end
1142 end
1143
1144 describe "fix_emoji/1" do
1145 test "returns not modified object when object not contains tags" do
1146 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
1147 assert Transmogrifier.fix_emoji(data) == data
1148 end
1149
1150 test "returns object with emoji when object contains list tags" do
1151 assert Transmogrifier.fix_emoji(%{
1152 "tag" => [
1153 %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
1154 %{"type" => "Hashtag"}
1155 ]
1156 }) == %{
1157 "emoji" => %{"bib" => "/test"},
1158 "tag" => [
1159 %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
1160 %{"type" => "Hashtag"}
1161 ]
1162 }
1163 end
1164
1165 test "returns object with emoji when object contains map tag" do
1166 assert Transmogrifier.fix_emoji(%{
1167 "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
1168 }) == %{
1169 "emoji" => %{"bib" => "/test"},
1170 "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
1171 }
1172 end
1173 end
1174
1175 describe "set_replies/1" do
1176 setup do: clear_config([:activitypub, :note_replies_output_limit], 2)
1177
1178 test "returns unmodified object if activity doesn't have self-replies" do
1179 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
1180 assert Transmogrifier.set_replies(data) == data
1181 end
1182
1183 test "sets `replies` collection with a limited number of self-replies" do
1184 [user, another_user] = insert_list(2, :user)
1185
1186 {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"})
1187
1188 {:ok, %{id: id2} = self_reply1} =
1189 CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1})
1190
1191 {:ok, self_reply2} =
1192 CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1})
1193
1194 # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
1195 {:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1})
1196
1197 {:ok, _} =
1198 CommonAPI.post(user, %{
1199 status: "self-reply to self-reply",
1200 in_reply_to_status_id: id2
1201 })
1202
1203 {:ok, _} =
1204 CommonAPI.post(another_user, %{
1205 status: "another user's reply",
1206 in_reply_to_status_id: id1
1207 })
1208
1209 object = Object.normalize(activity)
1210 replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
1211
1212 assert %{"type" => "Collection", "items" => ^replies_uris} =
1213 Transmogrifier.set_replies(object.data)["replies"]
1214 end
1215 end
1216
1217 test "take_emoji_tags/1" do
1218 user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}})
1219
1220 assert Transmogrifier.take_emoji_tags(user) == [
1221 %{
1222 "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
1223 "id" => "https://example.org/firefox.png",
1224 "name" => ":firefox:",
1225 "type" => "Emoji",
1226 "updated" => "1970-01-01T00:00:00Z"
1227 }
1228 ]
1229 end
1230 end