3756fdee0123d7b5c3f953defab97eac9b717810
[akkoma] / 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 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.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 end
111
112 describe "prepare outgoing" do
113 test "it inlines private announced objects" do
114 user = insert(:user)
115
116 {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"})
117
118 {:ok, announce_activity} = CommonAPI.repeat(activity.id, user)
119
120 {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
121
122 assert modified["object"]["content"] == "hey"
123 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
124 end
125
126 test "it turns mentions into tags" do
127 user = insert(:user)
128 other_user = insert(:user)
129
130 {:ok, activity} =
131 CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"})
132
133 with_mock Pleroma.Notification,
134 get_notified_from_activity: fn _, _ -> [] end do
135 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
136
137 object = modified["object"]
138
139 expected_mention = %{
140 "href" => other_user.ap_id,
141 "name" => "@#{other_user.nickname}",
142 "type" => "Mention"
143 }
144
145 expected_tag = %{
146 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
147 "type" => "Hashtag",
148 "name" => "#2hu"
149 }
150
151 refute called(Pleroma.Notification.get_notified_from_activity(:_, :_))
152 assert Enum.member?(object["tag"], expected_tag)
153 assert Enum.member?(object["tag"], expected_mention)
154 end
155 end
156
157 test "it adds the json-ld context and the conversation property" do
158 user = insert(:user)
159
160 {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
161 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
162
163 assert modified["@context"] == Utils.make_json_ld_header()["@context"]
164
165 assert modified["object"]["conversation"] == modified["context"]
166 end
167
168 test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
169 user = insert(:user)
170
171 {:ok, activity} = CommonAPI.post(user, %{status: "hey"})
172 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
173
174 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
175 end
176
177 test "it strips internal hashtag data" do
178 user = insert(:user)
179
180 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"})
181
182 expected_tag = %{
183 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
184 "type" => "Hashtag",
185 "name" => "#2hu"
186 }
187
188 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
189
190 assert modified["object"]["tag"] == [expected_tag]
191 end
192
193 test "it strips internal fields" do
194 user = insert(:user)
195
196 {:ok, activity} =
197 CommonAPI.post(user, %{
198 status: "#2hu :firefox:",
199 generator: %{type: "Application", name: "TestClient", url: "https://pleroma.social"}
200 })
201
202 # Ensure injected application data made it into the activity
203 # as we don't have a Token to derive it from, otherwise it will
204 # be nil and the test will pass
205 assert %{
206 type: "Application",
207 name: "TestClient",
208 url: "https://pleroma.social"
209 } == activity.object.data["generator"]
210
211 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
212
213 assert length(modified["object"]["tag"]) == 2
214
215 assert is_nil(modified["object"]["emoji"])
216 assert is_nil(modified["object"]["like_count"])
217 assert is_nil(modified["object"]["announcements"])
218 assert is_nil(modified["object"]["announcement_count"])
219 assert is_nil(modified["object"]["context_id"])
220 assert is_nil(modified["object"]["generator"])
221 end
222
223 test "it strips internal fields of article" do
224 activity = insert(:article_activity)
225
226 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
227
228 assert length(modified["object"]["tag"]) == 2
229
230 assert is_nil(modified["object"]["emoji"])
231 assert is_nil(modified["object"]["like_count"])
232 assert is_nil(modified["object"]["announcements"])
233 assert is_nil(modified["object"]["announcement_count"])
234 assert is_nil(modified["object"]["context_id"])
235 assert is_nil(modified["object"]["likes"])
236 end
237
238 test "the directMessage flag is present" do
239 user = insert(:user)
240 other_user = insert(:user)
241
242 {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"})
243
244 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
245
246 assert modified["directMessage"] == false
247
248 {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"})
249
250 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
251
252 assert modified["directMessage"] == false
253
254 {:ok, activity} =
255 CommonAPI.post(user, %{
256 status: "@#{other_user.nickname} :moominmamma:",
257 visibility: "direct"
258 })
259
260 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
261
262 assert modified["directMessage"] == true
263 end
264
265 test "it strips BCC field" do
266 user = insert(:user)
267 {:ok, list} = Pleroma.List.create("foo", user)
268
269 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
270
271 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
272
273 assert is_nil(modified["bcc"])
274 end
275
276 test "custom emoji urls are URI encoded" do
277 # :dinosaur: filename has a space -> dino walking.gif
278 user = insert(:user)
279
280 {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
281
282 {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
283
284 assert length(prepared["object"]["tag"]) == 1
285
286 url = prepared["object"]["tag"] |> List.first() |> Map.get("icon") |> Map.get("url")
287
288 assert url == "http://localhost:4001/emoji/dino%20walking.gif"
289 end
290 end
291
292 describe "user upgrade" do
293 test "it upgrades a user to activitypub" do
294 user =
295 insert(:user, %{
296 nickname: "rye@niu.moe",
297 local: false,
298 ap_id: "https://niu.moe/users/rye",
299 follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
300 })
301
302 user_two = insert(:user)
303 Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
304
305 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
306 {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"})
307 assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
308
309 user = User.get_cached_by_id(user.id)
310 assert user.note_count == 1
311
312 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
313 ObanHelpers.perform_all()
314
315 assert user.ap_enabled
316 assert user.note_count == 1
317 assert user.follower_address == "https://niu.moe/users/rye/followers"
318 assert user.following_address == "https://niu.moe/users/rye/following"
319
320 user = User.get_cached_by_id(user.id)
321 assert user.note_count == 1
322
323 activity = Activity.get_by_id(activity.id)
324 assert user.follower_address in activity.recipients
325
326 assert %{
327 "url" => [
328 %{
329 "href" =>
330 "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
331 }
332 ]
333 } = user.avatar
334
335 assert %{
336 "url" => [
337 %{
338 "href" =>
339 "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
340 }
341 ]
342 } = user.banner
343
344 refute "..." in activity.recipients
345
346 unrelated_activity = Activity.get_by_id(unrelated_activity.id)
347 refute user.follower_address in unrelated_activity.recipients
348
349 user_two = User.get_cached_by_id(user_two.id)
350 assert User.following?(user_two, user)
351 refute "..." in User.following(user_two)
352 end
353 end
354
355 describe "actor rewriting" do
356 test "it fixes the actor URL property to be a proper URI" do
357 data = %{
358 "url" => %{"href" => "http://example.com"}
359 }
360
361 rewritten = Transmogrifier.maybe_fix_user_object(data)
362 assert rewritten["url"] == "http://example.com"
363 end
364 end
365
366 describe "actor origin containment" do
367 test "it rejects activities which reference objects with bogus origins" do
368 data = %{
369 "@context" => "https://www.w3.org/ns/activitystreams",
370 "id" => "http://mastodon.example.org/users/admin/activities/1234",
371 "actor" => "http://mastodon.example.org/users/admin",
372 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
373 "object" => "https://info.pleroma.site/activity.json",
374 "type" => "Announce"
375 }
376
377 assert capture_log(fn ->
378 {:error, _} = Transmogrifier.handle_incoming(data)
379 end) =~ "Object containment failed"
380 end
381
382 test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
383 data = %{
384 "@context" => "https://www.w3.org/ns/activitystreams",
385 "id" => "http://mastodon.example.org/users/admin/activities/1234",
386 "actor" => "http://mastodon.example.org/users/admin",
387 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
388 "object" => "https://info.pleroma.site/activity2.json",
389 "type" => "Announce"
390 }
391
392 assert capture_log(fn ->
393 {:error, _} = Transmogrifier.handle_incoming(data)
394 end) =~ "Object containment failed"
395 end
396
397 test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
398 data = %{
399 "@context" => "https://www.w3.org/ns/activitystreams",
400 "id" => "http://mastodon.example.org/users/admin/activities/1234",
401 "actor" => "http://mastodon.example.org/users/admin",
402 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
403 "object" => "https://info.pleroma.site/activity3.json",
404 "type" => "Announce"
405 }
406
407 assert capture_log(fn ->
408 {:error, _} = Transmogrifier.handle_incoming(data)
409 end) =~ "Object containment failed"
410 end
411 end
412
413 describe "fix_explicit_addressing" do
414 setup do
415 user = insert(:user)
416 [user: user]
417 end
418
419 test "moves non-explicitly mentioned actors to cc", %{user: user} do
420 explicitly_mentioned_actors = [
421 "https://pleroma.gold/users/user1",
422 "https://pleroma.gold/user2"
423 ]
424
425 object = %{
426 "actor" => user.ap_id,
427 "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
428 "cc" => [],
429 "tag" =>
430 Enum.map(explicitly_mentioned_actors, fn href ->
431 %{"type" => "Mention", "href" => href}
432 end)
433 }
434
435 fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
436 assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
437 refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
438 assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
439 end
440
441 test "does not move actor's follower collection to cc", %{user: user} do
442 object = %{
443 "actor" => user.ap_id,
444 "to" => [user.follower_address],
445 "cc" => []
446 }
447
448 fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
449 assert user.follower_address in fixed_object["to"]
450 refute user.follower_address in fixed_object["cc"]
451 end
452
453 test "removes recipient's follower collection from cc", %{user: user} do
454 recipient = insert(:user)
455
456 object = %{
457 "actor" => user.ap_id,
458 "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
459 "cc" => [user.follower_address, recipient.follower_address]
460 }
461
462 fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
463
464 assert user.follower_address in fixed_object["cc"]
465 refute recipient.follower_address in fixed_object["cc"]
466 refute recipient.follower_address in fixed_object["to"]
467 end
468 end
469
470 describe "fix_summary/1" do
471 test "returns fixed object" do
472 assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
473 assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
474 assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
475 end
476 end
477
478 describe "fix_url/1" do
479 test "fixes data for object when url is map" do
480 object = %{
481 "url" => %{
482 "type" => "Link",
483 "mimeType" => "video/mp4",
484 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
485 }
486 }
487
488 assert Transmogrifier.fix_url(object) == %{
489 "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
490 }
491 end
492
493 test "returns non-modified object" do
494 assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
495 end
496 end
497
498 describe "get_obj_helper/2" do
499 test "returns nil when cannot normalize object" do
500 assert capture_log(fn ->
501 refute Transmogrifier.get_obj_helper("test-obj-id")
502 end) =~ "Unsupported URI scheme"
503 end
504
505 @tag capture_log: true
506 test "returns {:ok, %Object{}} for success case" do
507 assert {:ok, %Object{}} =
508 Transmogrifier.get_obj_helper(
509 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
510 )
511 end
512 end
513
514 describe "fix_attachments/1" do
515 test "puts dimensions into attachment url field" do
516 object = %{
517 "attachment" => [
518 %{
519 "type" => "Document",
520 "name" => "Hello world",
521 "url" => "https://media.example.tld/1.jpg",
522 "width" => 880,
523 "height" => 960,
524 "mediaType" => "image/jpeg",
525 "blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
526 }
527 ]
528 }
529
530 expected = %{
531 "attachment" => [
532 %{
533 "type" => "Document",
534 "name" => "Hello world",
535 "url" => [
536 %{
537 "type" => "Link",
538 "mediaType" => "image/jpeg",
539 "href" => "https://media.example.tld/1.jpg",
540 "width" => 880,
541 "height" => 960
542 }
543 ],
544 "mediaType" => "image/jpeg",
545 "blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
546 }
547 ]
548 }
549
550 assert Transmogrifier.fix_attachments(object) == expected
551 end
552 end
553 end