Merge branch 'issue/1411' into 'develop'
[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 with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
27 object_position = Map.get(query.aliases, :object, 0)
28
29 join(query, join_type, [{object, object_position}], a in Activity,
30 on:
31 fragment(
32 "COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
33 a.data,
34 a.data,
35 object.data,
36 a.data,
37 ^activity_type
38 ),
39 as: :object_activity
40 )
41 end
42
43 def create(data) do
44 Object.change(%Object{}, %{data: data})
45 |> Repo.insert()
46 end
47
48 def change(struct, params \\ %{}) do
49 struct
50 |> cast(params, [:data])
51 |> validate_required([:data])
52 |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
53 end
54
55 def get_by_id(nil), do: nil
56 def get_by_id(id), do: Repo.get(Object, id)
57
58 def get_by_id_and_maybe_refetch(id, opts \\ []) do
59 %{updated_at: updated_at} = object = get_by_id(id)
60
61 if opts[:interval] &&
62 NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
63 case Fetcher.refetch_object(object) do
64 {:ok, %Object{} = object} ->
65 object
66
67 e ->
68 Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
69 object
70 end
71 else
72 object
73 end
74 end
75
76 def get_by_ap_id(nil), do: nil
77
78 def get_by_ap_id(ap_id) do
79 Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
80 end
81
82 defp warn_on_no_object_preloaded(ap_id) do
83 "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
84 |> Logger.debug()
85
86 Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
87 end
88
89 def normalize(_, fetch_remote \\ true, options \\ [])
90
91 # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
92 # Use this whenever possible, especially when walking graphs in an O(N) loop!
93 def normalize(%Object{} = object, _, _), do: object
94 def normalize(%Activity{object: %Object{} = object}, _, _), do: object
95
96 # A hack for fake activities
97 def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
98 %Object{id: "pleroma:fake_object_id", data: data}
99 end
100
101 # No preloaded object
102 def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
103 warn_on_no_object_preloaded(ap_id)
104 normalize(ap_id, fetch_remote)
105 end
106
107 # No preloaded object
108 def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
109 warn_on_no_object_preloaded(ap_id)
110 normalize(ap_id, fetch_remote)
111 end
112
113 # Old way, try fetching the object through cache.
114 def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
115 def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
116
117 def normalize(ap_id, true, options) when is_binary(ap_id) do
118 Fetcher.fetch_object_from_id!(ap_id, options)
119 end
120
121 def normalize(_, _, _), do: nil
122
123 # Owned objects can only be mutated by their owner
124 def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
125 do: actor == ap_id
126
127 # Legacy objects can be mutated by anybody
128 def authorize_mutation(%Object{}, %User{}), do: true
129
130 def get_cached_by_ap_id(ap_id) do
131 key = "object:#{ap_id}"
132
133 Cachex.fetch!(:object_cache, key, fn _ ->
134 object = get_by_ap_id(ap_id)
135
136 if object do
137 {:commit, object}
138 else
139 {:ignore, object}
140 end
141 end)
142 end
143
144 def context_mapping(context) do
145 Object.change(%Object{}, %{data: %{"id" => context}})
146 end
147
148 def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
149 %ObjectTombstone{
150 id: id,
151 formerType: type,
152 deleted: deleted
153 }
154 |> Map.from_struct()
155 end
156
157 def swap_object_with_tombstone(object) do
158 tombstone = make_tombstone(object)
159
160 object
161 |> Object.change(%{data: tombstone})
162 |> Repo.update()
163 end
164
165 def delete(%Object{data: %{"id" => id}} = object) do
166 with {:ok, _obj} = swap_object_with_tombstone(object),
167 deleted_activity = Activity.delete_all_by_object_ap_id(id),
168 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
169 {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
170 {:ok, object, deleted_activity}
171 end
172 end
173
174 def prune(%Object{data: %{"id" => id}} = object) do
175 with {:ok, object} <- Repo.delete(object),
176 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
177 {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
178 {:ok, object}
179 end
180 end
181
182 def set_cache(%Object{data: %{"id" => ap_id}} = object) do
183 Cachex.put(:object_cache, "object:#{ap_id}", object)
184 {:ok, object}
185 end
186
187 def update_and_set_cache(changeset) do
188 with {:ok, object} <- Repo.update(changeset) do
189 set_cache(object)
190 end
191 end
192
193 def increase_replies_count(ap_id) do
194 Object
195 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
196 |> update([o],
197 set: [
198 data:
199 fragment(
200 """
201 safe_jsonb_set(?, '{repliesCount}',
202 (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
203 """,
204 o.data,
205 o.data
206 )
207 ]
208 )
209 |> Repo.update_all([])
210 |> case do
211 {1, [object]} -> set_cache(object)
212 _ -> {:error, "Not found"}
213 end
214 end
215
216 def decrease_replies_count(ap_id) do
217 Object
218 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
219 |> update([o],
220 set: [
221 data:
222 fragment(
223 """
224 safe_jsonb_set(?, '{repliesCount}',
225 (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
226 """,
227 o.data,
228 o.data
229 )
230 ]
231 )
232 |> Repo.update_all([])
233 |> case do
234 {1, [object]} -> set_cache(object)
235 _ -> {:error, "Not found"}
236 end
237 end
238
239 def increase_vote_count(ap_id, name) do
240 with %Object{} = object <- Object.normalize(ap_id),
241 "Question" <- object.data["type"] do
242 multiple = Map.has_key?(object.data, "anyOf")
243
244 options =
245 (object.data["anyOf"] || object.data["oneOf"] || [])
246 |> Enum.map(fn
247 %{"name" => ^name} = option ->
248 Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
249
250 option ->
251 option
252 end)
253
254 data =
255 if multiple do
256 Map.put(object.data, "anyOf", options)
257 else
258 Map.put(object.data, "oneOf", options)
259 end
260
261 object
262 |> Object.change(%{data: data})
263 |> update_and_set_cache()
264 else
265 _ -> :noop
266 end
267 end
268
269 @doc "Updates data field of an object"
270 def update_data(%Object{data: data} = object, attrs \\ %{}) do
271 object
272 |> Object.change(%{data: Map.merge(data || %{}, attrs)})
273 |> Repo.update()
274 end
275
276 def local?(%Object{data: %{"id" => id}}) do
277 String.starts_with?(id, Pleroma.Web.base_url() <> "/")
278 end
279 end