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