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