Merge branch 'develop' into feature/polls-2-electric-boogalo
[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)
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 def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
84 def normalize(_, _), do: nil
85
86 # Owned objects can only be mutated by their owner
87 def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
88 do: actor == ap_id
89
90 # Legacy objects can be mutated by anybody
91 def authorize_mutation(%Object{}, %User{}), do: true
92
93 def get_cached_by_ap_id(ap_id) do
94 key = "object:#{ap_id}"
95
96 Cachex.fetch!(:object_cache, key, fn _ ->
97 object = get_by_ap_id(ap_id)
98
99 if object do
100 {:commit, object}
101 else
102 {:ignore, object}
103 end
104 end)
105 end
106
107 def context_mapping(context) do
108 Object.change(%Object{}, %{data: %{"id" => context}})
109 end
110
111 def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
112 %ObjectTombstone{
113 id: id,
114 formerType: type,
115 deleted: deleted
116 }
117 |> Map.from_struct()
118 end
119
120 def swap_object_with_tombstone(object) do
121 tombstone = make_tombstone(object)
122
123 object
124 |> Object.change(%{data: tombstone})
125 |> Repo.update()
126 end
127
128 def delete(%Object{data: %{"id" => id}} = object) do
129 with {:ok, _obj} = swap_object_with_tombstone(object),
130 deleted_activity = Activity.delete_by_ap_id(id),
131 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
132 {:ok, object, deleted_activity}
133 end
134 end
135
136 def prune(%Object{data: %{"id" => id}} = object) do
137 with {:ok, object} <- Repo.delete(object),
138 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
139 {:ok, object}
140 end
141 end
142
143 def set_cache(%Object{data: %{"id" => ap_id}} = object) do
144 Cachex.put(:object_cache, "object:#{ap_id}", object)
145 {:ok, object}
146 end
147
148 def update_and_set_cache(changeset) do
149 with {:ok, object} <- Repo.update(changeset) do
150 set_cache(object)
151 else
152 e -> e
153 end
154 end
155
156 def increase_replies_count(ap_id) do
157 Object
158 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
159 |> update([o],
160 set: [
161 data:
162 fragment(
163 """
164 jsonb_set(?, '{repliesCount}',
165 (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
166 """,
167 o.data,
168 o.data
169 )
170 ]
171 )
172 |> Repo.update_all([])
173 |> case do
174 {1, [object]} -> set_cache(object)
175 _ -> {:error, "Not found"}
176 end
177 end
178
179 def decrease_replies_count(ap_id) do
180 Object
181 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
182 |> update([o],
183 set: [
184 data:
185 fragment(
186 """
187 jsonb_set(?, '{repliesCount}',
188 (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
189 """,
190 o.data,
191 o.data
192 )
193 ]
194 )
195 |> Repo.update_all([])
196 |> case do
197 {1, [object]} -> set_cache(object)
198 _ -> {:error, "Not found"}
199 end
200 end
201
202 def increase_vote_count(ap_id, name) do
203 with %Object{} = object <- Object.normalize(ap_id),
204 "Question" <- object.data["type"] do
205 multiple = Map.has_key?(object.data, "anyOf")
206
207 options =
208 (object.data["anyOf"] || object.data["oneOf"] || [])
209 |> Enum.map(fn
210 %{"name" => ^name} = option ->
211 Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
212
213 option ->
214 option
215 end)
216
217 data =
218 if multiple do
219 Map.put(object.data, "anyOf", options)
220 else
221 Map.put(object.data, "oneOf", options)
222 end
223
224 object
225 |> Object.change(%{data: data})
226 |> update_and_set_cache()
227 else
228 _ -> :noop
229 end
230 end
231 end