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