From 9862e1591e6c2a915d11d9a57d5564fc0822f9d3 Mon Sep 17 00:00:00 2001 From: Essem Date: Wed, 24 Jan 2024 22:03:51 -0600 Subject: [PATCH] Add list of reactions to API --- .../api/v1/statuses/reactions_controller.rb | 83 ++++++++++++++++++- app/models/status_reaction.rb | 2 + app/serializers/rest/reaction_serializer.rb | 14 ---- .../rest/status_reaction_serializer.rb | 50 +++++++++++ app/serializers/rest/status_serializer.rb | 2 +- config/routes/api.rb | 1 + 6 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 app/serializers/rest/status_reaction_serializer.rb diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb index c4b0fa307f..9b25dfebe2 100644 --- a/app/controllers/api/v1/statuses/reactions_controller.rb +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -1,8 +1,18 @@ # frozen_string_literal: true class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:favourites' } - before_action :require_user! + REACTIONS_LIMIT = 30 + + before_action -> { doorkeeper_authorize! :write, :'write:favourites' }, only: [:create, :destroy] + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: [:index] + before_action :require_user!, only: [:create, :destroy] + before_action :set_reactions, only: [:index] + after_action :insert_pagination_headers, only: [:index] + + def index + cache_if_unauthenticated! + render json: @reactions, each_serializer: REST::StatusReactionSerializer + end def create ReactService.new.call(current_account, @status, params[:id]) @@ -16,4 +26,73 @@ class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController rescue Mastodon::NotPermittedError not_found end + + private + + def set_reactions + @reactions = ordered_reactions.select( + [:id, :account_id, :name, :custom_emoji_id].tap do |values| + values << value_for_reaction_me_column(current_account) + end + ).to_a_paginated_by_id( + limit_param(REACTIONS_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + end + + def ordered_reactions + StatusReaction.where(status: @status) + .group(:status_id, :id, :account_id, :name, :custom_emoji_id).order( + Arel.sql('MIN(created_at)').asc + ) + end + + def value_for_reaction_me_column(account) + if account.nil? + 'FALSE AS me' + else + <<~SQL.squish + EXISTS( + SELECT 1 + FROM status_reactions inner_reactions + WHERE inner_reactions.account_id = #{account.id} + AND inner_reactions.status_id = status_reactions.status_id + AND inner_reactions.name = status_reactions.name + AND ( + inner_reactions.custom_emoji_id = status_reactions.custom_emoji_id + OR inner_reactions.custom_emoji_id IS NULL + AND status_reactions.custom_emoji_id IS NULL + ) + ) AS me + SQL + end + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_status_reactions_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_status_reactions_url pagination_params(since_id: pagination_since_id) unless @reactions.empty? + end + + def pagination_max_id + @reactions.last.id + end + + def pagination_since_id + @reactions.first.id + end + + def records_continue? + @reactions.size == limit_param(REACTIONS_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/models/status_reaction.rb b/app/models/status_reaction.rb index 2fdb3da0ac..f206282d58 100644 --- a/app/models/status_reaction.rb +++ b/app/models/status_reaction.rb @@ -13,6 +13,8 @@ # updated_at :datetime not null # class StatusReaction < ApplicationRecord + include Paginable + belongs_to :account belongs_to :status, inverse_of: :status_reactions belongs_to :custom_emoji, optional: true diff --git a/app/serializers/rest/reaction_serializer.rb b/app/serializers/rest/reaction_serializer.rb index b0f0732bf7..1a5dca0183 100644 --- a/app/serializers/rest/reaction_serializer.rb +++ b/app/serializers/rest/reaction_serializer.rb @@ -21,14 +21,6 @@ class REST::ReactionSerializer < ActiveModel::Serializer object.custom_emoji.present? end - def name - if extern? - [object.name, '@', object.custom_emoji.domain].join - else - object.name - end - end - def url full_asset_url(object.custom_emoji.image.url) end @@ -36,10 +28,4 @@ class REST::ReactionSerializer < ActiveModel::Serializer def static_url full_asset_url(object.custom_emoji.image.url(:static)) end - - private - - def extern? - custom_emoji? && object.custom_emoji.domain.present? - end end diff --git a/app/serializers/rest/status_reaction_serializer.rb b/app/serializers/rest/status_reaction_serializer.rb new file mode 100644 index 0000000000..cdf779f3a0 --- /dev/null +++ b/app/serializers/rest/status_reaction_serializer.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class REST::StatusReactionSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name + + attribute :me, if: :current_user? + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :count, if: :respond_to_count? + + belongs_to :account, serializer: REST::AccountSerializer, unless: :respond_to_count? + + delegate :count, to: :object + + def respond_to_count? + object.respond_to?(:count) + end + + def current_user? + !current_user.nil? + end + + def custom_emoji? + object.custom_emoji.present? + end + + def name + if extern? + [object.name, '@', object.custom_emoji.domain].join + else + object.name + end + end + + def url + full_asset_url(object.custom_emoji.image.url) + end + + def static_url + full_asset_url(object.custom_emoji.image.url(:static)) + end + + private + + def extern? + custom_emoji? && object.custom_emoji.domain.present? + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index a5058bdb10..59c4c89d11 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -28,7 +28,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :ordered_mentions, key: :mentions has_many :tags has_many :emojis, serializer: REST::CustomEmojiSerializer - has_many :reactions, serializer: REST::ReactionSerializer + has_many :reactions, serializer: REST::StatusReactionSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer diff --git a/config/routes/api.rb b/config/routes/api.rb index 50df7dc6d2..1a68b697bc 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -10,6 +10,7 @@ namespace :api, format: false do scope module: :statuses do resources :reblogged_by, controller: :reblogged_by_accounts, only: :index resources :favourited_by, controller: :favourited_by_accounts, only: :index + resources :reactions, controller: :reactions, only: :index resource :reblog, only: :create post :unreblog, to: 'reblogs#destroy'