Merge branch 'stable' of git.pleroma.social:pleroma/pleroma into pleroma-2.1-rc0
[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 # 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 has_one(:expiration, ActivityExpiration, on_delete: :delete_all)
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 change(struct, params \\ %{}) do
160 struct
161 |> cast(params, [:data, :recipients])
162 |> validate_required([:data])
163 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
164 end
165
166 def get_by_ap_id_with_object(ap_id) do
167 ap_id
168 |> Queries.by_ap_id()
169 |> with_preloaded_object(:left)
170 |> Repo.one()
171 end
172
173 @spec get_by_id(String.t()) :: Activity.t() | nil
174 def get_by_id(id) do
175 case FlakeId.flake_id?(id) do
176 true ->
177 Activity
178 |> where([a], a.id == ^id)
179 |> restrict_deactivated_users()
180 |> Repo.one()
181
182 _ ->
183 nil
184 end
185 end
186
187 def get_by_id_with_object(id) do
188 Activity
189 |> where(id: ^id)
190 |> with_preloaded_object()
191 |> Repo.one()
192 end
193
194 def all_by_ids_with_object(ids) do
195 Activity
196 |> where([a], a.id in ^ids)
197 |> with_preloaded_object()
198 |> Repo.all()
199 end
200
201 @doc """
202 Accepts `ap_id` or list of `ap_id`.
203 Returns a query.
204 """
205 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
206 def create_by_object_ap_id(ap_id) do
207 ap_id
208 |> Queries.by_object_id()
209 |> Queries.by_type("Create")
210 end
211
212 def get_all_create_by_object_ap_id(ap_id) do
213 ap_id
214 |> create_by_object_ap_id()
215 |> Repo.all()
216 end
217
218 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
219 create_by_object_ap_id(ap_id)
220 |> restrict_deactivated_users()
221 |> Repo.one()
222 end
223
224 def get_create_by_object_ap_id(_), do: nil
225
226 @doc """
227 Accepts `ap_id` or list of `ap_id`.
228 Returns a query.
229 """
230 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
231 def create_by_object_ap_id_with_object(ap_id) do
232 ap_id
233 |> create_by_object_ap_id()
234 |> with_preloaded_object()
235 end
236
237 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
238 ap_id
239 |> create_by_object_ap_id_with_object()
240 |> Repo.one()
241 end
242
243 def get_create_by_object_ap_id_with_object(_), do: nil
244
245 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
246 get_create_by_object_ap_id_with_object(ap_id)
247 end
248
249 defp get_in_reply_to_activity_from_object(_), do: nil
250
251 def get_in_reply_to_activity(%Activity{} = activity) do
252 get_in_reply_to_activity_from_object(Object.normalize(activity))
253 end
254
255 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
256 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
257 def normalize(_), do: nil
258
259 def delete_all_by_object_ap_id(id) when is_binary(id) do
260 id
261 |> Queries.by_object_id()
262 |> Queries.exclude_type("Delete")
263 |> select([u], u)
264 |> Repo.delete_all()
265 |> elem(1)
266 |> Enum.find(fn
267 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
268 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
269 _ -> nil
270 end)
271 |> purge_web_resp_cache()
272 end
273
274 def delete_all_by_object_ap_id(_), do: nil
275
276 defp purge_web_resp_cache(%Activity{} = activity) do
277 %{path: path} = URI.parse(activity.data["id"])
278 Cachex.del(:web_resp_cache, path)
279 activity
280 end
281
282 defp purge_web_resp_cache(nil), do: nil
283
284 def follow_accepted?(
285 %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
286 ) do
287 with %User{} = follower <- Activity.user_actor(activity),
288 %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
289 Pleroma.FollowingRelationship.following?(follower, followed)
290 else
291 _ -> false
292 end
293 end
294
295 def follow_accepted?(_), do: false
296
297 def all_by_actor_and_id(actor, status_ids \\ [])
298 def all_by_actor_and_id(_actor, []), do: []
299
300 def all_by_actor_and_id(actor, status_ids) do
301 Activity
302 |> where([s], s.id in ^status_ids)
303 |> where([s], s.actor == ^actor)
304 |> Repo.all()
305 end
306
307 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
308 ap_id
309 |> Queries.by_object_id()
310 |> Queries.by_type("Follow")
311 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
312 end
313
314 def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
315 Queries.by_type("Follow")
316 |> where([a], fragment("?->>'state' = 'pending'", a.data))
317 |> where([a], a.actor == ^ap_id)
318 |> Repo.all()
319 end
320
321 def restrict_deactivated_users(query) do
322 deactivated_users =
323 from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
324 |> Repo.all()
325
326 Activity.Queries.exclude_authors(query, deactivated_users)
327 end
328
329 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
330
331 def direct_conversation_id(activity, for_user) do
332 alias Pleroma.Conversation.Participation
333
334 with %{data: %{"context" => context}} when is_binary(context) <- activity,
335 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
336 %Participation{id: participation_id} <-
337 Participation.for_user_and_conversation(for_user, conversation) do
338 participation_id
339 else
340 _ -> nil
341 end
342 end
343
344 @spec pinned_by_actor?(Activity.t()) :: boolean()
345 def pinned_by_actor?(%Activity{} = activity) do
346 actor = user_actor(activity)
347 activity.id in actor.pinned_activities
348 end
349 end