[#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon...
[akkoma] / lib / pleroma / web / mastodon_api / controllers / mastodon_api_controller.ex
index d532ba685985d2ea94cf996b9c0e746f0e6ad66f..b1e9dee3d03f70726c371ac19c41b686ab2ff23e 100644 (file)
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
 
   import Pleroma.Web.ControllerHelper,
-    only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
+    only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
 
   alias Ecto.Changeset
   alias Pleroma.Activity
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Pagination
+  alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
@@ -52,6 +53,190 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   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:favourites"]} when action in [:fav_status, :unfav_status]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in [:get_filters, :get_filter])
+
+  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: ["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]
+  )
+
+  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
+         ]
+  )
+
+  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
+  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
@@ -147,6 +332,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       [
         :no_rich_text,
         :locked,
+        :hide_followers_count,
+        :hide_follows_count,
         :hide_followers,
         :hide_follows,
         :hide_favorites,
@@ -290,7 +477,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   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),
+    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)
@@ -365,7 +552,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:home_timeline, activities)
+    |> add_link_headers(activities)
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -384,13 +571,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
+    |> add_link_headers(activities, %{"local" => local_only})
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
 
   def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
-    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
+    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
       params =
         params
         |> Map.put("tag", params["tagged"])
@@ -398,7 +585,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
 
       conn
-      |> add_link_headers(:user_statuses, activities, params["id"])
+      |> add_link_headers(activities)
       |> put_view(StatusView)
       |> render("index.json", %{
         activities: activities,
@@ -420,14 +607,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       [user.ap_id]
       |> ActivityPub.fetch_activities_query(params)
       |> Pagination.fetch_paginated(params)
-      |> Map.get(:items)
 
     conn
-    |> add_link_headers(:dm_timeline, activities)
+    |> add_link_headers(activities)
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
 
+  def get_statuses(%{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))
+
+    conn
+    |> put_view(StatusView)
+    |> render("index.json", activities: activities, for: user, as: :activity)
+  end
+
   def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
          true <- Visibility.visible_for_user?(activity, user) do
@@ -472,7 +672,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with %Object{} = object <- Object.get_by_id(id),
+    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
@@ -524,7 +724,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
       conn
-      |> add_link_headers(:scheduled_statuses, scheduled_activities)
+      |> add_link_headers(scheduled_activities)
       |> put_view(ScheduledActivityView)
       |> render("index.json", %{scheduled_activities: scheduled_activities})
     end
@@ -707,7 +907,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     notifications = MastodonAPI.get_notifications(user, params)
 
     conn
-    |> add_link_headers(:notifications, notifications)
+    |> add_link_headers(notifications)
     |> put_view(NotificationView)
     |> render("index.json", %{notifications: notifications, for: user})
   end
@@ -741,7 +941,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+  def destroy_multiple_notifications(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
     Notification.destroy_multiple(user, ids)
     json(conn, %{})
   end
@@ -829,6 +1029,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
   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
       q = from(u in User, where: u.ap_id in ^likes)
 
@@ -840,12 +1041,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(AccountView)
       |> render("accounts.json", %{for: user, users: users, as: :user})
     else
+      {:visible, false} -> {:error, :not_found}
       _ -> json(conn, [])
     end
   end
 
   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
       q = from(u in User, where: u.ap_id in ^announces)
 
@@ -857,6 +1060,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(AccountView)
       |> render("accounts.json", %{for: user, users: users, as: :user})
     else
+      {:visible, false} -> {:error, :not_found}
       _ -> json(conn, [])
     end
   end
@@ -895,7 +1099,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
+    |> add_link_headers(activities, %{"local" => local_only})
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -911,7 +1115,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         end
 
       conn
-      |> add_link_headers(:followers, followers, user)
+      |> add_link_headers(followers)
       |> put_view(AccountView)
       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
     end
@@ -928,7 +1132,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         end
 
       conn
-      |> add_link_headers(:following, followers, user)
+      |> add_link_headers(followers)
       |> put_view(AccountView)
       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
     end
@@ -1153,7 +1357,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:favourites, activities)
+    |> add_link_headers(activities)
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -1180,7 +1384,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         |> Enum.reverse()
 
       conn
-      |> add_link_headers(:favourites, activities)
+      |> add_link_headers(activities)
       |> put_view(StatusView)
       |> render("index.json", %{activities: activities, for: for_user, as: :activity})
     else
@@ -1195,14 +1399,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     bookmarks =
       Bookmark.for_user_query(user.id)
       |> Pagination.fetch_paginated(params)
-      |> Map.get(:items)
 
     activities =
       bookmarks
       |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
 
     conn
-    |> add_link_headers(:bookmarks, bookmarks)
+    |> add_link_headers(bookmarks)
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -1454,6 +1657,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, %{})
   end
 
+  def endorsements(conn, params), do: empty_array(conn, params)
+
   def get_filters(%{assigns: %{user: user}} = conn, _) do
     filters = Filter.get_filters(user)
     res = FilterView.render("filters.json", filters: filters)
@@ -1576,7 +1781,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def reports(%{assigns: %{user: user}} = conn, params) do
+  def create_report(%{assigns: %{user: user}} = conn, params) do
     case CommonAPI.report(user, params) do
       {:ok, activity} ->
         conn
@@ -1642,7 +1847,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       end)
 
     conn
-    |> add_link_headers(:conversations, participations)
+    |> add_link_headers(participations)
     |> json(conversations)
   end