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