1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.FollowingRelationship do
12 alias FlakeId.Ecto.CompatType
13 alias Pleroma.FollowingRelationship.State
17 schema "following_relationships" do
18 field(:state, State, default: :follow_pending)
20 belongs_to(:follower, User, type: CompatType)
21 belongs_to(:following, User, type: CompatType)
26 @doc "Returns underlying integer code for state atom"
27 def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
29 def accept_state_code, do: state_int_code(:follow_accept)
31 def changeset(%__MODULE__{} = following_relationship, attrs) do
32 following_relationship
33 |> cast(attrs, [:state])
34 |> put_assoc(:follower, attrs.follower)
35 |> put_assoc(:following, attrs.following)
36 |> validate_required([:state, :follower, :following])
37 |> unique_constraint(:follower_id,
38 name: :following_relationships_follower_id_following_id_index
40 |> validate_not_self_relationship()
43 def state_to_enum(state) when state in ["pending", "accept", "reject"] do
44 String.to_existing_atom("follow_#{state}")
47 def state_to_enum(state) do
48 raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
51 def get(%User{} = follower, %User{} = following) do
53 |> where(follower_id: ^follower.id, following_id: ^following.id)
57 def update(follower, following, :follow_reject), do: unfollow(follower, following)
59 def update(%User{} = follower, %User{} = following, state) do
60 case get(follower, following) do
62 follow(follower, following, state)
64 following_relationship ->
66 following_relationship
67 |> cast(%{state: state}, [:state])
68 |> validate_required([:state])
75 def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
77 |> changeset(%{follower: follower, following: following, state: state})
78 |> Repo.insert(on_conflict: :nothing)
81 def unfollow(%User{} = follower, %User{} = following) do
82 case get(follower, following) do
83 %__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
88 def follower_count(%User{} = user) do
89 %{followers: user, deactivated: false}
91 |> Repo.aggregate(:count, :id)
94 def followers_query(%User{} = user) do
96 |> join(:inner, [r], u in User, on: r.follower_id == u.id)
97 |> where([r], r.following_id == ^user.id)
98 |> where([r], r.state == ^:follow_accept)
101 def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
105 |> select([r, u], u.ap_id)
109 where(query, [r, u], u.ap_id in ^from_ap_ids)
117 def following_count(%User{id: nil}), do: 0
119 def following_count(%User{} = user) do
120 %{friends: user, deactivated: false}
121 |> User.Query.build()
122 |> Repo.aggregate(:count, :id)
125 def get_follow_requests(%User{id: id}) do
127 |> join(:inner, [r], f in assoc(r, :follower))
128 |> where([r], r.state == ^:follow_pending)
129 |> where([r], r.following_id == ^id)
134 def following?(%User{id: follower_id}, %User{id: followed_id}) do
136 |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
140 def following_query(%User{} = user) do
142 |> join(:inner, [r], u in User, on: r.following_id == u.id)
143 |> where([r], r.follower_id == ^user.id)
144 |> where([r], r.state == ^:follow_accept)
147 def following(%User{} = user) do
149 following_query(user)
150 |> select([r, u], u.follower_address)
153 if not user.local or user.invisible do
156 [user.follower_address | following]
160 def move_following(origin, target) do
162 |> join(:inner, [r], f in assoc(r, :follower))
163 |> where(following_id: ^origin.id)
164 |> where([r, f], f.allow_following_move == true)
166 |> preload([:follower])
168 |> Enum.map(fn following_relationship ->
169 Repo.delete(following_relationship)
170 Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
174 User.update_follower_count(origin)
178 move_following(origin, target)
182 def all_between_user_sets(
186 when is_list(source_users) and is_list(target_users) do
187 source_user_ids = User.binary_id(source_users)
188 target_user_ids = User.binary_id(target_users)
193 "(follower_id = ANY(?) AND following_id = ANY(?)) OR \
194 (follower_id = ANY(?) AND following_id = ANY(?))",
204 def find(following_relationships, follower, following) do
205 Enum.find(following_relationships, fn
206 fr -> fr.follower_id == follower.id and fr.following_id == following.id
211 For a query with joined activity,
212 keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
214 def keep_following_or_not_domain_blocked(query, user) do
219 # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
221 NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
222 ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
223 ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
228 ^User.binary_id(user.id),
234 defp validate_not_self_relationship(%Changeset{} = changeset) do
236 |> validate_follower_id_following_id_inequality()
237 |> validate_following_id_follower_id_inequality()
240 defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
241 validate_change(changeset, :follower_id, fn _, follower_id ->
242 if follower_id == get_field(changeset, :following_id) do
243 [source_id: "can't be equal to following_id"]
250 defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
251 validate_change(changeset, :following_id, fn _, following_id ->
252 if following_id == get_field(changeset, :follower_id) do
253 [target_id: "can't be equal to follower_id"]