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