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