Merge remote-tracking branch 'remotes/origin/develop' into 1364-no-pushes-from-blocke...
[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.Repo
14 alias Pleroma.User
15
16 schema "following_relationships" do
17 field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
18
19 belongs_to(:follower, User, type: CompatType)
20 belongs_to(:following, User, type: CompatType)
21
22 timestamps()
23 end
24
25 def changeset(%__MODULE__{} = following_relationship, attrs) do
26 following_relationship
27 |> cast(attrs, [:state])
28 |> put_assoc(:follower, attrs.follower)
29 |> put_assoc(:following, attrs.following)
30 |> validate_required([:state, :follower, :following])
31 |> unique_constraint(:follower_id,
32 name: :following_relationships_follower_id_following_id_index
33 )
34 |> validate_not_self_relationship()
35 end
36
37 def state_to_enum(state) when state in ["pending", "accept", "reject"] do
38 String.to_existing_atom("follow_#{state}")
39 end
40
41 def state_to_enum(state) do
42 raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
43 end
44
45 def get(%User{} = follower, %User{} = following) do
46 __MODULE__
47 |> where(follower_id: ^follower.id, following_id: ^following.id)
48 |> Repo.one()
49 end
50
51 def update(follower, following, :follow_reject), do: unfollow(follower, following)
52
53 def update(%User{} = follower, %User{} = following, state) do
54 case get(follower, following) do
55 nil ->
56 follow(follower, following, state)
57
58 following_relationship ->
59 following_relationship
60 |> cast(%{state: state}, [:state])
61 |> validate_required([:state])
62 |> Repo.update()
63 end
64 end
65
66 def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
67 %__MODULE__{}
68 |> changeset(%{follower: follower, following: following, state: state})
69 |> Repo.insert(on_conflict: :nothing)
70 end
71
72 def unfollow(%User{} = follower, %User{} = following) do
73 case get(follower, following) do
74 %__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
75 _ -> {:ok, nil}
76 end
77 end
78
79 def follower_count(%User{} = user) do
80 %{followers: user, deactivated: false}
81 |> User.Query.build()
82 |> Repo.aggregate(:count, :id)
83 end
84
85 def followers_query(%User{} = user) do
86 __MODULE__
87 |> join(:inner, [r], u in User, on: r.follower_id == u.id)
88 |> where([r], r.following_id == ^user.id)
89 |> where([r], r.state == "accept")
90 end
91
92 def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
93 query =
94 user
95 |> followers_query()
96 |> select([r, u], u.ap_id)
97
98 query =
99 if from_ap_ids do
100 where(query, [r, u], u.ap_id in ^from_ap_ids)
101 else
102 query
103 end
104
105 Repo.all(query)
106 end
107
108 def following_count(%User{id: nil}), do: 0
109
110 def following_count(%User{} = user) do
111 %{friends: user, deactivated: false}
112 |> User.Query.build()
113 |> Repo.aggregate(:count, :id)
114 end
115
116 def get_follow_requests(%User{id: id}) do
117 __MODULE__
118 |> join(:inner, [r], f in assoc(r, :follower))
119 |> where([r], r.state == ^:follow_pending)
120 |> where([r], r.following_id == ^id)
121 |> select([r, f], f)
122 |> Repo.all()
123 end
124
125 def following?(%User{id: follower_id}, %User{id: followed_id}) do
126 __MODULE__
127 |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
128 |> Repo.exists?()
129 end
130
131 def following_query(%User{} = user) do
132 __MODULE__
133 |> join(:inner, [r], u in User, on: r.following_id == u.id)
134 |> where([r], r.follower_id == ^user.id)
135 |> where([r], r.state == ^:follow_accept)
136 end
137
138 def following(%User{} = user) do
139 following =
140 following_query(user)
141 |> select([r, u], u.follower_address)
142 |> Repo.all()
143
144 if not user.local or user.invisible do
145 following
146 else
147 [user.follower_address | following]
148 end
149 end
150
151 def move_following(origin, target) do
152 __MODULE__
153 |> join(:inner, [r], f in assoc(r, :follower))
154 |> where(following_id: ^origin.id)
155 |> where([r, f], f.allow_following_move == true)
156 |> limit(50)
157 |> preload([:follower])
158 |> Repo.all()
159 |> Enum.map(fn following_relationship ->
160 Repo.delete(following_relationship)
161 Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
162 end)
163 |> case do
164 [] ->
165 User.update_follower_count(origin)
166 :ok
167
168 _ ->
169 move_following(origin, target)
170 end
171 end
172
173 def all_between_user_sets(
174 source_users,
175 target_users
176 )
177 when is_list(source_users) and is_list(target_users) do
178 source_user_ids = User.binary_id(source_users)
179 target_user_ids = User.binary_id(target_users)
180
181 __MODULE__
182 |> where(
183 fragment(
184 "(follower_id = ANY(?) AND following_id = ANY(?)) OR \
185 (follower_id = ANY(?) AND following_id = ANY(?))",
186 ^source_user_ids,
187 ^target_user_ids,
188 ^target_user_ids,
189 ^source_user_ids
190 )
191 )
192 |> Repo.all()
193 end
194
195 def find(following_relationships, follower, following) do
196 Enum.find(following_relationships, fn
197 fr -> fr.follower_id == follower.id and fr.following_id == following.id
198 end)
199 end
200
201 defp validate_not_self_relationship(%Changeset{} = changeset) do
202 changeset
203 |> validate_follower_id_following_id_inequality()
204 |> validate_following_id_follower_id_inequality()
205 end
206
207 defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
208 validate_change(changeset, :follower_id, fn _, follower_id ->
209 if follower_id == get_field(changeset, :following_id) do
210 [source_id: "can't be equal to following_id"]
211 else
212 []
213 end
214 end)
215 end
216
217 defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
218 validate_change(changeset, :following_id, fn _, following_id ->
219 if following_id == get_field(changeset, :follower_id) do
220 [target_id: "can't be equal to follower_id"]
221 else
222 []
223 end
224 end)
225 end
226 end