Merge branch 'develop' into global-status-expiration
[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 ActivityPub.create(draft.changes, draft.preview?)
327 end
328 end
329
330 def pin(id, %{ap_id: user_ap_id} = user) do
331 with %Activity{
332 actor: ^user_ap_id,
333 data: %{"type" => "Create"},
334 object: %Object{data: %{"type" => object_type}}
335 } = activity <- Activity.get_by_id_with_object(id),
336 true <- object_type in ["Note", "Article", "Question"],
337 true <- Visibility.is_public?(activity),
338 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
339 {:ok, activity}
340 else
341 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
342 _ -> {:error, dgettext("errors", "Could not pin")}
343 end
344 end
345
346 def unpin(id, user) do
347 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
348 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
349 {:ok, activity}
350 else
351 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
352 _ -> {:error, dgettext("errors", "Could not unpin")}
353 end
354 end
355
356 def add_mute(user, activity) do
357 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
358 {:ok, activity}
359 else
360 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
361 end
362 end
363
364 def remove_mute(user, activity) do
365 ThreadMute.remove_mute(user.id, activity.data["context"])
366 {:ok, activity}
367 end
368
369 def thread_muted?(%{id: nil} = _user, _activity), do: false
370
371 def thread_muted?(user, activity) do
372 ThreadMute.exists?(user.id, activity.data["context"])
373 end
374
375 def report(user, data) do
376 with {:ok, account} <- get_reported_account(data.account_id),
377 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
378 {:ok, statuses} <- get_report_statuses(account, data) do
379 ActivityPub.flag(%{
380 context: Utils.generate_context_id(),
381 actor: user,
382 account: account,
383 statuses: statuses,
384 content: content_html,
385 forward: Map.get(data, :forward, false)
386 })
387 end
388 end
389
390 defp get_reported_account(account_id) do
391 case User.get_cached_by_id(account_id) do
392 %User{} = account -> {:ok, account}
393 _ -> {:error, dgettext("errors", "Account not found")}
394 end
395 end
396
397 def update_report_state(activity_ids, state) when is_list(activity_ids) do
398 case Utils.update_report_state(activity_ids, state) do
399 :ok -> {:ok, activity_ids}
400 _ -> {:error, dgettext("errors", "Could not update state")}
401 end
402 end
403
404 def update_report_state(activity_id, state) do
405 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
406 Utils.update_report_state(activity, state)
407 else
408 nil -> {:error, :not_found}
409 _ -> {:error, dgettext("errors", "Could not update state")}
410 end
411 end
412
413 def update_activity_scope(activity_id, opts \\ %{}) do
414 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
415 {:ok, activity} <- toggle_sensitive(activity, opts) do
416 set_visibility(activity, opts)
417 else
418 nil -> {:error, :not_found}
419 {:error, reason} -> {:error, reason}
420 end
421 end
422
423 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
424 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
425 end
426
427 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
428 when is_boolean(sensitive) do
429 new_data = Map.put(object.data, "sensitive", sensitive)
430
431 {:ok, object} =
432 object
433 |> Object.change(%{data: new_data})
434 |> Object.update_and_set_cache()
435
436 {:ok, Map.put(activity, :object, object)}
437 end
438
439 defp toggle_sensitive(activity, _), do: {:ok, activity}
440
441 defp set_visibility(activity, %{"visibility" => visibility}) do
442 Utils.update_activity_visibility(activity, visibility)
443 end
444
445 defp set_visibility(activity, _), do: {:ok, activity}
446
447 def hide_reblogs(%User{} = user, %User{} = target) do
448 UserRelationship.create_reblog_mute(user, target)
449 end
450
451 def show_reblogs(%User{} = user, %User{} = target) do
452 UserRelationship.delete_reblog_mute(user, target)
453 end
454 end