Merge branch 'develop' into 'remove-twitter-api'
[akkoma] / lib / pleroma / web / common_api / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.FollowingRelationship
10 alias Pleroma.Notification
11 alias Pleroma.Object
12 alias Pleroma.ThreadMute
13 alias Pleroma.User
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
20
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
23
24 require Pleroma.Constants
25 require Logger
26
27 def unblock(blocker, blocked) do
28 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
29 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
30 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
31 {:ok, unblock}
32 else
33 {:fetch_block, nil} ->
34 if User.blocks?(blocker, blocked) do
35 User.unblock(blocker, blocked)
36 {:ok, :no_activity}
37 else
38 {:error, :not_blocking}
39 end
40
41 e ->
42 e
43 end
44 end
45
46 def follow(follower, followed) do
47 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
48
49 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
50 {:ok, activity} <- ActivityPub.follow(follower, followed),
51 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
52 {:ok, follower, followed, activity}
53 end
54 end
55
56 def unfollow(follower, unfollowed) do
57 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
58 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
59 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
60 {:ok, follower}
61 end
62 end
63
64 def accept_follow_request(follower, followed) do
65 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
66 {:ok, follower} <- User.follow(follower, followed),
67 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
68 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
69 {:ok, _activity} <-
70 ActivityPub.accept(%{
71 to: [follower.ap_id],
72 actor: followed,
73 object: follow_activity.data["id"],
74 type: "Accept"
75 }) do
76 {:ok, follower}
77 end
78 end
79
80 def reject_follow_request(follower, followed) do
81 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
82 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
83 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
84 {:ok, _notifications} <- Notification.dismiss(follow_activity),
85 {:ok, _activity} <-
86 ActivityPub.reject(%{
87 to: [follower.ap_id],
88 actor: followed,
89 object: follow_activity.data["id"],
90 type: "Reject"
91 }) do
92 {:ok, follower}
93 end
94 end
95
96 def delete(activity_id, user) do
97 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
98 {:find_activity, Activity.get_by_id(activity_id)},
99 {_, %Object{} = object, _} <-
100 {:find_object, Object.normalize(activity, false), activity},
101 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
102 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
103 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
104 {:ok, delete}
105 else
106 {:find_activity, _} ->
107 {:error, :not_found}
108
109 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
110 # We have the create activity, but not the object, it was probably pruned.
111 # Insert a tombstone and try again
112 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
113 {:ok, _tombstone} <- Object.create(tombstone_data) do
114 delete(activity_id, user)
115 else
116 _ ->
117 Logger.error(
118 "Could not insert tombstone for missing object on deletion. Object is #{object}."
119 )
120
121 {:error, dgettext("errors", "Could not delete")}
122 end
123
124 _ ->
125 {:error, dgettext("errors", "Could not delete")}
126 end
127 end
128
129 def repeat(id, user, params \\ %{}) do
130 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do
131 object = Object.normalize(activity)
132 announce_activity = Utils.get_existing_announce(user.ap_id, object)
133 public = public_announce?(object, params)
134
135 if announce_activity do
136 {:ok, announce_activity, object}
137 else
138 ActivityPub.announce(user, object, nil, true, public)
139 end
140 else
141 _ -> {:error, :not_found}
142 end
143 end
144
145 def unrepeat(id, user) do
146 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
147 {:find_activity, Activity.get_by_id(id)},
148 %Object{} = note <- Object.normalize(activity, false),
149 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
150 {:ok, undo, _} <- Builder.undo(user, announce),
151 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
152 {:ok, activity}
153 else
154 {:find_activity, _} -> {:error, :not_found}
155 _ -> {:error, dgettext("errors", "Could not unrepeat")}
156 end
157 end
158
159 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
160 def favorite(%User{} = user, id) do
161 case favorite_helper(user, id) do
162 {:ok, _} = res ->
163 res
164
165 {:error, :not_found} = res ->
166 res
167
168 {:error, e} ->
169 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
170 {:error, dgettext("errors", "Could not favorite")}
171 end
172 end
173
174 def favorite_helper(user, id) do
175 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
176 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
177 {_, {:ok, %Activity{} = activity, _meta}} <-
178 {:common_pipeline,
179 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
180 {:ok, activity}
181 else
182 {:find_object, _} ->
183 {:error, :not_found}
184
185 {:common_pipeline,
186 {
187 :error,
188 {
189 :validate_object,
190 {
191 :error,
192 changeset
193 }
194 }
195 }} = e ->
196 if {:object, {"already liked by this actor", []}} in changeset.errors do
197 {:ok, :already_liked}
198 else
199 {:error, e}
200 end
201
202 e ->
203 {:error, e}
204 end
205 end
206
207 def unfavorite(id, user) do
208 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
209 {:find_activity, Activity.get_by_id(id)},
210 %Object{} = note <- Object.normalize(activity, false),
211 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
212 {:ok, undo, _} <- Builder.undo(user, like),
213 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
214 {:ok, activity}
215 else
216 {:find_activity, _} -> {:error, :not_found}
217 _ -> {:error, dgettext("errors", "Could not unfavorite")}
218 end
219 end
220
221 def react_with_emoji(id, user, emoji) do
222 with %Activity{} = activity <- Activity.get_by_id(id),
223 object <- Object.normalize(activity),
224 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
225 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
226 {:ok, activity}
227 else
228 _ ->
229 {:error, dgettext("errors", "Could not add reaction emoji")}
230 end
231 end
232
233 def unreact_with_emoji(id, user, emoji) do
234 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
235 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
236 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
237 {:ok, activity}
238 else
239 _ ->
240 {:error, dgettext("errors", "Could not remove reaction emoji")}
241 end
242 end
243
244 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
245 with :ok <- validate_not_author(object, user),
246 :ok <- validate_existing_votes(user, object),
247 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
248 answer_activities =
249 Enum.map(choices, fn index ->
250 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
251
252 {:ok, activity} =
253 ActivityPub.create(%{
254 to: answer_data["to"],
255 actor: user,
256 context: object.data["context"],
257 object: answer_data,
258 additional: %{"cc" => answer_data["cc"]}
259 })
260
261 activity
262 end)
263
264 object = Object.get_cached_by_ap_id(object.data["id"])
265 {:ok, answer_activities, object}
266 end
267 end
268
269 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
270 do: {:error, dgettext("errors", "Poll's author can't vote")}
271
272 defp validate_not_author(_, _), do: :ok
273
274 defp validate_existing_votes(%{ap_id: ap_id}, object) do
275 if Utils.get_existing_votes(ap_id, object) == [] do
276 :ok
277 else
278 {:error, dgettext("errors", "Already voted")}
279 end
280 end
281
282 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
283 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
284
285 defp normalize_and_validate_choices(choices, object) do
286 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
287 {options, max_count} = get_options_and_max_count(object)
288 count = Enum.count(options)
289
290 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
291 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
292 {:ok, options, choices}
293 else
294 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
295 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
296 end
297 end
298
299 def public_announce?(_, %{visibility: visibility})
300 when visibility in ~w{public unlisted private direct},
301 do: visibility in ~w(public unlisted)
302
303 def public_announce?(object, _) do
304 Visibility.is_public?(object)
305 end
306
307 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
308
309 def get_visibility(%{visibility: visibility}, in_reply_to, _)
310 when visibility in ~w{public unlisted private direct},
311 do: {visibility, get_replied_to_visibility(in_reply_to)}
312
313 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
314 visibility = {:list, String.to_integer(list_id)}
315 {visibility, get_replied_to_visibility(in_reply_to)}
316 end
317
318 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
319 visibility = get_replied_to_visibility(in_reply_to)
320 {visibility, visibility}
321 end
322
323 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
324
325 def get_replied_to_visibility(nil), do: nil
326
327 def get_replied_to_visibility(activity) do
328 with %Object{} = object <- Object.normalize(activity) do
329 Visibility.get_visibility(object)
330 end
331 end
332
333 def check_expiry_date({:ok, nil} = res), do: res
334
335 def check_expiry_date({:ok, in_seconds}) do
336 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
337
338 if ActivityExpiration.expires_late_enough?(expiry) do
339 {:ok, expiry}
340 else
341 {:error, "Expiry date is too soon"}
342 end
343 end
344
345 def check_expiry_date(expiry_str) do
346 Ecto.Type.cast(:integer, expiry_str)
347 |> check_expiry_date()
348 end
349
350 def listen(user, %{"title" => _} = data) do
351 with visibility <- data["visibility"] || "public",
352 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
353 listen_data <-
354 Map.take(data, ["album", "artist", "title", "length"])
355 |> Map.put("type", "Audio")
356 |> Map.put("to", to)
357 |> Map.put("cc", cc)
358 |> Map.put("actor", user.ap_id),
359 {:ok, activity} <-
360 ActivityPub.listen(%{
361 actor: user,
362 to: to,
363 object: listen_data,
364 context: Utils.generate_context_id(),
365 additional: %{"cc" => cc}
366 }) do
367 {:ok, activity}
368 end
369 end
370
371 def post(user, %{status: _} = data) do
372 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
373 draft.changes
374 |> ActivityPub.create(draft.preview?)
375 |> maybe_create_activity_expiration(draft.expires_at)
376 end
377 end
378
379 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
380 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
381 {:ok, activity}
382 end
383 end
384
385 defp maybe_create_activity_expiration(result, _), do: result
386
387 def pin(id, %{ap_id: user_ap_id} = user) do
388 with %Activity{
389 actor: ^user_ap_id,
390 data: %{"type" => "Create"},
391 object: %Object{data: %{"type" => object_type}}
392 } = activity <- Activity.get_by_id_with_object(id),
393 true <- object_type in ["Note", "Article", "Question"],
394 true <- Visibility.is_public?(activity),
395 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
396 {:ok, activity}
397 else
398 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
399 _ -> {:error, dgettext("errors", "Could not pin")}
400 end
401 end
402
403 def unpin(id, user) do
404 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
405 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
406 {:ok, activity}
407 else
408 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
409 _ -> {:error, dgettext("errors", "Could not unpin")}
410 end
411 end
412
413 def add_mute(user, activity) do
414 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
415 {:ok, activity}
416 else
417 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
418 end
419 end
420
421 def remove_mute(user, activity) do
422 ThreadMute.remove_mute(user.id, activity.data["context"])
423 {:ok, activity}
424 end
425
426 def thread_muted?(%{id: nil} = _user, _activity), do: false
427
428 def thread_muted?(user, activity) do
429 ThreadMute.exists?(user.id, activity.data["context"])
430 end
431
432 def report(user, data) do
433 with {:ok, account} <- get_reported_account(data.account_id),
434 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
435 {:ok, statuses} <- get_report_statuses(account, data) do
436 ActivityPub.flag(%{
437 context: Utils.generate_context_id(),
438 actor: user,
439 account: account,
440 statuses: statuses,
441 content: content_html,
442 forward: Map.get(data, :forward, false)
443 })
444 end
445 end
446
447 defp get_reported_account(account_id) do
448 case User.get_cached_by_id(account_id) do
449 %User{} = account -> {:ok, account}
450 _ -> {:error, dgettext("errors", "Account not found")}
451 end
452 end
453
454 def update_report_state(activity_ids, state) when is_list(activity_ids) do
455 case Utils.update_report_state(activity_ids, state) do
456 :ok -> {:ok, activity_ids}
457 _ -> {:error, dgettext("errors", "Could not update state")}
458 end
459 end
460
461 def update_report_state(activity_id, state) do
462 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
463 Utils.update_report_state(activity, state)
464 else
465 nil -> {:error, :not_found}
466 _ -> {:error, dgettext("errors", "Could not update state")}
467 end
468 end
469
470 def update_activity_scope(activity_id, opts \\ %{}) do
471 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
472 {:ok, activity} <- toggle_sensitive(activity, opts) do
473 set_visibility(activity, opts)
474 else
475 nil -> {:error, :not_found}
476 {:error, reason} -> {:error, reason}
477 end
478 end
479
480 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
481 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
482 end
483
484 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
485 when is_boolean(sensitive) do
486 new_data = Map.put(object.data, "sensitive", sensitive)
487
488 {:ok, object} =
489 object
490 |> Object.change(%{data: new_data})
491 |> Object.update_and_set_cache()
492
493 {:ok, Map.put(activity, :object, object)}
494 end
495
496 defp toggle_sensitive(activity, _), do: {:ok, activity}
497
498 defp set_visibility(activity, %{visibility: visibility}) do
499 Utils.update_activity_visibility(activity, visibility)
500 end
501
502 defp set_visibility(activity, _), do: {:ok, activity}
503
504 def hide_reblogs(%User{} = user, %User{} = target) do
505 UserRelationship.create_reblog_mute(user, target)
506 end
507
508 def show_reblogs(%User{} = user, %User{} = target) do
509 UserRelationship.delete_reblog_mute(user, target)
510 end
511 end