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