Fix status expires feature
This commit is contained in:
parent
8ebcbaded3
commit
0b8c3df283
61 changed files with 589 additions and 140 deletions
|
@ -18,7 +18,7 @@ class Api::V1::BookmarksController < Api::BaseController
|
|||
end
|
||||
|
||||
def cached_bookmarks
|
||||
cache_collection(Status.where(id: results.pluck(:status_id)), Status)
|
||||
cache_collection(Status.include_expired.where(id: results.pluck(:status_id)), Status)
|
||||
end
|
||||
|
||||
def results
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::EmojiReactionsController < Api::BaseController
|
|||
end
|
||||
|
||||
def cached_emoji_reactions
|
||||
cache_collection(Status.where(id: results.pluck(:status_id)), Status)
|
||||
cache_collection(Status.include_expired.where(id: results.pluck(:status_id)), Status)
|
||||
end
|
||||
|
||||
def results
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::FavouritesController < Api::BaseController
|
|||
end
|
||||
|
||||
def cached_favourites
|
||||
cache_collection(Status.where(id: results.pluck(:status_id)), Status)
|
||||
cache_collection(Status.include_expired.where(id: results.pluck(:status_id)), Status)
|
||||
end
|
||||
|
||||
def results
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
|
|||
if bookmark
|
||||
@status = bookmark.status
|
||||
else
|
||||
@status = Status.find(params[:status_id])
|
||||
@status = Status.include_expired.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
|
|||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
@status = Status.include_expired.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
|
|
|
@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
|||
@status = fav.status
|
||||
UnfavouriteWorker.perform_async(current_account.id, @status.id)
|
||||
else
|
||||
@status = Status.find(params[:status_id])
|
||||
@status = Status.include_expired.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
end
|
||||
|
||||
|
|
|
@ -8,19 +8,12 @@ class Api::V1::Statuses::PinsController < Api::BaseController
|
|||
before_action :set_status
|
||||
|
||||
def create
|
||||
StatusPin.create!(account: current_account, status: @status)
|
||||
distribute_add_activity!
|
||||
PinService.new.call(current_account, @status)
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
pin = StatusPin.find_by(account: current_account, status: @status)
|
||||
|
||||
if pin
|
||||
pin.destroy!
|
||||
distribute_remove_activity!
|
||||
end
|
||||
|
||||
UnpinService.new.call(current_account, @status)
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
|
@ -29,24 +22,4 @@ class Api::V1::Statuses::PinsController < Api::BaseController
|
|||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
end
|
||||
|
||||
def distribute_add_activity!
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
@status,
|
||||
serializer: ActivityPub::AddSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json
|
||||
|
||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id)
|
||||
end
|
||||
|
||||
def distribute_remove_activity!
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
@status,
|
||||
serializer: ActivityPub::RemoveSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json
|
||||
|
||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
before_action :set_status, only: [:show, :context]
|
||||
before_action :set_thread, only: [:create]
|
||||
before_action :set_circle, only: [:create]
|
||||
before_action :set_expire, only: [:create]
|
||||
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
|
||||
|
@ -46,7 +47,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
expires_at: status_params[:expires_at],
|
||||
expires_at: @expires_at,
|
||||
expires_action: status_params[:expires_action],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
|
@ -98,6 +99,10 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
render json: { error: I18n.t('statuses.errors.circle_not_found') }, status: 404
|
||||
end
|
||||
|
||||
def set_expire
|
||||
@expires_at = status_params[:expires_at] || status_params[:expires_in].blank? ? nil : status_params[:expires_in].to_i.seconds.from_now
|
||||
end
|
||||
|
||||
def status_params
|
||||
params.permit(
|
||||
:status,
|
||||
|
@ -108,6 +113,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
:visibility,
|
||||
:scheduled_at,
|
||||
:quote_id,
|
||||
:expires_in,
|
||||
:expires_at,
|
||||
:expires_action,
|
||||
media_ids: [],
|
||||
|
|
|
@ -4,6 +4,7 @@ import { connectStream } from '../stream';
|
|||
import {
|
||||
updateTimeline,
|
||||
deleteFromTimelines,
|
||||
expireFromTimelines,
|
||||
expandHomeTimeline,
|
||||
connectTimeline,
|
||||
disconnectTimeline,
|
||||
|
@ -80,6 +81,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
break;
|
||||
case 'expire':
|
||||
dispatch(expireFromTimelines(data.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||
break;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { uniq } from '../utils/uniq';
|
|||
|
||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||
export const TIMELINE_EXPIRE = 'TIMELINE_EXPIRE';
|
||||
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
|
||||
|
||||
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||
|
@ -88,6 +89,22 @@ export function deleteFromTimelines(id) {
|
|||
};
|
||||
};
|
||||
|
||||
export function expireFromTimelines(id) {
|
||||
return (dispatch, getState) => {
|
||||
const accountId = getState().getIn(['statuses', id, 'account']);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
|
||||
const quotes = getState().get('statuses').filter(status => status.get('quote_id') === id).map(status => status.get('id'));
|
||||
|
||||
dispatch({
|
||||
type: TIMELINE_EXPIRE,
|
||||
id,
|
||||
accountId,
|
||||
references,
|
||||
quotes,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function clearTimeline(timeline) {
|
||||
return (dispatch) => {
|
||||
dispatch({ type: TIMELINE_CLEAR, timeline });
|
||||
|
|
|
@ -376,11 +376,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar-button' disabled={expired} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate disabled={!me && expired} active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate disabled={!status.get('favourited') && expired} active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||
{show_quote_button && <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />}
|
||||
{enableReaction && <div className='status__action-bar-dropdown'><ReactionPickerDropdown disabled={expired} active={status.get('emoji_reactioned')} pressed={status.get('emoji_reactioned')} iconButtonClass='status__action-bar-button' onPickEmoji={this.handleEmojiPick} onRemoveEmoji={this.handleEmojiRemove} /></div>}
|
||||
{shareButton}
|
||||
{show_bookmark_button && <IconButton className='status__action-bar-button bookmark-icon' disabled={!me && expired} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />}
|
||||
{show_bookmark_button && <IconButton className='status__action-bar-button bookmark-icon' disabled={!status.get('bookmarked') && expired} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />}
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer
|
||||
|
|
|
@ -323,11 +323,11 @@ class ActionBar extends React.PureComponent {
|
|||
<div className='detailed-status__action-bar'>
|
||||
<div className='detailed-status__button'><IconButton disabled={expired} title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} disabled={!status.get('favourited') && expired} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
{show_quote_button && <div className='detailed-status__button'><IconButton disabled={!publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>}
|
||||
{enableReaction && <div className='detailed-status__button'><ReactionPickerDropdown disabled={expired} active={status.get('emoji_reactioned')} pressed={status.get('emoji_reactioned')} iconButtonClass='detailed-status__action-bar-button' onPickEmoji={this.handleEmojiPick} onRemoveEmoji={this.handleEmojiRemove} /></div>}
|
||||
{shareButton}
|
||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} disabled={!status.get('bookmarked') && expired} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||
|
||||
<div className='detailed-status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
||||
|
|
|
@ -46,7 +46,7 @@ import {
|
|||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||
} from '../actions/compose';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { TIMELINE_DELETE, TIMELINE_EXPIRE } from '../actions/timelines';
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
import { REDRAFT } from '../actions/statuses';
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
@ -476,6 +476,7 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_TAG_HISTORY_UPDATE:
|
||||
return state.set('tagHistory', fromJS(action.tags));
|
||||
case TIMELINE_DELETE:
|
||||
case TIMELINE_EXPIRE:
|
||||
if (action.id === state.get('in_reply_to')) {
|
||||
return state.set('in_reply_to', null);
|
||||
} else {
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
ACCOUNT_MUTE_SUCCESS,
|
||||
} from '../actions/accounts';
|
||||
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
|
||||
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
|
||||
import { TIMELINE_DELETE, TIMELINE_EXPIRE, TIMELINE_UPDATE } from '../actions/timelines';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import compareId from '../compare_id';
|
||||
|
||||
|
@ -97,6 +97,7 @@ export default function replies(state = initialState, action) {
|
|||
case CONTEXT_FETCH_SUCCESS:
|
||||
return normalizeContext(state, action.id, action.ancestors, action.descendants);
|
||||
case TIMELINE_DELETE:
|
||||
case TIMELINE_EXPIRE:
|
||||
return deleteFromContexts(state, [action.id]);
|
||||
case TIMELINE_UPDATE:
|
||||
return updateContext(state, action.status);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { TIMELINE_DELETE, TIMELINE_EXPIRE } from '../actions/timelines';
|
||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
||||
import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable';
|
||||
|
||||
|
@ -12,6 +12,7 @@ export default function modal(state = ImmutableStack(), action) {
|
|||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
||||
return state.getIn([0, 'modalType']) === 'FOCAL_POINT' ? state.shift() : state;
|
||||
case TIMELINE_DELETE:
|
||||
case TIMELINE_EXPIRE:
|
||||
return state.filterNot((modal) => modal.get('modalProps').statusId === action.id);
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
APP_UNFOCUS,
|
||||
} from '../actions/app';
|
||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||
import { TIMELINE_DELETE, TIMELINE_EXPIRE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import compareId from '../compare_id';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { TIMELINE_DELETE, TIMELINE_EXPIRE } from '../actions/timelines';
|
||||
|
||||
const initialState = {
|
||||
statusId: null,
|
||||
|
@ -18,6 +18,7 @@ export default function pictureInPicture(state = initialState, action) {
|
|||
case PICTURE_IN_PICTURE_REMOVE:
|
||||
return { ...initialState };
|
||||
case TIMELINE_DELETE:
|
||||
case TIMELINE_EXPIRE:
|
||||
return (state.statusId === action.id) ? { ...initialState } : state;
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
TIMELINE_UPDATE,
|
||||
TIMELINE_DELETE,
|
||||
TIMELINE_EXPIRE,
|
||||
TIMELINE_CLEAR,
|
||||
TIMELINE_EXPAND_SUCCESS,
|
||||
TIMELINE_EXPAND_REQUEST,
|
||||
|
@ -107,6 +108,22 @@ const deleteStatus = (state, id, references, exclude_account = null) => {
|
|||
return state;
|
||||
};
|
||||
|
||||
const expireStatus = (state, id, references, exclude_account) => {
|
||||
state.keySeq().forEach(timeline => {
|
||||
if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
|
||||
const helper = list => list.filterNot(item => item === id);
|
||||
state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove reblogs of deleted status
|
||||
references.forEach(ref => {
|
||||
state = deleteStatus(state, ref, []);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const clearTimeline = (state, timeline) => {
|
||||
return state.set(timeline, initialTimeline);
|
||||
};
|
||||
|
@ -153,6 +170,8 @@ export default function timelines(state = initialState, action) {
|
|||
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
case TIMELINE_EXPIRE:
|
||||
return expireStatus(state, action.id, action.references, action.accountId);
|
||||
case TIMELINE_CLEAR:
|
||||
return clearTimeline(state, action.timeline);
|
||||
case ACCOUNT_BLOCK_SUCCESS:
|
||||
|
|
|
@ -28,6 +28,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
|||
@params = {}
|
||||
|
||||
process_status_params
|
||||
process_expiry_params
|
||||
process_audience
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
|
@ -40,6 +41,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
|||
end
|
||||
|
||||
distribute(@status)
|
||||
expire_queue_action
|
||||
end
|
||||
|
||||
def process_status_params
|
||||
|
@ -50,11 +52,30 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
|||
uri: @json['id'],
|
||||
created_at: @json['published'],
|
||||
override_timestamps: @options[:override_timestamps],
|
||||
visibility: visibility_from_audience
|
||||
visibility: visibility_from_audience,
|
||||
expires_at: @json['expiry'],
|
||||
expires_action: :mark,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def process_expiry_params
|
||||
expiry = @object['expiry'].to_time rescue nil
|
||||
|
||||
if expiry.nil?
|
||||
@params
|
||||
elsif expiry <= Time.now.utc + PostStatusService::MIN_EXPIRE_OFFSET
|
||||
@params.merge!({
|
||||
expired_at: @object['expiry']
|
||||
})
|
||||
else
|
||||
@params.merge!({
|
||||
expires_at: @object['expiry'],
|
||||
expires_action: :mark,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def attach_mentions(status)
|
||||
@mentions.each do |mention|
|
||||
mention.status = status
|
||||
|
@ -62,6 +83,15 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
|||
end
|
||||
end
|
||||
|
||||
def expire_queue_action
|
||||
@status.status_expire.queue_action if expires_soon?
|
||||
end
|
||||
|
||||
def expires_soon?
|
||||
expires_at = @status&.status_expire&.expires_at
|
||||
expires_at.present? && expires_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET
|
||||
end
|
||||
|
||||
def announceable?(status)
|
||||
status.account_id == @account.id || (@account.group? && dereferenced?) || status.distributable? || status.account.mutual?(@account)
|
||||
end
|
||||
|
|
|
@ -75,6 +75,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
process_quote
|
||||
process_status_params
|
||||
process_expiry_params
|
||||
process_tags
|
||||
process_audience
|
||||
|
||||
|
@ -88,6 +89,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
distribute(@status)
|
||||
forward_for_conversation
|
||||
forward_for_reply
|
||||
expire_queue_action
|
||||
end
|
||||
|
||||
def find_existing_status
|
||||
|
@ -115,11 +117,27 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
media_attachment_ids: process_attachments.take(4).map(&:id),
|
||||
poll: process_poll,
|
||||
quote: quote,
|
||||
expires_at: @object['expiry'],
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def process_expiry_params
|
||||
expiry = @object['expiry'].to_time rescue nil
|
||||
|
||||
if expiry.nil?
|
||||
@params
|
||||
elsif expiry <= Time.now.utc + PostStatusService::MIN_EXPIRE_OFFSET
|
||||
@params.merge!({
|
||||
expired_at: @object['expiry']
|
||||
})
|
||||
else
|
||||
@params.merge!({
|
||||
expires_at: @object['expiry'],
|
||||
expires_action: :mark,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def attach_tags(status)
|
||||
@tags.each do |tag|
|
||||
status.tags << tag
|
||||
|
@ -475,6 +493,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
||||
end
|
||||
|
||||
def expire_queue_action
|
||||
@status.status_expire.queue_action if expires_soon?
|
||||
end
|
||||
|
||||
def expires_soon?
|
||||
expires_at = @status&.status_expire&.expires_at
|
||||
expires_at.present? && expires_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET
|
||||
end
|
||||
|
||||
def increment_voters_count!
|
||||
poll = replied_to_status.preloadable_poll
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
def perform
|
||||
if @account.uri == object_uri
|
||||
delete_person
|
||||
elsif @json['expiry'].present?
|
||||
expire_note
|
||||
else
|
||||
delete_note
|
||||
end
|
||||
|
@ -32,13 +34,27 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
||||
end
|
||||
|
||||
@status = Status.include_expired.find_by(uri: object_uri, account: @account)
|
||||
@status ||= Status.include_expired.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
return if @status.nil?
|
||||
|
||||
forward! if @json['signature'].present? && @status.distributable?
|
||||
delete_now!
|
||||
end
|
||||
end
|
||||
|
||||
def expire_note
|
||||
return if object_uri.nil?
|
||||
|
||||
lock_or_return("delete_status_in_progress:#{object_uri}", 5.minutes.seconds) do
|
||||
@status = Status.find_by(uri: object_uri, account: @account)
|
||||
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
return if @status.nil?
|
||||
|
||||
forward! if @json['signature'].present? && @status.distributable?
|
||||
delete_now!
|
||||
expire_now!
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -80,6 +96,10 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
RemoveStatusService.new.call(@status, redraft: false)
|
||||
end
|
||||
|
||||
def expire_now!
|
||||
RemoveStatusService.new.call(@status, redraft: false, mark_expired: true)
|
||||
end
|
||||
|
||||
def payload
|
||||
@payload ||= Oj.dump(@json)
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
|
|||
end
|
||||
|
||||
def try_undo_announce
|
||||
status = Status.where.not(reblog_of_id: nil).find_by(uri: object_uri, account: @account)
|
||||
status = Status.include_expired.where.not(reblog_of_id: nil).find_by(uri: object_uri, account: @account)
|
||||
if status.present?
|
||||
RemoveStatusService.new.call(status)
|
||||
true
|
||||
|
@ -79,8 +79,8 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
|
|||
def undo_announce
|
||||
return if object_uri.nil?
|
||||
|
||||
status = Status.find_by(uri: object_uri, account: @account)
|
||||
status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
status = Status.include_expired.find_by(uri: object_uri, account: @account)
|
||||
status ||= Status.include_expired.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
if status.nil?
|
||||
delete_later!(object_uri)
|
||||
|
|
|
@ -66,10 +66,10 @@ class FeedManager
|
|||
# @param [Account] account
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def unpush_from_home(account, status)
|
||||
def unpush_from_home(account, status, **options)
|
||||
return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||
|
||||
redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
||||
redis.publish("timeline:#{account.id}", Oj.dump(event: options[:mark_expired] ? :expire : :delete, payload: status.id.to_s))
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -89,10 +89,10 @@ class FeedManager
|
|||
# @param [List] list
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def unpush_from_list(list, status)
|
||||
def unpush_from_list(list, status, **options)
|
||||
return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
|
||||
redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
||||
redis.publish("timeline:list:#{list.id}", Oj.dump(event: options[:mark_expired] ? :expire : :delete, payload: status.id.to_s))
|
||||
true
|
||||
end
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
class StatusFilter
|
||||
attr_reader :status, :account
|
||||
|
||||
def initialize(status, account, preloaded_relations = {})
|
||||
@status = status
|
||||
@account = account
|
||||
@preloaded_relations = preloaded_relations
|
||||
def initialize(status, account, preloaded_account_relations = {}, preloaded_status_relations = {})
|
||||
@status = status
|
||||
@account = account
|
||||
@preloaded_account_relations = preloaded_account_relations
|
||||
@preloaded_status_relations = preloaded_status_relations
|
||||
end
|
||||
|
||||
def filtered?
|
||||
|
@ -25,15 +26,15 @@ class StatusFilter
|
|||
end
|
||||
|
||||
def blocking_account?
|
||||
@preloaded_relations[:blocking] ? @preloaded_relations[:blocking][status.account_id] : account.blocking?(status.account_id)
|
||||
@preloaded_account_relations[:blocking] ? @preloaded_account_relations[:blocking][status.account_id] : account.blocking?(status.account_id)
|
||||
end
|
||||
|
||||
def blocking_domain?
|
||||
@preloaded_relations[:domain_blocking_by_domain] ? @preloaded_relations[:domain_blocking_by_domain][status.account_domain] : account.domain_blocking?(status.account_domain)
|
||||
@preloaded_account_relations[:domain_blocking_by_domain] ? @preloaded_account_relations[:domain_blocking_by_domain][status.account_domain] : account.domain_blocking?(status.account_domain)
|
||||
end
|
||||
|
||||
def muting_account?
|
||||
@preloaded_relations[:muting] ? @preloaded_relations[:muting][status.account_id] : account.muting?(status.account_id)
|
||||
@preloaded_account_relations[:muting] ? @preloaded_account_relations[:muting][status.account_id] : account.muting?(status.account_id)
|
||||
end
|
||||
|
||||
def silenced_account?
|
||||
|
@ -45,7 +46,7 @@ class StatusFilter
|
|||
end
|
||||
|
||||
def account_following_status_account?
|
||||
@preloaded_relations[:following] ? @preloaded_relations[:following][status.account_id] : account&.following?(status.account_id)
|
||||
@preloaded_account_relations[:following] ? @preloaded_account_relations[:following][status.account_id] : account&.following?(status.account_id)
|
||||
end
|
||||
|
||||
def blocked_by_policy?
|
||||
|
@ -53,6 +54,6 @@ class StatusFilter
|
|||
end
|
||||
|
||||
def policy_allows_show?
|
||||
StatusPolicy.new(account, status, @preloaded_relations).show?
|
||||
StatusPolicy.new(account, status, @preloaded_account_relations, @preloaded_status_relations).show?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ class Bookmark < ApplicationRecord
|
|||
update_index('statuses', :status) if Chewy.enabled?
|
||||
|
||||
belongs_to :account, inverse_of: :bookmarks
|
||||
belongs_to :status, inverse_of: :bookmarks
|
||||
belongs_to :status, -> { unscope(where: :expired_at) }, inverse_of: :bookmarks
|
||||
|
||||
validates :status_id, uniqueness: { scope: :account_id }
|
||||
|
||||
|
|
|
@ -81,12 +81,12 @@ module StatusThreadingConcern
|
|||
end
|
||||
|
||||
def find_statuses_from_tree_path(ids, account, promote: false)
|
||||
statuses = Status.with_accounts(ids).to_a
|
||||
account_ids = statuses.map(&:account_id).uniq
|
||||
domains = statuses.filter_map(&:account_domain).uniq
|
||||
relations = relations_map_for_account(account, account_ids, domains)
|
||||
statuses = Status.with_accounts(ids).to_a
|
||||
account_ids = statuses.map(&:account_id).uniq
|
||||
account_relations = relations_map_for_account(account, account_ids)
|
||||
status_relations = relations_map_for_status(account, statuses)
|
||||
|
||||
statuses.reject! { |status| StatusFilter.new(status, account, relations).filtered? }
|
||||
statuses.reject! { |status| StatusFilter.new(status, account, account_relations, status_relations).filtered? }
|
||||
|
||||
# Order ancestors/descendants by tree path
|
||||
statuses.sort_by! { |status| ids.index(status.id) }
|
||||
|
@ -114,7 +114,7 @@ module StatusThreadingConcern
|
|||
arr
|
||||
end
|
||||
|
||||
def relations_map_for_account(account, account_ids, domains)
|
||||
def relations_map_for_account(account, account_ids)
|
||||
return {} if account.nil?
|
||||
|
||||
presenter = AccountRelationshipsPresenter.new(account_ids, account)
|
||||
|
@ -127,4 +127,18 @@ module StatusThreadingConcern
|
|||
domain_blocking_by_domain: presenter.domain_blocking,
|
||||
}
|
||||
end
|
||||
|
||||
def relations_map_for_status(account, statuses)
|
||||
return {} if account.nil?
|
||||
|
||||
presenter = StatusRelationshipsPresenter.new(statuses, account)
|
||||
{
|
||||
reblogs_map: presenter.reblogs_map,
|
||||
favourites_map: presenter.favourites_map,
|
||||
bookmarks_map: presenter.bookmarks_map,
|
||||
emoji_reactions_map: presenter.emoji_reactions_map,
|
||||
mutes_map: presenter.mutes_map,
|
||||
pins_map: presenter.pins_map,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ class EmojiReaction < ApplicationRecord
|
|||
after_commit :refresh_status
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :status, inverse_of: :emoji_reactions
|
||||
belongs_to :status, -> { unscope(where: :expired_at) }, inverse_of: :emoji_reactions
|
||||
belongs_to :custom_emoji, optional: true
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
|
|
@ -16,7 +16,7 @@ class Favourite < ApplicationRecord
|
|||
update_index('statuses', :status)
|
||||
|
||||
belongs_to :account, inverse_of: :favourites
|
||||
belongs_to :status, inverse_of: :favourites
|
||||
belongs_to :status, -> { unscope(where: :expired_at) }, inverse_of: :favourites
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
|
|
|
@ -46,11 +46,11 @@ class Status < ApplicationRecord
|
|||
attr_accessor :override_timestamps
|
||||
|
||||
attr_accessor :circle
|
||||
attr_accessor :expires_at, :expires_action
|
||||
|
||||
update_index('statuses', :proper)
|
||||
|
||||
enum visibility: [:public, :unlisted, :private, :direct, :limited, :mutual], _suffix: :visibility
|
||||
enum expires_action: [:delete, :hint], _prefix: :expires
|
||||
|
||||
belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
|
||||
|
||||
|
@ -82,6 +82,7 @@ class Status < ApplicationRecord
|
|||
has_one :notification, as: :activity, dependent: :destroy
|
||||
has_one :status_stat, inverse_of: :status
|
||||
has_one :poll, inverse_of: :status, dependent: :destroy
|
||||
has_one :status_expire, inverse_of: :status
|
||||
|
||||
validates :uri, uniqueness: true, presence: true, unless: :local?
|
||||
validates :text, presence: true, unless: -> { with_media? || reblog? }
|
||||
|
@ -100,7 +101,7 @@ class Status < ApplicationRecord
|
|||
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
||||
scope :local, -> { where(local: true).or(where(uri: nil)) }
|
||||
|
||||
scope :not_expired, -> { where("statuses.expires_at >= CURRENT_TIMESTAMP") }
|
||||
scope :not_expired, -> { where(expired_at: nil) }
|
||||
scope :include_expired, -> { unscoped.recent.kept }
|
||||
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
||||
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
||||
|
@ -129,6 +130,7 @@ class Status < ApplicationRecord
|
|||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:status_expire,
|
||||
:tags,
|
||||
:preview_cards,
|
||||
:preloadable_poll,
|
||||
|
@ -141,6 +143,7 @@ class Status < ApplicationRecord
|
|||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:status_expire,
|
||||
:preloadable_poll,
|
||||
account: [:account_stat, :user],
|
||||
active_mentions: { account: :account_stat },
|
||||
|
@ -151,10 +154,6 @@ class Status < ApplicationRecord
|
|||
|
||||
REAL_TIME_WINDOW = 6.hours
|
||||
|
||||
def expires_at=(val)
|
||||
super(val.nil? ? 'infinity' : val)
|
||||
end
|
||||
|
||||
def searchable_by(preloaded = nil)
|
||||
ids = []
|
||||
|
||||
|
@ -255,8 +254,16 @@ class Status < ApplicationRecord
|
|||
media_attachments.any?
|
||||
end
|
||||
|
||||
def expired?
|
||||
!expired_at.nil?
|
||||
end
|
||||
|
||||
def expires?
|
||||
expires_at != ::Float::INFINITY
|
||||
status_expire.present?
|
||||
end
|
||||
|
||||
def expiry
|
||||
expires? && status_expire&.expires_mark? && status_expire&.expires_at || expired_at
|
||||
end
|
||||
|
||||
def non_sensitive_with_media?
|
||||
|
@ -326,6 +333,8 @@ class Status < ApplicationRecord
|
|||
|
||||
after_create_commit :store_uri, if: :local?
|
||||
after_create_commit :update_statistics, if: :local?
|
||||
after_create_commit :set_status_expire, if: -> { expires_at.present? }
|
||||
after_update :update_status_expire, if: -> { expires_at.present? }
|
||||
|
||||
around_create Mastodon::Snowflake::Callbacks
|
||||
|
||||
|
@ -431,6 +440,14 @@ class Status < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def set_status_expire
|
||||
create_status_expire(expires_at: expires_at, action: expires_action)
|
||||
end
|
||||
|
||||
def update_status_expire
|
||||
status_expire&.update(expires_at: expires_at, action: expires_action) || set_status_expire
|
||||
end
|
||||
|
||||
def update_status_stat!(attrs)
|
||||
return if marked_for_destruction? || destroyed?
|
||||
|
||||
|
|
28
app/models/status_expire.rb
Normal file
28
app/models/status_expire.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: status_expires
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8) not null
|
||||
# expires_at :datetime not null
|
||||
# action :integer default("delete"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class StatusExpire < ApplicationRecord
|
||||
enum action: [:delete, :mark], _prefix: :expires
|
||||
|
||||
belongs_to :status
|
||||
|
||||
after_commit :reset_parent_cache
|
||||
|
||||
def queue_action
|
||||
ExpiredStatusWorker.perform_at(expires_at, status_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_parent_cache
|
||||
Rails.cache.delete("statuses/#{status_id}")
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class TimeLimit
|
||||
TIME_LIMIT_RE = /^exp(?<value>\d+)(?<unit>[mhd])$/
|
||||
VALID_DURATION = (1.minute..7.days)
|
||||
VALID_DURATION = (1.minute..430.days)
|
||||
|
||||
def self.from_tags(tags, created_at = Time.now.utc)
|
||||
return unless tags
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatusPolicy < ApplicationPolicy
|
||||
def initialize(current_account, record, preloaded_relations = {})
|
||||
def initialize(current_account, record, preloaded_account_relations = {}, preloaded_status_relations = {})
|
||||
super(current_account, record)
|
||||
|
||||
@preloaded_relations = preloaded_relations
|
||||
@preloaded_account_relations = preloaded_account_relations
|
||||
@preloaded_status_relations = preloaded_status_relations
|
||||
end
|
||||
|
||||
delegate :reply?, to: :record
|
||||
delegate :reply?, :expired?, to: :record
|
||||
|
||||
def index?
|
||||
staff?
|
||||
|
@ -15,6 +16,7 @@ class StatusPolicy < ApplicationPolicy
|
|||
|
||||
def show?
|
||||
return false if author.suspended?
|
||||
return false unless expired_show?
|
||||
|
||||
if requires_mention?
|
||||
owned? || mention_exists?
|
||||
|
@ -25,6 +27,10 @@ class StatusPolicy < ApplicationPolicy
|
|||
end
|
||||
end
|
||||
|
||||
def expired_show?
|
||||
!expired? || owned? || favourited_status? || bookmarked_status? || emoji_reactioned_status?
|
||||
end
|
||||
|
||||
def reblog?
|
||||
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
|
||||
end
|
||||
|
@ -84,19 +90,37 @@ class StatusPolicy < ApplicationPolicy
|
|||
def blocking_author?
|
||||
return false if current_account.nil?
|
||||
|
||||
@preloaded_relations[:blocking] ? @preloaded_relations[:blocking][author.id] : current_account.blocking?(author)
|
||||
@preloaded_account_relations[:blocking] ? @preloaded_account_relations[:blocking][author.id] : current_account.blocking?(author)
|
||||
end
|
||||
|
||||
def author_blocking?
|
||||
return false if current_account.nil?
|
||||
|
||||
@preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account)
|
||||
@preloaded_account_relations[:blocked_by] ? @preloaded_account_relations[:blocked_by][author.id] : author.blocking?(current_account)
|
||||
end
|
||||
|
||||
def following_author?
|
||||
return false if current_account.nil?
|
||||
|
||||
@preloaded_relations[:following] ? @preloaded_relations[:following][author.id] : current_account.following?(author)
|
||||
@preloaded_account_relations[:following] ? @preloaded_account_relations[:following][author.id] : current_account.following?(author)
|
||||
end
|
||||
|
||||
def favourited_status?
|
||||
return false if current_account.nil?
|
||||
|
||||
@preloaded_status_relations[:favourites_map] ? @preloaded_status_relations[:favourites_map][record.id] : current_account.favourited?(record)
|
||||
end
|
||||
|
||||
def bookmarked_status?
|
||||
return false if current_account.nil?
|
||||
|
||||
@preloaded_status_relations[:bookmarks_map] ? @preloaded_status_relations[:bookmarks_map][record.id] : current_account.bookmarked?(record)
|
||||
end
|
||||
|
||||
def emoji_reactioned_status?
|
||||
return false if current_account.nil?
|
||||
|
||||
@preloaded_status_relations[:emoji_reactions_map] ? @preloaded_status_relations[:emoji_reactions_map][record.id] : current_account.emoji_reactioned?(record)
|
||||
end
|
||||
|
||||
def author
|
||||
|
|
|
@ -6,7 +6,7 @@ class AccountRelationshipsPresenter
|
|||
:endorsed, :account_note
|
||||
|
||||
def initialize(account_ids, current_account_id, **options)
|
||||
@account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a.to_i }
|
||||
@account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a.to_i }.uniq
|
||||
@current_account_id = current_account_id
|
||||
|
||||
@following = cached[:following]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
|
||||
attributes :id, :type, :actor, :published, :to, :cc, :virtual_object
|
||||
attributes :id, :type, :actor, :published, :expiry, :to, :cc, :virtual_object
|
||||
|
||||
class << self
|
||||
def from_status(status, use_bearcap: true)
|
||||
|
@ -12,6 +12,7 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
|
|||
presenter.published = status.created_at
|
||||
presenter.to = ActivityPub::TagManager.instance.to(status)
|
||||
presenter.cc = ActivityPub::TagManager.instance.cc(status)
|
||||
presenter.expiry = status.expiry
|
||||
|
||||
presenter.virtual_object = begin
|
||||
if status.reblog?
|
||||
|
|
|
@ -13,6 +13,7 @@ class StatusRelationshipsPresenter
|
|||
@mutes_map = {}
|
||||
@pins_map = {}
|
||||
else
|
||||
statuses = Status.where(id: statuses) if statuses.first.is_a?(Integer)
|
||||
statuses = statuses.compact
|
||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
||||
conversation_ids = statuses.filter_map(&:conversation_id).uniq
|
||||
|
|
|
@ -13,10 +13,15 @@ class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
|||
end
|
||||
|
||||
attributes :id, :type, :actor, :published, :to, :cc
|
||||
attribute :expiry, if: -> { object.expiry.present? }
|
||||
|
||||
has_one :virtual_object, key: :object
|
||||
|
||||
def published
|
||||
object.published.iso8601
|
||||
end
|
||||
|
||||
def expiry
|
||||
object.expiry.iso8601
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ class ActivityPub::DeleteSerializer < ActivityPub::Serializer
|
|||
end
|
||||
|
||||
attributes :id, :type, :actor, :to
|
||||
attribute :expiry, if: -> { expiry? }
|
||||
|
||||
has_one :object, serializer: TombstoneSerializer
|
||||
|
||||
|
@ -38,4 +39,12 @@ class ActivityPub::DeleteSerializer < ActivityPub::Serializer
|
|||
def to
|
||||
[ActivityPub::TagManager::COLLECTIONS[:public]]
|
||||
end
|
||||
|
||||
def expiry?
|
||||
instance_options && instance_options[:expiry].present?
|
||||
end
|
||||
|
||||
def expiry
|
||||
instance_options[:expiry].iso8601
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
attribute :content
|
||||
attribute :content_map, if: :language?
|
||||
|
||||
attribute :expiry, if: -> { object.expires? }
|
||||
attribute :expiry, if: :has_expiry?
|
||||
|
||||
has_many :media_attachments, key: :attachment
|
||||
has_many :virtual_tags, key: :tag
|
||||
|
@ -84,8 +84,12 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
object.created_at.iso8601
|
||||
end
|
||||
|
||||
def has_expiry?
|
||||
object.expiry.present?
|
||||
end
|
||||
|
||||
def expiry
|
||||
object.expires_at.iso8601
|
||||
object.expiry.iso8601
|
||||
end
|
||||
|
||||
def url
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
||||
attributes :id, :type, :actor, :to
|
||||
attribute :expiry, if: -> { expiry? }
|
||||
|
||||
has_one :virtual_object, key: :object, serializer: ActivityPub::ActivitySerializer
|
||||
|
||||
|
@ -24,4 +25,12 @@ class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
|||
def virtual_object
|
||||
ActivityPub::ActivityPresenter.from_status(object)
|
||||
end
|
||||
|
||||
def expiry?
|
||||
instance_options && instance_options[:expiry].present?
|
||||
end
|
||||
|
||||
def expiry
|
||||
instance_options[:expiry].expiry.iso8601
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,6 +61,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
max_characters: StatusLengthValidator::MAX_CHARS,
|
||||
max_media_attachments: 4,
|
||||
characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
|
||||
min_expiration: TimeLimit::VALID_DURATION.min,
|
||||
max_expiration: TimeLimit::VALID_DURATION.max,
|
||||
supported_expires_actions: StatusExpire::actions.keys,
|
||||
},
|
||||
|
||||
media_attachments: {
|
||||
|
|
|
@ -19,7 +19,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
|
||||
attribute :quote_id, if: :quote?
|
||||
|
||||
attribute :expires_at, if: :expires?
|
||||
attribute :expires_at, if: :has_expires?
|
||||
attribute :visibility_ex, if: :visibility_ex?
|
||||
|
||||
belongs_to :reblog, serializer: REST::StatusSerializer
|
||||
|
@ -66,8 +66,12 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
object.quote?
|
||||
end
|
||||
|
||||
def expires?
|
||||
object.expires?
|
||||
def has_expires?
|
||||
object.expires? || object.expired?
|
||||
end
|
||||
|
||||
def expires_at
|
||||
object&.status_expire&.expires_at || object.expired_at
|
||||
end
|
||||
|
||||
def visibility_ex?
|
||||
|
|
|
@ -62,15 +62,21 @@ class BatchedRemoveStatusService < BaseService
|
|||
private
|
||||
|
||||
def unpush_from_home_timelines(account, statuses)
|
||||
account.followers_for_local_distribution.includes(:user).reorder(nil).find_each do |follower|
|
||||
Account.where(id: Account
|
||||
.union(account.followers_for_local_distribution.reorder(nil).select(:id))
|
||||
.union(account.subscribers_for_local_distribution.reorder(nil).select('account_subscribes.account_id as id'))
|
||||
).includes(:user).find_each do |follower_and_subscriber|
|
||||
statuses.each do |status|
|
||||
FeedManager.instance.unpush_from_home(follower, status)
|
||||
FeedManager.instance.unpush_from_home(follower_and_subscriber, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unpush_from_list_timelines(account, statuses)
|
||||
account.lists_for_local_distribution.select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |list|
|
||||
List.where(id: List
|
||||
.union(account.lists_for_local_distribution.reorder(nil).select(:id))
|
||||
.union(account.list_subscribers_for_local_distribution.reorder(nil).select('list_id as id'))
|
||||
).includes(account: :user).find_each do |list|
|
||||
statuses.each do |status|
|
||||
FeedManager.instance.unpush_from_list(list, status)
|
||||
end
|
||||
|
|
26
app/services/pin_service.rb
Normal file
26
app/services/pin_service.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PinService < BaseService
|
||||
def call(account, status)
|
||||
@account = account
|
||||
@status = status
|
||||
|
||||
return unless @account == @status.account
|
||||
|
||||
StatusPin.create!(account: @account, status: @status)
|
||||
distribute_add_activity! if @account.local?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def distribute_add_activity!
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
@status,
|
||||
serializer: ActivityPub::AddSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json
|
||||
|
||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), @account.id)
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,7 @@ class PostStatusService < BaseService
|
|||
include Redisable
|
||||
|
||||
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
||||
MIN_EXPIRE_OFFSET = 40.seconds.freeze # The original intention is 60 seconds, but we have a margin of 20 seconds.
|
||||
|
||||
# Post a text status update, fetch and notify remote users mentioned
|
||||
# @param [Account] account Account from which to post
|
||||
|
@ -35,6 +36,7 @@ class PostStatusService < BaseService
|
|||
return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
|
||||
|
||||
validate_media!
|
||||
validate_expires!
|
||||
preprocess_attributes!
|
||||
preprocess_quote!
|
||||
|
||||
|
@ -75,8 +77,6 @@ class PostStatusService < BaseService
|
|||
@visibility = :limited if @visibility&.to_sym != :direct && @in_reply_to&.limited_visibility?
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
@scheduled_at = nil if scheduled_in_the_past?
|
||||
@expires_at = @options[:expires_at]&.to_datetime
|
||||
@expires_action = @options[:expires_action]
|
||||
if @quote_id.nil? && md = @text.match(/QT:\s*\[\s*(https:\/\/.+?)\s*\]/)
|
||||
@quote_id = quote_from_url(md[1])&.id
|
||||
@text.sub!(/QT:\s*\[.*?\]/, '')
|
||||
|
@ -127,7 +127,12 @@ class PostStatusService < BaseService
|
|||
DistributionWorker.perform_async(@status.id)
|
||||
ActivityPub::DistributionWorker.perform_async(@status.id)
|
||||
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
||||
DeleteExpiredStatusWorker.perform_at(@status.expires_at, @status.id) if @status.expires? && @status.expires_delete?
|
||||
@status.status_expire.queue_action if expires_soon?
|
||||
end
|
||||
|
||||
def expires_soon?
|
||||
expires_at = @status&.status_expire&.expires_at
|
||||
expires_at.present? && expires_at <= Time.now.utc + MIN_SCHEDULE_OFFSET
|
||||
end
|
||||
|
||||
def validate_media!
|
||||
|
@ -141,6 +146,26 @@ class PostStatusService < BaseService
|
|||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if @media.any?(&:not_processed?)
|
||||
end
|
||||
|
||||
def validate_expires!
|
||||
return if @options[:expires_at].blank?
|
||||
|
||||
@expires_at = @options[:expires_at].is_a?(Time) ? @options[:expires_at] : @options[:expires_at].to_time rescue nil
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('status_expire.validations.invalid_expire_at') if @expires_at.nil?
|
||||
raise Mastodon::ValidationError, I18n.t('status_expire.validations.expire_in_the_past') if @expires_at <= Time.now.utc + MIN_EXPIRE_OFFSET
|
||||
|
||||
@expires_action = begin
|
||||
case @options[:expires_action]&.to_sym
|
||||
when :hint, :mark, nil
|
||||
:mark
|
||||
when :delete
|
||||
:delete
|
||||
else
|
||||
raise Mastodon::ValidationError, I18n.t('status_expire.validations.invalid_expire_action')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def language_from_option(str)
|
||||
ISO_639.find(str)&.alpha2
|
||||
end
|
||||
|
|
|
@ -8,12 +8,14 @@ class ProcessHashtagsService < BaseService
|
|||
Tag.find_or_create_by_names(tags) do |tag|
|
||||
status.tags << tag
|
||||
records << tag
|
||||
tag.use!(status.account, status: status, at_time: status.created_at) if status.public_visibility?
|
||||
tag.use!(status.account, status: status, at_time: status.created_at) if status.public_visibility? && !tag.name.match(TimeLimit::TIME_LIMIT_RE)
|
||||
end
|
||||
|
||||
time_limit = TimeLimit.from_status(status)
|
||||
if (time_limit.present?)
|
||||
status.update(expires_at: time_limit.to_datetime, expires_action: :delete)
|
||||
if status.local?
|
||||
time_limit = TimeLimit.from_status(status)
|
||||
if (time_limit.present?)
|
||||
status.update(expires_at: time_limit.to_datetime, expires_action: :mark)
|
||||
end
|
||||
end
|
||||
|
||||
return unless status.distributable?
|
||||
|
|
|
@ -10,19 +10,25 @@ class RemoveStatusService < BaseService
|
|||
# @option [Boolean] :redraft
|
||||
# @option [Boolean] :immediate
|
||||
# @option [Boolean] :original_removed
|
||||
# @option [Boolean] :mark_expired
|
||||
def call(status, **options)
|
||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||
@status = status
|
||||
@account = status.account
|
||||
@options = options
|
||||
@status = status
|
||||
@account = status.account
|
||||
@options = options
|
||||
@status_expire = status.status_expire
|
||||
@payload = Oj.dump(event: mark_expired? ? :expire : :delete, payload: status.id.to_s)
|
||||
|
||||
@status.discard
|
||||
return if mark_expired? && @status_expire.nil?
|
||||
|
||||
@status.discard unless mark_expired?
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
remove_from_self if @account.local?
|
||||
remove_from_followers
|
||||
remove_from_lists
|
||||
remove_from_subscribers
|
||||
remove_from_subscribers_lists
|
||||
|
||||
# There is no reason to send out Undo activities when the
|
||||
# cause is that the original object has been removed, since
|
||||
|
@ -41,10 +47,17 @@ class RemoveStatusService < BaseService
|
|||
remove_from_group if status.account.group?
|
||||
remove_from_public
|
||||
remove_from_media if @status.media_attachments.any?
|
||||
remove_media
|
||||
remove_media unless mark_expired?
|
||||
end
|
||||
|
||||
@status.destroy! if @options[:immediate] || !@status.reported?
|
||||
if mark_expired?
|
||||
UnpinService.new.call(@account, @status)
|
||||
@status.update!(expired_at: @status_expire.expires_at)
|
||||
@status_expire.destroy
|
||||
else
|
||||
@status_expire&.destroy
|
||||
@status.destroy! if @options[:immediate] || !@status.reported?
|
||||
end
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
|
@ -53,19 +66,35 @@ class RemoveStatusService < BaseService
|
|||
|
||||
private
|
||||
|
||||
def mark_expired?
|
||||
@options[:mark_expired]
|
||||
end
|
||||
|
||||
def remove_from_self
|
||||
FeedManager.instance.unpush_from_home(@account, @status)
|
||||
FeedManager.instance.unpush_from_home(@account, @status, @options)
|
||||
end
|
||||
|
||||
def remove_from_followers
|
||||
@account.followers_for_local_distribution.reorder(nil).find_each do |follower|
|
||||
FeedManager.instance.unpush_from_home(follower, @status)
|
||||
@account.followers_for_local_distribution.includes(:user).reorder(nil).find_each do |follower|
|
||||
FeedManager.instance.unpush_from_home(follower, @status, @options)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_lists
|
||||
@account.lists_for_local_distribution.select(:id, :account_id).reorder(nil).find_each do |list|
|
||||
FeedManager.instance.unpush_from_list(list, @status)
|
||||
@account.lists_for_local_distribution.select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |list|
|
||||
FeedManager.instance.unpush_from_list(list, @status, @options)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_subscribers
|
||||
@account.subscribers_for_local_distribution.with_reblog(@status.reblog?).with_media(@status.proper).includes(account: :user).reorder(nil).find_each do |subscribing|
|
||||
FeedManager.instance.unpush_from_home(subscribing.account, @status, @options)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_subscribers_lists
|
||||
@account.list_subscribers_for_local_distribution.with_reblog(@status.reblog?).with_media(@status.proper).includes(account: :user).reorder(nil).find_each do |subscribing|
|
||||
FeedManager.instance.unpush_from_list(subscribing.list, @status, @options)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -95,7 +124,7 @@ class RemoveStatusService < BaseService
|
|||
end
|
||||
|
||||
def signed_activity_json
|
||||
@signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
|
||||
@signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account, expiry: mark_expired? ? @status.expiry : nil))
|
||||
end
|
||||
|
||||
def remove_reblogs
|
||||
|
|
|
@ -48,11 +48,12 @@ class SearchService < BaseService
|
|||
definition = definition.filter(range: { id: range })
|
||||
end
|
||||
|
||||
results = definition.limit(@limit).offset(@offset).objects.compact
|
||||
account_ids = results.map(&:account_id)
|
||||
preloaded_relations = relations_map_for_account(@account, account_ids)
|
||||
results = definition.limit(@limit).offset(@offset).objects.compact
|
||||
account_ids = results.map(&:account_id)
|
||||
account_relations = relations_map_for_account(@account, account_ids)
|
||||
status_relations = relations_map_for_status(@account, results)
|
||||
|
||||
results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
|
||||
results.reject { |status| StatusFilter.new(status, @account, account_relations, status_relations).filtered? }
|
||||
rescue Faraday::ConnectionFailed, Parslet::ParseFailed
|
||||
[]
|
||||
end
|
||||
|
@ -123,6 +124,18 @@ class SearchService < BaseService
|
|||
}
|
||||
end
|
||||
|
||||
def relations_map_for_status(account, statuses)
|
||||
presenter = StatusRelationshipsPresenter.new(statuses, account)
|
||||
{
|
||||
reblogs_map: presenter.reblogs_map,
|
||||
favourites_map: presenter.favourites_map,
|
||||
bookmarks_map: presenter.bookmarks_map,
|
||||
emoji_reactions_map: presenter.emoji_reactions_map,
|
||||
mutes_map: presenter.mutes_map,
|
||||
pins_map: presenter.pins_map,
|
||||
}
|
||||
end
|
||||
|
||||
def parsed_query
|
||||
SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
|
||||
end
|
||||
|
|
29
app/services/unpin_service.rb
Normal file
29
app/services/unpin_service.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UnpinService < BaseService
|
||||
def call(account, status)
|
||||
@account = account
|
||||
@status = status
|
||||
|
||||
return unless @account == @status.account
|
||||
|
||||
pin = StatusPin.find_by(account: @account, status: @status)
|
||||
|
||||
if pin
|
||||
pin.destroy!
|
||||
distribute_remove_activity! if @account.local?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def distribute_remove_activity!
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
@status,
|
||||
serializer: ActivityPub::RemoveSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json
|
||||
|
||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), @account.id)
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeleteExpiredStatusWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options retry: 0, dead: false
|
||||
|
||||
def perform(status_id)
|
||||
@status = Status.include_expired.find(status_id)
|
||||
RemoveStatusService.new.call(@status, redraft: false)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
16
app/workers/expired_status_worker.rb
Normal file
16
app/workers/expired_status_worker.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ExpiredStatusWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options retry: 0, dead: false
|
||||
|
||||
def perform(status_id)
|
||||
status = Status.find(status_id)
|
||||
status_expire = status.status_expire
|
||||
|
||||
RemoveStatusService.new.call(status, redraft: false, mark_expired: status_expire.present? && status_expire.expires_mark?)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ class RemovalWorker
|
|||
include Sidekiq::Worker
|
||||
|
||||
def perform(status_id, options = {})
|
||||
RemoveStatusService.new.call(Status.with_discarded.find(status_id), **options.symbolize_keys)
|
||||
RemoveStatusService.new.call(Status.include_expired.with_discarded.find(status_id), **options.symbolize_keys)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class Scheduler::ScheduledStatusesScheduler
|
|||
publish_scheduled_statuses!
|
||||
publish_scheduled_announcements!
|
||||
unpublish_expired_announcements!
|
||||
process_expired_statuses!
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -40,4 +41,12 @@ class Scheduler::ScheduledStatusesScheduler
|
|||
def expired_announcements
|
||||
Announcement.published.where('ends_at IS NOT NULL AND ends_at <= ?', Time.now.utc)
|
||||
end
|
||||
|
||||
def process_expired_statuses!
|
||||
due_status_expires.find_each(&:queue_action)
|
||||
end
|
||||
|
||||
def due_status_expires
|
||||
StatusExpire.where('expires_at <= ?', Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1436,6 +1436,11 @@ en:
|
|||
min_favs_hint: Doesn't delete any of your posts that has received more than this amount of favourites. Leave blank to delete posts regardless of their number of favourites
|
||||
min_reblogs: Keep posts boosted more than
|
||||
min_reblogs_hint: Doesn't delete any of your posts that has been boosted more than this number of times. Leave blank to delete posts regardless of their number of boosts
|
||||
status_expire:
|
||||
validations:
|
||||
expire_in_the_past: It is already past expires. You cannot specify expires before the posting time.
|
||||
invalid_expire_at: Invalid expires are specified.
|
||||
invalid_expire_action: Invalid expires_action are specified.
|
||||
stream_entries:
|
||||
pinned: Pinned post
|
||||
reblogged: boosted
|
||||
|
|
|
@ -1339,6 +1339,11 @@ ja:
|
|||
public_long: 誰でも見ることができ、かつ公開タイムラインに表示されます
|
||||
unlisted: 未収載
|
||||
unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません
|
||||
status_expire:
|
||||
validations:
|
||||
expire_in_the_past: 既に公開期限を過ぎています。投稿時間より前の公開期限は指定できません
|
||||
invalid_expire_at: 不正な公開期限が指定されています
|
||||
invalid_expire_action: 不正な公開期限時のアクションが指定されています
|
||||
stream_entries:
|
||||
pinned: 固定された投稿
|
||||
reblogged: さんがブースト
|
||||
|
|
11
db/migrate/20210706063140_create_status_expires.rb
Normal file
11
db/migrate/20210706063140_create_status_expires.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
class CreateStatusExpires < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :status_expires do |t|
|
||||
t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
|
||||
t.datetime :expires_at, null: false, index: true
|
||||
t.integer :action, default: 0, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
17
db/migrate/20210708030726_add_expired_at_to_status.rb
Normal file
17
db/migrate/20210708030726_add_expired_at_to_status.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class AddExpiredAtToStatus < ActiveRecord::Migration[6.1]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
safety_assured { add_column :statuses, :expired_at, :datetime, default: nil, allow_null: true }
|
||||
safety_assured { add_index :statuses, :expired_at, algorithm: :concurrently, name: :index_statuses_on_expired_at }
|
||||
end
|
||||
|
||||
def down
|
||||
remove_index :statuses, name: :index_statuses_on_expired_at
|
||||
remove_column :statuses, :expired_at
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
class UpdateStatusesIndexExcludeExpired < ActiveRecord::Migration[6.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], where: 'deleted_at IS NULL', order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20210710 }
|
||||
remove_index :statuses, name: :index_statuses_20210627
|
||||
end
|
||||
|
||||
def down
|
||||
safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at, :expires_at], where: 'deleted_at IS NULL', order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20210627 }
|
||||
remove_index :statuses, name: :index_statuses_20210710
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
class MigrateToNormalizeExpires < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
def up
|
||||
safety_assured do
|
||||
execute 'insert into status_expires (status_id, expires_at, action, created_at, updated_at) select id as status_id, expires_at, expires_action as action, created_at, updated_at from statuses where expires_at is not null and expires_at != \'infinity\';'
|
||||
remove_column :statuses, :expires_at
|
||||
remove_column :statuses, :expires_action
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
safety_assured do
|
||||
add_column :statuses, :expires_at, :datetime
|
||||
add_column :statuses, :expires_action, :integer, default: 0, null: false
|
||||
execute 'update statuses set expires_at = se.expires_at, expires_action = se.action from status_expires se;'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
18
db/schema.rb
18
db/schema.rb
|
@ -926,6 +926,16 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
|
|||
t.index ["status_id"], name: "index_status_capability_tokens_on_status_id"
|
||||
end
|
||||
|
||||
create_table "status_expires", force: :cascade do |t|
|
||||
t.bigint "status_id", null: false
|
||||
t.datetime "expires_at", null: false
|
||||
t.integer "action", default: 0, null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["expires_at"], name: "index_status_expires_on_expires_at"
|
||||
t.index ["status_id"], name: "index_status_expires_on_status_id", unique: true
|
||||
end
|
||||
|
||||
create_table "status_pins", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "status_id", null: false
|
||||
|
@ -964,11 +974,10 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
|
|||
t.bigint "application_id"
|
||||
t.bigint "in_reply_to_account_id"
|
||||
t.bigint "poll_id"
|
||||
t.bigint "quote_id"
|
||||
t.datetime "deleted_at"
|
||||
t.datetime "expires_at", default: ::Float::INFINITY, null: false
|
||||
t.integer "expires_action", default: 0, null: false
|
||||
t.index ["account_id", "id", "visibility", "updated_at", "expires_at"], name: "index_statuses_20210627", order: { id: :desc }, where: "(deleted_at IS NULL)"
|
||||
t.bigint "quote_id"
|
||||
t.datetime "expired_at"
|
||||
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20210710", order: { id: :desc }, where: "((deleted_at IS NULL) AND (expired_at IS NULL))"
|
||||
t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
|
||||
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
|
||||
|
@ -1219,6 +1228,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
|
|||
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade
|
||||
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
|
||||
add_foreign_key "status_capability_tokens", "statuses"
|
||||
add_foreign_key "status_expires", "statuses", 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_stats", "statuses", on_delete: :cascade
|
||||
|
|
4
spec/fabricators/status_expire_fabricator.rb
Normal file
4
spec/fabricators/status_expire_fabricator.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:status_expire) do
|
||||
status_id nil
|
||||
action 0
|
||||
end
|
4
spec/models/status_expire_spec.rb
Normal file
4
spec/models/status_expire_spec.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe StatusExpire, type: :model do
|
||||
end
|
Loading…
Reference in a new issue