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