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