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