Add status references support
This commit is contained in:
parent
af161fa66b
commit
22ad776635
24 changed files with 321 additions and 7 deletions
|
@ -109,6 +109,7 @@ Metrics/ParameterLists:
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/policies/status_policy.rb'
|
- 'app/policies/status_policy.rb'
|
||||||
|
- 'app/services/post_status_service.rb'
|
||||||
|
|
||||||
# Reason: Prevailing style is argument file paths
|
# Reason: Prevailing style is argument file paths
|
||||||
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
|
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
|
||||||
|
|
|
@ -143,6 +143,7 @@ const excludeTypesFromFilter = filter => {
|
||||||
'favourite',
|
'favourite',
|
||||||
'emoji_reaction',
|
'emoji_reaction',
|
||||||
'reblog',
|
'reblog',
|
||||||
|
'status_reference',
|
||||||
'mention',
|
'mention',
|
||||||
'poll',
|
'poll',
|
||||||
'status',
|
'status',
|
||||||
|
|
|
@ -152,6 +152,17 @@ export default class ColumnSettings extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role='group' aria-labelledby='notifications-status_reference'>
|
||||||
|
<span id='notifications-status_reference' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status_reference' defaultMessage='References:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status_reference']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'status_reference']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'status_reference']} onChange={onChange} label={showStr} />
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status_reference']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-poll'>
|
<div role='group' aria-labelledby='notifications-poll'>
|
||||||
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ const tooltips = defineMessages({
|
||||||
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
|
||||||
emojiReactions: { id: 'notifications.filter.emoji_reactions', defaultMessage: 'Stamps' },
|
emojiReactions: { id: 'notifications.filter.emoji_reactions', defaultMessage: 'Stamps' },
|
||||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||||
|
status_references: { id: 'notifications.filter.status_references', defaultMessage: 'Status references' },
|
||||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||||
|
@ -90,6 +91,13 @@ class FilterBar extends PureComponent {
|
||||||
>
|
>
|
||||||
<Icon id='retweet' fixedWidth />
|
<Icon id='retweet' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedFilter === 'status_reference' ? 'active' : ''}
|
||||||
|
onClick={this.onClick('status_reference')}
|
||||||
|
title={intl.formatMessage(tooltips.status_references)}
|
||||||
|
>
|
||||||
|
<Icon id='retweet' fixedWidth />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'poll' ? 'active' : ''}
|
className={selectedFilter === 'poll' ? 'active' : ''}
|
||||||
onClick={this.onClick('poll')}
|
onClick={this.onClick('poll')}
|
||||||
|
|
|
@ -28,6 +28,7 @@ const messages = defineMessages({
|
||||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||||
|
statusReference: { id: 'notification.status_reference', defaultMessage: '{name} refered' },
|
||||||
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
|
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
|
||||||
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
|
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
|
||||||
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
|
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
|
||||||
|
@ -288,6 +289,39 @@ class Notification extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderStatusReference (notification, link) {
|
||||||
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='retweet' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.status_reference' defaultMessage='{name} referenced your status' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
withoutEmojiReactions
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderStatus (notification, link) {
|
renderStatus (notification, link) {
|
||||||
const { intl, unread, status } = this.props;
|
const { intl, unread, status } = this.props;
|
||||||
|
|
||||||
|
@ -479,6 +513,8 @@ class Notification extends ImmutablePureComponent {
|
||||||
return this.renderEmojiReaction(notification, link);
|
return this.renderEmojiReaction(notification, link);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
|
case 'status_reference':
|
||||||
|
return this.renderStatusReference(notification, link);
|
||||||
case 'status':
|
case 'status':
|
||||||
return this.renderStatus(notification, link);
|
return this.renderStatus(notification, link);
|
||||||
case 'update':
|
case 'update':
|
||||||
|
|
|
@ -415,6 +415,7 @@
|
||||||
"notification.poll": "アンケートが終了しました",
|
"notification.poll": "アンケートが終了しました",
|
||||||
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
||||||
"notification.status": "{name}さんが投稿しました",
|
"notification.status": "{name}さんが投稿しました",
|
||||||
|
"notification.status_reference": "{name}さんがあなたの投稿を参照しました",
|
||||||
"notification.update": "{name}さんが投稿を編集しました",
|
"notification.update": "{name}さんが投稿を編集しました",
|
||||||
"notifications.clear": "通知を消去",
|
"notifications.clear": "通知を消去",
|
||||||
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
||||||
|
|
|
@ -97,6 +97,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
fetch_replies(@status)
|
fetch_replies(@status)
|
||||||
distribute
|
distribute
|
||||||
forward_for_reply
|
forward_for_reply
|
||||||
|
process_references!
|
||||||
join_group!
|
join_group!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -471,6 +472,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_references!
|
||||||
|
references = []
|
||||||
|
references = ActivityPub::FetchReferencesService(@json['references']) unless @json['references'].nil?
|
||||||
|
ProcessReferencesWorker.perform_async(@status.id, [], urls: references)
|
||||||
|
end
|
||||||
|
|
||||||
def join_group!
|
def join_group!
|
||||||
GroupReblogService.new.call(@status)
|
GroupReblogService.new.call(@status)
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Notification < ApplicationRecord
|
||||||
'FollowRequest' => :follow_request,
|
'FollowRequest' => :follow_request,
|
||||||
'Favourite' => :favourite,
|
'Favourite' => :favourite,
|
||||||
'EmojiReaction' => :emoji_reaction,
|
'EmojiReaction' => :emoji_reaction,
|
||||||
|
'StatusReference' => :status_reference,
|
||||||
'Poll' => :poll,
|
'Poll' => :poll,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ class Notification < ApplicationRecord
|
||||||
mention
|
mention
|
||||||
status
|
status
|
||||||
reblog
|
reblog
|
||||||
|
status_reference
|
||||||
follow
|
follow
|
||||||
follow_request
|
follow_request
|
||||||
favourite
|
favourite
|
||||||
|
@ -47,6 +49,7 @@ class Notification < ApplicationRecord
|
||||||
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
||||||
status: :status,
|
status: :status,
|
||||||
reblog: [status: :reblog],
|
reblog: [status: :reblog],
|
||||||
|
status_reference: [status_reference: :status],
|
||||||
mention: [mention: :status],
|
mention: [mention: :status],
|
||||||
favourite: [favourite: :status],
|
favourite: [favourite: :status],
|
||||||
emoji_reaction: [emoji_reaction: :status],
|
emoji_reaction: [emoji_reaction: :status],
|
||||||
|
@ -67,6 +70,7 @@ class Notification < ApplicationRecord
|
||||||
belongs_to :follow_request, inverse_of: :notification
|
belongs_to :follow_request, inverse_of: :notification
|
||||||
belongs_to :favourite, inverse_of: :notification
|
belongs_to :favourite, inverse_of: :notification
|
||||||
belongs_to :emoji_reaction, inverse_of: :notification
|
belongs_to :emoji_reaction, inverse_of: :notification
|
||||||
|
belongs_to :status_reference, inverse_of: :notification
|
||||||
belongs_to :poll, inverse_of: false
|
belongs_to :poll, inverse_of: false
|
||||||
belongs_to :report, inverse_of: false
|
belongs_to :report, inverse_of: false
|
||||||
end
|
end
|
||||||
|
@ -85,6 +89,8 @@ class Notification < ApplicationRecord
|
||||||
status
|
status
|
||||||
when :reblog
|
when :reblog
|
||||||
status&.reblog
|
status&.reblog
|
||||||
|
when :status_reference
|
||||||
|
status_reference&.status
|
||||||
when :favourite
|
when :favourite
|
||||||
favourite&.status
|
favourite&.status
|
||||||
when :emoji_reaction, :reaction
|
when :emoji_reaction, :reaction
|
||||||
|
@ -136,6 +142,8 @@ class Notification < ApplicationRecord
|
||||||
notification.status = cached_status
|
notification.status = cached_status
|
||||||
when :reblog
|
when :reblog
|
||||||
notification.status.reblog = cached_status
|
notification.status.reblog = cached_status
|
||||||
|
when :status_reference
|
||||||
|
notification.status_reference.status = cached_status
|
||||||
when :favourite
|
when :favourite
|
||||||
notification.favourite.status = cached_status
|
notification.favourite.status = cached_status
|
||||||
when :emoji_reaction, :reaction
|
when :emoji_reaction, :reaction
|
||||||
|
@ -162,7 +170,7 @@ class Notification < ApplicationRecord
|
||||||
case activity_type
|
case activity_type
|
||||||
when 'Status', 'Follow', 'Favourite', 'EmojiReaction', 'EmojiReact', 'FollowRequest', 'Poll', 'Report'
|
when 'Status', 'Follow', 'Favourite', 'EmojiReaction', 'EmojiReact', 'FollowRequest', 'Poll', 'Report'
|
||||||
self.from_account_id = activity&.account_id
|
self.from_account_id = activity&.account_id
|
||||||
when 'Mention'
|
when 'Mention', 'StatusReference'
|
||||||
self.from_account_id = activity&.status&.account_id
|
self.from_account_id = activity&.status&.account_id
|
||||||
when 'Account'
|
when 'Account'
|
||||||
self.from_account_id = activity&.id
|
self.from_account_id = activity&.id
|
||||||
|
|
|
@ -75,6 +75,10 @@ class Status < ApplicationRecord
|
||||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||||
has_many :media_attachments, dependent: :nullify
|
has_many :media_attachments, dependent: :nullify
|
||||||
|
has_many :reference_objects, class_name: 'StatusReference', inverse_of: :status
|
||||||
|
has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status
|
||||||
|
has_many :referenced_by_status_objects, foreign_key: 'target_status_id', class_name: 'StatusReference', inverse_of: :target_status
|
||||||
|
has_many :referenced_by_statuses, through: :referenced_by_status_objects, class_name: 'Status', source: :status
|
||||||
|
|
||||||
has_and_belongs_to_many :tags
|
has_and_belongs_to_many :tags
|
||||||
has_and_belongs_to_many :preview_cards
|
has_and_belongs_to_many :preview_cards
|
||||||
|
@ -332,6 +336,10 @@ class Status < ApplicationRecord
|
||||||
status_stat&.emoji_reaction_accounts_count || 0
|
status_stat&.emoji_reaction_accounts_count || 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_referred_by_count
|
||||||
|
status_stat&.status_referred_by_count || 0
|
||||||
|
end
|
||||||
|
|
||||||
def increment_count!(key)
|
def increment_count!(key)
|
||||||
update_status_stat!(key => public_send(key) + 1)
|
update_status_stat!(key => public_send(key) + 1)
|
||||||
end
|
end
|
||||||
|
@ -340,6 +348,10 @@ class Status < ApplicationRecord
|
||||||
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_status_referred_by_count!(diff)
|
||||||
|
update_status_stat!(status_referred_by_count: [public_send(:status_referred_by_count) + diff, 0].max)
|
||||||
|
end
|
||||||
|
|
||||||
def emoji_reactions_grouped_by_name(account = nil)
|
def emoji_reactions_grouped_by_name(account = nil)
|
||||||
(Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions|
|
(Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions|
|
||||||
if account.present?
|
if account.present?
|
||||||
|
|
25
app/models/status_reference.rb
Normal file
25
app/models/status_reference.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: status_references
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# status_id :bigint(8) not null
|
||||||
|
# target_status_id :bigint(8) not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class StatusReference < ApplicationRecord
|
||||||
|
belongs_to :status
|
||||||
|
belongs_to :target_status, class_name: 'Status'
|
||||||
|
|
||||||
|
has_one :notification, as: :activity, dependent: :destroy
|
||||||
|
|
||||||
|
validate :validate_status_visibilities
|
||||||
|
|
||||||
|
def validate_status_visibilities
|
||||||
|
raise Mastodon::ValidationError, I18n.t('status_references.errors.invalid_status_visibilities') if [:public, :public_unlisted, :unlisted, :login].exclude?(target_status.visibility.to_sym)
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,6 +15,7 @@
|
||||||
# emoji_reactions_count :integer default(0), not null
|
# emoji_reactions_count :integer default(0), not null
|
||||||
# test :integer default(0), not null
|
# test :integer default(0), not null
|
||||||
# emoji_reaction_accounts_count :integer default(0), not null
|
# emoji_reaction_accounts_count :integer default(0), not null
|
||||||
|
# status_referred_by_count :integer default(0), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusStat < ApplicationRecord
|
class StatusStat < ApplicationRecord
|
||||||
|
@ -46,6 +47,10 @@ class StatusStat < ApplicationRecord
|
||||||
[attributes['emoji_reaction_accounts_count'], 0].max
|
[attributes['emoji_reaction_accounts_count'], 0].max
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_referred_by_count
|
||||||
|
[attributes['status_referred_by_count'], 0].max
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reset_parent_cache
|
def reset_parent_cache
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
# sign_up_ip :inet
|
# sign_up_ip :inet
|
||||||
# role_id :bigint(8)
|
# role_id :bigint(8)
|
||||||
# settings :text
|
# settings :text
|
||||||
|
# time_zone :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
|
|
@ -117,6 +117,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
:kmyblue_markdown,
|
:kmyblue_markdown,
|
||||||
:kmyblue_reaction_deck,
|
:kmyblue_reaction_deck,
|
||||||
:kmyblue_visibility_login,
|
:kmyblue_visibility_login,
|
||||||
|
:status_reference,
|
||||||
]
|
]
|
||||||
|
|
||||||
capabilities << :profile_search unless Chewy.enabled?
|
capabilities << :profile_search unless Chewy.enabled?
|
||||||
|
|
|
@ -13,7 +13,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_type?
|
def status_type?
|
||||||
[:favourite, :emoji_reaction, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
[:favourite, :emoji_reaction, :reaction, :reblog, :status_reference, :status, :mention, :poll, :update].include?(object.type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_type?
|
def report_type?
|
||||||
|
|
|
@ -6,6 +6,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, :visibility_ex, :language,
|
:sensitive, :spoiler_text, :visibility, :visibility_ex, :language,
|
||||||
:uri, :url, :replies_count, :reblogs_count, :searchability, :markdown,
|
:uri, :url, :replies_count, :reblogs_count, :searchability, :markdown,
|
||||||
|
:status_reference_ids, :status_references_count, :status_referred_by_count,
|
||||||
:favourites_count, :emoji_reactions, :emoji_reactions_count, :reactions, :edited_at
|
:favourites_count, :emoji_reactions, :emoji_reactions_count, :reactions, :edited_at
|
||||||
|
|
||||||
attribute :favourited, if: :current_user?
|
attribute :favourited, if: :current_user?
|
||||||
|
@ -92,6 +93,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
ActivityPub::TagManager.instance.url_for(object)
|
ActivityPub::TagManager.instance.url_for(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_reference_ids
|
||||||
|
@status_reference_ids = object.reference_objects.pluck(:target_status_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def status_references_count
|
||||||
|
status_reference_ids.size
|
||||||
|
end
|
||||||
|
|
||||||
def favourited
|
def favourited
|
||||||
if instance_options && instance_options[:relationships]
|
if instance_options && instance_options[:relationships]
|
||||||
instance_options[:relationships].favourites_map[object.id] || false
|
instance_options[:relationships].favourites_map[object.id] || false
|
||||||
|
|
|
@ -126,6 +126,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
||||||
:kmyblue_markdown,
|
:kmyblue_markdown,
|
||||||
:kmyblue_reaction_deck,
|
:kmyblue_reaction_deck,
|
||||||
:kmyblue_visibility_login,
|
:kmyblue_visibility_login,
|
||||||
|
:status_reference,
|
||||||
]
|
]
|
||||||
|
|
||||||
capabilities << :profile_search unless Chewy.enabled?
|
capabilities << :profile_search unless Chewy.enabled?
|
||||||
|
|
49
app/services/activitypub/fetch_references_service.rb
Normal file
49
app/services/activitypub/fetch_references_service.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::FetchReferencesService < BaseService
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
def call(status, collection_or_uri)
|
||||||
|
@account = status.account
|
||||||
|
|
||||||
|
collection_items(collection_or_uri)&.map { |item| value_or_id(item) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collection_items(collection_or_uri)
|
||||||
|
collection = fetch_collection(collection_or_uri)
|
||||||
|
return unless collection.is_a?(Hash) && collection['first'].present?
|
||||||
|
|
||||||
|
all_items = []
|
||||||
|
collection = fetch_collection(collection['first'])
|
||||||
|
|
||||||
|
while collection.is_a?(Hash)
|
||||||
|
items = begin
|
||||||
|
case collection['type']
|
||||||
|
when 'Collection', 'CollectionPage'
|
||||||
|
collection['items']
|
||||||
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
|
collection['orderedItems']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
break if items.blank?
|
||||||
|
|
||||||
|
all_items.concat(items)
|
||||||
|
|
||||||
|
break if all_items.size >= StatusReferenceValidator::LIMIT
|
||||||
|
|
||||||
|
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
all_items
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_collection(collection_or_uri)
|
||||||
|
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||||
|
return if invalid_origin?(collection_or_uri)
|
||||||
|
|
||||||
|
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||||
|
end
|
||||||
|
end
|
|
@ -34,6 +34,7 @@ class PostStatusService < BaseService
|
||||||
# @option [String] :idempotency Optional idempotency key
|
# @option [String] :idempotency Optional idempotency key
|
||||||
# @option [Boolean] :with_rate_limit
|
# @option [Boolean] :with_rate_limit
|
||||||
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
|
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
|
||||||
|
# @option [Enumerable] :status_reference_ids Optional array
|
||||||
# @return [Status]
|
# @return [Status]
|
||||||
def call(account, options = {})
|
def call(account, options = {})
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -78,6 +79,7 @@ class PostStatusService < BaseService
|
||||||
@markdown = @options[:markdown] || false
|
@markdown = @options[:markdown] || false
|
||||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||||
@scheduled_at = nil if scheduled_in_the_past?
|
@scheduled_at = nil if scheduled_in_the_past?
|
||||||
|
@reference_ids = (@options[:status_reference_ids] || []).map(&:to_i).filter(&:positive?)
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
raise ActiveRecord::RecordInvalid
|
raise ActiveRecord::RecordInvalid
|
||||||
end
|
end
|
||||||
|
@ -146,6 +148,7 @@ class PostStatusService < BaseService
|
||||||
|
|
||||||
def postprocess_status!
|
def postprocess_status!
|
||||||
process_hashtags_service.call(@status)
|
process_hashtags_service.call(@status)
|
||||||
|
ProcessReferencesWorker.perform_async(@status.id, @reference_ids)
|
||||||
Trends.tags.register(@status)
|
Trends.tags.register(@status)
|
||||||
LinkCrawlWorker.perform_async(@status.id)
|
LinkCrawlWorker.perform_async(@status.id)
|
||||||
DistributionWorker.perform_async(@status.id)
|
DistributionWorker.perform_async(@status.id)
|
||||||
|
@ -221,6 +224,7 @@ class PostStatusService < BaseService
|
||||||
media_attachments: @media || [],
|
media_attachments: @media || [],
|
||||||
ordered_media_attachment_ids: (@options[:media_ids] || []).map(&:to_i) & @media.map(&:id),
|
ordered_media_attachment_ids: (@options[:media_ids] || []).map(&:to_i) & @media.map(&:id),
|
||||||
thread: @in_reply_to,
|
thread: @in_reply_to,
|
||||||
|
status_reference_ids: @status_reference_ids,
|
||||||
poll_attributes: poll_attributes,
|
poll_attributes: poll_attributes,
|
||||||
sensitive: @sensitive,
|
sensitive: @sensitive,
|
||||||
spoiler_text: @options[:spoiler_text] || '',
|
spoiler_text: @options[:spoiler_text] || '',
|
||||||
|
|
86
app/services/process_references_service.rb
Normal file
86
app/services/process_references_service.rb
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ProcessReferencesService < BaseService
|
||||||
|
include Payloadable
|
||||||
|
|
||||||
|
DOMAIN = ENV['WEB_DOMAIN'] || ENV.fetch('LOCAL_DOMAIN', nil)
|
||||||
|
REFURL_EXP = /(RT|QT|BT|RN)((:)? +|:)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
|
||||||
|
STATUSID_EXP = %r{(http|https)://#{DOMAIN}/@[a-zA-Z0-9]+/([0-9]{16,})}
|
||||||
|
|
||||||
|
def call(status, reference_parameters, save_records: true, urls: nil)
|
||||||
|
@status = status
|
||||||
|
@reference_parameters = reference_parameters || []
|
||||||
|
@save_records = save_records
|
||||||
|
@urls = urls || []
|
||||||
|
|
||||||
|
old_references
|
||||||
|
|
||||||
|
StatusReference.transaction do
|
||||||
|
remove_old_references
|
||||||
|
add_references
|
||||||
|
|
||||||
|
@status.save! if @save_records
|
||||||
|
end
|
||||||
|
|
||||||
|
create_notifications!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def references
|
||||||
|
@references = @reference_parameters + scan_text!
|
||||||
|
end
|
||||||
|
|
||||||
|
def old_references
|
||||||
|
@old_references = @status.references.pluck(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def added_references
|
||||||
|
(references - old_references).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def removed_references
|
||||||
|
(old_references - references).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def scan_text!
|
||||||
|
text = @status.text.gsub(%r{</?[^>]*>}, '')
|
||||||
|
@scan_text = fetch_statuses!(text.scan(REFURL_EXP).pluck(3).uniq).map(&:id).uniq.filter { |status_id| !status_id.zero? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_statuses!(urls)
|
||||||
|
(urls + @urls)
|
||||||
|
.map { |url| ResolveURLService.new.call(url) }
|
||||||
|
.filter { |status| status }
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_references
|
||||||
|
return if added_references.empty?
|
||||||
|
|
||||||
|
@added_objects = []
|
||||||
|
|
||||||
|
statuses = Status.where(id: added_references)
|
||||||
|
statuses.each do |status|
|
||||||
|
@added_objects << @status.reference_objects.new(target_status: status)
|
||||||
|
status.increment_count!(:status_referred_by_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_notifications!
|
||||||
|
return if @added_objects.empty?
|
||||||
|
|
||||||
|
LocalNotificationWorker.push_bulk(@added_objects) do |ref|
|
||||||
|
[ref.target_status.account_id, ref.id, 'StatusReference', 'status_reference']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_old_references
|
||||||
|
return if removed_references.empty?
|
||||||
|
|
||||||
|
statuses = Status.where(id: removed_references)
|
||||||
|
@status.reference_objects.where(target_status: statuses).destroy_all
|
||||||
|
statuses.each do |status|
|
||||||
|
status.decrement_count!(:status_referred_by_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -142,6 +142,7 @@ class UpdateStatusService < BaseService
|
||||||
def update_metadata!
|
def update_metadata!
|
||||||
ProcessHashtagsService.new.call(@status)
|
ProcessHashtagsService.new.call(@status)
|
||||||
ProcessMentionsService.new.call(@status)
|
ProcessMentionsService.new.call(@status)
|
||||||
|
ProcessReferencesWorker.perform_async(@status.id, (@options[:status_reference_ids] || []).map(&:to_i).filter(&:positive?))
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast_updates!
|
def broadcast_updates!
|
||||||
|
|
11
app/workers/process_references_worker.rb
Normal file
11
app/workers/process_references_worker.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ProcessReferencesWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
def perform(status_id, ids, urls: nil)
|
||||||
|
ProcessReferencesService.new.call(Status.find(status_id), ids || [], urls: urls)
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
12
db/migrate/20230705232953_create_status_references.rb
Normal file
12
db/migrate/20230705232953_create_status_references.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateStatusReferences < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :status_references do |t|
|
||||||
|
t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade }
|
||||||
|
t.belongs_to :target_status, null: false, foreign_key: { on_delete: :cascade, to_table: :statuses }
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
t.datetime :updated_at, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddStatusReferredByCountToStatusStats < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
add_column :status_stats, :status_referred_by_count, :integer, null: false, default: 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :status_stats, :status_referred_by_count
|
||||||
|
end
|
||||||
|
end
|
21
db/schema.rb
21
db/schema.rb
|
@ -1,5 +1,3 @@
|
||||||
# rubocop:disable all
|
|
||||||
|
|
||||||
# This file is auto-generated from the current state of the database. Instead
|
# This file is auto-generated from the current state of the database. Instead
|
||||||
# of editing this file, please use the migrations feature of Active Record to
|
# of editing this file, please use the migrations feature of Active Record to
|
||||||
# incrementally modify your database, and then regenerate this schema definition.
|
# incrementally modify your database, and then regenerate this schema definition.
|
||||||
|
@ -12,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.define(version: 2023_06_05_085710) do
|
ActiveRecord::Schema.define(version: 2023_07_06_031715) 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"
|
||||||
|
@ -346,6 +344,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
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.bigint "dump_file_size"
|
t.bigint "dump_file_size"
|
||||||
|
t.index ["user_id"], name: "index_backups_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "blocks", force: :cascade do |t|
|
create_table "blocks", force: :cascade do |t|
|
||||||
|
@ -804,6 +803,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
t.bigint "owner_id"
|
t.bigint "owner_id"
|
||||||
t.boolean "confidential", default: true, null: false
|
t.boolean "confidential", default: true, null: false
|
||||||
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
|
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
|
||||||
|
t.index ["superapp"], name: "index_oauth_applications_on_superapp", where: "(superapp = true)"
|
||||||
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
|
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1041,6 +1041,15 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
t.index ["status_id"], name: "index_status_pins_on_status_id"
|
t.index ["status_id"], name: "index_status_pins_on_status_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "status_references", force: :cascade do |t|
|
||||||
|
t.bigint "status_id", null: false
|
||||||
|
t.bigint "target_status_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["status_id"], name: "index_status_references_on_status_id"
|
||||||
|
t.index ["target_status_id"], name: "index_status_references_on_target_status_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "status_stats", force: :cascade do |t|
|
create_table "status_stats", force: :cascade do |t|
|
||||||
t.bigint "status_id", null: false
|
t.bigint "status_id", null: false
|
||||||
t.bigint "replies_count", default: 0, null: false
|
t.bigint "replies_count", default: 0, null: false
|
||||||
|
@ -1052,6 +1061,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
t.integer "emoji_reactions_count", default: 0, null: false
|
t.integer "emoji_reactions_count", default: 0, null: false
|
||||||
t.integer "test", default: 0, null: false
|
t.integer "test", default: 0, null: false
|
||||||
t.integer "emoji_reaction_accounts_count", default: 0, null: false
|
t.integer "emoji_reaction_accounts_count", default: 0, null: false
|
||||||
|
t.integer "status_referred_by_count", default: 0, 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
|
||||||
|
|
||||||
|
@ -1211,6 +1221,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
t.boolean "skip_sign_in_token"
|
t.boolean "skip_sign_in_token"
|
||||||
t.bigint "role_id"
|
t.bigint "role_id"
|
||||||
t.text "settings"
|
t.text "settings"
|
||||||
|
t.string "time_zone"
|
||||||
t.index ["account_id"], name: "index_users_on_account_id"
|
t.index ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
|
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
|
||||||
|
@ -1374,6 +1385,8 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
add_foreign_key "status_edits", "statuses", on_delete: :cascade
|
add_foreign_key "status_edits", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade
|
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade
|
||||||
add_foreign_key "status_pins", "statuses", on_delete: :cascade
|
add_foreign_key "status_pins", "statuses", on_delete: :cascade
|
||||||
|
add_foreign_key "status_references", "statuses", column: "target_status_id", on_delete: :cascade
|
||||||
|
add_foreign_key "status_references", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "status_stats", "statuses", on_delete: :cascade
|
add_foreign_key "status_stats", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "status_trends", "accounts", on_delete: :cascade
|
add_foreign_key "status_trends", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "status_trends", "statuses", on_delete: :cascade
|
add_foreign_key "status_trends", "statuses", on_delete: :cascade
|
||||||
|
@ -1491,5 +1504,3 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
add_index "follow_recommendations", ["account_id"], name: "index_follow_recommendations_on_account_id", unique: true
|
add_index "follow_recommendations", ["account_id"], name: "index_follow_recommendations_on_account_id", unique: true
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop:enable all
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue