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