object.ex 8.65 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
6
defmodule Pleroma.Object do
  use Ecto.Schema
Haelwenn's avatar
Haelwenn committed
7
8

  alias Pleroma.Activity
9
  alias Pleroma.Object
10
  alias Pleroma.Object.Fetcher
Haelwenn's avatar
Haelwenn committed
11
  alias Pleroma.ObjectTombstone
12
13
  alias Pleroma.Repo
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
14
15
16

  import Ecto.Query
  import Ecto.Changeset
lain's avatar
lain committed
17

18
  require Logger
lain's avatar
lain committed
19

20
21
  @type t() :: %__MODULE__{}

22
23
  @derive {Jason.Encoder, only: [:data]}

lain's avatar
lain committed
24
  schema "objects" do
lain's avatar
lain committed
25
    field(:data, :map)
lain's avatar
lain committed
26
27
28

    timestamps()
  end
29

Maksim's avatar
Maksim committed
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
    object_position = Map.get(query.aliases, :object, 0)

    join(query, join_type, [{object, object_position}], a in Activity,
      on:
        fragment(
          "COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
          a.data,
          a.data,
          object.data,
          a.data,
          ^activity_type
        ),
      as: :object_activity
    )
  end

lain's avatar
lain committed
47
48
  def create(data) do
    Object.change(%Object{}, %{data: data})
lain's avatar
lain committed
49
    |> Repo.insert()
lain's avatar
lain committed
50
51
  end

lain's avatar
lain committed
52
  def change(struct, params \\ %{}) do
Thog's avatar
Thog committed
53
    struct
lain's avatar
lain committed
54
55
56
57
58
    |> cast(params, [:data])
    |> validate_required([:data])
    |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
  end

rinpatch's avatar
rinpatch committed
59
60
61
  def get_by_id(nil), do: nil
  def get_by_id(id), do: Repo.get(Object, id)

rinpatch's avatar
rinpatch committed
62
63
64
65
  def get_by_id_and_maybe_refetch(id, opts \\ []) do
    %{updated_at: updated_at} = object = get_by_id(id)

    if opts[:interval] &&
66
         NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
rinpatch's avatar
rinpatch committed
67
68
69
70
71
72
73
74
75
76
77
78
79
      case Fetcher.refetch_object(object) do
        {:ok, %Object{} = object} ->
          object

        e ->
          Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
          object
      end
    else
      object
    end
  end

lain's avatar
lain committed
80
  def get_by_ap_id(nil), do: nil
lain's avatar
lain committed
81

82
  def get_by_ap_id(ap_id) do
lain's avatar
lain committed
83
    Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
84
  end
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99
  @doc """
  Get a single attachment by it's name and href
  """
  @spec get_attachment_by_name_and_href(String.t(), String.t()) :: Object.t() | nil
  def get_attachment_by_name_and_href(name, href) do
    query =
      from(o in Object,
        where: fragment("(?)->>'name' = ?", o.data, ^name),
        where: fragment("(?)->>'href' = ?", o.data, ^href)
      )

    Repo.one(query)
  end

100
  defp warn_on_no_object_preloaded(ap_id) do
101
    "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
102
103
104
105
106
    |> Logger.debug()

    Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
  end

107
  def normalize(_, fetch_remote \\ true, options \\ [])
108

109
110
  # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
  # Use this whenever possible, especially when walking graphs in an O(N) loop!
111
112
  def normalize(%Object{} = object, _, _), do: object
  def normalize(%Activity{object: %Object{} = object}, _, _), do: object
113

rinpatch's avatar
rinpatch committed
114
  # A hack for fake activities
115
  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
rinpatch's avatar
rinpatch committed
116
117
118
    %Object{id: "pleroma:fake_object_id", data: data}
  end

119
  # No preloaded object
120
  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
121
    warn_on_no_object_preloaded(ap_id)
122
    normalize(ap_id, fetch_remote)
123
124
  end

125
  # No preloaded object
126
  def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
127
    warn_on_no_object_preloaded(ap_id)
128
    normalize(ap_id, fetch_remote)
129
130
131
  end

  # Old way, try fetching the object through cache.
132
133
134
135
136
137
138
139
  def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
  def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)

  def normalize(ap_id, true, options) when is_binary(ap_id) do
    Fetcher.fetch_object_from_id!(ap_id, options)
  end

  def normalize(_, _, _), do: nil
140

141
142
143
144
145
146
147
  # Owned objects can only be mutated by their owner
  def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
    do: actor == ap_id

  # Legacy objects can be mutated by anybody
  def authorize_mutation(%Object{}, %User{}), do: true

