Merge branch 'feature/expire-mutes' into 'develop'
Expiring mutes for users and activities Closes #1817 See merge request pleroma/pleroma!2971
This commit is contained in:
commit
294628d981
@ -43,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Admin API: (`GET /api/pleroma/admin/users`) added filters user by `actor_type`
|
||||
- Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending.
|
||||
- Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances.
|
||||
- Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute.
|
||||
|
||||
</details>
|
||||
|
||||
|
@ -562,7 +562,8 @@ config :pleroma, Oban,
|
||||
background: 5,
|
||||
remote_fetcher: 2,
|
||||
attachments_cleanup: 5,
|
||||
new_users_digest: 1
|
||||
new_users_digest: 1,
|
||||
mute_expire: 5
|
||||
],
|
||||
plugins: [Oban.Plugins.Pruner],
|
||||
crontab: [
|
||||
|
@ -255,6 +255,10 @@ There is an additional `user:pleroma_chat` stream. Incoming chat messages will m
|
||||
|
||||
For viewing remote server timelines, there are `public:remote` and `public:remote:media` streams. Each of these accept a parameter like `?instance=lain.com`.
|
||||
|
||||
## User muting and thread muting
|
||||
|
||||
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.
|
||||
|
||||
## Not implemented
|
||||
|
||||
Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer features and non-essential features are omitted. These features usually return an HTTP 200 status code, but with an empty response. While they may be added in the future, they are considered low priority.
|
||||
|
@ -1324,14 +1324,48 @@ defmodule Pleroma.User do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec mute(User.t(), User.t(), boolean()) ::
|
||||
@spec mute(User.t(), User.t(), map()) ::
|
||||
{:ok, list(UserRelationship.t())} | {:error, String.t()}
|
||||
def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
|
||||
add_to_mutes(muter, mutee, notifications?)
|
||||
def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
|
||||
notifications? = Map.get(params, :notifications, true)
|
||||
expires_in = Map.get(params, :expires_in, 0)
|
||||
|
||||
with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
|
||||
{:ok, user_notification_mute} <-
|
||||
(notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
|
||||
{:ok, nil} do
|
||||
if expires_in > 0 do
|
||||
Pleroma.Workers.MuteExpireWorker.enqueue(
|
||||
"unmute_user",
|
||||
%{"muter_id" => muter.id, "mutee_id" => mutee.id},
|
||||
schedule_in: expires_in
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
|
||||
end
|
||||
end
|
||||
|
||||
def unmute(%User{} = muter, %User{} = mutee) do
|
||||
remove_from_mutes(muter, mutee)
|
||||
with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
|
||||
{:ok, user_notification_mute} <-
|
||||
UserRelationship.delete_notification_mute(muter, mutee) do
|
||||
{:ok, [user_mute, user_notification_mute]}
|
||||
end
|
||||
end
|
||||
|
||||
def unmute(muter_id, mutee_id) do
|
||||
with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
|
||||
{:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
|
||||
unmute(muter, mutee)
|
||||
else
|
||||
{who, result} = error ->
|
||||
Logger.warn(
|
||||
"User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def subscribe(%User{} = subscriber, %User{} = target) do
|
||||
@ -2320,23 +2354,6 @@ defmodule Pleroma.User do
|
||||
UserRelationship.delete_block(user, blocked)
|
||||
end
|
||||
|
||||
defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
|
||||
with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
|
||||
{:ok, user_notification_mute} <-
|
||||
(notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
|
||||
{:ok, nil} do
|
||||
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_from_mutes(user, %User{} = muted_user) do
|
||||
with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
|
||||
{:ok, user_notification_mute} <-
|
||||
UserRelationship.delete_notification_mute(user, muted_user) do
|
||||
{:ok, [user_mute, user_notification_mute]}
|
||||
end
|
||||
end
|
||||
|
||||
def set_invisible(user, invisible) do
|
||||
params = %{invisible: invisible}
|
||||
|
||||
|
@ -262,6 +262,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: true},
|
||||
"Mute notifications in addition to statuses? Defaults to `true`."
|
||||
),
|
||||
Operation.parameter(
|
||||
:expires_in,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 0},
|
||||
"Expire the mute in `expires_in` seconds. Default 0 for infinity"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
@ -723,10 +729,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
||||
nullable: true,
|
||||
description: "Mute notifications in addition to statuses? Defaults to true.",
|
||||
default: true
|
||||
},
|
||||
expires_in: %Schema{
|
||||
type: :integer,
|
||||
nullable: true,
|
||||
description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"notifications" => true
|
||||
"notifications" => true,
|
||||
"expires_in" => 86_400
|
||||
}
|
||||
}
|
||||
end
|
||||
|
@ -223,7 +223,27 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||
security: [%{"oAuth" => ["write:mutes"]}],
|
||||
description: "Do not receive notifications for the thread that this status is part of.",
|
||||
operationId: "StatusController.mute_conversation",
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
expires_in: %Schema{
|
||||
type: :integer,
|
||||
nullable: true,
|
||||
description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
}),
|
||||
parameters: [
|
||||
id_param(),
|
||||
Operation.parameter(
|
||||
:expires_in,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 0},
|
||||
"Expire the mute in `expires_in` seconds. Default 0 for infinity"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => status_response(),
|
||||
400 => Operation.response("Error", "application/json", ApiError)
|
||||
|
@ -454,20 +454,46 @@ defmodule Pleroma.Web.CommonAPI do
|
||||
end
|
||||
end
|
||||
|
||||
def add_mute(user, activity) do
|
||||
def add_mute(user, activity, params \\ %{}) do
|
||||
expires_in = Map.get(params, :expires_in, 0)
|
||||
|
||||
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
|
||||
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
|
||||
if expires_in > 0 do
|
||||
Pleroma.Workers.MuteExpireWorker.enqueue(
|
||||
"unmute_conversation",
|
||||
%{"user_id" => user.id, "activity_id" => activity.id},
|
||||
schedule_in: expires_in
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_mute(user, activity) do
|
||||
def remove_mute(%User{} = user, %Activity{} = activity) do
|
||||
ThreadMute.remove_mute(user.id, activity.data["context"])
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def remove_mute(user_id, activity_id) do
|
||||
with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
|
||||
{:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
|
||||
remove_mute(user, activity)
|
||||
else
|
||||
{what, result} = error ->
|
||||
Logger.warn(
|
||||
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
|
||||
activity_id
|
||||
}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
|
||||
when is_binary(context) do
|
||||
ThreadMute.exists?(user_id, context)
|
||||
|
@ -394,7 +394,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||
|
||||
@doc "POST /api/v1/accounts/:id/mute"
|
||||
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
|
||||
with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
|
||||
with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
|
||||
render(conn, "relationship.json", user: muter, target: muted)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
@ -284,9 +284,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||
end
|
||||
|
||||
@doc "POST /api/v1/statuses/:id/mute"
|
||||
def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||
def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{:ok, activity} <- CommonAPI.add_mute(user, activity) do
|
||||
{:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
end
|
||||
end
|
||||
|
20
lib/pleroma/workers/mute_expire_worker.ex
Normal file
20
lib/pleroma/workers/mute_expire_worker.ex
Normal file
@ -0,0 +1,20 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.MuteExpireWorker do
|
||||
use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
|
||||
Pleroma.User.unmute(muter_id, mutee_id)
|
||||
:ok
|
||||
end
|
||||
|
||||
def perform(%Job{
|
||||
args: %{"op" => "unmute_conversation", "user_id" => user_id, "activity_id" => activity_id}
|
||||
}) do
|
||||
Pleroma.Web.CommonAPI.remove_mute(user_id, activity_id)
|
||||
:ok
|
||||
end
|
||||
end
|
@ -229,7 +229,7 @@ defmodule Pleroma.NotificationTest do
|
||||
muter = insert(:user)
|
||||
muted = insert(:user)
|
||||
|
||||
{:ok, _user_relationships} = User.mute(muter, muted, false)
|
||||
{:ok, _user_relationships} = User.mute(muter, muted, %{notifications: false})
|
||||
|
||||
{:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
|
||||
|
||||
@ -1015,7 +1015,7 @@ defmodule Pleroma.NotificationTest do
|
||||
|
||||
test "it returns notifications for muted user without notifications", %{user: user} do
|
||||
muted = insert(:user)
|
||||
{:ok, _user_relationships} = User.mute(user, muted, false)
|
||||
{:ok, _user_relationships} = User.mute(user, muted, %{notifications: false})
|
||||
|
||||
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
|
||||
|
||||
|
@ -1008,6 +1008,27 @@ defmodule Pleroma.UserTest do
|
||||
assert User.muted_notifications?(user, muted_user)
|
||||
end
|
||||
|
||||
test "expiring" do
|
||||
user = insert(:user)
|
||||
muted_user = insert(:user)
|
||||
|
||||
{:ok, _user_relationships} = User.mute(user, muted_user, %{expires_in: 60})
|
||||
assert User.mutes?(user, muted_user)
|
||||
|
||||
worker = Pleroma.Workers.MuteExpireWorker
|
||||
args = %{"op" => "unmute_user", "muter_id" => user.id, "mutee_id" => muted_user.id}
|
||||
|
||||
assert_enqueued(
|
||||
worker: worker,
|
||||
args: args
|
||||
)
|
||||
|
||||
assert :ok = perform_job(worker, args)
|
||||
|
||||
refute User.mutes?(user, muted_user)
|
||||
refute User.muted_notifications?(user, muted_user)
|
||||
end
|
||||
|
||||
test "it unmutes users" do
|
||||
user = insert(:user)
|
||||
muted_user = insert(:user)
|
||||
@ -1019,6 +1040,17 @@ defmodule Pleroma.UserTest do
|
||||
refute User.muted_notifications?(user, muted_user)
|
||||
end
|
||||
|
||||
test "it unmutes users by id" do
|
||||
user = insert(:user)
|
||||
muted_user = insert(:user)
|
||||
|
||||
{:ok, _user_relationships} = User.mute(user, muted_user)
|
||||
{:ok, _user_mute} = User.unmute(user.id, muted_user.id)
|
||||
|
||||
refute User.mutes?(user, muted_user)
|
||||
refute User.muted_notifications?(user, muted_user)
|
||||
end
|
||||
|
||||
test "it mutes user without notifications" do
|
||||
user = insert(:user)
|
||||
muted_user = insert(:user)
|
||||
@ -1026,7 +1058,7 @@ defmodule Pleroma.UserTest do
|
||||
refute User.mutes?(user, muted_user)
|
||||
refute User.muted_notifications?(user, muted_user)
|
||||
|
||||
{:ok, _user_relationships} = User.mute(user, muted_user, false)
|
||||
{:ok, _user_relationships} = User.mute(user, muted_user, %{notifications: false})
|
||||
|
||||
assert User.mutes?(user, muted_user)
|
||||
refute User.muted_notifications?(user, muted_user)
|
||||
|
@ -3,8 +3,8 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.CommonAPITest do
|
||||
use Pleroma.DataCase
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
@ -922,12 +922,34 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||
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)
|
||||
|
@ -502,7 +502,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
|
||||
|
||||
assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
|
||||
|
||||
{:ok, _user_relationships} = User.mute(user, user2, false)
|
||||
{:ok, _user_relationships} = User.mute(user, user2, %{notifications: false})
|
||||
|
||||
conn = get(conn, "/api/v1/notifications")
|
||||
|
||||
|
@ -277,7 +277,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
|
||||
{:ok, user} = User.follow(user, other_user)
|
||||
{:ok, other_user} = User.follow(other_user, user)
|
||||
{:ok, _subscription} = User.subscribe(user, other_user)
|
||||
{:ok, _user_relationships} = User.mute(user, other_user, true)
|
||||
{:ok, _user_relationships} = User.mute(user, other_user, %{notifications: true})
|
||||
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
|
||||
|
||||
expected =
|
||||
|
Loading…
Reference in New Issue
Block a user