fix formatting
[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, options \\ [])
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
82 def normalize(ap_id, true, options) when is_binary(ap_id) do
83 Fetcher.fetch_object_from_id!(ap_id, options)
84 end
85
86 def normalize(_, _, _), do: nil
87
88 # Owned objects can only be mutated by their owner
89 def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
90 do: actor == ap_id
91
92 # Legacy objects can be mutated by anybody
93 def authorize_mutation(%Object{}, %User{}), do: true
94
95 def get_cached_by_ap_id(ap_id) do
96 key = "object:#{ap_id}"
97
98 Cachex.fetch!(:object_cache, key, fn _ ->
99 object = get_by_ap_id(ap_id)
100
101 if object do
102 {:commit, object}
103 else
104 {:ignore, object}
105 end
106 end)
107 end
108
109 def context_mapping(context) do
110 Object.change(%Object{}, %{data: %{"id" => context}})
111 end
112
113 def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
114 %ObjectTombstone{
115 id: id,
116 formerType: type,
117 deleted: deleted
118 }
119 |> Map.from_struct()
120 end
121
122 def swap_object_with_tombstone(object) do
123 tombstone = make_tombstone(object)
124
125 object
126 |> Object.change(%{data: tombstone})
127 |> Repo.update()
128 end
129
130 def delete(%Object{data: %{"id" => id}} = object) do
131 with {:ok, _obj} = swap_object_with_tombstone(object),
132 deleted_activity = Activity.delete_by_ap_id(id),
133 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
134 {:ok, object, deleted_activity}
135 end
136 end
137
138 def prune(%Object{data: %{"id" => id}} = object) do
139 with {:ok, object} <- Repo.delete(object),
140 {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
141 {:ok, object}
142 end
143 end
144
145 def set_cache(%Object{data: %{"id" => ap_id}} = object) do
146 Cachex.put(:object_cache, "object:#{ap_id}", object)
147 {:ok, object}
148 end
149
150 def update_and_set_cache(changeset) do
151 with {:ok, object} <- Repo.update(changeset) do
152 set_cache(object)
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
232 @doc "Updates data field of an object"
233 def update_data(%Object{data: data} = object, attrs \\ %{}) do
234 object
235 |> Object.change(%{data: Map.merge(data || %{}, attrs)})
236 |> Repo.update()
237 end
238 end