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