Merge branch 'feature/move-activity' 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_by_ap_id(id) when is_binary(id) do
245 id
246 |> Queries.by_object_id()
247 |> select([u], u)
248 |> Repo.delete_all()
249 |> elem(1)
250 |> Enum.find(fn
251 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
252 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
253 _ -> nil
254 end)
255 |> purge_web_resp_cache()
256 end
257
258 def delete_by_ap_id(_), do: nil
259
260 defp purge_web_resp_cache(%Activity{} = activity) do
261 %{path: path} = URI.parse(activity.data["id"])
262 Cachex.del(:web_resp_cache, path)
263 activity
264 end
265
266 defp purge_web_resp_cache(nil), do: nil
267
268 for {ap_type, type} <- @mastodon_notification_types do
269 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
270 do: unquote(type)
271 end
272
273 def mastodon_notification_type(%Activity{}), do: nil
274
275 def from_mastodon_notification_type(type) do
276 Map.get(@mastodon_to_ap_notification_types, type)
277 end
278
279 def all_by_actor_and_id(actor, status_ids \\ [])
280 def all_by_actor_and_id(_actor, []), do: []
281
282 def all_by_actor_and_id(actor, status_ids) do
283 Activity
284 |> where([s], s.id in ^status_ids)
285 |> where([s], s.actor == ^actor)
286 |> Repo.all()
287 end
288
289 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
290 ap_id
291 |> Queries.by_object_id()
292 |> Queries.by_type("Follow")
293 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
294 end
295
296 def restrict_deactivated_users(query) do
297 deactivated_users =
298 from(u in User.Query.build(deactivated: true), select: u.ap_id)
299 |> Repo.all()
300
301 from(activity in query,
302 where: activity.actor not in ^deactivated_users
303 )
304 end
305
306 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
307
308 def direct_conversation_id(activity, for_user) do
309 alias Pleroma.Conversation.Participation
310
311 with %{data: %{"context" => context}} when is_binary(context) <- activity,
312 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
313 %Participation{id: participation_id} <-
314 Participation.for_user_and_conversation(for_user, conversation) do
315 participation_id
316 else
317 _ -> nil
318 end
319 end
320 end