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