Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
[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 # Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
99 def user_actor(%Activity{actor: nil}), do: nil
100
101 def user_actor(%Activity{} = activity) do
102 with %User{} <- activity.user_actor do
103 activity.user_actor
104 else
105 _ -> User.get_cached_by_ap_id(activity.actor)
106 end
107 end
108
109 def with_joined_user_actor(query, join_type \\ :inner) do
110 join(query, join_type, [activity], u in User,
111 on: u.ap_id == activity.actor,
112 as: :user_actor
113 )
114 end
115
116 def with_preloaded_user_actor(query, join_type \\ :inner) do
117 query
118 |> with_joined_user_actor(join_type)
119 |> preload([activity, user_actor: user_actor], user_actor: user_actor)
120 end
121
122 def with_preloaded_bookmark(query, %User{} = user) do
123 from([a] in query,
124 left_join: b in Bookmark,
125 on: b.user_id == ^user.id and b.activity_id == a.id,
126 preload: [bookmark: b]
127 )
128 end
129
130 def with_preloaded_bookmark(query, _), do: query
131
132 def with_preloaded_report_notes(query) do
133 from([a] in query,
134 left_join: r in ReportNote,
135 on: a.id == r.activity_id,
136 preload: [report_notes: r]
137 )
138 end
139
140 def with_preloaded_report_notes(query, _), do: query
141
142 def with_set_thread_muted_field(query, %User{} = user) do
143 from([a] in query,
144 left_join: tm in ThreadMute,
145 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
146 as: :thread_mute,
147 select: %Activity{a | thread_muted?: not is_nil(tm.id)}
148 )
149 end
150
151 def with_set_thread_muted_field(query, _), do: query
152
153 def get_by_ap_id(ap_id) do
154 ap_id
155 |> Queries.by_ap_id()
156 |> Repo.one()
157 end
158
159 def get_bookmark(%Activity{} = activity, %User{} = user) do
160 if Ecto.assoc_loaded?(activity.bookmark) do
161 activity.bookmark
162 else
163 Bookmark.get(user.id, activity.id)
164 end
165 end
166
167 def get_bookmark(_, _), do: nil
168
169 def change(struct, params \\ %{}) do
170 struct
171 |> cast(params, [:data, :recipients])
172 |> validate_required([:data])
173 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
174 end
175
176 def get_by_ap_id_with_object(ap_id) do
177 ap_id
178 |> Queries.by_ap_id()
179 |> with_preloaded_object(:left)
180 |> Repo.one()
181 end
182
183 @spec get_by_id(String.t()) :: Activity.t() | nil
184 def get_by_id(id) do
185 case FlakeId.flake_id?(id) do
186 true ->
187 Activity
188 |> where([a], a.id == ^id)
189 |> restrict_deactivated_users()
190 |> Repo.one()
191
192 _ ->
193 nil
194 end
195 end
196
197 def get_by_id_with_object(id) do
198 Activity
199 |> where(id: ^id)
200 |> with_preloaded_object()
201 |> Repo.one()
202 end
203
204 def all_by_ids_with_object(ids) do
205 Activity
206 |> where([a], a.id in ^ids)
207 |> with_preloaded_object()
208 |> Repo.all()
209 end
210
211 @doc """
212 Accepts `ap_id` or list of `ap_id`.
213 Returns a query.
214 """
215 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
216 def create_by_object_ap_id(ap_id) do
217 ap_id
218 |> Queries.by_object_id()
219 |> Queries.by_type("Create")
220 end
221
222 def get_all_create_by_object_ap_id(ap_id) do
223 ap_id
224 |> create_by_object_ap_id()
225 |> Repo.all()
226 end
227
228 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
229 create_by_object_ap_id(ap_id)
230 |> restrict_deactivated_users()
231 |> Repo.one()
232 end
233
234 def get_create_by_object_ap_id(_), do: nil
235
236 @doc """
237 Accepts `ap_id` or list of `ap_id`.
238 Returns a query.
239 """
240 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
241 def create_by_object_ap_id_with_object(ap_id) do
242 ap_id
243 |> create_by_object_ap_id()
244 |> with_preloaded_object()
245 end
246
247 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
248 ap_id
249 |> create_by_object_ap_id_with_object()
250 |> Repo.one()
251 end
252
253 def get_create_by_object_ap_id_with_object(_), do: nil
254
255 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
256 get_create_by_object_ap_id_with_object(ap_id)
257 end
258
259 defp get_in_reply_to_activity_from_object(_), do: nil
260
261 def get_in_reply_to_activity(%Activity{} = activity) do
262 get_in_reply_to_activity_from_object(Object.normalize(activity))
263 end
264
265 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
266 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
267 def normalize(_), do: nil
268
269 def delete_all_by_object_ap_id(id) when is_binary(id) do
270 id
271 |> Queries.by_object_id()
272 |> Queries.exclude_type("Delete")
273 |> select([u], u)
274 |> Repo.delete_all()
275 |> elem(1)
276 |> Enum.find(fn
277 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
278 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
279 _ -> nil
280 end)
281 |> purge_web_resp_cache()
282 end
283
284 def delete_all_by_object_ap_id(_), do: nil
285
286 defp purge_web_resp_cache(%Activity{} = activity) do
287 %{path: path} = URI.parse(activity.data["id"])
288 Cachex.del(:web_resp_cache, path)
289 activity
290 end
291
292 defp purge_web_resp_cache(nil), do: nil
293
294 for {ap_type, type} <- @mastodon_notification_types do
295 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
296 do: unquote(type)
297 end
298
299 def mastodon_notification_type(%Activity{}), do: nil
300
301 def from_mastodon_notification_type(type) do
302 Map.get(@mastodon_to_ap_notification_types, type)
303 end
304
305 def all_by_actor_and_id(actor, status_ids \\ [])
306 def all_by_actor_and_id(_actor, []), do: []
307
308 def all_by_actor_and_id(actor, status_ids) do
309 Activity
310 |> where([s], s.id in ^status_ids)
311 |> where([s], s.actor == ^actor)
312 |> Repo.all()
313 end
314
315 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
316 ap_id
317 |> Queries.by_object_id()
318 |> Queries.by_type("Follow")
319 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
320 end
321
322 def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
323 Queries.by_type("Follow")
324 |> where([a], fragment("?->>'state' = 'pending'", a.data))
325 |> where([a], a.actor == ^ap_id)
326 |> Repo.all()
327 end
328
329 def restrict_deactivated_users(query) do
330 deactivated_users =
331 from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
332 |> Repo.all()
333
334 Activity.Queries.exclude_authors(query, deactivated_users)
335 end
336
337 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
338
339 def direct_conversation_id(activity, for_user) do
340 alias Pleroma.Conversation.Participation
341
342 with %{data: %{"context" => context}} when is_binary(context) <- activity,
343 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
344 %Participation{id: participation_id} <-
345 Participation.for_user_and_conversation(for_user, conversation) do
346 participation_id
347 else
348 _ -> nil
349 end
350 end
351 end