diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7214f98..bcdd0615f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`) - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity - Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`) +- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes - ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set - Existing user id not being preserved on insert conflict - Rich Media: Parser failing when no TTL can be found by image TTL setters diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex index fadc89891..b9e80acdd 100644 --- a/lib/pleroma/user_invite_token.ex +++ b/lib/pleroma/user_invite_token.ex @@ -74,7 +74,7 @@ def find_by_token!(token), do: Repo.get_by!(UserInviteToken, token: token) @spec find_by_token(token()) :: {:ok, UserInviteToken.t()} | nil def find_by_token(token) do - with invite <- Repo.get_by(UserInviteToken, token: token) do + with %UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, token: token) do {:ok, invite} end end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 4a0bf4823..811be1eff 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -272,11 +272,13 @@ def invites(conn, _params) do @doc "Revokes invite by token" def revoke_invite(conn, %{"token" => token}) do - invite = UserInviteToken.find_by_token!(token) - {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) - - conn - |> json(AccountView.render("invite.json", %{invite: updated_invite})) + with {:ok, invite} <- UserInviteToken.find_by_token(token), + {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do + conn + |> json(AccountView.render("invite.json", %{invite: updated_invite})) + else + nil -> {:error, :not_found} + end end @doc "Get a password reset token (base64 string) for given nickname" diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index e8b43e475..d660f3f05 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -883,7 +883,10 @@ def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"likes" => likes}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^likes) - users = Repo.all(q) + + users = + Repo.all(q) + |> Enum.filter(&(not User.blocks?(user, &1))) conn |> put_view(AccountView) @@ -897,7 +900,10 @@ def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^announces) - users = Repo.all(q) + + users = + Repo.all(q) + |> Enum.filter(&(not User.blocks?(user, &1))) conn |> put_view(AccountView) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 4f325113a..86e2dc4dd 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.Streamer do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.NotificationView @keepalive_interval :timer.seconds(30) @@ -118,10 +119,14 @@ def handle_cast( topics |> Map.get("#{topic}:#{item.user_id}", []) |> Enum.each(fn socket -> - send( - socket.transport_pid, - {:text, represent_notification(socket.assigns[:user], item)} - ) + with %User{} = user <- User.get_cached_by_ap_id(socket.assigns[:user].ap_id), + true <- should_send?(user, item), + false <- CommonAPI.thread_muted?(user, item.activity) do + send( + socket.transport_pid, + {:text, represent_notification(socket.assigns[:user], item)} + ) + end end) {:noreply, topics} @@ -225,19 +230,32 @@ defp represent_notification(%User{} = user, %Notification{} = notify) do |> Jason.encode!() end + defp should_send?(%User{} = user, %Activity{} = item) do + blocks = user.info.blocks || [] + mutes = user.info.mutes || [] + reblog_mutes = user.info.muted_reblogs || [] + + with parent when not is_nil(parent) <- Object.normalize(item), + true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), + true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), + true <- thread_containment(item, user) do + true + else + _ -> false + end + end + + defp should_send?(%User{} = user, %Notification{activity: activity}) do + should_send?(user, activity) + end + def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do Enum.each(topics[topic] || [], fn socket -> # Get the current user so we have up-to-date blocks etc. if socket.assigns[:user] do user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id) - blocks = user.info.blocks || [] - mutes = user.info.mutes || [] - reblog_mutes = user.info.muted_reblogs || [] - with parent when not is_nil(parent) <- Object.normalize(item), - true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), - true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), - true <- thread_containment(item, user) do + if should_send?(user, item) do send(socket.transport_pid, {:text, represent_update(item, user)}) end else diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index 3d4b08fba..2b9453042 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -5,6 +5,9 @@ defmodule Mix.Tasks.Pleroma.UserTest do alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + use Pleroma.DataCase import Pleroma.Factory @@ -327,6 +330,13 @@ test "invite is revoked" do assert_received {:mix_shell, :info, [message]} assert message =~ "Invite for token #{invite.token} was revoked." end + + test "it prints an error message when invite is not exist" do + Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No invite found" + end end describe "running delete_activities" do @@ -337,6 +347,13 @@ test "activities are deleted" do assert_received {:mix_shell, :info, [message]} assert message == "User #{nickname} statuses deleted." end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end end describe "running toggle_confirmed" do @@ -364,6 +381,13 @@ test "user is not confirmed" do refute user.info.confirmation_pending refute user.info.confirmation_token end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end end describe "search" do @@ -386,4 +410,64 @@ test "it returns users matching" do User.Search.search("moon fediverse", for_user: user) |> Enum.map(& &1.id) end end + + describe "signing out" do + test "it deletes all user's tokens and authorizations" do + user = insert(:user) + insert(:oauth_token, user: user) + insert(:oauth_authorization, user: user) + + assert Repo.get_by(Token, user_id: user.id) + assert Repo.get_by(Authorization, user_id: user.id) + + :ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname]) + + refute Repo.get_by(Token, user_id: user.id) + refute Repo.get_by(Authorization, user_id: user.id) + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["sign_out", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "tagging" do + test "it add tags to a user" do + user = insert(:user) + + :ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"]) + + user = User.get_cached_by_nickname(user.nickname) + assert "pleroma" in user.tags + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["tag", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "Could not change user tags" + end + end + + describe "untagging" do + test "it deletes tags from a user" do + user = insert(:user, tags: ["pleroma"]) + assert "pleroma" in user.tags + + :ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"]) + + user = User.get_cached_by_nickname(user.nickname) + assert Enum.empty?(user.tags) + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["untag", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "Could not change user tags" + end + end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index ee48b752c..03aa46cae 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1010,6 +1010,17 @@ test "with token" do "uses" => 0 } end + + test "with invalid token" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + + assert json_response(conn, :not_found) == "Not found" + end end describe "GET /api/pleroma/admin/reports/:id" do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index b4b1dd785..a3e4c4136 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3768,6 +3768,24 @@ test "returns empty array when status has not been favorited yet", %{ assert Enum.empty?(response) end + + test "does not return users who have favorited the status but are blocked", %{ + conn: %{assigns: %{user: user}} = conn, + activity: activity + } do + other_user = insert(:user) + {:ok, user} = User.block(user, other_user) + + {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response(:ok) + + assert Enum.empty?(response) + end end describe "GET /api/v1/statuses/:id/reblogged_by" do @@ -3807,6 +3825,24 @@ test "returns empty array when status has not been reblogged yet", %{ assert Enum.empty?(response) end + + test "does not return users who have reblogged the status but are blocked", %{ + conn: %{assigns: %{user: user}} = conn, + activity: activity + } do + other_user = insert(:user) + {:ok, user} = User.block(user, other_user) + + {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response(:ok) + + assert Enum.empty?(response) + end end describe "POST /auth/password, with valid parameters" do diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index aae34804d..92e156347 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -5,9 +5,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory - import Mock - alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.OAuthController @@ -108,28 +106,26 @@ test "with user-bound registration, GET /oauth//callback redirects to "state" => "" } - with_mock Pleroma.Web.Auth.Authenticator, - get_registration: fn _ -> {:ok, registration} end do - conn = - get( - conn, - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) + conn = + conn + |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ end test "with user-unbound registration, GET /oauth//callback renders registration_details page", %{app: app, conn: conn} do - registration = insert(:registration, user: nil) + user = insert(:user) state_params = %{ "scope" => "read write", @@ -138,26 +134,28 @@ test "with user-unbound registration, GET /oauth//callback renders reg "state" => "a_state" } - with_mock Pleroma.Web.Auth.Authenticator, - get_registration: fn _ -> {:ok, registration} end do - conn = - get( - conn, - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) + conn = + conn + |> assign(:ueberauth_auth, %{ + provider: "twitter", + uid: "171799000", + info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} + }) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - assert response =~ Registration.email(registration) - assert response =~ Registration.nickname(registration) - end + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + assert response =~ user.email + assert response =~ user.nickname end test "on authentication error, GET /oauth//callback redirects to `redirect_uri`", %{ diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs index 4633d7765..8f56e7486 100644 --- a/test/web/streamer_test.exs +++ b/test/web/streamer_test.exs @@ -65,6 +65,44 @@ test "it sends notify to in the 'user:notification' stream", %{user: user, notif Streamer.stream("user:notification", notify) Task.await(task) end + + test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ + user: user + } do + blocked = insert(:user) + {:ok, user} = User.block(user, blocked) + + task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) + + Streamer.add_socket( + "user:notification", + %{transport_pid: task.pid, assigns: %{user: user}} + ) + + {:ok, activity} = CommonAPI.post(user, %{"status" => ":("}) + {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked) + + Streamer.stream("user:notification", notif) + Task.await(task) + end + + test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ + user: user + } do + user2 = insert(:user) + task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) + + Streamer.add_socket( + "user:notification", + %{transport_pid: task.pid, assigns: %{user: user}} + ) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + {:ok, activity} = CommonAPI.add_mute(user, activity) + {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) + Streamer.stream("user:notification", notif) + Task.await(task) + end end test "it sends to public" do