Merge branch '1335-user-api-id-fields-relations' into 'develop'

[#1335] User: refactoring of AP ID fields into relations

Closes #1335

See merge request pleroma/pleroma!1963
This commit is contained in:
lain 2019-12-05 17:16:23 +00:00
commit 67d8df04a4
34 changed files with 765 additions and 265 deletions

View File

@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- OStatus: Extract RSS functionality
- Deprecated `User.Info` embedded schema (fields moved to `User`)
- Store status data inside Flag activity
- Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`).
<details>
<summary>API Changes</summary>

13
lib/pleroma/ecto_enums.ex Normal file
View File

@ -0,0 +1,13 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
import EctoEnum
defenum(UserRelationshipTypeEnum,
block: 1,
mute: 2,
reblog_mute: 3,
notification_mute: 4,
inverse_subscription: 5
)

View File

@ -21,6 +21,8 @@ defmodule Pleroma.Notification do
@type t :: %__MODULE__{}
@include_muted_option :with_muted
schema "notifications" do
field(:seen, :boolean, default: false)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
@ -34,7 +36,25 @@ def changeset(%Notification{} = notification, attrs) do
|> cast(attrs, [:seen])
end
def for_user_query(user, opts \\ []) do
defp for_user_query_ap_id_opts(user, opts) do
ap_id_relations =
[:block] ++
if opts[@include_muted_option], do: [], else: [:notification_mute]
preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations)
exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
exclude_notification_muted_opts =
Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
{exclude_blocked_opts, exclude_notification_muted_opts}
end
def for_user_query(user, opts \\ %{}) do
{exclude_blocked_opts, exclude_notification_muted_opts} =
for_user_query_ap_id_opts(user, opts)
Notification
|> where(user_id: ^user.id)
|> where(
@ -54,27 +74,32 @@ def for_user_query(user, opts \\ []) do
)
)
|> preload([n, a, o], activity: {a, object: o})
|> exclude_muted(user, opts)
|> exclude_blocked(user)
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts)
|> exclude_visibility(opts)
end
defp exclude_blocked(query, user) do
defp exclude_blocked(query, user, opts) do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
|> where([n, a], a.actor not in ^user.blocks)
|> where([n, a], a.actor not in ^blocked_ap_ids)
|> where(
[n, a],
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
)
end
defp exclude_muted(query, _, %{with_muted: true}) do
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
query
end
defp exclude_muted(query, user, _opts) do
defp exclude_notification_muted(query, user, opts) do
notification_muted_ap_ids =
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
query
|> where([n, a], a.actor not in ^user.muted_notifications)
|> where([n, a], a.actor not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
)

View File

@ -7,6 +7,7 @@ defmodule Pleroma.User do
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
alias Comeonin.Pbkdf2
alias Ecto.Multi
@ -21,6 +22,7 @@ defmodule Pleroma.User do
alias Pleroma.Repo
alias Pleroma.RepoStreamer
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
@ -42,6 +44,32 @@ defmodule Pleroma.User do
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
# AP ID user relationships (blocks, mutes etc.)
# Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
@user_relationships_config [
block: [
blocker_blocks: :blocked_users,
blockee_blocks: :blocker_users
],
mute: [
muter_mutes: :muted_users,
mutee_mutes: :muter_users
],
reblog_mute: [
reblog_muter_mutes: :reblog_muted_users,
reblog_mutee_mutes: :reblog_muter_users
],
notification_mute: [
notification_muter_mutes: :notification_muted_users,
notification_mutee_mutes: :notification_muter_users
],
# Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
inverse_subscription: [
subscribee_subscriptions: :subscriber_users,
subscriber_subscriptions: :subscribee_users
]
]
schema "users" do
field(:bio, :string)
field(:email, :string)
@ -61,7 +89,6 @@ defmodule Pleroma.User do
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:source_data, :map, default: %{})
@ -73,12 +100,7 @@ defmodule Pleroma.User do
field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
field(:muted_notifications, {:array, :string}, default: [])
field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
@ -120,9 +142,82 @@ defmodule Pleroma.User do
has_many(:registrations, Registration)
has_many(:deliveries, Delivery)
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
{incoming_relation, incoming_relation_source}
]} <- @user_relationships_config do
# Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc.
has_many(outgoing_relation, UserRelationship,
foreign_key: :source_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc.
has_many(incoming_relation, UserRelationship,
foreign_key: :target_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many :blocked_users`, `has_many :muted_users` etc.
has_many(outgoing_relation_target, through: [outgoing_relation, :target])
# Definitions of `has_many :blocker_users`, `has_many :muter_users` etc.
has_many(incoming_relation_source, through: [incoming_relation, :source])
end
# `:blocks` is deprecated (replaced with `blocked_users` relation)
field(:blocks, {:array, :string}, default: [])
# `:mutes` is deprecated (replaced with `muted_users` relation)
field(:mutes, {:array, :string}, default: [])
# `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
field(:muted_reblogs, {:array, :string}, default: [])
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
field(:muted_notifications, {:array, :string}, default: [])
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
timestamps()
end
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do
# Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc.
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target))
if restrict_deactivated? do
restrict_deactivated(target_users_query)
else
target_users_query
end
end
# Definitions of `blocked_users/1`, `muted_users/1`, etc.
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> Repo.all()
end
# Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc.
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> select([u], u.ap_id)
|> Repo.all()
end
end
@doc "Returns if the user should be allowed to authenticate"
def auth_active?(%User{deactivated: true}), do: false
@ -946,34 +1041,45 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
|> Repo.all()
end
@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
add_to_mutes(muter, ap_id, notifications?)
@spec mute(User.t(), User.t(), boolean()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
add_to_mutes(muter, mutee, notifications?)
end
def unmute(muter, %{ap_id: ap_id}) do
remove_from_mutes(muter, ap_id)
def unmute(%User{} = muter, %User{} = mutee) do
remove_from_mutes(muter, mutee)
end
def subscribe(subscriber, %{ap_id: ap_id}) do
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
def subscribe(%User{} = subscriber, %User{} = target) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
if blocks?(subscribed, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else
User.add_to_subscribers(subscribed, subscriber.ap_id)
end
if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
else
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.create_inverse_subscription(target, subscriber)
end
end
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
subscribe(subscriber, subscribee)
end
end
def unsubscribe(%User{} = unsubscriber, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.delete_inverse_subscription(target, unsubscriber)
end
def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
User.remove_from_subscribers(user, unsubscriber.ap_id)
unsubscribe(unsubscriber, user)
end
end
def block(blocker, %User{ap_id: ap_id} = blocked) do
def block(%User{} = blocker, %User{} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
@ -990,50 +1096,53 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
nil -> blocked
end
blocker =
if subscribed_to?(blocked, blocker) do
{:ok, blocker} = unsubscribe(blocked, blocker)
blocker
else
blocker
end
unsubscribe(blocked, blocker)
if following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, ap_id)
add_to_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
def block(%User{} = blocker, %{ap_id: ap_id}) do
block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(blocker, %{ap_id: ap_id}) do
remove_from_block(blocker, ap_id)
def unblock(%User{} = blocker, %User{} = blocked) do
remove_from_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def unblock(%User{} = blocker, %{ap_id: ap_id}) do
unblock(blocker, get_cached_by_ap_id(ap_id))
end
def mutes?(nil, _), do: false
def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
def mutes_user?(%User{} = user, %User{} = target) do
UserRelationship.mute_exists?(user, target)
end
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false
def muted_notifications?(user, %{ap_id: ap_id}),
do: Enum.member?(user.muted_notifications, ap_id)
def blocks?(%User{} = user, %User{} = target) do
blocks_ap_id?(user, target) || blocks_domain?(user, target)
end
def muted_notifications?(%User{} = user, %User{} = target),
do: UserRelationship.notification_mute_exists?(user, target)
def blocks?(nil, _), do: false
def blocks_ap_id?(%User{} = user, %User{} = target) do
Enum.member?(user.blocks, target.ap_id)
def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) || blocks_domain?(user, target)
end
def blocks_ap_id?(_, _), do: false
def blocks_user?(%User{} = user, %User{} = target) do
UserRelationship.block_exists?(user, target)
end
def blocks_user?(_, _), do: false
def blocks_domain?(%User{} = user, %User{} = target) do
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
@ -1043,28 +1152,41 @@ def blocks_domain?(%User{} = user, %User{} = target) do
def blocks_domain?(_, _), do: false
def subscribed_to?(user, %{ap_id: ap_id}) do
def subscribed_to?(%User{} = user, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.inverse_subscription_exists?(target, user)
end
def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
Enum.member?(target.subscribers, user.ap_id)
subscribed_to?(user, target)
end
end
@spec muted_users(User.t()) :: [User.t()]
def muted_users(user) do
User.Query.build(%{ap_id: user.mutes, deactivated: false})
|> Repo.all()
end
@doc """
Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
"""
@spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
def outgoing_relations_ap_ids(_, []), do: %{}
@spec blocked_users(User.t()) :: [User.t()]
def blocked_users(user) do
User.Query.build(%{ap_id: user.blocks, deactivated: false})
|> Repo.all()
end
def outgoing_relations_ap_ids(%User{} = user, relationship_types)
when is_list(relationship_types) do
db_result =
user
|> assoc(:outgoing_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :target))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
|> group_by([user_rel, u], user_rel.relationship_type)
|> Repo.all()
|> Enum.into(%{}, fn [k, v] -> {k, v} end)
@spec subscribers(User.t()) :: [User.t()]
def subscribers(user) do
User.Query.build(%{ap_id: user.subscribers, deactivated: false})
|> Repo.all()
Enum.into(
relationship_types,
%{},
fn rel_type -> {rel_type, db_result[rel_type] || []} end
)
end
def deactivate_async(user, status \\ true) do
@ -1171,7 +1293,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _user_block} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
@ -1485,7 +1607,7 @@ def all_superusers do
end
def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.muted_reblogs
not UserRelationship.reblog_mute_exists?(user, target)
end
@doc """
@ -1808,23 +1930,6 @@ def update_email_notifications(user, settings) do
|> update_and_set_cache()
end
defp set_subscribers(user, subscribers) do
params = %{subscribers: subscribers}
user
|> cast(params, [:subscribers])
|> validate_required([:subscribers])
|> update_and_set_cache()
end
def add_to_subscribers(user, subscribed) do
set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
end
def remove_from_subscribers(user, subscribed) do
set_subscribers(user, List.delete(user.subscribers, subscribed))
end
defp set_domain_blocks(user, domain_blocks) do
params = %{domain_blocks: domain_blocks}
@ -1842,81 +1947,35 @@ def unblock_domain(user, domain_blocked) do
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
end
defp set_blocks(user, blocks) do
params = %{blocks: blocks}
user
|> cast(params, [:blocks])
|> validate_required([:blocks])
|> update_and_set_cache()
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
UserRelationship.create_block(user, blocked)
end
def add_to_block(user, blocked) do
set_blocks(user, Enum.uniq([blocked | user.blocks]))
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
UserRelationship.delete_block(user, blocked)
end
def remove_from_block(user, blocked) do
set_blocks(user, List.delete(user.blocks, blocked))
end
defp set_mutes(user, mutes) do
params = %{mutes: mutes}
user
|> cast(params, [:mutes])
|> validate_required([:mutes])
|> update_and_set_cache()
end
def add_to_mutes(user, muted, notifications?) do
with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
set_notification_mutes(
user,
Enum.uniq([muted | user.muted_notifications]),
notifications?
)
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
def remove_from_mutes(user, muted) do
with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
set_notification_mutes(
user,
List.delete(user.muted_notifications, muted),
true
)
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
defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
{:ok, user}
end
defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
params = %{muted_notifications: muted_notifications}
user
|> cast(params, [:muted_notifications])
|> validate_required([:muted_notifications])
|> update_and_set_cache()
end
def add_reblog_mute(user, ap_id) do
params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
user
|> cast(params, [:muted_reblogs])
|> update_and_set_cache()
end
def remove_reblog_mute(user, ap_id) do
params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
user
|> cast(params, [:muted_reblogs])
|> update_and_set_cache()
end
def set_invisible(user, invisible) do
params = %{invisible: invisible}

View File

@ -103,9 +103,13 @@ defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false)
end
defp filter_blocked_user(query, %User{blocks: blocks})
when length(blocks) > 0 do
from(q in query, where: not (q.ap_id in ^blocks))
defp filter_blocked_user(query, %User{} = blocker) do
query
|> join(:left, [u], b in Pleroma.UserRelationship,
as: :blocks,
on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id
)
|> where([blocks: b], is_nil(b.target_id))
end
defp filter_blocked_user(query, _), do: query

View File

@ -0,0 +1,92 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserRelationship do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:relationship_type, UserRelationshipTypeEnum)
timestamps(updated_at: false)
end
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do
# Definitions of `create_block/2`, `create_mute/2` etc.
def unquote(:"create_#{relationship_type}")(source, target),
do: create(unquote(relationship_type), source, target)
# Definitions of `delete_block/2`, `delete_mute/2` etc.
def unquote(:"delete_#{relationship_type}")(source, target),
do: delete(unquote(relationship_type), source, target)
# Definitions of `block_exists?/2`, `mute_exists?/2` etc.
def unquote(:"#{relationship_type}_exists?")(source, target),
do: exists?(unquote(relationship_type), source, target)
end
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship
|> cast(params, [:relationship_type, :source_id, :target_id])
|> validate_required([:relationship_type, :source_id, :target_id])
|> unique_constraint(:relationship_type,
name: :user_relationships_source_id_relationship_type_target_id_index
)
|> validate_not_self_relationship()
end
def exists?(relationship_type, %User{} = source, %User{} = target) do
UserRelationship
|> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id)
|> Repo.exists?()
end
def create(relationship_type, %User{} = source, %User{} = target) do
%UserRelationship{}
|> changeset(%{
relationship_type: relationship_type,
source_id: source.id,
target_id: target.id
})
|> Repo.insert(
on_conflict: :replace_all_except_primary_key,
conflict_target: [:source_id, :relationship_type, :target_id]
)
end
def delete(relationship_type, %User{} = source, %User{} = target) do
attrs = %{relationship_type: relationship_type, source_id: source.id, target_id: target.id}
case Repo.get_by(UserRelationship, attrs) do
%UserRelationship{} = existing_record -> Repo.delete(existing_record)
nil -> {:ok, nil}
end
end
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
changeset
|> validate_change(:target_id, fn _, target_id ->
if target_id == get_field(changeset, :source_id) do
[target_id: "can't be equal to source_id"]
else
[]
end
end)
|> validate_change(:source_id, fn _, source_id ->
if source_id == get_field(changeset, :target_id) do
[source_id: "can't be equal to target_id"]
else
[]
end
end)
end
end

View File

@ -919,7 +919,7 @@ defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
mutes = user.mutes
mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)
query =
from([activity] in query,
@ -936,8 +936,8 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do
blocks = user.blocks || []
defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
domain_blocks = user.domain_blocks || []
query =
@ -945,14 +945,14 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do
from(
[activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocks
^blocked_ap_ids
),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
@ -979,8 +979,8 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids})
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do
muted_reblogs = user.muted_reblogs || []
defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)
from(
activity in query,
@ -1061,7 +1061,33 @@ defp maybe_order(query, %{order: :asc}) do
defp maybe_order(query, _), do: query
defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts["muting_user"]
ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: []
ap_id_relations =
ap_id_relations ++
if opts["blocking_user"] && opts["blocking_user"] == source_user do
[:block]
else
[]
end
preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations)
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
restrict_muted_reblogs_opts =
Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts)
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
end
def fetch_activities_query(recipients, opts \\ %{}) do
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} =
fetch_activities_query_ap_ids_ops(opts)
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
@ -1081,15 +1107,15 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_muted(restrict_muted_opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@ -32,7 +33,7 @@ def follow(follower, followed) do
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
{:ok, follower}
end
end
@ -420,15 +421,11 @@ defp set_visibility(activity, %{"visibility" => visibility}) do
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
if ap_id not in user.muted_reblogs do
User.add_reblog_mute(user, ap_id)
end
def hide_reblogs(%User{} = user, %User{} = target) do
UserRelationship.create_reblog_mute(user, target)
end
def show_reblogs(user, %{ap_id: ap_id} = _muted) do
if ap_id in user.muted_reblogs do
User.remove_reblog_mute(user, ap_id)
end
def show_reblogs(%User{} = user, %User{} = target) do
UserRelationship.delete_reblog_mute(user, target)
end
end

View File

@ -494,7 +494,7 @@ def maybe_notify_subscribers(
with %User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids =
user
|> User.subscribers()
|> User.subscriber_users()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id)

View File

@ -324,7 +324,7 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
notifications? = params |> Map.get("notifications", true) |> truthy_param?()
with {:ok, muter} <- User.mute(muter, muted, notifications?) do
with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -333,7 +333,7 @@ def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
@doc "POST /api/v1/accounts/:id/unmute"
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
with {:ok, muter} <- User.unmute(muter, muted) do
with {:ok, _user_relationships} <- User.unmute(muter, muted) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -342,7 +342,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, blocker} <- User.block(blocker, blocked),
with {:ok, _user_block} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
@ -352,7 +352,7 @@ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, blocker} <- User.unblock(blocker, blocked),
with {:ok, _user_block} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
@ -374,12 +374,14 @@ def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
users = User.muted_users(user, _restrict_deactivated = true)
render(conn, "index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
users = User.blocked_users(user, _restrict_deactivated = true)
render(conn, "index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/endorsements"

View File

@ -24,19 +24,16 @@ def follow(follower, followed, params \\ %{}) do
with {:ok, follower, _followed, _} <- result do
options = cast_params(params)
case reblogs_visibility(options[:reblogs], result) do
{:ok, follower} -> {:ok, follower}
_ -> {:ok, follower}
end
set_reblogs_visibility(options[:reblogs], result)
{:ok, follower}
end
end
defp reblogs_visibility(false, {:ok, follower, followed, _}) do
defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do
CommonAPI.hide_reblogs(follower, followed)
end
defp reblogs_visibility(_, {:ok, follower, followed, _}) do
defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do
CommonAPI.show_reblogs(follower, followed)
end

View File

@ -50,8 +50,8 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
id: to_string(target.id),
following: User.following?(user, target),
followed_by: User.following?(target, user),
blocking: User.blocks_ap_id?(user, target),
blocked_by: User.blocks_ap_id?(target, user),
blocking: User.blocks_user?(user, target),
blocked_by: User.blocks_user?(target, user),
muting: User.mutes?(user, target),
muting_notifications: User.muted_notifications?(user, target),
subscribing: User.subscribed_to?(user, target),

View File

@ -144,7 +144,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
with {:ok, _subscription} <- User.subscribe(user, subscription_target) do
render(conn, "relationship.json", user: user, target: subscription_target)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -153,7 +153,7 @@ def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _p
@doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do
render(conn, "relationship.json", user: user, target: subscription_target)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})

View File

@ -129,16 +129,17 @@ defp do_stream(%{topic: topic, item: item}) do
end
defp should_send?(%User{} = user, %Activity{} = item) do
blocks = user.blocks || []
mutes = user.mutes || []
reblog_mutes = user.muted_reblogs || []
recipient_blocks = MapSet.new(blocks ++ mutes)
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute])
recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
recipients = MapSet.new(item.recipients)
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
with parent <- Object.normalize(item) || 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 <-
Enum.all?([blocked_ap_ids, muted_ap_ids, reblog_muted_ap_ids], &(item.actor not in &1)),
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
true <- MapSet.disjoint?(recipients, recipient_blocks),
%{host: item_host} <- URI.parse(item.actor),
%{host: parent_host} <- URI.parse(parent.data["actor"]),

View File

@ -100,6 +100,7 @@ defp deps do
{:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"},
{:ecto_enum, "~> 1.4"},
{:ecto_sql, "~> 3.2"},
{:postgrex, ">= 0.13.5"},
{:oban, "~> 0.12.0"},

View File

@ -24,6 +24,7 @@
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},

View File

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.CreateUserRelationships do
use Ecto.Migration
def change do
create_if_not_exists table(:user_relationships) do
add(:source_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:target_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:relationship_type, :integer, null: false)
timestamps(updated_at: false)
end
create_if_not_exists(
unique_index(:user_relationships, [:source_id, :relationship_type, :target_id])
)
end
end

View File

@ -0,0 +1,68 @@
defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do
use Ecto.Migration
alias Ecto.Adapters.SQL
alias Pleroma.Repo
require Logger
def up do
Enum.each(
[blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4, subscribers: 5],
fn {field, relationship_type_code} ->
migrate(field, relationship_type_code)
if field == :subscribers do
drop_if_exists(index(:users, [:subscribers]))
end
end
)
end
def down, do: :noop
defp migrate(field, relationship_type_code) do
Logger.info("Processing users.#{field}...")
{:ok, %{rows: field_rows}} =
SQL.query(Repo, "SELECT id, #{field} FROM users WHERE #{field} != '{}'")
target_ap_ids =
Enum.flat_map(
field_rows,
fn [_, ap_ids] -> ap_ids end
)
|> Enum.uniq()
# Selecting ids of all targets at once in order to reduce the number of SELECT queries
{:ok, %{rows: target_ap_id_id}} =
SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [target_ap_ids])
target_id_by_ap_id = Enum.into(target_ap_id_id, %{}, fn [k, v] -> {k, v} end)
Enum.each(
field_rows,
fn [source_id, target_ap_ids] ->
source_uuid = Ecto.UUID.cast!(source_id)
for target_ap_id <- target_ap_ids do
target_id = target_id_by_ap_id[target_ap_id]
with {:ok, target_uuid} <- target_id && Ecto.UUID.cast(target_id) do
execute("""
INSERT INTO user_relationships(
source_id, target_id, relationship_type, inserted_at
)
VALUES(
'#{source_uuid}'::uuid, '#{target_uuid}'::uuid, #{relationship_type_code}, now()
)
ON CONFLICT (source_id, relationship_type, target_id) DO NOTHING
""")
else
_ -> Logger.warn("Unresolved #{field} reference: (#{source_uuid}, #{target_id})")
end
end
end
)
end
end

View File

@ -252,7 +252,7 @@ test "when the user blocks a recipient, the existing conversations with them are
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
# The conversations with the blocked user are marked as read
assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
@ -274,7 +274,7 @@ test "the new conversation with the blocked user is not marked as unread " do
blocked = insert(:user)
third_user = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
# When the blocked user is the author
{:ok, _direct1} =
@ -311,7 +311,7 @@ test "the conversation with the blocked user is not marked as unread on a reply"
"visibility" => "direct"
})
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0

View File

@ -93,7 +93,7 @@ test "it creates a notification for user if the user blocks the activity author"
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
{:ok, user} = User.block(user, author)
{:ok, _user_relationship} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
@ -112,7 +112,7 @@ test "notification created if user is muted without notifications" do
muter = insert(:user)
muted = insert(:user)
{:ok, muter} = User.mute(muter, muted, false)
{:ok, _user_relationships} = User.mute(muter, muted, false)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
@ -665,7 +665,7 @@ test "move activity generates a notification" do
test "it returns notifications for muted user without notifications" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted, false)
{:ok, _user_relationships} = User.mute(user, muted, false)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
@ -675,7 +675,7 @@ test "it returns notifications for muted user without notifications" do
test "it doesn't return notifications for muted user with notifications" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
@ -685,7 +685,7 @@ test "it doesn't return notifications for muted user with notifications" do
test "it doesn't return notifications for blocked user" do
user = insert(:user)
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
@ -715,7 +715,7 @@ test "it doesn't return notifications for muted thread" do
test "it returns notifications from a muted user when with_muted is set" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
@ -725,7 +725,7 @@ test "it returns notifications from a muted user when with_muted is set" do
test "it doesn't return notifications from a blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})

View File

@ -42,6 +42,18 @@ def user_factory do
}
end
def user_relationship_factory(attrs \\ %{}) do
source = attrs[:source] || insert(:user)
target = attrs[:target] || insert(:user)
relationship_type = attrs[:relationship_type] || :block
%Pleroma.UserRelationship{
source_id: source.id,
target_id: target.id,
relationship_type: relationship_type
}
end
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")

View File

@ -0,0 +1,130 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserRelationshipTest do
alias Pleroma.UserRelationship
use Pleroma.DataCase
import Pleroma.Factory
describe "*_exists?/2" do
setup do
{:ok, users: insert_list(2, :user)}
end
test "returns false if record doesn't exist", %{users: [user1, user2]} do
refute UserRelationship.block_exists?(user1, user2)
refute UserRelationship.mute_exists?(user1, user2)
refute UserRelationship.notification_mute_exists?(user1, user2)
refute UserRelationship.reblog_mute_exists?(user1, user2)
refute UserRelationship.inverse_subscription_exists?(user1, user2)
end
test "returns true if record exists", %{users: [user1, user2]} do
for relationship_type <- [
:block,
:mute,
:notification_mute,
:reblog_mute,
:inverse_subscription
] do
insert(:user_relationship,
source: user1,
target: user2,
relationship_type: relationship_type
)
end
assert UserRelationship.block_exists?(user1, user2)
assert UserRelationship.mute_exists?(user1, user2)
assert UserRelationship.notification_mute_exists?(user1, user2)
assert UserRelationship.reblog_mute_exists?(user1, user2)
assert UserRelationship.inverse_subscription_exists?(user1, user2)
end
end
describe "create_*/2" do
setup do
{:ok, users: insert_list(2, :user)}
end
test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do
for relationship_type <- [
:block,
:mute,
:notification_mute,
:reblog_mute,
:inverse_subscription
] do
insert(:user_relationship,
source: user1,
target: user2,
relationship_type: relationship_type
)
end
UserRelationship.create_block(user1, user2)
UserRelationship.create_mute(user1, user2)
UserRelationship.create_notification_mute(user1, user2)
UserRelationship.create_reblog_mute(user1, user2)
UserRelationship.create_inverse_subscription(user1, user2)
assert UserRelationship.block_exists?(user1, user2)
assert UserRelationship.mute_exists?(user1, user2)
assert UserRelationship.notification_mute_exists?(user1, user2)
assert UserRelationship.reblog_mute_exists?(user1, user2)
assert UserRelationship.inverse_subscription_exists?(user1, user2)
end
test "if record already exists, returns it", %{users: [user1, user2]} do
user_block = UserRelationship.create_block(user1, user2)
assert user_block == UserRelationship.create_block(user1, user2)
end
end
describe "delete_*/2" do
setup do
{:ok, users: insert_list(2, :user)}
end
test "deletes user relationship record if it exists", %{users: [user1, user2]} do
for relationship_type <- [
:block,
:mute,
:notification_mute,
:reblog_mute,
:inverse_subscription
] do
insert(:user_relationship,
source: user1,
target: user2,
relationship_type: relationship_type
)
end
assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2)
assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2)
assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2)
assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2)
assert {:ok, %UserRelationship{}} =
UserRelationship.delete_inverse_subscription(user1, user2)
refute UserRelationship.block_exists?(user1, user2)
refute UserRelationship.mute_exists?(user1, user2)
refute UserRelationship.notification_mute_exists?(user1, user2)
refute UserRelationship.reblog_mute_exists?(user1, user2)
refute UserRelationship.inverse_subscription_exists?(user1, user2)
end
test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do
assert {:ok, nil} = UserRelationship.delete_block(user1, user2)
assert {:ok, nil} = UserRelationship.delete_mute(user1, user2)
assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2)
assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2)
assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2)
end
end
end

View File

@ -44,6 +44,56 @@ test "returns invisible actor" do
end
end
describe "AP ID user relationships" do
setup do
{:ok, user: insert(:user)}
end
test "outgoing_relations_ap_ids/1", %{user: user} do
rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription]
ap_ids_by_rel =
Enum.into(
rel_types,
%{},
fn rel_type ->
rel_records =
insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type})
ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end)
{rel_type, Enum.sort(ap_ids)}
end
)
assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user))
assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id))
assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user))
assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:notification_mute] ==
Enum.sort(User.notification_muted_users_ap_ids(user))
assert ap_ids_by_rel[:notification_mute] ==
Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user))
assert ap_ids_by_rel[:reblog_mute] ==
Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user))
assert ap_ids_by_rel[:inverse_subscription] ==
Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id))
outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types)
assert ap_ids_by_rel ==
Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end)
end
end
describe "when tags are nil" do
test "tagging a user" do
user = insert(:user, %{tags: nil})
@ -119,7 +169,7 @@ test "clears follow requests when requester is blocked" do
CommonAPI.follow(follower, followed)
assert [_activity] = User.get_follow_requests(followed)
{:ok, _follower} = User.block(followed, follower)
{:ok, _user_relationship} = User.block(followed, follower)
assert [] = User.get_follow_requests(followed)
end
@ -132,8 +182,8 @@ test "follow_all follows mutliple users" do
not_followed = insert(:user)
reverse_blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, reverse_blocked} = User.block(reverse_blocked, user)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _user_relationship} = User.block(reverse_blocked, user)
{:ok, user} = User.follow(user, followed_zero)
@ -186,7 +236,7 @@ test "can't follow a user who blocked us" do
blocker = insert(:user)
blockee = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
@ -195,7 +245,7 @@ test "can't subscribe to a user who blocked us" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
{:error, _} = User.subscribe(blocked, blocker)
end
@ -678,7 +728,7 @@ test "it mutes people" do
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, user} = User.mute(user, muted_user)
{:ok, _user_relationships} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
assert User.muted_notifications?(user, muted_user)
@ -688,8 +738,8 @@ 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)
{:ok, _user_relationships} = User.mute(user, muted_user)
{:ok, _user_mute} = User.unmute(user, muted_user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
@ -702,7 +752,7 @@ test "it mutes user without notifications" do
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, user} = User.mute(user, muted_user, false)
{:ok, _user_relationships} = User.mute(user, muted_user, false)
assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
@ -716,7 +766,7 @@ test "it blocks people" do
refute User.blocks?(user, blocked_user)
{:ok, user} = User.block(user, blocked_user)
{:ok, _user_relationship} = User.block(user, blocked_user)
assert User.blocks?(user, blocked_user)
end
@ -725,8 +775,8 @@ test "it unblocks users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, user} = User.block(user, blocked_user)
{:ok, user} = User.unblock(user, blocked_user)
{:ok, _user_relationship} = User.block(user, blocked_user)
{:ok, _user_block} = User.unblock(user, blocked_user)
refute User.blocks?(user, blocked_user)
end
@ -741,7 +791,7 @@ test "blocks tear down cyclical follow relationships" do
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@ -759,7 +809,7 @@ test "blocks tear down blocker->blocked follow relationships" do
assert User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@ -777,7 +827,7 @@ test "blocks tear down blocked->blocker follow relationships" do
refute User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
@ -790,12 +840,12 @@ test "blocks tear down blocked->blocker subscription relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.subscribe(blocked, blocker)
{:ok, _subscription} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
refute User.subscribed_to?(blocker, blocked)
@ -1324,7 +1374,8 @@ test "follower count is updated when a follower is blocked" do
{:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = User.follow(follower3, user)
{:ok, user} = User.block(user, follower)
{:ok, _user_relationship} = User.block(user, follower)
user = refresh_record(user)
assert user.follower_count == 2
end

View File

@ -487,7 +487,7 @@ test "retrieves activities that have a given context" do
activity_five = insert(:note_activity)
user = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
@ -500,7 +500,7 @@ test "doesn't return blocked activities" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
@ -509,7 +509,7 @@ test "doesn't return blocked activities" do
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
{:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
@ -518,7 +518,7 @@ test "doesn't return blocked activities" do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _user_relationship} = User.block(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 = Activity.get_by_id(activity_three.id)
@ -545,7 +545,7 @@ test "doesn't return transitive interactions concerning blocked users" do
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
@ -568,7 +568,7 @@ test "doesn't return announce activities concerning blocked users" do
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
@ -614,7 +614,9 @@ test "doesn't return muted activities" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
activity_one_actor = User.get_by_ap_id(activity_one.data["actor"])
{:ok, _user_relationships} = User.mute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
@ -635,7 +637,7 @@ test "doesn't return muted activities" do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
{:ok, _user_mute} = User.unmute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
@ -644,7 +646,8 @@ test "doesn't return muted activities" do
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
activity_three_actor = User.get_by_ap_id(activity_three.data["actor"])
{:ok, _user_relationships} = User.mute(user, activity_three_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 = Activity.get_by_id(activity_three.id)
@ -791,7 +794,7 @@ 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, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
@ -804,8 +807,8 @@ test "returns reblogs for users for whom reblogs have not been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, user} = CommonAPI.show_reblogs(user, booster)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)

View File

@ -128,7 +128,7 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl
user = insert(:user)
{:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin")
{:ok, user} = User.block(user, target)
{:ok, _user_relationship} = User.block(user, target)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")

View File

@ -509,14 +509,14 @@ test "updates state of multiple reports" do
end
test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
{: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, muter} = CommonAPI.hide_reblogs(muter, muted)
{:ok, muter} = CommonAPI.show_reblogs(muter, muted)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == true
end
@ -526,7 +526,7 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
{:ok, followed} = User.subscribe(follower, followed)
{:ok, _subscription} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)

View File

@ -891,7 +891,7 @@ test "getting a list of mutes", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
{:ok, _user_relationships} = User.mute(user, other_user)
conn =
conn
@ -906,7 +906,7 @@ test "getting a list of blocks", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user)
conn =
conn

View File

@ -289,7 +289,7 @@ test "doesn't see notifications after muting user with notifications", %{conn: c
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2)
{:ok, _user_relationships} = User.mute(user, user2)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
@ -310,7 +310,7 @@ test "see notifications after muting user without notifications", %{conn: conn}
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2, false)
{:ok, _user_relationships} = User.mute(user, user2, false)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications")
@ -333,7 +333,7 @@ test "see notifications after muting user with notifications and with_muted para
assert length(json_response(conn, 200)) == 1
{:ok, user} = User.mute(user, user2)
{:ok, _user_relationships} = User.mute(user, user2)
conn = assign(build_conn(), :user, user)
conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"})

View File

@ -1108,7 +1108,7 @@ test "does not return users who have favorited the status but are blocked", %{
activity: activity
} do
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
@ -1205,7 +1205,7 @@ test "does not return users who have reblogged the status but are blocked", %{
activity: activity
} do
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)

View File

@ -194,7 +194,7 @@ test "doesn't include DMs from blocked users", %{conn: conn} do
blocker = insert(:user)
blocked = insert(:user)
user = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
{:ok, _blocked_direct} =
CommonAPI.post(blocked, %{

View File

@ -190,9 +190,9 @@ test "represent a relationship for the following and followed user" do
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, user)
{:ok, other_user} = User.subscribe(user, other_user)
{:ok, user} = User.mute(user, other_user, true)
{:ok, user} = CommonAPI.hide_reblogs(user, other_user)
{:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationships} = User.mute(user, other_user, true)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
expected = %{
id: to_string(other_user.id),
@ -218,9 +218,9 @@ test "represent a relationship for the blocking and blocked user" do
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.subscribe(user, other_user)
{:ok, user} = User.block(user, other_user)
{:ok, other_user} = User.block(other_user, user)
{:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _user_relationship} = User.block(other_user, user)
expected = %{
id: to_string(other_user.id),
@ -291,7 +291,7 @@ test "represent an embedded relationship" do
other_user = insert(:user)
{:ok, other_user} = User.follow(other_user, user)
{:ok, other_user} = User.block(other_user, user)
{:ok, _user_relationship} = User.block(other_user, user)
{:ok, _} = User.follow(insert(:user), user)
expected = %{

View File

@ -183,7 +183,7 @@ test "tells if the message is muted for some reason" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
{:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity})
@ -199,7 +199,7 @@ test "tells if the message is thread muted" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
{:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity, for: user})

View File

@ -59,7 +59,7 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl
user: user
} do
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _user_relationship} = User.block(user, blocked)
task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
@ -259,7 +259,7 @@ test "it sends message if recipients invalid and thread containment is enabled b
test "it doesn't send messages involving blocked users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, user} = User.block(user, blocked_user)
{:ok, _user_relationship} = User.block(user, blocked_user)
task =
Task.async(fn ->
@ -301,7 +301,7 @@ test "it doesn't send messages transitively involving blocked users" do
"public" => [fake_socket]
}
{:ok, blocker} = User.block(blocker, blockee)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})

View File

@ -387,7 +387,7 @@ test "returns error when user is blocked", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _user} = Pleroma.User.block(user2, user)
{:ok, _user_block} = Pleroma.User.block(user2, user)
response =
conn
@ -485,7 +485,7 @@ test "returns error when user is blocked", %{conn: conn} do
Pleroma.Config.put([:user, :deny_follow_blocked], true)
user = insert(:user)
user2 = insert(:user)
{:ok, _user} = Pleroma.User.block(user2, user)
{:ok, _user_block} = Pleroma.User.block(user2, user)
response =
conn