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)
174 with {:ok, activity} <- CommonAPI.post(user, params) do
175 try_render(conn, "show.json",
179 with_direct_conversation_id: true
182 {:error, {:reject, message}} ->
184 |> put_status(:unprocessable_entity)
185 |> json(%{error: message})
189 |> put_status(:unprocessable_entity)
190 |> json(%{error: message})
194 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
195 params = Map.put(params, :status, "")
196 create(%Plug.Conn{conn | body_params: params}, %{})
199 @doc "GET /api/v1/statuses/:id/history"
200 def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
201 with user = assigns[:user],
202 %Activity{} = activity <- Activity.get_by_id_with_object(id),
203 true <- Visibility.visible_for_user?(activity, user) do
204 try_render(conn, "history.json",
207 with_direct_conversation_id: true,
208 with_muted: Map.get(params, :with_muted, false)
211 _ -> {:error, :not_found}
215 @doc "GET /api/v1/statuses/:id/source"
216 def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
217 with user = assigns[:user],
218 %Activity{} = activity <- Activity.get_by_id_with_object(id),
219 true <- Visibility.visible_for_user?(activity, user) do
220 try_render(conn, "source.json",
225 _ -> {:error, :not_found}
229 @doc "PUT /api/v1/statuses/:id"
230 def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
231 with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
232 {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
233 {_, true} <- {:is_create, activity.data["type"] == "Create"},
234 actor <- Activity.user_actor(activity),
235 {_, true} <- {:own_status, actor.id == user.id},
236 changes <- body_params |> put_application(conn),
237 {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
238 {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
239 try_render(conn, "show.json",
242 with_direct_conversation_id: true,
243 with_muted: Map.get(params, :with_muted, false)
246 {:own_status, _} -> {:error, :forbidden}
247 {:pipeline, _} -> {:error, :internal_server_error}
248 _ -> {:error, :not_found}
252 @doc "GET /api/v1/statuses/:id"
253 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
254 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
255 true <- Visibility.visible_for_user?(activity, user) do
256 try_render(conn, "show.json",
259 with_direct_conversation_id: true,
260 with_muted: Map.get(params, :with_muted, false)
263 _ -> {:error, :not_found}
267 @doc "DELETE /api/v1/statuses/:id"
268 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
269 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
270 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
271 try_render(conn, "show.json",
274 with_direct_conversation_id: true,
278 _e -> {:error, :not_found}
282 @doc "POST /api/v1/statuses/:id/reblog"
283 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
284 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
285 %Activity{} = announce <- Activity.normalize(announce.data) do
286 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
290 @doc "POST /api/v1/statuses/:id/unreblog"
291 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
292 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
293 %Activity{} = activity <- Activity.get_by_id(activity_id) do
294 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
298 @doc "POST /api/v1/statuses/:id/favourite"
299 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
300 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
301 %Activity{} = activity <- Activity.get_by_id(activity_id) do
302 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
306 @doc "POST /api/v1/statuses/:id/unfavourite"
307 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
308 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
309 %Activity{} = activity <- Activity.get_by_id(activity_id) do
310 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
314 @doc "POST /api/v1/statuses/:id/pin"
315 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
316 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
317 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
319 {:error, :pinned_statuses_limit_reached} ->
320 {:error, "You have already pinned the maximum number of statuses"}
322 {:error, :ownership_error} ->
323 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
325 {:error, :visibility_error} ->
326 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
333 @doc "POST /api/v1/statuses/:id/unpin"
334 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
335 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
336 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
340 @doc "POST /api/v1/statuses/:id/bookmark"
341 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
342 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
343 %User{} = user <- User.get_cached_by_nickname(user.nickname),
344 true <- Visibility.visible_for_user?(activity, user),
345 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
346 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
350 @doc "POST /api/v1/statuses/:id/unbookmark"
351 def unbookmark(%{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.destroy(user.id, activity.id) do
356 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
360 @doc "POST /api/v1/statuses/:id/mute"
361 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
362 with %Activity{} = activity <- Activity.get_by_id(id),
363 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
364 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
368 @doc "POST /api/v1/statuses/:id/unmute"
369 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
370 with %Activity{} = activity <- Activity.get_by_id(id),
371 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
372 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
376 @doc "GET /api/v1/statuses/:id/favourited_by"
377 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
378 with true <- Pleroma.Config.get([:instance, :show_reactions]),
379 %Activity{} = activity <- Activity.get_by_id_with_object(id),
380 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
381 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
384 |> Ecto.Query.where([u], u.ap_id in ^likes)
386 |> Enum.filter(&(not User.blocks?(user, &1)))
389 |> put_view(AccountView)
390 |> render("index.json", for: user, users: users, as: :user)
392 {:visible, false} -> {:error, :not_found}
397 @doc "GET /api/v1/statuses/:id/reblogged_by"
398 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
399 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
400 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
401 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
402 Object.normalize(activity, fetch: false) do
405 |> Activity.Queries.by_type()
406 |> Ecto.Query.where([a], a.actor in ^announces)
407 # this is to use the index
408 |> Activity.Queries.by_object_id(ap_id)
410 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
411 |> Enum.map(& &1.actor)
416 |> Ecto.Query.where([u], u.ap_id in ^announces)
418 |> Enum.filter(&(not User.blocks?(user, &1)))
421 |> put_view(AccountView)
422 |> render("index.json", for: user, users: users, as: :user)
424 {:visible, false} -> {:error, :not_found}
429 @doc "GET /api/v1/statuses/:id/context"
430 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
431 with %Activity{} = activity <- Activity.get_by_id(id) do
433 activity.data["context"]
434 |> ActivityPub.fetch_activities_for_context(%{
437 exclude_id: activity.id
439 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
441 render(conn, "context.json", activity: activity, activities: activities, user: user)
445 @doc "GET /api/v1/favourites"
446 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
447 activities = ActivityPub.fetch_favourites(user, params)
450 |> add_link_headers(activities)
451 |> render("index.json",
452 activities: activities,
458 @doc "GET /api/v1/bookmarks"
459 def bookmarks(%{assigns: %{user: user}} = conn, params) do
460 user = User.get_cached_by_id(user.id)
464 |> Bookmark.for_user_query()
465 |> Pleroma.Pagination.fetch_paginated(params)
469 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
472 |> add_link_headers(bookmarks)
473 |> render("index.json",
474 activities: activities,
480 @doc "GET /api/v1/statuses/:id/translations/:language"
481 def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) do
482 with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])},
483 %Activity{} = activity <- Activity.get_by_id_with_object(id),
484 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
485 translation_module <- Config.get([:translator, :module]),
486 {:ok, detected, translation} <-
489 activity.object.data["content"],
490 Map.get(params, :from, nil),
494 json(conn, %{detected_language: detected, text: translation})
498 |> put_status(:bad_request)
499 |> json(%{"error" => "Translation is not enabled"})
509 defp fetch_or_translate(status_id, text, source_language, target_language, translation_module) do
512 "translations:#{status_id}:#{source_language}:#{target_language}",
514 value = translation_module.translate(text, source_language, target_language)
516 with {:ok, _, _} <- value do
519 _ -> {:ignore, value}
525 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
526 if user.disclose_client do
527 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
528 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
530 Map.put(params, :generator, nil)
534 defp put_application(params, _), do: Map.put(params, :generator, nil)