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