1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.StatusController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
9 only: [try_render: 3, add_link_headers: 2]
13 alias Pleroma.Activity
14 alias Pleroma.Bookmark
18 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
25 alias Pleroma.Web.OAuth.Token
26 alias Pleroma.Web.Plugs.OAuthScopesPlug
27 alias Pleroma.Web.Plugs.RateLimiter
29 plug(Pleroma.Web.ApiSpec.CastAndValidate)
31 plug(:skip_public_check when action in [:index, :show])
33 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
34 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
38 %{@unauthenticated_access | scopes: ["read:statuses"]}
51 %{scopes: ["write:statuses"]}
61 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
65 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
70 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
75 %{@unauthenticated_access | scopes: ["read:accounts"]}
76 when action in [:favourited_by, :reblogged_by]
79 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
81 # Note: scope not present in Mastodon: read:bookmarks
82 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
84 # Note: scope not present in Mastodon: write:bookmarks
87 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
90 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
94 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
95 when action in ~w(reblog unreblog)a
100 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
101 when action in ~w(favourite unfavourite)a
104 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
106 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
108 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
111 GET `/api/v1/statuses?ids[]=1&ids[]=2`
113 `ids` query param is required
115 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
121 |> Activity.all_by_ids_with_object()
122 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
124 render(conn, "index.json",
125 activities: activities,
128 with_muted: Map.get(params, :with_muted, false)
133 POST /api/v1/statuses
135 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
138 assigns: %{user: user},
139 body_params: %{status: _, scheduled_at: scheduled_at} = params
143 when not is_nil(scheduled_at) do
145 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
146 |> put_application(conn)
149 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
150 scheduled_at: scheduled_at
153 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
154 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
156 |> put_view(ScheduledActivityView)
157 |> render("show.json", scheduled_activity: scheduled_activity)
160 params = Map.drop(params, [:scheduled_at])
161 create(%Plug.Conn{conn | body_params: params}, %{})
168 # Creates a regular status
169 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
171 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
172 |> put_application(conn)
175 if is_nil(user.status_ttl_days),
177 else: 60 * 60 * 24 * user.status_ttl_days
180 if is_nil(expires_in_seconds),
182 else: Map.put(params, :expires_in, expires_in_seconds)
184 with {:ok, activity} <- CommonAPI.post(user, params) do
185 try_render(conn, "show.json",
189 with_direct_conversation_id: true
192 {:error, {:reject, message}} ->
194 |> put_status(:unprocessable_entity)
195 |> json(%{error: message})
199 |> put_status(:unprocessable_entity)
200 |> json(%{error: message})
204 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
205 params = Map.put(params, :status, "")
206 create(%Plug.Conn{conn | body_params: params}, %{})
209 @doc "GET /api/v1/statuses/:id/history"
210 def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
211 with user = assigns[:user],
212 %Activity{} = activity <- Activity.get_by_id_with_object(id),
213 true <- Visibility.visible_for_user?(activity, user) do
214 try_render(conn, "history.json",
217 with_direct_conversation_id: true,
218 with_muted: Map.get(params, :with_muted, false)
221 _ -> {:error, :not_found}
225 @doc "GET /api/v1/statuses/:id/source"
226 def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
227 with user = assigns[:user],
228 %Activity{} = activity <- Activity.get_by_id_with_object(id),
229 true <- Visibility.visible_for_user?(activity, user) do
230 try_render(conn, "source.json",
235 _ -> {:error, :not_found}
239 @doc "PUT /api/v1/statuses/:id"
240 def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
241 with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
242 {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
243 {_, true} <- {:is_create, activity.data["type"] == "Create"},
244 actor <- Activity.user_actor(activity),
245 {_, true} <- {:own_status, actor.id == user.id},
246 changes <- body_params |> put_application(conn),
247 {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
248 {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
249 try_render(conn, "show.json",
252 with_direct_conversation_id: true,
253 with_muted: Map.get(params, :with_muted, false)
256 {:own_status, _} -> {:error, :forbidden}
257 {:pipeline, _} -> {:error, :internal_server_error}
258 _ -> {:error, :not_found}
262 @doc "GET /api/v1/statuses/:id"
263 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
264 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
265 true <- Visibility.visible_for_user?(activity, user) do
266 try_render(conn, "show.json",
269 with_direct_conversation_id: true,
270 with_muted: Map.get(params, :with_muted, false)
273 _ -> {:error, :not_found}
277 @doc "DELETE /api/v1/statuses/:id"
278 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
279 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
280 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
281 try_render(conn, "show.json",
284 with_direct_conversation_id: true,
288 _e -> {:error, :not_found}
292 @doc "POST /api/v1/statuses/:id/reblog"
293 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
294 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
295 %Activity{} = announce <- Activity.normalize(announce.data) do
296 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
300 @doc "POST /api/v1/statuses/:id/unreblog"
301 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
302 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
303 %Activity{} = activity <- Activity.get_by_id(activity_id) do
304 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
308 @doc "POST /api/v1/statuses/:id/favourite"
309 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
310 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
311 %Activity{} = activity <- Activity.get_by_id(activity_id) do
312 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
316 @doc "POST /api/v1/statuses/:id/unfavourite"
317 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
318 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
319 %Activity{} = activity <- Activity.get_by_id(activity_id) do
320 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
324 @doc "POST /api/v1/statuses/:id/pin"
325 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
326 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
327 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
329 {:error, :pinned_statuses_limit_reached} ->
330 {:error, "You have already pinned the maximum number of statuses"}
332 {:error, :ownership_error} ->
333 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
335 {:error, :visibility_error} ->
336 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
343 @doc "POST /api/v1/statuses/:id/unpin"
344 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
345 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
346 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
350 @doc "POST /api/v1/statuses/:id/bookmark"
351 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
352 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
353 %User{} = user <- User.get_cached_by_nickname(user.nickname),
354 true <- Visibility.visible_for_user?(activity, user),
355 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
356 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
360 @doc "POST /api/v1/statuses/:id/unbookmark"
361 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
362 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
363 %User{} = user <- User.get_cached_by_nickname(user.nickname),
364 true <- Visibility.visible_for_user?(activity, user),
365 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
366 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
370 @doc "POST /api/v1/statuses/:id/mute"
371 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
372 with %Activity{} = activity <- Activity.get_by_id(id),
373 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
374 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
378 @doc "POST /api/v1/statuses/:id/unmute"
379 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
380 with %Activity{} = activity <- Activity.get_by_id(id),
381 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
382 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
386 @doc "GET /api/v1/statuses/:id/favourited_by"
387 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
388 with true <- Pleroma.Config.get([:instance, :show_reactions]),
389 %Activity{} = activity <- Activity.get_by_id_with_object(id),
390 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
391 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
394 |> Ecto.Query.where([u], u.ap_id in ^likes)
396 |> Enum.filter(&(not User.blocks?(user, &1)))
399 |> put_view(AccountView)
400 |> render("index.json", for: user, users: users, as: :user)
402 {:visible, false} -> {:error, :not_found}
407 @doc "GET /api/v1/statuses/:id/reblogged_by"
408 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
409 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
410 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
411 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
412 Object.normalize(activity, fetch: false) do
415 |> Activity.Queries.by_type()
416 |> Ecto.Query.where([a], a.actor in ^announces)
417 # this is to use the index
418 |> Activity.Queries.by_object_id(ap_id)
420 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
421 |> Enum.map(& &1.actor)
426 |> Ecto.Query.where([u], u.ap_id in ^announces)
428 |> Enum.filter(&(not User.blocks?(user, &1)))
431 |> put_view(AccountView)
432 |> render("index.json", for: user, users: users, as: :user)
434 {:visible, false} -> {:error, :not_found}
439 @doc "GET /api/v1/statuses/:id/context"
440 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
441 with %Activity{} = activity <- Activity.get_by_id(id) do
443 activity.data["context"]
444 |> ActivityPub.fetch_activities_for_context(%{
447 exclude_id: activity.id
449 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
451 render(conn, "context.json", activity: activity, activities: activities, user: user)
455 @doc "GET /api/v1/favourites"
456 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
457 activities = ActivityPub.fetch_favourites(user, params)
460 |> add_link_headers(activities)
461 |> render("index.json",
462 activities: activities,
468 @doc "GET /api/v1/bookmarks"
469 def bookmarks(%{assigns: %{user: user}} = conn, params) do
470 user = User.get_cached_by_id(user.id)
474 |> Bookmark.for_user_query()
475 |> Pleroma.Pagination.fetch_paginated(params)
479 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
482 |> add_link_headers(bookmarks)
483 |> render("index.json",
484 activities: activities,
490 @doc "GET /api/v1/statuses/:id/translations/:language"
491 def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) do
492 with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])},
493 %Activity{} = activity <- Activity.get_by_id_with_object(id),
494 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
495 translation_module <- Config.get([:translator, :module]),
496 {:ok, detected, translation} <-
499 activity.object.data["content"],
500 Map.get(params, :from, nil),
504 json(conn, %{detected_language: detected, text: translation})
508 |> put_status(:bad_request)
509 |> json(%{"error" => "Translation is not enabled"})
519 defp fetch_or_translate(status_id, text, source_language, target_language, translation_module) do
522 "translations:#{status_id}:#{source_language}:#{target_language}",
524 value = translation_module.translate(text, source_language, target_language)
526 with {:ok, _, _} <- value do
529 _ -> {:ignore, value}
535 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
536 if user.disclose_client do
537 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
538 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
540 Map.put(params, :generator, nil)
544 defp put_application(params, _), do: Map.put(params, :generator, nil)