3556aaf9e00b558e9a59b418b433d4395fad8d1a
[anni] / lib / pleroma / activity.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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') = associated_object_id((?))",
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') = associated_object_id(?)",
73           o.data,
74           activity.data
75         ),
76       as: :object
77     )
78   end
79
80   def with_preloaded_object(query, join_type \\ :inner) do
81     query
82     |> has_named_binding?(:object)
83     |> if(do: query, else: with_joined_object(query, join_type))
84     |> preload([activity, object: object], object: object)
85   end
86
87   # Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
88   def user_actor(%Activity{actor: nil}), do: nil
89
90   def user_actor(%Activity{} = activity) do
91     with %User{} <- activity.user_actor do
92       activity.user_actor
93     else
94       _ -> User.get_cached_by_ap_id(activity.actor)
95     end
96   end
97
98   def with_joined_user_actor(query, join_type \\ :inner) do
99     join(query, join_type, [activity], u in User,
100       on: u.ap_id == activity.actor,
101       as: :user_actor
102     )
103   end
104
105   def with_preloaded_user_actor(query, join_type \\ :inner) do
106     query
107     |> with_joined_user_actor(join_type)
108     |> preload([activity, user_actor: user_actor], user_actor: user_actor)
109   end
110
111   def with_preloaded_bookmark(query, %User{} = user) do
112     from([a] in query,
113       left_join: b in Bookmark,
114       on: b.user_id == ^user.id and b.activity_id == a.id,
115       as: :bookmark,
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       as: :report_note,
127       preload: [report_notes: r]
128     )
129   end
130
131   def with_preloaded_report_notes(query, _), do: query
132
133   def with_set_thread_muted_field(query, %User{} = user) do
134     from([a] in query,
135       left_join: tm in ThreadMute,
136       on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
137       as: :thread_mute,
138       select: %Activity{a | thread_muted?: not is_nil(tm.id)}
139     )
140   end
141
142   def with_set_thread_muted_field(query, _), do: query
143
144   def get_by_ap_id(ap_id) do
145     ap_id
146     |> Queries.by_ap_id()
147     |> Repo.one()
148   end
149
150   def get_bookmark(%Activity{} = activity, %User{} = user) do
151     if Ecto.assoc_loaded?(activity.bookmark) do
152       activity.bookmark
153     else
154       Bookmark.get(user.id, activity.id)
155     end
156   end
157
158   def get_bookmark(_, _), do: nil
159
160   def get_report(activity_id) do
161     opts = %{
162       type: "Flag",
163       skip_preload: true,
164       preload_report_notes: true
165     }
166
167     ActivityPub.fetch_activities_query([], opts)
168     |> where(id: ^activity_id)
169     |> Repo.one()
170   end
171
172   def change(struct, params \\ %{}) do
173     struct
174     |> cast(params, [:data, :recipients])
175     |> validate_required([:data])
176     |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
177   end
178
179   def get_by_ap_id_with_object(ap_id) do
180     ap_id
181     |> Queries.by_ap_id()
182     |> with_preloaded_object(:left)
183     |> Repo.one()
184   end
185
186   @doc """
187   Gets activity by ID, doesn't load activities from deactivated actors by default.
188   """
189   @spec get_by_id(String.t(), keyword()) :: t() | nil
190   def get_by_id(id, opts \\ [filter: [:restrict_deactivated]]), do: get_by_id_with_opts(id, opts)
191
192   @spec get_by_id_with_user_actor(String.t()) :: t() | nil
193   def get_by_id_with_user_actor(id), do: get_by_id_with_opts(id, preload: [:user_actor])
194
195   @spec get_by_id_with_object(String.t()) :: t() | nil
196   def get_by_id_with_object(id), do: get_by_id_with_opts(id, preload: [:object])
197
198   defp get_by_id_with_opts(id, opts) do
199     if FlakeId.flake_id?(id) do
200       query = Queries.by_id(id)
201
202       with_filters_query =
203         if is_list(opts[:filter]) do
204           Enum.reduce(opts[:filter], query, fn
205             {:type, type}, acc -> Queries.by_type(acc, type)
206             :restrict_deactivated, acc -> restrict_deactivated_users(acc)
207             _, acc -> acc
208           end)
209         else
210           query
211         end
212
213       with_preloads_query =
214         if is_list(opts[:preload]) do
215           Enum.reduce(opts[:preload], with_filters_query, fn
216             :user_actor, acc -> with_preloaded_user_actor(acc)
217             :object, acc -> with_preloaded_object(acc)
218             _, acc -> acc
219           end)
220         else
221           with_filters_query
222         end
223
224       Repo.one(with_preloads_query)
225     end
226   end
227
228   def all_by_ids_with_object(ids) do
229     Activity
230     |> where([a], a.id in ^ids)
231     |> with_preloaded_object()
232     |> Repo.all()
233   end
234
235   @doc """
236   Accepts `ap_id` or list of `ap_id`.
237   Returns a query.
238   """
239   @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
240   def create_by_object_ap_id(ap_id) do
241     ap_id
242     |> Queries.by_object_id()
243     |> Queries.by_type("Create")
244   end
245
246   def get_all_create_by_object_ap_id(ap_id) do
247     ap_id
248     |> create_by_object_ap_id()
249     |> Repo.all()
250   end
251
252   def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
253     create_by_object_ap_id(ap_id)
254     |> restrict_deactivated_users()
255     |> Repo.one()
256   end
257
258   def get_create_by_object_ap_id(_), do: nil
259
260   @doc """
261   Accepts `ap_id` or list of `ap_id`.
262   Returns a query.
263   """
264   @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
265   def create_by_object_ap_id_with_object(ap_id) do
266     ap_id
267     |> create_by_object_ap_id()
268     |> with_preloaded_object()
269   end
270
271   def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
272     ap_id
273     |> create_by_object_ap_id_with_object()
274     |> Repo.one()
275   end
276
277   def get_create_by_object_ap_id_with_object(_), do: nil
278
279   @spec create_by_id_with_object(String.t()) :: t() | nil
280   def create_by_id_with_object(id) do
281     get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"])
282   end
283
284   defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
285     get_create_by_object_ap_id_with_object(ap_id)
286   end
287
288   defp get_in_reply_to_activity_from_object(_), do: nil
289
290   def get_in_reply_to_activity(%Activity{} = activity) do
291     get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false))
292   end
293
294   def normalize(%Activity{data: %{"id" => ap_id}}), do: get_by_ap_id_with_object(ap_id)
295   def normalize(%{"id" => ap_id}), do: get_by_ap_id_with_object(ap_id)
296   def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
297   def normalize(_), do: nil
298
299   def delete_all_by_object_ap_id(id) when is_binary(id) do
300     id
301     |> Queries.by_object_id()
302     |> Queries.exclude_type("Delete")
303     |> select([u], u)
304     |> Repo.delete_all(timeout: :infinity)
305     |> elem(1)
306     |> Enum.find(fn
307       %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
308       %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
309       _ -> nil
310     end)
311     |> purge_web_resp_cache()
312   end
313
314   def delete_all_by_object_ap_id(_), do: nil
315
316   defp purge_web_resp_cache(%Activity{data: %{"id" => id}} = activity) when is_binary(id) do
317     with %{path: path} <- URI.parse(id) do
318       @cachex.del(:web_resp_cache, path)
319     end
320
321     activity
322   end
323
324   defp purge_web_resp_cache(activity), do: activity
325
326   def follow_accepted?(
327         %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
328       ) do
329     with %User{} = follower <- Activity.user_actor(activity),
330          %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
331       Pleroma.FollowingRelationship.following?(follower, followed)
332     else
333       _ -> false
334     end
335   end
336
337   def follow_accepted?(_), do: false
338
339   def all_by_actor_and_id(actor, status_ids \\ [])
340   def all_by_actor_and_id(_actor, []), do: []
341
342   def all_by_actor_and_id(actor, status_ids) do
343     Activity
344     |> where([s], s.id in ^status_ids)
345     |> where([s], s.actor == ^actor)
346     |> Repo.all()
347   end
348
349   def follow_requests_for_actor(%User{ap_id: ap_id}) do
350     ap_id
351     |> Queries.by_object_id()
352     |> Queries.by_type("Follow")
353     |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
354   end
355
356   def following_requests_for_actor(%User{ap_id: ap_id}) do
357     Queries.by_type("Follow")
358     |> where([a], fragment("?->>'state' = 'pending'", a.data))
359     |> where([a], a.actor == ^ap_id)
360     |> Repo.all()
361   end
362
363   def restrict_deactivated_users(query) do
364     query
365     |> join(:inner, [activity], user in User,
366       as: :user,
367       on: activity.actor == user.ap_id and user.is_active == true
368     )
369   end
370
371   defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
372
373   def direct_conversation_id(activity, for_user) do
374     alias Pleroma.Conversation.Participation
375
376     with %{data: %{"context" => context}} when is_binary(context) <- activity,
377          %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
378          %Participation{id: participation_id} <-
379            Participation.for_user_and_conversation(for_user, conversation) do
380       participation_id
381     else
382       _ -> nil
383     end
384   end
385
386   @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil
387   def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
388     ap_id
389     |> Queries.by_object_id()
390     |> with_preloaded_object()
391     |> first()
392     |> Repo.one()
393   end
394
395   def get_by_object_ap_id_with_object(_), do: nil
396
397   @spec add_by_params_query(String.t(), String.t(), String.t()) :: Ecto.Query.t()
398   def add_by_params_query(object_id, actor, target) do
399     object_id
400     |> Queries.by_object_id()
401     |> Queries.by_type("Add")
402     |> Queries.by_actor(actor)
403     |> where([a], fragment("?->>'target' = ?", a.data, ^target))
404   end
405 end