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