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