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