b649d1c62c703d2038fe7cacbf6b7d2d8066296b
[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 vote(user, %{data: %{"type" => "Question"}} = object, choices) do
132 with :ok <- validate_not_author(object, user),
133 :ok <- validate_existing_votes(user, object),
134 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
135 answer_activities =
136 Enum.map(choices, fn index ->
137 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
138
139 {:ok, activity} =
140 ActivityPub.create(%{
141 to: answer_data["to"],
142 actor: user,
143 context: object.data["context"],
144 object: answer_data,
145 additional: %{"cc" => answer_data["cc"]}
146 })
147
148 activity
149 end)
150
151 object = Object.get_cached_by_ap_id(object.data["id"])
152 {:ok, answer_activities, object}
153 end
154 end
155
156 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
157 do: {:error, dgettext("errors", "Poll's author can't vote")}
158
159 defp validate_not_author(_, _), do: :ok
160
161 defp validate_existing_votes(%{ap_id: ap_id}, object) do
162 if Utils.get_existing_votes(ap_id, object) == [] do
163 :ok
164 else
165 {:error, dgettext("errors", "Already voted")}
166 end
167 end
168
169 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
170 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
171
172 defp normalize_and_validate_choices(choices, object) do
173 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
174 {options, max_count} = get_options_and_max_count(object)
175 count = Enum.count(options)
176
177 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
178 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
179 {:ok, options, choices}
180 else
181 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
182 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
183 end
184 end
185
186 def public_announce?(_, %{"visibility" => visibility})
187 when visibility in ~w{public unlisted private direct},
188 do: visibility in ~w(public unlisted)
189
190 def public_announce?(object, _) do
191 Visibility.is_public?(object)
192 end
193
194 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
195
196 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
197 when visibility in ~w{public unlisted private direct},
198 do: {visibility, get_replied_to_visibility(in_reply_to)}
199
200 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
201 visibility = {:list, String.to_integer(list_id)}
202 {visibility, get_replied_to_visibility(in_reply_to)}
203 end
204
205 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
206 visibility = get_replied_to_visibility(in_reply_to)
207 {visibility, visibility}
208 end
209
210 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
211
212 def get_replied_to_visibility(nil), do: nil
213
214 def get_replied_to_visibility(activity) do
215 with %Object{} = object <- Object.normalize(activity) do
216 Visibility.get_visibility(object)
217 end
218 end
219
220 def check_expiry_date({:ok, nil} = res), do: res
221
222 def check_expiry_date({:ok, in_seconds}) do
223 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
224
225 if ActivityExpiration.expires_late_enough?(expiry) do
226 {:ok, expiry}
227 else
228 {:error, "Expiry date is too soon"}
229 end
230 end
231
232 def check_expiry_date(expiry_str) do
233 Ecto.Type.cast(:integer, expiry_str)
234 |> check_expiry_date()
235 end
236
237 def listen(user, %{"title" => _} = data) do
238 with visibility <- data["visibility"] || "public",
239 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
240 listen_data <-
241 Map.take(data, ["album", "artist", "title", "length"])
242 |> Map.put("type", "Audio")
243 |> Map.put("to", to)
244 |> Map.put("cc", cc)
245 |> Map.put("actor", user.ap_id),
246 {:ok, activity} <-
247 ActivityPub.listen(%{
248 actor: user,
249 to: to,
250 object: listen_data,
251 context: Utils.generate_context_id(),
252 additional: %{"cc" => cc}
253 }) do
254 {:ok, activity}
255 end
256 end
257
258 def post(user, %{"status" => _} = data) do
259 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
260 draft.changes
261 |> ActivityPub.create(draft.preview?)
262 |> maybe_create_activity_expiration(draft.expires_at)
263 end
264 end
265
266 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
267 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
268 {:ok, activity}
269 end
270 end
271
272 defp maybe_create_activity_expiration(result, _), do: result
273
274 # Updates the emojis for a user based on their profile
275 def update(user) do
276 emoji = emoji_from_profile(user)
277 source_data = Map.put(user.source_data, "tag", emoji)
278
279 user =
280 case User.update_source_data(user, source_data) do
281 {:ok, user} -> user
282 _ -> user
283 end
284
285 ActivityPub.update(%{
286 local: true,
287 to: [Pleroma.Constants.as_public(), user.follower_address],
288 cc: [],
289 actor: user.ap_id,
290 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
291 })
292 end
293
294 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
295 with %Activity{
296 actor: ^user_ap_id,
297 data: %{"type" => "Create"},
298 object: %Object{data: %{"type" => "Note"}}
299 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
300 true <- Visibility.is_public?(activity),
301 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
302 {:ok, activity}
303 else
304 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
305 _ -> {:error, dgettext("errors", "Could not pin")}
306 end
307 end
308
309 def unpin(id_or_ap_id, user) do
310 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
311 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
312 {:ok, activity}
313 else
314 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
315 _ -> {:error, dgettext("errors", "Could not unpin")}
316 end
317 end
318
319 def add_mute(user, activity) do
320 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
321 {:ok, activity}
322 else
323 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
324 end
325 end
326
327 def remove_mute(user, activity) do
328 ThreadMute.remove_mute(user.id, activity.data["context"])
329 {:ok, activity}
330 end
331
332 def thread_muted?(%{id: nil} = _user, _activity), do: false
333
334 def thread_muted?(user, activity) do
335 ThreadMute.check_muted(user.id, activity.data["context"]) != []
336 end
337
338 def report(user, %{"account_id" => account_id} = data) do
339 with {:ok, account} <- get_reported_account(account_id),
340 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
341 {:ok, statuses} <- get_report_statuses(account, data) do
342 ActivityPub.flag(%{
343 context: Utils.generate_context_id(),
344 actor: user,
345 account: account,
346 statuses: statuses,
347 content: content_html,
348 forward: data["forward"] || false
349 })
350 end
351 end
352
353 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
354
355 defp get_reported_account(account_id) do
356 case User.get_cached_by_id(account_id) do
357 %User{} = account -> {:ok, account}
358 _ -> {:error, dgettext("errors", "Account not found")}
359 end
360 end
361
362 def update_report_state(activity_id, state) do
363 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
364 Utils.update_report_state(activity, state)
365 else
366 nil -> {:error, :not_found}
367 _ -> {:error, dgettext("errors", "Could not update state")}
368 end
369 end
370
371 def update_activity_scope(activity_id, opts \\ %{}) do
372 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
373 {:ok, activity} <- toggle_sensitive(activity, opts) do
374 set_visibility(activity, opts)
375 else
376 nil -> {:error, :not_found}
377 {:error, reason} -> {:error, reason}
378 end
379 end
380
381 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
382 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
383 end
384
385 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
386 when is_boolean(sensitive) do
387 new_data = Map.put(object.data, "sensitive", sensitive)
388
389 {:ok, object} =
390 object
391 |> Object.change(%{data: new_data})
392 |> Object.update_and_set_cache()
393
394 {:ok, Map.put(activity, :object, object)}
395 end
396
397 defp toggle_sensitive(activity, _), do: {:ok, activity}
398
399 defp set_visibility(activity, %{"visibility" => visibility}) do
400 Utils.update_activity_visibility(activity, visibility)
401 end
402
403 defp set_visibility(activity, _), do: {:ok, activity}
404
405 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
406 if ap_id not in user.muted_reblogs do
407 User.add_reblog_mute(user, ap_id)
408 end
409 end
410
411 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
412 if ap_id in user.muted_reblogs do
413 User.remove_reblog_mute(user, ap_id)
414 end
415 end
416 end