paginate follow requests (#460)
[akkoma] / lib / pleroma / following_relationship.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.FollowingRelationship do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Ecto.Changeset
12 alias FlakeId.Ecto.CompatType
13 alias Pleroma.FollowingRelationship.State
14 alias Pleroma.Repo
15 alias Pleroma.User
16
17 @type follow_state :: :follow_pending | :follow_accept | :follow_reject | :unfollow
18
19 schema "following_relationships" do
20 field(:state, State, default: :follow_pending)
21
22 belongs_to(:follower, User, type: CompatType)
23 belongs_to(:following, User, type: CompatType)
24
25 timestamps()
26 end
27
28 @doc "Returns underlying integer code for state atom"
29 def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
30
31 def accept_state_code, do: state_int_code(:follow_accept)
32
33 def changeset(%__MODULE__{} = following_relationship, attrs) do
34 following_relationship
35 |> cast(attrs, [:state])
36 |> put_assoc(:follower, attrs.follower)
37 |> put_assoc(:following, attrs.following)
38 |> validate_required([:state, :follower, :following])
39 |> unique_constraint(:follower_id,
40 name: :following_relationships_follower_id_following_id_index
41 )
42 |> validate_not_self_relationship()
43 end
44
45 def state_to_enum(state) when state in ["pending", "accept", "reject"] do
46 String.to_existing_atom("follow_#{state}")
47 end
48
49 def state_to_enum(state) do
50 raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
51 end
52
53 def get(%User{} = follower, %User{} = following) do
54 __MODULE__
55 |> where(follower_id: ^follower.id, following_id: ^following.id)
56 |> Repo.one()
57 end
58
59 def update(follower, following, :follow_reject), do: unfollow(follower, following)
60
61 def update(%User{} = follower, %User{} = following, state) do
62 case get(follower, following) do
63 nil ->
64 follow(follower, following, state)
65
66 following_relationship ->
67 with {:ok, _following_relationship} <-
68 following_relationship
69 |> cast(%{state: state}, [:state])
70 |> validate_required([:state])
71 |> Repo.update() do
72 after_update(state, follower, following)
73 end
74 end
75 end
76
77 @spec follow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, any}
78 def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
79 with {:ok, _following_relationship} <-
80 %__MODULE__{}
81 |> changeset(%{follower: follower, following: following, state: state})
82 |> Repo.insert(on_conflict: :nothing) do
83 after_update(state, follower, following)
84 end
85 end
86
87 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, any}
88 def unfollow(%User{} = follower, %User{} = following) do
89 case get(follower, following) do
90 %__MODULE__{} = following_relationship ->
91 with {:ok, _following_relationship} <- Repo.delete(following_relationship) do
92 after_update(:unfollow, follower, following)
93 end
94
95 _ ->
96 {:ok, follower, following}
97 end
98 end
99
100 @spec after_update(follow_state(), User.t(), User.t()) ::
101 {:ok, User.t(), User.t()} | {:error, any()}
102 defp after_update(state, %User{} = follower, %User{} = following) do
103 with {:ok, following} <- User.update_follower_count(following),
104 {:ok, follower} <- User.update_following_count(follower) do
105 Pleroma.Web.Streamer.stream("follow_relationship", %{
106 state: state,
107 following: following,
108 follower: follower
109 })
110
111 {:ok, follower, following}
112 else
113 err -> {:error, err}
114 end
115 end
116
117 def follower_count(%User{} = user) do
118 %{followers: user, deactivated: false}
119 |> User.Query.build()
120 |> Repo.aggregate(:count, :id)
121 end
122
123 def followers_query(%User{} = user) do
124 __MODULE__
125 |> join(:inner, [r], u in User, on: r.follower_id == u.id)
126 |> where([r], r.following_id == ^user.id)
127 |> where([r], r.state == ^:follow_accept)
128 end
129
130 def followers_ap_ids(user, from_ap_ids \\ nil)
131
132 def followers_ap_ids(_, []), do: []
133
134 def followers_ap_ids(%User{} = user, from_ap_ids) do
135 query =
136 user
137 |> followers_query()
138 |> select([r, u], u.ap_id)
139
140 query =
141 if from_ap_ids do
142 where(query, [r, u], u.ap_id in ^from_ap_ids)
143 else
144 query
145 end
146
147 Repo.all(query)
148 end
149
150 def following_count(%User{id: nil}), do: 0
151
152 def following_count(%User{} = user) do
153 %{friends: user, deactivated: false}
154 |> User.Query.build()
155 |> Repo.aggregate(:count, :id)
156 end
157
158 def get_follow_requests_query(%User{id: id}) do
159 __MODULE__
160 |> join(:inner, [r], f in assoc(r, :follower), as: :follower)
161 |> where([r], r.state == ^:follow_pending)
162 |> where([r], r.following_id == ^id)
163 |> where([r, follower: f], f.is_active == true)
164 |> select([r, follower: f], f)
165 end
166
167 def following?(%User{id: follower_id}, %User{id: followed_id}) do
168 __MODULE__
169 |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
170 |> Repo.exists?()
171 end
172
173 def following_query(%User{} = user) do
174 __MODULE__
175 |> join(:inner, [r], u in User, on: r.following_id == u.id)
176 |> where([r], r.follower_id == ^user.id)
177 |> where([r], r.state == ^:follow_accept)
178 end
179
180 def outgoing_pending_follow_requests_query(%User{} = follower) do
181 __MODULE__
182 |> where([r], r.follower_id == ^follower.id)
183 |> where([r], r.state == ^:follow_pending)
184 end
185
186 def following(%User{} = user) do
187 following =
188 following_query(user)
189 |> select([r, u], u.follower_address)
190 |> Repo.all()
191
192 if not user.local or user.invisible do
193 following
194 else
195 [user.follower_address | following]
196 end
197 end
198
199 def move_following(origin, target) do
200 __MODULE__
201 |> join(:inner, [r], f in assoc(r, :follower))
202 |> where(following_id: ^origin.id)
203 |> where([r, f], f.allow_following_move == true)
204 |> where([r, f], f.local == true)
205 |> limit(50)
206 |> preload([:follower])
207 |> Repo.all()
208 |> Enum.map(fn following_relationship ->
209 Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
210 Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
211 end)
212 |> case do
213 [] ->
214 User.update_follower_count(origin)
215 :ok
216
217 _ ->
218 move_following(origin, target)
219 end
220 end
221
222 def all_between_user_sets(
223 source_users,
224 target_users
225 )
226 when is_list(source_users) and is_list(target_users) do
227 source_user_ids = User.binary_id(source_users)
228 target_user_ids = User.binary_id(target_users)
229
230 __MODULE__
231 |> where(
232 fragment(
233 "(follower_id = ANY(?) AND following_id = ANY(?)) OR \
234 (follower_id = ANY(?) AND following_id = ANY(?))",
235 ^source_user_ids,
236 ^target_user_ids,
237 ^target_user_ids,
238 ^source_user_ids
239 )
240 )
241 |> Repo.all()
242 end
243
244 def find(following_relationships, follower, following) do
245 Enum.find(following_relationships, fn
246 fr -> fr.follower_id == follower.id and fr.following_id == following.id
247 end)
248 end
249
250 defp validate_not_self_relationship(%Changeset{} = changeset) do
251 changeset
252 |> validate_follower_id_following_id_inequality()
253 |> validate_following_id_follower_id_inequality()
254 end
255
256 defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
257 validate_change(changeset, :follower_id, fn _, follower_id ->
258 if follower_id == get_field(changeset, :following_id) do
259 [source_id: "can't be equal to following_id"]
260 else
261 []
262 end
263 end)
264 end
265
266 defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
267 validate_change(changeset, :following_id, fn _, following_id ->
268 if following_id == get_field(changeset, :follower_id) do
269 [target_id: "can't be equal to follower_id"]
270 else
271 []
272 end
273 end)
274 end
275
276 @spec following_ap_ids(User.t()) :: [String.t()]
277 def following_ap_ids(%User{} = user) do
278 user
279 |> following_query()
280 |> select([r, u], u.ap_id)
281 |> Repo.all()
282 end
283 end