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