# Pleroma: A lightweight social networking server # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.CommonAPITest do use Oban.Testing, repo: Pleroma.Repo use Pleroma.DataCase alias Pleroma.Activity alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI alias Pleroma.Workers.PollWorker import Pleroma.Factory import Mock import Ecto.Query, only: [from: 2] require Pleroma.Constants setup_all do clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) clear_config([Pleroma.Uploaders.Local, :uploads], "uploads") Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end setup do: clear_config([:instance, :safe_dm_mentions]) setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) describe "posting polls" do test "it posts a poll" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{ status: "who is the best", poll: %{expires_in: 600, options: ["reimu", "marisa"]} }) object = Object.normalize(activity, fetch: false) assert object.data["type"] == "Question" assert object.data["oneOf"] |> length() == 2 {:ok, time, _} = DateTime.from_iso8601(object.data["closed"]) assert_enqueued( worker: PollWorker, args: %{op: "poll_end", activity_id: activity.id}, scheduled_at: time ) end end describe "blocking" do setup do blocker = insert(:user) blocked = insert(:user, local: false) CommonAPI.follow(blocker, blocked) CommonAPI.follow(blocked, blocker) CommonAPI.accept_follow_request(blocker, blocked) CommonAPI.accept_follow_request(blocked, blocked) %{blocker: blocker, blocked: blocked} end test "it blocks and federates", %{blocker: blocker, blocked: blocked} do clear_config([:instance, :federating], true) clear_config([:activitypub, :outgoing_blocks], true) with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do assert User.get_follow_state(blocker, blocked) == :follow_accept refute is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked)) assert {:ok, block} = CommonAPI.block(blocker, blocked) assert block.local assert User.blocks?(blocker, blocked) refute User.following?(blocker, blocked) refute User.following?(blocked, blocker) refute User.get_follow_state(blocker, blocked) assert %{data: %{"state" => "reject"}} = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked) assert called(Pleroma.Web.Federator.publish(block)) end end test "it blocks and does not federate if outgoing blocks are disabled", %{ blocker: blocker, blocked: blocked } do clear_config([:instance, :federating], true) clear_config([:activitypub, :outgoing_blocks], false) with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do assert {:ok, block} = CommonAPI.block(blocker, blocked) assert block.local assert User.blocks?(blocker, blocked) refute User.following?(blocker, blocked) refute User.following?(blocked, blocker) refute called(Pleroma.Web.Federator.publish(block)) end end end describe "unblocking" do test "it works even without an existing block activity" do blocked = insert(:user) blocker = insert(:user) User.block(blocker, blocked) assert User.blocks?(blocker, blocked) assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked) refute User.blocks?(blocker, blocked) end end describe "deletion" do test "it works with pruned objects" do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) clear_config([:instance, :federating], true) Object.normalize(post, fetch: false) |> Object.prune() with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do assert {:ok, delete} = CommonAPI.delete(post.id, user) assert delete.local assert called(Pleroma.Web.Federator.publish(delete)) end refute Activity.get_by_id(post.id) end test "it allows users to delete their posts" do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) clear_config([:instance, :federating], true) with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do assert {:ok, delete} = CommonAPI.delete(post.id, user) assert delete.local assert called(Pleroma.Web.Federator.publish(delete)) end refute Activity.get_by_id(post.id) end test "it does not allow a user to delete their posts" do user = insert(:user) other_user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) assert Activity.get_by_id(post.id) end test "it allows moderators to delete other user's posts" do user = insert(:user) moderator = insert(:user, is_moderator: true) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) assert {:ok, delete} = CommonAPI.delete(post.id, moderator) assert delete.local refute Activity.get_by_id(post.id) end test "it allows admins to delete other user's posts" do user = insert(:user) moderator = insert(:user, is_admin: true) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) assert {:ok, delete} = CommonAPI.delete(post.id, moderator) assert delete.local refute Activity.get_by_id(post.id) end test "superusers deleting non-local posts won't federate the delete" do # This is the user of the ingested activity _user = insert(:user, local: false, ap_id: "http://mastodon.example.org/users/admin", last_refreshed_at: NaiveDateTime.utc_now() ) moderator = insert(:user, is_admin: true) data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() {:ok, post} = Transmogrifier.handle_incoming(data) with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do assert {:ok, delete} = CommonAPI.delete(post.id, moderator) assert delete.local refute called(Pleroma.Web.Federator.publish(:_)) end refute Activity.get_by_id(post.id) end test "it allows privileged users to delete banned user's posts" do clear_config([:instance, :moderator_privileges], [:messages_delete]) user = insert(:user) moderator = insert(:user, is_moderator: true) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) User.set_activation(user, false) assert {:ok, delete} = CommonAPI.delete(post.id, moderator) assert delete.local refute Activity.get_by_id(post.id) end end test "favoriting race condition" do user = insert(:user) users_serial = insert_list(10, :user) users = insert_list(10, :user) {:ok, activity} = CommonAPI.post(user, %{status: "."}) users_serial |> Enum.map(fn user -> CommonAPI.favorite(user, activity.id) end) object = Object.get_by_ap_id(activity.data["object"]) assert object.data["like_count"] == 10 users |> Enum.map(fn user -> Task.async(fn -> CommonAPI.favorite(user, activity.id) end) end) |> Enum.map(&Task.await/1) object = Object.get_by_ap_id(activity.data["object"]) assert object.data["like_count"] == 20 end test "repeating race condition" do user = insert(:user) users_serial = insert_list(10, :user) users = insert_list(10, :user) {:ok, activity} = CommonAPI.post(user, %{status: "."}) users_serial |> Enum.map(fn user -> CommonAPI.repeat(activity.id, user) end) object = Object.get_by_ap_id(activity.data["object"]) assert object.data["announcement_count"] == 10 users |> Enum.map(fn user -> Task.async(fn -> CommonAPI.repeat(activity.id, user) end) end) |> Enum.map(&Task.await/1) object = Object.get_by_ap_id(activity.data["object"]) assert object.data["announcement_count"] == 20 end test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) [participation] = Participation.for_user(user) {:ok, convo_reply} = CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id}) assert Visibility.is_direct?(convo_reply) assert activity.data["context"] == convo_reply.data["context"] end test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do har = insert(:user) jafnhar = insert(:user) tridi = insert(:user) {:ok, activity} = CommonAPI.post(har, %{ status: "@#{jafnhar.nickname} hey", visibility: "direct" }) assert har.ap_id in activity.recipients assert jafnhar.ap_id in activity.recipients [participation] = Participation.for_user(har) {:ok, activity} = CommonAPI.post(har, %{ status: "I don't really like @#{tridi.nickname}", visibility: "direct", in_reply_to_status_id: activity.id, in_reply_to_conversation_id: participation.id }) assert har.ap_id in activity.recipients assert jafnhar.ap_id in activity.recipients refute tridi.ap_id in activity.recipients end test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do har = insert(:user) jafnhar = insert(:user) tridi = insert(:user) clear_config([:instance, :safe_dm_mentions], true) {:ok, activity} = CommonAPI.post(har, %{ status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again", visibility: "direct" }) refute tridi.ap_id in activity.recipients assert jafnhar.ap_id in activity.recipients end test "it de-duplicates tags" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"}) object = Object.normalize(activity, fetch: false) assert Object.tags(object) == ["2hu"] end test "it adds emoji in the object" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"}) assert Object.normalize(activity, fetch: false).data["emoji"]["firefox"] end describe "posting" do test "it adds an emoji on an external site" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey :external_emoji:"}) assert %{"external_emoji" => url} = Object.normalize(activity).data["emoji"] assert url == "https://example.com/emoji.png" {:ok, activity} = CommonAPI.post(user, %{status: "hey :blank:"}) assert %{"blank" => url} = Object.normalize(activity).data["emoji"] assert url == "#{Pleroma.Web.Endpoint.url()}/emoji/blank.png" end test "it copies emoji from the subject of the parent post" do %Object{} = object = Object.normalize("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f", fetch: true ) activity = Activity.get_create_by_object_ap_id(object.data["id"]) user = insert(:user) {:ok, reply_activity} = CommonAPI.post(user, %{ in_reply_to_id: activity.id, status: ":joker_disapprove:", spoiler_text: ":joker_smile:" }) assert Object.normalize(reply_activity).data["emoji"]["joker_smile"] refute Object.normalize(reply_activity).data["emoji"]["joker_disapprove"] end test "deactivated users can't post" do user = insert(:user, is_active: false) assert {:error, _} = CommonAPI.post(user, %{status: "ye"}) end test "it supports explicit addressing" do user = insert(:user) user_two = insert(:user) user_three = insert(:user) user_four = insert(:user) {:ok, activity} = CommonAPI.post(user, %{ status: "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.", to: [user_two.nickname, user_four.nickname, "nonexistent"] }) assert user.ap_id in activity.recipients assert user_two.ap_id in activity.recipients assert user_four.ap_id in activity.recipients refute user_three.ap_id in activity.recipients end test "it filters out obviously bad tags when accepting a post as HTML" do user = insert(:user) post = "

