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