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