Add emoji reaction cache
This commit is contained in:
parent
a115072b6a
commit
15c11f2952
10 changed files with 111 additions and 27 deletions
|
@ -8,13 +8,19 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||||
|
|
||||||
before_action :require_user!, except: [:show, :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]
|
before_action :check_enabled_registrations, only: [:create]
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!, only: :create
|
skip_before_action :require_authenticated_user!, only: :create
|
||||||
|
|
||||||
override_rate_limit_headers :follow, family: :follows
|
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
|
def show
|
||||||
render json: @account, serializer: REST::AccountSerializer
|
render json: @account, serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
@ -83,6 +89,10 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_ids
|
||||||
|
Array(params[:id]).map(&:to_i)
|
||||||
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,7 @@ class EmojiReaction < ApplicationRecord
|
||||||
include Paginable
|
include Paginable
|
||||||
|
|
||||||
after_commit :queue_publish
|
after_commit :queue_publish
|
||||||
|
after_commit :refresh_status
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :status, inverse_of: :emoji_reactions
|
belongs_to :status, inverse_of: :emoji_reactions
|
||||||
|
@ -37,4 +38,8 @@ class EmojiReaction < ApplicationRecord
|
||||||
def queue_publish
|
def queue_publish
|
||||||
PublishEmojiReactionWorker.perform_async(status_id, name, custom_emoji_id) unless status.destroyed?
|
PublishEmojiReactionWorker.perform_async(status_id, name, custom_emoji_id) unless status.destroyed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_status
|
||||||
|
status.refresh_grouped_emoji_reactions! unless status.destroyed?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -129,6 +129,7 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
cache_associated :application,
|
cache_associated :application,
|
||||||
:media_attachments,
|
:media_attachments,
|
||||||
|
:emoji_reactions,
|
||||||
:conversation,
|
:conversation,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:tags,
|
:tags,
|
||||||
|
@ -141,6 +142,7 @@ class Status < ApplicationRecord
|
||||||
:tags,
|
:tags,
|
||||||
:preview_cards,
|
:preview_cards,
|
||||||
:media_attachments,
|
:media_attachments,
|
||||||
|
:emoji_reactions,
|
||||||
:conversation,
|
:conversation,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
|
@ -290,17 +292,25 @@ class Status < ApplicationRecord
|
||||||
status_stat&.favourites_count || 0
|
status_stat&.favourites_count || 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def grouped_reactions(account = nil)
|
def grouped_emoji_reactions(account = nil)
|
||||||
records = begin
|
(Oj.load(status_stat&.emoji_reactions_cache || '', mode: :strict) || []).tap do |emoji_reactions|
|
||||||
scope = emoji_reactions.group(:status_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC'))
|
if account.present?
|
||||||
if account.nil?
|
emoji_reactions.each do |emoji_reaction|
|
||||||
scope.select('name, custom_emoji_id, count(*) as count, false as me')
|
emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s)
|
||||||
else
|
end
|
||||||
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]))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji)
|
end
|
||||||
records
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def increment_count!(key)
|
def increment_count!(key)
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
#
|
#
|
||||||
# Table name: status_stats
|
# Table name: status_stats
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# status_id :bigint(8) not null
|
# status_id :bigint(8) not null
|
||||||
# replies_count :bigint(8) default(0), not null
|
# replies_count :bigint(8) default(0), not null
|
||||||
# reblogs_count :bigint(8) default(0), not null
|
# reblogs_count :bigint(8) default(0), not null
|
||||||
# favourites_count :bigint(8) default(0), not null
|
# favourites_count :bigint(8) default(0), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# emoji_reactions_cache :string default(""), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusStat < ApplicationRecord
|
class StatusStat < ApplicationRecord
|
||||||
|
|
|
@ -1,4 +1,42 @@
|
||||||
# frozen_string_literal: true
|
# 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
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
|
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
|
||||||
:sensitive, :spoiler_text, :visibility, :language,
|
:sensitive, :spoiler_text, :visibility, :language,
|
||||||
:uri, :url, :replies_count, :reblogs_count,
|
:uri, :url, :replies_count, :reblogs_count,
|
||||||
:favourites_count
|
:favourites_count, :emoji_reactions
|
||||||
|
|
||||||
attribute :favourited, if: :current_user?
|
attribute :favourited, if: :current_user?
|
||||||
attribute :reblogged, 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 :ordered_mentions, key: :mentions
|
||||||
has_many :tags
|
has_many :tags
|
||||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||||
has_many :emoji_reactions, serializer: REST::EmojiReactionSerializer
|
|
||||||
|
|
||||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
|
@ -127,7 +126,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def emoji_reactions
|
def emoji_reactions
|
||||||
object.grouped_reactions(current_user&.account)
|
object.grouped_emoji_reactions(current_user&.account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reblogged
|
def reblogged
|
||||||
|
|
|
@ -465,7 +465,7 @@ Rails.application.routes.draw do
|
||||||
resources :subscribing, only: :index, controller: 'subscribing_accounts'
|
resources :subscribing, only: :index, controller: 'subscribing_accounts'
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :accounts, only: [:create, :show] do
|
resources :accounts, only: [:index, :create, :show] do
|
||||||
resources :statuses, only: :index, controller: 'accounts/statuses'
|
resources :statuses, only: :index, controller: 'accounts/statuses'
|
||||||
resources :followers, only: :index, controller: 'accounts/follower_accounts'
|
resources :followers, only: :index, controller: 'accounts/follower_accounts'
|
||||||
resources :following, only: :index, controller: 'accounts/following_accounts'
|
resources :following, only: :index, controller: 'accounts/following_accounts'
|
||||||
|
|
|
@ -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
|
|
@ -941,6 +941,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
|
||||||
t.bigint "favourites_count", default: 0, null: false
|
t.bigint "favourites_count", default: 0, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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
|
t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ module Mastodon
|
||||||
|
|
||||||
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
||||||
option :verbose, type: :boolean, aliases: [:v]
|
option :verbose, type: :boolean, aliases: [:v]
|
||||||
|
option :reaction_only, type: :boolean
|
||||||
desc 'recount TYPE', 'Update hard-cached counters'
|
desc 'recount TYPE', 'Update hard-cached counters'
|
||||||
long_desc <<~LONG_DESC
|
long_desc <<~LONG_DESC
|
||||||
Update hard-cached counters of TYPE by counting referenced
|
Update hard-cached counters of TYPE by counting referenced
|
||||||
|
@ -41,11 +42,15 @@ module Mastodon
|
||||||
account_stat.save if account_stat.changed?
|
account_stat.save if account_stat.changed?
|
||||||
end
|
end
|
||||||
when 'statuses'
|
when 'statuses'
|
||||||
processed, = parallelize_with_progress(Status.includes(:status_stat)) do |status|
|
statuses = Status.includes(:status_stat)
|
||||||
status_stat = status.status_stat
|
statuses = statuses.joins(:emoji_reactions).distinct if options[:reaction_only]
|
||||||
status_stat.replies_count = status.replies.where.not(visibility: :direct).count
|
|
||||||
status_stat.reblogs_count = status.reblogs.count
|
processed, = parallelize_with_progress(statuses) do |status|
|
||||||
status_stat.favourites_count = status.favourites.count
|
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?
|
status_stat.save if status_stat.changed?
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue