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