Use likes
and shares
totalItems on status creations and updates (#32620)
This commit is contained in:
parent
77cd16f4ee
commit
9074c1fac9
@ -53,6 +53,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
@status = Status.create!(@params)
|
@status = Status.create!(@params)
|
||||||
attach_tags(@status)
|
attach_tags(@status)
|
||||||
|
attach_counts(@status)
|
||||||
end
|
end
|
||||||
|
|
||||||
resolve_thread(@status)
|
resolve_thread(@status)
|
||||||
@ -166,6 +167,18 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_counts(status)
|
||||||
|
likes = @status_parser.favourites_count
|
||||||
|
shares = @status_parser.reblogs_count
|
||||||
|
return if likes.nil? && shares.nil?
|
||||||
|
|
||||||
|
status.status_stat.tap do |status_stat|
|
||||||
|
status_stat.untrusted_reblogs_count = shares unless shares.nil?
|
||||||
|
status_stat.untrusted_favourites_count = likes unless likes.nil?
|
||||||
|
status_stat.save if status_stat.changed?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def process_tags
|
def process_tags
|
||||||
return if @object['tag'].nil?
|
return if @object['tag'].nil?
|
||||||
|
|
||||||
|
@ -93,6 +93,14 @@ class ActivityPub::Parser::StatusParser
|
|||||||
lang.presence && NORMALIZED_LOCALE_NAMES.fetch(lang.downcase.to_sym, lang)
|
lang.presence && NORMALIZED_LOCALE_NAMES.fetch(lang.downcase.to_sym, lang)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def favourites_count
|
||||||
|
@object.dig(:likes, :totalItems)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reblogs_count
|
||||||
|
@object.dig(:shares, :totalItems)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def raw_language_code
|
def raw_language_code
|
||||||
|
@ -303,13 +303,35 @@ class Status < ApplicationRecord
|
|||||||
status_stat&.favourites_count || 0
|
status_stat&.favourites_count || 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Reblogs count received from an external instance
|
||||||
|
def untrusted_reblogs_count
|
||||||
|
status_stat&.untrusted_reblogs_count unless local?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Favourites count received from an external instance
|
||||||
|
def untrusted_favourites_count
|
||||||
|
status_stat&.untrusted_favourites_count unless local?
|
||||||
|
end
|
||||||
|
|
||||||
def increment_count!(key)
|
def increment_count!(key)
|
||||||
|
if key == :favourites_count && !untrusted_favourites_count.nil?
|
||||||
|
update_status_stat!(favourites_count: favourites_count + 1, untrusted_favourites_count: untrusted_favourites_count + 1)
|
||||||
|
elsif key == :reblogs_count && !untrusted_reblogs_count.nil?
|
||||||
|
update_status_stat!(reblogs_count: reblogs_count + 1, untrusted_reblogs_count: untrusted_reblogs_count + 1)
|
||||||
|
else
|
||||||
update_status_stat!(key => public_send(key) + 1)
|
update_status_stat!(key => public_send(key) + 1)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def decrement_count!(key)
|
def decrement_count!(key)
|
||||||
|
if key == :favourites_count && !untrusted_favourites_count.nil?
|
||||||
|
update_status_stat!(favourites_count: [favourites_count - 1, 0].max, untrusted_favourites_count: [untrusted_favourites_count - 1, 0].max)
|
||||||
|
elsif key == :reblogs_count && !untrusted_reblogs_count.nil?
|
||||||
|
update_status_stat!(reblogs_count: [reblogs_count - 1, 0].max, untrusted_reblogs_count: [untrusted_reblogs_count - 1, 0].max)
|
||||||
|
else
|
||||||
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def trendable?
|
def trendable?
|
||||||
if attributes['trendable'].nil?
|
if attributes['trendable'].nil?
|
||||||
|
@ -11,11 +11,17 @@
|
|||||||
# 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
|
||||||
|
# untrusted_favourites_count :bigint(8)
|
||||||
|
# untrusted_reblogs_count :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusStat < ApplicationRecord
|
class StatusStat < ApplicationRecord
|
||||||
belongs_to :status, inverse_of: :status_stat
|
belongs_to :status, inverse_of: :status_stat
|
||||||
|
|
||||||
|
before_validation :clamp_untrusted_counts
|
||||||
|
|
||||||
|
MAX_UNTRUSTED_COUNT = 100_000_000
|
||||||
|
|
||||||
def replies_count
|
def replies_count
|
||||||
[attributes['replies_count'], 0].max
|
[attributes['replies_count'], 0].max
|
||||||
end
|
end
|
||||||
@ -27,4 +33,11 @@ class StatusStat < ApplicationRecord
|
|||||||
def favourites_count
|
def favourites_count
|
||||||
[attributes['favourites_count'], 0].max
|
[attributes['favourites_count'], 0].max
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def clamp_untrusted_counts
|
||||||
|
self.untrusted_favourites_count = untrusted_favourites_count.to_i.clamp(0, MAX_UNTRUSTED_COUNT) if untrusted_favourites_count.present?
|
||||||
|
self.untrusted_reblogs_count = untrusted_reblogs_count.to_i.clamp(0, MAX_UNTRUSTED_COUNT) if untrusted_reblogs_count.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -84,11 +84,11 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reblogs_count
|
def reblogs_count
|
||||||
relationships&.attributes_map&.dig(object.id, :reblogs_count) || object.reblogs_count
|
object.untrusted_reblogs_count || relationships&.attributes_map&.dig(object.id, :reblogs_count) || object.reblogs_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def favourites_count
|
def favourites_count
|
||||||
relationships&.attributes_map&.dig(object.id, :favourites_count) || object.favourites_count
|
object.untrusted_favourites_count || relationships&.attributes_map&.dig(object.id, :favourites_count) || object.favourites_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def favourited
|
def favourited
|
||||||
|
@ -43,6 +43,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||||||
update_poll!
|
update_poll!
|
||||||
update_immediate_attributes!
|
update_immediate_attributes!
|
||||||
update_metadata!
|
update_metadata!
|
||||||
|
update_counts!
|
||||||
create_edits!
|
create_edits!
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||||||
with_redis_lock("create:#{@uri}") do
|
with_redis_lock("create:#{@uri}") do
|
||||||
update_poll!(allow_significant_changes: false)
|
update_poll!(allow_significant_changes: false)
|
||||||
queue_poll_notifications!
|
queue_poll_notifications!
|
||||||
|
update_counts!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -239,6 +241,19 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_counts!
|
||||||
|
likes = @status_parser.favourites_count
|
||||||
|
shares = @status_parser.reblogs_count
|
||||||
|
return if likes.nil? && shares.nil?
|
||||||
|
|
||||||
|
@status.status_stat.tap do |status_stat|
|
||||||
|
status_stat.untrusted_reblogs_count = shares unless shares.nil?
|
||||||
|
status_stat.untrusted_favourites_count = likes unless likes.nil?
|
||||||
|
|
||||||
|
status_stat.save if status_stat.changed?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def expected_type?
|
def expected_type?
|
||||||
equals_or_includes_any?(@json['type'], %w(Note Question))
|
equals_or_includes_any?(@json['type'], %w(Note Question))
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddUntrustedFavouritesCountAndUntrustedReblogsCountToStatusStat < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :status_stats, :untrusted_favourites_count, :bigint, null: true
|
||||||
|
add_column :status_stats, :untrusted_reblogs_count, :bigint, null: true
|
||||||
|
end
|
||||||
|
end
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_10_14_010506) do
|
ActiveRecord::Schema[7.1].define(version: 2024_10_22_214312) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@ -1008,6 +1008,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_14_010506) do
|
|||||||
t.bigint "favourites_count", default: 0, null: false
|
t.bigint "favourites_count", default: 0, null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.bigint "untrusted_favourites_count"
|
||||||
|
t.bigint "untrusted_reblogs_count"
|
||||||
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
|
||||||
|
|
||||||
|
@ -928,6 +928,32 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
expect(poll.votes.first).to be_nil
|
expect(poll.votes.first).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with counts' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
likes: {
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar', '/likes'].join,
|
||||||
|
type: 'Collection',
|
||||||
|
totalItems: 50,
|
||||||
|
},
|
||||||
|
shares: {
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar', '/shares'].join,
|
||||||
|
type: 'Collection',
|
||||||
|
totalItems: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses the counts from the created object' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status.untrusted_favourites_count).to eq 50
|
||||||
|
expect(status.untrusted_reblogs_count).to eq 100
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when object URI uses bearcaps' do
|
context 'when object URI uses bearcaps' do
|
||||||
|
@ -115,5 +115,69 @@ RSpec.describe ActivityPub::Activity::Update do
|
|||||||
expect(status.edited_at).to be_nil
|
expect(status.edited_at).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a Note object' do
|
||||||
|
let(:updated) { nil }
|
||||||
|
let(:favourites) { 50 }
|
||||||
|
let(:reblogs) { 100 }
|
||||||
|
|
||||||
|
let!(:status) { Fabricate(:status, uri: 'https://example.com/statuses/poll', account: sender) }
|
||||||
|
let(:json) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Update',
|
||||||
|
actor: sender.uri,
|
||||||
|
object: {
|
||||||
|
type: 'Note',
|
||||||
|
id: status.uri,
|
||||||
|
content: 'Foo',
|
||||||
|
updated: updated,
|
||||||
|
likes: {
|
||||||
|
id: "#{status.uri}/likes",
|
||||||
|
type: 'Collection',
|
||||||
|
totalItems: favourites,
|
||||||
|
},
|
||||||
|
shares: {
|
||||||
|
id: "#{status.uri}/shares",
|
||||||
|
type: 'Collection',
|
||||||
|
totalItems: reblogs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'updates counts' do
|
||||||
|
it 'updates the reblog count' do
|
||||||
|
expect(status.untrusted_reblogs_count).to eq reblogs
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the favourites count' do
|
||||||
|
expect(status.untrusted_favourites_count).to eq favourites
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an implicit update' do
|
||||||
|
before do
|
||||||
|
status.update!(uri: ActivityPub::TagManager.instance.uri_for(status))
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'updates counts'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an explicit update' do
|
||||||
|
let(:favourites) { 150 }
|
||||||
|
let(:reblogs) { 200 }
|
||||||
|
let(:updated) { Time.now.utc.iso8601 }
|
||||||
|
|
||||||
|
before do
|
||||||
|
status.update!(uri: ActivityPub::TagManager.instance.uri_for(status))
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'updates counts'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -164,6 +164,31 @@ RSpec.describe Status do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#untrusted_reblogs_count' do
|
||||||
|
before do
|
||||||
|
alice.update(domain: 'example.com')
|
||||||
|
subject.status_stat.tap do |status_stat|
|
||||||
|
status_stat.untrusted_reblogs_count = 0
|
||||||
|
status_stat.save
|
||||||
|
end
|
||||||
|
subject.save
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is incremented by the number of reblogs' do
|
||||||
|
Fabricate(:status, account: bob, reblog: subject)
|
||||||
|
Fabricate(:status, account: alice, reblog: subject)
|
||||||
|
|
||||||
|
expect(subject.untrusted_reblogs_count).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is decremented when reblog is removed' do
|
||||||
|
reblog = Fabricate(:status, account: bob, reblog: subject)
|
||||||
|
expect(subject.untrusted_reblogs_count).to eq 1
|
||||||
|
reblog.destroy
|
||||||
|
expect(subject.untrusted_reblogs_count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#replies_count' do
|
describe '#replies_count' do
|
||||||
it 'is the number of replies' do
|
it 'is the number of replies' do
|
||||||
Fabricate(:status, account: bob, thread: subject)
|
Fabricate(:status, account: bob, thread: subject)
|
||||||
@ -194,6 +219,31 @@ RSpec.describe Status do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#untrusted_favourites_count' do
|
||||||
|
before do
|
||||||
|
alice.update(domain: 'example.com')
|
||||||
|
subject.status_stat.tap do |status_stat|
|
||||||
|
status_stat.untrusted_favourites_count = 0
|
||||||
|
status_stat.save
|
||||||
|
end
|
||||||
|
subject.save
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is incremented by favorites' do
|
||||||
|
Fabricate(:favourite, account: bob, status: subject)
|
||||||
|
Fabricate(:favourite, account: alice, status: subject)
|
||||||
|
|
||||||
|
expect(subject.untrusted_favourites_count).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is decremented when favourite is removed' do
|
||||||
|
favourite = Fabricate(:favourite, account: bob, status: subject)
|
||||||
|
expect(subject.untrusted_favourites_count).to eq 1
|
||||||
|
favourite.destroy
|
||||||
|
expect(subject.untrusted_favourites_count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#proper' do
|
describe '#proper' do
|
||||||
it 'is itself for original statuses' do
|
it 'is itself for original statuses' do
|
||||||
expect(subject.proper).to eq subject
|
expect(subject.proper).to eq subject
|
||||||
|
@ -39,6 +39,42 @@ RSpec.describe 'API V1 Trends Statuses' do
|
|||||||
end
|
end
|
||||||
Trends::Statuses.new(threshold: 1, decay_threshold: -1).refresh
|
Trends::Statuses.new(threshold: 1, decay_threshold: -1).refresh
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a comically inflated external interactions count' do
|
||||||
|
def prepare_fake_trends
|
||||||
|
fake_remote_account = Fabricate(:account, domain: 'other.com')
|
||||||
|
fake_status = Fabricate(:status, account: fake_remote_account, text: 'I am a big faker', trendable: true, language: 'en')
|
||||||
|
fake_status.status_stat.tap do |status_stat|
|
||||||
|
status_stat.reblogs_count = 0
|
||||||
|
status_stat.favourites_count = 0
|
||||||
|
status_stat.untrusted_reblogs_count = 1_000_000_000
|
||||||
|
status_stat.untrusted_favourites_count = 1_000_000_000
|
||||||
|
status_stat.save
|
||||||
|
end
|
||||||
|
real_remote_account = Fabricate(:account, domain: 'other.com')
|
||||||
|
real_status = Fabricate(:status, account: real_remote_account, text: 'I make real friends online', trendable: true, language: 'en')
|
||||||
|
real_status.status_stat.tap do |status_stat|
|
||||||
|
status_stat.reblogs_count = 10
|
||||||
|
status_stat.favourites_count = 10
|
||||||
|
status_stat.untrusted_reblogs_count = 10
|
||||||
|
status_stat.untrusted_favourites_count = 10
|
||||||
|
status_stat.save
|
||||||
|
end
|
||||||
|
Trends.statuses.add(fake_status, 100)
|
||||||
|
Trends.statuses.add(real_status, 101)
|
||||||
|
Trends::Statuses.new(threshold: 1, decay_threshold: 1).refresh
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores the feeble attempts at deception' do
|
||||||
|
prepare_fake_trends
|
||||||
|
stub_const('Api::BaseController::DEFAULT_STATUSES_LIMIT', 10)
|
||||||
|
get '/api/v1/trends/statuses'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.parsed_body.length).to eq(1)
|
||||||
|
expect(response.parsed_body[0]['content']).to eq('I make real friends online')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
55
spec/serializers/rest/status_serializer_spec.rb
Normal file
55
spec/serializers/rest/status_serializer_spec.rb
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe REST::StatusSerializer do
|
||||||
|
subject do
|
||||||
|
serialized_record_json(
|
||||||
|
status,
|
||||||
|
described_class,
|
||||||
|
options: {
|
||||||
|
scope: current_user,
|
||||||
|
scope_name: :current_user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:current_user) { Fabricate(:user) }
|
||||||
|
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||||
|
let(:bob) { Fabricate(:account, username: 'bob', domain: 'other.com') }
|
||||||
|
let(:status) { Fabricate(:status, account: alice) }
|
||||||
|
|
||||||
|
context 'with a remote status' do
|
||||||
|
let(:status) { Fabricate(:status, account: bob) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
status.status_stat.tap do |status_stat|
|
||||||
|
status_stat.reblogs_count = 10
|
||||||
|
status_stat.favourites_count = 20
|
||||||
|
status_stat.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with only trusted counts' do
|
||||||
|
it 'shows the trusted counts' do
|
||||||
|
expect(subject['reblogs_count']).to eq(10)
|
||||||
|
expect(subject['favourites_count']).to eq(20)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with untrusted counts' do
|
||||||
|
before do
|
||||||
|
status.status_stat.tap do |status_stat|
|
||||||
|
status_stat.untrusted_reblogs_count = 30
|
||||||
|
status_stat.untrusted_favourites_count = 40
|
||||||
|
status_stat.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the untrusted counts' do
|
||||||
|
expect(subject['reblogs_count']).to eq(30)
|
||||||
|
expect(subject['favourites_count']).to eq(40)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -34,8 +34,8 @@ RSpec.configure do |config|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialized_record_json(record, serializer, adapter: nil)
|
def serialized_record_json(record, serializer, adapter: nil, options: {})
|
||||||
options = { serializer: serializer }
|
options[:serializer] = serializer
|
||||||
options[:adapter] = adapter if adapter.present?
|
options[:adapter] = adapter if adapter.present?
|
||||||
JSON.parse(
|
JSON.parse(
|
||||||
ActiveModelSerializers::SerializableResource.new(
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
|
Loading…
Reference in New Issue
Block a user