logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

following_relationship.ex (8629B)


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