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