summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/mastodon_api
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/mastodon_api')
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex297
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex17
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex19
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex57
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex30
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex49
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex42
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex114
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex145
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex44
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex157
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex219
-rw-r--r--lib/pleroma/web/mastodon_api/views/app_view.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex13
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex77
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex162
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex25
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex186
-rw-r--r--lib/pleroma/web/mastodon_api/views/subscription_view.ex (renamed from lib/pleroma/web/mastodon_api/views/push_subscription_view.ex)4
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex67
36 files changed, 1333 insertions, 568 deletions
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 229d4be28..95d8452df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -6,32 +6,53 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
-
+ only: [
+ add_link_headers: 2,
+ truthy_param?: 1,
+ assign_account_by_id: 2,
+ embed_relationships?: 1,
+ json_response: 3
+ ]
+
+ alias Pleroma.Maps
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
- alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.OAuthController
+ alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.TwitterAPI.TwitterAPI
- plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
+
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
- when action == :show
+ when action in [:show, :followers, :following]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
+ when action == :statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
- when action in [:endorsements, :verify_credentials, :followers, :following]
+ when action in [:verify_credentials, :endorsements, :identity_proofs]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
@@ -50,27 +71,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
- # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
plug(
OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
+ %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
- plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action != :create
- )
-
@relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(
RateLimiter,
- [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
+ [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
@@ -79,37 +94,40 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- @doc "POST /api/v1/accounts"
- def create(
- %{assigns: %{app: app}} = conn,
- %{"username" => nickname, "password" => _, "agreement" => true} = params
- ) do
- params =
- params
- |> Map.take([
- "email",
- "captcha_solution",
- "captcha_token",
- "captcha_answer_data",
- "token",
- "password"
- ])
- |> Map.put("nickname", nickname)
- |> Map.put("fullname", params["fullname"] || nickname)
- |> Map.put("bio", params["bio"] || "")
- |> Map.put("confirm", params["password"])
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
+ @doc "POST /api/v1/accounts"
+ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
with :ok <- validate_email_param(params),
- {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
- {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
- json(conn, %{
- token_type: "Bearer",
- access_token: token.token,
- scope: app.scopes,
- created_at: Token.Utils.format_created_at(token)
- })
+ :ok <- TwitterAPI.validate_captcha(app, params),
+ {:ok, user} <- TwitterAPI.register_user(params),
+ {_, {:ok, token}} <-
+ {:login, OAuthController.login(user, app, app.scopes)} do
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
- {:error, errors} -> json_response(conn, :bad_request, errors)
+ {:login, {:account_status, :confirmation_pending}} ->
+ json_response(conn, :ok, %{
+ message: "You have been registered. Please check your email for further instructions.",
+ identifier: "missing_confirmed_email"
+ })
+
+ {:login, {:account_status, :approval_pending}} ->
+ json_response(conn, :ok, %{
+ message:
+ "You have been registered. You'll be able to log in once your account is approved.",
+ identifier: "awaiting_approval"
+ })
+
+ {:login, _} ->
+ json_response(conn, :ok, %{
+ message:
+ "You have been registered. Some post-registration steps may be pending. " <>
+ "Please log in manually.",
+ identifier: "manual_login_required"
+ })
+
+ {:error, error} ->
+ json_response(conn, :bad_request, %{error: error})
end
end
@@ -121,11 +139,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
render_error(conn, :forbidden, "Invalid credentials")
end
- defp validate_email_param(%{"email" => _}), do: :ok
+ defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
- true -> {:error, %{"error" => "Missing parameters"}}
+ true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
_ -> :ok
end
end
@@ -143,8 +161,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
- def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
- user = original_user
+ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
+ params =
+ params
+ |> Enum.filter(fn {_, value} -> not is_nil(value) end)
+ |> Enum.into(%{})
+
+ # We use an empty string as a special value to reset
+ # avatars, banners, backgrounds
+ user_image_value = fn
+ "" -> {:ok, nil}
+ value -> {:ok, value}
+ end
user_params =
[
@@ -158,56 +186,77 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:show_role,
:skip_thread_containment,
:allow_following_move,
- :discoverable
+ :discoverable,
+ :accepts_chat_messages
]
|> Enum.reduce(%{}, fn key, acc ->
- add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
+ Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
end)
- |> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio)
- |> add_if_present(params, "avatar", :avatar)
- |> add_if_present(params, "header", :banner)
- |> add_if_present(params, "pleroma_background_image", :background)
- |> add_if_present(
- params,
- "fields_attributes",
+ |> Maps.put_if_present(:name, params[:display_name])
+ |> Maps.put_if_present(:bio, params[:note])
+ |> Maps.put_if_present(:raw_bio, params[:note])
+ |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
+ |> Maps.put_if_present(:banner, params[:header], user_image_value)
+ |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
+ |> Maps.put_if_present(
:raw_fields,
+ params[:fields_attributes],
&{:ok, normalize_fields_attributes(&1)}
)
- |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
- |> add_if_present(params, "default_scope", :default_scope)
- |> add_if_present(params, "actor_type", :actor_type)
-
- changeset = User.update_changeset(user, user_params)
-
- with {:ok, user} <- User.update_and_set_cache(changeset) do
- if original_user != user, do: CommonAPI.update(user)
-
- render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
+ |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
+ |> Maps.put_if_present(:default_scope, params[:default_scope])
+ |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
+ |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
+ if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
+ end)
+ |> Maps.put_if_present(:actor_type, params[:actor_type])
+
+ # What happens here:
+ #
+ # We want to update the user through the pipeline, but the ActivityPub
+ # update information is not quite enough for this, because this also
+ # contains local settings that don't federate and don't even appear
+ # in the Update activity.
+ #
+ # So we first build the normal local changeset, then apply it to the
+ # user data, but don't persist it. With this, we generate the object
+ # data for our update activity. We feed this and the changeset as meta
+ # inforation into the pipeline, where they will be properly updated and
+ # federated.
+ with changeset <- User.update_changeset(user, user_params),
+ {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
+ updated_object <-
+ Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
+ |> Map.delete("@context"),
+ {:ok, update_data, []} <- Builder.update(user, updated_object),
+ {:ok, _update, _} <-
+ Pipeline.common_pipeline(update_data,
+ local: true,
+ user_update_changeset: changeset
+ ) do
+ render(conn, "show.json",
+ user: unpersisted_user,
+ for: unpersisted_user,
+ with_pleroma_settings: true
+ )
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end
- defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
- with true <- Map.has_key?(params, params_field),
- {:ok, new_value} <- value_function.(params[params_field]) do
- Map.put(map, map_field, new_value)
- else
- _ -> map
- end
- end
-
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
- fields
+ Enum.map(fields, fn
+ %{} = field -> %{"name" => field.name, "value" => field.value}
+ field -> field
+ end)
end
end
@doc "GET /api/v1/accounts/relationships"
- def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
@@ -217,34 +266,56 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
- def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
+ def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
- true <- User.visible_for?(user, for_user) do
+ :visible <- User.visible_for(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
else
- _e -> render_error(conn, :not_found, "Can't find user")
+ error -> user_visibility_error(conn, error)
end
end
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
+ :visible <- User.visible_for(user, reading_user) do
params =
params
- |> Map.put("tag", params["tagged"])
- |> Map.delete("godmode")
+ |> Map.delete(:tagged)
+ |> Map.put(:tag, params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
- |> render("index.json", activities: activities, for: reading_user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: reading_user,
+ as: :activity
+ )
+ else
+ error -> user_visibility_error(conn, error)
+ end
+ end
+
+ defp user_visibility_error(conn, error) do
+ case error do
+ :restrict_unauthenticated ->
+ render_error(conn, :unauthorized, "This API requires an authenticated user")
+
+ _ ->
+ render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Enum.map(fn {key, value} -> {to_string(key), value} end)
+ |> Enum.into(%{})
+
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
@@ -254,11 +325,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(followers)
- |> render("index.json", for: for_user, users: followers, as: :user)
+ # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
+ |> render("index.json",
+ for: for_user,
+ users: followers,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Enum.map(fn {key, value} -> {to_string(key), value} end)
+ |> Enum.into(%{})
+
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
@@ -268,7 +350,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(followers)
- |> render("index.json", for: for_user, users: followers, as: :user)
+ # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
+ |> render("index.json",
+ for: for_user,
+ users: followers,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/accounts/:id/lists"
@@ -282,11 +370,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
- {:error, :not_found}
+ {:error, "Can not follow yourself"}
end
- def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
- with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
+ def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
+ with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -295,7 +383,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
- {:error, :not_found}
+ {:error, "Can not unfollow yourself"}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
@@ -305,10 +393,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/accounts/:id/mute"
- def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
- notifications? = params |> Map.get("notifications", true) |> truthy_param?()
-
- with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
+ def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -326,8 +412,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _user_block} <- User.block(blocker, blocked),
- {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
+ with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -336,8 +421,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _user_block} <- User.unblock(blocker, blocked),
- {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
+ with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -345,14 +429,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/follows"
- def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
- render(conn, "show.json", user: followed, for: follower)
- else
- {:followed, _} -> {:error, :not_found}
- {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
+ case User.get_cached_by_nickname(uri) do
+ %User{} = user ->
+ conn
+ |> assign(:account, user)
+ |> follow(%{})
+
+ nil ->
+ {:error, :not_found}
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 5e2871f18..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.AppController do
use Pleroma.Web, :controller
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
@@ -13,18 +14,28 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+ when action == :create
+ )
+
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
@local_mastodon_name "Mastodon-Local"
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
+
@doc "POST /api/v1/apps"
- def create(conn, params) do
+ def create(%{body_params: params} = conn, _params) do
scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
- |> Map.drop(["scope", "scopes"])
- |> Map.put("scopes", scopes)
+ |> Map.take([:client_name, :redirect_uris, :website])
+ |> Map.put(:scopes, scopes)
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 37b389382..753b3db3e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- @local_mastodon_name "Mastodon-Local"
-
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
+ @local_mastodon_name "Mastodon-Local"
+
@doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index 7c9b11bf1..f35ec3596 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -13,10 +13,11 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
- plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read)
+ plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ConversationOperation
@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
@@ -28,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
end
@doc "POST /api/v1/conversations/:id/read"
- def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
with %Participation{} = participation <-
Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index d82de1db5..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -5,6 +5,16 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action == :index
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
+
def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index e4156cbe6..9c2d093cd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
+
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index
@@ -18,21 +21,29 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
%{scopes: ["follow", "write:blocks"]} when action != :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: user}} = conn, _) do
json(conn, Map.get(user, :domain_blocks, []))
end
@doc "POST /api/v1/domain_blocks"
- def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
+ User.block_domain(blocker, domain)
+ json(conn, %{})
+ end
+
+ def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
- def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
+ User.unblock_domain(blocker, domain)
+ json(conn, %{})
+ end
+
+ def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
index 0a257f604..8af557b61 100644
--- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
@@ -20,6 +20,10 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do
render_error(conn, :not_found, "Record not found")
end
+ def call(conn, {:error, :forbidden}) do
+ render_error(conn, :forbidden, "Access denied")
+ end
+
def call(conn, {:error, error_message}) do
conn
|> put_status(:bad_request)
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7b0b937a2..abbf0ce02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
plug(
@@ -17,62 +18,60 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
- render(conn, "filters.json", filters: filters)
+ render(conn, "index.json", filters: filters)
end
@doc "POST /api/v1/filters"
- def create(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context} = params
- ) do
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
query = %Filter{
user_id: user.id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", false),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
+ phrase: params.phrase,
+ context: params.context,
+ hide: params.irreversible,
+ whole_word: params.whole_word
+ # TODO: support `expires_in` parameter (as in Mastodon API)
}
{:ok, response} = Filter.create(query)
- render(conn, "filter.json", filter: response)
+ render(conn, "show.json", filter: response)
end
@doc "GET /api/v1/filters/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
filter = Filter.get(filter_id, user)
- render(conn, "filter.json", filter: filter)
+ render(conn, "show.json", filter: filter)
end
@doc "PUT /api/v1/filters/:id"
def update(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+ %{assigns: %{user: user}, body_params: params} = conn,
+ %{id: filter_id}
) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", nil),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.update(query)
- render(conn, "filter.json", filter: response)
+ params =
+ params
+ |> Map.delete(:irreversible)
+ |> Map.put(:hide, params[:irreversible])
+ |> Enum.reject(fn {_key, value} -> is_nil(value) end)
+ |> Map.new()
+
+ # TODO: support `expires_in` parameter (as in Mastodon API)
+
+ with %Filter{} = filter <- Filter.get(filter_id, user),
+ {:ok, %Filter{} = filter} <- Filter.update(filter, params) do
+ render(conn, "show.json", filter: filter)
+ end
end
@doc "DELETE /api/v1/filters/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
query = %Filter{
user_id: user.id,
filter_id: filter_id
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
index 1ca86f63f..748b6b475 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
alias Pleroma.Web.CommonAPI
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@@ -21,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
%{scopes: ["follow", "write:follows"]} when action != :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation
@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
@@ -44,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
end
end
- defp assign_follower(%{params: %{"id" => id}} = conn, _) do
+ defp assign_follower(%{params: %{id: id}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 27b5b1a52..d8859731d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -5,6 +5,16 @@
defmodule Pleroma.Web.MastodonAPI.InstanceController do
use Pleroma.Web, :controller
+ plug(OpenApiSpex.Plug.CastAndValidate)
+
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:show, :peers]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
+
@doc "GET /api/v1/instance"
def show(conn, _params) do
render(conn, "show.json")
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index dac4daa7b..acdc76fd2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -9,20 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
- plug(:list_by_id_and_user when action not in [:index, :create])
-
- plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:lists"]}
- when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
- )
+ @oauth_read_actions [:index, :show, :list_accounts]
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:list_by_id_and_user when action not in [:index, :create])
+ plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
+ plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation
+
# GET /api/v1/lists
def index(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
@@ -30,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists
- def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do
+ def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
render(conn, "show.json", list: list)
end
@@ -42,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# PUT /api/v1/lists/:id
- def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do
+ def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
render(conn, "show.json", list: list)
end
@@ -65,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists/:id/accounts
- def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
@@ -76,7 +73,10 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# DELETE /api/v1/lists/:id/accounts
- def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ def remove_from_list(
+ %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn,
+ _
+ ) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
@@ -86,7 +86,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
json(conn, %{})
end
- defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case Pleroma.List.get(id, user) do
%Pleroma.List{} = list -> assign(conn, :list, list)
nil -> conn |> render_error(:not_found, "List not found") |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 58e8a30c2..85310edfa 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]}
@@ -13,17 +15,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MarkerOperation
+
# GET /api/v1/markers
def index(%{assigns: %{user: user}} = conn, params) do
- markers = Pleroma.Marker.get_markers(user, params["timeline"])
+ markers = Pleroma.Marker.get_markers(user, params[:timeline])
render(conn, "markers.json", %{markers: markers})
end
# POST /api/v1/markers
- def upsert(%{assigns: %{user: user}} = conn, params) do
+ def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
markers <- Map.values(result) do
render(conn, "markers.json", %{markers: markers})
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index ac8c18f24..e7767de4e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -15,9 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
require Logger
- plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:empty_array, :empty_object]
+ )
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 2b6f00952..513de279f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -11,19 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
alias Pleroma.Web.ActivityPub.ActivityPub
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
- plug(OAuthScopesPlug, %{scopes: ["write:media"]})
+ plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show)
+ plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation
@doc "POST /api/v1/media"
- def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
- description: Map.get(data, "description")
+ description: Map.get(data, :description)
) do
attachment_data = Map.put(object.data, "id", object.id)
@@ -31,11 +33,30 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
end
end
+ def create(_conn, _data), do: {:error, :bad_request}
+
+ @doc "POST /api/v2/media"
+ def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ with {:ok, object} <-
+ ActivityPub.upload(
+ file,
+ actor: User.ap_id(user),
+ description: Map.get(data, :description)
+ ) do
+ attachment_data = Map.put(object.data, "id", object.id)
+
+ conn
+ |> put_status(202)
+ |> render("attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def create2(_conn, _data), do: {:error, :bad_request}
+
@doc "PUT /api/v1/media/:id"
- def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description})
- when is_binary(description) do
+ def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id(id),
- true <- Object.authorize_mutation(object, user),
+ :ok <- Object.authorize_access(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
attachment_data = Map.put(data, "id", object.id)
@@ -43,5 +64,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
end
end
- def update(_conn, _data), do: {:error, :bad_request}
+ def update(conn, data), do: show(conn, data)
+
+ @doc "GET /api/v1/media/:id"
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
+ :ok <- Object.authorize_access(object, user) do
+ attachment_data = Map.put(data, "id", object_id)
+
+ render(conn, "attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def show(_conn, _data), do: {:error, :bad_request}
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index 0c9218454..e25cef30b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -13,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
@@ -20,16 +22,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
# GET /api/v1/notifications
- def index(conn, %{"account_id" => account_id} = params) do
+ def index(conn, %{account_id: account_id} = params) do
case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} ->
params =
params
- |> Map.delete("account_id")
- |> Map.put("account_ap_id", account_ap_id)
+ |> Map.delete(:account_id)
+ |> Map.put(:account_ap_id, account_ap_id)
index(conn, params)
@@ -40,16 +42,32 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
+ @default_notification_types ~w{
+ mention
+ follow
+ follow_request
+ reblog
+ favourite
+ move
+ pleroma:emoji_reaction
+ }
def index(%{assigns: %{user: user}} = conn, params) do
+ params =
+ Map.new(params, fn {k, v} -> {to_string(k), v} end)
+ |> Map.put_new("include_types", @default_notification_types)
+
notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(notifications)
- |> render("index.json", notifications: notifications, for: user)
+ |> render("index.json",
+ notifications: notifications,
+ for: user
+ )
end
# GET /api/v1/notifications/:id
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
@@ -66,8 +84,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
json(conn, %{})
end
- # POST /api/v1/notifications/dismiss
- def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
+ # POST /api/v1/notifications/:id/dismiss
+
+ def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
@@ -78,8 +97,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
+ # POST /api/v1/notifications/dismiss (deprecated)
+ def dismiss_via_body(%{body_params: params} = conn, _) do
+ dismiss(conn, params)
+ end
+
# DELETE /api/v1/notifications/destroy_multiple
- def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+ def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index d9f894118..db46ffcfc 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show
@@ -22,10 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
@doc "GET /api/v1/polls/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
@@ -37,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
@doc "POST /api/v1/polls/:id/votes"
- def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
+ def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{id: id}) do
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f5782be13..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -9,12 +9,13 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
@doc "POST /api/v1/reports"
- def create(%{assigns: %{user: user}} = conn, params) do
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index e1e6bd89b..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -11,19 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI
- plug(:assign_scheduled_activity when action != :index)
-
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(:assign_scheduled_activity when action != :index)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
+
@doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
@@ -37,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
@doc "PUT /api/v1/scheduled_statuses/:id"
- def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+ def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@@ -50,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
end
- defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index fcab4ef63..5a983db39 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -17,25 +17,34 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
require Logger
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
- def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
+
+ def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
accounts = User.search(query, search_options(params, user))
conn
|> put_view(AccountView)
- |> render("index.json", users: accounts, for: user, as: :user)
+ |> render("index.json",
+ users: accounts,
+ for: user,
+ as: :user
+ )
end
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
- defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
+ query = String.trim(query)
options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
@@ -43,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
result =
default_values
|> Enum.map(fn {resource, default_value} ->
- if params["type"] in [nil, resource] do
+ if params[:type] in [nil, resource] do
{resource, fn -> resource_search(version, resource, query, options) end}
else
{resource, fn -> default_value end}
@@ -66,12 +75,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp search_options(params, user) do
[
- resolve: params["resolve"] == "true",
- following: params["following"] == "true",
- limit: ControllerHelper.fetch_integer_param(params, "limit"),
- offset: ControllerHelper.fetch_integer_param(params, "offset"),
- type: params["type"],
+ resolve: params[:resolve],
+ following: params[:following],
+ limit: params[:limit],
+ offset: params[:offset],
+ type: params[:type],
author: get_author(params),
+ embed_relationships: ControllerHelper.embed_relationships?(params),
for_user: user
]
|> Enum.filter(&elem(&1, 1))
@@ -79,36 +89,90 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end)
- AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
+
+ AccountView.render("index.json",
+ users: accounts,
+ for: options[:for_user],
+ embed_relationships: options[:embed_relationships]
+ )
end
defp resource_search(_, "statuses", query, options) do
statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
- StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity)
+
+ StatusView.render("index.json",
+ activities: statuses,
+ for: options[:for_user],
+ as: :activity
+ )
end
- defp resource_search(:v2, "hashtags", query, _options) do
+ defp resource_search(:v2, "hashtags", query, options) do
tags_path = Web.base_url() <> "/tag/"
query
- |> prepare_tags()
+ |> prepare_tags(options)
|> Enum.map(fn tag ->
- tag = String.trim_leading(tag, "#")
%{name: tag, url: tags_path <> tag}
end)
end
- defp resource_search(:v1, "hashtags", query, _options) do
- query
- |> prepare_tags()
- |> Enum.map(fn tag -> String.trim_leading(tag, "#") end)
+ defp resource_search(:v1, "hashtags", query, options) do
+ prepare_tags(query, options)
end
- defp prepare_tags(query) do
- query
- |> String.split()
- |> Enum.uniq()
- |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
+ defp prepare_tags(query, options) do
+ tags =
+ query
+ |> preprocess_uri_query()
+ |> String.split(~r/[^#\w]+/u, trim: true)
+ |> Enum.uniq_by(&String.downcase/1)
+
+ explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
+
+ tags =
+ if Enum.any?(explicit_tags) do
+ explicit_tags
+ else
+ tags
+ end
+
+ tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
+
+ tags =
+ if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
+ add_joined_tag(tags)
+ else
+ tags
+ end
+
+ Pleroma.Pagination.paginate(tags, options)
+ end
+
+ defp add_joined_tag(tags) do
+ tags
+ |> Kernel.++([joined_tag(tags)])
+ |> Enum.uniq_by(&String.downcase/1)
+ end
+
+ # If `query` is a URI, returns last component of its path, otherwise returns `query`
+ defp preprocess_uri_query(query) do
+ if query =~ ~r/https?:\/\// do
+ query
+ |> String.trim_trailing("/")
+ |> URI.parse()
+ |> Map.get(:path)
+ |> String.split("/")
+ |> Enum.at(-1)
+ else
+ query
+ end
+ end
+
+ defp joined_tag(tags) do
+ tags
+ |> Enum.map(fn tag -> String.capitalize(tag) end)
+ |> Enum.join()
end
defp with_fallback(f, fallback \\ []) do
@@ -121,7 +185,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
end
end
- defp get_author(%{"account_id" => account_id}) when is_binary(account_id),
+ defp get_author(%{account_id: account_id}) when is_binary(account_id),
do: User.get_cached_by_id(account_id)
defp get_author(_params), do: nil
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 5c90065f6..ecfa38489 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -5,7 +5,8 @@
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
+ import Pleroma.Web.ControllerHelper,
+ only: [try_render: 3, add_link_headers: 2]
require Ecto.Query
@@ -23,6 +24,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
+
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
plug(
@@ -76,19 +80,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
plug(
RateLimiter,
- [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
+ [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
- [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+ [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
when action in ~w(favourite unfavourite)a
)
@@ -96,12 +98,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
+
@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`
`ids` query param is required
"""
- def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
+ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
limit = 100
activities =
@@ -110,7 +114,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
- render(conn, "index.json", activities: activities, for: user, as: :activity)
+ render(conn, "index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
end
@doc """
@@ -119,20 +127,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
def create(
- %{assigns: %{user: user}} = conn,
- %{"status" => _, "scheduled_at" => scheduled_at} = params
- ) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ %{
+ assigns: %{user: user},
+ body_params: %{status: _, scheduled_at: scheduled_at} = params
+ } = conn,
+ _
+ )
+ when not is_nil(scheduled_at) do
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+
+ attrs = %{
+ params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
+ scheduled_at: scheduled_at
+ }
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
- attrs <- %{"params" => params, "scheduled_at" => scheduled_at},
{:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
else
{:far_enough, _} ->
- create(conn, Map.drop(params, ["scheduled_at"]))
+ params = Map.drop(params, [:scheduled_at])
+ create(%Plug.Conn{conn | body_params: params}, %{})
error ->
error
@@ -144,8 +161,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a regular status
"""
- def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
@@ -155,6 +172,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
with_direct_conversation_id: true
)
else
+ {:error, {:reject, message}} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
@@ -162,12 +184,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
- def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
- create(conn, Map.put(params, "status", ""))
+ def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
+ params = Map.put(params, :status, "")
+ create(%Plug.Conn{conn | body_params: params}, %{})
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
@@ -181,63 +204,68 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "DELETE /api/v1/statuses/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- json(conn, %{})
+ def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
+ try_render(conn, "show.json",
+ activity: activity,
+ for: user,
+ with_direct_conversation_id: true,
+ with_source: true
+ )
else
- {:error, :not_found} = e -> e
- _e -> render_error(conn, :forbidden, "Can't delete this post")
+ _e -> {:error, :not_found}
end
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
+ def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
+ with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/unreblog"
- def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
+ def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unfavourite"
- def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/pin"
- def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unpin"
- def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
- def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -247,7 +275,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unbookmark"
- def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -257,7 +285,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/mute"
- def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -265,7 +293,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unmute"
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -274,7 +302,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
- def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+ def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
@@ -285,8 +313,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/favourited_by"
- def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with true <- Pleroma.Config.get([:instance, :show_reactions]),
+ %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
users =
@@ -305,7 +334,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
- def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
@@ -337,13 +366,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/context"
- def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def context(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
- "blocking_user" => user,
- "user" => user,
- "exclude_id" => activity.id
+ blocking_user: user,
+ user: user,
+ exclude_id: activity.id
})
render(conn, "context.json", activity: activity, activities: activities, user: user)
@@ -351,16 +380,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/favourites"
- def favourites(%{assigns: %{user: user}} = conn, params) do
- activities =
- ActivityPub.fetch_favourites(
- user,
- Map.take(params, Pleroma.Pagination.page_keys())
- )
+ def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
+ activities = ActivityPub.fetch_favourites(user, params)
conn
|> add_link_headers(activities)
- |> render("index.json", activities: activities, for: user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
end
@doc "GET /api/v1/bookmarks"
@@ -378,6 +407,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
conn
|> add_link_headers(bookmarks)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 11df6fc4a..34eac97c5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -6,47 +6,42 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
@moduledoc "The module represents functions to manage user subscriptions."
use Pleroma.Web, :controller
- alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
action_fallback(:errors)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:restrict_push_enabled)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation
# Creates PushSubscription
# POST /api/v1/push/subscription
#
- def create(%{assigns: %{user: user, token: token}} = conn, params) do
- with true <- Push.enabled(),
- {:ok, _} <- Subscription.delete_if_exists(user, token),
+ def create(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
+ with {:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ render(conn, "show.json", subscription: subscription)
end
end
# Gets PushSubscription
# GET /api/v1/push/subscription
#
- def get(%{assigns: %{user: user, token: token}} = conn, _params) do
- with true <- Push.enabled(),
- {:ok, subscription} <- Subscription.get(user, token) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ def show(%{assigns: %{user: user, token: token}} = conn, _params) do
+ with {:ok, subscription} <- Subscription.get(user, token) do
+ render(conn, "show.json", subscription: subscription)
end
end
# Updates PushSubscription
# PUT /api/v1/push/subscription
#
- def update(%{assigns: %{user: user, token: token}} = conn, params) do
- with true <- Push.enabled(),
- {:ok, subscription} <- Subscription.update(user, token, params) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ def update(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
+ with {:ok, subscription} <- Subscription.update(user, token, params) do
+ render(conn, "show.json", subscription: subscription)
end
end
@@ -54,17 +49,26 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# DELETE /api/v1/push/subscription
#
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
- with true <- Push.enabled(),
- {:ok, _response} <- Subscription.delete(user, token),
+ with {:ok, _response} <- Subscription.delete(user, token),
do: json(conn, %{})
end
+ defp restrict_push_enabled(conn, _) do
+ if Push.enabled() do
+ conn
+ else
+ conn
+ |> render_error(:forbidden, "Web push subscription is disabled on this Pleroma instance")
+ |> halt()
+ end
+ end
+
# fallback action
#
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
- |> json(dgettext("errors", "Not found"))
+ |> json(%{error: dgettext("errors", "Record not found")})
end
def errors(conn, _) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index c93a43969..f91df9ab7 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -5,11 +5,26 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
-
require Logger
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %OpenApiSpex.Operation{
+ tags: ["Suggestions"],
+ summary: "Follow suggestions (Not implemented)",
+ operationId: "SuggestionController.index",
+ responses: %{
+ 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response()
+ }
+ }
+ end
@doc "GET /api/v1/suggestions"
def index(conn, params),
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index c3cebd71e..5272790d3 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -6,17 +6,21 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]
+ only: [add_link_headers: 2, add_link_headers: 3]
+ alias Pleroma.Config
alias Pleroma.Pagination
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- # TODO: Replace with a macro when there is a Phoenix release with
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
+
+ # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
- # in it
plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
@@ -27,18 +31,26 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action in [:public, :hashtag]
+ )
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
+
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
params =
params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
+ |> Map.put(:type, ["Create", "Announce"])
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:reply_filtering_user, user)
+ |> Map.put(:announce_filtering_user, user)
+ |> Map.put(:user, user)
activities =
[user.ap_id | User.following(user)]
@@ -47,16 +59,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities)
- |> render("index.json", activities: activities, for: user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
end
# GET /api/v1/timelines/direct
def direct(%{assigns: %{user: user}} = conn, params) do
params =
params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
+ |> Map.put(:type, "Create")
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:user, user)
|> Map.put(:visibility, "direct")
activities =
@@ -66,77 +82,110 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities)
- |> render("index.json", activities: activities, for: user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ end
+
+ defp restrict_unauthenticated?(true = _local_only) do
+ Config.restrict_unauthenticated_access?(:timelines, :local)
+ end
+
+ defp restrict_unauthenticated?(_) do
+ Config.restrict_unauthenticated_access?(:timelines, :federated)
end
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
+ local_only = params[:local]
- activities =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> ActivityPub.fetch_public_activities()
+ if is_nil(user) and restrict_unauthenticated?(local_only) do
+ fail_on_bad_auth(conn)
+ else
+ activities =
+ params
+ |> Map.put(:type, ["Create"])
+ |> Map.put(:local_only, local_only)
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:reply_filtering_user, user)
+ |> ActivityPub.fetch_public_activities()
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ end
+ end
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> render("index.json", activities: activities, for: user, as: :activity)
+ defp fail_on_bad_auth(conn) do
+ render_error(conn, :unauthorized, "authorization required for timeline view")
end
- def hashtag_fetching(params, user, local_only) do
+ defp hashtag_fetching(params, user, local_only) do
tags =
- [params["tag"], params["any"]]
+ [params[:tag], params[:any]]
|> List.flatten()
|> Enum.uniq()
- |> Enum.filter(& &1)
- |> Enum.map(&String.downcase(&1))
+ |> Enum.reject(&is_nil/1)
+ |> Enum.map(&String.downcase/1)
tag_all =
params
- |> Map.get("all", [])
- |> Enum.map(&String.downcase(&1))
+ |> Map.get(:all, [])
+ |> Enum.map(&String.downcase/1)
tag_reject =
params
- |> Map.get("none", [])
- |> Enum.map(&String.downcase(&1))
+ |> Map.get(:none, [])
+ |> Enum.map(&String.downcase/1)
_activities =
params
- |> Map.put("type", "Create")
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> Map.put("tag", tags)
- |> Map.put("tag_all", tag_all)
- |> Map.put("tag_reject", tag_reject)
+ |> Map.put(:type, "Create")
+ |> Map.put(:local_only, local_only)
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:user, user)
+ |> Map.put(:tag, tags)
+ |> Map.put(:tag_all, tag_all)
+ |> Map.put(:tag_reject, tag_reject)
|> ActivityPub.fetch_public_activities()
end
# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
+ local_only = params[:local]
- activities = hashtag_fetching(params, user, local_only)
-
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> render("index.json", activities: activities, for: user, as: :activity)
+ if is_nil(user) and restrict_unauthenticated?(local_only) do
+ fail_on_bad_auth(conn)
+ else
+ activities = hashtag_fetching(params, user, local_only)
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ end
end
# GET /api/v1/timelines/list/:list_id
- def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
+ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put("muting_user", user)
+ |> Map.put(:type, "Create")
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:user, user)
+ |> Map.put(:muting_user, user)
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
@@ -149,7 +198,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
- render(conn, "index.json", activities: activities, for: user, as: :activity)
+ render(conn, "index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
else
_e -> render_error(conn, :forbidden, "Error.")
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 3fe2be521..694bf5ca8 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
- alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
@@ -55,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
user
|> Notification.for_user_query(options)
+ |> restrict(:include_types, options)
|> restrict(:exclude_types, options)
|> restrict(:account_ap_id, options)
|> Pagination.fetch_paginated(params)
@@ -69,10 +69,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string},
+ include_types: {:array, :string},
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean,
- with_move: :boolean,
account_ap_id: :string
}
@@ -80,14 +80,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
changeset.changes
end
- defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
- ap_types =
- mastodon_types
- |> Enum.map(&Activity.from_mastodon_notification_type/1)
- |> Enum.filter(& &1)
+ defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
+ where(query, [n], n.type in ^mastodon_types)
+ end
- query
- |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
+ where(query, [n], n.type not in ^mastodon_types)
end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 6ff84c957..864c0417f 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -5,21 +5,60 @@
defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view
+ alias Pleroma.FollowingRelationship
alias Pleroma.User
+ alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users} = opts) do
+ reading_user = opts[:for]
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) || !opts[:embed_relationships] ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ UserRelationship.view_relationships_option(reading_user, users)
+ end
+
+ opts =
+ opts
+ |> Map.merge(%{relationships: relationships_opt, as: :user})
+ |> Map.delete(:users)
+
users
|> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
end
- def render("show.json", %{user: user} = opts) do
- if User.visible_for?(user, opts[:for]),
- do: do_render("show.json", opts),
- else: %{}
+ @doc """
+ Renders specified user account.
+ :skip_visibility_check option skips visibility check and renders any user (local or remote)
+ regardless of [:pleroma, :restrict_unauthenticated] setting.
+ :for option specifies the requester and can be a User record or nil.
+ Only use `user: user, for: user` when `user` is the actual requester of own profile.
+ """
+ def render("show.json", %{user: _user, skip_visibility_check: true} = opts) do
+ do_render("show.json", opts)
+ end
+
+ def render("show.json", %{user: user, for: for_user_or_nil} = opts) do
+ if User.visible_for(user, for_user_or_nil) == :visible do
+ do_render("show.json", opts)
+ else
+ %{}
+ end
+ end
+
+ def render("show.json", _) do
+ raise "In order to prevent account accessibility issues, " <>
+ ":skip_visibility_check or :for option is required."
end
def render("mention.json", %{user: user}) do
@@ -35,34 +74,107 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
%{}
end
- def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
- follow_state = User.get_cached_follow_state(user, target)
+ def render(
+ "relationship.json",
+ %{user: %User{} = reading_user, target: %User{} = target} = opts
+ ) do
+ user_relationships = get_in(opts, [:relationships, :user_relationships])
+ following_relationships = get_in(opts, [:relationships, :following_relationships])
- requested =
- if follow_state && !User.following?(user, target) do
- follow_state == "pending"
+ follow_state =
+ if following_relationships do
+ user_to_target_following_relation =
+ FollowingRelationship.find(following_relationships, reading_user, target)
+
+ User.get_follow_state(reading_user, target, user_to_target_following_relation)
else
- false
+ User.get_follow_state(reading_user, target)
end
+ followed_by =
+ if following_relationships do
+ case FollowingRelationship.find(following_relationships, target, reading_user) do
+ %{state: :follow_accept} -> true
+ _ -> false
+ end
+ else
+ User.following?(target, reading_user)
+ end
+
+ # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
- following: User.following?(user, target),
- followed_by: User.following?(target, user),
- blocking: User.blocks_user?(user, target),
- blocked_by: User.blocks_user?(target, user),
- muting: User.mutes?(user, target),
- muting_notifications: User.muted_notifications?(user, target),
- subscribing: User.subscribed_to?(user, target),
- requested: requested,
- domain_blocking: User.blocks_domain?(user, target),
- showing_reblogs: User.showing_reblogs?(user, target),
+ following: follow_state == :follow_accept,
+ followed_by: followed_by,
+ blocking:
+ UserRelationship.exists?(
+ user_relationships,
+ :block,
+ reading_user,
+ target,
+ &User.blocks_user?(&1, &2)
+ ),
+ blocked_by:
+ UserRelationship.exists?(
+ user_relationships,
+ :block,
+ target,
+ reading_user,
+ &User.blocks_user?(&1, &2)
+ ),
+ muting:
+ UserRelationship.exists?(
+ user_relationships,
+ :mute,
+ reading_user,
+ target,
+ &User.mutes?(&1, &2)
+ ),
+ muting_notifications:
+ UserRelationship.exists?(
+ user_relationships,
+ :notification_mute,
+ reading_user,
+ target,
+ &User.muted_notifications?(&1, &2)
+ ),
+ subscribing:
+ UserRelationship.exists?(
+ user_relationships,
+ :inverse_subscription,
+ target,
+ reading_user,
+ &User.subscribed_to?(&2, &1)
+ ),
+ requested: follow_state == :follow_pending,
+ domain_blocking: User.blocks_domain?(reading_user, target),
+ showing_reblogs:
+ not UserRelationship.exists?(
+ user_relationships,
+ :reblog_mute,
+ reading_user,
+ target,
+ &User.muting_reblogs?(&1, &2)
+ ),
endorsed: false
}
end
- def render("relationships.json", %{user: user, targets: targets}) do
- render_many(targets, AccountView, "relationship.json", user: user, as: :target)
+ def render("relationships.json", %{user: user, targets: targets} = opts) do
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ UserRelationship.view_relationships_option(user, targets)
+ end
+
+ render_opts = %{as: :target, user: user, relationships: relationships_opt}
+ render_many(targets, AccountView, "relationship.json", render_opts)
end
defp do_render("show.json", %{user: user} = opts) do
@@ -86,21 +198,42 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
0
end
- bot = user.actor_type in ["Application", "Service"]
+ bot = user.actor_type == "Service"
emojis =
- (user.source_data["tag"] || [])
- |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
- |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ Enum.map(user.emoji, fn {shortcode, raw_url} ->
+ url = MediaProxy.url(raw_url)
+
%{
- "shortcode" => String.trim(name, ":"),
- "url" => MediaProxy.url(url),
- "static_url" => MediaProxy.url(url),
- "visible_in_picker" => false
+ shortcode: shortcode,
+ url: url,
+ static_url: url,
+ visible_in_picker: false
}
end)
- relationship = render("relationship.json", %{user: opts[:for], target: user})
+ relationship =
+ if opts[:embed_relationships] do
+ render("relationship.json", %{
+ user: opts[:for],
+ target: user,
+ relationships: opts[:relationships]
+ })
+ else
+ %{}
+ end
+
+ favicon =
+ if Pleroma.Config.get([:instances_favicons, :enabled]) do
+ user
+ |> Map.get(:ap_id, "")
+ |> URI.parse()
+ |> URI.merge("/")
+ |> Pleroma.Instances.Instance.get_or_update_favicon()
+ |> MediaProxy.url()
+ else
+ nil
+ end
%{
id: to_string(user.id),
@@ -122,7 +255,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
fields: user.fields,
bot: bot,
source: %{
- note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
+ note: user.raw_bio || "",
sensitive: false,
fields: user.raw_fields,
pleroma: %{
@@ -133,6 +266,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# Pleroma extension
pleroma: %{
+ ap_id: user.ap_id,
confirmation_pending: user.confirmation_pending,
tags: user.tags,
hide_followers_count: user.hide_followers_count,
@@ -142,7 +276,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
hide_favorites: user.hide_favorites,
relationship: relationship,
skip_thread_containment: user.skip_thread_containment,
- background_image: image_url(user.background) |> MediaProxy.url()
+ background_image: image_url(user.background) |> MediaProxy.url(),
+ accepts_chat_messages: user.accepts_chat_messages,
+ favicon: favicon
}
}
|> maybe_put_role(user, opts[:for])
@@ -154,6 +290,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_allow_following_move(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for])
+ |> maybe_put_unread_notification_count(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@@ -224,7 +361,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
- Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings)
+ Kernel.put_in(
+ data,
+ [:pleroma, :notification_settings],
+ Map.from_struct(user.notification_settings)
+ )
end
defp maybe_put_notification_settings(data, _, _), do: data
@@ -251,6 +392,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_unread_conversation_count(data, _, _), do: data
+ defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: user_id} = user) do
+ Kernel.put_in(
+ data,
+ [:pleroma, :unread_notifications_count],
+ Pleroma.Notification.unread_notifications_count(user)
+ )
+ end
+
+ defp maybe_put_unread_notification_count(data, _, _), do: data
+
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex
index d934e2107..e44272c6f 100644
--- a/lib/pleroma/web/mastodon_api/views/app_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
alias Pleroma.Web.OAuth.App
+ def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do
+ %{
+ apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}),
+ count: count,
+ page_size: page_size
+ }
+ end
+
+ def render("show.json", %{admin: true, app: %App{} = app} = assigns) do
+ "show.json"
+ |> render(Map.delete(assigns, :admin))
+ |> Map.put(:trusted, app.trusted)
+ |> Map.put(:id, app.id)
+ end
+
def render("show.json", %{app: %App{} = app}) do
%{
id: app.id |> to_string,
@@ -30,10 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
defp with_vapid_key(data) do
vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key]
- if vapid_key do
- Map.put(data, "vapid_key", vapid_key)
- else
- data
- end
+ Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 2b6f84c72..a91994915 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -23,10 +23,13 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
last_activity_id =
with nil <- participation.last_activity_id do
- ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
- "user" => user,
- "blocking_user" => user
- })
+ ActivityPub.fetch_latest_direct_activity_id_for_context(
+ participation.conversation.ap_id,
+ %{
+ user: user,
+ blocking_user: user
+ }
+ )
end
activity = Activity.get_by_id_with_object(last_activity_id)
@@ -35,7 +38,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
%{
id: participation.id |> to_string(),
- accounts: render(AccountView, "index.json", users: users, as: :user),
+ accounts: render(AccountView, "index.json", users: users, for: user),
unread: !participation.read,
last_status:
render(StatusView, "show.json",
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 97fd1e83f..c37f624e0 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.FilterView
- def render("filters.json", %{filters: filters} = opts) do
- render_many(filters, FilterView, "filter.json", opts)
+ def render("index.json", %{filters: filters}) do
+ render_many(filters, FilterView, "show.json")
end
- def render("filter.json", %{filter: filter}) do
+ def render("show.json", %{filter: filter}) do
expires_at =
if filter.expires_at do
Utils.to_masto_date(filter.expires_at)
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
context: filter.context,
expires_at: expires_at,
irreversible: filter.hide,
- whole_word: false
+ whole_word: filter.whole_word
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 67214dbea..ea2d3aa9c 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view
+ alias Pleroma.Config
+ alias Pleroma.Web.ActivityPub.MRF
+
@mastodon_api_level "2.7.2"
def render("show.json", _) do
- instance = Pleroma.Config.get(:instance)
+ instance = Config.get(:instance)
%{
uri: Pleroma.Web.base_url(),
@@ -20,16 +23,84 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
- thumbnail: Pleroma.Web.base_url() <> "/instance/thumbnail.jpeg",
+ thumbnail: Keyword.get(instance, :instance_thumbnail),
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
+ approval_required: Keyword.get(instance, :account_approval_required),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
poll_limits: Keyword.get(instance, :poll_limits),
upload_limit: Keyword.get(instance, :upload_limit),
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
background_upload_limit: Keyword.get(instance, :background_upload_limit),
- banner_upload_limit: Keyword.get(instance, :banner_upload_limit)
+ banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
+ background_image: Keyword.get(instance, :background_image),
+ chat_limit: Keyword.get(instance, :chat_limit),
+ description_limit: Keyword.get(instance, :description_limit),
+ pleroma: %{
+ metadata: %{
+ account_activation_required: Keyword.get(instance, :account_activation_required),
+ features: features(),
+ federation: federation(),
+ fields_limits: fields_limits(),
+ post_formats: Config.get([:instance, :allowed_post_formats])
+ },
+ vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
+ }
+ end
+
+ def features do
+ [
+ "pleroma_api",
+ "mastodon_api",
+ "mastodon_api_streaming",
+ "polls",
+ "pleroma_explicit_addressing",
+ "shareable_emoji_packs",
+ "multifetch",
+ "pleroma:api/v1/notifications:include_types_filter",
+ if Config.get([:media_proxy, :enabled]) do
+ "media_proxy"
+ end,
+ if Config.get([:gopher, :enabled]) do
+ "gopher"
+ end,
+ if Config.get([:chat, :enabled]) do
+ "chat"
+ end,
+ if Config.get([:instance, :allow_relay]) do
+ "relay"
+ end,
+ if Config.get([:instance, :safe_dm_mentions]) do
+ "safe_dm_mentions"
+ end,
+ "pleroma_emoji_reactions",
+ "pleroma_chat_messages"
+ ]
+ |> Enum.filter(& &1)
+ end
+
+ def federation do
+ quarantined = Config.get([:instance, :quarantined_instances], [])
+
+ if Config.get([:mrf, :transparency]) do
+ {:ok, data} = MRF.describe()
+
+ data
+ |> Map.merge(%{quarantined_instances: quarantined})
+ else
+ %{}
+ end
+ |> Map.put(:enabled, Config.get([:instance, :federating]))
+ end
+
+ def fields_limits do
+ %{
+ max_fields: Config.get([:instance, :max_account_fields]),
+ max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
+ name_length: Config.get([:instance, :account_field_name_length]),
+ value_length: Config.get([:instance, :account_field_value_length])
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
index 985368fe5..21d535d54 100644
--- a/lib/pleroma/web/mastodon_api/views/marker_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -6,12 +6,16 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
use Pleroma.Web, :view
def render("markers.json", %{markers: markers}) do
- Enum.reduce(markers, %{}, fn m, acc ->
- Map.put_new(acc, m.timeline, %{
- last_read_id: m.last_read_id,
- version: m.lock_version,
- updated_at: NaiveDateTime.to_iso8601(m.updated_at)
- })
+ Map.new(markers, fn m ->
+ {m.timeline,
+ %{
+ last_read_id: m.last_read_id,
+ version: m.lock_version,
+ updated_at: NaiveDateTime.to_iso8601(m.updated_at),
+ pleroma: %{
+ unread_count: m.unread_count
+ }
+ }}
end)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 1720fbead..c97e6d32f 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -6,74 +6,150 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
+ alias Pleroma.Object
alias Pleroma.User
+ alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
- def render("index.json", %{notifications: notifications, for: user}) do
- safe_render_many(notifications, NotificationView, "show.json", %{for: user})
+ @parent_types ~w{Like Announce EmojiReact}
+
+ def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
+ activities = Enum.map(notifications, & &1.activity)
+
+ parent_activities =
+ activities
+ |> Enum.filter(fn
+ %{data: %{"type" => type}} ->
+ type in @parent_types
+ end)
+ |> Enum.map(& &1.data["object"])
+ |> Activity.create_by_object_ap_id()
+ |> Activity.with_preloaded_object(:left)
+ |> Pleroma.Repo.all()
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ move_activities_targets =
+ activities
+ |> Enum.filter(&(&1.data["type"] == "Move"))
+ |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
+ |> Enum.filter(& &1)
+
+ actors =
+ activities
+ |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end)
+ |> Enum.filter(& &1)
+ |> Kernel.++(move_activities_targets)
+
+ UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
+ end
+
+ opts =
+ opts
+ |> Map.put(:parent_activities, parent_activities)
+ |> Map.put(:relationships, relationships_opt)
+
+ safe_render_many(notifications, NotificationView, "show.json", opts)
end
- def render("show.json", %{
- notification: %Notification{activity: activity} = notification,
- for: user
- }) do
+ def render(
+ "show.json",
+ %{
+ notification: %Notification{activity: activity} = notification,
+ for: reading_user
+ } = opts
+ ) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
- parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
- mastodon_type = Activity.mastodon_notification_type(activity)
-
- with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do
- response = %{
- id: to_string(notification.id),
- type: mastodon_type,
- created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
- account: account,
- pleroma: %{
- is_seen: notification.seen
- }
+
+ parent_activity_fn = fn ->
+ if opts[:parent_activities] do
+ Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"])
+ else
+ Activity.get_create_by_object_ap_id(activity.data["object"])
+ end
+ end
+
+ # Note: :relationships contain user mutes (needed for :muted flag in :status)
+ status_render_opts = %{relationships: opts[:relationships]}
+ account = AccountView.render("show.json", %{user: actor, for: reading_user})
+
+ response = %{
+ id: to_string(notification.id),
+ type: notification.type,
+ created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
+ account: account,
+ pleroma: %{
+ is_muted: User.mutes?(reading_user, actor),
+ is_seen: notification.seen
}
+ }
- case mastodon_type do
- "mention" ->
- put_status(response, activity, user)
+ case notification.type do
+ "mention" ->
+ put_status(response, activity, reading_user, status_render_opts)
- "favourite" ->
- put_status(response, parent_activity, user)
+ "favourite" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
- "reblog" ->
- put_status(response, parent_activity, user)
+ "reblog" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
- "move" ->
- put_target(response, activity, user)
+ "move" ->
+ put_target(response, activity, reading_user, %{})
- "pleroma:emoji_reaction" ->
- put_status(response, parent_activity, user) |> put_emoji(activity)
+ "pleroma:emoji_reaction" ->
+ response
+ |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
+ |> put_emoji(activity)
- type when type in ["follow", "follow_request"] ->
- response
+ "pleroma:chat_mention" ->
+ put_chat_message(response, activity, reading_user, status_render_opts)
- _ ->
- nil
- end
- else
- _ -> nil
+ type when type in ["follow", "follow_request"] ->
+ response
end
end
defp put_emoji(response, activity) do
- response
- |> Map.put(:emoji, activity.data["content"])
+ Map.put(response, :emoji, activity.data["content"])
end
- defp put_status(response, activity, user) do
- Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
+ defp put_chat_message(response, activity, reading_user, opts) do
+ object = Object.normalize(activity)
+ author = User.get_cached_by_ap_id(object.data["actor"])
+ chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+ render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref})
+ chat_message_render = MessageReferenceView.render("show.json", render_opts)
+
+ Map.put(response, :chat_message, chat_message_render)
end
- defp put_target(response, activity, user) do
- target = User.get_cached_by_ap_id(activity.data["target"])
- Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
+ defp put_status(response, activity, reading_user, opts) do
+ status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
+ status_render = StatusView.render("show.json", status_render_opts)
+
+ Map.put(response, :status, status_render)
+ end
+
+ defp put_target(response, activity, reading_user, opts) do
+ target_user = User.get_cached_by_ap_id(activity.data["target"])
+ target_render_opts = Map.merge(opts, %{user: target_user, for: reading_user})
+ target_render = AccountView.render("show.json", target_render_opts)
+
+ Map.put(response, :target, target_render)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 40edbb213..1208dc9a0 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired,
multiple: multiple,
votes_count: votes_count,
+ voters_count: (multiple || nil) && voters_count(object),
options: options,
voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
@@ -27,10 +28,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
def render("show.json", %{object: object} = params) do
case object.data do
- %{"anyOf" => options} when is_list(options) ->
+ %{"anyOf" => [_ | _] = options} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
- %{"oneOf" => options} when is_list(options) ->
+ %{"oneOf" => [_ | _] = options} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
_ ->
@@ -39,15 +40,13 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end
defp end_time_and_expired(object) do
- case object.data["closed"] || object.data["endTime"] do
- end_time when is_binary(end_time) ->
- end_time = NaiveDateTime.from_iso8601!(end_time)
- expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
+ if object.data["closed"] do
+ end_time = NaiveDateTime.from_iso8601!(object.data["closed"])
+ expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
- {Utils.to_masto_date(end_time), expired}
-
- _ ->
- {nil, false}
+ {Utils.to_masto_date(end_time), expired}
+ else
+ {nil, false}
end
end
@@ -62,6 +61,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end)
end
+ defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
+ length(voters)
+ end
+
+ defp voters_count(_), do: 0
+
defp voted?(%{object: object} = opts) do
if opts[:for] do
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index 458f6bc78..5b896bf3b 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
defp with_media_attachments(data, _), do: data
defp status_params(params) do
- data = %{
+ %{
text: params["status"],
sensitive: params["sensitive"],
spoiler_text: params["spoiler_text"],
@@ -39,10 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
poll: params["poll"],
in_reply_to_id: params["in_reply_to_id"]
}
-
- case params["media_ids"] do
- nil -> data
- media_ids -> Map.put(data, :media_ids, media_ids)
- end
+ |> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"])
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index f0bdb49c3..01b8bb6bb 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
@@ -20,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
- import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
+ import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
@@ -44,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end
- defp get_user(ap_id) do
+ def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
@@ -52,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = User.get_by_guessed_nickname(ap_id) ->
user
- true ->
+ fake_record_fallback ->
+ # TODO: refactor (fake records is never a good idea)
User.error_user(ap_id)
+
+ true ->
+ nil
end
end
@@ -71,10 +76,47 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def render("index.json", opts) do
- replied_to_activities = get_replied_to_activities(opts.activities)
- opts = Map.put(opts, :replied_to_activities, replied_to_activities)
+ reading_user = opts[:for]
+
+ # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
+ activities = Enum.filter(opts.activities, & &1)
+ replied_to_activities = get_replied_to_activities(activities)
- safe_render_many(opts.activities, StatusView, "show.json", opts)
+ parent_activities =
+ activities
+ |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
+ |> Enum.map(&Object.normalize(&1).data["id"])
+ |> Activity.create_by_object_ap_id()
+ |> Activity.with_preloaded_object(:left)
+ |> Activity.with_preloaded_bookmark(reading_user)
+ |> Activity.with_set_thread_muted_field(reading_user)
+ |> Repo.all()
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ # Note: unresolved users are filtered out
+ actors =
+ (activities ++ parent_activities)
+ |> Enum.map(&get_user(&1.data["actor"], false))
+ |> Enum.filter(& &1)
+
+ UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
+ end
+
+ opts =
+ opts
+ |> Map.put(:replied_to_activities, replied_to_activities)
+ |> Map.put(:parent_activities, parent_activities)
+ |> Map.put(:relationships, relationships_opt)
+
+ safe_render_many(activities, StatusView, "show.json", opts)
end
def render(
@@ -85,17 +127,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
- reblogged_activity =
- Activity.create_by_object_ap_id(activity_object.data["id"])
- |> Activity.with_preloaded_bookmark(opts[:for])
- |> Activity.with_set_thread_muted_field(opts[:for])
- |> Repo.one()
+ reblogged_parent_activity =
+ if opts[:parent_activities] do
+ Activity.Queries.find_by_object_ap_id(
+ opts[:parent_activities],
+ activity_object.data["id"]
+ )
+ else
+ Activity.create_by_object_ap_id(activity_object.data["id"])
+ |> Activity.with_preloaded_bookmark(opts[:for])
+ |> Activity.with_set_thread_muted_field(opts[:for])
+ |> Repo.one()
+ end
- reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
+ reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
+ reblogged = render("show.json", reblog_rendering_opts)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
- bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
+ bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
mentions =
activity.recipients
@@ -107,7 +157,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: activity_object.data["id"],
url: activity_object.data["id"],
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for]
+ }),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
@@ -116,7 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
- reblogged: reblogged?(reblogged_activity, opts[:for]),
+ reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
@@ -183,9 +237,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
thread_muted? =
- case activity.thread_muted? do
- thread_muted? when is_boolean(thread_muted?) -> thread_muted?
- nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
+ cond do
+ is_nil(opts[:for]) -> false
+ is_boolean(activity.thread_muted?) -> activity.thread_muted?
+ true -> CommonAPI.thread_muted?(opts[:for], activity)
end
attachment_data = object.data["attachment"] || []
@@ -242,27 +297,47 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emoji_reactions =
with %{data: %{"reactions" => emoji_reactions}} <- object do
- Enum.map(emoji_reactions, fn [emoji, users] ->
- %{
- name: emoji,
- count: length(users),
- me: !!(opts[:for] && opts[:for].ap_id in users)
- }
+ Enum.map(emoji_reactions, fn
+ [emoji, users] when is_list(users) ->
+ build_emoji_map(emoji, users, opts[:for])
+
+ {emoji, users} when is_list(users) ->
+ build_emoji_map(emoji, users, opts[:for])
+
+ _ ->
+ nil
end)
+ |> Enum.reject(&is_nil/1)
else
_ -> []
end
+ # Status muted state (would do 1 request per status unless user mutes are preloaded)
+ muted =
+ thread_muted? ||
+ UserRelationship.exists?(
+ get_in(opts, [:relationships, :user_relationships]),
+ :mute,
+ opts[:for],
+ user,
+ fn for_user, user -> User.mutes?(for_user, user) end
+ )
+
%{
id: to_string(activity.id),
uri: object.data["id"],
url: url,
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for]
+ }),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content_html,
+ text: opts[:with_source] && object.data["source"],
created_at: created_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
@@ -270,7 +345,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
- muted: thread_muted? || User.mutes?(opts[:for], user),
+ muted: muted,
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: summary,
@@ -294,7 +369,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
expires_at: expires_at,
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?,
- emoji_reactions: emoji_reactions
+ emoji_reactions: emoji_reactions,
+ parent_visible: visible_for_user?(reply_to, opts[:for])
}
}
end
@@ -364,27 +440,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
- object = Object.normalize(activity)
-
- user = get_user(activity.data["actor"])
- created_at = Utils.to_masto_date(activity.data["published"])
-
- %{
- id: activity.id,
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
- created_at: created_at,
- title: object.data["title"] |> HTML.strip_tags(),
- artist: object.data["artist"] |> HTML.strip_tags(),
- album: object.data["album"] |> HTML.strip_tags(),
- length: object.data["length"]
- }
- end
-
- def render("listens.json", opts) do
- safe_render_many(opts.activities, StatusView, "listen.json", opts)
- end
-
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
@@ -418,23 +473,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
- def render_content(%{data: %{"type" => object_type}} = object)
- when object_type in ["Video", "Event", "Audio"] do
- with name when not is_nil(name) and name != "" <- object.data["name"] do
- "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
- else
- _ -> object.data["content"] || ""
- end
- end
+ def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
+ url = object.data["url"] || object.data["id"]
- def render_content(%{data: %{"type" => object_type}} = object)
- when object_type in ["Article", "Page"] do
- with summary when not is_nil(summary) and summary != "" <- object.data["name"],
- url when is_bitstring(url) <- object.data["url"] do
- "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
- else
- _ -> object.data["content"] || ""
- end
+ "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
end
def render_content(object), do: object.data["content"] || ""
@@ -451,11 +493,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
- object_tags = for tag when is_binary(tag) <- object_tags, do: tag
-
- Enum.reduce(object_tags, [], fn tag, tags ->
- tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
- end)
+ object_tags
+ |> Enum.filter(&is_binary/1)
+ |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
end
def build_tags(_), do: []
@@ -496,4 +536,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
do: id in pinned_activities
+
+ defp build_emoji_map(emoji, users, current_user) do
+ %{
+ name: emoji,
+ count: length(users),
+ me: !!(current_user && current_user.ap_id in users)
+ }
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
index d32cef6e2..7c67cc924 100644
--- a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
@@ -2,11 +2,11 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
use Pleroma.Web, :view
alias Pleroma.Web.Push
- def render("push_subscription.json", %{subscription: subscription}) do
+ def render("show.json", %{subscription: subscription}) do
%{
id: to_string(subscription.id),
endpoint: subscription.endpoint,
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index b1aebe014..94e4595d8 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -12,8 +12,12 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@behaviour :cowboy_websocket
- # Handled by periodic keepalive in Pleroma.Web.Streamer.Ping.
- @timeout :infinity
+ # Client ping period.
+ @tick :timer.seconds(30)
+ # Cowboy timeout period.
+ @timeout :timer.seconds(60)
+ # Hibernate every X messages
+ @hibernate_every 100
def init(%{qs: qs} = req, state) do
with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
@@ -28,7 +32,8 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
req
end
- {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
+ {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil},
+ %{idle_timeout: @timeout}}
else
{:error, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
@@ -43,28 +48,54 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
def websocket_init(state) do
- send(self(), :subscribe)
- {:ok, state}
- end
-
- # We never receive messages.
- def websocket_handle(_frame, state) do
- {:ok, state}
- end
-
- def websocket_info(:subscribe, state) do
Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{
(state.user || %{id: "anonymous"}).id
}, topic #{state.topic}"
)
- Streamer.add_socket(state.topic, streamer_socket(state))
+ Streamer.add_socket(state.topic, state.user)
+ {:ok, %{state | timer: timer()}}
+ end
+
+ # Client's Pong frame.
+ def websocket_handle(:pong, state) do
+ if state.timer, do: Process.cancel_timer(state.timer)
+ {:ok, %{state | timer: timer()}}
+ end
+
+ # We never receive messages.
+ def websocket_handle(frame, state) do
+ Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
+ def websocket_info({:render_with_user, view, template, item}, state) do
+ user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
+
+ unless Streamer.filtered_by_user?(user, item) do
+ websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
+ else
+ {:ok, state}
+ end
+ end
+
def websocket_info({:text, message}, state) do
- {:reply, {:text, message}, state}
+ # If the websocket processed X messages, force an hibernate/GC.
+ # We don't hibernate at every message to balance CPU usage/latency with RAM usage.
+ if state.count > @hibernate_every do
+ {:reply, {:text, message}, %{state | count: 0}, :hibernate}
+ else
+ {:reply, {:text, message}, %{state | count: state.count + 1}}
+ end
+ end
+
+ # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
+ # As we hibernate there, reset the count to 0.
+ # If the client misses :pong, Cowboy will automatically timeout the connection after
+ # `@idle_timeout`.
+ def websocket_info(:tick, state) do
+ {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
end
def terminate(reason, _req, state) do
@@ -74,7 +105,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
}, topic #{state.topic || "?"}: #{inspect(reason)}"
)
- Streamer.remove_socket(state.topic, streamer_socket(state))
+ Streamer.remove_socket(state.topic)
:ok
end
@@ -96,7 +127,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
- defp streamer_socket(state) do
- %{transport_pid: self(), assigns: state}
+ defp timer do
+ Process.send_after(self(), :tick, @tick)
end
end