diff options
| author | lain <lain@soykaf.club> | 2019-10-02 13:27:55 +0200 | 
|---|---|---|
| committer | lain <lain@soykaf.club> | 2019-10-02 13:27:55 +0200 | 
| commit | 557223b2b5b60956d3e1a19e9fdae9e9798c4fe2 (patch) | |
| tree | 71d711f8932a2bd952f9f54ccef1322c55aed1d7 /lib | |
| parent | 19bc0b8c79765dc485e081651a4e4c589d18b970 (diff) | |
| parent | 433f2c0f6854d2838819e08f0fb0a9e8cf058021 (diff) | |
| download | pleroma-557223b2b5b60956d3e1a19e9fdae9e9798c4fe2.tar.gz pleroma-557223b2b5b60956d3e1a19e9fdae9e9798c4fe2.zip | |
Merge remote-tracking branch 'origin/develop' into reactions
Diffstat (limited to 'lib')
35 files changed, 1141 insertions, 967 deletions
| diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 7aec2c545..9e35b02c0 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -102,7 +102,8 @@ defmodule Pleroma.Application do        build_cachex("scrubber", limit: 2500),        build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),        build_cachex("web_resp", limit: 2500), -      build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10) +      build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), +      build_cachex("failed_proxy_url", limit: 2500)      ]    end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index c5db1cb62..08a94c62c 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -84,22 +84,11 @@ defmodule Pleroma.List do    end    # Get lists to which the account belongs. -  def get_lists_account_belongs(%User{} = owner, account_id) do -    user = User.get_cached_by_id(account_id) - -    query = -      from( -        l in Pleroma.List, -        where: -          l.user_id == ^owner.id and -            fragment( -              "? = ANY(?)", -              ^user.follower_address, -              l.following -            ) -      ) - -    Repo.all(query) +  def get_lists_account_belongs(%User{} = owner, user) do +    Pleroma.List +    |> where([l], l.user_id == ^owner.id) +    |> where([l], fragment("? = ANY(?)", ^user.follower_address, l.following)) +    |> Repo.all()    end    def rename(%Pleroma.List{} = list, title) do diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 03efad30a..78144cae3 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -15,6 +15,7 @@ defmodule Pleroma.ReverseProxy do    @valid_resp_codes [200, 206, 304]    @max_read_duration :timer.seconds(30)    @max_body_length :infinity +  @failed_request_ttl :timer.seconds(60)    @methods ~w(GET HEAD)    @moduledoc """ @@ -48,6 +49,8 @@ defmodule Pleroma.ReverseProxy do    * `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to    read from the remote upstream. +  * `failed_request_ttl` (default `#{inspect(@failed_request_ttl)}` ms): the time the failed request is cached and cannot be retried. +    * `inline_content_types`:      * `true` will not alter `content-disposition` (up to the upstream),      * `false` will add `content-disposition: attachment` to any request, @@ -83,6 +86,7 @@ defmodule Pleroma.ReverseProxy do            {:keep_user_agent, boolean}            | {:max_read_duration, :timer.time() | :infinity}            | {:max_body_length, non_neg_integer() | :infinity} +          | {:failed_request_ttl, :timer.time() | :infinity}            | {:http, []}            | {:req_headers, [{String.t(), String.t()}]}            | {:resp_headers, [{String.t(), String.t()}]} @@ -108,7 +112,8 @@ defmodule Pleroma.ReverseProxy do          opts        end -    with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts), +    with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url), +         {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),           :ok <-             header_length_constraint(               headers, @@ -116,12 +121,18 @@ defmodule Pleroma.ReverseProxy do             ) do        response(conn, client, url, code, headers, opts)      else +      {:ok, true} -> +        conn +        |> error_or_redirect(url, 500, "Request failed", opts) +        |> halt() +        {:ok, code, headers} ->          head_response(conn, url, code, headers, opts)          |> halt()        {:error, {:invalid_http_response, code}} ->          Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}") +        track_failed_url(url, code, opts)          conn          |> error_or_redirect( @@ -134,6 +145,7 @@ defmodule Pleroma.ReverseProxy do        {:error, error} ->          Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}") +        track_failed_url(url, error, opts)          conn          |> error_or_redirect(url, 500, "Request failed", opts) @@ -388,4 +400,17 @@ defmodule Pleroma.ReverseProxy do    end    defp client, do: Pleroma.ReverseProxy.Client + +  defp track_failed_url(url, code, opts) do +    code = to_string(code) + +    ttl = +      if code in ["403", "404"] or String.starts_with?(code, "5") do +        Keyword.get(opts, :failed_request_ttl, @failed_request_ttl) +      else +        nil +      end + +    Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl) +  end  end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1f201d587..a9e53141d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -365,7 +365,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          local \\ true,          public \\ true        ) do -    with true <- is_public?(object), +    with true <- is_announceable?(object, user, public),           announce_data <- make_announce_data(user, object, activity_id, public),           {:ok, activity} <- insert(announce_data, local),           {:ok, object} <- add_announce_to_object(activity, object), diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3da08398b..cb868c336 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -774,6 +774,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end +  # For Undos that don't have the complete object attached, try to find it in our database. +  def handle_incoming( +        %{ +          "type" => "Undo", +          "object" => object +        } = activity, +        options +      ) +      when is_binary(object) do +    with %Activity{data: data} <- Activity.get_by_ap_id(object) do +      activity +      |> Map.put("object", data) +      |> handle_incoming(options) +    else +      _e -> :error +    end +  end +    def handle_incoming(_, _), do: :error    @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil @@ -833,6 +851,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      {:ok, data}    end +  def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do +    object = +      object_id +      |> Object.normalize() + +    data = +      if Visibility.is_private?(object) && object.data["actor"] == ap_id do +        data |> Map.put("object", object |> Map.get(:data) |> prepare_object) +      else +        data |> maybe_fix_object_url +      end + +    data = +      data +      |> strip_internal_fields +      |> Map.merge(Utils.make_json_ld_header()) +      |> Map.delete("bcc") + +    {:ok, data} +  end +    # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,    # because of course it does.    def prepare_outgoing(%{"type" => "Accept"} = data) do diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 8e98accb1..4c146fd86 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -525,7 +525,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    @spec add_announce_to_object(Activity.t(), Object.t()) ::            {:ok, Object.t()} | {:error, Ecto.Changeset.t()}    def add_announce_to_object( -        %Activity{data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}}, +        %Activity{data: %{"actor" => actor}},          object        ) do      announcements = take_announcements(object) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index c94c5a225..6bc55c85b 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do    def render("endpoints.json", %{user: %User{local: true} = _user}) do      %{        "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize), -      "oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app), +      "oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),        "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),        "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),        "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index dfb166b65..270d0fa02 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -27,6 +27,11 @@ defmodule Pleroma.Web.ActivityPub.Visibility do      end    end +  def is_announceable?(activity, user, public \\ true) do +    is_public?(activity) || +      (!public && is_private?(activity) && activity.data["actor"] == user.ap_id) +  end +    def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true    def is_direct?(%Object{data: %{"directMessage" => true}}), do: true diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 8c06364a3..101a74c63 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -43,7 +43,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do    end    defp merge_account_views(%User{} = user) do -    Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user}) +    Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})      |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))    end diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex index b543909f1..08841a3e8 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/chat_channel.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.ChatChannel do      if String.length(text) > 0 do        author = User.get_cached_by_nickname(user_name) -      author = Pleroma.Web.MastodonAPI.AccountView.render("account.json", user: author) +      author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)        message = ChatChannelState.add_message(%{text: text, author: author})        broadcast!(socket, "new_msg", message) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 0706e7ffc..53ada8fab 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -76,11 +76,12 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  def repeat(id_or_ap_id, user) do +  def repeat(id_or_ap_id, user, params \\ %{}) do      with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),           object <- Object.normalize(activity), -         nil <- Utils.get_existing_announce(user.ap_id, object) do -      ActivityPub.announce(user, object) +         nil <- Utils.get_existing_announce(user.ap_id, object), +         public <- public_announce?(object, params) do +      ActivityPub.announce(user, object, nil, true, public)      else        _ -> {:error, dgettext("errors", "Could not repeat")}      end @@ -179,6 +180,14 @@ defmodule Pleroma.Web.CommonAPI do      end    end +  def public_announce?(_, %{"visibility" => visibility}) +      when visibility in ~w{public unlisted private direct}, +      do: visibility in ~w(public unlisted) + +  def public_announce?(object, _) do +    Visibility.is_public?(object) +  end +    def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}    def get_visibility(%{"visibility" => visibility}, in_reply_to, _) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index e90bf842e..9a4e322c9 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -68,4 +68,23 @@ defmodule Pleroma.Web.ControllerHelper do          conn      end    end + +  def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do +    case Pleroma.User.get_cached_by_id(id) do +      %Pleroma.User{} = account -> assign(conn, :account, account) +      nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() +    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 +    render_error(conn, :not_implemented, "Can't display this activity") +  end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex new file mode 100644 index 000000000..df14ad66f --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -0,0 +1,304 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +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] + +  alias Pleroma.Emoji +  alias Pleroma.Plugs.RateLimiter +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.MastodonAPI.ListView +  alias Pleroma.Web.MastodonAPI.MastodonAPI +  alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.OAuth.Token +  alias Pleroma.Web.TwitterAPI.TwitterAPI + +  @relations [:follow, :unfollow] +  @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a + +  plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations) +  plug(RateLimiter, :relations_actions when action in @relations) +  plug(RateLimiter, :app_account_creation when action == :create) +  plug(:assign_account_by_id when action in @needs_account) + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  @doc "POST /api/v1/accounts" +  def create( +        %{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} -> json_response(conn, :bad_request, errors) +    end +  end + +  def create(%{assigns: %{app: _app}} = conn, _) do +    render_error(conn, :bad_request, "Missing parameters") +  end + +  def create(conn, _) do +    render_error(conn, :forbidden, "Invalid credentials") +  end + +  @doc "GET /api/v1/accounts/verify_credentials" +  def verify_credentials(%{assigns: %{user: user}} = conn, _) do +    chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + +    render(conn, "show.json", +      user: user, +      for: user, +      with_pleroma_settings: true, +      with_chat_token: chat_token +    ) +  end + +  @doc "PATCH /api/v1/accounts/update_credentials" +  def update_credentials(%{assigns: %{user: original_user}} = conn, params) do +    user = original_user + +    user_params = +      %{} +      |> add_if_present(params, "display_name", :name) +      |> 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 +          {:ok, object.data} +        end +      end) + +    emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") + +    user_info_emojis = +      user.info +      |> Map.get(:emoji, []) +      |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) +      |> Enum.dedup() + +    info_params = +      [ +        :no_rich_text, +        :locked, +        :hide_followers_count, +        :hide_follows_count, +        :hide_followers, +        :hide_follows, +        :hide_favorites, +        :show_role, +        :skip_thread_containment, +        :discoverable +      ] +      |> Enum.reduce(%{}, fn key, acc -> +        add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) +      end) +      |> add_if_present(params, "default_scope", :default_scope) +      |> add_if_present(params, "fields", :fields, fn fields -> +        fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) + +        {:ok, fields} +      end) +      |> add_if_present(params, "fields", :raw_fields) +      |> 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 +          {:ok, object.data} +        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} +        end +      end) +      |> Map.put(:emoji, user_info_emojis) + +    changeset = +      user +      |> User.update_changeset(user_params) +      |> User.change_info(&User.Info.profile_update(&1, info_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) +    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 + +  @doc "GET /api/v1/accounts/relationships" +  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) +  end + +  # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. +  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 +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user), +         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do +      render(conn, "show.json", user: user, for: for_user) +    else +      _e -> render_error(conn, :not_found, "Can't find user") +    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 +      params = Map.put(params, "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) +    end +  end + +  @doc "GET /api/v1/accounts/:id/followers" +  def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do +    followers = +      cond do +        for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params) +        user.info.hide_followers -> [] +        true -> MastodonAPI.get_followers(user, params) +      end + +    conn +    |> add_link_headers(followers) +    |> render("index.json", for: for_user, users: followers, as: :user) +  end + +  @doc "GET /api/v1/accounts/:id/following" +  def following(%{assigns: %{user: for_user, account: user}} = conn, params) do +    followers = +      cond do +        for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params) +        user.info.hide_follows -> [] +        true -> MastodonAPI.get_friends(user, params) +      end + +    conn +    |> add_link_headers(followers) +    |> render("index.json", for: for_user, users: followers, as: :user) +  end + +  @doc "GET /api/v1/accounts/:id/lists" +  def lists(%{assigns: %{user: user, account: account}} = conn, _params) do +    lists = Pleroma.List.get_lists_account_belongs(user, account) + +    conn +    |> put_view(ListView) +    |> render("index.json", lists: lists) +  end + +  @doc "POST /api/v1/accounts/:id/follow" +  def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do +    {:error, :not_found} +  end + +  def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do +    with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do +      render(conn, "relationship.json", user: follower, target: followed) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end + +  @doc "POST /api/v1/accounts/:id/unfollow" +  def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do +    {:error, :not_found} +  end + +  def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do +    with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do +      render(conn, "relationship.json", user: follower, target: followed) +    end +  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, muter} <- User.mute(muter, muted, notifications?) do +      render(conn, "relationship.json", user: muter, target: muted) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end + +  @doc "POST /api/v1/accounts/:id/unmute" +  def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do +    with {:ok, muter} <- User.unmute(muter, muted) do +      render(conn, "relationship.json", user: muter, target: muted) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end + +  @doc "POST /api/v1/accounts/:id/block" +  def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do +    with {:ok, blocker} <- User.block(blocker, blocked), +         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do +      render(conn, "relationship.json", user: blocker, target: blocked) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end + +  @doc "POST /api/v1/accounts/:id/unblock" +  def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do +    with {:ok, blocker} <- User.unblock(blocker, blocked), +         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do +      render(conn, "relationship.json", user: blocker, target: blocked) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex new file mode 100644 index 000000000..abbe16a88 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AppController do +  use Pleroma.Web, :controller + +  alias Pleroma.Repo +  alias Pleroma.Web.OAuth.App +  alias Pleroma.Web.OAuth.Scopes +  alias Pleroma.Web.OAuth.Token + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  @local_mastodon_name "Mastodon-Local" + +  @doc "POST /api/v1/apps" +  def create(conn, params) do +    scopes = Scopes.fetch_scopes(params, ["read"]) + +    app_attrs = +      params +      |> Map.drop(["scope", "scopes"]) +      |> Map.put("scopes", scopes) + +    with cs <- App.register_changeset(%App{}, app_attrs), +         false <- cs.changes[:client_name] == @local_mastodon_name, +         {:ok, app} <- Repo.insert(cs) do +      render(conn, "show.json", app: app) +    end +  end + +  @doc "GET /api/v1/apps/verify_credentials" +  def verify_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do +    with %Token{app: %App{} = app} <- Repo.preload(token, :app) do +      render(conn, "short.json", app: app) +    end +  end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex new file mode 100644 index 000000000..0dee670af --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AuthController do +  use Pleroma.Web, :controller + +  alias Pleroma.User +  alias Pleroma.Web.OAuth.App +  alias Pleroma.Web.OAuth.Authorization +  alias Pleroma.Web.OAuth.Token +  alias Pleroma.Web.TwitterAPI.TwitterAPI + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  @local_mastodon_name "Mastodon-Local" + +  plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset) + +  @doc "GET /web/login" +  def login(%{assigns: %{user: %User{}}} = conn, _params) do +    redirect(conn, to: local_mastodon_root_path(conn)) +  end + +  @doc "Local Mastodon FE login init action" +  def login(conn, %{"code" => auth_token}) do +    with {:ok, app} <- get_or_make_app(), +         {:ok, auth} <- Authorization.get_by_token(app, auth_token), +         {:ok, token} <- Token.exchange_token(app, auth) do +      conn +      |> put_session(:oauth_token, token.token) +      |> redirect(to: local_mastodon_root_path(conn)) +    end +  end + +  @doc "Local Mastodon FE callback action" +  def login(conn, _) do +    with {:ok, app} <- get_or_make_app() do +      path = +        o_auth_path(conn, :authorize, +          response_type: "code", +          client_id: app.client_id, +          redirect_uri: ".", +          scope: Enum.join(app.scopes, " ") +        ) + +      redirect(conn, to: path) +    end +  end + +  @doc "DELETE /auth/sign_out" +  def logout(conn, _) do +    conn +    |> clear_session +    |> redirect(to: "/") +  end + +  @doc "POST /auth/password" +  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(:no_content) +      |> json("") +    else +      {:error, "unknown user"} -> +        send_resp(conn, :not_found, "") + +      {:error, _} -> +        send_resp(conn, :bad_request, "") +    end +  end + +  defp local_mastodon_root_path(conn) do +    case get_session(conn, :return_to) do +      nil -> +        mastodon_api_path(conn, :index, ["getting-started"]) + +      return_to -> +        delete_session(conn, :return_to) +        return_to +    end +  end + +  @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} +  defp get_or_make_app do +    %{client_name: @local_mastodon_name, redirect_uris: "."} +    |> App.get_or_make(["read", "write", "follow", "push"]) +  end +end 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 267014b97..ce7b625ee 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do    def index(%{assigns: %{user: followed}} = conn, _params) do      follow_requests = User.get_follow_requests(followed) -    render(conn, "accounts.json", for: followed, users: follow_requests, as: :user) +    render(conn, "index.json", for: followed, users: follow_requests, as: :user)    end    @doc "POST /api/v1/follow_requests/:id/authorize" diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex new file mode 100644 index 000000000..a55f60fec --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.InstanceController do +  use Pleroma.Web, :controller + +  @doc "GET /api/v1/instance" +  def show(conn, _params) do +    render(conn, "show.json") +  end + +  @doc "GET /api/v1/instance/peers" +  def peers(conn, _params) do +    json(conn, Pleroma.Stats.get_peers()) +  end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 2873deda8..50f42bee5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -49,7 +49,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do      with {:ok, users} <- Pleroma.List.get_following(list) do        conn        |> put_view(AccountView) -      |> render("accounts.json", for: user, users: users, as: :user) +      |> render("index.json", for: user, users: users, as: :user)      end    end 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 3bdcea0f7..98dd9f375 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -5,297 +5,23 @@  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, -    only: [json_response: 3, add_link_headers: 2, truthy_param?: 1] +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] -  alias Ecto.Changeset -  alias Pleroma.Activity    alias Pleroma.Bookmark    alias Pleroma.Config -  alias Pleroma.Emoji -  alias Pleroma.HTTP -  alias Pleroma.Object    alias Pleroma.Pagination -  alias Pleroma.Plugs.RateLimiter -  alias Pleroma.Repo -  alias Pleroma.Stats    alias Pleroma.User    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub -  alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.MastodonAPI.AccountView -  alias Pleroma.Web.MastodonAPI.AppView -  alias Pleroma.Web.MastodonAPI.ListView -  alias Pleroma.Web.MastodonAPI.MastodonAPI    alias Pleroma.Web.MastodonAPI.MastodonView    alias Pleroma.Web.MastodonAPI.StatusView -  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    require Logger -  require Pleroma.Constants - -  @rate_limited_relations_actions ~w(follow unfollow)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, :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"    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) -  def create_app(conn, params) do -    scopes = Scopes.fetch_scopes(params, ["read"]) - -    app_attrs = -      params -      |> Map.drop(["scope", "scopes"]) -      |> Map.put("scopes", scopes) - -    with cs <- App.register_changeset(%App{}, app_attrs), -         false <- cs.changes[:client_name] == @local_mastodon_name, -         {:ok, app} <- Repo.insert(cs) do -      conn -      |> put_view(AppView) -      |> render("show.json", %{app: app}) -    end -  end - -  defp add_if_present( -         map, -         params, -         params_field, -         map_field, -         value_function \\ fn x -> {:ok, x} end -       ) do -    if Map.has_key?(params, params_field) do -      case value_function.(params[params_field]) do -        {:ok, new_value} -> Map.put(map, map_field, new_value) -        :error -> map -      end -    else -      map -    end -  end - -  def update_credentials(%{assigns: %{user: user}} = conn, params) do -    original_user = user - -    user_params = -      %{} -      |> add_if_present(params, "display_name", :name) -      |> 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 -          {:ok, object.data} -        else -          _ -> :error -        end -      end) - -    emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") - -    user_info_emojis = -      user.info -      |> Map.get(:emoji, []) -      |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) -      |> Enum.dedup() - -    info_params = -      [ -        :no_rich_text, -        :locked, -        :hide_followers_count, -        :hide_follows_count, -        :hide_followers, -        :hide_follows, -        :hide_favorites, -        :show_role, -        :skip_thread_containment, -        :discoverable -      ] -      |> Enum.reduce(%{}, fn key, acc -> -        add_if_present(acc, params, to_string(key), key, fn value -> -          {:ok, truthy_param?(value)} -        end) -      end) -      |> add_if_present(params, "default_scope", :default_scope) -      |> add_if_present(params, "fields", :fields, fn fields -> -        fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) - -        {:ok, fields} -      end) -      |> add_if_present(params, "fields", :raw_fields) -      |> 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 -          {:ok, object.data} -        else -          _ -> :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) - -    changeset = -      user -      |> User.update_changeset(user_params) -      |> User.change_info(&User.Info.profile_update(&1, info_params)) - -    with {:ok, user} <- User.update_and_set_cache(changeset) do -      if original_user != user, do: CommonAPI.update(user) - -      json( -        conn, -        AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) -      ) -    else -      _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 -    new_info = %{"banner" => %{}} - -    with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) 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}, -         {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do -      CommonAPI.update(user) -      %{"url" => [%{"href" => href} | _]} = object.data - -      json(conn, %{url: href}) -    end -  end - -  def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do -    new_info = %{"background" => %{}} - -    with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) 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}, -         {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do -      %{"url" => [%{"href" => href} | _]} = object.data - -      json(conn, %{url: href}) -    end -  end - -  def verify_credentials(%{assigns: %{user: user}} = conn, _) do -    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 - -  def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do -    with %Token{app: %App{} = app} <- Repo.preload(token, :app) do -      conn -      |> put_view(AppView) -      |> render("short.json", %{app: app}) -    end -  end - -  def user(%{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.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do -      account = AccountView.render("account.json", %{user: user, for: for_user}) -      json(conn, account) -    else -      _e -> render_error(conn, :not_found, "Can't find user") -    end -  end - -  @mastodon_api_level "2.7.2" - -  def masto_instance(conn, _params) do -    instance = Config.get(:instance) - -    response = %{ -      uri: Web.base_url(), -      title: Keyword.get(instance, :name), -      description: Keyword.get(instance, :description), -      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", -      email: Keyword.get(instance, :email), -      urls: %{ -        streaming_api: Pleroma.Web.Endpoint.websocket_url() -      }, -      stats: Stats.get_stats(), -      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", -      languages: ["en"], -      registrations: Pleroma.Config.get([:instance, :registrations_open]), -      # Extra (not present in Mastodon): -      max_toot_chars: Keyword.get(instance, :limit), -      poll_limits: Keyword.get(instance, :poll_limits) -    } - -    json(conn, response) -  end - -  def peers(conn, _params) do -    json(conn, Stats.get_peers()) -  end -    defp mastodonized_emoji do      Pleroma.Emoji.get_all()      |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} -> @@ -318,200 +44,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      json(conn, mastodon_emoji)    end -  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do -    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do -      params = -        params -        |> 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 -      }) -    end -  end - -  def get_poll(%{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 -      conn -      |> put_view(StatusView) -      |> try_render("poll.json", %{object: object, for: user}) -    else -      error when is_nil(error) or error == 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 relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    targets = User.get_all_by_ids(List.wrap(id)) - -    conn -    |> put_view(AccountView) -    |> render("relationships.json", %{user: user, targets: targets}) -  end - -  # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. -  def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) - -  def update_media( -        %{assigns: %{user: user}} = conn, -        %{"id" => id, "description" => description} = _ -      ) -      when is_binary(description) do -    with %Object{} = object <- Repo.get(Object, id), -         true <- Object.authorize_mutation(object, user), -         {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do -      attachment_data = Map.put(data, "id", object.id) - -      conn -      |> put_view(StatusView) -      |> render("attachment.json", %{attachment: attachment_data}) -    end -  end - -  def update_media(_conn, _data), do: {:error, :bad_request} - -  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) 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_view(StatusView) -      |> render("attachment.json", %{attachment: attachment_data}) -    end -  end - -  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), -         # Reject if not an image -         %{type: "image"} = rendered <- -           StatusView.render("attachment.json", %{attachment: attachment_data}) do -      # Sure! -      # Save to the user's info -      {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered)) - -      json(conn, rendered) -    else -      %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images") -    end -  end - -  def get_mascot(%{assigns: %{user: user}} = conn, _params) do -    mascot = User.get_mascot(user) - -    json(conn, mascot) -  end - -  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- User.get_cached_by_id(id), -         followers <- MastodonAPI.get_followers(user, params) do -      followers = -        cond do -          for_user && user.id == for_user.id -> followers -          user.info.hide_followers -> [] -          true -> followers -        end - -      conn -      |> add_link_headers(followers) -      |> put_view(AccountView) -      |> render("accounts.json", %{for: for_user, users: followers, as: :user}) -    end -  end - -  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- User.get_cached_by_id(id), -         followers <- MastodonAPI.get_friends(user, params) do -      followers = -        cond do -          for_user && user.id == for_user.id -> followers -          user.info.hide_follows -> [] -          true -> followers -        end - -      conn -      |> add_link_headers(followers) -      |> put_view(AccountView) -      |> render("accounts.json", %{for: for_user, users: followers, as: :user}) -    end -  end - -  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do -    with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, -         {_, true} <- {:followed, follower.id != followed.id}, -         {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: follower, target: followed}) -    else -      {:followed, _} -> -        {:error, :not_found} - -      {:error, message} -> -        conn -        |> put_status(:forbidden) -        |> json(%{error: message}) -    end -  end - -  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do +  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        conn        |> put_view(AccountView) -      |> render("account.json", %{user: followed, for: follower}) +      |> render("show.json", %{user: followed, for: follower})      else        {:followed, _} ->          {:error, :not_found} @@ -523,123 +62,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do -    with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, -         {_, true} <- {:followed, follower.id != followed.id}, -         {:ok, follower} <- CommonAPI.unfollow(follower, followed) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: follower, target: followed}) -    else -      {:followed, _} -> -        {:error, :not_found} - -      error -> -        error -    end -  end - -  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, notifications) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: muter, target: muted}) -    else -      {:error, message} -> -        conn -        |> put_status(:forbidden) -        |> json(%{error: message}) -    end -  end - -  def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- User.get_cached_by_id(id), -         {:ok, muter} <- User.unmute(muter, muted) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: muter, target: muted}) -    else -      {:error, message} -> -        conn -        |> put_status(:forbidden) -        |> json(%{error: message}) -    end -  end -    def mutes(%{assigns: %{user: user}} = conn, _) do      with muted_accounts <- User.muted_users(user) do -      res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user) +      res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)        json(conn, res)      end    end -  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- User.get_cached_by_id(id), -         {:ok, blocker} <- User.block(blocker, blocked), -         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: blocker, target: blocked}) -    else -      {:error, message} -> -        conn -        |> put_status(:forbidden) -        |> json(%{error: message}) -    end -  end - -  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- User.get_cached_by_id(id), -         {:ok, blocker} <- User.unblock(blocker, blocked), -         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: blocker, target: blocked}) -    else -      {:error, message} -> -        conn -        |> put_status(:forbidden) -        |> json(%{error: message}) -    end -  end -    def blocks(%{assigns: %{user: user}} = conn, _) do      with blocked_accounts <- User.blocked_users(user) do -      res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user) +      res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)        json(conn, res)      end    end -  def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %User{} = subscription_target <- User.get_cached_by_id(id), -         {:ok, subscription_target} = User.subscribe(user, subscription_target) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: user, target: subscription_target}) -    else -      nil -> {:error, :not_found} -      e -> e -    end -  end - -  def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %User{} = subscription_target <- User.get_cached_by_id(id), -         {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do -      conn -      |> put_view(AccountView) -      |> render("relationship.json", %{user: user, target: subscription_target}) -    else -      nil -> {:error, :not_found} -      e -> e -    end -  end -    def favourites(%{assigns: %{user: user}} = conn, params) do      params =        params @@ -657,37 +93,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      |> render("index.json", %{activities: activities, for: user, as: :activity})    end -  def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- User.get_by_id(id), -         false <- user.info.hide_favorites do -      params = -        params -        |> Map.put("type", "Create") -        |> Map.put("favorited_by", user.ap_id) -        |> Map.put("blocking_user", for_user) - -      recipients = -        if for_user do -          [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] -        else -          [Pleroma.Constants.as_public()] -        end - -      activities = -        recipients -        |> ActivityPub.fetch_activities(params) -        |> Enum.reverse() - -      conn -      |> add_link_headers(activities) -      |> put_view(StatusView) -      |> render("index.json", %{activities: activities, for: for_user, as: :activity}) -    else -      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) @@ -705,14 +110,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      |> render("index.json", %{activities: activities, for: user, as: :activity})    end -  def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do -    lists = Pleroma.List.get_lists_account_belongs(user, account_id) - -    conn -    |> put_view(ListView) -    |> render("index.json", %{lists: lists}) -  end -    def index(%{assigns: %{user: user}} = conn, _params) do      token = get_session(conn, :oauth_token) @@ -721,8 +118,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        limit = Config.get([:instance, :limit]) -      accounts = -        Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) +      accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))        initial_state =          %{ @@ -829,61 +225,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def login(%{assigns: %{user: %User{}}} = conn, _params) do -    redirect(conn, to: local_mastodon_root_path(conn)) -  end - -  @doc "Local Mastodon FE login init action" -  def login(conn, %{"code" => auth_token}) do -    with {:ok, app} <- get_or_make_app(), -         {:ok, auth} <- Authorization.get_by_token(app, auth_token), -         {:ok, token} <- Token.exchange_token(app, auth) do -      conn -      |> put_session(:oauth_token, token.token) -      |> redirect(to: local_mastodon_root_path(conn)) -    end -  end - -  @doc "Local Mastodon FE callback action" -  def login(conn, _) do -    with {:ok, app} <- get_or_make_app() do -      path = -        o_auth_path(conn, :authorize, -          response_type: "code", -          client_id: app.client_id, -          redirect_uri: ".", -          scope: Enum.join(app.scopes, " ") -        ) - -      redirect(conn, to: path) -    end -  end - -  defp local_mastodon_root_path(conn) do -    case get_session(conn, :return_to) do -      nil -> -        mastodon_api_path(conn, :index, ["getting-started"]) - -      return_to -> -        delete_session(conn, :return_to) -        return_to -    end -  end - -  @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} -  defp get_or_make_app do -    App.get_or_make( -      %{client_name: @local_mastodon_name, redirect_uris: "."}, -      ["read", "write", "follow", "push"] -    ) -  end - -  def logout(conn, _) do -    conn -    |> clear_session -    |> redirect(to: "/") -  end -    # Stubs for unimplemented mastodon api    #    def empty_array(conn, _) do @@ -896,134 +237,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      json(conn, %{})    end -  def suggestions(%{assigns: %{user: user}} = conn, _) do -    suggestions = Config.get(:suggestions) - -    if Keyword.get(suggestions, :enabled, false) do -      api = Keyword.get(suggestions, :third_party_engine, "") -      timeout = Keyword.get(suggestions, :timeout, 5000) -      limit = Keyword.get(suggestions, :limit, 23) - -      host = Config.get([Pleroma.Web.Endpoint, :url, :host]) - -      user = user.nickname - -      url = -        api -        |> String.replace("{{host}}", host) -        |> String.replace("{{user}}", user) - -      with {:ok, %{status: 200, body: body}} <- -             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 -> -            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) - -        json(conn, data) -      else -        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 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 - -  def account_register(%{assigns: %{app: _app}} = conn, _) do -    render_error(conn, :bad_request, "Missing parameters") -  end - -  def account_register(conn, _) do -    render_error(conn, :forbidden, "Invalid credentials") -  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(:no_content) -      |> json("") -    else -      {: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 -    render_error(conn, :not_implemented, "Can't display this activity") -  end -    defp present?(nil), do: false    defp present?(false), do: false    defp present?(_), do: true diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex new file mode 100644 index 000000000..57a5b60fb --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MediaController do +  use Pleroma.Web, :controller + +  alias Pleroma.Object +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) +  plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) + +  @doc "POST /api/v1/media" +  def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) 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) + +      render(conn, "attachment.json", %{attachment: attachment_data}) +    end +  end + +  @doc "PUT /api/v1/media/:id" +  def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description}) +      when is_binary(description) do +    with %Object{} = object <- Object.get_by_id(id), +         true <- Object.authorize_mutation(object, user), +         {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do +      attachment_data = Map.put(data, "id", object.id) + +      render(conn, "attachment.json", %{attachment: attachment_data}) +    end +  end + +  def update(_conn, _data), do: {:error, :bad_request} +end diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex new file mode 100644 index 000000000..fbf7f8673 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PollController do +  use Pleroma.Web, :controller + +  import Pleroma.Web.ControllerHelper, only: [try_render: 3, json_response: 3] + +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.Web.ActivityPub.Visibility +  alias Pleroma.Web.CommonAPI + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  @doc "GET /api/v1/polls/:id" +  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 +      try_render(conn, "show.json", %{object: object, for: user}) +    else +      error when is_nil(error) or error == false -> +        render_error(conn, :not_found, "Record not found") +    end +  end + +  @doc "POST /api/v1/polls/:id/votes" +  def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) 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), +         {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do +      try_render(conn, "show.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} -> json_response(conn, :unprocessable_entity, %{error: message}) +    end +  end + +  defp get_cached_vote_or_vote(user, object, choices) do +    idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + +    Cachex.fetch!(:idempotency_cache, idempotency_key, fn -> +      case CommonAPI.vote(user, object, choices) do +        {:error, _message} = res -> {:ignore, res} +        res -> {:commit, res} +      end +    end) +  end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index c91713773..3fc89d645 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do      conn      |> put_view(AccountView) -    |> render("accounts.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) @@ -72,7 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp resource_search(_, "accounts", query, options) do      accounts = with_fallback(fn -> User.search(query, options) end) -    AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user) +    AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)    end    defp resource_search(_, "statuses", query, options) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index f4de9285b..79cced163 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.MastodonAPI.StatusController do    use Pleroma.Web, :controller -  import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3] +  import Pleroma.Web.ControllerHelper, only: [try_render: 3]    require Ecto.Query @@ -125,8 +125,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    end    @doc "POST /api/v1/statuses/:id/reblog" -  def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do -    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), +  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),           %Activity{} = announce <- Activity.normalize(announce.data) do        try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})      end @@ -231,7 +231,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do        conn        |> put_view(AccountView) -      |> render("accounts.json", for: user, users: users, as: :user) +      |> render("index.json", for: user, users: users, as: :user)      else        {:visible, false} -> {:error, :not_found}        _ -> json(conn, []) @@ -242,7 +242,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusController 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}} <- Object.normalize(activity) do +         %Object{data: %{"announcements" => announces, "id" => ap_id}} <- +           Object.normalize(activity) do +      announces = +        "Announce" +        |> Activity.Queries.by_type() +        |> Ecto.Query.where([a], a.actor in ^announces) +        # this is to use the index +        |> Activity.Queries.by_object_id(ap_id) +        |> Repo.all() +        |> Enum.filter(&Visibility.visible_for_user?(&1, user)) +        |> Enum.map(& &1.actor) +        |> Enum.uniq() +        users =          User          |> Ecto.Query.where([u], u.ap_id in ^announces) @@ -251,7 +263,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do        conn        |> put_view(AccountView) -      |> render("accounts.json", for: user, users: users, as: :user) +      |> render("index.json", for: user, users: users, as: :user)      else        {:visible, false} -> {:error, :not_found}        _ -> json(conn, []) diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex new file mode 100644 index 000000000..9076bb849 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SuggestionController do +  use Pleroma.Web, :controller + +  require Logger + +  alias Pleroma.Config +  alias Pleroma.User +  alias Pleroma.Web.MediaProxy + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  @doc "GET /api/v1/suggestions" +  def index(%{assigns: %{user: user}} = conn, _) do +    if Config.get([:suggestions, :enabled], false) do +      with {:ok, data} <- fetch_suggestions(user) do +        limit = Config.get([:suggestions, :limit], 23) + +        data = +          data +          |> Enum.slice(0, limit) +          |> Enum.map(fn x -> +            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) + +        json(conn, data) +      end +    else +      json(conn, []) +    end +  end + +  defp fetch_suggestions(user) do +    api = Config.get([:suggestions, :third_party_engine], "") +    timeout = Config.get([:suggestions, :timeout], 5000) +    host = Config.get([Pleroma.Web.Endpoint, :url, :host]) + +    url = +      api +      |> String.replace("{{host}}", host) +      |> String.replace("{{user}}", user.nickname) + +    with {:ok, %{status: 200, body: body}} <- +           Pleroma.HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]) do +      Jason.decode(body) +    else +      e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}") +    end +  end + +  defp fetch_suggestion_id(attrs) do +    case User.get_or_fetch(attrs["acct"]) do +      {:ok, %User{id: id}} -> id +      _ -> 0 +    end +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 8cf9e9d5c..99169ef95 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -11,15 +11,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MediaProxy -  def render("accounts.json", %{users: users} = opts) do +  def render("index.json", %{users: users} = opts) do      users -    |> render_many(AccountView, "account.json", opts) +    |> render_many(AccountView, "show.json", opts)      |> Enum.filter(&Enum.any?/1)    end -  def render("account.json", %{user: user} = opts) do +  def render("show.json", %{user: user} = opts) do      if User.visible_for?(user, opts[:for]), -      do: do_render("account.json", opts), +      do: do_render("show.json", opts),        else: %{}    end @@ -66,7 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      render_many(targets, AccountView, "relationship.json", user: user, as: :target)    end -  defp do_render("account.json", %{user: user} = opts) do +  defp do_render("show.json", %{user: user} = opts) do      display_name = HTML.strip_tags(user.name || user.nickname)      image = User.avatar_url(user) |> MediaProxy.url() diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 2c5767dd8..e9d2735b3 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -32,7 +32,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do      %{        id: participation.id |> to_string(), -      accounts: render(AccountView, "accounts.json", users: users, as: :user), +      accounts: render(AccountView, "index.json", users: users, as: :user),        unread: !participation.read,        last_status: render(StatusView, "show.json", activity: activity, for: user)      } diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex new file mode 100644 index 000000000..c4866e510 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.InstanceView do +  use Pleroma.Web, :view + +  @mastodon_api_level "2.7.2" + +  def render("show.json", _) do +    instance = Pleroma.Config.get(:instance) + +    %{ +      uri: Pleroma.Web.base_url(), +      title: Keyword.get(instance, :name), +      description: Keyword.get(instance, :description), +      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", +      email: Keyword.get(instance, :email), +      urls: %{ +        streaming_api: Pleroma.Web.Endpoint.websocket_url() +      }, +      stats: Pleroma.Stats.get_stats(), +      thumbnail: Pleroma.Web.base_url() <> "/instance/thumbnail.jpeg", +      languages: ["en"], +      registrations: Keyword.get(instance, :registrations_open), +      # 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) +    } +  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 05110a192..60b58dc90 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do        id: to_string(notification.id),        type: mastodon_type,        created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), -      account: AccountView.render("account.json", %{user: actor, for: user}), +      account: AccountView.render("show.json", %{user: actor, for: user}),        pleroma: %{          is_seen: notification.seen        } diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex new file mode 100644 index 000000000..753039da3 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PollView do +  use Pleroma.Web, :view + +  alias Pleroma.HTML +  alias Pleroma.Web.CommonAPI.Utils + +  def render("show.json", %{object: object, multiple: multiple, options: options} = params) do +    {end_time, expired} = end_time_and_expired(object) +    {options, votes_count} = options_and_votes_count(options) + +    %{ +      # Mastodon uses separate ids for polls, but an object can't have +      # more than one poll embedded so object id is fine +      id: to_string(object.id), +      expires_at: end_time, +      expired: expired, +      multiple: multiple, +      votes_count: votes_count, +      options: options, +      voted: voted?(params), +      emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]) +    } +  end + +  def render("show.json", %{object: object} = params) do +    case object.data do +      %{"anyOf" => options} when is_list(options) -> +        render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options})) + +      %{"oneOf" => options} when is_list(options) -> +        render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options})) + +      _ -> +        nil +    end +  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 + +        {Utils.to_masto_date(end_time), expired} + +      _ -> +        {nil, false} +    end +  end + +  defp options_and_votes_count(options) do +    Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> +      current_count = option["replies"]["totalItems"] || 0 + +      {%{ +         title: HTML.strip_tags(name), +         votes_count: current_count +       }, current_count + count} +    end) +  end + +  defp voted?(%{object: object} = opts) do +    if opts[:for] do +      existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) +      existing_votes != [] or opts[:for].ap_id == object.data["actor"] +    else +      false +    end +  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 d398f7853..9b8dd3086 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.CommonAPI.Utils    alias Pleroma.Web.MastodonAPI.AccountView +  alias Pleroma.Web.MastodonAPI.PollView    alias Pleroma.Web.MastodonAPI.StatusView    alias Pleroma.Web.MediaProxy @@ -108,7 +109,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        id: to_string(activity.id),        uri: activity_object.data["id"],        url: activity_object.data["id"], -      account: AccountView.render("account.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, @@ -124,7 +125,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        pinned: pinned?(activity, user),        sensitive: false,        spoiler_text: "", -      visibility: "public", +      visibility: get_visibility(activity),        media_attachments: reblogged[:media_attachments] || [],        mentions: mentions,        tags: reblogged[:tags] || [], @@ -258,7 +259,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        id: to_string(activity.id),        uri: object.data["id"],        url: url, -      account: AccountView.render("account.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, @@ -277,7 +278,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        spoiler_text: summary_html,        visibility: get_visibility(object),        media_attachments: attachments, -      poll: render("poll.json", %{object: object, for: opts[:for]}), +      poll: render(PollView, "show.json", object: object, for: opts[:for]),        mentions: mentions,        tags: build_tags(tags),        application: %{ @@ -376,7 +377,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      %{        id: activity.id, -      account: AccountView.render("account.json", %{user: user, for: opts[:for]}), +      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(), @@ -389,75 +390,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      safe_render_many(opts.activities, StatusView, "listen.json", opts)    end -  def render("poll.json", %{object: object} = opts) do -    {multiple, options} = -      case object.data do -        %{"anyOf" => options} when is_list(options) -> {true, options} -        %{"oneOf" => options} when is_list(options) -> {false, options} -        _ -> {nil, nil} -      end - -    if options do -      {end_time, expired} = -        case object.data["closed"] || object.data["endTime"] do -          end_time when is_binary(end_time) -> -            end_time = -              (object.data["closed"] || object.data["endTime"]) -              |> NaiveDateTime.from_iso8601!() - -            expired = -              end_time -              |> NaiveDateTime.compare(NaiveDateTime.utc_now()) -              |> case do -                :lt -> true -                _ -> false -              end - -            end_time = Utils.to_masto_date(end_time) - -            {end_time, expired} - -          _ -> -            {nil, false} -        end - -      voted = -        if opts[:for] do -          existing_votes = -            Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) - -          existing_votes != [] or opts[:for].ap_id == object.data["actor"] -        else -          false -        end - -      {options, votes_count} = -        Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> -          current_count = option["replies"]["totalItems"] || 0 - -          {%{ -             title: HTML.strip_tags(name), -             votes_count: current_count -           }, current_count + count} -        end) - -      %{ -        # Mastodon uses separate ids for polls, but an object can't have -        # more than one poll embedded so object id is fine -        id: to_string(object.id), -        expires_at: end_time, -        expired: expired, -        multiple: multiple, -        votes_count: votes_count, -        options: options, -        voted: voted, -        emojis: build_emojis(object.data["emoji"]) -      } -    else -      nil -    end -  end -    def render("context.json", %{activity: activity, activities: activities, user: user}) do      %{ancestors: ancestors, descendants: descendants} =        activities diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index a57670e02..e418dc70d 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -212,13 +212,31 @@ defmodule Pleroma.Web.OAuth.OAuthController do        {:auth_active, false} ->          # Per https://github.com/tootsuite/mastodon/blob/          #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 -        render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address") +        render_error( +          conn, +          :forbidden, +          "Your login is missing a confirmed e-mail address", +          %{}, +          "missing_confirmed_email" +        )        {:user_active, false} -> -        render_error(conn, :forbidden, "Your account is currently disabled") +        render_error( +          conn, +          :forbidden, +          "Your account is currently disabled", +          %{}, +          "account_is_disabled" +        )        {:password_reset_pending, true} -> -        render_error(conn, :forbidden, "Password reset is required") +        render_error( +          conn, +          :forbidden, +          "Password reset is required", +          %{}, +          "password_reset_required" +        )        _error ->          render_invalid_credentials_error(conn) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex new file mode 100644 index 000000000..63c44086c --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -0,0 +1,143 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.AccountController do +  use Pleroma.Web, :controller + +  import Pleroma.Web.ControllerHelper, +    only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] + +  alias Ecto.Changeset +  alias Pleroma.Plugs.RateLimiter +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.MastodonAPI.StatusView + +  require Pleroma.Constants + +  plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend) +  plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) +  plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + +  @doc "POST /api/v1/pleroma/accounts/confirmation_resend" +  def 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 +      json_response(conn, :no_content, "") +    end +  end + +  @doc "PATCH /api/v1/pleroma/accounts/update_avatar" +  def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do +    {:ok, user} = +      user +      |> Changeset.change(%{avatar: nil}) +      |> User.update_and_set_cache() + +    CommonAPI.update(user) + +    json(conn, %{url: nil}) +  end + +  def update_avatar(%{assigns: %{user: user}} = conn, params) do +    {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) +    {:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() +    %{"url" => [%{"href" => href} | _]} = data + +    CommonAPI.update(user) + +    json(conn, %{url: href}) +  end + +  @doc "PATCH /api/v1/pleroma/accounts/update_banner" +  def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do +    new_info = %{"banner" => %{}} + +    with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) 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}, +         {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do +      CommonAPI.update(user) +      %{"url" => [%{"href" => href} | _]} = object.data + +      json(conn, %{url: href}) +    end +  end + +  @doc "PATCH /api/v1/pleroma/accounts/update_background" +  def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do +    new_info = %{"background" => %{}} + +    with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) 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}, +         {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do +      %{"url" => [%{"href" => href} | _]} = object.data + +      json(conn, %{url: href}) +    end +  end + +  @doc "GET /api/v1/pleroma/accounts/:id/favourites" +  def favourites(%{assigns: %{account: %{info: %{hide_favorites: true}}}} = conn, _params) do +    render_error(conn, :forbidden, "Can't get favorites") +  end + +  def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do +    params = +      params +      |> Map.put("type", "Create") +      |> Map.put("favorited_by", user.ap_id) +      |> Map.put("blocking_user", for_user) + +    recipients = +      if for_user do +        [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] +      else +        [Pleroma.Constants.as_public()] +      end + +    activities = +      recipients +      |> ActivityPub.fetch_activities(params) +      |> Enum.reverse() + +    conn +    |> add_link_headers(activities) +    |> put_view(StatusView) +    |> render("index.json", activities: activities, for: for_user, as: :activity) +  end + +  @doc "POST /api/v1/pleroma/accounts/:id/subscribe" +  def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do +    with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do +      render(conn, "relationship.json", user: user, target: subscription_target) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end + +  @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe" +  def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do +    with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do +      render(conn, "relationship.json", user: user, target: subscription_target) +    else +      {:error, message} -> json_response(conn, :forbidden, %{error: message}) +    end +  end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex new file mode 100644 index 000000000..7f6a76c0e --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.MascotController do +  use Pleroma.Web, :controller + +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub + +  @doc "GET /api/v1/pleroma/mascot" +  def show(%{assigns: %{user: user}} = conn, _params) do +    json(conn, User.get_mascot(user)) +  end + +  @doc "PUT /api/v1/pleroma/mascot" +  def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do +    with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), +         # Reject if not an image +         %{type: "image"} = attachment <- render_attachment(object) do +      # Sure! +      # Save to the user's info +      {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, attachment)) + +      json(conn, attachment) +    else +      %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images") +    end +  end + +  defp render_attachment(object) do +    attachment_data = Map.put(object.data, "id", object.id) +    Pleroma.Web.MastodonAPI.StatusView.render("attachment.json", %{attachment: attachment_data}) +  end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 09cbca766..eae1f676b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -296,22 +296,44 @@ defmodule Pleroma.Web.Router do      pipe_through(:authenticated_api)      scope [] do +      pipe_through(:authenticated_api)        pipe_through(:oauth_read)        get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)        get("/conversations/:id", PleromaAPIController, :conversation)      end      scope [] do +      pipe_through(:authenticated_api)        pipe_through(:oauth_write)        patch("/conversations/:id", PleromaAPIController, :update_conversation)        post("/statuses/:id/react_with_emoji", PleromaAPIController, :react_with_emoji)        post("/notifications/read", PleromaAPIController, :read_notification) + +      patch("/accounts/update_avatar", AccountController, :update_avatar) +      patch("/accounts/update_banner", AccountController, :update_banner) +      patch("/accounts/update_background", AccountController, :update_background) + +      get("/mascot", MascotController, :show) +      put("/mascot", MascotController, :update) + +      post("/scrobble", ScrobbleController, :new_scrobble)      end      scope [] do -      pipe_through(:oauth_write) -      post("/scrobble", ScrobbleController, :new_scrobble) +      pipe_through(:api) +      pipe_through(:oauth_read_or_public) +      get("/accounts/:id/favourites", AccountController, :favourites) +    end + +    scope [] do +      pipe_through(:authenticated_api) +      pipe_through(:oauth_follow) + +      post("/accounts/:id/subscribe", AccountController, :subscribe) +      post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)      end + +    post("/accounts/confirmation_resend", AccountController, :confirmation_resend)    end    scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do @@ -326,11 +348,11 @@ defmodule Pleroma.Web.Router do      scope [] do        pipe_through(:oauth_read) -      get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) +      get("/accounts/verify_credentials", AccountController, :verify_credentials) -      get("/accounts/relationships", MastodonAPIController, :relationships) +      get("/accounts/relationships", AccountController, :relationships) -      get("/accounts/:id/lists", MastodonAPIController, :account_lists) +      get("/accounts/:id/lists", AccountController, :lists)        get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)        get("/follow_requests", FollowRequestController, :index) @@ -360,7 +382,7 @@ defmodule Pleroma.Web.Router do        get("/filters", FilterController, :index) -      get("/suggestions", MastodonAPIController, :suggestions) +      get("/suggestions", SuggestionController, :index)        get("/conversations", ConversationController, :index)        post("/conversations/:id/read", ConversationController, :read) @@ -371,7 +393,7 @@ defmodule Pleroma.Web.Router do      scope [] do        pipe_through(:oauth_write) -      patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) +      patch("/accounts/update_credentials", AccountController, :update_credentials)        post("/statuses", StatusController, :create)        delete("/statuses/:id", StatusController, :delete) @@ -390,10 +412,10 @@ defmodule Pleroma.Web.Router do        put("/scheduled_statuses/:id", ScheduledActivityController, :update)        delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) -      post("/polls/:id/votes", MastodonAPIController, :poll_vote) +      post("/polls/:id/votes", PollController, :vote) -      post("/media", MastodonAPIController, :upload) -      put("/media/:id", MastodonAPIController, :update_media) +      post("/media", MediaController, :create) +      put("/media/:id", MediaController, :update)        delete("/lists/:id", ListController, :delete)        post("/lists", ListController, :create) @@ -407,36 +429,25 @@ defmodule Pleroma.Web.Router do        put("/filters/:id", FilterController, :update)        delete("/filters/:id", FilterController, :delete) -      patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar) -      patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner) -      patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background) - -      get("/pleroma/mascot", MastodonAPIController, :get_mascot) -      put("/pleroma/mascot", MastodonAPIController, :set_mascot) -        post("/reports", ReportController, :create)      end      scope [] do        pipe_through(:oauth_follow) -      post("/follows", MastodonAPIController, :follow) -      post("/accounts/:id/follow", MastodonAPIController, :follow) - -      post("/accounts/:id/unfollow", MastodonAPIController, :unfollow) -      post("/accounts/:id/block", MastodonAPIController, :block) -      post("/accounts/:id/unblock", MastodonAPIController, :unblock) -      post("/accounts/:id/mute", MastodonAPIController, :mute) -      post("/accounts/:id/unmute", MastodonAPIController, :unmute) +      post("/follows", MastodonAPIController, :follows) +      post("/accounts/:id/follow", AccountController, :follow) +      post("/accounts/:id/unfollow", AccountController, :unfollow) +      post("/accounts/:id/block", AccountController, :block) +      post("/accounts/:id/unblock", AccountController, :unblock) +      post("/accounts/:id/mute", AccountController, :mute) +      post("/accounts/:id/unmute", AccountController, :unmute)        post("/follow_requests/:id/authorize", FollowRequestController, :authorize)        post("/follow_requests/:id/reject", FollowRequestController, :reject)        post("/domain_blocks", DomainBlockController, :create)        delete("/domain_blocks", DomainBlockController, :delete) - -      post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe) -      post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)      end      scope [] do @@ -458,16 +469,17 @@ defmodule Pleroma.Web.Router do    scope "/api/v1", Pleroma.Web.MastodonAPI do      pipe_through(:api) -    post("/accounts", MastodonAPIController, :account_register) +    post("/accounts", AccountController, :create) + +    get("/instance", InstanceController, :show) +    get("/instance/peers", InstanceController, :peers) + +    post("/apps", AppController, :create) +    get("/apps/verify_credentials", AppController, :verify_credentials) -    get("/instance", MastodonAPIController, :masto_instance) -    get("/instance/peers", MastodonAPIController, :peers) -    post("/apps", MastodonAPIController, :create_app) -    get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)      get("/custom_emojis", MastodonAPIController, :custom_emojis)      get("/statuses/:id/card", StatusController, :card) -      get("/statuses/:id/favourited_by", StatusController, :favourited_by)      get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) @@ -475,12 +487,6 @@ defmodule Pleroma.Web.Router do      get("/accounts/search", SearchController, :account_search) -    post( -      "/pleroma/accounts/confirmation_resend", -      MastodonAPIController, -      :account_confirmation_resend -    ) -      scope [] do        pipe_through(:oauth_read_or_public) @@ -492,16 +498,14 @@ defmodule Pleroma.Web.Router do        get("/statuses/:id", StatusController, :show)        get("/statuses/:id/context", StatusController, :context) -      get("/polls/:id", MastodonAPIController, :get_poll) +      get("/polls/:id", PollController, :show) -      get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) -      get("/accounts/:id/followers", MastodonAPIController, :followers) -      get("/accounts/:id/following", MastodonAPIController, :following) -      get("/accounts/:id", MastodonAPIController, :user) +      get("/accounts/:id/statuses", AccountController, :statuses) +      get("/accounts/:id/followers", AccountController, :followers) +      get("/accounts/:id/following", AccountController, :following) +      get("/accounts/:id", AccountController, :show)        get("/search", SearchController, :search) - -      get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)      end    end @@ -667,10 +671,10 @@ defmodule Pleroma.Web.Router do    scope "/", Pleroma.Web.MastodonAPI do      pipe_through(:mastodon_html) -    get("/web/login", MastodonAPIController, :login) -    delete("/auth/sign_out", MastodonAPIController, :logout) +    get("/web/login", AuthController, :login) +    delete("/auth/sign_out", AuthController, :logout) -    post("/auth/password", MastodonAPIController, :password_reset) +    post("/auth/password", AuthController, :password_reset)      scope [] do        pipe_through(:oauth_read) diff --git a/lib/pleroma/web/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex index 8f5a43bf6..a104ea6b8 100644 --- a/lib/pleroma/web/translation_helpers.ex +++ b/lib/pleroma/web/translation_helpers.ex @@ -3,15 +3,27 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.TranslationHelpers do -  defmacro render_error(conn, status, msgid, bindings \\ Macro.escape(%{})) do +  defmacro render_error( +             conn, +             status, +             msgid, +             bindings \\ Macro.escape(%{}), +             identifier \\ Macro.escape("") +           ) do      quote do        require Pleroma.Web.Gettext +      error_map = +        %{ +          error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)), +          identifier: unquote(identifier) +        } +        |> Enum.reject(fn {_k, v} -> v == "" end) +        |> Map.new() +        unquote(conn)        |> Plug.Conn.put_status(unquote(status)) -      |> Phoenix.Controller.json(%{ -        error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)) -      }) +      |> Phoenix.Controller.json(error_map)      end    end  end | 
