Merge branch 'develop' into global-status-expiration
[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),
131 object = %Object{} <- Object.normalize(activity, false),
132 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
133 public = public_announce?(object, params),
134 {:ok, announce, _} <- Builder.announce(user, object, public: public),
135 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
136 {:ok, activity}
137 else
138 {:existing_announce, %Activity{} = announce} ->
139 {:ok, announce}
140
141 _ ->
142 {:error, :not_found}
143 end
144 end
145
146 def unrepeat(id, user) do
147 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
148 {:find_activity, Activity.get_by_id(id)},
149 %Object{} = note <- Object.normalize(activity, false),
150 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
151 {:ok, undo, _} <- Builder.undo(user, announce),
152 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
153 {:ok, activity}
154 else
155 {:find_activity, _} -> {:error, :not_found}
156 _ -> {:error, dgettext("errors", "Could not unrepeat")}
157 end
158 end
159
160 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
161 def favorite(%User{} = user, id) do
162 case favorite_helper(user, id) do
163 {:ok, _} = res ->
164 res
165
166 {:error, :not_found} = res ->
167 res
168
169 {:error, e} ->
170 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
171 {:error, dgettext("errors", "Could not favorite")}
172 end
173 end
174
175 def favorite_helper(user, id) do
176 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
177 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
178 {_, {:ok, %Activity{} = activity, _meta}} <-
179 {:common_pipeline,
180 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
181 {:ok, activity}
182 else
183 {:find_object, _} ->
184 {:error, :not_found}
185
186 {:common_pipeline,
187 {
188 :error,
189 {
190 :validate_object,
191 {
192 :error,
193 changeset
194 }
195 }
196 }} = e ->
197 if {:object, {"already liked by this actor", []}} in changeset.errors do
198 {:ok, :already_liked}
199 else
200 {:error, e}
201 end
202
203 e ->
204 {:error, e}
205 end
206 end
207
208 def unfavorite(id, user) do
209 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
210 {:find_activity, Activity.get_by_id(id)},
211 %Object{} = note <- Object.normalize(activity, false),
212 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
213 {:ok, undo, _} <- Builder.undo(user, like),
214 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
215 {:ok, activity}
216 else
217 {:find_activity, _} -> {:error, :not_found}
218 _ -> {:error, dgettext("errors", "Could not unfavorite")}
219 end
220 end
221
222 def react_with_emoji(id, user, emoji) do
223 with %Activity{} = activity <- Activity.get_by_id(id),
224 object <- Object.normalize(activity),
225 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
226 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
227 {:ok, activity}
228 else
229 _ ->
230 {:error, dgettext("errors", "Could not add reaction emoji")}
231 end
232 end
233
234 def unreact_with_emoji(id, user, emoji) do
235 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
236 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
237 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
238 {:ok, activity}
239 else
240 _ ->
241 {:error, dgettext("errors", "Could not remove reaction emoji")}
242 end
243 end
244
245 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
246 with :ok <- validate_not_author(object, user),
247 :ok <- validate_existing_votes(user, object),
248 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
249 answer_activities =
250 Enum.map(choices, fn index ->
251 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
252
253 {:ok, activity} =
254 ActivityPub.create(%{
255 to: answer_data["to"],
256 actor: user,
257 context: object.data["context"],
258 object: answer_data,
259 additional: %{"cc" => answer_data["cc"]}
260 })
261
262 activity
263 end)
264
265 object = Object.get_cached_by_ap_id(object.data["id"])
266 {:ok, answer_activities, object}
267 end
268 end
269
270 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
271 do: {:error, dgettext("errors", "Poll's author can't vote")}
272
273 defp validate_not_author(_, _), do: :ok
274
275 defp validate_existing_votes(%{ap_id: ap_id}, object) do
276 if Utils.get_existing_votes(ap_id, object) == [] do
277 :ok
278 else
279 {:error, dgettext("errors", "Already voted")}
280 end
281 end
282
283 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
284 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
285
286 defp normalize_and_validate_choices(choices, object) do
287 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
288 {options, max_count} = get_options_and_max_count(object)
289 count = Enum.count(options)
290
291 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
292 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
293 {:ok, options, choices}
294 else
295 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
296 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
297 end
298 end
299
300 def public_announce?(_, %{visibility: visibility})
301 when visibility in ~w{public unlisted private direct},
302 do: visibility in ~w(public unlisted)
303
304 def public_announce?(object, _) do
305 Visibility.is_public?(object)
306 end
307
308 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
309
310 def get_visibility(%{visibility: visibility}, in_reply_to, _)
311 when visibility in ~w{public unlisted private direct},
312 do: {visibility, get_replied_to_visibility(in_reply_to)}
313
314 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
315 visibility = {:list, String.to_integer(list_id)}
316 {visibility, get_replied_to_visibility(in_reply_to)}
317 end
318
319 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
320 visibility = get_replied_to_visibility(in_reply_to)
321 {visibility, visibility}
322 end
323
324 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
325
326 def get_replied_to_visibility(nil), do: nil
327
328 def get_replied_to_visibility(activity) do
329 with %Object{} = object <- Object.normalize(activity) do
330 Visibility.get_visibility(object)
331 end
332 end
333
334 def check_expiry_date({:ok, nil} = res), do: res
335
336 def check_expiry_date({:ok, in_seconds}) do
337 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
338
339 if ActivityExpiration.expires_late_enough?(expiry) do
340 {:ok, expiry}
341 else
342 {:error, "Expiry date is too soon"}
343 end
344 end
345
346 def check_expiry_date(expiry_str) do
347 Ecto.Type.cast(:integer, expiry_str)
348 |> check_expiry_date()
349 end
350
351 def listen(user, data) do
352 visibility = Map.get(data, :visibility, "public")
353
354 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
355 listen_data <-
356 data
357 |> Map.take([:album, :artist, :title, :length])
358 |> Map.new(fn {key, value} -> {to_string(key), value} end)
359 |> Map.put("type", "Audio")
360 |> Map.put("to", to)
361 |> Map.put("cc", cc)
362 |> Map.put("actor", user.ap_id),
363 {:ok, activity} <-
364 ActivityPub.listen(%{
365 actor: user,
366 to: to,
367 object: listen_data,
368 context: Utils.generate_context_id(),
369 additional: %{"cc" => cc}
370 }) do
371 {:ok, activity}
372 end
373 end
374
375 def post(user, %{status: _} = data) do
376 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
377 ActivityPub.create(draft.changes, draft.preview?)
378 end
379 end
380
381 def pin(id, %{ap_id: user_ap_id} = user) do
382 with %Activity{
383 actor: ^user_ap_id,
384 data: %{"type" => "Create"},
385 object: %Object{data: %{"type" => object_type}}
386 } = activity <- Activity.get_by_id_with_object(id),
387 true <- object_type in ["Note", "Article", "Question"],
388 true <- Visibility.is_public?(activity),
389 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
390 {:ok, activity}
391 else
392 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
393 _ -> {:error, dgettext("errors", "Could not pin")}
394 end
395 end
396
397 def unpin(id, user) do
398 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
399 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
400 {:ok, activity}
401 else
402 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
403 _ -> {:error, dgettext("errors", "Could not unpin")}
404 end
405 end
406
407 def add_mute(user, activity) do
408 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
409 {:ok, activity}
410 else
411 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
412 end
413 end
414
415 def remove_mute(user, activity) do
416 ThreadMute.remove_mute(user.id, activity.data["context"])
417 {:ok, activity}
418 end
419
420 def thread_muted?(%{id: nil} = _user, _activity), do: false
421
422 def thread_muted?(user, activity) do
423 ThreadMute.exists?(user.id, activity.data["context"])
424 end
425
426 def report(user, data) do
427 with {:ok, account} <- get_reported_account(data.account_id),
428 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
429 {:ok, statuses} <- get_report_statuses(account, data) do
430 ActivityPub.flag(%{
431 context: Utils.generate_context_id(),
432 actor: user,
433 account: account,
434 statuses: statuses,
435 content: content_html,
436 forward: Map.get(data, :forward, false)
437 })
438 end
439 end
440
441 defp get_reported_account(account_id) do
442 case User.get_cached_by_id(account_id) do
443 %User{} = account -> {:ok, account}
444 _ -> {:error, dgettext("errors", "Account not found")}
445 end
446 end
447
448 def update_report_state(activity_ids, state) when is_list(activity_ids) do
449 case Utils.update_report_state(activity_ids, state) do
450 :ok -> {:ok, activity_ids}
451 _ -> {:error, dgettext("errors", "Could not update state")}
452 end
453 end
454
455 def update_report_state(activity_id, state) do
456 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
457 Utils.update_report_state(activity, state)
458 else
459 nil -> {:error, :not_found}
460 _ -> {:error, dgettext("errors", "Could not update state")}
461 end
462 end
463
464 def update_activity_scope(activity_id, opts \\ %{}) do
465 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
466 {:ok, activity} <- toggle_sensitive(activity, opts) do
467 set_visibility(activity, opts)
468 else
469 nil -> {:error, :not_found}
470 {:error, reason} -> {:error, reason}
471 end
472 end
473
474 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
475 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
476 end
477
478 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
479 when is_boolean(sensitive) do
480 new_data = Map.put(object.data, "sensitive", sensitive)
481
482 {:ok, object} =
483 object
484 |> Object.change(%{data: new_data})
485 |> Object.update_and_set_cache()
486
487 {:ok, Map.put(activity, :object, object)}
488 end
489
490 defp toggle_sensitive(activity, _), do: {:ok, activity}
491
492 defp set_visibility(activity, %{visibility: visibility}) do
493 Utils.update_activity_visibility(activity, visibility)
494 end
495
496 defp set_visibility(activity, _), do: {:ok, activity}
497
498 def hide_reblogs(%User{} = user, %User{} = target) do
499 UserRelationship.create_reblog_mute(user, target)
500 end
501
502 def show_reblogs(%User{} = user, %User{} = target) do
503 UserRelationship.delete_reblog_mute(user, target)
504 end
505 end