Line is too long!
[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:
96 fragment(
97 "? = ?->>'actor'",
98 u.ap_id,
99 activity.data
100 ),
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_set_thread_muted_field(query, %User{} = user) do
122 from([a] in query,
123 left_join: tm in ThreadMute,
124 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
125 as: :thread_mute,
126 select: %Activity{a | thread_muted?: not is_nil(tm.id)}
127 )
128 end
129
130 def with_set_thread_muted_field(query, _), do: query
131
132 def get_by_ap_id(ap_id) do
133 ap_id
134 |> Queries.by_ap_id()
135 |> Repo.one()
136 end
137
138 def get_bookmark(%Activity{} = activity, %User{} = user) do
139 if Ecto.assoc_loaded?(activity.bookmark) do
140 activity.bookmark
141 else
142 Bookmark.get(user.id, activity.id)
143 end
144 end
145
146 def get_bookmark(_, _), do: nil
147
148 def change(struct, params \\ %{}) do
149 struct
150 |> cast(params, [:data, :recipients])
151 |> validate_required([:data])
152 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
153 end
154
155 def get_by_ap_id_with_object(ap_id) do
156 ap_id
157 |> Queries.by_ap_id()
158 |> with_preloaded_object(:left)
159 |> Repo.one()
160 end
161
162 @spec get_by_id(String.t()) :: Activity.t() | nil
163 def get_by_id(id) do
164 case FlakeId.flake_id?(id) do
165 true ->
166 Activity
167 |> where([a], a.id == ^id)
168 |> restrict_deactivated_users()
169 |> Repo.one()
170
171 _ ->
172 nil
173 end
174 end
175
176 def get_by_id_with_object(id) do
177 Activity
178 |> where(id: ^id)
179 |> with_preloaded_object()
180 |> Repo.one()
181 end
182
183 def all_by_ids_with_object(ids) do
184 Activity
185 |> where([a], a.id in ^ids)
186 |> with_preloaded_object()
187 |> Repo.all()
188 end
189
190 @doc """
191 Accepts `ap_id` or list of `ap_id`.
192 Returns a query.
193 """
194 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
195 def create_by_object_ap_id(ap_id) do
196 ap_id
197 |> Queries.by_object_id()
198 |> Queries.by_type("Create")
199 end
200
201 def get_all_create_by_object_ap_id(ap_id) do
202 ap_id
203 |> create_by_object_ap_id()
204 |> Repo.all()
205 end
206
207 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
208 create_by_object_ap_id(ap_id)
209 |> restrict_deactivated_users()
210 |> Repo.one()
211 end
212
213 def get_create_by_object_ap_id(_), do: nil
214
215 @doc """
216 Accepts `ap_id` or list of `ap_id`.
217 Returns a query.
218 """
219 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
220 def create_by_object_ap_id_with_object(ap_id) do
221 ap_id
222 |> create_by_object_ap_id()
223 |> with_preloaded_object()
224 end
225
226 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
227 ap_id
228 |> create_by_object_ap_id_with_object()
229 |> Repo.one()
230 end
231
232 def get_create_by_object_ap_id_with_object(_), do: nil
233
234 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
235 get_create_by_object_ap_id_with_object(ap_id)
236 end
237
238 defp get_in_reply_to_activity_from_object(_), do: nil
239
240 def get_in_reply_to_activity(%Activity{} = activity) do
241 get_in_reply_to_activity_from_object(Object.normalize(activity))
242 end
243
244 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
245 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
246 def normalize(_), do: nil
247
248 def delete_by_ap_id(id) when is_binary(id) do
249 id
250 |> Queries.by_object_id()
251 |> select([u], u)
252 |> Repo.delete_all()
253 |> elem(1)
254 |> Enum.find(fn
255 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
256 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
257 _ -> nil
258 end)
259 |> purge_web_resp_cache()
260 end
261
262 def delete_by_ap_id(_), do: nil
263
264 defp purge_web_resp_cache(%Activity{} = activity) do
265 %{path: path} = URI.parse(activity.data["id"])
266 Cachex.del(:web_resp_cache, path)
267 activity
268 end
269
270 defp purge_web_resp_cache(nil), do: nil
271
272 for {ap_type, type} <- @mastodon_notification_types do
273 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
274 do: unquote(type)
275 end
276
277 def mastodon_notification_type(%Activity{}), do: nil
278
279 def from_mastodon_notification_type(type) do
280 Map.get(@mastodon_to_ap_notification_types, type)
281 end
282
283 def all_by_actor_and_id(actor, status_ids \\ [])
284 def all_by_actor_and_id(_actor, []), do: []
285
286 def all_by_actor_and_id(actor, status_ids) do
287 Activity
288 |> where([s], s.id in ^status_ids)
289 |> where([s], s.actor == ^actor)
290 |> Repo.all()
291 end
292
293 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
294 ap_id
295 |> Queries.by_object_id()
296 |> Queries.by_type("Follow")
297 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
298 end
299
300 def restrict_deactivated_users(query) do
301 deactivated_users =
302 from(u in User.Query.build(deactivated: true), select: u.ap_id)
303 |> Repo.all()
304
305 from(activity in query,
306 where: activity.actor not in ^deactivated_users
307 )
308 end
309
310 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
311 end