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