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