[#1559] Support for "follow_request" notifications (configurable).
[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 for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
304 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
305 do: unquote(type)
306 end
307
308 def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
309 if follow_accepted?(activity) do
310 "follow"
311 else
312 "follow_request"
313 end
314 end
315
316 def mastodon_notification_type(%Activity{}), do: nil
317
318 def from_mastodon_notification_type(type) do
319 with {k, _v} <-
320 Enum.find(@mastodon_notification_types, fn {_k, v} ->
321 v == type or (is_list(v) and type in v)
322 end) do
323 k
324 end
325 end
326
327 def all_by_actor_and_id(actor, status_ids \\ [])
328 def all_by_actor_and_id(_actor, []), do: []
329
330 def all_by_actor_and_id(actor, status_ids) do
331 Activity
332 |> where([s], s.id in ^status_ids)
333 |> where([s], s.actor == ^actor)
334 |> Repo.all()
335 end
336
337 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
338 ap_id
339 |> Queries.by_object_id()
340 |> Queries.by_type("Follow")
341 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
342 end
343
344 def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
345 Queries.by_type("Follow")
346 |> where([a], fragment("?->>'state' = 'pending'", a.data))
347 |> where([a], a.actor == ^ap_id)
348 |> Repo.all()
349 end
350
351 def restrict_deactivated_users(query) do
352 deactivated_users =
353 from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
354 |> Repo.all()
355
356 Activity.Queries.exclude_authors(query, deactivated_users)
357 end
358
359 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
360
361 def direct_conversation_id(activity, for_user) do
362 alias Pleroma.Conversation.Participation
363
364 with %{data: %{"context" => context}} when is_binary(context) <- activity,
365 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
366 %Participation{id: participation_id} <-
367 Participation.for_user_and_conversation(for_user, conversation) do
368 participation_id
369 else
370 _ -> nil
371 end
372 end
373 end