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