Commit 3da7ebd3 authored by xxxkurosukexxx's avatar xxxkurosukexxx

Merge branch 'follow_master' into mstdn.kurosuke.org

parents 3019387f df51dcb2
......@@ -20,7 +20,8 @@ RUN echo "Etc/UTC" > /etc/localtime && \
# Install jemalloc
ENV JE_VER="5.1.0"
RUN apt -y install autoconf && \
RUN apt update && \
apt -y install autoconf && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
......@@ -34,7 +35,8 @@ RUN apt -y install autoconf && \
ENV RUBY_VER="2.6.1"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt -y install build-essential \
RUN apt update && \
apt -y install build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \
......@@ -52,13 +54,14 @@ RUN apt -y install build-essential \
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \
gem install bundler
gem install bundler && \
apt update && \
apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler
COPY . /opt/mastodon
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler && \
cd /opt/mastodon && \
RUN cd /opt/mastodon && \
bundle install -j$(nproc) --deployment --without development test && \
yarn install --pure-lockfile
......@@ -85,9 +88,6 @@ RUN echo "Etc/UTC" > /etc/localtime && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
# Copy over masto source from building and set permissions
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
# Install masto runtime deps
RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \
......@@ -95,11 +95,9 @@ RUN apt -y --no-install-recommends install \
file ca-certificates tzdata libreadline7 && \
apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \
gem install bundler
# Clean up more dirs
RUN rm -rf /var/cache && \
rm -rf /var/apt
gem install bundler && \
rm -rf /var/cache && \
rm -rf /var/lib/apt
# Add tini
ENV TINI_VERSION="0.18.0"
......@@ -108,6 +106,10 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin
RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini
# Copy over masto source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
# Run masto services in prod mode
ENV RAILS_ENV="production"
ENV NODE_ENV="production"
......
......@@ -400,7 +400,7 @@ GEM
pg (1.1.4)
pghero (2.2.0)
activerecord
pkg-config (1.3.4)
pkg-config (1.3.5)
powerpack (0.1.2)
premailer (1.11.1)
addressable
......@@ -641,7 +641,7 @@ GEM
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
webpush (0.3.6)
webpush (0.3.7)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket-driver (0.7.0)
......
......@@ -44,7 +44,18 @@ sudo apt-get install \
# Install rvm
read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
$($gpg_command)
if [ $? -ne 0 ];then
echo "GPG command failed, This prevented RVM from installing."
echo "Retrying once..." && $($gpg_command)
if [ $? -ne 0 ];then
echo "GPG failed for the second time, please ensure network connectivity."
echo "Exiting..." && exit 1
fi
fi
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm
......
......@@ -13,11 +13,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
......@@ -158,7 +158,9 @@ export function submitCompose(routerHistory) {
// into the columns
const insertIfOnline = timelineId => {
if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
......
......@@ -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();
};
......
......@@ -3,6 +3,7 @@ import {
updateTimeline,
deleteFromTimelines,
expandHomeTimeline,
connectTimeline,
disconnectTimeline,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
......@@ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
return {
onConnect() {
dispatch(connectTimeline(timelineId));
},
onDisconnect() {
dispatch(disconnectTimeline(timelineId));
},
......
......@@ -12,6 +12,7 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export function updateTimeline(timeline, status, accept) {
......@@ -143,6 +144,13 @@ export function scrollTopTimeline(timeline, top) {
};
};
export function connectTimeline(timeline) {
return {
type: TIMELINE_CONNECT,
timeline,
};
};
export function disconnectTimeline(timeline) {
return {
type: TIMELINE_DISCONNECT,
......
......@@ -94,7 +94,7 @@ class Poll extends ImmutablePureComponent {
renderOption (option, optionIndex) {
const { poll, disabled } = this.props;
const percent = (option.get('votes_count') / poll.get('votes_count')) * 100;
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
const active = !!this.state.selected[`${optionIndex}`];
const showResults = poll.get('voted') || poll.get('expired');
......
......@@ -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;
......
......@@ -240,6 +240,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?",
......
......@@ -73,6 +73,10 @@
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
"compose_form.lock_disclaimer.lock": "zablokowane",
"compose_form.placeholder": "Co Ci chodzi po głowie?",
"compose_form.poll.add_option": "Dodaj opcję",
"compose_form.poll.duration": "Czas trwania głosowania",
"compose_form.poll.option_placeholder": "Opcja {number}",
"compose_form.poll.remove_option": "Usuń tę opcję",
"compose_form.publish": "Wyślij",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.marked": "Zawartość multimedia jest oznaczona jako wrażliwa",
......@@ -142,8 +146,8 @@
"hashtag.column_header.tag_mode.all": "i {additional}",
"hashtag.column_header.tag_mode.any": "lub {additional}",
"hashtag.column_header.tag_mode.none": "bez {additional}",
"hashtag.column_settings.select.no_options_message": "No suggestions found",
"hashtag.column_settings.select.placeholder": "Enter hashtags…",
"hashtag.column_settings.select.no_options_message": "Nie odnaleziono sugestii",
"hashtag.column_settings.select.placeholder": "Wprowadź hashtagi…",
"hashtag.column_settings.tag_mode.all": "Wszystkie",
"hashtag.column_settings.tag_mode.any": "Dowolne",
"hashtag.column_settings.tag_mode.none": "Żadne",
......@@ -151,6 +155,9 @@
"home.column_settings.basic": "Podstawowe",
"home.column_settings.show_reblogs": "Pokazuj podbicia",
"home.column_settings.show_replies": "Pokazuj odpowiedzi",
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
"intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}",
"intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
"introduction.federation.action": "Dalej",
"introduction.federation.federated.headline": "Oś czasu federacji",
"introduction.federation.federated.text": "Publiczne wpisy osób z tego całego Fediwersum pojawiają się na lokalnej osi czasu.",
......@@ -206,7 +213,7 @@
"lists.account.remove": "Usunąć z listy",
"lists.delete": "Usuń listę",
"lists.edit": "Edytuj listę",
"lists.edit.submit": "Change title",
"lists.edit.submit": "Zmień tytuł",
"lists.new.create": "Utwórz listę",
"lists.new.title_placeholder": "Wprowadź tytuł listy",
"lists.search": "Szukaj wśród osób które śledzisz",
......@@ -260,10 +267,12 @@
"notifications.filter.follows": "Śledzenia",
"notifications.filter.mentions": "Wspomienia",
"notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.vote": "Vote",
"poll.closed": "Zamknięte",
"poll.refresh": "Odśwież",
"poll.total_votes": "{count, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}",
"poll.vote": "Zagłosuj",
"poll_button.add_poll": "Dodaj głosowanie",
"poll_button.remove_poll": "Usuń głosowanie",
"privacy.change": "Dostosuj widoczność wpisów",
"privacy.direct.long": "Widoczny tylko dla wspomnianych",
"privacy.direct.short": "Bezpośrednio",
......@@ -304,7 +313,7 @@
"status.block": "Zablokuj @{name}",
"status.cancel_reblog_private": "Cofnij podbicie",
"status.cannot_reblog": "Ten wpis nie może zostać podbity",
"status.copy": "Copy link to status",
"status.copy": "Skopiuj odnośnik do wpisu",
"status.delete": "Usuń",
"status.detailed_status": "Szczegółowy widok konwersacji",
"status.direct": "Wyślij wiadomość bezpośrednią do @{name}",
......@@ -346,11 +355,11 @@
"tabs_bar.local_timeline": "Lokalne",
"tabs_bar.notifications": "Powiadomienia",
"tabs_bar.search": "Szukaj",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"time_remaining.days": "{number, plural, one {Pozostał # dzień} few {Pozostały # dni} many {Pozostało # dni} other {Pozostało # dni}}",
"time_remaining.hours": "{number, plural, one {Pozostała # godzina} few {Pozostały # godziny} many {Pozostało # godzin} other {Pozostało # godzin}}",
"time_remaining.minutes": "{number, plural, one {Pozostała # minuta} few {Pozostały # minuty} many {Pozostało # minut} other {Pozostało # minut}}",
"time_remaining.moments": "Pozostała chwila",
"time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}",
"trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym",
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
"upload_area.title": "Przeciągnij i upuść aby wysłać",
......@@ -359,7 +368,7 @@
"upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
"upload_form.focus": "Dopasuj podgląd",
"upload_form.undo": "Usuń",
"upload_progress.label": "Wysyłanie...",
"upload_progress.label": "Wysyłanie",
"video.close": "Zamknij film",
"video.exit_fullscreen": "Opuść tryb pełnoekranowy",
"video.expand": "Rozszerz film",
......
......@@ -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,
}),
}),
......
......@@ -6,6 +6,7 @@ import {
TIMELINE_EXPAND_REQUEST,
TIMELINE_EXPAND_FAIL,
TIMELINE_SCROLL_TOP,
TIMELINE_CONNECT,
TIMELINE_DISCONNECT,
} from '../actions/timelines';
import {
......@@ -20,6 +21,7 @@ const initialState = ImmutableMap();
const initialTimeline = ImmutableMap({
unread: 0,
online: false,
top: true,
isLoading: false,
hasMore: true,
......@@ -142,14 +144,13 @@ export default function timelines(state = initialState, action) {
return filterTimeline('home', state, action.relationship, action.statuses);
case TIMELINE_SCROLL_TOP:
return updateTop(state, action.timeline, action.top);
case TIMELINE_CONNECT:
return state.update(action.timeline, initialTimeline, map => map.set('online', true));
case TIMELINE_DISCONNECT:
return state.update(
action.timeline,
initialTimeline,
map => map.update(
'items',
items => items.first() ? items.unshift(null) : items
)
map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
);
default:
return state;
......
......@@ -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'] || '',
......
......@@ -2,11 +2,11 @@ import WebSocketClient from 'websocket.js';
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) {
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
return (dispatch, getState) => {
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = getState().getIn(['meta', 'access_token']);
const { onDisconnect, onReceive } = callbacks(dispatch, getState);
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
let polling = null;
......@@ -28,6 +28,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
if (pollingRefresh) {
clearPolling();
}
onConnect();
},
disconnected () {
......@@ -47,6 +49,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
clearPolling();
pollingRefresh(dispatch);
}
onConnect();
},
});
......
......@@ -241,7 +241,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def poll_vote?
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
......@@ -266,6 +266,7 @@ class Account < ApplicationRecord
return if fields.size >= DEFAULT_FIELDS_SIZE
tmp = self[:fields] || []
tmp = [] if tmp.is_a?(Hash)
(DEFAULT_FIELDS_SIZE - tmp.size).times do
tmp << { name: '', value: '' }
......
......@@ -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
......
......@@ -28,7 +28,7 @@ class Poll < ApplicationRecord
validates :options, presence: true
validates :expires_at, presence: true, if: :local?
validates_with PollValidator, if: :local?
validates_with PollValidator, on: :create, if: :local?
scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) }
......@@ -41,17 +41,17 @@ class Poll < ApplicationRecord
after_commit :reset_parent_cache, on: :update
def loaded_options
options.map.with_index { |title, key| Option.new(self, key.to_s, title, cached_tallies[key]) }
end
def unloaded_options
options.map.with_index { |title, key| Option.new(self, key.to_s, title, nil) }
options.map.with_index { |title, key| Option.new(self, key.to_s, title, show_totals_now? ? cached_tallies[key] : nil) }
end
def possibly_stale?
remote? && last_fetched_before_expiration? && time_passed_since_last_fetch?
end
def voted?(account)
account.id == account_id || votes.where(account: account).exists?
end
delegate :local?, to: :account
def remote?
......@@ -95,4 +95,8 @@ class Poll < ApplicationRecord
def time_passed_since_last_fetch?
last_fetched_at.nil? || last_fetched_at < 1.minute.ago
end
def show_totals_now?
expired? || !hide_totals?
end
end
......@@ -122,11 +122,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
end
def poll_options
if !object.poll.expired? && object.poll.hide_totals?
object.poll.unloaded_options
else
object.poll.loaded_options
end
object.poll.loaded_options
end
def poll_and_multiple?
......
# 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,7 +4,7 @@ class REST::PollSerializer < ActiveModel::Serializer
attributes :id, :expires_at, :expired,
:multiple, :votes_count
has_many :dynamic_options, key: :options
has_many :loaded_options, key: :options
attribute :voted, if: :current_user?
......@@ -12,20 +12,12 @@ class REST::PollSerializer < ActiveModel::Serializer
object.id.to_s
end
def dynamic_options
if !object.expired? && object.hide_totals?
object.unloaded_options
else
object.loaded_options
end
end
def expired
object.expired?
end
def voted
object.votes.where(account: current_user.account).exists?
object.voted?(current_user.account)
end
def current_user?
......
......@@ -4,49 +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
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 }
)
end