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