Let pins federate
[akkoma] / test / pleroma / web / activity_pub / transmogrifier_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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 require Pleroma.Constants
10
11 alias Pleroma.Activity
12 alias Pleroma.Object
13 alias Pleroma.Tests.ObanHelpers
14 alias Pleroma.User
15 alias Pleroma.Web.ActivityPub.Transmogrifier
16 alias Pleroma.Web.AdminAPI.AccountView
17 alias Pleroma.Web.CommonAPI
18
19 import Mock
20 import Pleroma.Factory
21 import ExUnit.CaptureLog
22
23 setup_all do
24 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
25 :ok
26 end
27
28 setup do: clear_config([:instance, :max_remote_account_fields])
29
30 describe "handle_incoming" do
31 test "it works for incoming unfollows with an existing follow" do
32 user = insert(:user)
33
34 follow_data =
35 File.read!("test/fixtures/mastodon-follow-activity.json")
36 |> Jason.decode!()
37 |> Map.put("object", user.ap_id)
38
39 {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
40
41 data =
42 File.read!("test/fixtures/mastodon-unfollow-activity.json")
43 |> Jason.decode!()
44 |> Map.put("object", follow_data)
45
46 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
47
48 assert data["type"] == "Undo"
49 assert data["object"]["type"] == "Follow"
50 assert data["object"]["object"] == user.ap_id
51 assert data["actor"] == "http://mastodon.example.org/users/admin"
52
53 refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
54 end
55
56 test "it accepts Flag activities" do
57 user = insert(:user)
58 other_user = insert(:user)
59
60 {:ok, activity} = CommonAPI.post(user, %{status: "test post"})
61 object = Object.normalize(activity, fetch: false)
62
63 note_obj = %{
64 "type" => "Note",
65 "id" => activity.data["id"],
66 "content" => "test post",
67 "published" => object.data["published"],
68 "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true})
69 }
70
71 message = %{
72 "@context" => "https://www.w3.org/ns/activitystreams",
73 "cc" => [user.ap_id],
74 "object" => [user.ap_id, activity.data["id"]],
75 "type" => "Flag",
76 "content" => "blocked AND reported!!!",
77 "actor" => other_user.ap_id
78 }
79
80 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
81
82 assert activity.data["object"] == [user.ap_id, note_obj]
83 assert activity.data["content"] == "blocked AND reported!!!"
84 assert activity.data["actor"] == other_user.ap_id
85 assert activity.data["cc"] == [user.ap_id]
86 end
87
88 test "it accepts Move activities" do
89 old_user = insert(:user)
90 new_user = insert(:user)
91
92 message = %{
93 "@context" => "https://www.w3.org/ns/activitystreams",
94 "type" => "Move",
95 "actor" => old_user.ap_id,
96 "object" => old_user.ap_id,
97 "target" => new_user.ap_id
98 }
99
100 assert :error = Transmogrifier.handle_incoming(message)
101
102 {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
103
104 assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
105 assert activity.actor == old_user.ap_id
106 assert activity.data["actor"] == old_user.ap_id
107 assert activity.data["object"] == old_user.ap_id
108 assert activity.data["target"] == new_user.ap_id
109 assert activity.data["type"] == "Move"
110 end
111
112 test "it accepts Add/Remove activities" do
113 user =
114 "test/fixtures/users_mock/user.json"
115 |> File.read!()
116 |> String.replace("{{nickname}}", "lain")
117
118 object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
119
120 object =
121 "test/fixtures/statuses/note.json"
122 |> File.read!()
123 |> String.replace("{{nickname}}", "lain")
124 |> String.replace("{{object_id}}", object_id)
125
126 object_url = "https://example.com/objects/#{object_id}"
127
128 actor = "https://example.com/users/lain"
129
130 Tesla.Mock.mock(fn
131 %{
132 method: :get,
133 url: ^actor
134 } ->
135 %Tesla.Env{
136 status: 200,
137 body: user,
138 headers: [{"content-type", "application/activity+json"}]
139 }
140
141 %{
142 method: :get,
143 url: ^object_url
144 } ->
145 %Tesla.Env{
146 status: 200,
147 body: object,
148 headers: [{"content-type", "application/activity+json"}]
149 }
150 end)
151
152 message = %{
153 "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
154 "actor" => actor,
155 "object" => object_url,
156 "target" => "https://example.com/users/lain/collections/featured",
157 "type" => "Add",
158 "to" => [Pleroma.Constants.as_public()],
159 "cc" => ["https://example.com/users/lain/followers"]
160 }
161
162 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
163 assert activity.data == message
164 user = User.get_cached_by_ap_id(actor)
165 assert user.pinned_objects[object_url]
166
167 remove = %{
168 "id" => "http://localhost:400/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
169 "actor" => actor,
170 "object" => object_url,
171 "target" => "http://example.com/users/lain/collections/featured",
172 "type" => "Remove",
173 "to" => [Pleroma.Constants.as_public()],
174 "cc" => ["https://example.com/users/lain/followers"]
175 }
176
177 assert {:ok, activity} = Transmogrifier.handle_incoming(remove)
178 assert activity.data == remove
179
180 user = refresh_record(user)
181 refute user.pinned_objects[object_url]
182 end
183 end
184
185 describe "prepare outgoing" do
186 test "it inlines private announced objects" do
187 user = insert(:user)
188
189 {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"})
190
191 {:ok, announce_activity} = CommonAPI.repeat(activity.id, user)
192
193 {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
194
195 assert modified["object"]["content"] == "hey"
196 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
197 end
198
199 test "it turns mentions into tags" do
200 user = insert(:user)
201 other_user = insert(:user)
202
203 {:ok, activity} =
204 CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"})
205
206 with_mock Pleroma.Notification,
207 get_notified_from_activity: fn _, _ -> [] end do
208 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
209
210 object = modified["object"]
211
212 expected_mention = %{
213 "href" => other_user.ap_id,
214 "name" => "@#{other_user.nickname}",
215 "type" => "Mention"
216 }
217
218 expected_tag = %{
219 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
220 "type" => "Hashtag",
221 "name" => "#2hu"
222 }
223
224 refute called(Pleroma.Notification.get_notified_from_activity(:_, :_))
225 assert Enum.member?(object["tag"], expected_tag)
226 assert Enum.member?(object["tag"], expected_mention)
227 end
228 end
229
230 test "it adds the json-ld context and the conversation property" do
231 user = insert(:user)
232
233 {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
234 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
235
236 assert modified["@context"] ==
237 Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"]
238
239 assert modified["object"]["conversation"] == modified["context"]
240 end
241
242 test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
243 user = insert(:user)
244
245 {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
246 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
247
248 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
249 end
250
251 test "it strips internal hashtag data" do
252 user = insert(:user)
253
254 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"})
255
256 expected_tag = %{
257 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
258 "type" => "Hashtag",
259 "name" => "#2hu"
260 }
261
262 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
263
264 assert modified["object"]["tag"] == [expected_tag]
265 end
266
267 test "it strips internal fields" do
268 user = insert(:user)
269
270 {:ok, activity} =
271 CommonAPI.post(user, %{
272 status: "#2hu :firefox:",
273 generator: %{type: "Application", name: "TestClient", url: "https://pleroma.social"}
274 })
275
276 # Ensure injected application data made it into the activity
277 # as we don't have a Token to derive it from, otherwise it will
278 # be nil and the test will pass
279 assert %{
280 type: "Application",
281 name: "TestClient",
282 url: "https://pleroma.social"
283 } == activity.object.data["generator"]
284
285 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
286
287 assert length(modified["object"]["tag"]) == 2
288
289 assert is_nil(modified["object"]["emoji"])
290 assert is_nil(modified["object"]["like_count"])
291 assert is_nil(modified["object"]["announcements"])
292 assert is_nil(modified["object"]["announcement_count"])
293 assert is_nil(modified["object"]["context_id"])
294 assert is_nil(modified["object"]["generator"])
295 end
296
297 test "it strips internal fields of article" do
298 activity = insert(:article_activity)
299
300 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
301
302 assert length(modified["object"]["tag"]) == 2
303
304 assert is_nil(modified["object"]["emoji"])
305 assert is_nil(modified["object"]["like_count"])
306 assert is_nil(modified["object"]["announcements"])
307 assert is_nil(modified["object"]["announcement_count"])
308 assert is_nil(modified["object"]["context_id"])
309 assert is_nil(modified["object"]["likes"])
310 end
311
312 test "the directMessage flag is present" do
313 user = insert(:user)
314 other_user = insert(:user)
315
316 {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"})
317
318 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
319
320 assert modified["directMessage"] == false
321
322 {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"})
323
324 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
325
326 assert modified["directMessage"] == false
327
328 {:ok, activity} =
329 CommonAPI.post(user, %{
330 status: "@#{other_user.nickname} :moominmamma:",
331 visibility: "direct"
332 })
333
334 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
335
336 assert modified["directMessage"] == true
337 end
338
339 test "it strips BCC field" do
340 user = insert(:user)
341 {:ok, list} = Pleroma.List.create("foo", user)
342
343 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
344
345 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
346
347 assert is_nil(modified["bcc"])
348 end
349
350 test "it can handle Listen activities" do
351 listen_activity = insert(:listen)
352
353 {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
354
355 assert modified["type"] == "Listen"
356
357 user = insert(:user)
358
359 {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
360
361 {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
362 end
363
364 test "custom emoji urls are URI encoded" do
365 # :dinosaur: filename has a space -> dino walking.gif
366 user = insert(:user)
367
368 {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
369
370 {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
371
372 assert length(prepared["object"]["tag"]) == 1
373
374 url = prepared["object"]["tag"] |> List.first() |> Map.get("icon") |> Map.get("url")
375
376 assert url == "http://localhost:4001/emoji/dino%20walking.gif"
377 end
378 end
379
380 describe "user upgrade" do
381 test "it upgrades a user to activitypub" do
382 user =
383 insert(:user, %{
384 nickname: "rye@niu.moe",
385 local: false,
386 ap_id: "https://niu.moe/users/rye",
387 follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
388 })
389
390 user_two = insert(:user)
391 Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
392
393 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
394 {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"})
395 assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
396
397 user = User.get_cached_by_id(user.id)
398 assert user.note_count == 1
399
400 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
401 ObanHelpers.perform_all()
402
403 assert user.ap_enabled
404 assert user.note_count == 1
405 assert user.follower_address == "https://niu.moe/users/rye/followers"
406 assert user.following_address == "https://niu.moe/users/rye/following"
407
408 user = User.get_cached_by_id(user.id)
409 assert user.note_count == 1
410
411 activity = Activity.get_by_id(activity.id)
412 assert user.follower_address in activity.recipients
413
414 assert %{
415 "url" => [
416 %{
417 "href" =>
418 "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
419 }
420 ]
421 } = user.avatar
422
423 assert %{
424 "url" => [
425 %{
426 "href" =>
427 "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
428 }
429 ]
430 } = user.banner
431
432 refute "..." in activity.recipients
433
434 unrelated_activity = Activity.get_by_id(unrelated_activity.id)
435 refute user.follower_address in unrelated_activity.recipients
436
437 user_two = User.get_cached_by_id(user_two.id)
438 assert User.following?(user_two, user)
439 refute "..." in User.following(user_two)
440 end
441 end
442
443 describe "actor rewriting" do
444 test "it fixes the actor URL property to be a proper URI" do
445 data = %{
446 "url" => %{"href" => "http://example.com"}
447 }
448
449 rewritten = Transmogrifier.maybe_fix_user_object(data)
450 assert rewritten["url"] == "http://example.com"
451 end
452 end
453
454 describe "actor origin containment" do
455 test "it rejects activities which reference objects with bogus origins" do
456 data = %{
457 "@context" => "https://www.w3.org/ns/activitystreams",
458 "id" => "http://mastodon.example.org/users/admin/activities/1234",
459 "actor" => "http://mastodon.example.org/users/admin",
460 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
461 "object" => "https://info.pleroma.site/activity.json",
462 "type" => "Announce"
463 }
464
465 assert capture_log(fn ->
466 {:error, _} = Transmogrifier.handle_incoming(data)
467 end) =~ "Object containment failed"
468 end
469
470 test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
471 data = %{
472 "@context" => "https://www.w3.org/ns/activitystreams",
473 "id" => "http://mastodon.example.org/users/admin/activities/1234",
474 "actor" => "http://mastodon.example.org/users/admin",
475 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
476 "object" => "https://info.pleroma.site/activity2.json",
477 "type" => "Announce"
478 }
479
480 assert capture_log(fn ->
481 {:error, _} = Transmogrifier.handle_incoming(data)
482 end) =~ "Object containment failed"
483 end
484
485 test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
486 data = %{
487 "@context" => "https://www.w3.org/ns/activitystreams",
488 "id" => "http://mastodon.example.org/users/admin/activities/1234",
489 "actor" => "http://mastodon.example.org/users/admin",
490 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
491 "object" => "https://info.pleroma.site/activity3.json",
492 "type" => "Announce"
493 }
494
495 assert capture_log(fn ->
496 {:error, _} = Transmogrifier.handle_incoming(data)
497 end) =~ "Object containment failed"
498 end
499 end
500
501 describe "fix_explicit_addressing" do
502 setup do
503 user = insert(:user)
504 [user: user]
505 end
506
507 test "moves non-explicitly mentioned actors to cc", %{user: user} do
508 explicitly_mentioned_actors = [
509 "https://pleroma.gold/users/user1",
510 "https://pleroma.gold/user2"
511 ]
512
513 object = %{
514 "actor" => user.ap_id,
515 "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
516 "cc" => [],
517 "tag" =>
518 Enum.map(explicitly_mentioned_actors, fn href ->
519 %{"type" => "Mention", "href" => href}
520 end)
521 }
522
523 fixed_object = Transmogrifier.fix_explicit_addressing(object)
524 assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
525 refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
526 assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
527 end
528
529 test "does not move actor's follower collection to cc", %{user: user} do
530 object = %{
531 "actor" => user.ap_id,
532 "to" => [user.follower_address],
533 "cc" => []
534 }
535
536 fixed_object = Transmogrifier.fix_explicit_addressing(object)
537 assert user.follower_address in fixed_object["to"]
538 refute user.follower_address in fixed_object["cc"]
539 end
540
541 test "removes recipient's follower collection from cc", %{user: user} do
542 recipient = insert(:user)
543
544 object = %{
545 "actor" => user.ap_id,
546 "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
547 "cc" => [user.follower_address, recipient.follower_address]
548 }
549
550 fixed_object = Transmogrifier.fix_explicit_addressing(object)
551
552 assert user.follower_address in fixed_object["cc"]
553 refute recipient.follower_address in fixed_object["cc"]
554 refute recipient.follower_address in fixed_object["to"]
555 end
556 end
557
558 describe "fix_summary/1" do
559 test "returns fixed object" do
560 assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
561 assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
562 assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
563 end
564 end
565
566 describe "fix_url/1" do
567 test "fixes data for object when url is map" do
568 object = %{
569 "url" => %{
570 "type" => "Link",
571 "mimeType" => "video/mp4",
572 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
573 }
574 }
575
576 assert Transmogrifier.fix_url(object) == %{
577 "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
578 }
579 end
580
581 test "returns non-modified object" do
582 assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
583 end
584 end
585
586 describe "get_obj_helper/2" do
587 test "returns nil when cannot normalize object" do
588 assert capture_log(fn ->
589 refute Transmogrifier.get_obj_helper("test-obj-id")
590 end) =~ "Unsupported URI scheme"
591 end
592
593 @tag capture_log: true
594 test "returns {:ok, %Object{}} for success case" do
595 assert {:ok, %Object{}} =
596 Transmogrifier.get_obj_helper(
597 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
598 )
599 end
600 end
601 end