Merge branch 'develop' into tests/mastodon_api_controller.ex
[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 @spec get_by_id(String.t()) :: Activity.t() | nil
141 def get_by_id(id) do
142 case Pleroma.FlakeId.is_flake_id?(id) do
143 true ->
144 Activity
145 |> where([a], a.id == ^id)
146 |> restrict_deactivated_users()
147 |> Repo.one()
148
149 _ ->
150 nil
151 end
152 end
153
154 def get_by_id_with_object(id) do
155 Activity
156 |> where(id: ^id)
157 |> with_preloaded_object()
158 |> Repo.one()
159 end
160
161 def all_by_ids_with_object(ids) do
162 Activity
163 |> where([a], a.id in ^ids)
164 |> with_preloaded_object()
165 |> Repo.all()
166 end
167
168 @doc """
169 Accepts `ap_id` or list of `ap_id`.
170 Returns a query.
171 """
172 @spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
173 def create_by_object_ap_id(ap_id) do
174 ap_id
175 |> Queries.by_object_id()
176 |> Queries.by_type("Create")
177 end
178
179 def get_all_create_by_object_ap_id(ap_id) do
180 ap_id
181 |> create_by_object_ap_id()
182 |> Repo.all()
183 end
184
185 def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
186 create_by_object_ap_id(ap_id)
187 |> restrict_deactivated_users()
188 |> Repo.one()
189 end
190
191 def get_create_by_object_ap_id(_), do: nil
192
193 @doc """
194 Accepts `ap_id` or list of `ap_id`.
195 Returns a query.
196 """
197 @spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
198 def create_by_object_ap_id_with_object(ap_id) do
199 ap_id
200 |> create_by_object_ap_id()
201 |> with_preloaded_object()
202 end
203
204 def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
205 ap_id
206 |> create_by_object_ap_id_with_object()
207 |> Repo.one()
208 end
209
210 def get_create_by_object_ap_id_with_object(_), do: nil
211
212 defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
213 get_create_by_object_ap_id_with_object(ap_id)
214 end
215
216 defp get_in_reply_to_activity_from_object(_), do: nil
217
218 def get_in_reply_to_activity(%Activity{} = activity) do
219 get_in_reply_to_activity_from_object(Object.normalize(activity))
220 end
221
222 def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
223 def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
224 def normalize(_), do: nil
225
226 def delete_by_ap_id(id) when is_binary(id) do
227 id
228 |> Queries.by_object_id()
229 |> select([u], u)
230 |> Repo.delete_all()
231 |> elem(1)
232 |> Enum.find(fn
233 %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
234 %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
235 _ -> nil
236 end)
237 |> purge_web_resp_cache()
238 end
239
240 def delete_by_ap_id(_), do: nil
241
242 defp purge_web_resp_cache(%Activity{} = activity) do
243 %{path: path} = URI.parse(activity.data["id"])
244 Cachex.del(:web_resp_cache, path)
245 activity
246 end
247
248 defp purge_web_resp_cache(nil), do: nil
249
250 for {ap_type, type} <- @mastodon_notification_types do
251 def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
252 do: unquote(type)
253 end
254
255 def mastodon_notification_type(%Activity{}), do: nil
256
257 def from_mastodon_notification_type(type) do
258 Map.get(@mastodon_to_ap_notification_types, type)
259 end
260
261 def all_by_actor_and_id(actor, status_ids \\ [])
262 def all_by_actor_and_id(_actor, []), do: []
263
264 def all_by_actor_and_id(actor, status_ids) do
265 Activity
266 |> where([s], s.id in ^status_ids)
267 |> where([s], s.actor == ^actor)
268 |> Repo.all()
269 end
270
271 def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
272 ap_id
273 |> Queries.by_object_id()
274 |> Queries.by_type("Follow")
275 |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
276 end
277
278 def restrict_deactivated_users(query) do
279 deactivated_users =
280 from(u in User.Query.build(deactivated: true), select: u.ap_id)
281 |> Repo.all()
282
283 from(activity in query,
284 where: activity.actor not in ^deactivated_users
285 )
286 end
287
288 defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
289 end