Alternative implementation of hashtag-filtering queries in ActivityPub. Fixed GROUP...
[akkoma] / lib / pleroma / activity.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 as: :bookmark,
117 preload: [bookmark: b]
118 )
119 end
120
121 def with_preloaded_bookmark(query, _), do: query
122
123 def with_preloaded_report_notes(query) do
124 from([a] in query,
125 left_join: r in ReportNote,
126 on: a.id == r.activity_id,
127 as: :report_note,
128 preload: [report_notes: r]
129 )
130 end
131
132 def with_preloaded_report_notes(query, _), do: query
133
134 def with_set_thread_muted_field(query, %User{} = user) do
135 from([a] in query,
136 left_join: tm in ThreadMute,
137 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
138 as: :thread_mute,
139 select: %Activity{a | thread_muted?: not is_nil(tm.id)}
140 )
141 end
142
143 def with_set_thread_muted_field(query, _), do: query
144
145 def get_by_ap_id(ap_id) do
146 ap_id
147 |> Queries.by_ap_id()
148 |> Repo.one()
149 end
150
151 def get_bookmark(%Activity{} = activity, %User{} = user) do
152 if Ecto.assoc_loaded?(activity.bookmark) do
153 activity.bookmark
154 else
155 Bookmark.get(user.id, activity.id)
156 end
157 end
158
159 def get_bookmark(_, _), do: nil
160
161 def get_report(activity_id) do
162 opts = %{
163 type: "Flag",
164 skip_preload: true,
165 preload_report_notes: true
166 }
167
168 ActivityPub.fetch_activities_query([], opts)
169 |> where(id: ^activity_id)
170 |> Repo.one()
171 end
172
173 def change(struct, params \\ %{}) do
174 struct
175 |> cast(params, [:data, :recipients])
176 |> validate_required([:data])
177 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
178 end
179
180 def get_by_ap_id_with_object(ap_id) do
181 ap_id
182 |> Queries.by_ap_id()
183 |> with_preloaded_object(:left)
184 |> Repo.one()
185 end
186
187 @spec get_by_id(String.t()) :: Activity.t() | nil
188 def get_by_id(id) do
189 case FlakeId.flake_id?(id) do
190 true ->
191 Activity
192 |> where([a], a.id == ^id)
193 |> restrict_deactivated_users()
194 |> Repo.one()
195
196 _ ->
197 nil
198 end
199 end
200
201 def get_by_id_with_user_actor(id) do
202 case FlakeId.flake_id?(id) do
203 true ->
204 Activity
205 |> where([a], a.id == ^id)
206 |> with_preloaded_user_actor()
207 |> Repo.one()
208
209 _ ->
210 nil
211 end
212 end
213
214 def get_by_id_with_object(id) do
215 Activity
216 |> where(id: ^id)
217 |> with_preloaded_object()
218 |> Repo.one()
219 end
220
221 def all_by_ids_with_object(ids) do
222 Activity
223 |> where([a], a.id in ^ids)
224 |> with_preloaded_object()
225 |> Repo.all()
226 end
227
228 @doc """
229 Accepts `ap_id` or list of `ap_id`.
230 Returns a query.
231 """
232 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
233 def create_by_object_ap_id(ap_id) do
234 ap_id
235 |> Queries.by_object_id()
236 |> Queries.by_type("Create")
237 end
238
239 def get_all_create_by_object_ap_id(ap_id) do
240 ap_id
241 |> create_by_object_ap_id()
242 |> Repo.all()
243 end
244
245 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
246 create_by_object_ap_id(ap_id)
247 |> restrict_deactivated_users()
248 |> Repo.one()
249 end
250
251 def get_create_by_object_ap_id(_), do: nil
252
253 @doc """
254 Accepts `ap_id` or list of `ap_id`.
255 Returns a query.
256 """
257 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
258 def create_by_object_ap_id_with_object(ap_id) do
259 ap_id
260 |> create_by_object_ap_id()
261 |> with_preloaded_object()
262 end
263
264 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
265 ap_id
266 |> create_by_object_ap_id_with_object()
267 |> Repo.one()
268 end
269
270 def get_create_by_object_ap_id_with_object(_), do: nil
271
272 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
273 get_create_by_object_ap_id_with_object(ap_id)
274 end
275
276 defp get_in_reply_to_activity_from_object(_), do: nil
277
278 def get_in_reply_to_activity(%Activity{} = activity) do
279 get_in_reply_to_activity_from_object(Object.normalize(activity))
280 end
281
282 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
283 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
284 def normalize(_), do: nil
285
286 def delete_all_by_object_ap_id(id) when is_binary(id) do
287 id
288 |> Queries.by_object_id()
289 |> Queries.exclude_type("Delete")
290 |> select([u], u)
291 |> Repo.delete_all()
292 |> elem(1)
293 |> Enum.find(fn
294 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
295 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
296 _ -> nil
297 end)
298 |> purge_web_resp_cache()
299 end
300
301 def delete_all_by_object_ap_id(_), do: nil
302
303 defp purge_web_resp_cache(%Activity{} = activity) do
304 %{path: path} = URI.parse(activity.data["id"])
305 @cachex.del(:web_resp_cache, path)
306 activity
307 end
308
309 defp purge_web_resp_cache(nil), do: nil
310
311 def follow_accepted?(
312 %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
313 ) do
314 with %User{} = follower <- Activity.user_actor(activity),
315 %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
316 Pleroma.FollowingRelationship.following?(follower, followed)
317 else
318 _ -> false
319 end
320 end
321
322 def follow_accepted?(_), do: false
323
324 def all_by_actor_and_id(actor, status_ids \\ [])
325 def all_by_actor_and_id(_actor, []), do: []
326
327 def all_by_actor_and_id(actor, status_ids) do
328 Activity
329 |> where([s], s.id in ^status_ids)
330 |> where([s], s.actor == ^actor)
331 |> Repo.all()
332 end
333
334 def follow_requests_for_actor(%User{ap_id: ap_id}) do
335 ap_id
336 |> Queries.by_object_id()
337 |> Queries.by_type("Follow")
338 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
339 end
340
341 def following_requests_for_actor(%User{ap_id: ap_id}) do
342 Queries.by_type("Follow")
343 |> where([a], fragment("?->>'state' = 'pending'", a.data))
344 |> where([a], a.actor == ^ap_id)
345 |> Repo.all()
346 end
347
348 def restrict_deactivated_users(query) do
349 deactivated_users =
350 from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
351 |> Repo.all()
352
353 Activity.Queries.exclude_authors(query, deactivated_users)
354 end
355
356 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
357
358 def direct_conversation_id(activity, for_user) do
359 alias Pleroma.Conversation.Participation
360
361 with %{data: %{"context" => context}} when is_binary(context) <- activity,
362 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
363 %Participation{id: participation_id} <-
364 Participation.for_user_and_conversation(for_user, conversation) do
365 participation_id
366 else
367 _ -> nil
368 end
369 end
370
371 @spec pinned_by_actor?(Activity.t()) :: boolean()
372 def pinned_by_actor?(%Activity{} = activity) do
373 actor = user_actor(activity)
374 activity.id in actor.pinned_activities
375 end
376
377 @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil
378 def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
379 ap_id
380 |> Queries.by_object_id()
381 |> with_preloaded_object()
382 |> first()
383 |> Repo.one()
384 end
385
386 def get_by_object_ap_id_with_object(_), do: nil
387 end