X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Fmastodon_api%2Fmastodon_api_controller.ex;h=c3c75bd9a5a6b6f654426c0785f8edf17fe52dd0;hb=47c20ab79635257f4a8dcd9886821d10276516a3;hp=ceb88511b0e5e0d03d8dfc922f4ce9651c3f5607;hpb=a7994185739522dee80e22f76e5fdac1a3b8279b;p=akkoma diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index ceb88511b..c3c75bd9a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,6 +4,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + alias Ecto.Changeset alias Pleroma.Activity alias Pleroma.Bookmark @@ -15,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Pagination + alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.Stats @@ -45,9 +49,36 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do import Ecto.Query require Logger + require Pleroma.Constants + + @rate_limited_relations_actions ~w(follow unfollow)a + + @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status + post_status delete_status)a + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} + when action in ~w(reblog_status unreblog_status)a + ) + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} + when action in ~w(fav_status unfav_status)a + ) + + plug( + RateLimiter, + {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions + ) - plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register) - plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search]) + plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions) + plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) + plug(RateLimiter, :app_account_creation when action == :account_register) + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + plug(RateLimiter, :password_reset when action == :password_reset) + plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend) @local_mastodon_name "Mastodon-Local" @@ -160,10 +191,70 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) ) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Invalid request"}) + _e -> render_error(conn, :forbidden, "Invalid request") + end + end + + def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + change = Changeset.change(user, %{avatar: nil}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + + def update_avatar(%{assigns: %{user: user}} = conn, params) do + {:ok, object} = ActivityPub.upload(params, type: :avatar) + change = Changeset.change(user, %{avatar: object.data}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + + def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + with new_info <- %{"banner" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + end + + def update_banner(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), + new_info <- %{"banner" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + with new_info <- %{"background" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + json(conn, %{url: nil}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(params, type: :background), + new_info <- %{"background" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) end end @@ -195,10 +286,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do account = AccountView.render("account.json", %{user: user, for: for_user}) json(conn, account) else - _e -> - conn - |> put_status(404) - |> json(%{error: "Can't find user"}) + _e -> render_error(conn, :not_found, "Can't find user") end end @@ -242,7 +330,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do "static_url" => url, "visible_in_picker" => true, "url" => url, - "tags" => tags + "tags" => tags, + # Assuming that a comma is authorized in the category name + "category" => (tags -- ["Custom"]) |> Enum.join(",") } end) end @@ -345,6 +435,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) + |> Map.put("user", user) |> ActivityPub.fetch_public_activities() |> Enum.reverse() @@ -355,7 +446,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_id(params["id"]) do + with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do params = params |> Map.put("tag", params["tagged"]) @@ -446,15 +537,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> try_render("poll.json", %{object: object, for: user}) else - nil -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) - - false -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + error when is_nil(error) or error == false -> + render_error(conn, :not_found, "Record not found") end end @@ -483,18 +567,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> try_render("poll.json", %{object: object, for: user}) else nil -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + render_error(conn, :not_found, "Record not found") false -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + render_error(conn, :not_found, "Record not found") {:error, message} -> conn - |> put_status(422) + |> put_status(:unprocessable_entity) |> json(%{error: message}) end end @@ -583,10 +663,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Can't delete this post"}) + _e -> render_error(conn, :forbidden, "Can't delete this post") end end @@ -631,11 +708,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - else - {:error, reason} -> - conn - |> put_resp_content_type("application/json") - |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) end end @@ -676,11 +748,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - else - {:error, reason} -> - conn - |> put_resp_content_type("application/json") - |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) end end @@ -711,8 +778,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => reason})) + |> put_status(:forbidden) + |> json(%{"error" => reason}) end end @@ -727,8 +794,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => reason})) + |> put_status(:forbidden) + |> json(%{"error" => reason}) end end @@ -806,9 +873,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> json(rendered) else - conn - |> put_resp_content_type("application/json") - |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"})) + render_error(conn, :unsupported_media_type, "mascots can only be images") end end end @@ -821,10 +886,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), - %Object{data: %{"likes" => likes}} <- Object.normalize(object) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do q = from(u in User, where: u.ap_id in ^likes) - users = Repo.all(q) + + users = + Repo.all(q) + |> Enum.filter(&(not User.blocks?(user, &1))) conn |> put_view(AccountView) @@ -835,10 +903,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), - %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do q = from(u in User, where: u.ap_id in ^announces) - users = Repo.all(q) + + users = + Repo.all(q) + |> Enum.filter(&(not User.blocks?(user, &1))) conn |> put_view(AccountView) @@ -874,6 +945,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> 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) @@ -937,8 +1009,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -951,8 +1023,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -969,8 +1041,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -987,8 +1059,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1008,17 +1080,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do + def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do + notifications = + if Map.has_key?(params, "notifications"), + do: params["notifications"] in [true, "True", "true", "1"], + else: true + with %User{} = muted <- User.get_cached_by_id(id), - {:ok, muter} <- User.mute(muter, muted) do + {:ok, muter} <- User.mute(muter, muted, notifications) do conn |> put_view(AccountView) |> render("relationship.json", %{user: muter, target: muted}) else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1031,8 +1108,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1053,8 +1130,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1068,8 +1145,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1103,8 +1180,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1117,8 +1194,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1150,10 +1227,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do recipients = if for_user do - ["https://www.w3.org/ns/activitystreams#Public"] ++ - [for_user.ap_id | for_user.following] + [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] else - ["https://www.w3.org/ns/activitystreams#Public"] + [Pleroma.Constants.as_public()] end activities = @@ -1166,13 +1242,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> render("index.json", %{activities: activities, for: for_user, as: :activity}) else - nil -> - {:error, :not_found} - - true -> - conn - |> put_status(403) - |> json(%{error: "Can't get favorites"}) + nil -> {:error, :not_found} + true -> render_error(conn, :forbidden, "Can't get favorites") end end @@ -1204,10 +1275,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do res = ListView.render("list.json", list: list) json(conn, res) else - _e -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + _e -> render_error(conn, :not_found, "Record not found") end end @@ -1223,7 +1291,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1274,7 +1342,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, res) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1284,6 +1352,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do params |> 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 @@ -1298,10 +1367,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> render("index.json", %{activities: activities, for: user, as: :activity}) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Error."}) + _e -> render_error(conn, :forbidden, "Error.") end end @@ -1420,8 +1486,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else e -> conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) + |> put_status(:internal_server_error) + |> json(%{error: inspect(e)}) end end @@ -1589,20 +1655,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> Enum.map_join(", ", fn {_k, v} -> v end) conn - |> put_status(422) + |> put_status(:unprocessable_entity) |> json(%{error: error_message}) end def errors(conn, {:error, :not_found}) do + render_error(conn, :not_found, "Record not found") + end + + def errors(conn, {:error, error_message}) do conn - |> put_status(404) - |> json(%{error: "Record not found"}) + |> put_status(:bad_request) + |> json(%{error: error_message}) end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end def suggestions(%{assigns: %{user: user}} = conn, _) do @@ -1623,45 +1693,35 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> String.replace("{{user}}", user) with {:ok, %{status: 200, body: body}} <- - HTTP.get( - url, - [], - adapter: [ - recv_timeout: timeout, - pool: :default - ] - ), + HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]), {:ok, data} <- Jason.decode(body) do data = data |> Enum.slice(0, limit) |> Enum.map(fn x -> - Map.put( - x, - "id", - case User.get_or_fetch(x["acct"]) do - {:ok, %User{id: id}} -> id - _ -> 0 - end - ) - end) - |> Enum.map(fn x -> - Map.put(x, "avatar", MediaProxy.url(x["avatar"])) - end) - |> Enum.map(fn x -> - Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"])) + x + |> Map.put("id", fetch_suggestion_id(x)) + |> Map.put("avatar", MediaProxy.url(x["avatar"])) + |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"])) end) - conn - |> json(data) + json(conn, data) else - e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}") + e -> + Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}") end else json(conn, []) end end + defp fetch_suggestion_id(attrs) do + case User.get_or_fetch(attrs["acct"]) do + {:ok, %User{id: id}} -> id + _ -> 0 + end + end + def status_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 @@ -1722,21 +1782,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, errors} -> conn - |> put_status(400) - |> json(Jason.encode!(errors)) + |> put_status(:bad_request) + |> json(errors) end end def account_register(%{assigns: %{app: _app}} = conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Missing parameters"}) + render_error(conn, :bad_request, "Missing parameters") end def account_register(conn, _) do - conn - |> put_status(403) - |> json(%{error: "Invalid credentials"}) + render_error(conn, :forbidden, "Invalid credentials") end def conversations(%{assigns: %{user: user}} = conn, params) do @@ -1764,23 +1820,42 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def try_render(conn, target, params) - when is_binary(target) do - res = render(conn, target, params) + def password_reset(conn, params) do + nickname_or_email = params["email"] || params["nickname"] - if res == nil do + with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do conn - |> put_status(501) - |> json(%{error: "Can't display this activity"}) + |> put_status(:no_content) + |> json("") else - res + {:error, "unknown user"} -> + send_resp(conn, :not_found, "") + + {:error, _} -> + send_resp(conn, :bad_request, "") + end + end + + def account_confirmation_resend(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), + {:ok, _} <- User.try_send_confirmation_email(user) do + conn + |> json_response(:no_content, "") + end + end + + def try_render(conn, target, params) + when is_binary(target) do + case render(conn, target, params) do + nil -> render_error(conn, :not_implemented, "Can't display this activity") + res -> res end end def try_render(conn, _, _) do - conn - |> put_status(501) - |> json(%{error: "Can't display this activity"}) + render_error(conn, :not_implemented, "Can't display this activity") end defp present?(nil), do: false