make conversation-id deterministic (#154)
[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 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
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 "custom emoji urls are URI encoded" do
291 # :dinosaur: filename has a space -> dino walking.gif
292 user = insert(:user)
293
294 {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
295
296 {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
297
298 assert length(prepared["object"]["tag"]) == 1
299
300 url = prepared["object"]["tag"] |> List.first() |> Map.get("icon") |> Map.get("url")
301
302 assert url == "http://localhost:4001/emoji/dino%20walking.gif"
303 end
304 end
305
306 describe "user upgrade" do
307 test "it upgrades a user to activitypub" do
308 user =
309 insert(:user, %{
310 nickname: "rye@niu.moe",
311 local: false,
312 ap_id: "https://niu.moe/users/rye",
313 follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
314 })
315
316 user_two = insert(:user)
317 Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
318
319 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
320 {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"})
321 assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
322
323 user = User.get_cached_by_id(user.id)
324 assert user.note_count == 1
325
326 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
327 ObanHelpers.perform_all()
328
329 assert user.ap_enabled
330 assert user.note_count == 1
331 assert user.follower_address == "https://niu.moe/users/rye/followers"
332 assert user.following_address == "https://niu.moe/users/rye/following"
333
334 user = User.get_cached_by_id(user.id)
335 assert user.note_count == 1
336
337 activity = Activity.get_by_id(activity.id)
338 assert user.follower_address in activity.recipients
339
340 assert %{
341 "url" => [
342 %{
343 "href" =>
344 "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
345 }
346 ]
347 } = user.avatar
348
349 assert %{
350 "url" => [
351 %{
352 "href" =>
353 "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
354 }
355 ]
356 } = user.banner
357
358 refute "..." in activity.recipients
359
360 unrelated_activity = Activity.get_by_id(unrelated_activity.id)
361 refute user.follower_address in unrelated_activity.recipients
362
363 user_two = User.get_cached_by_id(user_two.id)
364 assert User.following?(user_two, user)
365 refute "..." in User.following(user_two)
366 end
367 end
368
369 describe "actor rewriting" do
370 test "it fixes the actor URL property to be a proper URI" do
371 data = %{
372 "url" => %{"href" => "http://example.com"}
373 }
374
375 rewritten = Transmogrifier.maybe_fix_user_object(data)
376 assert rewritten["url"] == "http://example.com"
377 end
378 end
379
380 describe "actor origin containment" do
381 test "it rejects activities which reference objects with bogus origins" do
382 data = %{
383 "@context" => "https://www.w3.org/ns/activitystreams",
384 "id" => "http://mastodon.example.org/users/admin/activities/1234",
385 "actor" => "http://mastodon.example.org/users/admin",
386 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
387 "object" => "https://info.pleroma.site/activity.json",
388 "type" => "Announce"
389 }
390
391 assert capture_log(fn ->
392 {:error, _} = Transmogrifier.handle_incoming(data)
393 end) =~ "Object containment failed"
394 end
395
396 test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
397 data = %{
398 "@context" => "https://www.w3.org/ns/activitystreams",
399 "id" => "http://mastodon.example.org/users/admin/activities/1234",
400 "actor" => "http://mastodon.example.org/users/admin",
401 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
402 "object" => "https://info.pleroma.site/activity2.json",
403 "type" => "Announce"
404 }
405
406 assert capture_log(fn ->
407 {:error, _} = Transmogrifier.handle_incoming(data)
408 end) =~ "Object containment failed"
409 end
410
411 test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
412 data = %{
413 "@context" => "https://www.w3.org/ns/activitystreams",
414 "id" => "http://mastodon.example.org/users/admin/activities/1234",
415 "actor" => "http://mastodon.example.org/users/admin",
416 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
417 "object" => "https://info.pleroma.site/activity3.json",
418 "type" => "Announce"
419 }
420
421 assert capture_log(fn ->
422 {:error, _} = Transmogrifier.handle_incoming(data)
423 end) =~ "Object containment failed"
424 end
425 end
426
427 describe "fix_explicit_addressing" do
428 setup do
429 user = insert(:user)
430 [user: user]
431 end
432
433 test "moves non-explicitly mentioned actors to cc", %{user: user} do
434 explicitly_mentioned_actors = [
435 "https://pleroma.gold/users/user1",
436 "https://pleroma.gold/user2"
437 ]
438
439 object = %{
440 "actor" => user.ap_id,
441 "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
442 "cc" => [],
443 "tag" =>
444 Enum.map(explicitly_mentioned_actors, fn href ->
445 %{"type" => "Mention", "href" => href}
446 end)
447 }
448
449 fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
450 assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
451 refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
452 assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
453 end
454
455 test "does not move actor's follower collection to cc", %{user: user} do
456 object = %{
457 "actor" => user.ap_id,
458 "to" => [user.follower_address],
459 "cc" => []
460 }
461
462 fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
463 assert user.follower_address in fixed_object["to"]
464 refute user.follower_address in fixed_object["cc"]
465 end
466
467 test "removes recipient's follower collection from cc", %{user: user} do
468 recipient = insert(:user)
469
470 object = %{
471 "actor" => user.ap_id,
472 "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
473 "cc" => [user.follower_address, recipient.follower_address]
474 }
475
476 fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
477
478 assert user.follower_address in fixed_object["cc"]
479 refute recipient.follower_address in fixed_object["cc"]
480 refute recipient.follower_address in fixed_object["to"]
481 end
482 end
483
484 describe "fix_summary/1" do
485 test "returns fixed object" do
486 assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
487 assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
488 assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
489 end
490 end
491
492 describe "fix_url/1" do
493 test "fixes data for object when url is map" do
494 object = %{
495 "url" => %{
496 "type" => "Link",
497 "mimeType" => "video/mp4",
498 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
499 }
500 }
501
502 assert Transmogrifier.fix_url(object) == %{
503 "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
504 }
505 end
506
507 test "returns non-modified object" do
508 assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
509 end
510 end
511
512 describe "get_obj_helper/2" do
513 test "returns nil when cannot normalize object" do
514 assert capture_log(fn ->
515 refute Transmogrifier.get_obj_helper("test-obj-id")
516 end) =~ "Unsupported URI scheme"
517 end
518
519 @tag capture_log: true
520 test "returns {:ok, %Object{}} for success case" do
521 assert {:ok, %Object{}} =
522 Transmogrifier.get_obj_helper(
523 "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
524 )
525 end
526 end
527
528 describe "fix_attachments/1" do
529 test "puts dimensions into attachment url field" do
530 object = %{
531 "attachment" => [
532 %{
533 "type" => "Document",
534 "name" => "Hello world",
535 "url" => "https://media.example.tld/1.jpg",
536 "width" => 880,
537 "height" => 960,
538 "mediaType" => "image/jpeg",
539 "blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
540 }
541 ]
542 }
543
544 expected = %{
545 "attachment" => [
546 %{
547 "type" => "Document",
548 "name" => "Hello world",
549 "url" => [
550 %{
551 "type" => "Link",
552 "mediaType" => "image/jpeg",
553 "href" => "https://media.example.tld/1.jpg",
554 "width" => 880,
555 "height" => 960
556 }
557 ],
558 "mediaType" => "image/jpeg",
559 "blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
560 }
561 ]
562 }
563
564 assert Transmogrifier.fix_attachments(object) == expected
565 end
566 end
567 end