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