import Ecto.Changeset
import Ecto.Query
+ alias Ecto.Changeset
alias FlakeId.Ecto.CompatType
+ alias Pleroma.FollowingRelationship.State
alias Pleroma.Repo
alias Pleroma.User
schema "following_relationships" do
- field(:state, :string, default: "accept")
+ field(:state, State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType)
timestamps()
end
+ @doc "Returns underlying integer code for state atom"
+ def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
+
+ def accept_state_code, do: state_int_code(:follow_accept)
+
def changeset(%__MODULE__{} = following_relationship, attrs) do
following_relationship
|> cast(attrs, [:state])
|> put_assoc(:follower, attrs.follower)
|> put_assoc(:following, attrs.following)
|> validate_required([:state, :follower, :following])
+ |> unique_constraint(:follower_id,
+ name: :following_relationships_follower_id_following_id_index
+ )
+ |> validate_not_self_relationship()
+ end
+
+ def state_to_enum(state) when state in ["pending", "accept", "reject"] do
+ String.to_existing_atom("follow_#{state}")
+ end
+
+ def state_to_enum(state) do
+ raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
end
def get(%User{} = follower, %User{} = following) do
|> Repo.one()
end
- def update(follower, following, "reject"), do: unfollow(follower, following)
+ def update(follower, following, :follow_reject), do: unfollow(follower, following)
def update(%User{} = follower, %User{} = following, state) do
case get(follower, following) do
end
end
- def follow(%User{} = follower, %User{} = following, state \\ "accept") do
+ def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
%__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing)
|> Repo.aggregate(:count, :id)
end
+ def followers_query(%User{} = user) do
+ __MODULE__
+ |> join(:inner, [r], u in User, on: r.follower_id == u.id)
+ |> where([r], r.following_id == ^user.id)
+ |> where([r], r.state == ^:follow_accept)
+ end
+
+ def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
+ query =
+ user
+ |> followers_query()
+ |> select([r, u], u.ap_id)
+
+ query =
+ if from_ap_ids do
+ where(query, [r, u], u.ap_id in ^from_ap_ids)
+ else
+ query
+ end
+
+ Repo.all(query)
+ end
+
def following_count(%User{id: nil}), do: 0
def following_count(%User{} = user) do
def get_follow_requests(%User{id: id}) do
__MODULE__
|> join(:inner, [r], f in assoc(r, :follower))
- |> where([r], r.state == "pending")
+ |> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id)
+ |> where([r, f], f.deactivated != true)
|> select([r, f], f)
|> Repo.all()
end
def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__
- |> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept")
+ |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
|> Repo.exists?()
end
+ def following_query(%User{} = user) do
+ __MODULE__
+ |> join(:inner, [r], u in User, on: r.following_id == u.id)
+ |> where([r], r.follower_id == ^user.id)
+ |> where([r], r.state == ^:follow_accept)
+ end
+
+ def outgoing_pending_follow_requests_query(%User{} = follower) do
+ __MODULE__
+ |> where([r], r.follower_id == ^follower.id)
+ |> where([r], r.state == ^:follow_pending)
+ end
+
def following(%User{} = user) do
following =
- __MODULE__
- |> join(:inner, [r], u in User, on: r.following_id == u.id)
- |> where([r], r.follower_id == ^user.id)
- |> where([r], r.state == "accept")
+ following_query(user)
|> select([r, u], u.follower_address)
|> Repo.all()
move_following(origin, target)
end
end
+
+ def all_between_user_sets(
+ source_users,
+ target_users
+ )
+ when is_list(source_users) and is_list(target_users) do
+ source_user_ids = User.binary_id(source_users)
+ target_user_ids = User.binary_id(target_users)
+
+ __MODULE__
+ |> where(
+ fragment(
+ "(follower_id = ANY(?) AND following_id = ANY(?)) OR \
+ (follower_id = ANY(?) AND following_id = ANY(?))",
+ ^source_user_ids,
+ ^target_user_ids,
+ ^target_user_ids,
+ ^source_user_ids
+ )
+ )
+ |> Repo.all()
+ end
+
+ def find(following_relationships, follower, following) do
+ Enum.find(following_relationships, fn
+ fr -> fr.follower_id == follower.id and fr.following_id == following.id
+ end)
+ end
+
+ @doc """
+ For a query with joined activity,
+ keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+ """
+ def keep_following_or_not_domain_blocked(query, user) do
+ where(
+ query,
+ [_, activity],
+ fragment(
+ # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
+ """
+ NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
+ ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
+ ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
+ """,
+ activity.actor,
+ ^user.domain_blocks,
+ activity.actor,
+ ^User.binary_id(user.id),
+ ^accept_state_code()
+ )
+ )
+ end
+
+ defp validate_not_self_relationship(%Changeset{} = changeset) do
+ changeset
+ |> validate_follower_id_following_id_inequality()
+ |> validate_following_id_follower_id_inequality()
+ end
+
+ defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
+ validate_change(changeset, :follower_id, fn _, follower_id ->
+ if follower_id == get_field(changeset, :following_id) do
+ [source_id: "can't be equal to following_id"]
+ else
+ []
+ end
+ end)
+ end
+
+ defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
+ validate_change(changeset, :following_id, fn _, following_id ->
+ if following_id == get_field(changeset, :follower_id) do
+ [target_id: "can't be equal to follower_id"]
+ else
+ []
+ end
+ end)
+ end
end