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