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.

429 lines
12KB

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