2hu

" {:ok, activity} = CommonAPI.post(user, %{ status: post, content_type: "text/html" }) object = Object.normalize(activity, fetch: false) assert object.data["content"] == "

2hu

alert('xss')" assert object.data["source"] == %{ "mediaType" => "text/html", "content" => post } end test "it filters out obviously bad tags when accepting a post as Markdown" do user = insert(:user) post = "

2hu

" {:ok, activity} = CommonAPI.post(user, %{ status: post, content_type: "text/markdown" }) object = Object.normalize(activity, fetch: false) assert object.data["content"] == "

2hu

" assert object.data["source"] == %{ "mediaType" => "text/markdown", "content" => post } end test "it does not allow replies to direct messages that are not direct messages themselves" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) assert {:ok, _} = CommonAPI.post(user, %{ status: "suya..", visibility: "direct", in_reply_to_status_id: activity.id }) Enum.each(["public", "private", "unlisted"], fn visibility -> assert {:error, "The message visibility must be direct"} = CommonAPI.post(user, %{ status: "suya..", visibility: visibility, in_reply_to_status_id: activity.id }) end) end test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do user = insert(:user) other_user = insert(:user) third_user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"}) {:ok, open_answer} = CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id}) # The OP is implicitly added assert user.ap_id in open_answer.recipients {:ok, secret_answer} = CommonAPI.post(other_user, %{ status: "lol, that guy really is stupid, right, @#{third_user.nickname}?", in_reply_to_status_id: post.id, visibility: "direct" }) assert third_user.ap_id in secret_answer.recipients # The OP is not added refute user.ap_id in secret_answer.recipients end test "it allows to address a list" do user = insert(:user) {:ok, list} = Pleroma.List.create("foo", user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) assert activity.data["bcc"] == [list.ap_id] assert activity.recipients == [list.ap_id, user.ap_id] assert activity.data["listMessage"] == list.ap_id end test "it returns error when status is empty and no attachments" do user = insert(:user) assert {:error, "Cannot post an empty status without attachments"} = CommonAPI.post(user, %{status: ""}) end test "it validates character limits are correctly enforced" do clear_config([:instance, :limit], 5) user = insert(:user) assert {:error, "The status is over the character limit"} = CommonAPI.post(user, %{status: "foobar"}) assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"}) end test "it can handle activities that expire" do user = insert(:user) expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000}) assert_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: activity.id}, scheduled_at: expires_at ) end end describe "reactions" do test "reacting to a status with an emoji" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") assert reaction.data["actor"] == user.ap_id assert reaction.data["content"] == "👍" {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".") end test "unreacting to a status with an emoji" do user = insert(:user) other_user = insert(:user) clear_config([:instance, :federating], true) with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") assert unreaction.data["type"] == "Undo" assert unreaction.data["object"] == reaction.data["id"] assert unreaction.local # On federation, it contains the undone (and deleted) object unreaction_with_object = %{ unreaction | data: Map.put(unreaction.data, "object", reaction.data) } assert called(Pleroma.Web.Federator.publish(unreaction_with_object)) end end test "repeating a status" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user) assert Visibility.is_public?(announce_activity) end test "can't repeat a repeat" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user) refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user)) end test "repeating a status privately" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user, %{visibility: "private"}) assert Visibility.is_private?(announce_activity) refute Visibility.visible_for_user?(announce_activity, nil) end test "author can repeat own private statuses" do author = insert(:user) follower = insert(:user) CommonAPI.follow(follower, author) {:ok, activity} = CommonAPI.post(author, %{status: "cofe", visibility: "private"}) {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author) assert Visibility.is_private?(announce_activity) refute Visibility.visible_for_user?(announce_activity, nil) assert Visibility.visible_for_user?(activity, follower) assert {:error, :not_found} = CommonAPI.repeat(activity.id, follower) end test "favoriting a status" do user = insert(:user) other_user = insert(:user) {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id) assert data["type"] == "Like" assert data["actor"] == user.ap_id assert data["object"] == post_activity.data["object"] end test "retweeting a status twice returns the status" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user) {:ok, ^announce} = CommonAPI.repeat(activity.id, user) end test "favoriting a status twice returns ok, but without the like activity" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id) end end describe "pinned statuses" do setup do clear_config([:instance, :max_pinned_statuses], 1) user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) [user: user, activity: activity] end test "activity not found error", %{user: user} do assert {:error, :not_found} = CommonAPI.pin("id", user) end test "pin status", %{user: user, activity: activity} do assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) %{data: %{"id" => object_id}} = Object.normalize(activity) user = refresh_record(user) assert user.pinned_objects |> Map.keys() == [object_id] end test "pin poll", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{ status: "How is fediverse today?", poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20} }) assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) %{data: %{"id" => object_id}} = Object.normalize(activity) user = refresh_record(user) assert user.pinned_objects |> Map.keys() == [object_id] end test "unlisted statuses can be pinned", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"}) assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) end test "only self-authored can be pinned", %{activity: activity} do user = insert(:user) assert {:error, :ownership_error} = CommonAPI.pin(activity.id, user) end test "max pinned statuses", %{user: user, activity: activity_one} do {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user) user = refresh_record(user) assert {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity_two.id, user) end test "only public can be pinned", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"}) {:error, :visibility_error} = CommonAPI.pin(activity.id, user) end test "unpin status", %{user: user, activity: activity} do {:ok, activity} = CommonAPI.pin(activity.id, user) user = refresh_record(user) id = activity.id assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user)) user = refresh_record(user) assert user.pinned_objects == %{} end test "should unpin when deleting a status", %{user: user, activity: activity} do {:ok, activity} = CommonAPI.pin(activity.id, user) user = refresh_record(user) assert {:ok, _} = CommonAPI.delete(activity.id, user) user = refresh_record(user) assert user.pinned_objects == %{} end test "ephemeral activity won't be deleted if was pinned", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{status: "Hello!", expires_in: 601}) assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) {:ok, _activity} = CommonAPI.pin(activity.id, user) refute Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) user = refresh_record(user) {:ok, _} = CommonAPI.unpin(activity.id, user) # recreates expiration job on unpin assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) end test "ephemeral activity deletion job won't be deleted on pinning error", %{ user: user, activity: activity } do clear_config([:instance, :max_pinned_statuses], 1) {:ok, _activity} = CommonAPI.pin(activity.id, user) {:ok, activity2} = CommonAPI.post(user, %{status: "another status", expires_in: 601}) assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id) user = refresh_record(user) {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity2.id, user) assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id) end end describe "mute tests" do setup do user = insert(:user) activity = insert(:note_activity) [user: user, activity: activity] end test "marks notifications as read after mute" do author = insert(:user) activity = insert(:note_activity, user: author) friend1 = insert(:user) friend2 = insert(:user) {:ok, reply_activity} = CommonAPI.post( friend2, %{ status: "@#{author.nickname} @#{friend1.nickname} test reply", in_reply_to_status_id: activity.id } ) {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id) {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1) assert Repo.aggregate( from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), :count ) == 1 unread_notifications = Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id)) assert Enum.any?(unread_notifications, fn n -> n.type == "favourite" && n.activity_id == favorite_activity.id end) assert Enum.any?(unread_notifications, fn n -> n.type == "reblog" && n.activity_id == repeat_activity.id end) assert Enum.any?(unread_notifications, fn n -> n.type == "mention" && n.activity_id == reply_activity.id end) {:ok, _} = CommonAPI.add_mute(author, activity) assert CommonAPI.thread_muted?(author, activity) assert Repo.aggregate( from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), :count ) == 1 read_notifications = Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id)) assert Enum.any?(read_notifications, fn n -> n.type == "favourite" && n.activity_id == favorite_activity.id end) assert Enum.any?(read_notifications, fn n -> n.type == "reblog" && n.activity_id == repeat_activity.id end) assert Enum.any?(read_notifications, fn n -> n.type == "mention" && n.activity_id == reply_activity.id end) end test "add mute", %{user: user, activity: activity} do {:ok, _} = CommonAPI.add_mute(user, activity) assert CommonAPI.thread_muted?(user, activity) end test "add expiring mute", %{user: user, activity: activity} do {:ok, _} = CommonAPI.add_mute(user, activity, %{expires_in: 60}) assert CommonAPI.thread_muted?(user, activity) worker = Pleroma.Workers.MuteExpireWorker args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id} assert_enqueued( worker: worker, args: args ) assert :ok = perform_job(worker, args) refute CommonAPI.thread_muted?(user, activity) end test "remove mute", %{user: user, activity: activity} do CommonAPI.add_mute(user, activity) {:ok, _} = CommonAPI.remove_mute(user, activity) refute CommonAPI.thread_muted?(user, activity) end test "remove mute by ids", %{user: user, activity: activity} do CommonAPI.add_mute(user, activity) {:ok, _} = CommonAPI.remove_mute(user.id, activity.id) refute CommonAPI.thread_muted?(user, activity) end test "check that mutes can't be duplicate", %{user: user, activity: activity} do CommonAPI.add_mute(user, activity) {:error, _} = CommonAPI.add_mute(user, activity) end end describe "reports" do test "creates a report" do reporter = insert(:user) target_user = insert(:user) {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) reporter_ap_id = reporter.ap_id target_ap_id = target_user.ap_id activity_ap_id = activity.data["id"] comment = "foobar" report_data = %{ account_id: target_user.id, comment: comment, status_ids: [activity.id] } note_obj = %{ "type" => "Note", "id" => activity_ap_id, "content" => "foobar", "published" => activity.object.data["published"], "actor" => AccountView.render("show.json", %{user: target_user}) } assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) assert %Activity{ actor: ^reporter_ap_id, data: %{ "type" => "Flag", "content" => ^comment, "object" => [^target_ap_id, ^note_obj], "state" => "open" } } = flag_activity end test "updates report state" do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{ account_id: target_user.id, comment: "I feel offended", status_ids: [activity.id] }) {:ok, report} = CommonAPI.update_report_state(report_id, "resolved") assert report.data["state"] == "resolved" [reported_user, activity_id] = report.data["object"] assert reported_user == target_user.ap_id assert activity_id == activity.data["id"] end test "does not update report state when state is unsupported" do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{ account_id: target_user.id, comment: "I feel offended", status_ids: [activity.id] }) assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} end test "updates state of multiple reports" do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) {:ok, %Activity{id: first_report_id}} = CommonAPI.report(reporter, %{ account_id: target_user.id, comment: "I feel offended", status_ids: [activity.id] }) {:ok, %Activity{id: second_report_id}} = CommonAPI.report(reporter, %{ account_id: target_user.id, comment: "I feel very offended!", status_ids: [activity.id] }) {:ok, report_ids} = CommonAPI.update_report_state([first_report_id, second_report_id], "resolved") first_report = Activity.get_by_id(first_report_id) second_report = Activity.get_by_id(second_report_id) assert report_ids -- [first_report_id, second_report_id] == [] assert first_report.data["state"] == "resolved" assert second_report.data["state"] == "resolved" end end describe "reblog muting" do setup do muter = insert(:user) muted = insert(:user) [muter: muter, muted: muted] end test "add a reblog mute", %{muter: muter, muted: muted} do {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) assert User.showing_reblogs?(muter, muted) == false end test "remove a reblog mute", %{muter: muter, muted: muted} do {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) assert User.showing_reblogs?(muter, muted) == true end end describe "follow/2" do test "directly follows a non-locked local user" do [follower, followed] = insert_pair(:user) {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) assert User.following?(follower, followed) end end describe "unfollow/2" do test "also unsubscribes a user" do [follower, followed] = insert_pair(:user) {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) {:ok, _subscription} = User.subscribe(follower, followed) assert User.subscribed_to?(follower, followed) {:ok, follower} = CommonAPI.unfollow(follower, followed) refute User.subscribed_to?(follower, followed) end test "removes a pending follow for a local user" do follower = insert(:user) followed = insert(:user, is_locked: true) assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) assert User.get_follow_state(follower, followed) == :follow_pending assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert User.get_follow_state(follower, followed) == nil assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)) assert %{ data: %{ "type" => "Undo", "object" => %{"type" => "Follow"} } } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) end test "cancels a pending follow for a remote user" do follower = insert(:user) followed = insert(:user, is_locked: true, local: false, ap_enabled: true) assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) assert User.get_follow_state(follower, followed) == :follow_pending assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert User.get_follow_state(follower, followed) == nil assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)) assert %{ data: %{ "type" => "Undo", "object" => %{"type" => "Follow"} } } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) end end describe "accept_follow_request/2" do test "after acceptance, it sets all existing pending follow request states to 'accept'" do user = insert(:user, is_locked: true) follower = insert(:user) follower_two = insert(:user) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) assert follow_activity.data["state"] == "pending" assert follow_activity_two.data["state"] == "pending" assert follow_activity_three.data["state"] == "pending" {:ok, _follower} = CommonAPI.accept_follow_request(follower, user) assert Repo.get(Activity, follow_activity.id).data["state"] == "accept" assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept" assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" end test "after rejection, it sets all existing pending follow request states to 'reject'" do user = insert(:user, is_locked: true) follower = insert(:user) follower_two = insert(:user) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) assert follow_activity.data["state"] == "pending" assert follow_activity_two.data["state"] == "pending" assert follow_activity_three.data["state"] == "pending" {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) assert Repo.get(Activity, follow_activity.id).data["state"] == "reject" assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" end test "doesn't create a following relationship if the corresponding follow request doesn't exist" do user = insert(:user, is_locked: true) not_follower = insert(:user) CommonAPI.accept_follow_request(not_follower, user) assert Pleroma.FollowingRelationship.following?(not_follower, user) == false end end describe "vote/3" do test "does not allow to vote twice" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{ status: "Am I cute?", poll: %{options: ["Yes", "No"], expires_in: 20} }) object = Object.normalize(activity, fetch: false) {:ok, _, object} = CommonAPI.vote(other_user, object, [0]) assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1]) end end describe "get_user/1" do test "gets user by ap_id" do user = insert(:user) assert CommonAPI.get_user(user.ap_id) == user end test "gets user by guessed nickname" do user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom") assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user end test "fallback" do assert %User{ name: "", ap_id: "", nickname: "erroruser@example.com" } = CommonAPI.get_user("") end end describe "with `local` visibility" do setup do: clear_config([:instance, :federating], true) test "post" do user = insert(:user) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) assert Visibility.is_local_public?(activity) assert_not_called(Pleroma.Web.Federator.publish(activity)) end end test "delete" do user = insert(:user) {:ok, %Activity{id: activity_id}} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} = CommonAPI.delete(activity_id, user) assert Visibility.is_local_public?(activity) assert_not_called(Pleroma.Web.Federator.publish(activity)) end end test "repeat" do user = insert(:user) other_user = insert(:user) {:ok, %Activity{id: activity_id}} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"}) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} = CommonAPI.repeat(activity_id, user) assert Visibility.is_local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end test "unrepeat" do user = insert(:user) other_user = insert(:user) {:ok, %Activity{id: activity_id}} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"}) assert {:ok, _} = CommonAPI.repeat(activity_id, user) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} = CommonAPI.unrepeat(activity_id, user) assert Visibility.is_local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end test "favorite" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"}) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} = CommonAPI.favorite(user, activity.id) assert Visibility.is_local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end test "unfavorite" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"}) {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user) assert Visibility.is_local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end test "react_with_emoji" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"}) with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} = CommonAPI.react_with_emoji(activity.id, user, "👍") assert Visibility.is_local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end test "unreact_with_emoji" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"}) {:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") assert Visibility.is_local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end end describe "update/3" do test "updates a post" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"}) {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"}) updated_object = Object.normalize(updated) assert updated_object.data["content"] == "updated 2" assert Map.get(updated_object.data, "summary", "") == "" assert Map.has_key?(updated_object.data, "updated") end test "does not change visibility" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"}) {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"}) updated_object = Object.normalize(updated) assert updated_object.data["content"] == "updated 2" assert Map.get(updated_object.data, "summary", "") == "" assert Visibility.get_visibility(updated_object) == "private" assert Visibility.get_visibility(updated) == "private" end test "updates a post with emoji" do [{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all() user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"}) {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"}) updated_object = Object.normalize(updated) assert updated_object.data["content"] == "updated 2 :#{emoji2}:" assert %{^emoji2 => _} = updated_object.data["emoji"] end test "updates a post with emoji and federate properly" do [{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all() user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"}) clear_config([:instance, :federating], true) with_mock Pleroma.Web.Federator, publish: fn _p -> nil end do {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"}) assert updated.data["object"]["content"] == "updated 2 :#{emoji2}:" assert %{^emoji2 => _} = updated.data["object"]["emoji"] assert called(Pleroma.Web.Federator.publish(updated)) end end test "editing a post that copied a remote title with remote emoji should keep that emoji" do remote_emoji_uri = "https://remote.org/emoji.png" note = insert( :note, data: %{ "summary" => ":remoteemoji:", "emoji" => %{ "remoteemoji" => remote_emoji_uri }, "tag" => [ %{ "type" => "Emoji", "name" => "remoteemoji", "icon" => %{"url" => remote_emoji_uri} } ] } ) note_activity = insert(:note_activity, note: note) user = insert(:user) {:ok, reply} = CommonAPI.post(user, %{ status: "reply", spoiler_text: ":remoteemoji:", in_reply_to_id: note_activity.id }) assert reply.object.data["emoji"]["remoteemoji"] == remote_emoji_uri {:ok, edit} = CommonAPI.update(user, reply, %{status: "reply mew mew", spoiler_text: ":remoteemoji:"}) edited_note = Pleroma.Object.normalize(edit) assert edited_note.data["emoji"]["remoteemoji"] == remote_emoji_uri end test "respects MRF" do user = insert(:user) clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) clear_config([:mrf_keyword, :replace], [{"updated", "mewmew"}]) {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "updated 1"}) assert Object.normalize(activity).data["summary"] == "mewmew 1" {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"}) updated_object = Object.normalize(updated) assert updated_object.data["content"] == "mewmew 2" assert Map.get(updated_object.data, "summary", "") == "" assert Map.has_key?(updated_object.data, "updated") end end end