Merge branch 'muting' into 'develop'

Implement muting, add it to the whole mastodon API

See merge request pleroma/pleroma!319
This commit is contained in:
lambda 2019-02-20 13:19:37 +00:00
commit 11b3c10c54
9 changed files with 205 additions and 22 deletions

View file

@ -888,6 +888,30 @@ def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_i
) )
end end
def mute(muter, %User{ap_id: ap_id}) do
info_cng =
muter.info
|> User.Info.add_to_mutes(ap_id)
cng =
change(muter)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def unmute(muter, %{ap_id: ap_id}) do
info_cng =
muter.info
|> User.Info.remove_from_mutes(ap_id)
cng =
change(muter)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def block(blocker, %User{ap_id: ap_id} = blocked) do def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker = blocker =
@ -930,6 +954,8 @@ def unblock(blocker, %{ap_id: ap_id}) do
update_and_set_cache(cng) update_and_set_cache(cng)
end end
def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
def blocks?(user, %{ap_id: ap_id}) do def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info.blocks blocks = user.info.blocks
domain_blocks = user.info.domain_blocks domain_blocks = user.info.domain_blocks
@ -941,6 +967,9 @@ def blocks?(user, %{ap_id: ap_id}) do
end) end)
end end
def muted_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
def blocked_users(user), def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks)) do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

View file

@ -19,6 +19,7 @@ defmodule Pleroma.User.Info do
field(:default_scope, :string, default: "public") field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: []) field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: []) field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false) field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false) field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false) field(:ap_enabled, :boolean, default: false)
@ -74,6 +75,14 @@ def set_follower_count(info, number) do
|> validate_required([:follower_count]) |> validate_required([:follower_count])
end end
def set_mutes(info, mutes) do
params = %{mutes: mutes}
info
|> cast(params, [:mutes])
|> validate_required([:mutes])
end
def set_blocks(info, blocks) do def set_blocks(info, blocks) do
params = %{blocks: blocks} params = %{blocks: blocks}
@ -82,6 +91,14 @@ def set_blocks(info, blocks) do
|> validate_required([:blocks]) |> validate_required([:blocks])
end end
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted))
end
def add_to_block(info, blocked) do def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks])) set_blocks(info, Enum.uniq([blocked | info.blocks]))
end end

View file

@ -576,6 +576,18 @@ defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or
defp restrict_reblogs(query, _), do: query defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
mutes = info.mutes
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
end
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || [] blocks = info.blocks || []
domain_blocks = info.domain_blocks || [] domain_blocks = info.domain_blocks || []
@ -629,6 +641,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_type(opts) |> restrict_type(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_replies(opts) |> restrict_replies(opts)

View file

@ -232,6 +232,7 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
params params
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user) |> Map.put("user", user)
activities = activities =
@ -254,6 +255,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse() |> Enum.reverse()
@ -620,6 +622,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("tag", tags) |> Map.put("tag", tags)
|> Map.put("tag_all", tag_all) |> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject) |> Map.put("tag_reject", tag_reject)
@ -763,6 +766,41 @@ def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
end end
end end
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
{:ok, muter} <- User.mute(muter, muted) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: muter, target: muted})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
{:ok, muter} <- User.unmute(muter, muted) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: muter, target: muted})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def mutes(%{assigns: %{user: user}} = conn, _) do
with muted_accounts <- User.muted_users(user) do
res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
json(conn, res)
end
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id), with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked), {:ok, blocker} <- User.block(blocker, blocked),
@ -1018,6 +1056,7 @@ def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params)
params params
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
# we must filter the following list for the user to avoid leaking statuses the user # we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270). # does not actually have permission to see (for more info, peruse security issue #270).

View file

@ -47,7 +47,7 @@ def render("relationship.json", %{user: user, target: target}) do
following: User.following?(user, target), following: User.following?(user, target),
followed_by: User.following?(target, user), followed_by: User.following?(target, user),
blocking: User.blocks?(user, target), blocking: User.blocks?(user, target),
muting: false, muting: User.mutes?(user, target),
muting_notifications: false, muting_notifications: false,
requested: requested, requested: requested,
domain_blocking: false, domain_blocking: false,

View file

@ -168,8 +168,8 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow) post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block) post("/accounts/:id/block", MastodonAPIController, :block)
post("/accounts/:id/unblock", MastodonAPIController, :unblock) post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop) post("/accounts/:id/mute", MastodonAPIController, :mute)
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop) post("/accounts/:id/unmute", MastodonAPIController, :unmute)
get("/accounts/:id/lists", MastodonAPIController, :account_lists) get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/follow_requests", MastodonAPIController, :follow_requests) get("/follow_requests", MastodonAPIController, :follow_requests)
@ -180,7 +180,7 @@ defmodule Pleroma.Web.Router do
get("/blocks", MastodonAPIController, :blocks) get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :empty_array) get("/mutes", MastodonAPIController, :mutes)
get("/timelines/home", MastodonAPIController, :home_timeline) get("/timelines/home", MastodonAPIController, :home_timeline)

View file

@ -594,6 +594,29 @@ test "it imports user followings from list" do
end end
end end
describe "mutes" do
test "it mutes people" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
{:ok, user} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
end
test "it unmutes users" do
user = insert(:user)
muted_user = insert(:user)
{:ok, user} = User.mute(user, muted_user)
{:ok, user} = User.unmute(user, muted_user)
refute User.mutes?(user, muted_user)
end
end
describe "blocks" do describe "blocks" do
test "it blocks people" do test "it blocks people" do
user = insert(:user) user = insert(:user)

View file

@ -277,6 +277,48 @@ test "doesn't return blocked activities" do
assert Enum.member?(activities, activity_one) assert Enum.member?(activities, activity_one)
end end
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Repo.get(Activity, activity_three.id)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities = ActivityPub.fetch_activities([], %{"muting_user" => nil})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "excludes reblogs on request" do test "excludes reblogs on request" do
user = insert(:user) user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})

View file

@ -1206,6 +1206,42 @@ test "following / unfollowing a user", %{conn: conn} do
assert id == to_string(other_user.id) assert id == to_string(other_user.id)
end end
test "muting / unmuting a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/mute")
assert %{"id" => _id, "muting" => true} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unmute")
assert %{"id" => _id, "muting" => false} = json_response(conn, 200)
end
test "getting a list of mutes", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/mutes")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
test "blocking / unblocking a user", %{conn: conn} do test "blocking / unblocking a user", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -1282,26 +1318,10 @@ test "getting a list of domain blocks", %{conn: conn} do
assert "even.worse.site" in domain_blocks assert "even.worse.site" in domain_blocks
end end
test "unimplemented mute endpoints" do test "unimplemented follow_requests, blocks, domain blocks" do
user = insert(:user)
other_user = insert(:user)
["mute", "unmute"]
|> Enum.each(fn endpoint ->
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/#{endpoint}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id)
end)
end
test "unimplemented mutes, follow_requests, blocks, domain blocks" do
user = insert(:user) user = insert(:user)
["blocks", "domain_blocks", "mutes", "follow_requests"] ["blocks", "domain_blocks", "follow_requests"]
|> Enum.each(fn endpoint -> |> Enum.each(fn endpoint ->
conn = conn =
build_conn() build_conn()