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