logo

pleroma

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

object.ex (13707B)


  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.Object do
  5. use Ecto.Schema
  6. import Ecto.Query
  7. import Ecto.Changeset
  8. alias Pleroma.Activity
  9. alias Pleroma.Config
  10. alias Pleroma.Hashtag
  11. alias Pleroma.Object
  12. alias Pleroma.Object.Fetcher
  13. alias Pleroma.ObjectTombstone
  14. alias Pleroma.Repo
  15. alias Pleroma.User
  16. alias Pleroma.Workers.AttachmentsCleanupWorker
  17. require Logger
  18. @type t() :: %__MODULE__{}
  19. @derive {Jason.Encoder, only: [:data]}
  20. @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
  21. schema "objects" do
  22. field(:data, :map)
  23. many_to_many(:hashtags, Hashtag, join_through: "hashtags_objects", on_replace: :delete)
  24. timestamps()
  25. end
  26. def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
  27. object_position = Map.get(query.aliases, :object, 0)
  28. join(query, join_type, [{object, object_position}], a in Activity,
  29. on:
  30. fragment(
  31. "associated_object_id(?) = (? ->> 'id') AND (?->>'type' = ?) ",
  32. a.data,
  33. object.data,
  34. a.data,
  35. ^activity_type
  36. ),
  37. as: :object_activity
  38. )
  39. end
  40. def create(data) do
  41. %Object{}
  42. |> Object.change(%{data: data})
  43. |> Repo.insert()
  44. end
  45. def change(struct, params \\ %{}) do
  46. struct
  47. |> cast(params, [:data])
  48. |> validate_required([:data])
  49. |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
  50. # Expecting `maybe_handle_hashtags_change/1` to run last:
  51. |> maybe_handle_hashtags_change(struct)
  52. end
  53. # Note: not checking activity type (assuming non-legacy objects are associated with Create act.)
  54. defp maybe_handle_hashtags_change(changeset, struct) do
  55. with %Ecto.Changeset{valid?: true} <- changeset,
  56. data_hashtags_change = get_change(changeset, :data),
  57. {_, true} <- {:changed, hashtags_changed?(struct, data_hashtags_change)},
  58. {:ok, hashtag_records} <-
  59. data_hashtags_change
  60. |> object_data_hashtags()
  61. |> Hashtag.get_or_create_by_names() do
  62. put_assoc(changeset, :hashtags, hashtag_records)
  63. else
  64. %{valid?: false} ->
  65. changeset
  66. {:changed, false} ->
  67. changeset
  68. {:error, _} ->
  69. validate_change(changeset, :data, fn _, _ ->
  70. [data: "error referencing hashtags"]
  71. end)
  72. end
  73. end
  74. defp hashtags_changed?(%Object{} = struct, %{"tag" => _} = data) do
  75. Enum.sort(embedded_hashtags(struct)) !=
  76. Enum.sort(object_data_hashtags(data))
  77. end
  78. defp hashtags_changed?(_, _), do: false
  79. def get_by_id(nil), do: nil
  80. def get_by_id(id), do: Repo.get(Object, id)
  81. def get_by_id_and_maybe_refetch(id, opts \\ []) do
  82. %{updated_at: updated_at} = object = get_by_id(id)
  83. if opts[:interval] &&
  84. NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
  85. case Fetcher.refetch_object(object) do
  86. {:ok, %Object{} = object} ->
  87. object
  88. e ->
  89. Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
  90. object
  91. end
  92. else
  93. object
  94. end
  95. end
  96. def get_by_ap_id(nil), do: nil
  97. def get_by_ap_id(ap_id) do
  98. Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
  99. end
  100. @doc """
  101. Get a single attachment by it's name and href
  102. """
  103. @spec get_attachment_by_name_and_href(String.t(), String.t()) :: Object.t() | nil
  104. def get_attachment_by_name_and_href(name, href) do
  105. query =
  106. from(o in Object,
  107. where: fragment("(?)->>'name' = ?", o.data, ^name),
  108. where: fragment("(?)->>'href' = ?", o.data, ^href)
  109. )
  110. Repo.one(query)
  111. end
  112. defp warn_on_no_object_preloaded(ap_id) do
  113. "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
  114. |> Logger.debug()
  115. Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
  116. end
  117. def normalize(_, options \\ [fetch: false, id_only: false])
  118. # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
  119. # Use this whenever possible, especially when walking graphs in an O(N) loop!
  120. def normalize(%Object{} = object, _), do: object
  121. def normalize(%Activity{object: %Object{} = object}, _), do: object
  122. # A hack for fake activities
  123. def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
  124. %Object{id: "pleroma:fake_object_id", data: data}
  125. end
  126. # No preloaded object
  127. def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, options) do
  128. warn_on_no_object_preloaded(ap_id)
  129. normalize(ap_id, options)
  130. end
  131. # No preloaded object
  132. def normalize(%Activity{data: %{"object" => ap_id}}, options) do
  133. warn_on_no_object_preloaded(ap_id)
  134. normalize(ap_id, options)
  135. end
  136. # Old way, try fetching the object through cache.
  137. def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
  138. def normalize(ap_id, options) when is_binary(ap_id) do
  139. cond do
  140. Keyword.get(options, :id_only) ->
  141. ap_id
  142. Keyword.get(options, :fetch) ->
  143. case Fetcher.fetch_object_from_id(ap_id, options) do
  144. {:ok, object} -> object
  145. _ -> nil
  146. end
  147. true ->
  148. get_cached_by_ap_id(ap_id)
  149. end
  150. end
  151. def normalize(_, _), do: nil
  152. # Owned objects can only be accessed by their owner
  153. def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
  154. if actor == ap_id do
  155. :ok
  156. else
  157. {:error, :forbidden}
  158. end
  159. end
  160. # Legacy objects can be accessed by anybody
  161. def authorize_access(%Object{}, %User{}), do: :ok
  162. @spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
  163. def get_cached_by_ap_id(ap_id) do
  164. key = "object:#{ap_id}"
  165. with {:ok, nil} <- @cachex.get(:object_cache, key),
  166. object when not is_nil(object) <- get_by_ap_id(ap_id),
  167. {:ok, true} <- @cachex.put(:object_cache, key, object) do
  168. object
  169. else
  170. {:ok, object} -> object
  171. nil -> nil
  172. end
  173. end
  174. def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
  175. %ObjectTombstone{
  176. id: id,
  177. formerType: type,
  178. deleted: deleted
  179. }
  180. |> Map.from_struct()
  181. end
  182. def swap_object_with_tombstone(object) do
  183. tombstone = make_tombstone(object)
  184. with {:ok, object} <-
  185. object
  186. |> Object.change(%{data: tombstone})
  187. |> Repo.update() do
  188. Hashtag.unlink(object)
  189. {:ok, object}
  190. end
  191. end
  192. def delete(%Object{data: %{"id" => id}} = object) do
  193. with {:ok, _obj} = swap_object_with_tombstone(object),
  194. deleted_activity = Activity.delete_all_by_object_ap_id(id),
  195. {:ok, _} <- invalid_object_cache(object) do
  196. cleanup_attachments(
  197. Config.get([:instance, :cleanup_attachments]),
  198. object
  199. )
  200. {:ok, object, deleted_activity}
  201. end
  202. end
  203. @spec cleanup_attachments(boolean(), Object.t()) ::
  204. {:ok, Oban.Job.t() | nil}
  205. def cleanup_attachments(true, %Object{} = object) do
  206. AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{"object" => object})
  207. end
  208. def cleanup_attachments(_, _), do: {:ok, nil}
  209. def prune(%Object{data: %{"id" => _id}} = object) do
  210. with {:ok, object} <- Repo.delete(object),
  211. {:ok, _} <- invalid_object_cache(object) do
  212. {:ok, object}
  213. end
  214. end
  215. def invalid_object_cache(%Object{data: %{"id" => id}}) do
  216. with {:ok, true} <- @cachex.del(:object_cache, "object:#{id}") do
  217. @cachex.del(:web_resp_cache, URI.parse(id).path)
  218. end
  219. end
  220. def set_cache(%Object{data: %{"id" => ap_id}} = object) do
  221. @cachex.put(:object_cache, "object:#{ap_id}", object)
  222. {:ok, object}
  223. end
  224. def update_and_set_cache(changeset) do
  225. with {:ok, object} <- Repo.update(changeset) do
  226. set_cache(object)
  227. end
  228. end
  229. def increase_replies_count(ap_id) do
  230. Object
  231. |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
  232. |> update([o],
  233. set: [
  234. data:
  235. fragment(
  236. """
  237. safe_jsonb_set(?, '{repliesCount}',
  238. (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
  239. """,
  240. o.data,
  241. o.data
  242. )
  243. ]
  244. )
  245. |> Repo.update_all([])
  246. |> case do
  247. {1, [object]} -> set_cache(object)
  248. _ -> {:error, "Not found"}
  249. end
  250. end
  251. defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true
  252. defp poll_is_multiple?(_), do: false
  253. def decrease_replies_count(ap_id) do
  254. Object
  255. |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
  256. |> update([o],
  257. set: [
  258. data:
  259. fragment(
  260. """
  261. safe_jsonb_set(?, '{repliesCount}',
  262. (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
  263. """,
  264. o.data,
  265. o.data
  266. )
  267. ]
  268. )
  269. |> Repo.update_all([])
  270. |> case do
  271. {1, [object]} -> set_cache(object)
  272. _ -> {:error, "Not found"}
  273. end
  274. end
  275. def increase_quotes_count(ap_id) do
  276. Object
  277. |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
  278. |> update([o],
  279. set: [
  280. data:
  281. fragment(
  282. """
  283. safe_jsonb_set(?, '{quotesCount}',
  284. (coalesce((?->>'quotesCount')::int, 0) + 1)::varchar::jsonb, true)
  285. """,
  286. o.data,
  287. o.data
  288. )
  289. ]
  290. )
  291. |> Repo.update_all([])
  292. |> case do
  293. {1, [object]} -> set_cache(object)
  294. _ -> {:error, "Not found"}
  295. end
  296. end
  297. def decrease_quotes_count(ap_id) do
  298. Object
  299. |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
  300. |> update([o],
  301. set: [
  302. data:
  303. fragment(
  304. """
  305. safe_jsonb_set(?, '{quotesCount}',
  306. (greatest(0, (?->>'quotesCount')::int - 1))::varchar::jsonb, true)
  307. """,
  308. o.data,
  309. o.data
  310. )
  311. ]
  312. )
  313. |> Repo.update_all([])
  314. |> case do
  315. {1, [object]} -> set_cache(object)
  316. _ -> {:error, "Not found"}
  317. end
  318. end
  319. def increase_vote_count(ap_id, name, actor) do
  320. with %Object{} = object <- Object.normalize(ap_id, fetch: false),
  321. "Question" <- object.data["type"] do
  322. key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
  323. options =
  324. object.data[key]
  325. |> Enum.map(fn
  326. %{"name" => ^name} = option ->
  327. Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
  328. option ->
  329. option
  330. end)
  331. voters = [actor | object.data["voters"] || []] |> Enum.uniq()
  332. data =
  333. object.data
  334. |> Map.put(key, options)
  335. |> Map.put("voters", voters)
  336. object
  337. |> Object.change(%{data: data})
  338. |> update_and_set_cache()
  339. else
  340. _ -> :noop
  341. end
  342. end
  343. @doc "Updates data field of an object"
  344. def update_data(%Object{data: data} = object, attrs \\ %{}) do
  345. object
  346. |> Object.change(%{data: Map.merge(data || %{}, attrs)})
  347. |> Repo.update()
  348. end
  349. def local?(%Object{data: %{"id" => id}}) do
  350. String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
  351. end
  352. def replies(object, opts \\ []) do
  353. object = Object.normalize(object, fetch: false)
  354. query =
  355. Object
  356. |> where(
  357. [o],
  358. fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
  359. )
  360. |> order_by([o], asc: o.id)
  361. if opts[:self_only] do
  362. actor = object.data["actor"]
  363. where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
  364. else
  365. query
  366. end
  367. end
  368. def self_replies(object, opts \\ []),
  369. do: replies(object, Keyword.put(opts, :self_only, true))
  370. def tags(%Object{data: %{"tag" => tags}}) when is_list(tags), do: tags
  371. def tags(_), do: []
  372. def hashtags(%Object{} = object) do
  373. # Note: always using embedded hashtags regardless whether they are migrated to hashtags table
  374. # (embedded hashtags stay in sync anyways, and we avoid extra joins and preload hassle)
  375. embedded_hashtags(object)
  376. end
  377. def embedded_hashtags(%Object{data: data}) do
  378. object_data_hashtags(data)
  379. end
  380. def embedded_hashtags(_), do: []
  381. def object_data_hashtags(%{"tag" => tags}) when is_list(tags) do
  382. tags
  383. |> Enum.filter(fn
  384. %{"type" => "Hashtag"} = data -> Map.has_key?(data, "name")
  385. plain_text when is_bitstring(plain_text) -> true
  386. _ -> false
  387. end)
  388. |> Enum.map(fn
  389. %{"name" => "#" <> hashtag} -> String.downcase(hashtag)
  390. %{"name" => hashtag} -> String.downcase(hashtag)
  391. hashtag when is_bitstring(hashtag) -> String.downcase(hashtag)
  392. end)
  393. |> Enum.uniq()
  394. # Note: "" elements (plain text) might occur in `data.tag` for incoming objects
  395. |> Enum.filter(&(&1 not in [nil, ""]))
  396. end
  397. def object_data_hashtags(_), do: []
  398. def get_emoji_reactions(object) do
  399. reactions = object.data["reactions"]
  400. if is_list(reactions) or is_map(reactions) do
  401. reactions
  402. |> Enum.map(fn
  403. [_emoji, users, _maybe_url] = item when is_list(users) ->
  404. item
  405. [emoji, users] when is_list(users) ->
  406. [emoji, users, nil]
  407. # This case is here to process the Map situation, which will happen
  408. # only with the legacy two-value format.
  409. {emoji, users} when is_list(users) ->
  410. [emoji, users, nil]
  411. _ ->
  412. nil
  413. end)
  414. |> Enum.reject(&is_nil/1)
  415. else
  416. []
  417. end
  418. end
  419. end