lain's avatar
lain committed
148
149
150
151
152
153
154
155
156
157
158
159
  def get_cached_by_ap_id(ap_id) do
    key = "object:#{ap_id}"

    Cachex.fetch!(:object_cache, key, fn _ ->
      object = get_by_ap_id(ap_id)

      if object do
        {:commit, object}
      else
        {:ignore, object}
      end
    end)
lain's avatar
lain committed
160
161
  end

162
  def context_mapping(context) do
163
    Object.change(%Object{}, %{data: %{"id" => context}})
164
  end
165

166
167
168
169
  def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
    %ObjectTombstone{
      id: id,
      formerType: type,
170
171
      deleted: deleted
    }
172
    |> Map.from_struct()
173
174
  end

175
176
  def swap_object_with_tombstone(object) do
    tombstone = make_tombstone(object)
177
178
179
180
181
182

    object
    |> Object.change(%{data: tombstone})
    |> Repo.update()
  end

183
  def delete(%Object{data: %{"id" => id}} = object) do
184
    with {:ok, _obj} = swap_object_with_tombstone(object),
185
         deleted_activity = Activity.delete_all_by_object_ap_id(id),
minibikini's avatar
minibikini committed
186
         {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
187
188
189
190
191
192
193
194
         {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
      with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do
        {:ok, _} =
          Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
            "object" => object
          })
      end

195
      {:ok, object, deleted_activity}
196
197
    end
  end
lain's avatar
lain committed
198

kaniini's avatar
kaniini committed
199
200
  def prune(%Object{data: %{"id" => id}} = object) do
    with {:ok, object} <- Repo.delete(object),
minibikini's avatar
minibikini committed
201
202
         {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
         {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
kaniini's avatar
kaniini committed
203
204
205
206
      {:ok, object}
    end
  end

lain's avatar
lain committed
207
208
209
210
211
212
213
214
215
216
  def set_cache(%Object{data: %{"id" => ap_id}} = object) do
    Cachex.put(:object_cache, "object:#{ap_id}", object)
    {:ok, object}
  end

  def update_and_set_cache(changeset) do
    with {:ok, object} <- Repo.update(changeset) do
      set_cache(object)
    end
  end
217
218
219
220
221
222
223
224
225

  def increase_replies_count(ap_id) do
    Object
    |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
    |> update([o],
      set: [
        data:
          fragment(
            """
226
            safe_jsonb_set(?, '{repliesCount}',
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
              (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
            """,
            o.data,
            o.data
          )
      ]
    )
    |> Repo.update_all([])
    |> case do
      {1, [object]} -> set_cache(object)
      _ -> {:error, "Not found"}
    end
  end

  def decrease_replies_count(ap_id) do
    Object
    |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
    |> update([o],
      set: [
        data:
          fragment(
            """
249
            safe_jsonb_set(?, '{repliesCount}',
250
251
252
253
254
255
256
257
258
259
260
              (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
            """,
            o.data,
            o.data
          )
      ]
    )
    |> Repo.update_all([])
    |> case do
      {1, [object]} -> set_cache(object)
      _ -> {:error, "Not found"}
261
262
    end
  end
rinpatch's avatar
rinpatch committed
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292

  def increase_vote_count(ap_id, name) do
    with %Object{} = object <- Object.normalize(ap_id),
         "Question" <- object.data["type"] do
      multiple = Map.has_key?(object.data, "anyOf")

      options =
        (object.data["anyOf"] || object.data["oneOf"] || [])
        |> Enum.map(fn
          %{"name" => ^name} = option ->
            Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))

          option ->
            option
        end)

      data =
        if multiple do
          Map.put(object.data, "anyOf", options)
        else
          Map.put(object.data, "oneOf", options)
        end

      object
      |> Object.change(%{data: data})
      |> update_and_set_cache()
    else
      _ -> :noop
    end
  end
Maksim's avatar
Maksim committed
293
294
295
296
297
298
299

  @doc "Updates data field of an object"
  def update_data(%Object{data: data} = object, attrs \\ %{}) do
    object
    |> Object.change(%{data: Map.merge(data || %{}, attrs)})
    |> Repo.update()
  end
300
301
302
303

  def local?(%Object{data: %{"id" => id}}) do
    String.starts_with?(id, Pleroma.Web.base_url() <> "/")
  end
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325

  def replies(object, opts \\ []) do
    object = Object.normalize(object)

    query =
      Object
      |> where(
        [o],
        fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
      )
      |> order_by([o], asc: o.id)

    if opts[:self_only] do
      actor = object.data["actor"]
      where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
    else
      query
    end
  end

  def self_replies(object, opts \\ []),
    do: replies(object, Keyword.put(opts, :self_only, true))
lain's avatar
lain committed
326
end