Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into features/poll-valid...
[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 defp poll_is_multiple?(%Object{data: %{"anyOf" => anyOf}}) do
259 !Enum.empty?(anyOf)
260 end
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