Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

353 lines
9.3KB

  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. 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.Object
  11. alias Pleroma.Object.Fetcher
  12. alias Pleroma.ObjectTombstone
  13. alias Pleroma.Repo
  14. alias Pleroma.User
  15. alias Pleroma.Workers.AttachmentsCleanupWorker
  16. require Logger
  17. @type t() :: %__MODULE__{}
  18. @derive {Jason.Encoder, only: [:data]}
  19. @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
  20. schema "objects" do
  21. field(:data, :map)
  22. timestamps()
  23. end
  24. def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
  25. object_position = Map.get(query.aliases, :object, 0)
  26. join(query, join_type, [{object, object_position}], a in Activity,
  27. on:
  28. fragment(
  29. "COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
  30. a.data,
  31. a.data,
  32. object.data,
  33. a.data,
  34. ^activity_type
  35. ),
  36. as: :object_activity
  37. )
  38. end
  39. def create(data) do
  40. Object.change(%Object{}, %{data: data})
  41. |> Repo.insert()
  42. end
  43. def change(struct, params \\ %{}) do
  44. struct
  45. |> cast(params, [:data])
  46. |> validate_required([:data])
  47. |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
  48. end
  49. def get_by_id(nil), do: nil
  50. def get_by_id(id), do: Repo.get(Object, id)
  51. def get_by_id_and_maybe_refetch(id, opts \\ []) do
  52. %{updated_at: updated_at} = object = get_by_id(id)
  53. if opts[:interval] &&
  54. NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
  55. case Fetcher.refetch_object(object) do
  56. {:ok, %Object{} = object} ->
  57. object
  58. e ->
  59. Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
  60. object
  61. end
  62. else
  63. object
  64. end
  65. end
  66. def get_by_ap_id(nil), do: nil
  67. def get_by_ap_id(ap_id) do
  68. Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
  69. end
  70. @doc """
  71. Get a single attachment by it's name and href
  72. """
  73. @spec get_attachment_by_name_and_href(String.t(), String.t()) :: Object.t() | nil
  74. def get_attachment_by_name_and_href(name, href) do
  75. query =
  76. from(o in Object,
  77. where: fragment("(?)->>'name' = ?", o.data, ^name),
  78. where: fragment("(?)->>'href' = ?", o.data, ^href)
  79. )
  80. Repo.one(query)
  81. end
  82. defp warn_on_no_object_preloaded(ap_id) do
  83. "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
  84. |> Logger.debug()
  85. Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
  86. end
  87. def normalize(_, options \\ [fetch: false])
  88. # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
  89. # Use this whenever possible, especially when walking graphs in an O(N) loop!
  90. def normalize(%Object{} = object, _), do: object
  91. def normalize(%Activity{object: %Object{} = object}, _), do: object
  92. # A hack for fake activities
  93. def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
  94. %Object{id: "pleroma:fake_object_id", data: data}
  95. end
  96. # No preloaded object
  97. def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, options) do
  98. warn_on_no_object_preloaded(ap_id)
  99. normalize(ap_id, options)
  100. end
  101. # No preloaded object
  102. def normalize(%Activity{data: %{"object" => ap_id}}, options) do
  103. warn_on_no_object_preloaded(ap_id)
  104. normalize(ap_id, options)
  105. end
  106. # Old way, try fetching the object through cache.
  107. def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
  108. def normalize(ap_id, options) when is_binary(ap_id) do
  109. if Keyword.get(options, :fetch) do
  110. Fetcher.fetch_object_from_id!(ap_id, options)
  111. else
  112. get_cached_by_ap_id(ap_id)
  113. end
  114. end
  115. def normalize(_, _), do: nil
  116. # Owned objects can only be accessed by their owner
  117. def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
  118. if actor == ap_id do
  119. :ok
  120. else
  121. {:error, :forbidden}
  122. end
  123. end
  124. # Legacy objects can be accessed by anybody
  125. def authorize_access(%Object{}, %User{}), do: :ok
  126. @spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
  127. def get_cached_by_ap_id(ap_id) do
  128. key = "object:#{ap_id}"
  129. with {:ok, nil} <- @cachex.get(:object_cache, key),
  130. object when not is_nil(object) <- get_by_ap_id(ap_id),
  131. {:ok, true} <- @cachex.put(:object_cache, key, object) do
  132. object
  133. else
  134. {:ok, object} -> object
  135. nil -> nil
  136. end
  137. end
  138. def context_mapping(context) do
  139. Object.change(%Object{}, %{data: %{"id" => context}})
  140. end
  141. def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
  142. %ObjectTombstone{
  143. id: id,
  144. formerType: type,
  145. deleted: deleted
  146. }
  147. |> Map.from_struct()
  148. end
  149. def swap_object_with_tombstone(object) do
  150. tombstone = make_tombstone(object)
  151. object
  152. |> Object.change(%{data: tombstone})
  153. |> Repo.update()
  154. end
  155. def delete(%Object{data: %{"id" => id}} = object) do
  156. with {:ok, _obj} = swap_object_with_tombstone(object),
  157. deleted_activity = Activity.delete_all_by_object_ap_id(id),
  158. {:ok, _} <- invalid_object_cache(object) do
  159. cleanup_attachments(
  160. Config.get([:instance, :cleanup_attachments]),
  161. %{"object" => object}
  162. )
  163. {:ok, object, deleted_activity}
  164. end
  165. end
  166. @spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
  167. {:ok, Oban.Job.t() | nil}
  168. def cleanup_attachments(true, %{"object" => _} = params) do
  169. AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
  170. end
  171. def cleanup_attachments(_, _), do: {:ok, nil}
  172. def prune(%Object{data: %{"id" => _id}} = object) do
  173. with {:ok, object} <- Repo.delete(object),
  174. {:ok, _} <- invalid_object_cache(object) do
  175. {:ok, object}
  176. end
  177. end
  178. def invalid_object_cache(%Object{data: %{"id" => id}}) do
  179. with {:ok, true} <- @cachex.del(:object_cache, "object:#{id}") do
  180. @cachex.del(:web_resp_cache, URI.parse(id).path)
  181. end
  182. end
  183. def set_cache(%Object{data: %{"id" => ap_id}} = object) do
  184. @cachex.put(:object_cache, "object:#{ap_id}", object)
  185. {:ok, object}
  186. end
  187. def update_and_set_cache(changeset) do
  188. with {:ok, object} <- Repo.update(changeset) do
  189. set_cache(object)
  190. end
  191. end
  192. def increase_replies_count(ap_id) do
  193. Object
  194. |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
  195. |> update([o],
  196. set: [
  197. data:
  198. fragment(
  199. """
  200. safe_jsonb_set(?, '{repliesCount}',
  201. (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
  202. """,
  203. o.data,
  204. o.data
  205. )
  206. ]
  207. )
  208. |> Repo.update_all([])
  209. |> case do
  210. {1, [object]} -> set_cache(object)
  211. _ -> {:error, "Not found"}
  212. end
  213. end
  214. defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true
  215. defp poll_is_multiple?(_), do: false
  216. def decrease_replies_count(ap_id) do
  217. Object
  218. |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
  219. |> update([o],
  220. set: [
  221. data:
  222. fragment(
  223. """
  224. safe_jsonb_set(?, '{repliesCount}',
  225. (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
  226. """,
  227. o.data,
  228. o.data
  229. )
  230. ]
  231. )
  232. |> Repo.update_all([])
  233. |> case do
  234. {1, [object]} -> set_cache(object)
  235. _ -> {:error, "Not found"}
  236. end
  237. end
  238. def increase_vote_count(ap_id, name, actor) do
  239. with %Object{} = object <- Object.normalize(ap_id, fetch: false),
  240. "Question" <- object.data["type"] do
  241. key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
  242. options =
  243. object.data[key]
  244. |> Enum.map(fn
  245. %{"name" => ^name} = option ->
  246. Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
  247. option ->
  248. option
  249. end)
  250. voters = [actor | object.data["voters"] || []] |> Enum.uniq()
  251. data =
  252. object.data
  253. |> Map.put(key, options)
  254. |> Map.put("voters", voters)
  255. object
  256. |> Object.change(%{data: data})
  257. |> update_and_set_cache()
  258. else
  259. _ -> :noop
  260. end
  261. end
  262. @doc "Updates data field of an object"
  263. def update_data(%Object{data: data} = object, attrs \\ %{}) do
  264. object
  265. |> Object.change(%{data: Map.merge(data || %{}, attrs)})
  266. |> Repo.update()
  267. end
  268. def local?(%Object{data: %{"id" => id}}) do
  269. String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
  270. end
  271. def replies(object, opts \\ []) do
  272. object = Object.normalize(object, fetch: false)
  273. query =
  274. Object
  275. |> where(
  276. [o],
  277. fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
  278. )
  279. |> order_by([o], asc: o.id)
  280. if opts[:self_only] do
  281. actor = object.data["actor"]
  282. where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
  283. else
  284. query
  285. end
  286. end
  287. def self_replies(object, opts \\ []),
  288. do: replies(object, Keyword.put(opts, :self_only, true))
  289. end