Merge branch 'fix/minor-doc-fix' into 'develop'
[akkoma] / lib / pleroma / activity.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Activity do
6 use Ecto.Schema
7
8 alias Pleroma.Activity
9 alias Pleroma.Activity.Queries
10 alias Pleroma.Bookmark
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.Repo
14 alias Pleroma.ReportNote
15 alias Pleroma.ThreadMute
16 alias Pleroma.User
17 alias Pleroma.Web.ActivityPub.ActivityPub
18
19 import Ecto.Changeset
20 import Ecto.Query
21
22 @type t :: %__MODULE__{}
23 @type actor :: String.t()
24
25 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
26
27 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
28
29 schema "activities" do
30 field(:data, :map)
31 field(:local, :boolean, default: true)
32 field(:actor, :string)
33 field(:recipients, {:array, :string}, default: [])
34 field(:thread_muted?, :boolean, virtual: true)
35
36 # A field that can be used if you need to join some kind of other
37 # id to order / paginate this field by
38 field(:pagination_id, :string, virtual: true)
39
40 # This is a fake relation,
41 # do not use outside of with_preloaded_user_actor/with_joined_user_actor
42 has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
43 # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
44 has_one(:bookmark, Bookmark)
45 # This is a fake relation, do not use outside of with_preloaded_report_notes
46 has_many(:report_notes, ReportNote)
47 has_many(:notifications, Notification, on_delete: :delete_all)
48
49 # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
50 # The foreign key is embedded in a jsonb field.
51 #
52 # To use it, you probably want to do an inner join and a preload:
53 #
54 # ```
55 # |> join(:inner, [activity], o in Object,
56 # on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
57 # o.data, activity.data, activity.data))
58 # |> preload([activity, object], [object: object])
59 # ```
60 #
61 # As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
62 # typical case.
63 has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
64
65 timestamps()
66 end
67
68 def with_joined_object(query, join_type \\ :inner) do
69 join(query, join_type, [activity], o in Object,
70 on:
71 fragment(
72 "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
73 o.data,
74 activity.data,
75 activity.data
76 ),
77 as: :object
78 )
79 end
80
81 def with_preloaded_object(query, join_type \\ :inner) do
82 query
83 |> has_named_binding?(:object)
84 |> if(do: query, else: with_joined_object(query, join_type))
85 |> preload([activity, object: object], object: object)
86 end
87
88 # Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
89 def user_actor(%Activity{actor: nil}), do: nil
90
91 def user_actor(%Activity{} = activity) do
92 with %User{} <- activity.user_actor do
93 activity.user_actor
94 else
95 _ -> User.get_cached_by_ap_id(activity.actor)
96 end
97 end
98
99 def with_joined_user_actor(query, join_type \\ :inner) do
100 join(query, join_type, [activity], u in User,
101 on: u.ap_id == activity.actor,
102 as: :user_actor
103 )
104 end
105
106 def with_preloaded_user_actor(query, join_type \\ :inner) do
107 query
108 |> with_joined_user_actor(join_type)
109 |> preload([activity, user_actor: user_actor], user_actor: user_actor)
110 end
111
112 def with_preloaded_bookmark(query, %User{} = user) do
113 from([a] in query,
114 left_join: b in Bookmark,
115 on: b.user_id == ^user.id and b.activity_id == a.id,
116 preload: [bookmark: b]
117 )
118 end
119
120 def with_preloaded_bookmark(query, _), do: query
121
122 def with_preloaded_report_notes(query) do
123 from([a] in query,
124 left_join: r in ReportNote,
125 on: a.id == r.activity_id,
126 preload: [report_notes: r]
127 )
128 end
129
130 def with_preloaded_report_notes(query, _), do: query
131
132 def with_set_thread_muted_field(query, %User{} = user) do
133 from([a] in query,
134 left_join: tm in ThreadMute,
135 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
136 as: :thread_mute,
137 select: %Activity{a | thread_muted?: not is_nil(tm.id)}
138 )
139 end
140
141 def with_set_thread_muted_field(query, _), do: query
142
143 def get_by_ap_id(ap_id) do
144 ap_id
145 |> Queries.by_ap_id()
146 |> Repo.one()
147 end
148
149 def get_bookmark(%Activity{} = activity, %User{} = user) do
150 if Ecto.assoc_loaded?(activity.bookmark) do
151 activity.bookmark
152 else
153 Bookmark.get(user.id, activity.id)
154 end
155 end
156
157 def get_bookmark(_, _), do: nil
158
159 def get_report(activity_id) do
160 opts = %{
161 type: "Flag",
162 skip_preload: true,
163 preload_report_notes: true
164 }
165
166 ActivityPub.fetch_activities_query([], opts)
167 |> where(id: ^activity_id)
168 |> Repo.one()
169 end
170
171 def change(struct, params \\ %{}) do
172 struct
173 |> cast(params, [:data, :recipients])
174 |> validate_required([:data])
175 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
176 end
177
178 def get_by_ap_id_with_object(ap_id) do
179 ap_id
180 |> Queries.by_ap_id()
181 |> with_preloaded_object(:left)
182 |> Repo.one()
183 end
184
185 @spec get_by_id(String.t()) :: Activity.t() | nil
186 def get_by_id(id) do
187 case FlakeId.flake_id?(id) do
188 true ->
189 Activity
190 |> where([a], a.id == ^id)
191 |> restrict_deactivated_users()
192 |> Repo.one()
193
194 _ ->
195 nil
196 end
197 end
198
199 def get_by_id_with_user_actor(id) do
200 case FlakeId.flake_id?(id) do
201 true ->
202 Activity
203 |> where([a], a.id == ^id)
204 |> with_preloaded_user_actor()
205 |> Repo.one()
206
207 _ ->
208 nil
209 end
210 end
211
212 def get_by_id_with_object(id) do
213 Activity
214 |> where(id: ^id)
215 |> with_preloaded_object()
216 |> Repo.one()
217 end
218
219 def all_by_ids_with_object(ids) do
220 Activity
221 |> where([a], a.id in ^ids)
222 |> with_preloaded_object()
223 |> Repo.all()
224 end
225
226 @doc """
227 Accepts `ap_id` or list of `ap_id`.
228 Returns a query.
229 """
230 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
231 def create_by_object_ap_id(ap_id) do
232 ap_id
233 |> Queries.by_object_id()
234 |> Queries.by_type("Create")
235 end
236
237 def get_all_create_by_object_ap_id(ap_id) do
238 ap_id
239 |> create_by_object_ap_id()
240 |> Repo.all()
241 end
242
243 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
244 create_by_object_ap_id(ap_id)
245 |> restrict_deactivated_users()
246 |> Repo.one()
247 end
248
249 def get_create_by_object_ap_id(_), do: nil
250
251 @doc """
252 Accepts `ap_id` or list of `ap_id`.
253 Returns a query.
254 """
255 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
256 def create_by_object_ap_id_with_object(ap_id) do
257 ap_id
258 |> create_by_object_ap_id()
259 |> with_preloaded_object()
260 end
261
262 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
263 ap_id
264 |> create_by_object_ap_id_with_object()
265 |> Repo.one()
266 end
267
268 def get_create_by_object_ap_id_with_object(_), do: nil
269
270 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
271 get_create_by_object_ap_id_with_object(ap_id)
272 end
273
274 defp get_in_reply_to_activity_from_object(_), do: nil
275
276 def get_in_reply_to_activity(%Activity{} = activity) do
277 get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false))
278 end
279
280 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
281 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
282 def normalize(_), do: nil
283
284 def delete_all_by_object_ap_id(id) when is_binary(id) do
285 id
286 |> Queries.by_object_id()
287 |> Queries.exclude_type("Delete")
288 |> select([u], u)
289 |> Repo.delete_all()
290 |> elem(1)
291 |> Enum.find(fn
292 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
293 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
294 _ -> nil
295 end)
296 |> purge_web_resp_cache()
297 end
298
299 def delete_all_by_object_ap_id(_), do: nil
300
301 defp purge_web_resp_cache(%Activity{} = activity) do
302 %{path: path} = URI.parse(activity.data["id"])
303 @cachex.del(:web_resp_cache, path)
304 activity
305 end
306
307 defp purge_web_resp_cache(nil), do: nil
308
309 def follow_accepted?(
310 %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
311 ) do
312 with %User{} = follower <- Activity.user_actor(activity),
313 %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
314 Pleroma.FollowingRelationship.following?(follower, followed)
315 else
316 _ -> false
317 end
318 end
319
320 def follow_accepted?(_), do: false
321
322 def all_by_actor_and_id(actor, status_ids \\ [])
323 def all_by_actor_and_id(_actor, []), do: []
324
325 def all_by_actor_and_id(actor, status_ids) do
326 Activity
327 |> where([s], s.id in ^status_ids)
328 |> where([s], s.actor == ^actor)
329 |> Repo.all()
330 end
331
332 def follow_requests_for_actor(%User{ap_id: ap_id}) do
333 ap_id
334 |> Queries.by_object_id()
335 |> Queries.by_type("Follow")
336 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
337 end
338
339 def following_requests_for_actor(%User{ap_id: ap_id}) do
340 Queries.by_type("Follow")
341 |> where([a], fragment("?->>'state' = 'pending'", a.data))
342 |> where([a], a.actor == ^ap_id)
343 |> Repo.all()
344 end
345
346 def restrict_deactivated_users(query) do
347 deactivated_users =
348 from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
349 |> Repo.all()
350
351 Activity.Queries.exclude_authors(query, deactivated_users)
352 end
353
354 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
355
356 def direct_conversation_id(activity, for_user) do
357 alias Pleroma.Conversation.Participation
358
359 with %{data: %{"context" => context}} when is_binary(context) <- activity,
360 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
361 %Participation{id: participation_id} <-
362 Participation.for_user_and_conversation(for_user, conversation) do
363 participation_id
364 else
365 _ -> nil
366 end
367 end
368
369 @spec pinned_by_actor?(Activity.t()) :: boolean()
370 def pinned_by_actor?(%Activity{} = activity) do
371 actor = user_actor(activity)
372 activity.id in actor.pinned_activities
373 end
374
375 @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil
376 def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
377 ap_id
378 |> Queries.by_object_id()
379 |> with_preloaded_object()
380 |> first()
381 |> Repo.one()
382 end
383
384 def get_by_object_ap_id_with_object(_), do: nil
385 end