Introduce new ingestion pipeline structure, implement internal Likes with it.
[akkoma] / lib / pleroma / web / common_api / common_api.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.Web.CommonAPI do
6 alias Pleroma.Activity
7 alias Pleroma.ActivityExpiration
8 alias Pleroma.Conversation.Participation
9 alias Pleroma.Object
10 alias Pleroma.ThreadMute
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Builder
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.ActivityPub.Visibility
16
17 import Pleroma.Web.Gettext
18 import Pleroma.Web.CommonAPI.Utils
19
20 require Pleroma.Constants
21 require Logger
22
23 def follow(follower, followed) do
24 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
25
26 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
27 {:ok, activity} <- ActivityPub.follow(follower, followed),
28 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
29 {:ok, follower, followed, activity}
30 end
31 end
32
33 def unfollow(follower, unfollowed) do
34 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
35 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
36 {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
37 {:ok, follower}
38 end
39 end
40
41 def accept_follow_request(follower, followed) do
42 with {:ok, follower} <- User.follow(follower, followed),
43 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
44 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
45 {:ok, _activity} <-
46 ActivityPub.accept(%{
47 to: [follower.ap_id],
48 actor: followed,
49 object: follow_activity.data["id"],
50 type: "Accept"
51 }) do
52 {:ok, follower}
53 end
54 end
55
56 def reject_follow_request(follower, followed) do
57 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
58 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
59 {:ok, _activity} <-
60 ActivityPub.reject(%{
61 to: [follower.ap_id],
62 actor: followed,
63 object: follow_activity.data["id"],
64 type: "Reject"
65 }) do
66 {:ok, follower}
67 end
68 end
69
70 def delete(activity_id, user) do
71 with %Activity{data: %{"object" => _}} = activity <-
72 Activity.get_by_id_with_object(activity_id),
73 %Object{} = object <- Object.normalize(activity),
74 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
75 {:ok, _} <- unpin(activity_id, user),
76 {:ok, delete} <- ActivityPub.delete(object) do
77 {:ok, delete}
78 else
79 _ -> {:error, dgettext("errors", "Could not delete")}
80 end
81 end
82
83 def repeat(id_or_ap_id, user, params \\ %{}) do
84 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
85 object <- Object.normalize(activity),
86 nil <- Utils.get_existing_announce(user.ap_id, object),
87 public <- public_announce?(object, params) do
88 ActivityPub.announce(user, object, nil, true, public)
89 else
90 _ -> {:error, dgettext("errors", "Could not repeat")}
91 end
92 end
93
94 def unrepeat(id_or_ap_id, user) do
95 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
96 object = Object.normalize(activity)
97 ActivityPub.unannounce(user, object)
98 else
99 _ -> {:error, dgettext("errors", "Could not unrepeat")}
100 end
101 end
102
103 @spec favorite(User.t(), binary()) :: {:ok, Activity.t()} | {:error, any()}
104 def favorite(%User{} = user, id) do
105 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
106 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
107 {_, {:ok, %Activity{} = activity, _meta}} <-
108 {:common_pipeline,
109 ActivityPub.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
110 {:ok, activity}
111 else
112 e ->
113 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
114 {:error, dgettext("errors", "Could not favorite")}
115 end
116 end
117
118 # def favorite(id_or_ap_id, user) do
119 # with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
120 # object <- Object.normalize(activity),
121 # nil <- Utils.get_existing_like(user.ap_id, object) do
122 # ActivityPub.like(user, object)
123 # else
124 # _ -> {:error, dgettext("errors", "Could not favorite")}
125 # end
126 # end
127
128 def unfavorite(id_or_ap_id, user) do
129 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
130 object = Object.normalize(activity)
131 ActivityPub.unlike(user, object)
132 else
133 _ -> {:error, dgettext("errors", "Could not unfavorite")}
134 end
135 end
136
137 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
138 with :ok <- validate_not_author(object, user),
139 :ok <- validate_existing_votes(user, object),
140 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
141 answer_activities =
142 Enum.map(choices, fn index ->
143 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
144
145 {:ok, activity} =
146 ActivityPub.create(%{
147 to: answer_data["to"],
148 actor: user,
149 context: object.data["context"],
150 object: answer_data,
151 additional: %{"cc" => answer_data["cc"]}
152 })
153
154 activity
155 end)
156
157 object = Object.get_cached_by_ap_id(object.data["id"])
158 {:ok, answer_activities, object}
159 end
160 end
161
162 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
163 do: {:error, dgettext("errors", "Poll's author can't vote")}
164
165 defp validate_not_author(_, _), do: :ok
166
167 defp validate_existing_votes(%{ap_id: ap_id}, object) do
168 if Utils.get_existing_votes(ap_id, object) == [] do
169 :ok
170 else
171 {:error, dgettext("errors", "Already voted")}
172 end
173 end
174
175 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
176 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
177
178 defp normalize_and_validate_choices(choices, object) do
179 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
180 {options, max_count} = get_options_and_max_count(object)
181 count = Enum.count(options)
182
183 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
184 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
185 {:ok, options, choices}
186 else
187 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
188 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
189 end
190 end
191
192 def public_announce?(_, %{"visibility" => visibility})
193 when visibility in ~w{public unlisted private direct},
194 do: visibility in ~w(public unlisted)
195
196 def public_announce?(object, _) do
197 Visibility.is_public?(object)
198 end
199
200 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
201
202 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
203 when visibility in ~w{public unlisted private direct},
204 do: {visibility, get_replied_to_visibility(in_reply_to)}
205
206 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
207 visibility = {:list, String.to_integer(list_id)}
208 {visibility, get_replied_to_visibility(in_reply_to)}
209 end
210
211 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
212 visibility = get_replied_to_visibility(in_reply_to)
213 {visibility, visibility}
214 end
215
216 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
217
218 def get_replied_to_visibility(nil), do: nil
219
220 def get_replied_to_visibility(activity) do
221 with %Object{} = object <- Object.normalize(activity) do
222 Visibility.get_visibility(object)
223 end
224 end
225
226 def check_expiry_date({:ok, nil} = res), do: res
227
228 def check_expiry_date({:ok, in_seconds}) do
229 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
230
231 if ActivityExpiration.expires_late_enough?(expiry) do
232 {:ok, expiry}
233 else
234 {:error, "Expiry date is too soon"}
235 end
236 end
237
238 def check_expiry_date(expiry_str) do
239 Ecto.Type.cast(:integer, expiry_str)
240 |> check_expiry_date()
241 end
242
243 def listen(user, %{"title" => _} = data) do
244 with visibility <- data["visibility"] || "public",
245 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
246 listen_data <-
247 Map.take(data, ["album", "artist", "title", "length"])
248 |> Map.put("type", "Audio")
249 |> Map.put("to", to)
250 |> Map.put("cc", cc)
251 |> Map.put("actor", user.ap_id),
252 {:ok, activity} <-
253 ActivityPub.listen(%{
254 actor: user,
255 to: to,
256 object: listen_data,
257 context: Utils.generate_context_id(),
258 additional: %{"cc" => cc}
259 }) do
260 {:ok, activity}
261 end
262 end
263
264 def post(user, %{"status" => _} = data) do
265 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
266 draft.changes
267 |> ActivityPub.create(draft.preview?)
268 |> maybe_create_activity_expiration(draft.expires_at)
269 end
270 end
271
272 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
273 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
274 {:ok, activity}
275 end
276 end
277
278 defp maybe_create_activity_expiration(result, _), do: result
279
280 # Updates the emojis for a user based on their profile
281 def update(user) do
282 emoji = emoji_from_profile(user)
283 source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
284
285 user =
286 case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
287 {:ok, user} -> user
288 _ -> user
289 end
290
291 ActivityPub.update(%{
292 local: true,
293 to: [Pleroma.Constants.as_public(), user.follower_address],
294 cc: [],
295 actor: user.ap_id,
296 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
297 })
298 end
299
300 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
301 with %Activity{
302 actor: ^user_ap_id,
303 data: %{"type" => "Create"},
304 object: %Object{data: %{"type" => "Note"}}
305 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
306 true <- Visibility.is_public?(activity),
307 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
308 {:ok, activity}
309 else
310 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
311 _ -> {:error, dgettext("errors", "Could not pin")}
312 end
313 end
314
315 def unpin(id_or_ap_id, user) do
316 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
317 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
318 {:ok, activity}
319 else
320 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
321 _ -> {:error, dgettext("errors", "Could not unpin")}
322 end
323 end
324
325 def add_mute(user, activity) do
326 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
327 {:ok, activity}
328 else
329 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
330 end
331 end
332
333 def remove_mute(user, activity) do
334 ThreadMute.remove_mute(user.id, activity.data["context"])
335 {:ok, activity}
336 end
337
338 def thread_muted?(%{id: nil} = _user, _activity), do: false
339
340 def thread_muted?(user, activity) do
341 ThreadMute.check_muted(user.id, activity.data["context"]) != []
342 end
343
344 def report(user, %{"account_id" => account_id} = data) do
345 with {:ok, account} <- get_reported_account(account_id),
346 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
347 {:ok, statuses} <- get_report_statuses(account, data) do
348 ActivityPub.flag(%{
349 context: Utils.generate_context_id(),
350 actor: user,
351 account: account,
352 statuses: statuses,
353 content: content_html,
354 forward: data["forward"] || false
355 })
356 end
357 end
358
359 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
360
361 defp get_reported_account(account_id) do
362 case User.get_cached_by_id(account_id) do
363 %User{} = account -> {:ok, account}
364 _ -> {:error, dgettext("errors", "Account not found")}
365 end
366 end
367
368 def update_report_state(activity_id, state) do
369 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
370 Utils.update_report_state(activity, state)
371 else
372 nil -> {:error, :not_found}
373 _ -> {:error, dgettext("errors", "Could not update state")}
374 end
375 end
376
377 def update_activity_scope(activity_id, opts \\ %{}) do
378 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
379 {:ok, activity} <- toggle_sensitive(activity, opts) do
380 set_visibility(activity, opts)
381 else
382 nil -> {:error, :not_found}
383 {:error, reason} -> {:error, reason}
384 end
385 end
386
387 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
388 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
389 end
390
391 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
392 when is_boolean(sensitive) do
393 new_data = Map.put(object.data, "sensitive", sensitive)
394
395 {:ok, object} =
396 object
397 |> Object.change(%{data: new_data})
398 |> Object.update_and_set_cache()
399
400 {:ok, Map.put(activity, :object, object)}
401 end
402
403 defp toggle_sensitive(activity, _), do: {:ok, activity}
404
405 defp set_visibility(activity, %{"visibility" => visibility}) do
406 Utils.update_activity_visibility(activity, visibility)
407 end
408
409 defp set_visibility(activity, _), do: {:ok, activity}
410
411 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
412 if ap_id not in user.info.muted_reblogs do
413 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
414 end
415 end
416
417 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
418 if ap_id in user.info.muted_reblogs do
419 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
420 end
421 end
422 end