Merge branch 'asonix/changes' into asonix/downstream
This commit is contained in:
commit
8e5294931e
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -3,6 +3,40 @@ Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [2.6.2] - 2018-11-23
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add Page to whitelisted ActivityPub types (#9188)
|
||||||
|
- Add 20px to column width in web UI (#9227)
|
||||||
|
- Add amount of freed disk space in `tootctl media remove` (#9229, #9239, #9288)
|
||||||
|
- Add "Show thread" link to self-replies (#9228)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change order of Atom and RSS links so Atom is first (#9302)
|
||||||
|
- Change Nginx configuration for Nanobox apps (#9310)
|
||||||
|
- Change the follow action to appear instant in web UI (#9220)
|
||||||
|
- Change how the ActiveRecord connection is instantiated in on_worker_boot (#9238)
|
||||||
|
- Change `tootctl accounts cull` to always touch accounts so they can be skipped (#9293)
|
||||||
|
- Change mime type comparison to ignore JSON-LD profile (#9179)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix web UI crash when conversation has no last status (#9207)
|
||||||
|
- Fix follow limit validator reporting lower number past threshold (#9230)
|
||||||
|
- Fix form validation flash message color and input borders (#9235)
|
||||||
|
- Fix invalid twitter:player cards being displayed (#9254)
|
||||||
|
- Fix emoji update date being processed incorrectly (#9255)
|
||||||
|
- Fix playing embed resetting if status is reloaded in web UI (#9270, #9275)
|
||||||
|
- Fix web UI crash when favouriting a deleted status (#9272)
|
||||||
|
- Fix intermediary arrays being created for hash maps (#9291)
|
||||||
|
- Fix filter ID not being a string in REST API (#9303)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix multiple remote account deletions being able to deadlock the database (#9292)
|
||||||
|
- Fix HTTP connection timeout of 10s not being enforced (#9329)
|
||||||
|
|
||||||
## [2.6.1] - 2018-10-30
|
## [2.6.1] - 2018-10-30
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs))
|
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
||||||
|
|
||||||
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ class ApplicationController < ActionController::Base
|
||||||
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
||||||
|
|
||||||
unless uncached_ids.empty?
|
unless uncached_ids.empty?
|
||||||
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
|
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
|
||||||
|
|
||||||
uncached.each_value do |item|
|
uncached.each_value do |item|
|
||||||
Rails.cache.write(item, item)
|
Rails.cache.write(item, item)
|
||||||
|
|
|
@ -145,12 +145,14 @@ export function fetchAccountFail(id, error) {
|
||||||
export function followAccount(id, reblogs = true) {
|
export function followAccount(id, reblogs = true) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
dispatch(followAccountRequest(id));
|
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||||
|
|
||||||
|
dispatch(followAccountRequest(id, locked));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(followAccountFail(error));
|
dispatch(followAccountFail(error, locked));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -167,10 +169,12 @@ export function unfollowAccount(id) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function followAccountRequest(id) {
|
export function followAccountRequest(id, locked) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_FOLLOW_REQUEST,
|
type: ACCOUNT_FOLLOW_REQUEST,
|
||||||
id,
|
id,
|
||||||
|
locked,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,13 +183,16 @@ export function followAccountSuccess(relationship, alreadyFollowing) {
|
||||||
type: ACCOUNT_FOLLOW_SUCCESS,
|
type: ACCOUNT_FOLLOW_SUCCESS,
|
||||||
relationship,
|
relationship,
|
||||||
alreadyFollowing,
|
alreadyFollowing,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function followAccountFail(error) {
|
export function followAccountFail(error, locked) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_FOLLOW_FAIL,
|
type: ACCOUNT_FOLLOW_FAIL,
|
||||||
error,
|
error,
|
||||||
|
locked,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -193,6 +200,7 @@ export function unfollowAccountRequest(id) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNFOLLOW_REQUEST,
|
type: ACCOUNT_UNFOLLOW_REQUEST,
|
||||||
id,
|
id,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -201,6 +209,7 @@ export function unfollowAccountSuccess(relationship, statuses) {
|
||||||
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
||||||
relationship,
|
relationship,
|
||||||
statuses,
|
statuses,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -208,6 +217,7 @@ export function unfollowAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNFOLLOW_FAIL,
|
type: ACCOUNT_UNFOLLOW_FAIL,
|
||||||
error,
|
error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ class Status extends ImmutablePureComponent {
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
onMoveDown: PropTypes.func,
|
onMoveDown: PropTypes.func,
|
||||||
|
showThread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -168,7 +169,7 @@ class Status extends ImmutablePureComponent {
|
||||||
let media = null;
|
let media = null;
|
||||||
let statusAvatar, prepend, rebloggedByText;
|
let statusAvatar, prepend, rebloggedByText;
|
||||||
|
|
||||||
const { intl, hidden, featured, otherAccounts, unread } = this.props;
|
const { intl, hidden, featured, otherAccounts, unread, showThread } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
|
@ -309,6 +310,12 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
|
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||||
|
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||||
|
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatusActionBar status={status} account={account} {...other} />
|
<StatusActionBar status={status} account={account} {...other} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -148,7 +148,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
let replyIcon;
|
|
||||||
let replyTitle;
|
let replyTitle;
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
@ -191,10 +190,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('in_reply_to_id', null) === null) {
|
if (status.get('in_reply_to_id', null) === null) {
|
||||||
replyIcon = 'reply';
|
|
||||||
replyTitle = intl.formatMessage(messages.reply);
|
replyTitle = intl.formatMessage(messages.reply);
|
||||||
} else {
|
} else {
|
||||||
replyIcon = 'reply-all';
|
|
||||||
replyTitle = intl.formatMessage(messages.replyAll);
|
replyTitle = intl.formatMessage(messages.replyAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +201,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||||
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
||||||
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate 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' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
|
@ -104,6 +104,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
|
showThread
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -117,6 +118,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
|
showThread
|
||||||
/>
|
/>
|
||||||
)).concat(scrollableContent);
|
)).concat(scrollableContent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} 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')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default class Card extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (this.props.card !== nextProps.card) {
|
if (!Immutable.is(this.props.card, nextProps.card)) {
|
||||||
this.setState({ embedded: false });
|
this.setState({ embedded: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,13 @@ const expandNormalizedConversations = (state, conversations, next) => {
|
||||||
|
|
||||||
list = list.concat(items);
|
list = list.concat(items);
|
||||||
|
|
||||||
return list.sortBy(x => x.get('last_status'), (a, b) => compareId(a, b) * -1);
|
return list.sortBy(x => x.get('last_status'), (a, b) => {
|
||||||
|
if(a === null || b === null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareId(a, b) * -1;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import {
|
import {
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
ACCOUNT_FOLLOW_SUCCESS,
|
||||||
|
ACCOUNT_FOLLOW_REQUEST,
|
||||||
|
ACCOUNT_FOLLOW_FAIL,
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||||
|
ACCOUNT_UNFOLLOW_REQUEST,
|
||||||
|
ACCOUNT_UNFOLLOW_FAIL,
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
ACCOUNT_UNBLOCK_SUCCESS,
|
ACCOUNT_UNBLOCK_SUCCESS,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
|
@ -37,6 +41,14 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function relationships(state = initialState, action) {
|
export default function relationships(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case ACCOUNT_FOLLOW_REQUEST:
|
||||||
|
return state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
||||||
|
case ACCOUNT_FOLLOW_FAIL:
|
||||||
|
return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
|
||||||
|
case ACCOUNT_UNFOLLOW_REQUEST:
|
||||||
|
return state.setIn([action.id, 'following'], false);
|
||||||
|
case ACCOUNT_UNFOLLOW_FAIL:
|
||||||
|
return state.setIn([action.id, 'following'], true);
|
||||||
case ACCOUNT_FOLLOW_SUCCESS:
|
case ACCOUNT_FOLLOW_SUCCESS:
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
case ACCOUNT_UNFOLLOW_SUCCESS:
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
|
|
|
@ -38,11 +38,11 @@ export default function statuses(state = initialState, action) {
|
||||||
case FAVOURITE_REQUEST:
|
case FAVOURITE_REQUEST:
|
||||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
return state.setIn([action.status.get('id'), 'favourited'], true);
|
||||||
case FAVOURITE_FAIL:
|
case FAVOURITE_FAIL:
|
||||||
return state.setIn([action.status.get('id'), 'favourited'], false);
|
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
||||||
case REBLOG_REQUEST:
|
case REBLOG_REQUEST:
|
||||||
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
||||||
case REBLOG_FAIL:
|
case REBLOG_FAIL:
|
||||||
return state.setIn([action.status.get('id'), 'reblogged'], false);
|
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
|
||||||
case STATUS_MUTE_SUCCESS:
|
case STATUS_MUTE_SUCCESS:
|
||||||
return state.setIn([action.id, 'muted'], true);
|
return state.setIn([action.id, 'muted'], true);
|
||||||
case STATUS_UNMUTE_SUCCESS:
|
case STATUS_UNMUTE_SUCCESS:
|
||||||
|
|
|
@ -1847,7 +1847,7 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
width: 330px;
|
width: 350px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -330,9 +330,12 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text],
|
input[type=text],
|
||||||
|
input[type=number],
|
||||||
input[type=email],
|
input[type=email],
|
||||||
input[type=password] {
|
input[type=password],
|
||||||
border-bottom-color: $valid-value-color;
|
textarea,
|
||||||
|
select {
|
||||||
|
border-color: lighten($error-red, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
|
@ -129,4 +129,10 @@ class ActivityPub::Activity
|
||||||
::FetchRemoteStatusService.new.call(@object['url'])
|
::FetchRemoteStatusService.new.call(@object['url'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock_or_return(key, expire_after = 7.days.seconds)
|
||||||
|
yield if redis.set(key, true, nx: true, ex: expire_after)
|
||||||
|
ensure
|
||||||
|
redis.del(key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -177,7 +177,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
updated = tag['updated']
|
updated = tag['updated']
|
||||||
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
|
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
|
||||||
|
|
||||||
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
|
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
|
||||||
|
|
||||||
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
||||||
emoji.image_remote_url = image_url
|
emoji.image_remote_url = image_url
|
||||||
|
|
|
@ -12,8 +12,10 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
private
|
private
|
||||||
|
|
||||||
def delete_person
|
def delete_person
|
||||||
SuspendAccountService.new.call(@account)
|
lock_or_return("delete_in_progress:#{@account.id}") do
|
||||||
@account.destroy!
|
SuspendAccountService.new.call(@account)
|
||||||
|
@account.destroy!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_note
|
def delete_note
|
||||||
|
|
|
@ -21,7 +21,7 @@ class EntityCache
|
||||||
end
|
end
|
||||||
|
|
||||||
unless uncached_ids.empty?
|
unless uncached_ids.empty?
|
||||||
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).map { |item| [item.shortcode, item] }.to_h
|
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).each_with_object({}) { |item, h| h[item.shortcode] = item }
|
||||||
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
|
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -128,9 +128,9 @@ class Formatter
|
||||||
return html if emojis.empty?
|
return html if emojis.empty?
|
||||||
|
|
||||||
emoji_map = if animate
|
emoji_map = if animate
|
||||||
emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
|
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url) }
|
||||||
else
|
else
|
||||||
emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
|
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url(:static)) }
|
||||||
end
|
end
|
||||||
|
|
||||||
i = -1
|
i = -1
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
require 'ipaddr'
|
require 'ipaddr'
|
||||||
require 'socket'
|
require 'socket'
|
||||||
|
require 'resolv'
|
||||||
|
|
||||||
class Request
|
class Request
|
||||||
REQUEST_TARGET = '(request-target)'
|
REQUEST_TARGET = '(request-target)'
|
||||||
|
@ -45,7 +46,7 @@ class Request
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
yield response.extend(ClientLimit)
|
yield response.extend(ClientLimit) if block_given?
|
||||||
ensure
|
ensure
|
||||||
http_client.close
|
http_client.close
|
||||||
end
|
end
|
||||||
|
@ -94,7 +95,7 @@ class Request
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
def timeout
|
||||||
{ write: 10, connect: 10, read: 10 }
|
{ connect: nil, read: 10, write: 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
def http_client
|
def http_client
|
||||||
|
@ -139,16 +140,29 @@ class Request
|
||||||
class Socket < TCPSocket
|
class Socket < TCPSocket
|
||||||
class << self
|
class << self
|
||||||
def open(host, *args)
|
def open(host, *args)
|
||||||
return super host, *args if thru_hidden_service? host
|
return super(host, *args) if thru_hidden_service?(host)
|
||||||
|
|
||||||
outer_e = nil
|
outer_e = nil
|
||||||
Addrinfo.foreach(host, nil, nil, :SOCK_STREAM) do |address|
|
|
||||||
begin
|
Resolv::DNS.open do |dns|
|
||||||
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address? IPAddr.new(address.ip_address)
|
dns.timeouts = 1
|
||||||
return super address.ip_address, *args
|
|
||||||
rescue => e
|
addresses = dns.getaddresses(host).take(2)
|
||||||
outer_e = e
|
time_slot = 10.0 / addresses.size
|
||||||
|
|
||||||
|
addresses.each do |address|
|
||||||
|
begin
|
||||||
|
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
|
||||||
|
|
||||||
|
::Timeout.timeout(time_slot, HTTP::TimeoutError) do
|
||||||
|
return super(address.to_s, *args)
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
outer_e = e
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
raise outer_e if outer_e
|
raise outer_e if outer_e
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ module Settings
|
||||||
|
|
||||||
def all_as_records
|
def all_as_records
|
||||||
vars = thing_scoped
|
vars = thing_scoped
|
||||||
records = vars.map { |r| [r.var, r] }.to_h
|
records = vars.each_with_object({}) { |r, h| h[r.var] = r }
|
||||||
|
|
||||||
Setting.default_settings.each do |key, default_value|
|
Setting.default_settings.each do |key, default_value|
|
||||||
next if records.key?(key) || default_value.is_a?(Hash)
|
next if records.key?(key) || default_value.is_a?(Hash)
|
||||||
|
@ -65,7 +65,7 @@ module Settings
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def default_settings
|
def default_settings
|
||||||
defaulting = DEFAULTING_TO_UNSCOPED.map { |k| [k, Setting[k]] }.to_h
|
defaulting = DEFAULTING_TO_UNSCOPED.each_with_object({}) { |k, h| h[k] = Setting[k] }
|
||||||
Setting.default_settings.merge!(defaulting)
|
Setting.default_settings.merge!(defaulting)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,9 +45,9 @@ module AccountInteractions
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_blocking_map(target_account_ids, account_id)
|
def domain_blocking_map(target_account_ids, account_id)
|
||||||
accounts_map = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
|
accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
|
||||||
blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
|
blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
|
||||||
accounts_map.map { |id, domain| [id, blocked_domains[domain]] }.to_h
|
accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_blocking_map_by_domain(target_domains, account_id)
|
def domain_blocking_map_by_domain(target_domains, account_id)
|
||||||
|
|
|
@ -75,7 +75,7 @@ class Notification < ApplicationRecord
|
||||||
|
|
||||||
return if account_ids.empty?
|
return if account_ids.empty?
|
||||||
|
|
||||||
accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
|
accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
|
||||||
|
|
||||||
cached_items.each do |item|
|
cached_items.each do |item|
|
||||||
item.from_account = accounts[item.from_account_id]
|
item.from_account = accounts[item.from_account_id]
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Setting < RailsSettings::Base
|
||||||
|
|
||||||
def all_as_records
|
def all_as_records
|
||||||
vars = thing_scoped
|
vars = thing_scoped
|
||||||
records = vars.map { |r| [r.var, r] }.to_h
|
records = vars.each_with_object({}) { |r, h| h[r.var] = r }
|
||||||
|
|
||||||
default_settings.each do |key, default_value|
|
default_settings.each do |key, default_value|
|
||||||
next if records.key?(key) || default_value.is_a?(Hash)
|
next if records.key?(key) || default_value.is_a?(Hash)
|
||||||
|
|
|
@ -310,19 +310,19 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def favourites_map(status_ids, account_id)
|
def favourites_map(status_ids, account_id)
|
||||||
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
|
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def reblogs_map(status_ids, account_id)
|
def reblogs_map(status_ids, account_id)
|
||||||
select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h
|
select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def mutes_map(conversation_ids, account_id)
|
def mutes_map(conversation_ids, account_id)
|
||||||
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h
|
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def pins_map(status_ids, account_id)
|
def pins_map(status_ids, account_id)
|
||||||
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h
|
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload_stale_associations!(cached_items)
|
def reload_stale_associations!(cached_items)
|
||||||
|
@ -337,7 +337,7 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
return if account_ids.empty?
|
return if account_ids.empty?
|
||||||
|
|
||||||
accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
|
accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
|
||||||
|
|
||||||
cached_items.each do |item|
|
cached_items.each do |item|
|
||||||
item.account = accounts[item.account_id]
|
item.account = accounts[item.account_id]
|
||||||
|
|
|
@ -18,7 +18,7 @@ class TrendingTags
|
||||||
def get(limit)
|
def get(limit)
|
||||||
key = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}"
|
key = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}"
|
||||||
tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i)
|
tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i)
|
||||||
tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h
|
tags = Tag.where(id: tag_ids).to_a.each_with_object({}) { |tag, h| h[tag.id] = tag }
|
||||||
tag_ids.map { |tag_id| tags[tag_id] }.compact
|
tag_ids.map { |tag_id| tags[tag_id] }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,4 +3,8 @@
|
||||||
class REST::FilterSerializer < ActiveModel::Serializer
|
class REST::FilterSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :phrase, :context, :whole_word, :expires_at,
|
attributes :id, :phrase, :context, :whole_word, :expires_at,
|
||||||
:irreversible
|
:irreversible
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -232,7 +232,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
updated = tag['updated']
|
updated = tag['updated']
|
||||||
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
|
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
|
||||||
|
|
||||||
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
|
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
|
||||||
|
|
||||||
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
||||||
emoji.image_remote_url = image_url
|
emoji.image_remote_url = image_url
|
||||||
|
|
|
@ -12,12 +12,12 @@ class BatchedRemoveStatusService < BaseService
|
||||||
def call(statuses)
|
def call(statuses)
|
||||||
statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }
|
statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }
|
||||||
|
|
||||||
@mentions = statuses.map { |s| [s.id, s.active_mentions.includes(:account).to_a] }.to_h
|
@mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a }
|
||||||
@tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h
|
@tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) }
|
||||||
|
|
||||||
@stream_entry_batches = []
|
@stream_entry_batches = []
|
||||||
@salmon_batches = []
|
@salmon_batches = []
|
||||||
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
|
@json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) }
|
||||||
@activity_xml = {}
|
@activity_xml = {}
|
||||||
|
|
||||||
# Ensure that rendered XML reflects destroyed state
|
# Ensure that rendered XML reflects destroyed state
|
||||||
|
|
|
@ -18,6 +18,6 @@ module AuthorExtractor
|
||||||
acct = "#{username}@#{domain}"
|
acct = "#{username}@#{domain}"
|
||||||
end
|
end
|
||||||
|
|
||||||
ResolveAccountService.new.call(acct, update_profile)
|
ResolveAccountService.new.call(acct, update_profile: update_profile)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ class FetchAtomService < BaseService
|
||||||
|
|
||||||
def perform_request(&block)
|
def perform_request(&block)
|
||||||
accept = 'text/html'
|
accept = 'text/html'
|
||||||
accept = 'application/activity+json, application/ld+json, application/atom+xml, ' + accept unless @unsupported_activity
|
accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity
|
||||||
|
|
||||||
Request.new(:get, @url).add_headers('Accept' => accept).perform(&block)
|
Request.new(:get, @url).add_headers('Accept' => accept).perform(&block)
|
||||||
end
|
end
|
||||||
|
@ -39,7 +39,7 @@ class FetchAtomService < BaseService
|
||||||
|
|
||||||
if response.mime_type == 'application/atom+xml'
|
if response.mime_type == 'application/atom+xml'
|
||||||
[@url, { prefetched_body: response.body_with_limit }, :ostatus]
|
[@url, { prefetched_body: response.body_with_limit }, :ostatus]
|
||||||
elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(response.mime_type)
|
elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
|
||||||
body = response.body_with_limit
|
body = response.body_with_limit
|
||||||
json = body_to_json(body)
|
json = body_to_json(body)
|
||||||
if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?
|
if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?
|
||||||
|
|
|
@ -136,14 +136,15 @@ class FetchLinkCardService < BaseService
|
||||||
detector = CharlockHolmes::EncodingDetector.new
|
detector = CharlockHolmes::EncodingDetector.new
|
||||||
detector.strip_tags = true
|
detector.strip_tags = true
|
||||||
|
|
||||||
guess = detector.detect(@html, @html_charset)
|
guess = detector.detect(@html, @html_charset)
|
||||||
page = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
|
page = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
|
||||||
|
player_url = meta_property(page, 'twitter:player')
|
||||||
|
|
||||||
if meta_property(page, 'twitter:player')
|
if player_url && !bad_url?(Addressable::URI.parse(player_url))
|
||||||
@card.type = :video
|
@card.type = :video
|
||||||
@card.width = meta_property(page, 'twitter:player:width') || 0
|
@card.width = meta_property(page, 'twitter:player:width') || 0
|
||||||
@card.height = meta_property(page, 'twitter:player:height') || 0
|
@card.height = meta_property(page, 'twitter:player:height') || 0
|
||||||
@card.html = content_tag(:iframe, nil, src: meta_property(page, 'twitter:player'),
|
@card.html = content_tag(:iframe, nil, src: player_url,
|
||||||
width: @card.width,
|
width: @card.width,
|
||||||
height: @card.height,
|
height: @card.height,
|
||||||
allowtransparency: 'true',
|
allowtransparency: 'true',
|
||||||
|
|
|
@ -7,9 +7,9 @@ class FollowService < BaseService
|
||||||
# @param [Account] source_account From which to follow
|
# @param [Account] source_account From which to follow
|
||||||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
||||||
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
|
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
|
||||||
def call(source_account, uri, reblogs: nil)
|
def call(source_account, target_account, reblogs: nil)
|
||||||
reblogs = true if reblogs.nil?
|
reblogs = true if reblogs.nil?
|
||||||
target_account = uri.is_a?(Account) ? uri : ResolveAccountService.new.call(uri)
|
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
||||||
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
|
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
|
||||||
|
@ -42,7 +42,7 @@ class FollowService < BaseService
|
||||||
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
|
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
|
||||||
|
|
||||||
if target_account.local?
|
if target_account.local?
|
||||||
NotifyService.new.call(target_account, follow_request)
|
LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
|
||||||
elsif target_account.ostatus?
|
elsif target_account.ostatus?
|
||||||
NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
|
NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
|
||||||
AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
|
AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
|
||||||
|
@ -57,7 +57,7 @@ class FollowService < BaseService
|
||||||
follow = source_account.follow!(target_account, reblogs: reblogs)
|
follow = source_account.follow!(target_account, reblogs: reblogs)
|
||||||
|
|
||||||
if target_account.local?
|
if target_account.local?
|
||||||
NotifyService.new.call(target_account, follow)
|
LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
|
||||||
else
|
else
|
||||||
Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
|
Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
|
||||||
NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
|
NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ProcessMentionsService < BaseService
|
||||||
mentioned_account = mention.account
|
mentioned_account = mention.account
|
||||||
|
|
||||||
if mentioned_account.local?
|
if mentioned_account.local?
|
||||||
LocalNotificationWorker.perform_async(mention.id)
|
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
|
||||||
elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
|
elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
|
||||||
NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
|
NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
|
||||||
elsif mentioned_account.activitypub?
|
elsif mentioned_account.activitypub?
|
||||||
|
|
|
@ -9,17 +9,27 @@ class ResolveAccountService < BaseService
|
||||||
# Find or create a local account for a remote user.
|
# Find or create a local account for a remote user.
|
||||||
# When creating, look up the user's webfinger and fetch all
|
# When creating, look up the user's webfinger and fetch all
|
||||||
# important information from their feed
|
# important information from their feed
|
||||||
# @param [String] uri User URI in the form of username@domain
|
# @param [String, Account] uri User URI in the form of username@domain
|
||||||
|
# @param [Hash] options
|
||||||
# @return [Account]
|
# @return [Account]
|
||||||
def call(uri, update_profile = true, redirected = nil)
|
def call(uri, options = {})
|
||||||
@username, @domain = uri.split('@')
|
@options = options
|
||||||
@update_profile = update_profile
|
|
||||||
|
|
||||||
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
if uri.is_a?(Account)
|
||||||
|
@account = uri
|
||||||
|
@username = @account.username
|
||||||
|
@domain = @account.domain
|
||||||
|
|
||||||
@account = Account.find_remote(@username, @domain)
|
return @account if @account.local? || !webfinger_update_due?
|
||||||
|
else
|
||||||
|
@username, @domain = uri.split('@')
|
||||||
|
|
||||||
return @account unless webfinger_update_due?
|
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
||||||
|
|
||||||
|
@account = Account.find_remote(@username, @domain)
|
||||||
|
|
||||||
|
return @account unless webfinger_update_due?
|
||||||
|
end
|
||||||
|
|
||||||
Rails.logger.debug "Looking up webfinger for #{uri}"
|
Rails.logger.debug "Looking up webfinger for #{uri}"
|
||||||
|
|
||||||
|
@ -30,8 +40,8 @@ class ResolveAccountService < BaseService
|
||||||
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
||||||
@username = confirmed_username
|
@username = confirmed_username
|
||||||
@domain = confirmed_domain
|
@domain = confirmed_domain
|
||||||
elsif redirected.nil?
|
elsif options[:redirected].nil?
|
||||||
return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
|
return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true))
|
||||||
else
|
else
|
||||||
Rails.logger.debug 'Requested and returned acct URIs do not match'
|
Rails.logger.debug 'Requested and returned acct URIs do not match'
|
||||||
return
|
return
|
||||||
|
@ -76,7 +86,7 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def webfinger_update_due?
|
def webfinger_update_due?
|
||||||
@account.nil? || @account.possibly_stale?
|
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def activitypub_ready?
|
def activitypub_ready?
|
||||||
|
@ -93,7 +103,7 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_profile?
|
def update_profile?
|
||||||
@update_profile
|
@options[:update_profile]
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_activitypub
|
def handle_activitypub
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ResolveURLService < BaseService
|
||||||
def process_url
|
def process_url
|
||||||
if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
|
if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
|
||||||
FetchRemoteAccountService.new.call(atom_url, body, protocol)
|
FetchRemoteAccountService.new.call(atom_url, body, protocol)
|
||||||
elsif equals_or_includes_any?(type, %w(Note Article Image Video))
|
elsif equals_or_includes_any?(type, %w(Note Article Image Video Page))
|
||||||
FetchRemoteStatusService.new.call(atom_url, body, protocol)
|
FetchRemoteStatusService.new.call(atom_url, body, protocol)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class FollowLimitValidator < ActiveModel::Validator
|
||||||
if account.following_count < LIMIT
|
if account.following_count < LIMIT
|
||||||
LIMIT
|
LIMIT
|
||||||
else
|
else
|
||||||
account.followers_count * RATIO
|
[(account.followers_count * RATIO).round, LIMIT].max
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
%meta{ name: 'robots', content: 'noindex' }/
|
%meta{ name: 'robots', content: 'noindex' }/
|
||||||
|
|
||||||
%link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
|
%link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
|
||||||
%link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
|
|
||||||
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
|
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
|
||||||
|
%link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
|
||||||
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
|
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
|
||||||
|
|
||||||
- if @older_url
|
- if @older_url
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
- if object.errors.any?
|
- if object.errors.any?
|
||||||
.flash-message#error_explanation
|
.flash-message.alert#error_explanation
|
||||||
%strong= t('generic.validation_errors', count: object.errors.count)
|
%strong= t('generic.validation_errors', count: object.errors.count)
|
||||||
|
|
|
@ -3,9 +3,16 @@
|
||||||
class LocalNotificationWorker
|
class LocalNotificationWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(mention_id)
|
def perform(receiver_account_id, activity_id = nil, activity_class_name = nil)
|
||||||
mention = Mention.find(mention_id)
|
if activity_id.nil? && activity_class_name.nil?
|
||||||
NotifyService.new.call(mention.account, mention)
|
activity = Mention.find(receiver_account_id)
|
||||||
|
receiver = activity.account
|
||||||
|
else
|
||||||
|
receiver = Account.find(receiver_account_id)
|
||||||
|
activity = activity_class_name.constantize.find(activity_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
NotifyService.new.call(receiver, activity)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,9 @@ workers ENV.fetch('WEB_CONCURRENCY') { 2 }
|
||||||
preload_app!
|
preload_app!
|
||||||
|
|
||||||
on_worker_boot do
|
on_worker_boot do
|
||||||
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
ActiveSupport.on_load(:active_record) do
|
||||||
|
ActiveRecord::Base.establish_connection
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
plugin :tmp_restart
|
plugin :tmp_restart
|
||||||
|
|
|
@ -242,8 +242,9 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
culled += 1
|
culled += 1
|
||||||
say('.', :green, false)
|
say('+', :green, false)
|
||||||
else
|
else
|
||||||
|
account.touch # Touch account even during dry run to avoid getting the account into the window again
|
||||||
say('.', nil, false)
|
say('.', nil, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,8 @@ require_relative 'cli_helper'
|
||||||
|
|
||||||
module Mastodon
|
module Mastodon
|
||||||
class MediaCLI < Thor
|
class MediaCLI < Thor
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
def self.exit_on_failure?
|
def self.exit_on_failure?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -36,16 +38,19 @@ module Mastodon
|
||||||
time_ago = options[:days].days.ago
|
time_ago = options[:days].days.ago
|
||||||
queued = 0
|
queued = 0
|
||||||
processed = 0
|
processed = 0
|
||||||
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
size = 0
|
||||||
|
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
||||||
|
|
||||||
if options[:background]
|
if options[:background]
|
||||||
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id).reorder(nil).find_in_batches do |media_attachments|
|
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id, :file_file_size).reorder(nil).find_in_batches do |media_attachments|
|
||||||
queued += media_attachments.size
|
queued += media_attachments.size
|
||||||
|
size += media_attachments.reduce(0) { |sum, m| sum + (m.file_file_size || 0) }
|
||||||
Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id)) unless options[:dry_run]
|
Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id)) unless options[:dry_run]
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).reorder(nil).find_in_batches do |media_attachments|
|
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).reorder(nil).find_in_batches do |media_attachments|
|
||||||
media_attachments.each do |m|
|
media_attachments.each do |m|
|
||||||
|
size += m.file_file_size || 0
|
||||||
Maintenance::UncacheMediaWorker.new.perform(m) unless options[:dry_run]
|
Maintenance::UncacheMediaWorker.new.perform(m) unless options[:dry_run]
|
||||||
options[:verbose] ? say(m.id) : say('.', :green, false)
|
options[:verbose] ? say(m.id) : say('.', :green, false)
|
||||||
processed += 1
|
processed += 1
|
||||||
|
@ -56,9 +61,9 @@ module Mastodon
|
||||||
say
|
say
|
||||||
|
|
||||||
if options[:background]
|
if options[:background]
|
||||||
say("Scheduled the deletion of #{queued} media attachments #{dry_run}", :green, true)
|
say("Scheduled the deletion of #{queued} media attachments (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
|
||||||
else
|
else
|
||||||
say("Removed #{processed} media attachments #{dry_run}", :green, true)
|
say("Removed #{processed} media attachments (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,7 +38,7 @@ http {
|
||||||
|
|
||||||
root /app/public;
|
root /app/public;
|
||||||
|
|
||||||
client_max_body_size 8M;
|
client_max_body_size 80M;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri @rails;
|
try_files $uri @rails;
|
||||||
|
|
|
@ -32,7 +32,7 @@ http {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000";
|
add_header Strict-Transport-Security "max-age=31536000";
|
||||||
add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
|
# add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
|
||||||
|
|
||||||
root /app/public;
|
root /app/public;
|
||||||
|
|
||||||
|
|
|
@ -32,11 +32,11 @@ http {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000";
|
add_header Strict-Transport-Security "max-age=31536000";
|
||||||
add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
|
# add_header Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'self'; img-src data: https:; media-src data: https:; connect-src 'self' wss://<%= ENV["LOCAL_DOMAIN"] %>; upgrade-insecure-requests";
|
||||||
|
|
||||||
root /app/public;
|
root /app/public;
|
||||||
|
|
||||||
client_max_body_size 8M;
|
client_max_body_size 80M;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri @rails;
|
try_files $uri @rails;
|
||||||
|
|
|
@ -99,10 +99,12 @@ describe AuthorizeInteractionsController do
|
||||||
|
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
||||||
|
allow(service).to receive(:call).with(target_account, skip_webfinger: true).and_return(target_account)
|
||||||
|
|
||||||
|
|
||||||
post :create, params: { acct: 'acct:user@hostname' }
|
post :create, params: { acct: 'acct:user@hostname' }
|
||||||
|
|
||||||
expect(service).to have_received(:call).with('user@hostname')
|
expect(service).to have_received(:call).with(target_account, skip_webfinger: true)
|
||||||
expect(account.following?(target_account)).to be true
|
expect(account.following?(target_account)).to be true
|
||||||
expect(response).to render_template(:success)
|
expect(response).to render_template(:success)
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,9 +48,11 @@ describe Request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'executes a HTTP request when the first address is private' do
|
it 'executes a HTTP request when the first address is private' do
|
||||||
allow(Addrinfo).to receive(:foreach).with('example.com', nil, nil, :SOCK_STREAM)
|
resolver = double
|
||||||
.and_yield(Addrinfo.new(["AF_INET", 0, "example.com", "0.0.0.0"], :PF_INET, :SOCK_STREAM))
|
|
||||||
.and_yield(Addrinfo.new(["AF_INET6", 0, "example.com", "2001:4860:4860::8844"], :PF_INET6, :SOCK_STREAM))
|
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844))
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
expect { |block| subject.perform &block }.to yield_control
|
expect { |block| subject.perform &block }.to yield_control
|
||||||
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
||||||
|
@ -81,9 +83,12 @@ describe Request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises Mastodon::ValidationError' do
|
it 'raises Mastodon::ValidationError' do
|
||||||
allow(Addrinfo).to receive(:foreach).with('example.com', nil, nil, :SOCK_STREAM)
|
resolver = double
|
||||||
.and_yield(Addrinfo.new(["AF_INET", 0, "example.com", "0.0.0.0"], :PF_INET, :SOCK_STREAM))
|
|
||||||
.and_yield(Addrinfo.new(["AF_INET6", 0, "example.com", "2001:db8::face"], :PF_INET6, :SOCK_STREAM))
|
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face))
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
expect { subject.perform }.to raise_error Mastodon::ValidationError
|
expect { subject.perform }.to raise_error Mastodon::ValidationError
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -101,7 +101,7 @@ RSpec.describe Notification, type: :model do
|
||||||
before do
|
before do
|
||||||
allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1)
|
allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1)
|
||||||
allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2)
|
allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2)
|
||||||
allow(Account).to receive_message_chain(:where, :map, :to_h).and_return(accounts_with_ids)
|
allow(Account).to receive_message_chain(:where, :each_with_object).and_return(accounts_with_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:cached_items) do
|
let(:cached_items) do
|
||||||
|
|
|
@ -60,8 +60,15 @@ RSpec.describe FetchAtomService, type: :service do
|
||||||
it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] }
|
it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'content_type is json' do
|
context 'content_type is activity+json' do
|
||||||
let(:content_type) { 'application/activity+json' }
|
let(:content_type) { 'application/activity+json; charset=utf-8' }
|
||||||
|
let(:body) { json }
|
||||||
|
|
||||||
|
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'content_type is ld+json with profile' do
|
||||||
|
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
|
||||||
let(:body) { json }
|
let(:body) { json }
|
||||||
|
|
||||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
||||||
|
|
Loading…
Reference in a new issue