Add `local` visibility
[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.Bookmark
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.Repo
14 alias Pleroma.ReportNote
15 alias Pleroma.ThreadMute
16 alias Pleroma.User
17 alias Pleroma.Web.ActivityPub.ActivityPub
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 schema "activities" do
28 field(:data, :map)
29 field(:local, :boolean, default: true)
30 field(:actor, :string)
31 field(:recipients, {:array, :string}, default: [])
32 field(:thread_muted?, :boolean, virtual: true)
33
34 # A field that can be used if you need to join some kind of other
35 # id to order / paginate this field by
36 field(:pagination_id, :string, virtual: true)
37
38 # This is a fake relation,
39 # do not use outside of with_preloaded_user_actor/with_joined_user_actor
40 has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
41 # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
42 has_one(:bookmark, Bookmark)
43 # This is a fake relation, do not use outside of with_preloaded_report_notes
44 has_many(:report_notes, ReportNote)
45 has_many(:notifications, Notification, on_delete: :delete_all)
46
47 # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
48 # The foreign key is embedded in a jsonb field.
49 #
50 # To use it, you probably want to do an inner join and a preload:
51 #
52 # ```
53 # |> join(:inner, [activity], o in Object,
54 # on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
55 # o.data, activity.data, activity.data))
56 # |> preload([activity, object], [object: object])
57 # ```
58 #
59 # As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
60 # typical case.
61 has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
62
63 timestamps()
64 end
65
66 def with_joined_object(query, join_type \\ :inner) do
67 join(query, join_type, [activity], o in Object,
68 on:
69 fragment(
70 "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
71 o.data,
72 activity.data,
73 activity.data
74 ),
75 as: :object
76 )
77 end
78
79 def with_preloaded_object(query, join_type \\ :inner) do
80 query
81 |> has_named_binding?(:object)
82 |> if(do: query, else: with_joined_object(query, join_type))
83 |> preload([activity, object: object], object: object)
84 end
85
86 # Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
87 def user_actor(%Activity{actor: nil}), do: nil
88
89 def user_actor(%Activity{} = activity) do
90 with %User{} <- activity.user_actor do
91 activity.user_actor
92 else
93 _ -> User.get_cached_by_ap_id(activity.actor)
94 end
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 get_report(activity_id) do
158 opts = %{
159 type: "Flag",
160 skip_preload: true,
161 preload_report_notes: true
162 }
163
164 ActivityPub.fetch_activities_query([], opts)
165 |> where(id: ^activity_id)
166 |> Repo.one()
167 end
168
169 def change(struct, params \\ %{}) do
170 struct
171 |> cast(params, [:data, :recipients])
172 |> validate_required([:data])
173 |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
174 end
175
176 def get_by_ap_id_with_object(ap_id) do
177 ap_id
178 |> Queries.by_ap_id()
179 |> with_preloaded_object(:left)
180 |> Repo.one()
181 end
182
183 @spec get_by_id(String.t()) :: Activity.t() | nil
184 def get_by_id(id) do
185 case FlakeId.flake_id?(id) do
186 true ->
187 Activity
188 |> where([a], a.id == ^id)
189 |> restrict_deactivated_users()
190 |> Repo.one()
191
192 _ ->
193 nil
194 end
195 end
196
197 def get_by_id_with_object(id) do
198 Activity
199 |> where(id: ^id)
200 |> with_preloaded_object()
201 |> Repo.one()
202 end
203
204 def all_by_ids_with_object(ids) do
205 Activity
206 |> where([a], a.id in ^ids)
207 |> with_preloaded_object()
208 |> Repo.all()
209 end
210
211 @doc """
212 Accepts `ap_id` or list of `ap_id`.
213 Returns a query.
214 """
215 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
216 def create_by_object_ap_id(ap_id) do
217 ap_id
218 |> Queries.by_object_id()
219 |> Queries.by_type("Create")
220 end
221
222 def get_all_create_by_object_ap_id(ap_id) do
223 ap_id
224 |> create_by_object_ap_id()
225 |> Repo.all()
226 end
227
228 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
229 create_by_object_ap_id(ap_id)
230 |> restrict_deactivated_users()
231 |> Repo.one()
232 end
233
234 def get_create_by_object_ap_id(_), do: nil
235
236 @doc """
237 Accepts `ap_id` or list of `ap_id`.
238 Returns a query.
239 """
240 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
241 def create_by_object_ap_id_with_object(ap_id) do
242 ap_id
243 |> create_by_object_ap_id()
244 |> with_preloaded_object()
245 end
246
247 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
248 ap_id
249 |> create_by_object_ap_id_with_object()
250 |> Repo.one()
251 end
252
253 def get_create_by_object_ap_id_with_object(_), do: nil
254
255 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
256 get_create_by_object_ap_id_with_object(ap_id)
257 end
258
259 defp get_in_reply_to_activity_from_object(_), do: nil
260
261 def get_in_reply_to_activity(%Activity{} = activity) do
262 get_in_reply_to_activity_from_object(Object.normalize(activity))
263 end
264
265 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
266 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
267 def normalize(_), do: nil
268
269 def delete_all_by_object_ap_id(id) when is_binary(id) do
270 id
271 |> Queries.by_object_id()
272 |> Queries.exclude_type("Delete")
273 |> select([u], u)
274 |> Repo.delete_all()
275 |> elem(1)
276 |> Enum.find(fn
277 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
278 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
279 _ -> nil
280 end)
281 |> purge_web_resp_cache()
282 end
283
284 def delete_all_by_object_ap_id(_), do: nil
285
286 defp purge_web_resp_cache(%Activity{} = activity) do
287 %{path: path} = URI.parse(activity.data["id"])
288 Cachex.del(:web_resp_cache, path)
289 activity
290 end
291
292 defp purge_web_resp_cache(nil), do: nil
293
294 def follow_accepted?(
295 %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
296 ) do
297 with %User{} = follower <- Activity.user_actor(activity),
298 %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
299 Pleroma.FollowingRelationship.following?(follower, followed)
300 else
301 _ -> false
302 end
303 end
304
305 def follow_accepted?(_), do: false
306
307 def all_by_actor_and_id(actor, status_ids \\ [])
308 def all_by_actor_and_id(_actor, []), do: []
309
310 def all_by_actor_and_id(actor, status_ids) do
311 Activity
312 |> where([s], s.id in ^status_ids)
313 |> where([s], s.actor == ^actor)
314 |> Repo.all()
315 end
316
317 def follow_requests_for_actor(%User{ap_id: ap_id}) do
318 ap_id
319 |> Queries.by_object_id()
320 |> Queries.by_type("Follow")
321 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
322 end
323
324 def following_requests_for_actor(%User{ap_id: ap_id}) do
325 Queries.by_type("Follow")
326 |> where([a], fragment("?->>'state' = 'pending'", a.data))
327 |> where([a], a.actor == ^ap_id)
328 |> Repo.all()
329 end
330
331 def restrict_deactivated_users(query) do
332 deactivated_users =
333 from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
334 |> Repo.all()
335
336 Activity.Queries.exclude_authors(query, deactivated_users)
337 end
338
339 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
340
341 def direct_conversation_id(activity, for_user) do
342 alias Pleroma.Conversation.Participation
343
344 with %{data: %{"context" => context}} when is_binary(context) <- activity,
345 %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
346 %Participation{id: participation_id} <-
347 Participation.for_user_and_conversation(for_user, conversation) do
348 participation_id
349 else
350 _ -> nil
351 end
352 end
353
354 @spec pinned_by_actor?(Activity.t()) :: boolean()
355 def pinned_by_actor?(%Activity{} = activity) do
356 actor = user_actor(activity)
357 activity.id in actor.pinned_activities
358 end
359 end