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