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