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