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"]}
49 %{scopes: ["write:statuses"]}
58 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
62 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
67 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
72 %{@unauthenticated_access | scopes: ["read:accounts"]}
73 when action in [:favourited_by, :reblogged_by]
76 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
78 # Note: scope not present in Mastodon: read:bookmarks
79 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
81 # Note: scope not present in Mastodon: write:bookmarks
84 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
87 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
91 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
92 when action in ~w(reblog unreblog)a
97 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
98 when action in ~w(favourite unfavourite)a
101 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
103 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
105 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
108 GET `/api/v1/statuses?ids[]=1&ids[]=2`
110 `ids` query param is required
112 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
118 |> Activity.all_by_ids_with_object()
119 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
121 render(conn, "index.json",
122 activities: activities,
125 with_muted: Map.get(params, :with_muted, false)
130 POST /api/v1/statuses
132 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
135 assigns: %{user: user},
136 body_params: %{status: _, scheduled_at: scheduled_at} = params
140 when not is_nil(scheduled_at) do
142 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
143 |> put_application(conn)
146 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
147 scheduled_at: scheduled_at
150 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
151 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
153 |> put_view(ScheduledActivityView)
154 |> render("show.json", scheduled_activity: scheduled_activity)
157 params = Map.drop(params, [:scheduled_at])
158 create(%Plug.Conn{conn | body_params: params}, %{})
165 # Creates a regular status
166 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
168 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
169 |> put_application(conn)
171 with {:ok, activity} <- CommonAPI.post(user, params) do
172 try_render(conn, "show.json",
176 with_direct_conversation_id: true
179 {:error, {:reject, message}} ->
181 |> put_status(:unprocessable_entity)
182 |> json(%{error: message})
186 |> put_status(:unprocessable_entity)
187 |> json(%{error: message})
191 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
192 params = Map.put(params, :status, "")
193 create(%Plug.Conn{conn | body_params: params}, %{})
196 @doc "GET /api/v1/statuses/:id"
197 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
198 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
199 true <- Visibility.visible_for_user?(activity, user) do
200 try_render(conn, "show.json",
203 with_direct_conversation_id: true,
204 with_muted: Map.get(params, :with_muted, false)
207 _ -> {:error, :not_found}
211 @doc "DELETE /api/v1/statuses/:id"
212 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
213 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
214 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
215 try_render(conn, "show.json",
218 with_direct_conversation_id: true,
222 _e -> {:error, :not_found}
226 @doc "POST /api/v1/statuses/:id/reblog"
227 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
228 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
229 %Activity{} = announce <- Activity.normalize(announce.data) do
230 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
234 @doc "POST /api/v1/statuses/:id/unreblog"
235 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
236 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
237 %Activity{} = activity <- Activity.get_by_id(activity_id) do
238 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
242 @doc "POST /api/v1/statuses/:id/favourite"
243 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
244 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
245 %Activity{} = activity <- Activity.get_by_id(activity_id) do
246 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
250 @doc "POST /api/v1/statuses/:id/unfavourite"
251 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
252 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
253 %Activity{} = activity <- Activity.get_by_id(activity_id) do
254 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
258 @doc "POST /api/v1/statuses/:id/pin"
259 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
260 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
261 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
263 {:error, :pinned_statuses_limit_reached} ->
264 {:error, "You have already pinned the maximum number of statuses"}
266 {:error, :ownership_error} ->
267 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
269 {:error, :visibility_error} ->
270 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
277 @doc "POST /api/v1/statuses/:id/unpin"
278 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
279 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
280 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
284 @doc "POST /api/v1/statuses/:id/bookmark"
285 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
286 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
287 %User{} = user <- User.get_cached_by_nickname(user.nickname),
288 true <- Visibility.visible_for_user?(activity, user),
289 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
290 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
294 @doc "POST /api/v1/statuses/:id/unbookmark"
295 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
296 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
297 %User{} = user <- User.get_cached_by_nickname(user.nickname),
298 true <- Visibility.visible_for_user?(activity, user),
299 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
300 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
304 @doc "POST /api/v1/statuses/:id/mute"
305 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
306 with %Activity{} = activity <- Activity.get_by_id(id),
307 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
308 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
312 @doc "POST /api/v1/statuses/:id/unmute"
313 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
314 with %Activity{} = activity <- Activity.get_by_id(id),
315 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
316 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
320 @doc "GET /api/v1/statuses/:id/favourited_by"
321 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
322 with true <- Pleroma.Config.get([:instance, :show_reactions]),
323 %Activity{} = activity <- Activity.get_by_id_with_object(id),
324 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
325 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
328 |> Ecto.Query.where([u], u.ap_id in ^likes)
330 |> Enum.filter(&(not User.blocks?(user, &1)))
333 |> put_view(AccountView)
334 |> render("index.json", for: user, users: users, as: :user)
336 {:visible, false} -> {:error, :not_found}
341 @doc "GET /api/v1/statuses/:id/reblogged_by"
342 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
343 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
344 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
345 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
346 Object.normalize(activity, fetch: false) do
349 |> Activity.Queries.by_type()
350 |> Ecto.Query.where([a], a.actor in ^announces)
351 # this is to use the index
352 |> Activity.Queries.by_object_id(ap_id)
354 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
355 |> Enum.map(& &1.actor)
360 |> Ecto.Query.where([u], u.ap_id in ^announces)
362 |> Enum.filter(&(not User.blocks?(user, &1)))
365 |> put_view(AccountView)
366 |> render("index.json", for: user, users: users, as: :user)
368 {:visible, false} -> {:error, :not_found}
373 @doc "GET /api/v1/statuses/:id/context"
374 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
375 with %Activity{} = activity <- Activity.get_by_id(id) do
377 activity.data["context"]
378 |> ActivityPub.fetch_activities_for_context(%{
381 exclude_id: activity.id
383 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
385 render(conn, "context.json", activity: activity, activities: activities, user: user)
389 @doc "GET /api/v1/favourites"
390 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
391 activities = ActivityPub.fetch_favourites(user, params)
394 |> add_link_headers(activities)
395 |> render("index.json",
396 activities: activities,
402 @doc "GET /api/v1/bookmarks"
403 def bookmarks(%{assigns: %{user: user}} = conn, params) do
404 user = User.get_cached_by_id(user.id)
408 |> Bookmark.for_user_query()
409 |> Pleroma.Pagination.fetch_paginated(params)
413 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
416 |> add_link_headers(bookmarks)
417 |> render("index.json",
418 activities: activities,
424 @doc "GET /api/v1/statuses/:id/translations/:language"
425 def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) do
426 with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])},
427 %Activity{} = activity <- Activity.get_by_id_with_object(id),
428 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
429 translation_module <- Config.get([:translator, :module]),
430 {:ok, detected, translation} <-
433 activity.object.data["content"],
434 Map.get(params, :from, nil),
438 json(conn, %{detected_language: detected, text: translation})
442 |> put_status(:bad_request)
443 |> json(%{"error" => "Translation is not enabled"})
453 defp fetch_or_translate(status_id, text, source_language, target_language, translation_module) do
456 "translations:#{status_id}:#{source_language}:#{target_language}",
458 value = translation_module.translate(text, source_language, target_language)
460 with {:ok, _, _} <- value do
463 _ -> {:ignore, value}
469 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
470 if user.disclose_client do
471 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
472 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
474 Map.put(params, :generator, nil)
478 defp put_application(params, _), do: Map.put(params, :generator, nil)