Merge branch 'fix/mrf-delete' into 'develop'
[akkoma] / lib / pleroma / activity.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.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 # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
27 @mastodon_notification_types %{
28 "Create" => "mention",
29 "Follow" => "follow",
30 "Announce" => "reblog",
31 "Like" => "favourite",
32 "Move" => "move"
33 }
34
35 @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
36 into: %{},
37 do: {v, k}
38
39 schema "activities" do
40 field(:data, :map)
41 field(:local, :boolean, default: true)
42 field(:actor, :string)
43 field(:recipients, {:array, :string}, default: [])
44 field(:thread_muted?, :boolean, virtual: true)
45
46 # This is a fake relation,
47 # do not use outside of with_preloaded_user_actor/with_joined_user_actor
48 has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
49 # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
50 has_one(:bookmark, Bookmark)
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 def with_joined_user_actor(query, join_type \\ :inner) do
95 join(query, join_type, [activity], u in User,
96 on: u.ap_id == activity.actor,
97 as: :user_actor
98 )
99 end
100
101 def with_preloaded_user_actor(query, join_type \\ :inner) do
102 query
103 |> with_joined_user_actor(join_type)
104 |> preload([activity, user_actor: user_actor], user_actor: user_actor)
105 end
106
107 def with_preloaded_bookmark(query, %User{} = user) do
108 from([a] in query,
109 left_join: b in Bookmark,
110 on: b.user_id == ^user.id and b.activity_id == a.id,
111 preload: [bookmark: b]
112 )
113 end
114
115 def with_preloaded_bookmark(query, _), do: query
116
117 def with_set_thread_muted_field(query, %User{} = user) do
118 from([a] in query,
119 left_join: tm in ThreadMute,
120 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
121 as: :thread_mute,
122 select: %Activity{a | thread_muted?: not is_nil(tm.id)}
123 )
124 end
125
126 def with_set_thread_muted_field(query, _), do: query
127
128 def get_by_ap_id(ap_id) do
129 ap_id
130 |> Queries.by_ap_id()
131 |> Repo.one()
132 end
133
134 def get_bookmark(%Activity{} = activity, %User{} = user) do
135 if Ecto.assoc_loaded?(activity.bookmark) do
136 activity.bookmark
137 else
138 Bookmark.get(user.id, activity.id)
139 end
140 end
141
142 def get_bookmark(_, _), do: nil
143
144 def change(struct, params \\ %{}) do
145 struct
146 |> cast(params, [:data, :recipients])
147 |> validate_required([:data])
148 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
149 end
150
151 def get_by_ap_id_with_object(ap_id) do
152 ap_id
153 |> Queries.by_ap_id()
154 |> with_preloaded_object(:left)
155 |> Repo.one()
156 end
157
158 @spec get_by_id(String.t()) :: Activity.t() | nil
159 def get_by_id(id) do
160 case FlakeId.flake_id?(id) do
161 true ->
162 Activity
163 |> where([a], a.id == ^id)
164 |> restrict_deactivated_users()
165 |> Repo.one()
166
167 _ ->
168 nil
169 end
170 end
171
172 def get_by_id_with_object(id) do
173 Activity
174 |> where(id: ^id)
175 |> with_preloaded_object()
176 |> Repo.one()
177 end
178
179 def all_by_ids_with_object(ids) do
180 Activity
181 |> where([a], a.id in ^ids)
182 |> with_preloaded_object()
183 |> Repo.all()
184 end
185
186 @doc """
187 Accepts `ap_id` or list of `ap_id`.
188 Returns a query.
189 """
190 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
191 def create_by_object_ap_id(ap_id) do
192 ap_id
193 |> Queries.by_object_id()
194 |> Queries.by_type("Create")
195 end
196
197 def get_all_create_by_object_ap_id(ap_id) do
198 ap_id
199 |> create_by_object_ap_id()
200 |> Repo.all()
201 end
202
203 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
204 create_by_object_ap_id(ap_id)
205 |> restrict_deactivated_users()
206 |> Repo.one()
207 end
208
209 def get_create_by_object_ap_id(_), do: nil
210
211 @doc """
212 Accepts `ap_id` or list of `ap_id`.
213 Returns a query.
214 """
215 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
216 def create_by_object_ap_id_with_object(ap_id) do
217 ap_id
218 |> create_by_object_ap_id()
219 |> with_preloaded_object()
220 end
221
222 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
223 ap_id
224 |> create_by_object_ap_id_with_object()
225 |> Repo.one()
226 end
227
228 def get_create_by_object_ap_id_with_object(_), do: nil
229
230 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
231 get_create_by_object_ap_id_with_object(ap_id)
232 end
233
234 defp get_in_reply_to_activity_from_object(_), do: nil
235
236 def get_in_reply_to_activity(%Activity{} = activity) do
237 get_in_reply_to_activity_from_object(Object.normalize(activity))
238 end
239
240 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
241 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
242 def normalize(_), do: nil
243
244 def delete_all_by_object_ap_id(id) when is_binary(id) do
245 id
246 |> Queries.by_object_id()
247 |> Queries.exclude_type("Delete")
248 |> select([u], u)
249 |> Repo.delete_all()
250 |> elem(1)
251 |> Enum.find(fn
252 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
253 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
254 _ -> nil
255 end)
256 |> purge_web_resp_cache()
257 end
258
259 def delete_all_by_object_ap_id(_), do: nil
260
261 defp purge_web_resp_cache(%Activity{} = activity) do
262 %{path: path} = URI.parse(activity.data["id"])
263 Cachex.del(:web_resp_cache, path)
264 activity
265 end
266
267 defp purge_web_resp_cache(nil), do: nil
268
269 for {ap_type, type} <- @mastodon_notification_types do
270 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
271 do: unquote(type)
272 end
273
274 def mastodon_notification_type(%Activity{}), do: nil
275
276 def from_mastodon_notification_type(type) do
277 Map.get(@mastodon_to_ap_notification_types, type)
278 end
279
280 def all_by_actor_and_id(actor, status_ids \\ [])
281 def all_by_actor_and_id(_actor, []), do: []
282
283 def all_by_actor_and_id(actor, status_ids) do
284 Activity
285 |> where([s], s.id in ^status_ids)
286 |> where([s], s.actor == ^actor)
287 |> Repo.all()
288 end
289
290 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
291 ap_id
292 |> Queries.by_object_id()
293 |> Queries.by_type("Follow")
294 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
295 end
296
297 def restrict_deactivated_users(query) do
298 deactivated_users =
299 from(u in User.Query.build(deactivated: true), select: u.ap_id)
300 |> Repo.all()
301
302 from(activity in query,
303 where: activity.actor not in ^deactivated_users
304 )
305 end
306
307 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
308
309 def direct_conversation_id(activity, for_user) do
310 alias Pleroma.Conversation.Participation
311
312 with %{data: %{"context" => context}} when is_binary(context) <- activity,
313 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
314 %Participation{id: participation_id} <-
315 Participation.for_user_and_conversation(for_user, conversation) do
316 participation_id
317 else
318 _ -> nil
319 end
320 end
321 end