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