Merge branch 'develop' into feature/database-compaction
[akkoma] / lib / pleroma / object.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 alias Pleroma.Activity
9 alias Pleroma.Object
10 alias Pleroma.Object.Fetcher
11 alias Pleroma.ObjectTombstone
12 alias Pleroma.Repo
13 alias Pleroma.User
14
15 import Ecto.Query
16 import Ecto.Changeset
17
18 require Logger
19
20 schema "objects" do
21 field(:data, :map)
22
23 timestamps()
24 end
25
26 def create(data) do
27 Object.change(%Object{}, %{data: data})
28 |> Repo.insert()
29 end
30
31 def change(struct, params \\ %{}) do
32 struct
33 |> cast(params, [:data])
34 |> validate_required([:data])
35 |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
36 end
37
38 def get_by_ap_id(nil), do: nil
39
40 def get_by_ap_id(ap_id) do
41 Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
42 end
43
44 def normalize(_, fetch_remote \\ true)
45 # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
46 # Use this whenever possible, especially when walking graphs in an O(N) loop!
47 def normalize(%Activity{object: %Object{} = object}, _), do: object
48
49 # A hack for fake activities
50 def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
51 %Object{id: "pleroma:fake_object_id", data: data}
52 end
53
54 # Catch and log Object.normalize() calls where the Activity's child object is not
55 # preloaded.
56 def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do
57 Logger.debug(
58 "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
59 )
60
61 Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
62
63 normalize(ap_id, fetch_remote)
64 end
65
66 def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do
67 Logger.debug(
68 "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
69 )
70
71 Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
72
73 normalize(ap_id, fetch_remote)
74 end
75
76 # Old way, try fetching the object through cache.
77 def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote)
78 def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
79 def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
80 def normalize(_, _), do: nil
81
82 # Owned objects can only be mutated by their owner
83 def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
84 do: actor == ap_id
85
86 # Legacy objects can be mutated by anybody
87 def authorize_mutation(%Object{}, %User{}), do: true
88
89 def get_cached_by_ap_id(ap_id) do
90 key = "object:#{ap_id}"
91
92 Cachex.fetch!(:object_cache, key, fn _ ->
93 object = get_by_ap_id(ap_id)
94
95 if object do
96 {:commit, object}
97 else
98 {:ignore, object}
99 end
100 end)
101 end
102
103 def context_mapping(context) do
104 Object.change(%Object{}, %{data: %{"id" => context}})
105 end
106
107 def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
108 %ObjectTombstone{
109 id: id,
110 formerType: type,
111 deleted: deleted
112 }
113 |> Map.from_struct()
114 end
115
116 def swap_object_with_tombstone(object) do
117 tombstone = make_tombstone(object)
118
119 object
120 |> Object.change(%{data: tombstone})
121 |> Repo.update()
122 end
123
124 def delete(%Object{data: %{"id" => id}} = object) do
125 with {:ok, _obj} = swap_object_with_tombstone(object),
126 deleted_activity = Activity.delete_by_ap_id(id),
127 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
128 {:ok, object, deleted_activity}
129 end
130 end
131
132 def set_cache(%Object{data: %{"id" => ap_id}} = object) do
133 Cachex.put(:object_cache, "object:#{ap_id}", object)
134 {:ok, object}
135 end
136
137 def update_and_set_cache(changeset) do
138 with {:ok, object} <- Repo.update(changeset) do
139 set_cache(object)
140 else
141 e -> e
142 end
143 end
144
145 def increase_replies_count(ap_id) do
146 Object
147 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
148 |> update([o],
149 set: [
150 data:
151 fragment(
152 """
153 jsonb_set(?, '{repliesCount}',
154 (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
155 """,
156 o.data,
157 o.data
158 )
159 ]
160 )
161 |> Repo.update_all([])
162 |> case do
163 {1, [object]} -> set_cache(object)
164 _ -> {:error, "Not found"}
165 end
166 end
167
168 def decrease_replies_count(ap_id) do
169 Object
170 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
171 |> update([o],
172 set: [
173 data:
174 fragment(
175 """
176 jsonb_set(?, '{repliesCount}',
177 (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
178 """,
179 o.data,
180 o.data
181 )
182 ]
183 )
184 |> Repo.update_all([])
185 |> case do
186 {1, [object]} -> set_cache(object)
187 _ -> {:error, "Not found"}
188 end
189 end
190 end