1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
6 use Pleroma.Web.ConnCase
7 use Oban.Testing, repo: Pleroma.Repo
10 alias Pleroma.Delivery
11 alias Pleroma.Instances
13 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.ObjectView
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.Endpoint
22 alias Pleroma.Workers.ReceiverWorker
24 import Pleroma.Factory
26 require Pleroma.Constants
29 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
33 setup do: clear_config([:instance, :federating], true)
36 setup do: clear_config([:instance, :allow_relay])
38 test "with the relay active, it returns the relay user", %{conn: conn} do
41 |> get(activity_pub_path(conn, :relay))
44 assert res["id"] =~ "/relay"
47 test "with the relay disabled, it returns 404", %{conn: conn} do
48 clear_config([:instance, :allow_relay], false)
51 |> get(activity_pub_path(conn, :relay))
55 test "on non-federating instance, it returns 404", %{conn: conn} do
56 clear_config([:instance, :federating], false)
60 |> assign(:user, user)
61 |> get(activity_pub_path(conn, :relay))
66 describe "/internal/fetch" do
67 test "it returns the internal fetch user", %{conn: conn} do
70 |> get(activity_pub_path(conn, :internal_fetch))
73 assert res["id"] =~ "/fetch"
76 test "on non-federating instance, it returns 404", %{conn: conn} do
77 clear_config([:instance, :federating], false)
81 |> assign(:user, user)
82 |> get(activity_pub_path(conn, :internal_fetch))
87 describe "/users/:nickname" do
88 test "it returns a json representation of the user with accept application/json", %{
95 |> put_req_header("accept", "application/json")
96 |> get("/users/#{user.nickname}")
98 user = User.get_cached_by_id(user.id)
100 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
103 test "it returns a json representation of the user with accept application/activity+json", %{
110 |> put_req_header("accept", "application/activity+json")
111 |> get("/users/#{user.nickname}")
113 user = User.get_cached_by_id(user.id)
115 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
118 test "it returns a json representation of the user with accept application/ld+json", %{
127 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
129 |> get("/users/#{user.nickname}")
131 user = User.get_cached_by_id(user.id)
133 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
136 test "it returns 404 for remote users", %{
139 user = insert(:user, local: false, nickname: "remoteuser@example.com")
143 |> put_req_header("accept", "application/json")
144 |> get("/users/#{user.nickname}.json")
146 assert json_response(conn, 404)
149 test "it returns error when user is not found", %{conn: conn} do
152 |> put_req_header("accept", "application/json")
153 |> get("/users/jimm")
154 |> json_response(404)
156 assert response == "Not found"
160 describe "mastodon compatibility routes" do
161 test "it returns a json representation of the object with accept application/json", %{
168 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
169 "actor" => Endpoint.url() <> "/users/raymoo",
170 "to" => [Pleroma.Constants.as_public()]
176 |> put_req_header("accept", "application/json")
177 |> get("/users/raymoo/statuses/999999999")
179 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
182 test "it returns a json representation of the activity with accept application/json", %{
189 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
190 "actor" => Endpoint.url() <> "/users/raymoo",
191 "to" => [Pleroma.Constants.as_public()]
197 "id" => object.data["id"] <> "/activity",
199 "object" => object.data["id"],
200 "actor" => object.data["actor"],
201 "to" => object.data["to"]
203 |> ActivityPub.persist(local: true)
207 |> put_req_header("accept", "application/json")
208 |> get("/users/raymoo/statuses/999999999/activity")
210 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
214 describe "/objects/:uuid" do
215 test "it doesn't return a local-only object", %{conn: conn} do
217 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
219 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
221 object = Object.normalize(post, fetch: false)
222 uuid = String.split(object.data["id"], "/") |> List.last()
226 |> put_req_header("accept", "application/json")
227 |> get("/objects/#{uuid}")
229 assert json_response(conn, 404)
232 test "returns local-only objects when authenticated", %{conn: conn} do
234 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
236 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
238 object = Object.normalize(post, fetch: false)
239 uuid = String.split(object.data["id"], "/") |> List.last()
243 |> assign(:user, user)
244 |> put_req_header("accept", "application/activity+json")
245 |> get("/objects/#{uuid}")
247 assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
250 test "it returns a json representation of the object with accept application/json", %{
254 uuid = String.split(note.data["id"], "/") |> List.last()
258 |> put_req_header("accept", "application/json")
259 |> get("/objects/#{uuid}")
261 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
264 test "it returns a json representation of the object with accept application/activity+json",
267 uuid = String.split(note.data["id"], "/") |> List.last()
271 |> put_req_header("accept", "application/activity+json")
272 |> get("/objects/#{uuid}")
274 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
277 test "it returns a json representation of the object with accept application/ld+json", %{
281 uuid = String.split(note.data["id"], "/") |> List.last()
287 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
289 |> get("/objects/#{uuid}")
291 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
294 test "it returns 404 for non-public messages", %{conn: conn} do
295 note = insert(:direct_note)
296 uuid = String.split(note.data["id"], "/") |> List.last()
300 |> put_req_header("accept", "application/activity+json")
301 |> get("/objects/#{uuid}")
303 assert json_response(conn, 404)
306 test "returns visible non-public messages when authenticated", %{conn: conn} do
307 note = insert(:direct_note)
308 uuid = String.split(note.data["id"], "/") |> List.last()
309 user = User.get_by_ap_id(note.data["actor"])
310 marisa = insert(:user)
313 |> assign(:user, marisa)
314 |> put_req_header("accept", "application/activity+json")
315 |> get("/objects/#{uuid}")
316 |> json_response(404)
320 |> assign(:user, user)
321 |> put_req_header("accept", "application/activity+json")
322 |> get("/objects/#{uuid}")
323 |> json_response(200)
325 assert response == ObjectView.render("object.json", %{object: note})
328 test "it returns 404 for tombstone objects", %{conn: conn} do
329 tombstone = insert(:tombstone)
330 uuid = String.split(tombstone.data["id"], "/") |> List.last()
334 |> put_req_header("accept", "application/activity+json")
335 |> get("/objects/#{uuid}")
337 assert json_response(conn, 404)
340 test "it caches a response", %{conn: conn} do
342 uuid = String.split(note.data["id"], "/") |> List.last()
346 |> put_req_header("accept", "application/activity+json")
347 |> get("/objects/#{uuid}")
349 assert json_response(conn1, :ok)
350 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
354 |> put_req_header("accept", "application/activity+json")
355 |> get("/objects/#{uuid}")
357 assert json_response(conn1, :ok) == json_response(conn2, :ok)
358 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
361 test "cached purged after object deletion", %{conn: conn} do
363 uuid = String.split(note.data["id"], "/") |> List.last()
367 |> put_req_header("accept", "application/activity+json")
368 |> get("/objects/#{uuid}")
370 assert json_response(conn1, :ok)
371 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
377 |> put_req_header("accept", "application/activity+json")
378 |> get("/objects/#{uuid}")
380 assert "Not found" == json_response(conn2, :not_found)
384 describe "/activities/:uuid" do
385 test "it doesn't return a local-only activity", %{conn: conn} do
387 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
389 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
391 uuid = String.split(post.data["id"], "/") |> List.last()
395 |> put_req_header("accept", "application/json")
396 |> get("/activities/#{uuid}")
398 assert json_response(conn, 404)
401 test "returns local-only activities when authenticated", %{conn: conn} do
403 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
405 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
407 uuid = String.split(post.data["id"], "/") |> List.last()
411 |> assign(:user, user)
412 |> put_req_header("accept", "application/activity+json")
413 |> get("/activities/#{uuid}")
415 assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
418 test "it returns a json representation of the activity", %{conn: conn} do
419 activity = insert(:note_activity)
420 uuid = String.split(activity.data["id"], "/") |> List.last()
424 |> put_req_header("accept", "application/activity+json")
425 |> get("/activities/#{uuid}")
427 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
430 test "it returns 404 for non-public activities", %{conn: conn} do
431 activity = insert(:direct_note_activity)
432 uuid = String.split(activity.data["id"], "/") |> List.last()
436 |> put_req_header("accept", "application/activity+json")
437 |> get("/activities/#{uuid}")
439 assert json_response(conn, 404)
442 test "returns visible non-public messages when authenticated", %{conn: conn} do
443 note = insert(:direct_note_activity)
444 uuid = String.split(note.data["id"], "/") |> List.last()
445 user = User.get_by_ap_id(note.data["actor"])
446 marisa = insert(:user)
449 |> assign(:user, marisa)
450 |> put_req_header("accept", "application/activity+json")
451 |> get("/activities/#{uuid}")
452 |> json_response(404)
456 |> assign(:user, user)
457 |> put_req_header("accept", "application/activity+json")
458 |> get("/activities/#{uuid}")
459 |> json_response(200)
461 assert response == ObjectView.render("object.json", %{object: note})
464 test "it caches a response", %{conn: conn} do
465 activity = insert(:note_activity)
466 uuid = String.split(activity.data["id"], "/") |> List.last()
470 |> put_req_header("accept", "application/activity+json")
471 |> get("/activities/#{uuid}")
473 assert json_response(conn1, :ok)
474 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
478 |> put_req_header("accept", "application/activity+json")
479 |> get("/activities/#{uuid}")
481 assert json_response(conn1, :ok) == json_response(conn2, :ok)
482 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
485 test "cached purged after activity deletion", %{conn: conn} do
487 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
489 uuid = String.split(activity.data["id"], "/") |> List.last()
493 |> put_req_header("accept", "application/activity+json")
494 |> get("/activities/#{uuid}")
496 assert json_response(conn1, :ok)
497 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
499 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
503 |> put_req_header("accept", "application/activity+json")
504 |> get("/activities/#{uuid}")
506 assert "Not found" == json_response(conn2, :not_found)
511 test "it inserts an incoming activity into the database", %{conn: conn} do
512 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
516 |> assign(:valid_signature, true)
517 |> put_req_header("content-type", "application/activity+json")
518 |> post("/inbox", data)
520 assert "ok" == json_response(conn, 200)
522 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
523 assert Activity.get_by_ap_id(data["id"])
526 @tag capture_log: true
527 test "it inserts an incoming activity into the database" <>
528 "even if we can't fetch the user but have it in our db",
532 ap_id: "https://mastodon.example.org/users/raymoo",
535 last_refreshed_at: nil
539 File.read!("test/fixtures/mastodon-post-activity.json")
541 |> Map.put("actor", user.ap_id)
542 |> put_in(["object", "attridbutedTo"], user.ap_id)
546 |> assign(:valid_signature, true)
547 |> put_req_header("content-type", "application/activity+json")
548 |> post("/inbox", data)
550 assert "ok" == json_response(conn, 200)
552 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
553 assert Activity.get_by_ap_id(data["id"])
556 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
557 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
559 sender_url = data["actor"]
560 Instances.set_consistently_unreachable(sender_url)
561 refute Instances.reachable?(sender_url)
565 |> assign(:valid_signature, true)
566 |> put_req_header("content-type", "application/activity+json")
567 |> post("/inbox", data)
569 assert "ok" == json_response(conn, 200)
570 assert Instances.reachable?(sender_url)
573 test "accept follow activity", %{conn: conn} do
574 clear_config([:instance, :federating], true)
575 relay = Relay.get_actor()
577 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
579 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
580 relay = refresh_record(relay)
583 File.read!("test/fixtures/relay/accept-follow.json")
584 |> String.replace("{{ap_id}}", relay.ap_id)
585 |> String.replace("{{activity_id}}", activity.data["id"])
589 |> assign(:valid_signature, true)
590 |> put_req_header("content-type", "application/activity+json")
591 |> post("/inbox", accept)
592 |> json_response(200)
594 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
596 assert Pleroma.FollowingRelationship.following?(
601 Mix.shell(Mix.Shell.Process)
604 Mix.shell(Mix.Shell.IO)
607 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
608 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
611 @tag capture_log: true
612 test "without valid signature, " <>
613 "it only accepts Create activities and requires enabled federation",
615 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
616 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
618 conn = put_req_header(conn, "content-type", "application/activity+json")
620 clear_config([:instance, :federating], false)
623 |> post("/inbox", data)
624 |> json_response(403)
627 |> post("/inbox", non_create_data)
628 |> json_response(403)
630 clear_config([:instance, :federating], true)
632 ret_conn = post(conn, "/inbox", data)
633 assert "ok" == json_response(ret_conn, 200)
636 |> post("/inbox", non_create_data)
637 |> json_response(400)
640 test "accepts Add/Remove activities", %{conn: conn} do
641 object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
644 File.read!("test/fixtures/statuses/note.json")
645 |> String.replace("{{nickname}}", "lain")
646 |> String.replace("{{object_id}}", object_id)
648 object_url = "https://example.com/objects/#{object_id}"
651 File.read!("test/fixtures/users_mock/user.json")
652 |> String.replace("{{nickname}}", "lain")
654 actor = "https://example.com/users/lain"
664 headers: [{"content-type", "application/activity+json"}]
674 headers: [{"content-type", "application/activity+json"}]
679 "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
681 "object" => object_url,
682 "target" => "https://example.com/users/lain/collections/featured",
684 "to" => [Pleroma.Constants.as_public()]
689 |> assign(:valid_signature, true)
690 |> put_req_header("content-type", "application/activity+json")
691 |> post("/inbox", data)
692 |> json_response(200)
694 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
695 assert Activity.get_by_ap_id(data["id"])
696 user = User.get_cached_by_ap_id(data["actor"])
697 assert user.pinned_objects[data["object"]]
700 "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
702 "object" => object_url,
703 "target" => "https://example.com/users/lain/collections/featured",
705 "to" => [Pleroma.Constants.as_public()]
710 |> assign(:valid_signature, true)
711 |> put_req_header("content-type", "application/activity+json")
712 |> post("/inbox", data)
713 |> json_response(200)
715 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
716 user = refresh_record(user)
717 refute user.pinned_objects[data["object"]]
721 describe "/users/:nickname/inbox" do
724 File.read!("test/fixtures/mastodon-post-activity.json")
730 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
732 data = Map.put(data, "bcc", [user.ap_id])
736 |> assign(:valid_signature, true)
737 |> put_req_header("content-type", "application/activity+json")
738 |> post("/users/#{user.nickname}/inbox", data)
740 assert "ok" == json_response(conn, 200)
741 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
742 assert Activity.get_by_ap_id(data["id"])
745 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
749 Map.put(data, "to", user.ap_id)
754 |> assign(:valid_signature, true)
755 |> put_req_header("content-type", "application/activity+json")
756 |> post("/users/#{user.nickname}/inbox", data)
758 assert "ok" == json_response(conn, 200)
759 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
760 assert Activity.get_by_ap_id(data["id"])
763 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
767 Map.put(data, "cc", user.ap_id)
772 |> assign(:valid_signature, true)
773 |> put_req_header("content-type", "application/activity+json")
774 |> post("/users/#{user.nickname}/inbox", data)
776 assert "ok" == json_response(conn, 200)
777 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
778 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
779 assert user.ap_id in activity.recipients
782 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
786 Map.put(data, "bcc", user.ap_id)
792 |> assign(:valid_signature, true)
793 |> put_req_header("content-type", "application/activity+json")
794 |> post("/users/#{user.nickname}/inbox", data)
796 assert "ok" == json_response(conn, 200)
797 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
798 assert Activity.get_by_ap_id(data["id"])
801 test "it accepts announces with to as string instead of array", %{conn: conn} do
804 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
805 announcer = insert(:user, local: false)
808 "@context" => "https://www.w3.org/ns/activitystreams",
809 "actor" => announcer.ap_id,
810 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
811 "object" => post.data["object"],
812 "to" => "https://www.w3.org/ns/activitystreams#Public",
813 "cc" => [user.ap_id],
819 |> assign(:valid_signature, true)
820 |> put_req_header("content-type", "application/activity+json")
821 |> post("/users/#{user.nickname}/inbox", data)
823 assert "ok" == json_response(conn, 200)
824 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
825 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
826 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
829 test "it accepts messages from actors that are followed by the user", %{
833 recipient = insert(:user)
834 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
836 {:ok, recipient, actor} = User.follow(recipient, actor)
840 |> Map.put("attributedTo", actor.ap_id)
844 |> Map.put("actor", actor.ap_id)
845 |> Map.put("object", object)
849 |> assign(:valid_signature, true)
850 |> put_req_header("content-type", "application/activity+json")
851 |> post("/users/#{recipient.nickname}/inbox", data)
853 assert "ok" == json_response(conn, 200)
854 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
855 assert Activity.get_by_ap_id(data["id"])
858 test "it rejects reads from other users", %{conn: conn} do
860 other_user = insert(:user)
864 |> assign(:user, other_user)
865 |> put_req_header("accept", "application/activity+json")
866 |> get("/users/#{user.nickname}/inbox")
868 assert json_response(conn, 403)
871 test "it returns a note activity in a collection", %{conn: conn} do
872 note_activity = insert(:direct_note_activity)
873 note_object = Object.normalize(note_activity, fetch: false)
874 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
878 |> assign(:user, user)
879 |> put_req_header("accept", "application/activity+json")
880 |> get("/users/#{user.nickname}/inbox?page=true")
882 assert response(conn, 200) =~ note_object.data["content"]
885 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
887 data = Map.put(data, "bcc", [user.ap_id])
889 sender_host = URI.parse(data["actor"]).host
890 Instances.set_consistently_unreachable(sender_host)
891 refute Instances.reachable?(sender_host)
895 |> assign(:valid_signature, true)
896 |> put_req_header("content-type", "application/activity+json")
897 |> post("/users/#{user.nickname}/inbox", data)
899 assert "ok" == json_response(conn, 200)
900 assert Instances.reachable?(sender_host)
903 test "it removes all follower collections but actor's", %{conn: conn} do
904 [actor, recipient] = insert_pair(:user)
907 File.read!("test/fixtures/activitypub-client-post-activity.json")
910 object = Map.put(data["object"], "attributedTo", actor.ap_id)
914 |> Map.put("id", Utils.generate_object_id())
915 |> Map.put("actor", actor.ap_id)
916 |> Map.put("object", object)
918 recipient.follower_address,
919 actor.follower_address
923 recipient.follower_address,
924 "https://www.w3.org/ns/activitystreams#Public"
928 |> assign(:valid_signature, true)
929 |> put_req_header("content-type", "application/activity+json")
930 |> post("/users/#{recipient.nickname}/inbox", data)
931 |> json_response(200)
933 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
935 activity = Activity.get_by_ap_id(data["id"])
938 assert actor.follower_address in activity.recipients
939 assert actor.follower_address in activity.data["cc"]
941 refute recipient.follower_address in activity.recipients
942 refute recipient.follower_address in activity.data["cc"]
943 refute recipient.follower_address in activity.data["to"]
946 test "it requires authentication", %{conn: conn} do
948 conn = put_req_header(conn, "accept", "application/activity+json")
950 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
951 assert json_response(ret_conn, 403)
955 |> assign(:user, user)
956 |> get("/users/#{user.nickname}/inbox")
958 assert json_response(ret_conn, 200)
961 @tag capture_log: true
962 test "forwarded report", %{conn: conn} do
963 admin = insert(:user, is_admin: true)
964 actor = insert(:user, local: false)
965 remote_domain = URI.parse(actor.ap_id).host
966 reported_user = insert(:user)
968 note = insert(:note_activity, user: reported_user)
972 "https://www.w3.org/ns/activitystreams",
973 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
978 "actor" => actor.ap_id,
983 "context" => "context",
984 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
985 "nickname" => reported_user.nickname,
990 "actor_type" => "Person",
991 "approval_pending" => false,
993 "confirmation_pending" => false,
994 "deactivated" => false,
995 "display_name" => "test user",
996 "id" => reported_user.id,
998 "nickname" => reported_user.nickname,
999 "registration_reason" => nil,
1002 "moderator" => false
1005 "url" => reported_user.ap_id
1008 "id" => note.data["id"],
1009 "published" => note.data["published"],
1013 "published" => note.data["published"],
1020 |> assign(:valid_signature, true)
1021 |> put_req_header("content-type", "application/activity+json")
1022 |> post("/users/#{reported_user.nickname}/inbox", data)
1023 |> json_response(200)
1025 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1027 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
1029 ObanHelpers.perform_all()
1031 Swoosh.TestAssertions.assert_email_sent(
1032 to: {admin.name, admin.email},
1033 html_body: ~r/Reported Account:/i
1037 @tag capture_log: true
1038 test "forwarded report from mastodon", %{conn: conn} do
1039 admin = insert(:user, is_admin: true)
1040 actor = insert(:user, local: false)
1041 remote_domain = URI.parse(actor.ap_id).host
1042 remote_actor = "https://#{remote_domain}/actor"
1043 [reported_user, another] = insert_list(2, :user)
1045 note = insert(:note_activity, user: reported_user)
1047 Pleroma.Web.CommonAPI.favorite(another, note.id)
1050 "test/fixtures/mastodon/application_actor.json"
1052 |> String.replace("{{DOMAIN}}", remote_domain)
1054 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
1057 body: mock_json_body,
1058 headers: [{"content-type", "application/activity+json"}]
1063 "@context" => "https://www.w3.org/ns/activitystreams",
1064 "actor" => remote_actor,
1065 "content" => "test report",
1066 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
1067 "nickname" => reported_user.nickname,
1069 reported_user.ap_id,
1076 |> assign(:valid_signature, true)
1077 |> put_req_header("content-type", "application/activity+json")
1078 |> post("/users/#{reported_user.nickname}/inbox", data)
1079 |> json_response(200)
1081 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1083 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
1084 reported_user_ap_id = reported_user.ap_id
1086 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
1088 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
1089 ObanHelpers.perform_all()
1091 Swoosh.TestAssertions.assert_email_sent(
1092 to: {admin.name, admin.email},
1093 html_body: ~r/#{note.data["object"]}/i
1098 describe "GET /users/:nickname/outbox" do
1099 test "it paginates correctly", %{conn: conn} do
1100 user = insert(:user)
1101 conn = assign(conn, :user, user)
1102 outbox_endpoint = user.ap_id <> "/outbox"
1106 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
1112 |> put_req_header("accept", "application/activity+json")
1113 |> get(outbox_endpoint <> "?page=true")
1114 |> json_response(200)
1116 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
1117 assert length(result["orderedItems"]) == 20
1118 assert length(result_ids) == 20
1119 assert result["next"]
1120 assert String.starts_with?(result["next"], outbox_endpoint)
1124 |> put_req_header("accept", "application/activity+json")
1125 |> get(result["next"])
1126 |> json_response(200)
1128 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
1129 assert length(result_next["orderedItems"]) == 6
1130 assert length(result_next_ids) == 6
1131 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
1132 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
1133 assert String.starts_with?(result["id"], outbox_endpoint)
1137 |> put_req_header("accept", "application/activity+json")
1138 |> get(result_next["id"])
1139 |> json_response(200)
1141 assert result_next == result_next_again
1144 test "it returns 200 even if there're no activities", %{conn: conn} do
1145 user = insert(:user)
1146 outbox_endpoint = user.ap_id <> "/outbox"
1150 |> assign(:user, user)
1151 |> put_req_header("accept", "application/activity+json")
1152 |> get(outbox_endpoint)
1154 result = json_response(conn, 200)
1155 assert outbox_endpoint == result["id"]
1158 test "it returns a note activity in a collection", %{conn: conn} do
1159 note_activity = insert(:note_activity)
1160 note_object = Object.normalize(note_activity, fetch: false)
1161 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1165 |> assign(:user, user)
1166 |> put_req_header("accept", "application/activity+json")
1167 |> get("/users/#{user.nickname}/outbox?page=true")
1169 assert response(conn, 200) =~ note_object.data["content"]
1172 test "it returns an announce activity in a collection", %{conn: conn} do
1173 announce_activity = insert(:announce_activity)
1174 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1178 |> assign(:user, user)
1179 |> put_req_header("accept", "application/activity+json")
1180 |> get("/users/#{user.nickname}/outbox?page=true")
1182 assert response(conn, 200) =~ announce_activity.data["object"]
1185 test "It returns poll Answers when authenticated", %{conn: conn} do
1186 poller = insert(:user)
1187 voter = insert(:user)
1190 CommonAPI.post(poller, %{
1192 poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
1195 assert question = Object.normalize(activity, fetch: false)
1197 {:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
1201 |> assign(:user, voter)
1202 |> put_req_header("accept", "application/activity+json")
1203 |> get(voter.ap_id <> "/outbox?page=true")
1204 |> json_response(200)
1206 assert [answer_outbox] = outbox_get["orderedItems"]
1207 assert answer_outbox["id"] == activity.data["id"]
1211 describe "POST /users/:nickname/outbox (C2S)" do
1212 setup do: clear_config([:instance, :limit])
1217 "@context" => "https://www.w3.org/ns/activitystreams",
1219 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1220 "to" => "https://www.w3.org/ns/activitystreams#Public",
1226 test "it rejects posts from other users / unauthenticated users", %{
1230 user = insert(:user)
1231 other_user = insert(:user)
1232 conn = put_req_header(conn, "content-type", "application/activity+json")
1235 |> post("/users/#{user.nickname}/outbox", activity)
1236 |> json_response(403)
1239 |> assign(:user, other_user)
1240 |> post("/users/#{user.nickname}/outbox", activity)
1241 |> json_response(403)
1244 test "it inserts an incoming create activity into the database", %{
1248 user = insert(:user)
1252 |> assign(:user, user)
1253 |> put_req_header("content-type", "application/activity+json")
1254 |> post("/users/#{user.nickname}/outbox", activity)
1255 |> json_response(201)
1257 assert Activity.get_by_ap_id(result["id"])
1258 assert result["object"]
1259 assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
1260 assert object["content"] == activity["object"]["content"]
1263 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1264 user = insert(:user)
1268 |> put_in(["object", "type"], "Benis")
1272 |> assign(:user, user)
1273 |> put_req_header("content-type", "application/activity+json")
1274 |> post("/users/#{user.nickname}/outbox", activity)
1275 |> json_response(400)
1278 test "it inserts an incoming sensitive activity into the database", %{
1282 user = insert(:user)
1283 conn = assign(conn, :user, user)
1284 object = Map.put(activity["object"], "sensitive", true)
1285 activity = Map.put(activity, "object", object)
1289 |> put_req_header("content-type", "application/activity+json")
1290 |> post("/users/#{user.nickname}/outbox", activity)
1291 |> json_response(201)
1293 assert Activity.get_by_ap_id(response["id"])
1294 assert response["object"]
1295 assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
1296 assert response_object["sensitive"] == true
1297 assert response_object["content"] == activity["object"]["content"]
1301 |> put_req_header("accept", "application/activity+json")
1302 |> get(response["id"])
1303 |> json_response(200)
1305 assert representation["object"]["sensitive"] == true
1308 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1309 user = insert(:user)
1310 activity = Map.put(activity, "type", "BadType")
1314 |> assign(:user, user)
1315 |> put_req_header("content-type", "application/activity+json")
1316 |> post("/users/#{user.nickname}/outbox", activity)
1318 assert json_response(conn, 400)
1321 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1322 note_activity = insert(:note_activity)
1323 note_object = Object.normalize(note_activity, fetch: false)
1324 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1329 id: note_object.data["id"]
1335 |> assign(:user, user)
1336 |> put_req_header("content-type", "application/activity+json")
1337 |> post("/users/#{user.nickname}/outbox", data)
1339 result = json_response(conn, 201)
1340 assert Activity.get_by_ap_id(result["id"])
1342 assert object = Object.get_by_ap_id(note_object.data["id"])
1343 assert object.data["type"] == "Tombstone"
1346 test "it rejects delete activity of object from other actor", %{conn: conn} do
1347 note_activity = insert(:note_activity)
1348 note_object = Object.normalize(note_activity, fetch: false)
1349 user = insert(:user)
1354 id: note_object.data["id"]
1360 |> assign(:user, user)
1361 |> put_req_header("content-type", "application/activity+json")
1362 |> post("/users/#{user.nickname}/outbox", data)
1364 assert json_response(conn, 400)
1367 test "it increases like count when receiving a like action", %{conn: conn} do
1368 note_activity = insert(:note_activity)
1369 note_object = Object.normalize(note_activity, fetch: false)
1370 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1375 id: note_object.data["id"]
1381 |> assign(:user, user)
1382 |> put_req_header("content-type", "application/activity+json")
1383 |> post("/users/#{user.nickname}/outbox", data)
1385 result = json_response(conn, 201)
1386 assert Activity.get_by_ap_id(result["id"])
1388 assert object = Object.get_by_ap_id(note_object.data["id"])
1389 assert object.data["like_count"] == 1
1392 test "it doesn't spreads faulty attributedTo or actor fields", %{
1396 reimu = insert(:user, nickname: "reimu")
1397 cirno = insert(:user, nickname: "cirno")
1404 |> put_in(["object", "actor"], reimu.ap_id)
1405 |> put_in(["object", "attributedTo"], reimu.ap_id)
1406 |> put_in(["actor"], reimu.ap_id)
1407 |> put_in(["attributedTo"], reimu.ap_id)
1411 |> assign(:user, cirno)
1412 |> put_req_header("content-type", "application/activity+json")
1413 |> post("/users/#{reimu.nickname}/outbox", activity)
1414 |> json_response(403)
1418 |> assign(:user, cirno)
1419 |> put_req_header("content-type", "application/activity+json")
1420 |> post("/users/#{cirno.nickname}/outbox", activity)
1421 |> json_response(201)
1423 assert cirno_outbox["attributedTo"] == nil
1424 assert cirno_outbox["actor"] == cirno.ap_id
1426 assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
1427 assert cirno_object.data["actor"] == cirno.ap_id
1428 assert cirno_object.data["attributedTo"] == cirno.ap_id
1431 test "Character limitation", %{conn: conn, activity: activity} do
1432 clear_config([:instance, :limit], 5)
1433 user = insert(:user)
1437 |> assign(:user, user)
1438 |> put_req_header("content-type", "application/activity+json")
1439 |> post("/users/#{user.nickname}/outbox", activity)
1440 |> json_response(400)
1442 assert result == "Note is over the character limit"
1446 describe "/relay/followers" do
1447 test "it returns relay followers", %{conn: conn} do
1448 relay_actor = Relay.get_actor()
1449 user = insert(:user)
1450 User.follow(user, relay_actor)
1454 |> get("/relay/followers")
1455 |> json_response(200)
1457 assert result["first"]["orderedItems"] == [user.ap_id]
1460 test "on non-federating instance, it returns 404", %{conn: conn} do
1461 clear_config([:instance, :federating], false)
1462 user = insert(:user)
1465 |> assign(:user, user)
1466 |> get("/relay/followers")
1467 |> json_response(404)
1471 describe "/relay/following" do
1472 test "it returns relay following", %{conn: conn} do
1475 |> get("/relay/following")
1476 |> json_response(200)
1478 assert result["first"]["orderedItems"] == []
1481 test "on non-federating instance, it returns 404", %{conn: conn} do
1482 clear_config([:instance, :federating], false)
1483 user = insert(:user)
1486 |> assign(:user, user)
1487 |> get("/relay/following")
1488 |> json_response(404)
1492 describe "/users/:nickname/followers" do
1493 test "it returns the followers in a collection", %{conn: conn} do
1494 user = insert(:user)
1495 user_two = insert(:user)
1496 User.follow(user, user_two)
1500 |> assign(:user, user_two)
1501 |> get("/users/#{user_two.nickname}/followers")
1502 |> json_response(200)
1504 assert result["first"]["orderedItems"] == [user.ap_id]
1507 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1508 user = insert(:user)
1509 user_two = insert(:user, hide_followers: true)
1510 User.follow(user, user_two)
1514 |> assign(:user, user)
1515 |> get("/users/#{user_two.nickname}/followers")
1516 |> json_response(200)
1518 assert is_binary(result["first"])
1521 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1523 user = insert(:user)
1524 other_user = insert(:user, hide_followers: true)
1528 |> assign(:user, user)
1529 |> get("/users/#{other_user.nickname}/followers?page=1")
1531 assert result.status == 403
1532 assert result.resp_body == ""
1535 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1537 user = insert(:user, hide_followers: true)
1538 other_user = insert(:user)
1539 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1543 |> assign(:user, user)
1544 |> get("/users/#{user.nickname}/followers?page=1")
1545 |> json_response(200)
1547 assert result["totalItems"] == 1
1548 assert result["orderedItems"] == [other_user.ap_id]
1551 test "it works for more than 10 users", %{conn: conn} do
1552 user = insert(:user)
1554 Enum.each(1..15, fn _ ->
1555 other_user = insert(:user)
1556 User.follow(other_user, user)
1561 |> assign(:user, user)
1562 |> get("/users/#{user.nickname}/followers")
1563 |> json_response(200)
1565 assert length(result["first"]["orderedItems"]) == 10
1566 assert result["first"]["totalItems"] == 15
1567 assert result["totalItems"] == 15
1571 |> assign(:user, user)
1572 |> get("/users/#{user.nickname}/followers?page=2")
1573 |> json_response(200)
1575 assert length(result["orderedItems"]) == 5
1576 assert result["totalItems"] == 15
1579 test "does not require authentication", %{conn: conn} do
1580 user = insert(:user)
1583 |> get("/users/#{user.nickname}/followers")
1584 |> json_response(200)
1588 describe "/users/:nickname/following" do
1589 test "it returns the following in a collection", %{conn: conn} do
1590 user = insert(:user)
1591 user_two = insert(:user)
1592 User.follow(user, user_two)
1596 |> assign(:user, user)
1597 |> get("/users/#{user.nickname}/following")
1598 |> json_response(200)
1600 assert result["first"]["orderedItems"] == [user_two.ap_id]
1603 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1604 user = insert(:user)
1605 user_two = insert(:user, hide_follows: true)
1606 User.follow(user, user_two)
1610 |> assign(:user, user)
1611 |> get("/users/#{user_two.nickname}/following")
1612 |> json_response(200)
1614 assert is_binary(result["first"])
1617 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1619 user = insert(:user)
1620 user_two = insert(:user, hide_follows: true)
1624 |> assign(:user, user)
1625 |> get("/users/#{user_two.nickname}/following?page=1")
1627 assert result.status == 403
1628 assert result.resp_body == ""
1631 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1633 user = insert(:user, hide_follows: true)
1634 other_user = insert(:user)
1635 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1639 |> assign(:user, user)
1640 |> get("/users/#{user.nickname}/following?page=1")
1641 |> json_response(200)
1643 assert result["totalItems"] == 1
1644 assert result["orderedItems"] == [other_user.ap_id]
1647 test "it works for more than 10 users", %{conn: conn} do
1648 user = insert(:user)
1650 Enum.each(1..15, fn _ ->
1651 user = User.get_cached_by_id(user.id)
1652 other_user = insert(:user)
1653 User.follow(user, other_user)
1658 |> assign(:user, user)
1659 |> get("/users/#{user.nickname}/following")
1660 |> json_response(200)
1662 assert length(result["first"]["orderedItems"]) == 10
1663 assert result["first"]["totalItems"] == 15
1664 assert result["totalItems"] == 15
1668 |> assign(:user, user)
1669 |> get("/users/#{user.nickname}/following?page=2")
1670 |> json_response(200)
1672 assert length(result["orderedItems"]) == 5
1673 assert result["totalItems"] == 15
1676 test "does not require authentication", %{conn: conn} do
1677 user = insert(:user)
1680 |> get("/users/#{user.nickname}/following")
1681 |> json_response(200)
1685 describe "delivery tracking" do
1686 test "it tracks a signed object fetch", %{conn: conn} do
1687 user = insert(:user, local: false)
1688 activity = insert(:note_activity)
1689 object = Object.normalize(activity, fetch: false)
1691 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1694 |> put_req_header("accept", "application/activity+json")
1695 |> assign(:user, user)
1697 |> json_response(200)
1699 assert Delivery.get(object.id, user.id)
1702 test "it tracks a signed activity fetch", %{conn: conn} do
1703 user = insert(:user, local: false)
1704 activity = insert(:note_activity)
1705 object = Object.normalize(activity, fetch: false)
1707 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1710 |> put_req_header("accept", "application/activity+json")
1711 |> assign(:user, user)
1712 |> get(activity_path)
1713 |> json_response(200)
1715 assert Delivery.get(object.id, user.id)
1718 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1719 user = insert(:user, local: false)
1720 other_user = insert(:user, local: false)
1721 activity = insert(:note_activity)
1722 object = Object.normalize(activity, fetch: false)
1724 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1727 |> put_req_header("accept", "application/activity+json")
1728 |> assign(:user, user)
1730 |> json_response(200)
1733 |> put_req_header("accept", "application/activity+json")
1734 |> assign(:user, other_user)
1736 |> json_response(200)
1738 assert Delivery.get(object.id, user.id)
1739 assert Delivery.get(object.id, other_user.id)
1742 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1743 user = insert(:user, local: false)
1744 other_user = insert(:user, local: false)
1745 activity = insert(:note_activity)
1746 object = Object.normalize(activity, fetch: false)
1748 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1751 |> put_req_header("accept", "application/activity+json")
1752 |> assign(:user, user)
1753 |> get(activity_path)
1754 |> json_response(200)
1757 |> put_req_header("accept", "application/activity+json")
1758 |> assign(:user, other_user)
1759 |> get(activity_path)
1760 |> json_response(200)
1762 assert Delivery.get(object.id, user.id)
1763 assert Delivery.get(object.id, other_user.id)
1767 describe "Additional ActivityPub C2S endpoints" do
1768 test "GET /api/ap/whoami", %{conn: conn} do
1769 user = insert(:user)
1773 |> assign(:user, user)
1774 |> get("/api/ap/whoami")
1776 user = User.get_cached_by_id(user.id)
1778 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1781 |> get("/api/ap/whoami")
1782 |> json_response(403)
1785 setup do: clear_config([:media_proxy])
1786 setup do: clear_config([Pleroma.Upload])
1788 test "POST /api/ap/upload_media", %{conn: conn} do
1789 user = insert(:user)
1791 desc = "Description of the image"
1793 image = %Plug.Upload{
1794 content_type: "image/jpeg",
1795 path: Path.absname("test/fixtures/image.jpg"),
1796 filename: "an_image.jpg"
1801 |> assign(:user, user)
1802 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1803 |> json_response(:created)
1805 assert object["name"] == desc
1806 assert object["type"] == "Document"
1807 assert object["actor"] == user.ap_id
1808 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1809 assert is_binary(object_href)
1810 assert object_mediatype == "image/jpeg"
1811 assert String.ends_with?(object_href, ".jpg")
1813 activity_request = %{
1814 "@context" => "https://www.w3.org/ns/activitystreams",
1818 "content" => "AP C2S test, attachment",
1819 "attachment" => [object]
1821 "to" => "https://www.w3.org/ns/activitystreams#Public",
1827 |> assign(:user, user)
1828 |> post("/users/#{user.nickname}/outbox", activity_request)
1829 |> json_response(:created)
1831 assert activity_response["id"]
1832 assert activity_response["object"]
1833 assert activity_response["actor"] == user.ap_id
1835 assert %Object{data: %{"attachment" => [attachment]}} =
1836 Object.normalize(activity_response["object"], fetch: false)
1838 assert attachment["type"] == "Document"
1839 assert attachment["name"] == desc
1843 "href" => ^object_href,
1845 "mediaType" => ^object_mediatype
1847 ] = attachment["url"]
1849 # Fails if unauthenticated
1851 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1852 |> json_response(403)
1856 test "pinned collection", %{conn: conn} do
1857 clear_config([:instance, :max_pinned_statuses], 2)
1858 user = insert(:user)
1859 objects = insert_list(2, :note, user: user)
1861 Enum.reduce(objects, user, fn %{data: %{"id" => object_id}}, user ->
1862 {:ok, updated} = User.add_pinned_object_id(user, object_id)
1866 %{nickname: nickname, featured_address: featured_address, pinned_objects: pinned_objects} =
1867 refresh_record(user)
1869 %{"id" => ^featured_address, "orderedItems" => items} =
1871 |> get("/users/#{nickname}/collections/featured")
1872 |> json_response(200)
1874 object_ids = Enum.map(items, & &1["id"])
1876 assert Enum.all?(pinned_objects, fn {obj_id, _} ->
1877 obj_id in object_ids