X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Fmastodon_api%2Fmastodon_api_controller.ex;h=174e93468be711608b67b2306efdac8219ef8777;hb=b93498eb5289dc92587b77c316ed9f697bb9e5c8;hp=ed585098a7b520253e1ce8a258e3670d6d261d31;hpb=0bcdaf378ee41c3845151b59d8820b799532c9a8;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 ed585098a..174e93468 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,15 +4,21 @@ 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 alias Pleroma.Config + alias Pleroma.Conversation.Participation alias Pleroma.Filter + alias Pleroma.Formatter + alias Pleroma.HTTP alias Pleroma.Notification alias Pleroma.Object - alias Pleroma.Object.Fetcher alias Pleroma.Pagination + alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.Stats @@ -23,6 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AppView + alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -34,20 +41,51 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.MediaProxy alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.ControllerHelper 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(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) - @httpoison Application.get_env(:pleroma, :httpoison) @local_mastodon_name "Mastodon-Local" action_fallback(:errors) def create_app(conn, params) do - scopes = ControllerHelper.oauth_scopes(params, ["read"]) + scopes = Scopes.fetch_scopes(params, ["read"]) app_attrs = params @@ -86,7 +124,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do user_params = %{} |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end) + |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) |> add_if_present(params, "avatar", :avatar, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :avatar) do @@ -96,14 +134,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end) + emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") + + user_info_emojis = + ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) + |> Enum.dedup() + info_params = - [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role] + [ + :no_rich_text, + :locked, + :hide_followers, + :hide_follows, + :hide_favorites, + :show_role, + :skip_thread_containment + ] |> Enum.reduce(%{}, fn key, acc -> add_if_present(acc, params, to_string(key), key, fn value -> {:ok, ControllerHelper.truthy_param?(value)} end) end) |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> + {:ok, Map.merge(user.info.pleroma_settings_store, value)} + end) |> add_if_present(params, "header", :banner, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -112,6 +167,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do _ -> :error end end) + |> add_if_present(params, "pleroma_background_image", :background, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :background) do + {:ok, object.data} + else + _ -> :error + end + end) + |> Map.put(:emoji, user_info_emojis) info_cng = User.Info.profile_update(user.info, info_params) @@ -122,17 +186,89 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do CommonAPI.update(user) end - json(conn, AccountView.render("account.json", %{user: user, for: user})) + json( + conn, + 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 def verify_credentials(%{assigns: %{user: user}} = conn, _) do - account = AccountView.render("account.json", %{user: user, for: user}) + chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + + account = + AccountView.render("account.json", %{ + user: user, + for: user, + with_pleroma_settings: true, + with_chat_token: chat_token + }) + json(conn, account) end @@ -150,14 +286,11 @@ 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 - @mastodon_api_level "2.5.0" + @mastodon_api_level "2.7.2" def masto_instance(conn, _params) do instance = Config.get(:instance) @@ -176,7 +309,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do languages: ["en"], registrations: Pleroma.Config.get([:instance, :registrations_open]), # Extra (not present in Mastodon): - max_toot_chars: Keyword.get(instance, :limit) + max_toot_chars: Keyword.get(instance, :limit), + poll_limits: Keyword.get(instance, :poll_limits) } json(conn, response) @@ -196,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 @@ -282,11 +418,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do activities = [user.ap_id | user.following] |> ActivityPub.fetch_activities(params) - |> ActivityPub.contain_timeline(user) |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:home_timeline, activities) |> put_view(StatusView) @@ -305,8 +438,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> ActivityPub.fetch_public_activities() |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) |> put_view(StatusView) @@ -314,8 +445,11 @@ 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"]), - reading_user <- Repo.preload(reading_user, :bookmarks) do + with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do + params = + params + |> Map.put("tag", params["tagged"]) + activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn @@ -342,8 +476,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> ActivityPub.fetch_activities_query(params) |> Pagination.fetch_paginated(params) - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:dm_timeline, activities) |> put_view(StatusView) @@ -353,8 +485,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def get_status(%{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 - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user}) @@ -398,6 +528,56 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Object{} = 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) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> render_error(conn, :not_found, "Record not found") + false -> render_error(conn, :not_found, "Record not found") + end + end + + defp get_cached_vote_or_vote(user, object, choices) do + idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + + {_, res} = + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.vote(user, object, choices) do + {:error, _message} = res -> {:ignore, res} + res -> {:commit, res} + end + end) + + res + end + + def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do + with %Object{} = object <- Object.get_by_id(id), + true <- object.data["type"] == "Question", + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + render_error(conn, :not_found, "Record not found") + + false -> + render_error(conn, :not_found, "Record not found") + + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + end + end + def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do conn @@ -447,26 +627,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params) - when length(media_ids) > 0 do - params = - params - |> Map.put("status", ".") - - post_status(conn, params) - end - def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do params = params |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) - idempotency_key = - case get_req_header(conn, "idempotency-key") do - [key] -> key - _ -> Ecto.UUID.generate() - end - scheduled_at = params["scheduled_at"] if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do @@ -479,14 +644,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else params = Map.drop(params, ["scheduled_at"]) - {:ok, activity} = - Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> - CommonAPI.post(user, params) - end) + case CommonAPI.post(user, params) do + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + {:ok, activity} -> + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end end end @@ -494,18 +662,13 @@ 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 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), %Activity{} = announce <- Activity.normalize(announce.data) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: announce, for: user, as: :activity}) @@ -515,8 +678,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def unreblog_status(%{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 - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -546,11 +707,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 @@ -567,8 +723,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -580,8 +734,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do - user = Repo.preload(user, bookmarks: :activity) - conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -595,11 +747,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 @@ -630,8 +777,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 @@ -646,8 +793,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 @@ -704,29 +851,68 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def favourited_by(conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), + def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do + with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), + %{} = attachment_data <- Map.put(object.data, "id", object.id), + %{type: type} = rendered <- + StatusView.render("attachment.json", %{attachment: attachment_data}) do + # Reject if not an image + if type == "image" do + # Sure! + # Save to the user's info + info_changeset = User.Info.mascot_update(user.info, rendered) + + user_changeset = + user + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_embed(:info, info_changeset) + + {:ok, _user} = User.update_and_set_cache(user_changeset) + + conn + |> json(rendered) + else + render_error(conn, :unsupported_media_type, "mascots can only be images") + end + end + end + + def get_mascot(%{assigns: %{user: user}} = conn, _params) do + mascot = User.get_mascot(user) + + conn + |> json(mascot) + end + + def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"likes" => likes}} <- Object.normalize(object) 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) - |> render(AccountView, "accounts.json", %{users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) else _ -> json(conn, []) end end - def reblogged_by(conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), + def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"announcements" => announces}} <- Object.normalize(object) 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) - |> render("accounts.json", %{users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) else _ -> json(conn, []) end @@ -783,7 +969,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> add_link_headers(:followers, followers, user) |> put_view(AccountView) - |> render("accounts.json", %{users: followers, as: :user}) + |> render("accounts.json", %{for: for_user, users: followers, as: :user}) end end @@ -800,7 +986,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> add_link_headers(:following, followers, user) |> put_view(AccountView) - |> render("accounts.json", %{users: followers, as: :user}) + |> render("accounts.json", %{for: for_user, users: followers, as: :user}) end end @@ -808,7 +994,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do with {:ok, follow_requests} <- User.get_follow_requests(followed) do conn |> put_view(AccountView) - |> render("accounts.json", %{users: follow_requests, as: :user}) + |> render("accounts.json", %{for: followed, users: follow_requests, as: :user}) end end @@ -821,8 +1007,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 @@ -835,8 +1021,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 @@ -853,8 +1039,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 @@ -871,8 +1057,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 @@ -892,17 +1078,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 @@ -915,8 +1106,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 @@ -937,8 +1128,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 @@ -952,8 +1143,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 @@ -987,8 +1178,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 @@ -1001,96 +1192,11 @@ 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 - def status_search(user, query) do - fetched = - if Regex.match?(~r/https?:/, query) do - with {:ok, object} <- Fetcher.fetch_object_from_id(query), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), - true <- Visibility.visible_for_user?(activity, user) do - [activity] - else - _e -> [] - end - end || [] - - q = - from( - [a, o] in Activity.with_preloaded_object(Activity), - where: fragment("?->>'type' = 'Create'", a.data), - where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, - where: - fragment( - "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", - o.data, - ^query - ), - limit: 20, - order_by: [desc: :id] - ) - - Repo.all(q) ++ fetched - end - - def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - statuses = status_search(user, query) - - tags_path = Web.base_url() <> "/tag/" - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - statuses = status_search(user, query) - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - - json(conn, res) - end - def favourites(%{assigns: %{user: user}} = conn, params) do params = params @@ -1102,8 +1208,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do ActivityPub.fetch_activities([], params) |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> add_link_headers(:favourites, activities) |> put_view(StatusView) @@ -1121,10 +1225,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 = @@ -1137,19 +1240,13 @@ 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 def bookmarks(%{assigns: %{user: user}} = conn, params) do user = User.get_cached_by_id(user.id) - user = Repo.preload(user, bookmarks: :activity) bookmarks = Bookmark.for_user_query(user.id) @@ -1157,7 +1254,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do activities = bookmarks - |> Enum.map(fn b -> b.activity end) + |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) conn |> add_link_headers(:bookmarks, bookmarks) @@ -1176,10 +1273,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 @@ -1195,7 +1289,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1222,7 +1316,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do accounts |> Enum.each(fn account_id -> with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do + %User{} = followed <- User.get_cached_by_id(account_id) do Pleroma.List.unfollow(list, followed) end end) @@ -1235,7 +1329,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:ok, users} = Pleroma.List.get_following(list) do conn |> put_view(AccountView) - |> render("accounts.json", %{users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) end end @@ -1246,7 +1340,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, res) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1266,16 +1360,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> ActivityPub.fetch_activities_bounded(following, params) |> Enum.reverse() - user = Repo.preload(user, bookmarks: :activity) - conn |> 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 @@ -1290,13 +1379,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) - flavour = get_user_flavour(user) - initial_state = %{ meta: %{ - streaming_api_base_url: - String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), + streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(), access_token: token, locale: "en", domain: Pleroma.Web.Endpoint.host(), @@ -1309,8 +1395,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do display_sensitive_media: false, reduce_motion: false, max_toot_chars: limit, - mascot: "/images/pleroma-fox-tan-smol.png" + mascot: User.get_mascot(user)["url"] }, + poll_limits: Config.get([:instance, :poll_limits]), rights: %{ delete_others_notice: present?(user.info.is_moderator), admin: present?(user.info.is_admin) @@ -1378,7 +1465,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_layout(false) |> put_view(MastodonView) - |> render("index.html", %{initial_state: initial_state, flavour: flavour}) + |> render("index.html", %{initial_state: initial_state}) else conn |> put_session(:return_to, conn.request_path) @@ -1396,48 +1483,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else e -> conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) - end - end - - @supported_flavours ["glitch", "vanilla"] - - def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params) - when flavour in @supported_flavours do - flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour) - - with changeset <- Ecto.Changeset.change(user), - changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng), - {:ok, user} <- User.update_and_set_cache(changeset), - flavour <- user.info.flavour do - json(conn, flavour) - 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 - def set_flavour(conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Unsupported flavour"}) - end - - def get_flavour(%{assigns: %{user: user}} = conn, _params) do - json(conn, get_user_flavour(user)) - end - - defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do - flavour - end - - defp get_user_flavour(_) do - "glitch" - end - def login(%{assigns: %{user: %User{}}} = conn, _params) do redirect(conn, to: local_mastodon_root_path(conn)) end @@ -1548,7 +1598,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do user_id: user.id, phrase: phrase, context: context, - hide: Map.get(params, "irreversible", nil), + hide: Map.get(params, "irreversible", false), whole_word: Map.get(params, "boolean", true) # expires_at } @@ -1602,20 +1652,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 @@ -1636,7 +1690,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> String.replace("{{user}}", user) with {:ok, %{status: 200, body: body}} <- - @httpoison.get( + HTTP.get( url, [], adapter: [ @@ -1705,23 +1759,110 @@ 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 account_register( + %{assigns: %{app: app}} = conn, + %{"username" => nickname, "email" => _, "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"]) + + with {: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) + }) + else + {:error, errors} -> + conn + |> put_status(:bad_request) + |> json(errors) + end + end - if res == nil do + def account_register(%{assigns: %{app: _app}} = conn, _params) do + render_error(conn, :bad_request, "Missing parameters") + end + + def account_register(conn, _) do + render_error(conn, :forbidden, "Invalid credentials") + end + + def conversations(%{assigns: %{user: user}} = conn, params) do + participations = Participation.for_user_with_last_activity_id(user, params) + + conversations = + Enum.map(participations, fn participation -> + ConversationView.render("participation.json", %{participation: participation, user: user}) + end) + + conn + |> add_link_headers(:conversations, participations) + |> json(conversations) + end + + def conversation_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 + participation_view = + ConversationView.render("participation.json", %{participation: participation, user: user}) + + conn + |> json(participation_view) + end + end + + def password_reset(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + 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