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.Emoji
10 alias Pleroma.Object
11 alias Pleroma.ThreadMute
12 alias Pleroma.User
13 alias Pleroma.Web.ActivityPub.ActivityPub
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 def follow(follower, followed) do
21 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
22 {:ok, activity} <- ActivityPub.follow(follower, followed),
23 {:ok, follower, followed} <-
24 User.wait_and_refresh(
25 Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
26 follower,
27 followed
28 ) 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 _ ->
80 {:error, dgettext("errors", "Could not delete")}
81 end
82 end
83
84 def repeat(id_or_ap_id, user) do
85 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
86 object <- Object.normalize(activity),
87 nil <- Utils.get_existing_announce(user.ap_id, object) do
88 ActivityPub.announce(user, object)
89 else
90 _ ->
91 {:error, dgettext("errors", "Could not repeat")}
92 end
93 end
94
95 def unrepeat(id_or_ap_id, user) do
96 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
97 object <- Object.normalize(activity) do
98 ActivityPub.unannounce(user, object)
99 else
100 _ ->
101 {:error, dgettext("errors", "Could not unrepeat")}
102 end
103 end
104
105 def favorite(id_or_ap_id, user) do
106 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
107 object <- Object.normalize(activity),
108 nil <- Utils.get_existing_like(user.ap_id, object) do
109 ActivityPub.like(user, object)
110 else
111 _ ->
112 {:error, dgettext("errors", "Could not favorite")}
113 end
114 end
115
116 def unfavorite(id_or_ap_id, user) do
117 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
118 object <- Object.normalize(activity) do
119 ActivityPub.unlike(user, object)
120 else
121 _ ->
122 {:error, dgettext("errors", "Could not unfavorite")}
123 end
124 end
125
126 def vote(user, object, choices) do
127 with "Question" <- object.data["type"],
128 {:author, false} <- {:author, object.data["actor"] == user.ap_id},
129 {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
130 {options, max_count} <- get_options_and_max_count(object),
131 option_count <- Enum.count(options),
132 {:choice_check, {choices, true}} <-
133 {:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
134 {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
135 answer_activities =
136 Enum.map(choices, fn index ->
137 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
138
139 {:ok, activity} =
140 ActivityPub.create(%{
141 to: answer_data["to"],
142 actor: user,
143 context: object.data["context"],
144 object: answer_data,
145 additional: %{"cc" => answer_data["cc"]}
146 })
147
148 activity
149 end)
150
151 object = Object.get_cached_by_ap_id(object.data["id"])
152 {:ok, answer_activities, object}
153 else
154 {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
155 {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
156 {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
157 {:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
158 end
159 end
160
161 defp get_options_and_max_count(object) do
162 if Map.has_key?(object.data, "anyOf") do
163 {object.data["anyOf"], Enum.count(object.data["anyOf"])}
164 else
165 {object.data["oneOf"], 1}
166 end
167 end
168
169 defp normalize_and_validate_choice_indices(choices, count) do
170 Enum.map_reduce(choices, true, fn index, valid ->
171 index = if is_binary(index), do: String.to_integer(index), else: index
172 {index, if(valid, do: index < count, else: valid)}
173 end)
174 end
175
176 def get_visibility(_, _, %Participation{}) do
177 {"direct", "direct"}
178 end
179
180 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
181 when visibility in ~w{public unlisted private direct},
182 do: {visibility, get_replied_to_visibility(in_reply_to)}
183
184 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
185 visibility = {:list, String.to_integer(list_id)}
186 {visibility, get_replied_to_visibility(in_reply_to)}
187 end
188
189 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
190 visibility = get_replied_to_visibility(in_reply_to)
191 {visibility, visibility}
192 end
193
194 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
195
196 def get_replied_to_visibility(nil), do: nil
197
198 def get_replied_to_visibility(activity) do
199 with %Object{} = object <- Object.normalize(activity) do
200 Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
201 end
202 end
203
204 defp check_expiry_date({:ok, nil} = res), do: res
205
206 defp check_expiry_date({:ok, in_seconds}) do
207 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
208
209 if ActivityExpiration.expires_late_enough?(expiry) do
210 {:ok, expiry}
211 else
212 {:error, "Expiry date is too soon"}
213 end
214 end
215
216 defp check_expiry_date(expiry_str) do
217 Ecto.Type.cast(:integer, expiry_str)
218 |> check_expiry_date()
219 end
220
221 def post(user, %{"status" => status} = data) do
222 limit = Pleroma.Config.get([:instance, :limit])
223
224 with status <- String.trim(status),
225 attachments <- attachments_from_ids(data),
226 in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
227 in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
228 {visibility, in_reply_to_visibility} <-
229 get_visibility(data, in_reply_to, in_reply_to_conversation),
230 {_, false} <-
231 {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
232 {content_html, mentions, tags} <-
233 make_content_html(
234 status,
235 attachments,
236 data,
237 visibility
238 ),
239 mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
240 addressed_users <- get_addressed_users(mentioned_users, data["to"]),
241 {poll, poll_emoji} <- make_poll_data(data),
242 {to, cc} <-
243 get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
244 context <- make_context(in_reply_to, in_reply_to_conversation),
245 cw <- data["spoiler_text"] || "",
246 sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
247 {:ok, expires_at} <- check_expiry_date(data["expires_in"]),
248 full_payload <- String.trim(status <> cw),
249 :ok <- validate_character_limit(full_payload, attachments, limit),
250 object <-
251 make_note_data(
252 user.ap_id,
253 to,
254 context,
255 content_html,
256 attachments,
257 in_reply_to,
258 tags,
259 cw,
260 cc,
261 sensitive,
262 poll
263 ),
264 object <- put_emoji(object, full_payload, poll_emoji) do
265 preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
266 direct? = visibility == "direct"
267
268 result =
269 %{
270 to: to,
271 actor: user,
272 context: context,
273 object: object,
274 additional: %{"cc" => cc, "directMessage" => direct?}
275 }
276 |> maybe_add_list_data(user, visibility)
277 |> ActivityPub.create(preview?)
278
279 if expires_at do
280 with {:ok, activity} <- result do
281 {:ok, _} = ActivityExpiration.create(activity, expires_at)
282 end
283 end
284
285 result
286 else
287 {:private_to_public, true} ->
288 {:error, dgettext("errors", "The message visibility must be direct")}
289
290 {:error, _} = e ->
291 e
292
293 e ->
294 {:error, e}
295 end
296 end
297
298 # parse and put emoji to object data
299 defp put_emoji(map, text, emojis) do
300 Map.put(
301 map,
302 "emoji",
303 Map.merge(Emoji.Formatter.get_emoji_map(text), emojis)
304 )
305 end
306
307 # Updates the emojis for a user based on their profile
308 def update(user) do
309 emoji = emoji_from_profile(user)
310 source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
311
312 user =
313 with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
314 user
315 else
316 _e -> user
317 end
318
319 ActivityPub.update(%{
320 local: true,
321 to: [user.follower_address],
322 cc: [],
323 actor: user.ap_id,
324 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
325 })
326 end
327
328 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
329 with %Activity{
330 actor: ^user_ap_id,
331 data: %{
332 "type" => "Create"
333 },
334 object: %Object{
335 data: %{
336 "type" => "Note"
337 }
338 }
339 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
340 true <- Visibility.is_public?(activity),
341 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
342 {:ok, activity}
343 else
344 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
345 _ -> {:error, dgettext("errors", "Could not pin")}
346 end
347 end
348
349 def unpin(id_or_ap_id, user) do
350 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
351 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
352 {:ok, activity}
353 else
354 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
355 _ -> {:error, dgettext("errors", "Could not unpin")}
356 end
357 end
358
359 def add_mute(user, activity) do
360 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
361 {:ok, activity}
362 else
363 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
364 end
365 end
366
367 def remove_mute(user, activity) do
368 ThreadMute.remove_mute(user.id, activity.data["context"])
369 {:ok, activity}
370 end
371
372 def thread_muted?(%{id: nil} = _user, _activity), do: false
373
374 def thread_muted?(user, activity) do
375 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
376 false
377 else
378 _ -> true
379 end
380 end
381
382 def report(user, data) do
383 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
384 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
385 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
386 {:ok, statuses} <- get_report_statuses(account, data),
387 {:ok, activity} <-
388 ActivityPub.flag(%{
389 context: Utils.generate_context_id(),
390 actor: user,
391 account: account,
392 statuses: statuses,
393 content: content_html,
394 forward: data["forward"] || false
395 }) do
396 {:ok, activity}
397 else
398 {:error, err} -> {:error, err}
399 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
400 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
401 end
402 end
403
404 def update_report_state(activity_id, state) do
405 with %Activity{} = activity <- Activity.get_by_id(activity_id),
406 {:ok, activity} <- Utils.update_report_state(activity, state) do
407 {:ok, activity}
408 else
409 nil -> {:error, :not_found}
410 {:error, reason} -> {:error, reason}
411 _ -> {:error, dgettext("errors", "Could not update state")}
412 end
413 end
414
415 def update_activity_scope(activity_id, opts \\ %{}) do
416 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
417 {:ok, activity} <- toggle_sensitive(activity, opts),
418 {:ok, activity} <- set_visibility(activity, opts) do
419 {:ok, activity}
420 else
421 nil -> {:error, :not_found}
422 {:error, reason} -> {:error, reason}
423 end
424 end
425
426 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
427 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
428 end
429
430 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
431 when is_boolean(sensitive) do
432 new_data = Map.put(object.data, "sensitive", sensitive)
433
434 {:ok, object} =
435 object
436 |> Object.change(%{data: new_data})
437 |> Object.update_and_set_cache()
438
439 {:ok, Map.put(activity, :object, object)}
440 end
441
442 defp toggle_sensitive(activity, _), do: {:ok, activity}
443
444 defp set_visibility(activity, %{"visibility" => visibility}) do
445 Utils.update_activity_visibility(activity, visibility)
446 end
447
448 defp set_visibility(activity, _), do: {:ok, activity}
449
450 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
451 if ap_id not in user.info.muted_reblogs do
452 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
453 end
454 end
455
456 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
457 if ap_id in user.info.muted_reblogs do
458 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
459 end
460 end
461 end