Merge pull request 'Manually define PATH for Arch Linux users in systemd unit' (...
[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 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 defp validate_not_self_relationship(%Changeset{} = changeset) do
244 changeset
245 |> validate_follower_id_following_id_inequality()
246 |> validate_following_id_follower_id_inequality()
247 end
248
249 defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
250 validate_change(changeset, :follower_id, fn _, follower_id ->
251 if follower_id == get_field(changeset, :following_id) do
252 [source_id: "can't be equal to following_id"]
253 else
254 []
255 end
256 end)
257 end
258
259 defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
260 validate_change(changeset, :following_id, fn _, following_id ->
261 if following_id == get_field(changeset, :follower_id) do
262 [target_id: "can't be equal to follower_id"]
263 else
264 []
265 end
266 end)
267 end
268
269 @spec following_ap_ids(User.t()) :: [String.t()]
270 def following_ap_ids(%User{} = user) do
271 user
272 |> following_query()
273 |> select([r, u], u.ap_id)
274 |> Repo.all()
275 end
276 end