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