[#161] Limited replies depth on incoming federation in order to prevent memory leaks...
[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_id(nil), do: nil
39 def get_by_id(id), do: Repo.get(Object, id)
40
41 def get_by_ap_id(nil), do: nil
42
43 def get_by_ap_id(ap_id) do
44 Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
45 end
46
47 def normalize(_, fetch_remote \\ true, options \\ [])
48 # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
49 # Use this whenever possible, especially when walking graphs in an O(N) loop!
50 def normalize(%Object{} = object, _, _), do: object
51 def normalize(%Activity{object: %Object{} = object}, _, _), do: object
52
53 # A hack for fake activities
54 def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
55 %Object{id: "pleroma:fake_object_id", data: data}
56 end
57
58 # Catch and log Object.normalize() calls where the Activity's child object is not
59 # preloaded.
60 def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
61 Logger.debug(
62 "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
63 )
64
65 Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
66
67 normalize(ap_id, fetch_remote)
68 end
69
70 def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
71 Logger.debug(
72 "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!"
73 )
74
75 Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
76
77 normalize(ap_id, fetch_remote)
78 end
79
80 # Old way, try fetching the object through cache.
81 def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
82 def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
83
84 def normalize(ap_id, true, options) when is_binary(ap_id) do
85 Fetcher.fetch_object_from_id!(ap_id, options)
86 end
87
88 def normalize(_, _, _), do: nil
89
90 # Owned objects can only be mutated by their owner
91 def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
92 do: actor == ap_id
93
94 # Legacy objects can be mutated by anybody
95 def authorize_mutation(%Object{}, %User{}), do: true
96
97 def get_cached_by_ap_id(ap_id) do
98 key = "object:#{ap_id}"
99
100 Cachex.fetch!(:object_cache, key, fn _ ->
101 object = get_by_ap_id(ap_id)
102
103 if object do
104 {:commit, object}
105 else
106 {:ignore, object}
107 end
108 end)
109 end
110
111 def context_mapping(context) do
112 Object.change(%Object{}, %{data: %{"id" => context}})
113 end
114
115 def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
116 %ObjectTombstone{
117 id: id,
118 formerType: type,
119 deleted: deleted
120 }
121 |> Map.from_struct()
122 end
123
124 def swap_object_with_tombstone(object) do
125 tombstone = make_tombstone(object)
126
127 object
128 |> Object.change(%{data: tombstone})
129 |> Repo.update()
130 end
131
132 def delete(%Object{data: %{"id" => id}} = object) do
133 with {:ok, _obj} = swap_object_with_tombstone(object),
134 deleted_activity = Activity.delete_by_ap_id(id),
135 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
136 {:ok, object, deleted_activity}
137 end
138 end
139
140 def prune(%Object{data: %{"id" => id}} = object) do
141 with {:ok, object} <- Repo.delete(object),
142 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
143 {:ok, object}
144 end
145 end
146
147 def set_cache(%Object{data: %{"id" => ap_id}} = object) do
148 Cachex.put(:object_cache, "object:#{ap_id}", object)
149 {:ok, object}
150 end
151
152 def update_and_set_cache(changeset) do
153 with {:ok, object} <- Repo.update(changeset) do
154 set_cache(object)
155 else
156 e -> e
157 end
158 end
159
160 def increase_replies_count(ap_id) do
161 Object
162 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
163 |> update([o],
164 set: [
165 data:
166 fragment(
167 """
168 jsonb_set(?, '{repliesCount}',
169 (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
170 """,
171 o.data,
172 o.data
173 )
174 ]
175 )
176 |> Repo.update_all([])
177 |> case do
178 {1, [object]} -> set_cache(object)
179 _ -> {:error, "Not found"}
180 end
181 end
182
183 def decrease_replies_count(ap_id) do
184 Object
185 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
186 |> update([o],
187 set: [
188 data:
189 fragment(
190 """
191 jsonb_set(?, '{repliesCount}',
192 (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
193 """,
194 o.data,
195 o.data
196 )
197 ]
198 )
199 |> Repo.update_all([])
200 |> case do
201 {1, [object]} -> set_cache(object)
202 _ -> {:error, "Not found"}
203 end
204 end
205
206 def increase_vote_count(ap_id, name) do
207 with %Object{} = object <- Object.normalize(ap_id),
208 "Question" <- object.data["type"] do
209 multiple = Map.has_key?(object.data, "anyOf")
210
211 options =
212 (object.data["anyOf"] || object.data["oneOf"] || [])
213 |> Enum.map(fn
214 %{"name" => ^name} = option ->
215 Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
216
217 option ->
218 option
219 end)
220
221 data =
222 if multiple do
223 Map.put(object.data, "anyOf", options)
224 else
225 Map.put(object.data, "oneOf", options)
226 end
227
228 object
229 |> Object.change(%{data: data})
230 |> update_and_set_cache()
231 else
232 _ -> :noop
233 end
234 end
235 end