Merge branch 'develop' into issue/2099
[akkoma] / lib / pleroma / following_relationship.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 following_relationship
66 |> cast(%{state: state}, [:state])
67 |> validate_required([:state])
68 |> Repo.update()
69 end
70 end
71
72 def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
73 %__MODULE__{}
74 |> changeset(%{follower: follower, following: following, state: state})
75 |> Repo.insert(on_conflict: :nothing)
76 end
77
78 def unfollow(%User{} = follower, %User{} = following) do
79 case get(follower, following) do
80 %__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
81 _ -> {:ok, nil}
82 end
83 end
84
85 def follower_count(%User{} = user) do
86 %{followers: user, deactivated: false}
87 |> User.Query.build()
88 |> Repo.aggregate(:count, :id)
89 end
90
91 def followers_query(%User{} = user) do
92 __MODULE__
93 |> join(:inner, [r], u in User, on: r.follower_id == u.id)
94 |> where([r], r.following_id == ^user.id)
95 |> where([r], r.state == ^:follow_accept)
96 end
97
98 def followers_ap_ids(user, from_ap_ids \\ nil)
99
100 def followers_ap_ids(_, []), do: []
101
102 def followers_ap_ids(%User{} = user, from_ap_ids) do
103 query =
104 user
105 |> followers_query()
106 |> select([r, u], u.ap_id)
107
108 query =
109 if from_ap_ids do
110 where(query, [r, u], u.ap_id in ^from_ap_ids)
111 else
112 query
113 end
114
115 Repo.all(query)
116 end
117
118 def following_count(%User{id: nil}), do: 0
119
120 def following_count(%User{} = user) do
121 %{friends: user, deactivated: false}
122 |> User.Query.build()
123 |> Repo.aggregate(:count, :id)
124 end
125
126 def get_follow_requests(%User{id: id}) do
127 __MODULE__
128 |> join(:inner, [r], f in assoc(r, :follower))
129 |> where([r], r.state == ^:follow_pending)
130 |> where([r], r.following_id == ^id)
131 |> where([r, f], f.deactivated != true)
132 |> select([r, f], f)
133 |> Repo.all()
134 end
135
136 def following?(%User{id: follower_id}, %User{id: followed_id}) do
137 __MODULE__
138 |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
139 |> Repo.exists?()
140 end
141
142 def following_query(%User{} = user) do
143 __MODULE__
144 |> join(:inner, [r], u in User, on: r.following_id == u.id)
145 |> where([r], r.follower_id == ^user.id)
146 |> where([r], r.state == ^:follow_accept)
147 end
148
149 def outgoing_pending_follow_requests_query(%User{} = follower) do
150 __MODULE__
151 |> where([r], r.follower_id == ^follower.id)
152 |> where([r], r.state == ^:follow_pending)
153 end
154
155 def following(%User{} = user) do
156 following =
157 following_query(user)
158 |> select([r, u], u.follower_address)
159 |> Repo.all()
160
161 if not user.local or user.invisible do
162 following
163 else
164 [user.follower_address | following]
165 end
166 end
167
168 def move_following(origin, target) do
169 __MODULE__
170 |> join(:inner, [r], f in assoc(r, :follower))
171 |> where(following_id: ^origin.id)
172 |> where([r, f], f.allow_following_move == true)
173 |> limit(50)
174 |> preload([:follower])
175 |> Repo.all()
176 |> Enum.map(fn following_relationship ->
177 Repo.delete(following_relationship)
178 Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
179 end)
180 |> case do
181 [] ->
182 User.update_follower_count(origin)
183 :ok
184
185 _ ->
186 move_following(origin, target)
187 end
188 end
189
190 def all_between_user_sets(
191 source_users,
192 target_users
193 )
194 when is_list(source_users) and is_list(target_users) do
195 source_user_ids = User.binary_id(source_users)
196 target_user_ids = User.binary_id(target_users)
197
198 __MODULE__
199 |> where(
200 fragment(
201 "(follower_id = ANY(?) AND following_id = ANY(?)) OR \
202 (follower_id = ANY(?) AND following_id = ANY(?))",
203 ^source_user_ids,
204 ^target_user_ids,
205 ^target_user_ids,
206 ^source_user_ids
207 )
208 )
209 |> Repo.all()
210 end
211
212 def find(following_relationships, follower, following) do
213 Enum.find(following_relationships, fn
214 fr -> fr.follower_id == follower.id and fr.following_id == following.id
215 end)
216 end
217
218 @doc """
219 For a query with joined activity,
220 keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
221 """
222 def keep_following_or_not_domain_blocked(query, user) do
223 where(
224 query,
225 [_, activity],
226 fragment(
227 # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
228 """
229 NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
230 ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
231 ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
232 """,
233 activity.actor,
234 ^user.domain_blocks,
235 activity.actor,
236 ^User.binary_id(user.id),
237 ^accept_state_code()
238 )
239 )
240 end
241
242 defp validate_not_self_relationship(%Changeset{} = changeset) do
243 changeset
244 |> validate_follower_id_following_id_inequality()
245 |> validate_following_id_follower_id_inequality()
246 end
247
248 defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
249 validate_change(changeset, :follower_id, fn _, follower_id ->
250 if follower_id == get_field(changeset, :following_id) do
251 [source_id: "can't be equal to following_id"]
252 else
253 []
254 end
255 end)
256 end
257
258 defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
259 validate_change(changeset, :following_id, fn _, following_id ->
260 if following_id == get_field(changeset, :follower_id) do
261 [target_id: "can't be equal to follower_id"]
262 else
263 []
264 end
265 end)
266 end
267
268 @spec following_ap_ids(User.t()) :: [String.t()]
269 def following_ap_ids(%User{} = user) do
270 user
271 |> following_query()
272 |> select([r, u], u.ap_id)
273 |> Repo.all()
274 end
275 end