From: Ivan Tashkinov Date: Wed, 2 Oct 2019 17:42:40 +0000 (+0300) Subject: [#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon... X-Git-Url: https://git.squeep.com/?a=commitdiff_plain;h=64095961fe490029886cae358683532ea65bf2c0;p=akkoma [#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon-2-4-3-oauth-scopes # Conflicts: # CHANGELOG.md # lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex # lib/pleroma/web/router.ex --- 64095961fe490029886cae358683532ea65bf2c0 diff --cc CHANGELOG.md index e3806166f,3d9424c8f..87b6d1180 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@@ -100,9 -112,11 +112,12 @@@ The format is based on [Keep a Changelo - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=` for resending account confirmation. - Pleroma API: Email change endpoint. - Admin API: Added moderation log + - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Web response cache (currently, enabled for ActivityPub) - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`) + - ActivityPub: Add ActivityPub actor's `discoverable` parameter. + - Admin API: Added moderation log filters (user/start date/end date/search/pagination) +- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/) ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text diff --cc lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 000000000,df14ad66f..3bc9ed8ae mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@@ -1,0 -1,304 +1,344 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # 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.OAuthScopesPlug + 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 + ++ plug( ++ OAuthScopesPlug, ++ %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} ++ when action == :show ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["read:accounts"]} ++ when action in [:endorsements, :verify_credentials, :followers, :following] ++ ) ++ ++ plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials) ++ ++ plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock] ++ ) ++ ++ plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["follow", "write:follows"]} when action in [:follow, :unfollow] ++ ) ++ ++ plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) ++ ++ plug( ++ Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug ++ when action != :create ++ ) ++ + @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 ++ ++ @doc "GET /api/v1/endorsements" ++ def endorsements(conn, params), ++ do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params) + end diff --cc lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index 000000000,ea1e36a12..6c0584c54 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@@ -1,0 -1,32 +1,38 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.ConversationController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + + alias Pleroma.Conversation.Participation ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Repo + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + ++ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index) ++ plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ + @doc "GET /api/v1/conversations" + def index(%{assigns: %{user: user}} = conn, params) do + participations = Participation.for_user_with_last_activity_id(user, params) + + conn + |> add_link_headers(participations) + |> render("participations.json", participations: participations, for: user) + end + + @doc "POST /api/v1/conversations/:id/read" + def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + with %Participation{} = participation <- + Repo.get_by(Participation, id: participation_id, user_id: user.id), + {:ok, participation} <- Participation.mark_as_read(participation) do + render(conn, "participation.json", participation: participation, for: user) + end + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index 000000000,03db6c9b8..45c5ef8a4 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@@ -1,0 -1,26 +1,37 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.DomainBlockController do + use Pleroma.Web, :controller + ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["follow", "read:blocks"]} when action == :index ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["follow", "write:blocks"]} when action != :index ++ ) ++ + @doc "GET /api/v1/domain_blocks" + def index(%{assigns: %{user: %{info: info}}} = conn, _) do + json(conn, Map.get(info, :domain_blocks, [])) + end + + @doc "POST /api/v1/domain_blocks" + def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do + User.block_domain(blocker, domain) + json(conn, %{}) + end + + @doc "DELETE /api/v1/domain_blocks" + def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do + User.unblock_domain(blocker, domain) + json(conn, %{}) + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index 000000000,19041304e..cadef72e1 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@@ -1,0 -1,72 +1,84 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.FilterController do + use Pleroma.Web, :controller + + alias Pleroma.Filter ++ alias Pleroma.Plugs.OAuthScopesPlug ++ ++ @oauth_read_actions [:show, :index] ++ ++ plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["write:filters"]} when action not in @oauth_read_actions ++ ) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + + @doc "GET /api/v1/filters" + def index(%{assigns: %{user: user}} = conn, _) do + filters = Filter.get_filters(user) + + render(conn, "filters.json", filters: filters) + end + + @doc "POST /api/v1/filters" + def create( + %{assigns: %{user: user}} = conn, + %{"phrase" => phrase, "context" => context} = params + ) do + query = %Filter{ + user_id: user.id, + phrase: phrase, + context: context, + hide: Map.get(params, "irreversible", false), + whole_word: Map.get(params, "boolean", true) + # expires_at + } + + {:ok, response} = Filter.create(query) + + render(conn, "filter.json", filter: response) + end + + @doc "GET /api/v1/filters/:id" + def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do + filter = Filter.get(filter_id, user) + + render(conn, "filter.json", filter: filter) + end + + @doc "PUT /api/v1/filters/:id" + def update( + %{assigns: %{user: user}} = conn, + %{"phrase" => phrase, "context" => context, "id" => filter_id} = params + ) do + query = %Filter{ + user_id: user.id, + filter_id: filter_id, + phrase: phrase, + context: context, + hide: Map.get(params, "irreversible", nil), + whole_word: Map.get(params, "boolean", true) + # expires_at + } + + {:ok, response} = Filter.update(query) + render(conn, "filter.json", filter: response) + end + + @doc "DELETE /api/v1/filters/:id" + def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do + query = %Filter{ + user_id: user.id, + filter_id: filter_id + } + + {:ok, _} = Filter.delete(query) + json(conn, %{}) + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 000000000,ce7b625ee..06672e2bb mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@@ -1,0 -1,49 +1,57 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.FollowRequestController do + use Pleroma.Web, :controller + ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + plug(:assign_follower when action != :index) + + action_fallback(:errors) + ++ plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :index) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["follow", "write:follows"]} when action != :index ++ ) ++ + @doc "GET /api/v1/follow_requests" + def index(%{assigns: %{user: followed}} = conn, _params) do + follow_requests = User.get_follow_requests(followed) + + render(conn, "index.json", for: followed, users: follow_requests, as: :user) + end + + @doc "POST /api/v1/follow_requests/:id/authorize" + def authorize(%{assigns: %{user: followed, follower: follower}} = conn, _params) do + with {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do + render(conn, "relationship.json", user: followed, target: follower) + end + end + + @doc "POST /api/v1/follow_requests/:id/reject" + def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do + with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do + render(conn, "relationship.json", user: followed, target: follower) + end + end + + defp assign_follower(%{params: %{"id" => id}} = conn, _) do + case User.get_cached_by_id(id) do + %User{} = follower -> assign(conn, :follower, follower) + nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() + end + end + + defp errors(conn, {:error, message}) do + conn + |> put_status(:forbidden) + |> json(%{error: message}) + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/list_controller.ex index be7089630,50f42bee5..e0ffdba21 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@@ -11,14 -10,6 +11,16 @@@ defmodule Pleroma.Web.MastodonAPI.ListC plug(:list_by_id_and_user when action not in [:index, :create]) + plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts]) + + plug( + OAuthScopesPlug, + %{scopes: ["write:lists"]} + when action in [:create, :update, :delete, :add_to_list, :remove_from_list] + ) + ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ action_fallback(Pleroma.Web.MastodonAPI.FallbackController) # GET /api/v1/lists diff --cc lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index b1e9dee3d,1484a0174..ee644abe3 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@@ -11,18 -10,11 +10,12 @@@ defmodule Pleroma.Web.MastodonAPI.Masto alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Config - alias Pleroma.Conversation.Participation - alias Pleroma.Filter - alias Pleroma.Formatter alias Pleroma.HTTP - alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Pagination + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo - alias Pleroma.ScheduledActivity alias Pleroma.Stats alias Pleroma.User alias Pleroma.Web @@@ -47,224 -32,9 +33,67 @@@ alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Pleroma.Web.ControllerHelper - import Ecto.Query - require Logger - require Pleroma.Constants + @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} + + # Note: :index action handles attempt of unauthenticated access to private instance with redirect + plug( + OAuthScopesPlug, + Map.merge(@unauthenticated_access, %{scopes: ["read"], skip_instance_privacy_check: true}) + when action == :index + ) + + plug( + OAuthScopesPlug, + %{scopes: ["read"]} when action in [:suggestions, :verify_app_credentials] + ) + - plug( - OAuthScopesPlug, - %{scopes: ["write:accounts"]} - # Note: the following actions are not permission-secured in Mastodon: - when action in [ - :put_settings, - :update_avatar, - :update_banner, - :update_background, - :set_mascot - ] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:accounts"]} - when action in [:pin_status, :unpin_status, :update_credentials] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["read:statuses"]} - when action in [ - :conversations, - :scheduled_statuses, - :show_scheduled_status, - :home_timeline, - :dm_timeline - ] - ) - - plug( - OAuthScopesPlug, - %{@unauthenticated_access | scopes: ["read:statuses"]} - when action in [ - :user_statuses, - :get_statuses, - :get_status, - :get_context, - :status_card, - :get_poll - ] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:statuses"]} - when action in [ - :update_scheduled_status, - :delete_scheduled_status, - :post_status, - :delete_status, - :reblog_status, - :unreblog_status, - :poll_vote - ] - ) - - plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :conversation_read) - - plug( - OAuthScopesPlug, - %{scopes: ["read:accounts"]} - when action in [:endorsements, :verify_credentials, :followers, :following, :get_mascot] - ) - - plug( - OAuthScopesPlug, - %{@unauthenticated_access | scopes: ["read:accounts"]} - when action in [:user, :favourited_by, :reblogged_by] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["read:favourites"]} when action in [:favourites, :user_favourites] - ) ++ plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) + + plug( + OAuthScopesPlug, - %{scopes: ["write:favourites"]} when action in [:fav_status, :unfav_status] ++ %{@unauthenticated_access | scopes: ["read:statuses"]} when action == :get_poll + ) + - plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in [:get_filters, :get_filter]) ++ plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :poll_vote) + - plug( - OAuthScopesPlug, - %{scopes: ["write:filters"]} when action in [:create_filter, :update_filter, :delete_filter] - ) - - plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:account_lists, :list_timeline]) ++ plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites) + + plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action in [:upload, :update_media]) + + plug( + OAuthScopesPlug, - %{scopes: ["read:notifications"]} when action in [:notifications, :get_notification] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:notifications"]} - when action in [:clear_notifications, :dismiss_notification, :destroy_multiple_notifications] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:reports"]} - when action in [:create_report, :report_update_state, :report_respond] ++ %{scopes: ["follow", "read:blocks"]} when action == :blocks + ) + ++ # To do: POST /api/v1/follows is not present in Mastodon; consider removing the action + plug( + OAuthScopesPlug, - %{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["follow", "write:blocks"]} - when action in [:block, :unblock, :block_domain, :unblock_domain] - ) - - plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships) - plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests) - - plug( - OAuthScopesPlug, - %{scopes: ["follow", "write:follows"]} - when action in [ - :follow, - :unfollow, - :subscribe, - :unsubscribe, - :authorize_follow_request, - :reject_follow_request - ] ++ %{scopes: ["follow", "write:follows"]} when action == :follows + ) + + plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes) - plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) - - plug( - OAuthScopesPlug, - %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation] - ) + - # Note: scopes not present in Mastodon: read:bookmarks, write:bookmarks ++ # Note: scope not present in Mastodon: read:bookmarks + plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks) + - plug( - OAuthScopesPlug, - %{scopes: ["write:bookmarks"]} when action in [:bookmark_status, :unbookmark_status] - ) - + # An extra safety measure for possible actions not guarded by OAuth permissions specification + plug( + Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + when action not in [ - :account_register, + :create_app, + :index, + :login, + :logout, + :password_reset, - :account_confirmation_resend, + :masto_instance, + :peers, + :custom_emojis + ] + ) + - @rate_limited_relations_actions ~w(follow unfollow)a - - @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status - post_status delete_status)a - - plug( - RateLimiter, - {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} - when action in ~w(reblog_status unreblog_status)a - ) - - plug( - RateLimiter, - {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} - when action in ~w(fav_status unfav_status)a - ) - - plug( - RateLimiter, - {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions - ) - - plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions) - plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) - plug(RateLimiter, :app_account_creation when action == :account_register) - plug(RateLimiter, :search when action in [:search, :search2, :account_search]) plug(RateLimiter, :password_reset when action == :password_reset) - plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend) @local_mastodon_name "Mastodon-Local" diff --cc lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 000000000,7e4d7297c..36c6defc2 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@@ -1,0 -1,57 +1,67 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.NotificationController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + + alias Pleroma.Notification ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.MastodonAPI.MastodonAPI + ++ @oauth_read_actions [:show, :index] ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["read:notifications"]} when action in @oauth_read_actions ++ ) ++ ++ plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions) ++ + # GET /api/v1/notifications + def index(%{assigns: %{user: user}} = conn, params) do + notifications = MastodonAPI.get_notifications(user, params) + + conn + |> add_link_headers(notifications) + |> render("index.json", notifications: notifications, for: user) + end + + # GET /api/v1/notifications/:id + def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with {:ok, notification} <- Notification.get(user, id) do + render(conn, "show.json", notification: notification, for: user) + else + {:error, reason} -> + conn + |> put_status(:forbidden) + |> json(%{"error" => reason}) + end + end + + # POST /api/v1/notifications/clear + def clear(%{assigns: %{user: user}} = conn, _params) do + Notification.clear(user) + json(conn, %{}) + end + + # POST /api/v1/notifications/dismiss + def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + with {:ok, _notif} <- Notification.dismiss(user, id) do + json(conn, %{}) + else + {:error, reason} -> + conn + |> put_status(:forbidden) + |> json(%{"error" => reason}) + end + end + + # DELETE /api/v1/notifications/destroy_multiple + def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do + Notification.destroy_multiple(user, ids) + json(conn, %{}) + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/report_controller.ex index 000000000,1c084b740..313f885a6 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex @@@ -1,0 -1,16 +1,20 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.ReportController do ++ alias Pleroma.Plugs.OAuthScopesPlug ++ + use Pleroma.Web, :controller + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + ++ plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) ++ + @doc "POST /api/v1/reports" + def create(%{assigns: %{user: user}} = conn, params) do + with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do + render(conn, "show.json", activity: activity) + end + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex index 000000000,0a56b10b6..ff9276541 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@@ -1,0 -1,51 +1,59 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.ScheduledActivity + alias Pleroma.Web.MastodonAPI.MastodonAPI + + plug(:assign_scheduled_activity when action != :index) + ++ @oauth_read_actions [:show, :index] ++ ++ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions) ++ plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + @doc "GET /api/v1/scheduled_statuses" + def index(%{assigns: %{user: user}} = conn, params) do + with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do + conn + |> add_link_headers(scheduled_activities) + |> render("index.json", scheduled_activities: scheduled_activities) + end + end + + @doc "GET /api/v1/scheduled_statuses/:id" + def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do + render(conn, "show.json", scheduled_activity: scheduled_activity) + end + + @doc "PUT /api/v1/scheduled_statuses/:id" + def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do + with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do + render(conn, "show.json", scheduled_activity: scheduled_activity) + end + end + + @doc "DELETE /api/v1/scheduled_statuses/:id" + def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do + with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do + render(conn, "show.json", scheduled_activity: scheduled_activity) + end + end + + defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do + case ScheduledActivity.get(user, id) do + %ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity) + nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() + end + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 000000000,3c6987a5f..ee9047d1c mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@@ -1,0 -1,274 +1,325 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.StatusController do + use Pleroma.Web, :controller + + import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3] + + require Ecto.Query + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Object ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.ScheduledActivityView + ++ @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} ++ ++ plug( ++ OAuthScopesPlug, ++ %{@unauthenticated_access | scopes: ["read:statuses"]} ++ when action in [ ++ :index, ++ :show, ++ :card, ++ :context ++ ] ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["write:statuses"]} ++ when action in [ ++ :create, ++ :delete, ++ :reblog, ++ :unreblog ++ ] ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite] ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation] ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{@unauthenticated_access | scopes: ["read:accounts"]} ++ when action in [:favourited_by, :reblogged_by] ++ ) ++ ++ plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin]) ++ ++ # Note: scope not present in Mastodon: write:bookmarks ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] ++ ) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ + @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} + when action in ~w(reblog unreblog)a + ) + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} + when action in ~w(favourite unfavourite)a + ) + + plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + @doc """ + GET `/api/v1/statuses?ids[]=1&ids[]=2` + + `ids` query param is required + """ + def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do + limit = 100 + + activities = + ids + |> Enum.take(limit) + |> Activity.all_by_ids_with_object() + |> Enum.filter(&Visibility.visible_for_user?(&1, user)) + + render(conn, "index.json", activities: activities, for: user, as: :activity) + end + + @doc """ + POST /api/v1/statuses + + Creates a scheduled status when `scheduled_at` param is present and it's far enough + """ + def create( + %{assigns: %{user: user}} = conn, + %{"status" => _, "scheduled_at" => scheduled_at} = params + ) do + params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"]) + + if ScheduledActivity.far_enough?(scheduled_at) do + with {:ok, scheduled_activity} <- + ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do + conn + |> put_view(ScheduledActivityView) + |> render("show.json", scheduled_activity: scheduled_activity) + end + else + create(conn, Map.drop(params, ["scheduled_at"])) + end + end + + @doc """ + POST /api/v1/statuses + + Creates a regular status + """ + def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do + params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"]) + + with {:ok, activity} <- CommonAPI.post(user, params) do + try_render(conn, "show.json", + activity: activity, + for: user, + as: :activity, + with_direct_conversation_id: true + ) + else + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + end + end + + def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do + create(conn, Map.put(params, "status", "")) + end + + @doc "GET /api/v1/statuses/:id" + def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + true <- Visibility.visible_for_user?(activity, user) do + try_render(conn, "show.json", activity: activity, for: user) + end + end + + @doc "DELETE /api/v1/statuses/:id" + def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + json(conn, %{}) + else + _e -> render_error(conn, :forbidden, "Can't delete this post") + end + 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), + %Activity{} = announce <- Activity.normalize(announce.data) do + try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) + end + end + + @doc "POST /api/v1/statuses/:id/unreblog" + def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do + try_render(conn, "show.json", %{activity: activity, for: user, as: :activity}) + end + end + + @doc "POST /api/v1/statuses/:id/favourite" + def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/unfavourite" + def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/pin" + def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/unpin" + def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/bookmark" + def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %User{} = user <- User.get_cached_by_nickname(user.nickname), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/unbookmark" + def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %User{} = user <- User.get_cached_by_nickname(user.nickname), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/mute" + def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id(id), + {:ok, activity} <- CommonAPI.add_mute(user, activity) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "POST /api/v1/statuses/:id/unmute" + def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id(id), + {:ok, activity} <- CommonAPI.remove_mute(user, activity) do + try_render(conn, "show.json", activity: activity, for: user, as: :activity) + end + end + + @doc "GET /api/v1/statuses/:id/card" + @deprecated "https://github.com/tootsuite/mastodon/pull/11213" + def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do + with %Activity{} = activity <- Activity.get_by_id(status_id), + true <- Visibility.visible_for_user?(activity, user) do + data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + render(conn, "card.json", data) + else + _ -> render_error(conn, :not_found, "Record not found") + end + end + + @doc "GET /api/v1/statuses/:id/favourited_by" + def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, + %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do + users = + User + |> Ecto.Query.where([u], u.ap_id in ^likes) + |> Repo.all() + |> Enum.filter(&(not User.blocks?(user, &1))) + + conn + |> put_view(AccountView) + |> render("index.json", for: user, users: users, as: :user) + else + {:visible, false} -> {:error, :not_found} + _ -> json(conn, []) + end + end + + @doc "GET /api/v1/statuses/:id/reblogged_by" + 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 + users = + User + |> Ecto.Query.where([u], u.ap_id in ^announces) + |> Repo.all() + |> Enum.filter(&(not User.blocks?(user, &1))) + + conn + |> put_view(AccountView) + |> render("index.json", for: user, users: users, as: :user) + else + {:visible, false} -> {:error, :not_found} + _ -> json(conn, []) + end + end + + @doc "GET /api/v1/statuses/:id/context" + def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Activity.get_by_id(id) do + activities = + ActivityPub.fetch_activities_for_context(activity.data["context"], %{ + "blocking_user" => user, + "user" => user, + "exclude_id" => activity.id + }) + + render(conn, "context.json", activity: activity, activities: activities, user: user) + end + end + end diff --cc lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 000000000,bb8b0eb32..9f086a8c2 mode 000000,100644..100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@@ -1,0 -1,136 +1,142 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.MastodonAPI.TimelineController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, + only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1] + + alias Pleroma.Pagination ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ActivityPub.ActivityPub + ++ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) ++ plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ + plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) + + # GET /api/v1/timelines/home + def home(%{assigns: %{user: user}} = conn, params) do + params = + params + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + + recipients = [user.ap_id | user.following] + + activities = + recipients + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() + + conn + |> add_link_headers(activities) + |> render("index.json", activities: activities, for: user, as: :activity) + end + + # GET /api/v1/timelines/direct + def direct(%{assigns: %{user: user}} = conn, params) do + params = + params + |> Map.put("type", "Create") + |> Map.put("blocking_user", user) + |> Map.put("user", user) + |> Map.put(:visibility, "direct") + + activities = + [user.ap_id] + |> ActivityPub.fetch_activities_query(params) + |> Pagination.fetch_paginated(params) + + conn + |> add_link_headers(activities) + |> render("index.json", activities: activities, for: user, as: :activity) + end + + # GET /api/v1/timelines/public + def public(%{assigns: %{user: user}} = conn, params) do + local_only = truthy_param?(params["local"]) + + activities = + params + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("local_only", local_only) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> ActivityPub.fetch_public_activities() + |> Enum.reverse() + + conn + |> add_link_headers(activities, %{"local" => local_only}) + |> render("index.json", activities: activities, for: user, as: :activity) + end + + # GET /api/v1/timelines/tag/:tag + def hashtag(%{assigns: %{user: user}} = conn, params) do + local_only = truthy_param?(params["local"]) + + tags = + [params["tag"], params["any"]] + |> List.flatten() + |> Enum.uniq() + |> Enum.filter(& &1) + |> Enum.map(&String.downcase(&1)) + + tag_all = + params + |> Map.get("all", []) + |> Enum.map(&String.downcase(&1)) + + tag_reject = + params + |> Map.get("none", []) + |> Enum.map(&String.downcase(&1)) + + activities = + params + |> Map.put("type", "Create") + |> Map.put("local_only", local_only) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + |> Map.put("tag", tags) + |> Map.put("tag_all", tag_all) + |> Map.put("tag_reject", tag_reject) + |> ActivityPub.fetch_public_activities() + |> Enum.reverse() + + conn + |> add_link_headers(activities, %{"local" => local_only}) + |> render("index.json", activities: activities, for: user, as: :activity) + end + + # GET /api/v1/timelines/list/:list_id + def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do + with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do + params = + params + |> Map.put("type", "Create") + |> Map.put("blocking_user", user) + |> Map.put("user", user) + |> Map.put("muting_user", user) + + # we must filter the following list for the user to avoid leaking statuses the user + # does not actually have permission to see (for more info, peruse security issue #270). + activities = + following + |> Enum.filter(fn x -> x in user.following end) + |> ActivityPub.fetch_activities_bounded(following, params) + |> Enum.reverse() + + render(conn, "index.json", activities: activities, for: user, as: :activity) + else + _e -> render_error(conn, :forbidden, "Error.") + end + end + end diff --cc lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 000000000,63c44086c..9012e2175 mode 000000,100644..100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@@ -1,0 -1,143 +1,168 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # 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.OAuthScopesPlug + 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( ++ OAuthScopesPlug, ++ %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] ++ ) ++ ++ plug( ++ OAuthScopesPlug, ++ %{scopes: ["write:accounts"]} ++ # Note: the following actions are not permission-secured in Mastodon: ++ when action in [ ++ :update_avatar, ++ :update_banner, ++ :update_background ++ ] ++ ) ++ ++ plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites) ++ ++ # An extra safety measure for possible actions not guarded by OAuth permissions specification ++ plug( ++ Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug ++ when action != :confirmation_resend ++ ) ++ + 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 --cc lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index 000000000,7f6a76c0e..d71d72dd5 mode 000000,100644..100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@@ -1,0 -1,35 +1,41 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.PleromaAPI.MascotController do + use Pleroma.Web, :controller + ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + ++ plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) ++ plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ + @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 --cc lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index f3dc4616c,d17ccf84d..9d50a7ca9 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@@ -15,18 -14,6 +15,20 @@@ defmodule Pleroma.Web.PleromaAPI.Plerom alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"]} when action in [:conversation, :conversation_statuses] + ) + + plug( + OAuthScopesPlug, - %{scopes: ["write:conversations"]} when action in [:conversations, :conversation_read] ++ %{scopes: ["write:conversations"]} when action == :update_conversation + ) + + plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification) + ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do with %Participation{} = participation <- Participation.get(participation_id), true <- user.id == participation.user_id do diff --cc lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 000000000,0fb978c5d..b74b3debc mode 000000,100644..100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@@ -1,0 -1,52 +1,58 @@@ + # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors + # SPDX-License-Identifier: AGPL-3.0-only + + defmodule Pleroma.Web.PleromaAPI.ScrobbleController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2] + ++ alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView + ++ plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles) ++ plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) ++ ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ + def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do + params = + if !params["length"] do + params + else + params + |> Map.put("length", fetch_integer_param(params, "length")) + end + + with {:ok, activity} <- CommonAPI.listen(user, params) do + conn + |> put_view(StatusView) + |> render("listen.json", %{activity: activity, for: user}) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(%{"error" => message}) + end + end + + def user_scrobbles(%{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, "type", ["Listen"]) + + activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) + + conn + |> add_link_headers(activities) + |> put_view(StatusView) + |> render("listens.json", %{ + activities: activities, + for: reading_user, + as: :activity + }) + end + end + end diff --cc lib/pleroma/web/router.ex index 3002d0738,eab55a27c..cc6bcfa1a --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@@ -180,6 -206,30 +181,30 @@@ defmodule Pleroma.Web.Router d get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) get("/moderation_log", AdminAPIController, :list_log) + + post("/reload_emoji", AdminAPIController, :reload_emoji) + end + + scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do + scope "/packs" do + # Modifying packs - pipe_through([:admin_api, :oauth_write]) ++ pipe_through(:admin_api) + + post("/import_from_fs", EmojiAPIController, :import_from_fs) + + post("/:pack_name/update_file", EmojiAPIController, :update_file) + post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata) + put("/:name", EmojiAPIController, :create) + delete("/:name", EmojiAPIController, :delete) + post("/download_from", EmojiAPIController, :download_from) + post("/list_from", EmojiAPIController, :list_from) + end + + scope "/packs" do + # Pack info / downloading + get("/", EmojiAPIController, :list_packs) + get("/:name/download_shared/", EmojiAPIController, :download_shared) + end end scope "/", Pleroma.Web.TwitterAPI do @@@ -225,134 -287,168 +250,152 @@@ end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI 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("/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(:api) - pipe_through(:oauth_read_or_public) + get("/accounts/:id/favourites", AccountController, :favourites) + end + + scope [] do + pipe_through(:authenticated_api) - pipe_through(:oauth_follow) - get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) - get("/conversations/:id", PleromaAPIController, :conversation) + post("/accounts/:id/subscribe", AccountController, :subscribe) + post("/accounts/:id/unsubscribe", AccountController, :unsubscribe) + end - patch("/conversations/:id", PleromaAPIController, :update_conversation) - post("/notifications/read", PleromaAPIController, :read_notification) + post("/accounts/confirmation_resend", AccountController, :confirmation_resend) + end + + scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do - pipe_through([:api, :oauth_read_or_public]) ++ pipe_through(:api) + + get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles) end scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) - get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) - scope [] do - pipe_through(:oauth_read) ++ get("/accounts/verify_credentials", AccountController, :verify_credentials) - get("/accounts/relationships", MastodonAPIController, :relationships) - get("/accounts/verify_credentials", AccountController, :verify_credentials) ++ get("/accounts/relationships", AccountController, :relationships) - get("/accounts/:id/lists", MastodonAPIController, :account_lists) - get("/accounts/relationships", AccountController, :relationships) ++ get("/accounts/:id/lists", AccountController, :lists) + get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) - get("/follow_requests", MastodonAPIController, :follow_requests) - get("/accounts/:id/lists", AccountController, :lists) - get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) ++ get("/follow_requests", FollowRequestController, :index) + get("/blocks", MastodonAPIController, :blocks) + get("/mutes", MastodonAPIController, :mutes) - get("/timelines/home", MastodonAPIController, :home_timeline) - get("/timelines/direct", MastodonAPIController, :dm_timeline) - get("/follow_requests", FollowRequestController, :index) - get("/blocks", MastodonAPIController, :blocks) - get("/mutes", MastodonAPIController, :mutes) ++ get("/timelines/home", TimelineController, :home) ++ get("/timelines/direct", TimelineController, :direct) - get("/timelines/home", TimelineController, :home) - get("/timelines/direct", TimelineController, :direct) + get("/favourites", MastodonAPIController, :favourites) - # Note: not present in Mastodon: bookmarks + get("/bookmarks", MastodonAPIController, :bookmarks) - post("/notifications/clear", MastodonAPIController, :clear_notifications) - post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) - get("/notifications", MastodonAPIController, :notifications) - get("/notifications/:id", MastodonAPIController, :get_notification) - get("/favourites", MastodonAPIController, :favourites) - get("/bookmarks", MastodonAPIController, :bookmarks) ++ get("/notifications", NotificationController, :index) ++ get("/notifications/:id", NotificationController, :show) ++ post("/notifications/clear", NotificationController, :clear) ++ post("/notifications/dismiss", NotificationController, :dismiss) ++ delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) - delete( - "/notifications/destroy_multiple", - MastodonAPIController, - :destroy_multiple_notifications - ) - - get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) - get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) - get("/notifications", NotificationController, :index) - get("/notifications/:id", NotificationController, :show) - post("/notifications/clear", NotificationController, :clear) - post("/notifications/dismiss", NotificationController, :dismiss) - delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) ++ get("/scheduled_statuses", ScheduledActivityController, :index) ++ get("/scheduled_statuses/:id", ScheduledActivityController, :show) - get("/scheduled_statuses", ScheduledActivityController, :index) - get("/scheduled_statuses/:id", ScheduledActivityController, :show) + get("/lists", ListController, :index) + get("/lists/:id", ListController, :show) + get("/lists/:id/accounts", ListController, :list_accounts) - get("/domain_blocks", MastodonAPIController, :domain_blocks) - get("/lists", ListController, :index) - get("/lists/:id", ListController, :show) - get("/lists/:id/accounts", ListController, :list_accounts) ++ get("/domain_blocks", DomainBlockController, :index) - get("/filters", MastodonAPIController, :get_filters) - get("/domain_blocks", DomainBlockController, :index) ++ get("/filters", FilterController, :index) - get("/filters", FilterController, :index) + get("/suggestions", MastodonAPIController, :suggestions) - get("/conversations", MastodonAPIController, :conversations) - post("/conversations/:id/read", MastodonAPIController, :conversation_read) - get("/suggestions", MastodonAPIController, :suggestions) ++ get("/conversations", ConversationController, :index) ++ post("/conversations/:id/read", ConversationController, :read) - get("/endorsements", MastodonAPIController, :endorsements) - get("/conversations", ConversationController, :index) - post("/conversations/:id/read", ConversationController, :read) ++ get("/endorsements", AccountController, :endorsements) - patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) - get("/endorsements", MastodonAPIController, :empty_array) - end ++ patch("/accounts/update_credentials", AccountController, :update_credentials) - post("/statuses", MastodonAPIController, :post_status) - delete("/statuses/:id", MastodonAPIController, :delete_status) - scope [] do - pipe_through(:oauth_write) - - patch("/accounts/update_credentials", AccountController, :update_credentials) ++ post("/statuses", StatusController, :create) ++ delete("/statuses/:id", StatusController, :delete) - post("/statuses/:id/reblog", MastodonAPIController, :reblog_status) - post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status) - post("/statuses/:id/favourite", MastodonAPIController, :fav_status) - post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status) - post("/statuses/:id/pin", MastodonAPIController, :pin_status) - post("/statuses/:id/unpin", MastodonAPIController, :unpin_status) - # Note: not present in Mastodon: bookmark - post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status) - # Note: not present in Mastodon: unbookmark - post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status) - post("/statuses/:id/mute", MastodonAPIController, :mute_conversation) - post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation) - post("/statuses", StatusController, :create) - delete("/statuses/:id", StatusController, :delete) ++ post("/statuses/:id/reblog", StatusController, :reblog) ++ post("/statuses/:id/unreblog", StatusController, :unreblog) ++ post("/statuses/:id/favourite", StatusController, :favourite) ++ post("/statuses/:id/unfavourite", StatusController, :unfavourite) ++ post("/statuses/:id/pin", StatusController, :pin) ++ post("/statuses/:id/unpin", StatusController, :unpin) ++ post("/statuses/:id/bookmark", StatusController, :bookmark) ++ post("/statuses/:id/unbookmark", StatusController, :unbookmark) ++ post("/statuses/:id/mute", StatusController, :mute_conversation) ++ post("/statuses/:id/unmute", StatusController, :unmute_conversation) - put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) - delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) - post("/statuses/:id/reblog", StatusController, :reblog) - post("/statuses/:id/unreblog", StatusController, :unreblog) - post("/statuses/:id/favourite", StatusController, :favourite) - post("/statuses/:id/unfavourite", StatusController, :unfavourite) - post("/statuses/:id/pin", StatusController, :pin) - post("/statuses/:id/unpin", StatusController, :unpin) - post("/statuses/:id/bookmark", StatusController, :bookmark) - post("/statuses/:id/unbookmark", StatusController, :unbookmark) - post("/statuses/:id/mute", StatusController, :mute_conversation) - post("/statuses/:id/unmute", StatusController, :unmute_conversation) ++ put("/scheduled_statuses/:id", ScheduledActivityController, :update) ++ delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) - put("/scheduled_statuses/:id", ScheduledActivityController, :update) - delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) + post("/polls/:id/votes", MastodonAPIController, :poll_vote) - post("/polls/:id/votes", MastodonAPIController, :poll_vote) + post("/media", MastodonAPIController, :upload) + put("/media/:id", MastodonAPIController, :update_media) - post("/media", MastodonAPIController, :upload) - put("/media/:id", MastodonAPIController, :update_media) + delete("/lists/:id", ListController, :delete) + post("/lists", ListController, :create) + put("/lists/:id", ListController, :update) - delete("/lists/:id", ListController, :delete) - post("/lists", ListController, :create) - put("/lists/:id", ListController, :update) + post("/lists/:id/accounts", ListController, :add_to_list) + delete("/lists/:id/accounts", ListController, :remove_from_list) - post("/filters", MastodonAPIController, :create_filter) - get("/filters/:id", MastodonAPIController, :get_filter) - put("/filters/:id", MastodonAPIController, :update_filter) - delete("/filters/:id", MastodonAPIController, :delete_filter) - - 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("/lists/:id/accounts", ListController, :add_to_list) - delete("/lists/:id/accounts", ListController, :remove_from_list) ++ post("/filters", FilterController, :create) ++ get("/filters/:id", FilterController, :show) ++ put("/filters/:id", FilterController, :update) ++ delete("/filters/:id", FilterController, :delete) - post("/reports", MastodonAPIController, :create_report) - post("/filters", FilterController, :create) - get("/filters/:id", FilterController, :show) - put("/filters/:id", FilterController, :update) - delete("/filters/:id", FilterController, :delete) ++ post("/reports", ReportController, :create) - post("/follows", MastodonAPIController, :follow) - post("/accounts/:id/follow", MastodonAPIController, :follow) - post("/reports", ReportController, :create) - end ++ # To do: POST /api/v1/follows is not present in Mastodon - consider removing ++ post("/follows", MastodonAPIController, :follows) - 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) - scope [] do - pipe_through(:oauth_follow) ++ 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", MastodonAPIController, :authorize_follow_request) - post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request) - 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", MastodonAPIController, :block_domain) - delete("/domain_blocks", MastodonAPIController, :unblock_domain) - - post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe) - post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe) - 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("/domain_blocks", DomainBlockController, :create) - delete("/domain_blocks", DomainBlockController, :delete) - end - - scope [] do - pipe_through(:oauth_push) - - post("/push/subscription", SubscriptionController, :create) - get("/push/subscription", SubscriptionController, :get) - put("/push/subscription", SubscriptionController, :update) - delete("/push/subscription", SubscriptionController, :delete) - end + post("/push/subscription", SubscriptionController, :create) + get("/push/subscription", SubscriptionController, :get) + put("/push/subscription", SubscriptionController, :update) + delete("/push/subscription", SubscriptionController, :delete) end scope "/api/web", Pleroma.Web.MastodonAPI do @@@ -381,30 -477,26 +424,22 @@@ get("/accounts/search", SearchController, :account_search) - post( - "/pleroma/accounts/confirmation_resend", - MastodonAPIController, - :account_confirmation_resend - ) - scope [] do - pipe_through(:oauth_read_or_public) -- - get("/timelines/public", MastodonAPIController, :public_timeline) - get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) - get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) - get("/timelines/public", TimelineController, :public) - get("/timelines/tag/:tag", TimelineController, :hashtag) - get("/timelines/list/:list_id", TimelineController, :list) ++ get("/timelines/public", TimelineController, :public) ++ get("/timelines/tag/:tag", TimelineController, :hashtag) ++ get("/timelines/list/:list_id", TimelineController, :list) - get("/statuses", MastodonAPIController, :get_statuses) - get("/statuses/:id", MastodonAPIController, :get_status) - get("/statuses/:id/context", MastodonAPIController, :get_context) - get("/statuses", StatusController, :index) - get("/statuses/:id", StatusController, :show) - get("/statuses/:id/context", StatusController, :context) ++ get("/statuses", StatusController, :index) ++ get("/statuses/:id", StatusController, :show) ++ get("/statuses/:id/context", StatusController, :context) - get("/polls/:id", MastodonAPIController, :get_poll) + get("/polls/:id", MastodonAPIController, :get_poll) - 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("/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) - end + get("/search", SearchController, :search) - - get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) end scope "/api/v2", Pleroma.Web.MastodonAPI do @@@ -504,11 -600,23 +539,14 @@@ scope "/", Pleroma.Web.ActivityPub do pipe_through([:activitypub_client]) - scope [] do - pipe_through(:oauth_read) - get("/api/ap/whoami", ActivityPubController, :whoami) - get("/users/:nickname/inbox", ActivityPubController, :read_inbox) - end + get("/api/ap/whoami", ActivityPubController, :whoami) + get("/users/:nickname/inbox", ActivityPubController, :read_inbox) + - scope [] do - pipe_through(:oauth_write) - post("/users/:nickname/outbox", ActivityPubController, :update_outbox) - post("/api/ap/upload_media", ActivityPubController, :upload_media) - end + post("/users/:nickname/outbox", ActivityPubController, :update_outbox) ++ post("/api/ap/upload_media", ActivityPubController, :upload_media) + - scope [] do - pipe_through(:oauth_read_or_public) - get("/users/:nickname/followers", ActivityPubController, :followers) - get("/users/:nickname/following", ActivityPubController, :following) - end + get("/users/:nickname/followers", ActivityPubController, :followers) + get("/users/:nickname/following", ActivityPubController, :following) end scope "/", Pleroma.Web.ActivityPub do diff --cc lib/pleroma/web/twitter_api/twitter_api_controller.ex index 42bd74eb5,5024ac70d..bf5a6ae42 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@@ -5,17 -5,13 +5,18 @@@ defmodule Pleroma.Web.TwitterAPI.Controller do use Pleroma.Web, :controller - alias Ecto.Changeset alias Pleroma.Notification + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TokenView require Logger + plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) + ++ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) ++ action_fallback(:errors) def confirm_email(conn, %{"user_id" => uid, "token" => token}) do