Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - app/services/post_status_service.rb Small conflict due to handling of instance-local toots. A subsequent change is required to ensure instance-local polls are not leaked through Update.
This commit is contained in:
		
						commit
						89bee860cd
					
				
					 19 changed files with 280 additions and 67 deletions
				
			
		| 
						 | 
				
			
			@ -9,11 +9,25 @@ class Settings::ExportsController < Settings::BaseController
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  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!
 | 
			
		||||
      else
 | 
			
		||||
        raise Mastodon::RaceConditionError
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    backup = current_user.backups.create!
 | 
			
		||||
    BackupWorker.perform_async(backup.id)
 | 
			
		||||
 | 
			
		||||
    redirect_to settings_export_path
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def lock_options
 | 
			
		||||
    { redis: Redis.current, key: "backup:#{current_user.id}" }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,7 +92,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
			
		|||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 () {
 | 
			
		||||
    const { notification } = this.props;
 | 
			
		||||
    const account          = notification.get('account');
 | 
			
		||||
| 
						 | 
				
			
			@ -220,6 +252,8 @@ class Notification extends ImmutablePureComponent {
 | 
			
		|||
      return this.renderFavourite(notification, link);
 | 
			
		||||
    case 'reblog':
 | 
			
		||||
      return this.renderReblog(notification, link);
 | 
			
		||||
    case 'poll':
 | 
			
		||||
      return this.renderPoll(notification);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -245,6 +245,7 @@
 | 
			
		|||
  "notification.favourite": "{name} favourited your status",
 | 
			
		||||
  "notification.follow": "{name} followed you",
 | 
			
		||||
  "notification.mention": "{name} mentioned you",
 | 
			
		||||
  "notification.poll": "Your poll has ended",
 | 
			
		||||
  "notification.reblog": "{name} boosted your status",
 | 
			
		||||
  "notifications.clear": "Clear notifications",
 | 
			
		||||
  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ const initialState = ImmutableMap({
 | 
			
		|||
      favourite: true,
 | 
			
		||||
      reblog: true,
 | 
			
		||||
      mention: true,
 | 
			
		||||
      poll: true,
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    quickFilter: ImmutableMap({
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +45,7 @@ const initialState = ImmutableMap({
 | 
			
		|||
      favourite: true,
 | 
			
		||||
      reblog: true,
 | 
			
		||||
      mention: true,
 | 
			
		||||
      poll: true,
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    sounds: ImmutableMap({
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +53,7 @@ const initialState = ImmutableMap({
 | 
			
		|||
      favourite: true,
 | 
			
		||||
      reblog: true,
 | 
			
		||||
      mention: true,
 | 
			
		||||
      poll: true,
 | 
			
		||||
    }),
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ filenames.forEach(filename => {
 | 
			
		|||
    'notification.follow': full['notification.follow'] || '',
 | 
			
		||||
    'notification.mention': full['notification.mention'] || '',
 | 
			
		||||
    'notification.reblog': full['notification.reblog'] || '',
 | 
			
		||||
    'notification.poll': full['notification.poll'] || '',
 | 
			
		||||
 | 
			
		||||
    'status.show_more': full['status.show_more'] || '',
 | 
			
		||||
    '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 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'])
 | 
			
		||||
    ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.poll.hide_totals
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_thread(status)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
 | 
			
		|||
 | 
			
		||||
  def perform
 | 
			
		||||
    update_account if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
 | 
			
		||||
    update_poll if equals_or_includes_any?(@object['type'], %w(Question))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ class Notification < ApplicationRecord
 | 
			
		|||
    follow:         'Follow',
 | 
			
		||||
    follow_request: 'FollowRequest',
 | 
			
		||||
    favourite:      'Favourite',
 | 
			
		||||
    poll:           'Poll',
 | 
			
		||||
  }.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_request, foreign_type: 'FollowRequest', 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 :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +46,7 @@ class Notification < ApplicationRecord
 | 
			
		|||
    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
 | 
			
		||||
    @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +60,8 @@ class Notification < ApplicationRecord
 | 
			
		|||
      favourite&.status
 | 
			
		||||
    when :mention
 | 
			
		||||
      mention&.status
 | 
			
		||||
    when :poll
 | 
			
		||||
      poll&.status
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +101,7 @@ class Notification < ApplicationRecord
 | 
			
		|||
    return unless new_record?
 | 
			
		||||
 | 
			
		||||
    case activity_type
 | 
			
		||||
    when 'Status', 'Follow', 'Favourite', 'FollowRequest'
 | 
			
		||||
    when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll'
 | 
			
		||||
      self.from_account_id = activity&.account_id
 | 
			
		||||
    when 'Mention'
 | 
			
		||||
      self.from_account_id = activity&.status&.account_id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								app/serializers/activitypub/update_poll_serializer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/serializers/activitypub/update_poll_serializer.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
  def status_type?
 | 
			
		||||
    [:favourite, :reblog, :mention].include?(object.type)
 | 
			
		||||
    [:favourite, :reblog, :mention, :poll].include?(object.type)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,54 +4,7 @@ class ActivityPub::FetchRemotePollService < BaseService
 | 
			
		|||
  include JsonLdHelper
 | 
			
		||||
 | 
			
		||||
  def call(poll, on_behalf_of = nil)
 | 
			
		||||
    @json = fetch_resource(poll.status.uri, true, on_behalf_of)
 | 
			
		||||
 | 
			
		||||
    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))
 | 
			
		||||
    json = fetch_resource(poll.status.uri, true, on_behalf_of)
 | 
			
		||||
    ActivityPub::ProcessPollService.new.call(poll, json)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										64
									
								
								app/services/activitypub/process_poll_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/services/activitypub/process_poll_service.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def blocked_poll?
 | 
			
		||||
    false
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def following_sender?
 | 
			
		||||
    return @following_sender if defined?(@following_sender)
 | 
			
		||||
    @following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +92,7 @@ class NotifyService < BaseService
 | 
			
		|||
 | 
			
		||||
  def blocked?
 | 
			
		||||
    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?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,10 +91,13 @@ class PostStatusService < BaseService
 | 
			
		|||
  def postprocess_status!
 | 
			
		||||
    LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
 | 
			
		||||
    DistributionWorker.perform_async(@status.id)
 | 
			
		||||
 | 
			
		||||
    unless @status.local_only?
 | 
			
		||||
      Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
 | 
			
		||||
      ActivityPub::DistributionWorker.perform_async(@status.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def validate_media!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,14 +19,17 @@ class VoteService < BaseService
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return if @poll.account.local?
 | 
			
		||||
 | 
			
		||||
    @votes.each do |vote|
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(
 | 
			
		||||
        build_json(vote),
 | 
			
		||||
        @account.id,
 | 
			
		||||
        @poll.account.inbox_url
 | 
			
		||||
      )
 | 
			
		||||
    if @poll.account.local?
 | 
			
		||||
      ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, @poll.status.id) unless @poll.hide_totals
 | 
			
		||||
    else
 | 
			
		||||
      @votes.each do |vote|
 | 
			
		||||
        ActivityPub::DeliveryWorker.perform_async(
 | 
			
		||||
          build_json(vote),
 | 
			
		||||
          @account.id,
 | 
			
		||||
          @poll.account.inbox_url
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
      PollExpirationNotifyWorker.perform_at(@poll.expires_at + 5.minutes, @poll.id) unless @poll.expires_at.nil?
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										62
									
								
								app/workers/activitypub/distribute_poll_update_worker.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/workers/activitypub/distribute_poll_update_worker.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 unless @status.poll
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
							
								
								
									
										24
									
								
								app/workers/poll_expiration_notify_worker.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/workers/poll_expiration_notify_worker.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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,
 | 
			
		||||
      port:     process.env.DB_PORT || pg.defaults.port,
 | 
			
		||||
      max:      10,
 | 
			
		||||
      ssl:      !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable' ? true : undefined,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    production: {
 | 
			
		||||
| 
						 | 
				
			
			@ -99,11 +98,15 @@ const startWorker = (workerId) => {
 | 
			
		|||
      host:     process.env.DB_HOST || 'localhost',
 | 
			
		||||
      port:     process.env.DB_PORT || 5432,
 | 
			
		||||
      max:      10,
 | 
			
		||||
      ssl:      !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable' ? true : undefined,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const app    = express();
 | 
			
		||||
  if (!!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable') {
 | 
			
		||||
    pgConfigs.development.ssl = true;
 | 
			
		||||
    pgConfigs.production.ssl  = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const app = express();
 | 
			
		||||
  app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal');
 | 
			
		||||
 | 
			
		||||
  const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL)));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue