commit
91045bef61
|
@ -9,11 +9,25 @@ class Settings::ExportsController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize :backup, :create?
|
raise Mastodon::NotPermittedError unless user_signed_in?
|
||||||
|
|
||||||
|
backup = nil
|
||||||
|
|
||||||
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
|
if lock.acquired?
|
||||||
|
authorize :backup, :create?
|
||||||
backup = current_user.backups.create!
|
backup = current_user.backups.create!
|
||||||
|
else
|
||||||
|
raise Mastodon::RaceConditionError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
BackupWorker.perform_async(backup.id)
|
BackupWorker.perform_async(backup.id)
|
||||||
|
|
||||||
redirect_to settings_export_path
|
redirect_to settings_export_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock_options
|
||||||
|
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -106,7 +106,7 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification
|
||||||
|
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
const excludeTypesFromFilter = filter => {
|
||||||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
|
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,13 @@ export default class StatusPrepend extends React.PureComponent {
|
||||||
values={{ name : link }}
|
values={{ name : link }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'poll':
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.poll'
|
||||||
|
defaultMessage='Your poll has ended'
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +82,7 @@ export default class StatusPrepend extends React.PureComponent {
|
||||||
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
|
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
|
||||||
<i
|
<i
|
||||||
className={`fa fa-fw fa-${
|
className={`fa fa-fw fa-${
|
||||||
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : 'retweet')
|
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet'))
|
||||||
} status__prepend-icon`}
|
} status__prepend-icon`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -108,6 +108,27 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
withDismiss
|
withDismiss
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'poll':
|
||||||
|
return (
|
||||||
|
<StatusContainer
|
||||||
|
containerId={notification.get('id')}
|
||||||
|
hidden={hidden}
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
prepend='poll'
|
||||||
|
muted
|
||||||
|
notification={notification}
|
||||||
|
onMoveDown={onMoveDown}
|
||||||
|
onMoveUp={onMoveUp}
|
||||||
|
onMention={onMention}
|
||||||
|
getScrollPosition={getScrollPosition}
|
||||||
|
updateScrollBottom={updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
onUnmount={this.props.onUnmount}
|
||||||
|
withDismiss
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ const initialState = ImmutableMap({
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
poll: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
quickFilter: ImmutableMap({
|
quickFilter: ImmutableMap({
|
||||||
|
@ -46,6 +47,7 @@ const initialState = ImmutableMap({
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
poll: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sounds: ImmutableMap({
|
sounds: ImmutableMap({
|
||||||
|
@ -53,6 +55,7 @@ const initialState = ImmutableMap({
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
poll: true,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
const excludeTypesFromFilter = filter => {
|
||||||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
|
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -205,6 +205,38 @@ class Notification extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPoll (notification) {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.poll', defaultMessage: 'Your poll has ended' }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='tasks' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.poll' defaultMessage='Your poll has ended' />
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
|
@ -220,6 +252,8 @@ class Notification extends ImmutablePureComponent {
|
||||||
return this.renderFavourite(notification, link);
|
return this.renderFavourite(notification, link);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
|
case 'poll':
|
||||||
|
return this.renderPoll(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -245,6 +245,7 @@
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your status",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
"notification.mention": "{name} mentioned you",
|
"notification.mention": "{name} mentioned you",
|
||||||
|
"notification.poll": "Your poll has ended",
|
||||||
"notification.reblog": "{name} boosted your status",
|
"notification.reblog": "{name} boosted your status",
|
||||||
"notifications.clear": "Clear notifications",
|
"notifications.clear": "Clear notifications",
|
||||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||||
|
|
|
@ -31,6 +31,7 @@ const initialState = ImmutableMap({
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
poll: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
quickFilter: ImmutableMap({
|
quickFilter: ImmutableMap({
|
||||||
|
@ -44,6 +45,7 @@ const initialState = ImmutableMap({
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
poll: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sounds: ImmutableMap({
|
sounds: ImmutableMap({
|
||||||
|
@ -51,6 +53,7 @@ const initialState = ImmutableMap({
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
poll: true,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ filenames.forEach(filename => {
|
||||||
'notification.follow': full['notification.follow'] || '',
|
'notification.follow': full['notification.follow'] || '',
|
||||||
'notification.mention': full['notification.mention'] || '',
|
'notification.mention': full['notification.mention'] || '',
|
||||||
'notification.reblog': full['notification.reblog'] || '',
|
'notification.reblog': full['notification.reblog'] || '',
|
||||||
|
'notification.poll': full['notification.poll'] || '',
|
||||||
|
|
||||||
'status.show_more': full['status.show_more'] || '',
|
'status.show_more': full['status.show_more'] || '',
|
||||||
'status.reblog': full['status.reblog'] || '',
|
'status.reblog': full['status.reblog'] || '',
|
||||||
|
|
|
@ -243,6 +243,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name'])
|
return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name'])
|
||||||
return true if replied_to_status.poll.expired?
|
return true if replied_to_status.poll.expired?
|
||||||
replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id'])
|
replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id'])
|
||||||
|
ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.poll.hide_totals
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_thread(status)
|
def resolve_thread(status)
|
||||||
|
|
|
@ -5,6 +5,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
update_account if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
|
update_account if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
|
||||||
|
update_poll if equals_or_includes_any?(@object['type'], %w(Question))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -14,4 +15,14 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||||
|
|
||||||
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true)
|
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_poll
|
||||||
|
return reject_payload! if invalid_origin?(@object['id'])
|
||||||
|
status = Status.find_by(uri: object_uri, account_id: @account.id)
|
||||||
|
return if status.nil? || status.poll_id.nil?
|
||||||
|
poll = Poll.find(status.poll_id)
|
||||||
|
return if poll.nil?
|
||||||
|
|
||||||
|
ActivityPub::ProcessPollService.new.call(poll, @object)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,6 +22,7 @@ class Notification < ApplicationRecord
|
||||||
follow: 'Follow',
|
follow: 'Follow',
|
||||||
follow_request: 'FollowRequest',
|
follow_request: 'FollowRequest',
|
||||||
favourite: 'Favourite',
|
favourite: 'Favourite',
|
||||||
|
poll: 'Poll',
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
STATUS_INCLUDES = [:account, :application, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :media_attachments, :tags, active_mentions: :account]].freeze
|
STATUS_INCLUDES = [:account, :application, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :media_attachments, :tags, active_mentions: :account]].freeze
|
||||||
|
@ -35,6 +36,7 @@ class Notification < ApplicationRecord
|
||||||
belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id', optional: true
|
belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id', optional: true
|
||||||
belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id', optional: true
|
belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id', optional: true
|
||||||
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
||||||
|
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
||||||
|
|
||||||
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
||||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
||||||
|
@ -44,7 +46,7 @@ class Notification < ApplicationRecord
|
||||||
where(activity_type: types)
|
where(activity_type: types)
|
||||||
}
|
}
|
||||||
|
|
||||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
|
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, poll: [status: STATUS_INCLUDES]
|
||||||
|
|
||||||
def type
|
def type
|
||||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||||
|
@ -58,6 +60,8 @@ class Notification < ApplicationRecord
|
||||||
favourite&.status
|
favourite&.status
|
||||||
when :mention
|
when :mention
|
||||||
mention&.status
|
mention&.status
|
||||||
|
when :poll
|
||||||
|
poll&.status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -97,7 +101,7 @@ class Notification < ApplicationRecord
|
||||||
return unless new_record?
|
return unless new_record?
|
||||||
|
|
||||||
case activity_type
|
case activity_type
|
||||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest'
|
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll'
|
||||||
self.from_account_id = activity&.account_id
|
self.from_account_id = activity&.account_id
|
||||||
when 'Mention'
|
when 'Mention'
|
||||||
self.from_account_id = activity&.status&.account_id
|
self.from_account_id = activity&.status&.account_id
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::UpdatePollSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :type, :actor, :to
|
||||||
|
|
||||||
|
has_one :object, serializer: ActivityPub::NoteSerializer
|
||||||
|
|
||||||
|
def id
|
||||||
|
[ActivityPub::TagManager.instance.uri_for(object), '#updates/', object.poll.updated_at.to_i].join
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Update'
|
||||||
|
end
|
||||||
|
|
||||||
|
def actor
|
||||||
|
ActivityPub::TagManager.instance.uri_for(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to
|
||||||
|
ActivityPub::TagManager.instance.to(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cc
|
||||||
|
ActivityPub::TagManager.instance.cc(object)
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_type?
|
def status_type?
|
||||||
[:favourite, :reblog, :mention].include?(object.type)
|
[:favourite, :reblog, :mention, :poll].include?(object.type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,54 +4,7 @@ class ActivityPub::FetchRemotePollService < BaseService
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
|
||||||
def call(poll, on_behalf_of = nil)
|
def call(poll, on_behalf_of = nil)
|
||||||
@json = fetch_resource(poll.status.uri, true, on_behalf_of)
|
json = fetch_resource(poll.status.uri, true, on_behalf_of)
|
||||||
|
ActivityPub::ProcessPollService.new.call(poll, json)
|
||||||
return unless supported_context? && expected_type?
|
|
||||||
|
|
||||||
expires_at = begin
|
|
||||||
if @json['closed'].is_a?(String)
|
|
||||||
@json['closed']
|
|
||||||
elsif !@json['closed'].nil? && !@json['closed'].is_a?(FalseClass)
|
|
||||||
Time.now.utc
|
|
||||||
else
|
|
||||||
@json['endTime']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
items = begin
|
|
||||||
if @json['anyOf'].is_a?(Array)
|
|
||||||
@json['anyOf']
|
|
||||||
else
|
|
||||||
@json['oneOf']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
latest_options = items.map { |item| item['name'].presence || item['content'] }
|
|
||||||
|
|
||||||
# If for some reasons the options were changed, it invalidates all previous
|
|
||||||
# votes, so we need to remove them
|
|
||||||
poll.votes.delete_all if latest_options != poll.options
|
|
||||||
|
|
||||||
begin
|
|
||||||
poll.update!(
|
|
||||||
last_fetched_at: Time.now.utc,
|
|
||||||
expires_at: expires_at,
|
|
||||||
options: latest_options,
|
|
||||||
cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
|
|
||||||
)
|
|
||||||
rescue ActiveRecord::StaleObjectError
|
|
||||||
poll.reload
|
|
||||||
retry
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def supported_context?
|
|
||||||
super(@json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def expected_type?
|
|
||||||
equals_or_includes_any?(@json['type'], %w(Question))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::ProcessPollService < BaseService
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
def call(poll, json)
|
||||||
|
@json = json
|
||||||
|
return unless supported_context? && expected_type?
|
||||||
|
|
||||||
|
previous_expires_at = poll.expires_at
|
||||||
|
|
||||||
|
expires_at = begin
|
||||||
|
if @json['closed'].is_a?(String)
|
||||||
|
@json['closed']
|
||||||
|
elsif !@json['closed'].nil? && !@json['closed'].is_a?(FalseClass)
|
||||||
|
Time.now.utc
|
||||||
|
else
|
||||||
|
@json['endTime']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
items = begin
|
||||||
|
if @json['anyOf'].is_a?(Array)
|
||||||
|
@json['anyOf']
|
||||||
|
else
|
||||||
|
@json['oneOf']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
latest_options = items.map { |item| item['name'].presence || item['content'] }
|
||||||
|
|
||||||
|
# If for some reasons the options were changed, it invalidates all previous
|
||||||
|
# votes, so we need to remove them
|
||||||
|
poll.votes.delete_all if latest_options != poll.options
|
||||||
|
|
||||||
|
begin
|
||||||
|
poll.update!(
|
||||||
|
last_fetched_at: Time.now.utc,
|
||||||
|
expires_at: expires_at,
|
||||||
|
options: latest_options,
|
||||||
|
cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
|
||||||
|
)
|
||||||
|
rescue ActiveRecord::StaleObjectError
|
||||||
|
poll.reload
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the poll had no expiration date set but now has, and people have voted,
|
||||||
|
# schedule a notification.
|
||||||
|
if previous_expires_at.nil? && poll.expires_at.present? && poll.votes.exists?
|
||||||
|
PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def supported_context?
|
||||||
|
super(@json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expected_type?
|
||||||
|
equals_or_includes_any?(@json['type'], %w(Question))
|
||||||
|
end
|
||||||
|
end
|
|
@ -38,6 +38,10 @@ class NotifyService < BaseService
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def blocked_poll?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def following_sender?
|
def following_sender?
|
||||||
return @following_sender if defined?(@following_sender)
|
return @following_sender if defined?(@following_sender)
|
||||||
@following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
|
@following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
|
||||||
|
@ -88,7 +92,7 @@ class NotifyService < BaseService
|
||||||
|
|
||||||
def blocked?
|
def blocked?
|
||||||
blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway
|
blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway
|
||||||
blocked ||= from_self? # Skip for interactions with self
|
blocked ||= from_self? unless @notification.type == :poll # Skip for interactions with self
|
||||||
|
|
||||||
return blocked if message? && from_staff?
|
return blocked if message? && from_staff?
|
||||||
|
|
||||||
|
|
|
@ -91,10 +91,13 @@ class PostStatusService < BaseService
|
||||||
def postprocess_status!
|
def postprocess_status!
|
||||||
LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
|
LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
|
||||||
DistributionWorker.perform_async(@status.id)
|
DistributionWorker.perform_async(@status.id)
|
||||||
|
|
||||||
unless @status.local_only?
|
unless @status.local_only?
|
||||||
Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
|
Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
|
||||||
ActivityPub::DistributionWorker.perform_async(@status.id)
|
ActivityPub::DistributionWorker.perform_async(@status.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_media!
|
def validate_media!
|
||||||
|
|
|
@ -19,8 +19,9 @@ class VoteService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return if @poll.account.local?
|
if @poll.account.local?
|
||||||
|
ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, @poll.status.id) unless @poll.hide_totals
|
||||||
|
else
|
||||||
@votes.each do |vote|
|
@votes.each do |vote|
|
||||||
ActivityPub::DeliveryWorker.perform_async(
|
ActivityPub::DeliveryWorker.perform_async(
|
||||||
build_json(vote),
|
build_json(vote),
|
||||||
|
@ -28,6 +29,8 @@ class VoteService < BaseService
|
||||||
@poll.account.inbox_url
|
@poll.account.inbox_url
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
PollExpirationNotifyWorker.perform_at(@poll.expires_at + 5.minutes, @poll.id) unless @poll.expires_at.nil?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::DistributePollUpdateWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'push', unique: :until_executed, retry: 0
|
||||||
|
|
||||||
|
def perform(status_id)
|
||||||
|
@status = Status.find(status_id)
|
||||||
|
@account = @status.account
|
||||||
|
|
||||||
|
return if @status.poll.nil? || @status.local_only?
|
||||||
|
|
||||||
|
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
|
||||||
|
[payload, @account.id, inbox_url]
|
||||||
|
end
|
||||||
|
|
||||||
|
relay! if relayable?
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def relayable?
|
||||||
|
@status.public_visibility?
|
||||||
|
end
|
||||||
|
|
||||||
|
def inboxes
|
||||||
|
return @inboxes if defined?(@inboxes)
|
||||||
|
target_accounts = @status.mentions.map(&:account).reject(&:local?)
|
||||||
|
target_accounts += @status.reblogs.map(&:account).reject(&:local?)
|
||||||
|
target_accounts += @status.poll.votes.map(&:account).reject(&:local?)
|
||||||
|
target_accounts.uniq!(&:id)
|
||||||
|
@inboxes = target_accounts.select(&:activitypub?).pluck(&:inbox_url)
|
||||||
|
@inboxes += @account.followers.inboxes unless @status.direct_visibility?
|
||||||
|
@inboxes.uniq!
|
||||||
|
@inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_payload
|
||||||
|
Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsigned_payload
|
||||||
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
|
@status,
|
||||||
|
serializer: ActivityPub::UpdatePollSerializer,
|
||||||
|
adapter: ActivityPub::Adapter
|
||||||
|
).as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload
|
||||||
|
@payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def relay!
|
||||||
|
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
|
||||||
|
[payload, @account.id, inbox_url]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PollExpirationNotifyWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options unique: :until_executed
|
||||||
|
|
||||||
|
def perform(poll_id)
|
||||||
|
poll = Poll.find(poll_id)
|
||||||
|
|
||||||
|
# Notify poll owner and remote voters
|
||||||
|
if poll.local?
|
||||||
|
ActivityPub::DistributePollUpdateWorker.perform_async(poll.status.id)
|
||||||
|
NotifyService.new.call(poll.account, poll)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Notify local voters
|
||||||
|
poll.votes.includes(:account).map(&:account).filter(&:local?).each do |account|
|
||||||
|
NotifyService.new.call(account, poll)
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
|
@ -89,7 +89,6 @@ const startWorker = (workerId) => {
|
||||||
host: process.env.DB_HOST || pg.defaults.host,
|
host: process.env.DB_HOST || pg.defaults.host,
|
||||||
port: process.env.DB_PORT || pg.defaults.port,
|
port: process.env.DB_PORT || pg.defaults.port,
|
||||||
max: 10,
|
max: 10,
|
||||||
ssl: !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable' ? true : undefined,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
production: {
|
production: {
|
||||||
|
@ -99,10 +98,14 @@ const startWorker = (workerId) => {
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: process.env.DB_PORT || 5432,
|
port: process.env.DB_PORT || 5432,
|
||||||
max: 10,
|
max: 10,
|
||||||
ssl: !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable' ? true : undefined,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable') {
|
||||||
|
pgConfigs.development.ssl = true;
|
||||||
|
pgConfigs.production.ssl = true;
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal');
|
app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue