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