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