# Pleroma: A lightweight social networking server # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.FollowingRelationship do use Ecto.Schema 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, 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 __MODULE__ |> where(follower_id: ^follower.id, following_id: ^following.id) |> Repo.one() end def update(follower, following, :follow_reject), do: unfollow(follower, following) def update(%User{} = follower, %User{} = following, state) do case get(follower, following) do nil -> follow(follower, following, state) following_relationship -> following_relationship |> cast(%{state: state}, [:state]) |> validate_required([:state]) |> Repo.update() end end def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do %__MODULE__{} |> changeset(%{follower: follower, following: following, state: state}) |> Repo.insert(on_conflict: :nothing) end def unfollow(%User{} = follower, %User{} = following) do case get(follower, following) do %__MODULE__{} = following_relationship -> Repo.delete(following_relationship) _ -> {:ok, nil} end end def follower_count(%User{} = user) do %{followers: user, deactivated: false} |> User.Query.build() |> 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, from_ap_ids \\ nil) def followers_ap_ids(_, []), do: [] def followers_ap_ids(%User{} = user, from_ap_ids) 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 %{friends: user, deactivated: false} |> User.Query.build() |> Repo.aggregate(:count, :id) end def get_follow_requests(%User{id: id}) do __MODULE__ |> join(:inner, [r], f in assoc(r, :follower)) |> 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: ^: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 = following_query(user) |> select([r, u], u.follower_address) |> Repo.all() if not user.local or user.invisible do following else [user.follower_address | following] end end def move_following(origin, target) do __MODULE__ |> join(:inner, [r], f in assoc(r, :follower)) |> where(following_id: ^origin.id) |> where([r, f], f.allow_following_move == true) |> limit(50) |> preload([:follower]) |> Repo.all() |> Enum.map(fn following_relationship -> Repo.delete(following_relationship) Pleroma.Web.CommonAPI.follow(following_relationship.follower, target) end) |> case do [] -> User.update_follower_count(origin) :ok _ -> 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 @spec following_ap_ids(User.t()) :: [String.t()] def following_ap_ids(%User{} = user) do user |> following_query() |> select([r, u], u.ap_id) |> Repo.all() end end