Merge branch 'feature/reblog-muting' into 'develop'
Implement mastodon's reblog hiding feature See merge request pleroma/pleroma!916
This commit is contained in:
commit
c69dc2acf1
|
@ -1385,4 +1385,8 @@ defp paginate(query, page, page_size) do
|
||||||
offset: ^((page - 1) * page_size)
|
offset: ^((page - 1) * page_size)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def showing_reblogs?(%User{} = user, %User{} = target) do
|
||||||
|
target.ap_id not in user.info.muted_reblogs
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.User.Info do
|
||||||
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(:mutes, {:array, :string}, default: [])
|
||||||
|
field(:muted_reblogs, {: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)
|
||||||
|
@ -259,4 +260,16 @@ def roles(%Info{is_moderator: is_moderator, is_admin: is_admin}) do
|
||||||
moderator: is_moderator
|
moderator: is_moderator
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_reblog_mute(info, ap_id) do
|
||||||
|
params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
|
||||||
|
|
||||||
|
cast(info, params, [:muted_reblogs])
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_reblog_mute(info, ap_id) do
|
||||||
|
params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
|
||||||
|
|
||||||
|
cast(info, params, [:muted_reblogs])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -679,6 +679,18 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids})
|
||||||
|
|
||||||
defp restrict_pinned(query, _), do: query
|
defp restrict_pinned(query, _), do: query
|
||||||
|
|
||||||
|
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
|
||||||
|
muted_reblogs = info.muted_reblogs || []
|
||||||
|
|
||||||
|
from(
|
||||||
|
activity in query,
|
||||||
|
where: fragment("not ?->>'type' = 'Announce'", activity.data),
|
||||||
|
where: fragment("not ? = ANY(?)", activity.actor, ^muted_reblogs)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restrict_muted_reblogs(query, _), do: query
|
||||||
|
|
||||||
def fetch_activities_query(recipients, opts \\ %{}) do
|
def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
base_query =
|
base_query =
|
||||||
from(
|
from(
|
||||||
|
@ -706,6 +718,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
|> restrict_replies(opts)
|
|> restrict_replies(opts)
|
||||||
|> restrict_reblogs(opts)
|
|> restrict_reblogs(opts)
|
||||||
|> restrict_pinned(opts)
|
|> restrict_pinned(opts)
|
||||||
|
|> restrict_muted_reblogs(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_activities(recipients, opts \\ %{}) do
|
def fetch_activities(recipients, opts \\ %{}) do
|
||||||
|
|
|
@ -299,4 +299,24 @@ def report(user, data) do
|
||||||
{:account, nil} -> {:error, "Account not found"}
|
{:account, nil} -> {:error, "Account not found"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hide_reblogs(user, muted) do
|
||||||
|
ap_id = muted.ap_id
|
||||||
|
|
||||||
|
if ap_id not in user.info.muted_reblogs do
|
||||||
|
info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
|
||||||
|
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
|
||||||
|
User.update_and_set_cache(changeset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_reblogs(user, muted) do
|
||||||
|
ap_id = muted.ap_id
|
||||||
|
|
||||||
|
if ap_id in user.info.muted_reblogs do
|
||||||
|
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
|
||||||
|
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
|
||||||
|
User.update_and_set_cache(changeset)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -723,11 +723,25 @@ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) d
|
||||||
|
|
||||||
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
|
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
|
||||||
with %User{} = followed <- Repo.get(User, id),
|
with %User{} = followed <- Repo.get(User, id),
|
||||||
|
false <- User.following?(follower, followed),
|
||||||
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
|
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
|
||||||
conn
|
conn
|
||||||
|> put_view(AccountView)
|
|> put_view(AccountView)
|
||||||
|> render("relationship.json", %{user: follower, target: followed})
|
|> render("relationship.json", %{user: follower, target: followed})
|
||||||
else
|
else
|
||||||
|
true ->
|
||||||
|
followed = User.get_cached_by_id(id)
|
||||||
|
|
||||||
|
{:ok, follower} =
|
||||||
|
case conn.params["reblogs"] do
|
||||||
|
true -> CommonAPI.show_reblogs(follower, followed)
|
||||||
|
false -> CommonAPI.hide_reblogs(follower, followed)
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(AccountView)
|
||||||
|
|> render("relationship.json", %{user: follower, target: followed})
|
||||||
|
|
||||||
{:error, message} ->
|
{:error, message} ->
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/json")
|
|> put_resp_content_type("application/json")
|
||||||
|
|
|
@ -55,7 +55,7 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
|
||||||
muting_notifications: false,
|
muting_notifications: false,
|
||||||
requested: requested,
|
requested: requested,
|
||||||
domain_blocking: false,
|
domain_blocking: false,
|
||||||
showing_reblogs: false,
|
showing_reblogs: User.showing_reblogs?(user, target),
|
||||||
endorsed: false
|
endorsed: false
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.Streamer do
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||||
|
|
||||||
|
@ -199,10 +200,12 @@ def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = ite
|
||||||
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
|
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
|
||||||
blocks = user.info.blocks || []
|
blocks = user.info.blocks || []
|
||||||
mutes = user.info.mutes || []
|
mutes = user.info.mutes || []
|
||||||
|
reblog_mutes = user.info.muted_reblogs || []
|
||||||
|
|
||||||
parent = Object.normalize(item.data["object"])
|
parent = Object.normalize(item.data["object"])
|
||||||
|
|
||||||
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
|
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
|
||||||
|
item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or
|
||||||
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
|
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
|
||||||
send(socket.transport_pid, {:text, represent_update(item, user)})
|
send(socket.transport_pid, {:text, represent_update(item, user)})
|
||||||
end
|
end
|
||||||
|
@ -233,7 +236,8 @@ def push_to_socket(topics, topic, item) do
|
||||||
blocks = user.info.blocks || []
|
blocks = user.info.blocks || []
|
||||||
mutes = user.info.mutes || []
|
mutes = user.info.mutes || []
|
||||||
|
|
||||||
unless item.actor in blocks or item.actor in mutes do
|
unless item.actor in blocks or item.actor in mutes or
|
||||||
|
not ActivityPub.contain_activity(item, user) do
|
||||||
send(socket.transport_pid, {:text, represent_update(item, user)})
|
send(socket.transport_pid, {:text, represent_update(item, user)})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
|
@ -424,6 +424,19 @@ test "retrieves ids up to max_id" do
|
||||||
assert length(activities) == 20
|
assert length(activities) == 20
|
||||||
assert last == last_expected
|
assert last == last_expected
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "doesn't return reblogs for users for whom reblogs have been muted" do
|
||||||
|
activity = insert(:note_activity)
|
||||||
|
user = insert(:user)
|
||||||
|
booster = insert(:user)
|
||||||
|
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
|
||||||
|
|
||||||
|
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
|
||||||
|
|
||||||
|
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
|
||||||
|
|
||||||
|
refute Enum.member?(activities, activity)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "like an object" do
|
describe "like an object" do
|
||||||
|
|
|
@ -221,4 +221,27 @@ test "creates a report" do
|
||||||
} = flag_activity
|
} = flag_activity
|
||||||
end
|
end
|
||||||
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, muter} = CommonAPI.hide_reblogs(muter, muted)
|
||||||
|
|
||||||
|
assert Pleroma.User.showing_reblogs?(muter, muted) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove a reblog mute", %{muter: muter, muted: muted} do
|
||||||
|
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
|
||||||
|
{:ok, muter} = CommonAPI.show_reblogs(muter, muted)
|
||||||
|
|
||||||
|
assert Pleroma.User.showing_reblogs?(muter, muted) == true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -144,7 +144,7 @@ test "represent a relationship" do
|
||||||
muting_notifications: false,
|
muting_notifications: false,
|
||||||
requested: false,
|
requested: false,
|
||||||
domain_blocking: false,
|
domain_blocking: false,
|
||||||
showing_reblogs: false,
|
showing_reblogs: true,
|
||||||
endorsed: false
|
endorsed: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ test "represent an embedded relationship" do
|
||||||
muting_notifications: false,
|
muting_notifications: false,
|
||||||
requested: false,
|
requested: false,
|
||||||
domain_blocking: false,
|
domain_blocking: false,
|
||||||
showing_reblogs: false,
|
showing_reblogs: true,
|
||||||
endorsed: false
|
endorsed: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,4 +202,34 @@ test "it send wanted private posts to list" do
|
||||||
|
|
||||||
Task.await(task)
|
Task.await(task)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it doesn't send muted reblogs" do
|
||||||
|
user1 = insert(:user)
|
||||||
|
user2 = insert(:user)
|
||||||
|
user3 = insert(:user)
|
||||||
|
CommonAPI.hide_reblogs(user1, user2)
|
||||||
|
|
||||||
|
task =
|
||||||
|
Task.async(fn ->
|
||||||
|
refute_receive {:text, _}, 1_000
|
||||||
|
end)
|
||||||
|
|
||||||
|
fake_socket = %{
|
||||||
|
transport_pid: task.pid,
|
||||||
|
assigns: %{
|
||||||
|
user: user1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
|
||||||
|
{:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2)
|
||||||
|
|
||||||
|
topics = %{
|
||||||
|
"public" => [fake_socket]
|
||||||
|
}
|
||||||
|
|
||||||
|
Streamer.push_to_socket(topics, "public", announce_activity)
|
||||||
|
|
||||||
|
Task.await(task)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue