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