3ffa290ebee33fe40ece3bba9347409a288cdc02
[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 set_cache(%Object{data: %{"id" => ap_id}} = object) do
137 Cachex.put(:object_cache, "object:#{ap_id}", object)
138 {:ok, object}
139 end
140
141 def update_and_set_cache(changeset) do
142 with {:ok, object} <- Repo.update(changeset) do
143 set_cache(object)
144 else
145 e -> e
146 end
147 end
148
149 def increase_replies_count(ap_id) do
150 Object
151 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
152 |> update([o],
153 set: [
154 data:
155 fragment(
156 """
157 jsonb_set(?, '{repliesCount}',
158 (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
159 """,
160 o.data,
161 o.data
162 )
163 ]
164 )
165 |> Repo.update_all([])
166 |> case do
167 {1, [object]} -> set_cache(object)
168 _ -> {:error, "Not found"}
169 end
170 end
171
172 def decrease_replies_count(ap_id) do
173 Object
174 |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
175 |> update([o],
176 set: [
177 data:
178 fragment(
179 """
180 jsonb_set(?, '{repliesCount}',
181 (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
182 """,
183 o.data,
184 o.data
185 )
186 ]
187 )
188 |> Repo.update_all([])
189 |> case do
190 {1, [object]} -> set_cache(object)
191 _ -> {:error, "Not found"}
192 end
193 end
194
195 def increase_vote_count(ap_id, name) do
196 with %Object{} = object <- Object.normalize(ap_id),
197 "Question" <- object.data["type"] do
198 multiple = Map.has_key?(object.data, "anyOf")
199
200 options =
201 (object.data["anyOf"] || object.data["oneOf"] || [])
202 |> Enum.map(fn
203 %{"name" => ^name} = option ->
204 Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
205
206 option ->
207 option
208 end)
209
210 data =
211 if multiple do
212 Map.put(object.data, "anyOf", options)
213 else
214 Map.put(object.data, "oneOf", options)
215 end
216
217 object
218 |> Object.change(%{data: data})
219 |> update_and_set_cache()
220 else
221 _ -> :noop
222 end
223 end
224 end