diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 59d91318a..b69b75b63 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -8,13 +8,19 @@ class Api::V1::AccountsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create] before_action :require_user!, except: [:show, :create] - before_action :set_account, except: [:create] + before_action :set_account, except: [:index, :create] before_action :check_enabled_registrations, only: [:create] skip_before_action :require_authenticated_user!, only: :create override_rate_limit_headers :follow, family: :follows + def index + accounts = Account.where(id: account_ids) + + render json: accounts, each_serializer: REST::AccountSerializer + end + def show render json: @account, serializer: REST::AccountSerializer end @@ -83,6 +89,10 @@ class Api::V1::AccountsController < Api::BaseController AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) end + def account_ids + Array(params[:id]).map(&:to_i) + end + def account_params params.permit(:username, :email, :password, :agreement, :locale, :reason) end diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 33b17fb65..f867a1918 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -18,6 +18,7 @@ class EmojiReaction < ApplicationRecord include Paginable after_commit :queue_publish + after_commit :refresh_status belongs_to :account belongs_to :status, inverse_of: :emoji_reactions @@ -37,4 +38,8 @@ class EmojiReaction < ApplicationRecord def queue_publish PublishEmojiReactionWorker.perform_async(status_id, name, custom_emoji_id) unless status.destroyed? end + + def refresh_status + status.refresh_grouped_emoji_reactions! unless status.destroyed? + end end diff --git a/app/models/status.rb b/app/models/status.rb index e98bc3ed8..3b5e6c9e0 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -129,6 +129,7 @@ class Status < ApplicationRecord cache_associated :application, :media_attachments, + :emoji_reactions, :conversation, :status_stat, :tags, @@ -141,6 +142,7 @@ class Status < ApplicationRecord :tags, :preview_cards, :media_attachments, + :emoji_reactions, :conversation, :status_stat, :preloadable_poll, @@ -290,17 +292,25 @@ class Status < ApplicationRecord status_stat&.favourites_count || 0 end - def grouped_reactions(account = nil) - records = begin - scope = emoji_reactions.group(:status_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC')) - if account.nil? - scope.select('name, custom_emoji_id, count(*) as count, false as me') - else - scope.select(ActiveRecord::Base.sanitize_sql_array(["name, custom_emoji_id, count(*) as count, exists(select 1 from emoji_reactions r where r.account_id = :account_id and r.status_id = emoji_reactions.status_id and r.name = emoji_reactions.name and (r.custom_emoji_id IS NULL and emoji_reactions.custom_emoji_id IS NULL or r.custom_emoji_id = emoji_reactions.custom_emoji_id)) as me", account_id: account.id])) + def grouped_emoji_reactions(account = nil) + (Oj.load(status_stat&.emoji_reactions_cache || '', mode: :strict) || []).tap do |emoji_reactions| + if account.present? + emoji_reactions.each do |emoji_reaction| + emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s) + end end end - ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) - records + end + + def generate_grouped_emoji_reactions + records = emoji_reactions.group(:status_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC')).select('name, custom_emoji_id, count(*) as count, array_agg(account_id::text order by created_at) as account_ids') + ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::EmojiReactionSerializer, scope: nil, scope_name: :current_user).to_json + end + + def refresh_grouped_emoji_reactions! + generate_grouped_emoji_reactions.tap do |emoji_reactions_cache| + update_status_stat!(emoji_reactions_cache: emoji_reactions_cache) + end end def increment_count!(key) diff --git a/app/models/status_stat.rb b/app/models/status_stat.rb index 024c467e7..538a849c4 100644 --- a/app/models/status_stat.rb +++ b/app/models/status_stat.rb @@ -3,13 +3,14 @@ # # Table name: status_stats # -# id :bigint(8) not null, primary key -# status_id :bigint(8) not null -# replies_count :bigint(8) default(0), not null -# reblogs_count :bigint(8) default(0), not null -# favourites_count :bigint(8) default(0), not null -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# status_id :bigint(8) not null +# replies_count :bigint(8) default(0), not null +# reblogs_count :bigint(8) default(0), not null +# favourites_count :bigint(8) default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# emoji_reactions_cache :string default(""), not null # class StatusStat < ApplicationRecord diff --git a/app/serializers/rest/emoji_reaction_serializer.rb b/app/serializers/rest/emoji_reaction_serializer.rb index 81c12e5b2..caeba97fa 100644 --- a/app/serializers/rest/emoji_reaction_serializer.rb +++ b/app/serializers/rest/emoji_reaction_serializer.rb @@ -1,4 +1,42 @@ # frozen_string_literal: true -class REST::EmojiReactionSerializer < REST::ReactionSerializer +class REST::EmojiReactionSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :count + + attribute :me, if: :current_user? + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + attribute :account_ids, if: :has_account_ids? + + def count + object.respond_to?(:count) ? object.count : 0 + end + + def current_user? + !current_user.nil? + end + + def custom_emoji? + object.custom_emoji.present? + end + + def has_account_ids? + object.respond_to?(:account_ids) + 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 + + def domain + object.custom_emoji.domain + end end + \ No newline at end of file diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 435ccfe16..58dd7e4e1 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility, :language, :uri, :url, :replies_count, :reblogs_count, - :favourites_count + :favourites_count, :emoji_reactions attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? @@ -30,7 +30,6 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :ordered_mentions, key: :mentions has_many :tags has_many :emojis, serializer: REST::CustomEmojiSerializer - has_many :emoji_reactions, serializer: REST::EmojiReactionSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer @@ -127,7 +126,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def emoji_reactions - object.grouped_reactions(current_user&.account) + object.grouped_emoji_reactions(current_user&.account) end def reblogged diff --git a/config/routes.rb b/config/routes.rb index b0ef1097d..915cb7cba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -465,7 +465,7 @@ Rails.application.routes.draw do resources :subscribing, only: :index, controller: 'subscribing_accounts' end - resources :accounts, only: [:create, :show] do + resources :accounts, only: [:index, :create, :show] do resources :statuses, only: :index, controller: 'accounts/statuses' resources :followers, only: :index, controller: 'accounts/follower_accounts' resources :following, only: :index, controller: 'accounts/following_accounts' diff --git a/db/migrate/20210628130510_add_emoji_reactions_json_to_status_stat.rb b/db/migrate/20210628130510_add_emoji_reactions_json_to_status_stat.rb new file mode 100644 index 000000000..6301f5b9d --- /dev/null +++ b/db/migrate/20210628130510_add_emoji_reactions_json_to_status_stat.rb @@ -0,0 +1,15 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddEmojiReactionsJsonToStatusStat < ActiveRecord::Migration[6.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured { add_column_with_default :status_stats, :emoji_reactions_cache, :string, default: '', allow_null: false } + end + + def down + remove_column :status_stats, :emoji_reactions_cache + end +end diff --git a/db/schema.rb b/db/schema.rb index d2ec7d19f..f1682b416 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -941,6 +941,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do t.bigint "favourites_count", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "emoji_reactions_cache", default: "", null: false t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true end diff --git a/lib/mastodon/cache_cli.rb b/lib/mastodon/cache_cli.rb index 3613bb151..b525664d5 100644 --- a/lib/mastodon/cache_cli.rb +++ b/lib/mastodon/cache_cli.rb @@ -20,6 +20,7 @@ module Mastodon option :concurrency, type: :numeric, default: 5, aliases: [:c] option :verbose, type: :boolean, aliases: [:v] + option :reaction_only, type: :boolean desc 'recount TYPE', 'Update hard-cached counters' long_desc <<~LONG_DESC Update hard-cached counters of TYPE by counting referenced @@ -41,11 +42,15 @@ module Mastodon account_stat.save if account_stat.changed? end when 'statuses' - processed, = parallelize_with_progress(Status.includes(:status_stat)) do |status| - status_stat = status.status_stat - status_stat.replies_count = status.replies.where.not(visibility: :direct).count - status_stat.reblogs_count = status.reblogs.count - status_stat.favourites_count = status.favourites.count + statuses = Status.includes(:status_stat) + statuses = statuses.joins(:emoji_reactions).distinct if options[:reaction_only] + + processed, = parallelize_with_progress(statuses) do |status| + status_stat = status.status_stat + status_stat.replies_count = status.replies.where.not(visibility: :direct).count + status_stat.reblogs_count = status.reblogs.count + status_stat.favourites_count = status.favourites.count + status_stat.emoji_reactions_cache = status.generate_grouped_emoji_reactions status_stat.save if status_stat.changed? end