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