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