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