{ancestors}
diff --git a/app/javascript/mastodon/features/ui/components/audio_modal.js b/app/javascript/mastodon/features/ui/components/audio_modal.js
index 0676bd9cf1..c46fefce89 100644
--- a/app/javascript/mastodon/features/ui/components/audio_modal.js
+++ b/app/javascript/mastodon/features/ui/components/audio_modal.js
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { previewState } from './video_modal';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({
@@ -25,32 +24,6 @@ class AudioModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired,
};
- static contextTypes = {
- router: PropTypes.object,
- };
-
- componentDidMount () {
- if (this.context.router) {
- const history = this.context.router.history;
-
- history.push(history.location.pathname, previewState);
-
- this.unlistenHistory = history.listen(() => {
- this.props.onClose();
- });
- }
- }
-
- componentWillUnmount () {
- if (this.context.router) {
- this.unlistenHistory();
-
- if (this.context.router.history.location.state === previewState) {
- this.context.router.history.goBack();
- }
- }
- }
-
render () {
const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {};
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modal.js b/app/javascript/mastodon/features/ui/components/confirmation_modal.js
index 1227fa453e..65d97ca161 100644
--- a/app/javascript/mastodon/features/ui/components/confirmation_modal.js
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modal.js
@@ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent {
onConfirm: PropTypes.func.isRequired,
secondary: PropTypes.string,
onSecondary: PropTypes.func,
+ closeWhenConfirm: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
+ static defaultProps = {
+ closeWhenConfirm: true,
+ };
+
componentDidMount() {
this.button.focus();
}
handleClick = () => {
- this.props.onClose();
+ if (this.props.closeWhenConfirm) {
+ this.props.onClose();
+ }
this.props.onConfirm();
}
diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js
index 447862499d..ab4260a5eb 100644
--- a/app/javascript/mastodon/features/ui/components/link_footer.js
+++ b/app/javascript/mastodon/features/ui/components/link_footer.js
@@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
+ closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
},
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 08da103304..061776e24e 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -20,8 +20,6 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
-export const previewState = 'previewMediaModal';
-
export default @injectIntl
class MediaModal extends ImmutablePureComponent {
@@ -37,10 +35,6 @@ class MediaModal extends ImmutablePureComponent {
volume: PropTypes.number,
};
- static contextTypes = {
- router: PropTypes.object,
- };
-
state = {
index: null,
navigationHidden: false,
@@ -98,16 +92,6 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false);
- if (this.context.router) {
- const history = this.context.router.history;
-
- history.push(history.location.pathname, previewState);
-
- this.unlistenHistory = history.listen(() => {
- this.props.onClose();
- });
- }
-
this._sendBackgroundColor();
}
@@ -131,14 +115,6 @@ class MediaModal extends ImmutablePureComponent {
componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown);
- if (this.context.router) {
- this.unlistenHistory();
-
- if (this.context.router.history.location.state === previewState) {
- this.context.router.history.goBack();
- }
- }
-
this.props.onChangeBackgroundColor(null);
}
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 2f13a175a1..7e8e1329d7 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -6,8 +6,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
-export const previewState = 'previewVideoModal';
-
export default class VideoModal extends ImmutablePureComponent {
static propTypes = {
@@ -22,19 +20,9 @@ export default class VideoModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired,
};
- static contextTypes = {
- router: PropTypes.object,
- };
-
componentDidMount () {
- const { router } = this.context;
const { media, onChangeBackgroundColor, onClose } = this.props;
- if (router) {
- router.history.push(router.history.location.pathname, previewState);
- this.unlistenHistory = router.history.listen(() => onClose());
- }
-
const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
if (backgroundColor) {
@@ -42,18 +30,6 @@ export default class VideoModal extends ImmutablePureComponent {
}
}
- componentWillUnmount () {
- const { router } = this.context;
-
- if (router) {
- this.unlistenHistory();
-
- if (router.history.location.state === previewState) {
- router.history.goBack();
- }
- }
- }
-
render () {
const { media, statusId, onClose } = this.props;
const options = this.props.options || {};
diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js
index 2d27180f7e..ad1e8a2eea 100644
--- a/app/javascript/mastodon/features/ui/containers/modal_container.js
+++ b/app/javascript/mastodon/features/ui/containers/modal_container.js
@@ -3,8 +3,8 @@ import { closeModal } from '../../../actions/modal';
import ModalRoot from '../components/modal_root';
const mapStateToProps = state => ({
- type: state.get('modal').modalType,
- props: state.get('modal').modalProps,
+ type: state.getIn(['modal', 0, 'modalType'], null),
+ props: state.getIn(['modal', 0, 'modalProps'], {}),
});
const mapDispatchToProps = dispatch => ({
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index c1c6ac7397..756a692934 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -54,8 +54,6 @@ import {
FollowRecommendations,
} from './util/async-components';
import { me } from '../../initial_state';
-import { previewState as previewMediaState } from './components/media_modal';
-import { previewState as previewVideoState } from './components/video_modal';
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
// Dummy import, to make sure that
ends up in the application bundle.
@@ -138,10 +136,6 @@ class SwitchingColumnsArea extends React.PureComponent {
}
}
- shouldUpdateScroll (_, { location }) {
- return location.state !== previewMediaState && location.state !== previewVideoState;
- }
-
setRef = c => {
if (c) {
this.node = c.getWrappedInstance();
@@ -158,38 +152,38 @@ class SwitchingColumnsArea extends React.PureComponent {
{redirect}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js
index cb53887c74..ea81b43323 100644
--- a/app/javascript/mastodon/reducers/modal.js
+++ b/app/javascript/mastodon/reducers/modal.js
@@ -1,19 +1,15 @@
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
import { TIMELINE_DELETE } from '../actions/timelines';
+import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable';
-const initialState = {
- modalType: null,
- modalProps: {},
-};
-
-export default function modal(state = initialState, action) {
+export default function modal(state = ImmutableStack(), action) {
switch(action.type) {
case MODAL_OPEN:
- return { modalType: action.modalType, modalProps: action.modalProps };
+ return state.unshift(ImmutableMap({ modalType: action.modalType, modalProps: action.modalProps }));
case MODAL_CLOSE:
- return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
+ return (action.modalType === undefined || action.modalType === state.getIn([0, 'modalType'])) ? state.shift() : state;
case TIMELINE_DELETE:
- return (state.modalProps.statusId === action.id) ? initialState : state;
+ return state.filterNot((modal) => modal.get('modalProps').statusId === action.id);
default:
return state;
}
diff --git a/app/javascript/mastodon/reducers/picture_in_picture.js b/app/javascript/mastodon/reducers/picture_in_picture.js
index 06cd8c5e87..48772ae7f3 100644
--- a/app/javascript/mastodon/reducers/picture_in_picture.js
+++ b/app/javascript/mastodon/reducers/picture_in_picture.js
@@ -1,4 +1,5 @@
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
+import { TIMELINE_DELETE } from '../actions/timelines';
const initialState = {
statusId: null,
@@ -16,6 +17,8 @@ export default function pictureInPicture(state = initialState, action) {
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState };
+ case TIMELINE_DELETE:
+ return (state.statusId === action.id) ? { ...initialState } : state;
default:
return state;
}
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 25ebc19b09..2744c6c6c3 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -829,6 +829,7 @@ a.name-tag,
padding: 0 5px;
margin-bottom: 10px;
flex: 1 0 50%;
+ max-width: 100%;
}
.account__header__fields,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 7ca027e81d..36c1461b0d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -7284,6 +7284,7 @@ noscript {
&__account {
display: flex;
text-decoration: none;
+ overflow: hidden;
}
.account__avatar {
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 9a29605072..d70ce61038 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -446,10 +446,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def supported_blurhash?(blurhash)
- components = blurhash.blank? ? nil : Blurhash.components(blurhash)
+ components = blurhash.blank? || !blurhash_valid_chars?(blurhash) ? nil : Blurhash.components(blurhash)
components.present? && components.none? { |comp| comp > 5 }
end
+ def blurhash_valid_chars?(blurhash)
+ /^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
+ end
+
def skip_download?
return @skip_download if defined?(@skip_download)
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index f6b5e10d39..f6b9741fa1 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -64,6 +64,10 @@ class ActivityPub::TagManager
account_status_replies_url(target.account, target, page_params)
end
+ def followers_uri_for(target)
+ target.local? ? account_followers_url(target) : target.followers_url.presence
+ end
+
# Primary audience of a status
# Public statuses go out to primarily the public collection
# Unlisted and private statuses go out primarily to the followers collection
@@ -80,17 +84,17 @@ class ActivityPub::TagManager
account_ids = status.active_mentions.pluck(:account_id)
to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
result << uri_for(account)
- result << account_followers_url(account) if account.group?
+ result << followers_uri_for(account) if account.group?
end
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
result << uri_for(request.account)
- result << account_followers_url(request.account) if request.account.group?
- end)
+ result << followers_uri_for(request.account) if request.account.group?
+ end).compact
else
status.active_mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account)
- result << account_followers_url(mention.account) if mention.account.group?
- end
+ result << followers_uri_for(mention.account) if mention.account.group?
+ end.compact
end
end
end
@@ -118,17 +122,17 @@ class ActivityPub::TagManager
account_ids = status.active_mentions.pluck(:account_id)
cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
result << uri_for(account)
- result << account_followers_url(account) if account.group?
- end)
+ result << followers_uri_for(account) if account.group?
+ end.compact)
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
result << uri_for(request.account)
- result << account_followers_url(request.account) if request.account.group?
- end)
+ result << followers_uri_for(request.account) if request.account.group?
+ end.compact)
else
cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account)
- result << account_followers_url(mention.account) if mention.account.group?
- end)
+ result << followers_uri_for(mention.account) if mention.account.group?
+ end.compact)
end
end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index f6eb019131..9d5bd67e88 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -238,39 +238,10 @@ class Formatter
result.flatten.join
end
- UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/
-
def utf8_friendly_extractor(text, options = {})
- old_to_new_index = [0]
-
- escaped = text.chars.map do |c|
- output = begin
- if c.ord.to_s(16).length > 2 && !UNICODE_ESCAPE_BLACKLIST_RE.match?(c)
- CGI.escape(c)
- else
- c
- end
- end
-
- old_to_new_index << old_to_new_index.last + output.length
-
- output
- end.join
-
# Note: I couldn't obtain list_slug with @user/list-name format
# for mention so this requires additional check
- special = Extractor.extract_urls_with_indices(escaped, options).map do |extract|
- new_indices = [
- old_to_new_index.find_index(extract[:indices].first),
- old_to_new_index.find_index(extract[:indices].last),
- ]
-
- next extract.merge(
- indices: new_indices,
- url: text[new_indices.first..new_indices.last - 1]
- )
- end
-
+ special = Extractor.extract_urls_with_indices(text, options)
standard = Extractor.extract_entities_with_indices(text, options)
extra = Extractor.extract_extra_uris_with_indices(text, options)
diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb
index e0e022cea0..1ffb5b4bf3 100644
--- a/app/lib/webfinger.rb
+++ b/app/lib/webfinger.rb
@@ -46,7 +46,9 @@ class Webfinger
def body_from_webfinger(url = standard_url, use_fallback = true)
webfinger_request(url).perform do |res|
if res.code == 200
- res.body_with_limit
+ body = res.body_with_limit
+ raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
+ body
elsif res.code == 404 && use_fallback
body_from_host_meta
elsif res.code == 410
diff --git a/app/models/account.rb b/app/models/account.rb
index 1a5feb349a..2f2a55b553 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -58,8 +58,9 @@ class Account < ApplicationRecord
hub_url
)
- USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
- MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
+ USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
+ MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
+ URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
include AccountAssociations
include AccountAvatar
@@ -295,7 +296,11 @@ class Account < ApplicationRecord
end
def fields
- (self[:fields] || []).map { |f| Field.new(self, f) }
+ (self[:fields] || []).map do |f|
+ Field.new(self, f)
+ rescue
+ nil
+ end.compact
end
def fields_attributes=(attributes)
@@ -375,7 +380,7 @@ class Account < ApplicationRecord
def synchronization_uri_prefix
return 'local' if local?
- @synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
+ @synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end
class Field < ActiveModelSerializers::Model
@@ -570,7 +575,11 @@ class Account < ApplicationRecord
def create_canonical_email_block!
return unless local? && user_email.present?
- CanonicalEmailBlock.create(reference_account: self, email: user_email)
+ begin
+ CanonicalEmailBlock.create(reference_account: self, email: user_email)
+ rescue ActiveRecord::RecordNotUnique
+ # A canonical e-mail block may already exist for the same e-mail
+ end
end
def destroy_canonical_email_block!
diff --git a/app/models/account_note.rb b/app/models/account_note.rb
index bf61df923f..b338bc92f2 100644
--- a/app/models/account_note.rb
+++ b/app/models/account_note.rb
@@ -17,4 +17,5 @@ class AccountNote < ApplicationRecord
belongs_to :target_account, class_name: 'Account'
validates :account_id, uniqueness: { scope: :target_account_id }
+ validates :comment, length: { maximum: 2_000 }
end
diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb
index a8546d65af..be8c45bfe3 100644
--- a/app/models/canonical_email_block.rb
+++ b/app/models/canonical_email_block.rb
@@ -15,7 +15,7 @@ class CanonicalEmailBlock < ApplicationRecord
belongs_to :reference_account, class_name: 'Account'
- validates :canonical_email_hash, presence: true
+ validates :canonical_email_hash, presence: true, uniqueness: true
def email=(email)
self.canonical_email_hash = email_to_canonical_email_hash(email)
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 958f6c78e2..763567f42b 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -251,10 +251,13 @@ module AccountInteractions
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
end
- def remote_followers_hash(url_prefix)
- Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
+ def remote_followers_hash(url)
+ url_prefix = url[Account::URL_PREFIX_RE]
+ return if url_prefix.blank?
+
+ Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
digest = "\x00" * 32
- followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
+ followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
end
digest.unpack('H*')[0]
diff --git a/app/models/status.rb b/app/models/status.rb
index 0cda8ad0aa..338fb8d146 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -339,7 +339,7 @@ class Status < ApplicationRecord
def from_text(text)
return [] if text.blank?
- text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.filter_map do |url|
+ text.scan(FetchLinkCardService::URL_PATTERN).map(&:second).uniq.filter_map do |url|
status = begin
if TagManager.instance.local_url?(url)
ActivityPub::TagManager.instance.uri_to_resource(url, Status)
@@ -427,7 +427,7 @@ class Status < ApplicationRecord
end
def decrement_counter_caches
- return if direct_visibility?
+ return if direct_visibility? || new_record?
account&.decrement_count!(:statuses_count)
reblog&.decrement_count!(:reblogs_count) if reblog?
diff --git a/app/models/user.rb b/app/models/user.rb
index afa7cc0eac..fee32cba27 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -63,7 +63,7 @@ class User < ApplicationRecord
devise :two_factor_backupable,
otp_number_of_backup_codes: 10
- devise :registerable, :recoverable, :rememberable, :validatable,
+ devise :registerable, :recoverable, :validatable,
:confirmable
include Omniauthable
diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb
index dafe8f55b9..4786aa760a 100644
--- a/app/serializers/manifest_serializer.rb
+++ b/app/serializers/manifest_serializer.rb
@@ -48,7 +48,7 @@ class ManifestSerializer < ActiveModel::Serializer
end
def scope
- root_url
+ '/'
end
def share_target
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 16dcc7ad62..7d385a00f1 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -6,6 +6,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail,
:languages, :registrations, :approval_required, :invites_enabled,
+ :configuration,
:max_toot_chars, :poll_limits
has_one :contact_account, serializer: REST::AccountSerializer
@@ -54,6 +55,32 @@ class REST::InstanceSerializer < ActiveModel::Serializer
{ streaming_api: Rails.configuration.x.streaming_api_base_url }
end
+ def configuration
+ {
+ statuses: {
+ max_characters: StatusLengthValidator::MAX_CHARS,
+ max_media_attachments: 4,
+ characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
+ },
+
+ media_attachments: {
+ supported_mime_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES,
+ image_size_limit: MediaAttachment::IMAGE_LIMIT,
+ image_matrix_limit: Attachmentable::MAX_MATRIX_LIMIT,
+ video_size_limit: MediaAttachment::VIDEO_LIMIT,
+ video_frame_rate_limit: MediaAttachment::MAX_VIDEO_FRAME_RATE,
+ video_matrix_limit: MediaAttachment::MAX_VIDEO_MATRIX_LIMIT,
+ },
+
+ polls: {
+ max_options: PollValidator::MAX_OPTIONS,
+ max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
+ min_expiration: PollValidator::MIN_EXPIRATION,
+ max_expiration: PollValidator::MAX_EXPIRATION,
+ },
+ }
+ end
+
def languages
[I18n.default_locale]
end
diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb
index 60be9b9dc0..4cbaa04c62 100644
--- a/app/services/fetch_oembed_service.rb
+++ b/app/services/fetch_oembed_service.rb
@@ -2,6 +2,7 @@
class FetchOEmbedService
ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
+ URL_REGEX = /(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i.freeze
attr_reader :url, :options, :format, :endpoint_url
@@ -65,10 +66,12 @@ class FetchOEmbedService
end
def cache_endpoint!
+ return unless URL_REGEX.match?(@endpoint_url)
+
url_domain = Addressable::URI.parse(@url).normalized_host
endpoint_hash = {
- endpoint: @endpoint_url.gsub(/(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i, '={url}'),
+ endpoint: @endpoint_url.gsub(URL_REGEX, '={url}'),
format: @format,
}
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index fc187db403..e78c74d1ed 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -67,8 +67,49 @@ class NotifyService < BaseService
message? && @notification.target_status.direct_visibility?
end
+ # Returns true if the sender has been mentionned by the recipient up the thread
def response_to_recipient?
- @notification.target_status.in_reply_to_account_id == @recipient.id && @notification.target_status.thread&.direct_visibility?
+ return false if @notification.target_status.in_reply_to_id.nil?
+
+ # Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
+ !Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero?
+ WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender) AS (
+ SELECT
+ s.id, s.in_reply_to_id, (CASE
+ WHEN s.account_id = :recipient_id THEN
+ EXISTS (
+ SELECT *
+ FROM mentions m
+ WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
+ )
+ ELSE
+ FALSE
+ END)
+ FROM statuses s
+ WHERE s.id = :id
+ UNION ALL
+ SELECT
+ s.id,
+ s.in_reply_to_id,
+ (CASE
+ WHEN s.account_id = :recipient_id THEN
+ EXISTS (
+ SELECT *
+ FROM mentions m
+ WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
+ )
+ ELSE
+ FALSE
+ END)
+ FROM ancestors st
+ JOIN statuses s ON s.id = st.in_reply_to_id
+ WHERE st.replying_to_sender IS FALSE
+ )
+ SELECT COUNT(*)
+ FROM ancestors st
+ JOIN statuses s ON s.id = st.id
+ WHERE st.replying_to_sender IS TRUE AND s.visibility = 3
+ SQL
end
def from_staff?
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 0a383d6a39..85aaec4d65 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -74,6 +74,9 @@ class PostStatusService < BaseService
status_for_validation = @account.statuses.build(status_attributes)
if status_for_validation.valid?
+ # Marking the status as destroyed is necessary to prevent the status from being
+ # persisted when the associated media attachments get updated when creating the
+ # scheduled status.
status_for_validation.destroy
# The following transaction block is needed to wrap the UPDATEs to
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 5400612bfd..b266c019e2 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -142,6 +142,7 @@ class ResolveAccountService < BaseService
end
def queue_deletion!
+ @account.suspend!(origin: :remote)
AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true)
end
diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb
index 949c670aac..7e52a75944 100644
--- a/app/services/unsuspend_account_service.rb
+++ b/app/services/unsuspend_account_service.rb
@@ -7,7 +7,7 @@ class UnsuspendAccountService < BaseService
unsuspend!
refresh_remote_account!
- return if @account.nil?
+ return if @account.nil? || @account.suspended?
merge_into_home_timelines!
merge_into_list_timelines!
diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb
index 6ad5095511..fc664e105a 100644
--- a/app/validators/status_length_validator.rb
+++ b/app/validators/status_length_validator.rb
@@ -2,7 +2,8 @@
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 5000
- URL_PLACEHOLDER = "\1#{'x' * 23}"
+ URL_PLACEHOLDER_CHARS = 23
+ URL_PLACEHOLDER = "\1#{'x' * URL_PLACEHOLDER_CHARS}"
def validate(status)
return unless status.local? && !status.reblog?
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index cd93b0f580..f4429622c5 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -17,11 +17,11 @@
.row__information-board
.information-board__section
%span= t 'about.user_count_before'
- %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
+ %strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count
.information-board__section
%span= t 'about.status_count_before'
- %strong= number_to_human @instance_presenter.status_count, strip_insignificant_zeros: true
+ %strong= friendly_number_to_human @instance_presenter.status_count
%span= t 'about.status_count_after', count: @instance_presenter.status_count
.row__mascot
.landing-page__mascot
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 9cc72785e2..d367b2155c 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -67,10 +67,10 @@
.hero-widget__counters__wrapper
.hero-widget__counter
- %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
+ %strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count
.hero-widget__counter
- %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
+ %strong= friendly_number_to_human @instance_presenter.active_user_count
%span
= t 'about.active_count_after'
%abbr{ title: t('about.active_footnote') } *
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index cae5a5ac92..d9966723a7 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -15,17 +15,17 @@
.details-counters
.counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
- %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
+ %span.counter-number= friendly_number_to_human account.statuses_count
%span.counter-label= t('accounts.posts', count: account.statuses_count)
.counter{ class: active_nav_class(account_following_index_url(account)) }
= link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
- %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
+ %span.counter-number= friendly_number_to_human account.following_count
%span.counter-label= t('accounts.following', count: account.following_count)
.counter{ class: active_nav_class(account_followers_url(account)) }
= link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do
- %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
+ %span.counter-number= friendly_number_to_human account.followers_count
%span.counter-label= t('accounts.followers', count: account.followers_count)
.spacer
.public-account-header__tabs__tabs__buttons
@@ -36,8 +36,8 @@
.public-account-header__extra__links
= link_to account_following_index_url(account) do
- %strong= number_to_human account.following_count, strip_insignificant_zeros: true
+ %strong= friendly_number_to_human account.following_count
= t('accounts.following', count: account.following_count)
= link_to account_followers_url(account) do
- %strong= number_to_human account.followers_count, strip_insignificant_zeros: true
+ %strong= friendly_number_to_human account.followers_count
= t('accounts.followers', count: account.followers_count)
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 1a81b96f6c..72e9c66111 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -81,6 +81,6 @@
= t('accounts.nothing_here')
- else
%time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
- .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
+ .trends__item__current= friendly_number_to_human featured_tag.statuses_count
= render 'application/sidebar'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e8a2b46fd7..05b585ab6d 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -13,42 +13,42 @@
%div
= link_to admin_accounts_url(local: 1, recent: 1) do
.dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) }
- = number_to_human @users_count, strip_insignificant_zeros: true
+ = friendly_number_to_human @users_count
.dashboard__counters__label= t 'admin.dashboard.total_users'
%div
%div
.dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) }
- = number_to_human @registrations_week, strip_insignificant_zeros: true
+ = friendly_number_to_human @registrations_week
.dashboard__counters__label= t 'admin.dashboard.week_users_new'
%div
%div
.dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) }
- = number_to_human @logins_week, strip_insignificant_zeros: true
+ = friendly_number_to_human @logins_week
.dashboard__counters__label= t 'admin.dashboard.week_users_active'
%div
= link_to admin_pending_accounts_path do
.dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) }
- = number_to_human @pending_users_count, strip_insignificant_zeros: true
+ = friendly_number_to_human @pending_users_count
.dashboard__counters__label= t 'admin.dashboard.pending_users'
%div
= link_to admin_reports_url do
.dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) }
- = number_to_human @reports_count, strip_insignificant_zeros: true
+ = friendly_number_to_human @reports_count
.dashboard__counters__label= t 'admin.dashboard.open_reports'
%div
= link_to admin_tags_path(pending_review: '1') do
.dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) }
- = number_to_human @pending_tags_count, strip_insignificant_zeros: true
+ = friendly_number_to_human @pending_tags_count
.dashboard__counters__label= t 'admin.dashboard.pending_tags'
%div
%div
.dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) }
- = number_to_human @interactions_week, strip_insignificant_zeros: true
+ = friendly_number_to_human @interactions_week
.dashboard__counters__label= t 'admin.dashboard.week_interactions'
%div
= link_to sidekiq_url do
.dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) }
- = number_to_human @queue_backlog, strip_insignificant_zeros: true
+ = friendly_number_to_human @queue_backlog
.dashboard__counters__label= t 'admin.dashboard.backlog'
.dashboard__widgets
diff --git a/app/views/admin/follow_recommendations/_account.html.haml b/app/views/admin/follow_recommendations/_account.html.haml
index af5a4aaf7d..00196dd01a 100644
--- a/app/views/admin/follow_recommendations/_account.html.haml
+++ b/app/views/admin/follow_recommendations/_account.html.haml
@@ -7,10 +7,10 @@
%tr
%td= account_link_to account
%td.accounts-table__count.optional
- = number_to_human account.statuses_count, strip_insignificant_zeros: true
+ = friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional
- = number_to_human account.followers_count, strip_insignificant_zeros: true
+ = friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count
- if account.last_status_at.present?
diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml
index 990cf9ec88..dc81007ac9 100644
--- a/app/views/admin/instances/_instance.html.haml
+++ b/app/views/admin/instances/_instance.html.haml
@@ -30,4 +30,4 @@
= ' / '
%span.negative-hint
= t('admin.instances.delivery.unavailable_message')
- .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
+ .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= friendly_number_to_human instance.accounts_count
diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml
index adf4ca7b2f..ac0c728165 100644
--- a/app/views/admin/tags/_tag.html.haml
+++ b/app/views/admin/tags/_tag.html.haml
@@ -16,4 +16,4 @@
= fa_icon 'fire fw'
= t('admin.tags.trending_right_now')
- .trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true
+ .trends__item__current= friendly_number_to_human tag.history.first[:uses]
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index 7975ee9997..04639e32c2 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -39,10 +39,10 @@
.directory__card__extra
.accounts-table__count
- = number_to_human account.statuses_count, strip_insignificant_zeros: true
+ = friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase
.accounts-table__count
- = number_to_human account.followers_count, strip_insignificant_zeros: true
+ = friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase
.accounts-table__count
- if account.last_status_at.present?
diff --git a/app/views/relationships/_account.html.haml b/app/views/relationships/_account.html.haml
index f521aff225..0fa3cffb55 100644
--- a/app/views/relationships/_account.html.haml
+++ b/app/views/relationships/_account.html.haml
@@ -9,10 +9,10 @@
= interrelationships_icon(@relationships, account.id)
%td= account_link_to account
%td.accounts-table__count.optional
- = number_to_human account.statuses_count, strip_insignificant_zeros: true
+ = friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional
- = number_to_human account.followers_count, strip_insignificant_zeros: true
+ = friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count
- if account.last_status_at.present?
diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml
index 297379893a..65de7f8f30 100644
--- a/app/views/settings/featured_tags/index.html.haml
+++ b/app/views/settings/featured_tags/index.html.haml
@@ -28,4 +28,4 @@
- else
%time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
= table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
- .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
+ .trends__item__current= friendly_number_to_human featured_tag.statuses_count
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 7463b266f2..11ca79592b 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -55,18 +55,18 @@
= fa_icon('comment')
- else
= fa_icon('comments')
- %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true
+ %span.detailed-status__reblogs>= friendly_number_to_human status.replies_count
= " "
·
- if status.public_visibility? || status.unlisted_visibility?
= link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
= fa_icon('retweet')
- %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true
+ %span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
= " "
·
= link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
= fa_icon('heart')
- %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true
+ %span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
= " "
- if user_signed_in?
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 6c5a576a70..788f2cf809 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -44,11 +44,7 @@ class ActivityPub::DeliveryWorker
end
def synchronization_header
- "collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(inbox_url_prefix)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
- end
-
- def inbox_url_prefix
- @inbox_url[/http(s?):\/\/[^\/]+\//]
+ "collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
end
def perform_request
diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb
index 39e3213166..4a900e3b80 100644
--- a/app/workers/move_worker.rb
+++ b/app/workers/move_worker.rb
@@ -13,9 +13,13 @@ class MoveWorker
queue_follow_unfollows!
end
+ @deferred_error = nil
+
copy_account_notes!
carry_blocks_over!
carry_mutes_over!
+
+ raise @deferred_error unless @deferred_error.nil?
rescue ActiveRecord::RecordNotFound
true
end
@@ -36,21 +40,31 @@ class MoveWorker
@source_account.followers.local.select(:id).find_in_batches do |accounts|
UnfollowFollowWorker.push_bulk(accounts.map(&:id)) { |follower_id| [follower_id, @source_account.id, @target_account.id, bypass_locked] }
+ rescue => e
+ @deferred_error = e
end
end
def copy_account_notes!
AccountNote.where(target_account: @source_account).find_each do |note|
- text = I18n.with_locale(note.account.user.locale || I18n.default_locale) do
+ text = I18n.with_locale(note.account.user&.locale || I18n.default_locale) do
I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct)
end
new_note = AccountNote.find_by(account: note.account, target_account: @target_account)
if new_note.nil?
- AccountNote.create!(account: note.account, target_account: @target_account, comment: [text, note.comment].join('\n'))
+ begin
+ AccountNote.create!(account: note.account, target_account: @target_account, comment: [text, note.comment].join("\n"))
+ rescue ActiveRecord::RecordInvalid
+ AccountNote.create!(account: note.account, target_account: @target_account, comment: note.comment)
+ end
else
- new_note.update!(comment: [text, note.comment, '\n', new_note.comment].join('\n'))
+ new_note.update!(comment: [text, note.comment, "\n", new_note.comment].join("\n"))
end
+ rescue ActiveRecord::RecordInvalid
+ nil
+ rescue => e
+ @deferred_error = e
end
end
@@ -60,6 +74,8 @@ class MoveWorker
BlockService.new.call(block.account, @target_account)
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
end
+ rescue => e
+ @deferred_error = e
end
end
@@ -67,12 +83,14 @@ class MoveWorker
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account)
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
+ rescue => e
+ @deferred_error = e
end
end
def add_account_note_if_needed!(account, id)
unless AccountNote.where(account: account, target_account: @target_account).exists?
- text = I18n.with_locale(account.user.locale || I18n.default_locale) do
+ text = I18n.with_locale(account.user&.locale || I18n.default_locale) do
I18n.t(id, acct: @source_account.acct)
end
AccountNote.create!(account: account, target_account: @target_account, comment: text)
diff --git a/config/application.rb b/config/application.rb
index 024d405736..9f7e85b73a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,6 +1,21 @@
require_relative 'boot'
-require 'rails/all'
+require 'rails'
+
+require 'active_record/railtie'
+#require 'active_storage/engine'
+require 'action_controller/railtie'
+require 'action_view/railtie'
+require 'action_mailer/railtie'
+require 'active_job/railtie'
+#require 'action_cable/engine'
+#require 'action_mailbox/engine'
+#require 'action_text/engine'
+#require 'rails/test_unit/railtie'
+require 'sprockets/railtie'
+
+# Used to be implicitly required in action_mailbox/engine
+require 'mail'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
diff --git a/config/deploy.rb b/config/deploy.rb
index f844cc8714..f642e6e59d 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -2,7 +2,7 @@
lock '3.16.0'
-set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
+set :repo_url, ENV.fetch('REPO', 'https://github.com/mastodon/mastodon.git')
set :branch, ENV.fetch('BRANCH', 'master')
set :application, 'mastodon'
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index ef612e177d..5232e6cfda 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -1,3 +1,5 @@
+require 'devise/strategies/authenticatable'
+
Warden::Manager.after_set_user except: :fetch do |user, warden|
if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'])
session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']
@@ -72,17 +74,48 @@ module Devise
mattr_accessor :ldap_uid_conversion_replace
@@ldap_uid_conversion_replace = nil
- class Strategies::PamAuthenticatable
- def valid?
- super && ::Devise.pam_authentication
+ module Strategies
+ class PamAuthenticatable
+ def valid?
+ super && ::Devise.pam_authentication
+ end
+ end
+
+ class SessionActivationRememberable < Authenticatable
+ def valid?
+ @session_cookie = nil
+ session_cookie.present?
+ end
+
+ def authenticate!
+ resource = SessionActivation.find_by(session_id: session_cookie)&.user
+
+ unless resource
+ cookies.delete('_session_id')
+ return pass
+ end
+
+ if validate(resource)
+ success!(resource)
+ end
+ end
+
+ private
+
+ def session_cookie
+ @session_cookie ||= cookies.signed['_session_id']
+ end
end
end
end
+Warden::Strategies.add(:session_activation_rememberable, Devise::Strategies::SessionActivationRememberable)
+
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(scope: :user).unshift :two_factor_ldap_authenticatable if Devise.ldap_authentication
manager.default_strategies(scope: :user).unshift :two_factor_pam_authenticatable if Devise.pam_authentication
+ manager.default_strategies(scope: :user).unshift :session_activation_rememberable
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
manager.default_strategies(scope: :user).unshift :two_factor_backupable
end
diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb
index 3ff2aa9e5d..84c09ff35c 100644
--- a/config/initializers/twitter_regex.rb
+++ b/config/initializers/twitter_regex.rb
@@ -24,6 +24,10 @@ module Twitter::TwitterText
)
\)
/iox
+ REGEXEN[:valid_iri_ucschar] = /[\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}]/iou
+ REGEXEN[:valid_iri_iprivate] = /[\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/iou
+ REGEXEN[:valid_url_query_chars] = /(?:#{REGEXEN[:valid_iri_ucschar]})|(?:#{REGEXEN[:valid_iri_iprivate]})|[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@]/iou
+ REGEXEN[:valid_url_query_ending_chars] = /(?:#{REGEXEN[:valid_iri_ucschar]})|(?:#{REGEXEN[:valid_iri_iprivate]})|[a-z0-9_&=#\/\-]/iou
REGEXEN[:valid_url_path] = /(?:
(?:
#{REGEXEN[:valid_general_url_path_chars]}*
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6274031dcc..9d5185fe6e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1451,7 +1451,7 @@ en:
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
signed_in_as: 'Signed in as:'
- suspicious_sign_in_confirmation: You appear to not have logged in from this device before, and you haven't logged in for a while, so we're sending a security code to your e-mail address to confirm that it's you.
+ suspicious_sign_in_confirmation: You appear to not have logged in from this device before, so we're sending a security code to your e-mail address to confirm that it's you.
verification:
explanation_html: 'You can
verify yourself as the owner of the links in your profile metadata. For that, the linked website must contain a link back to your Mastodon profile. The link back
must have a
rel="me"
attribute. The text content of the link does not matter. Here is an example:'
verification: Verification
diff --git a/db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb b/db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb
index bd4f4c2a36..40537e9c9e 100644
--- a/db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb
+++ b/db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb
@@ -1,6 +1,46 @@
class RemoveFauxRemoteAccountDuplicates < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
+ class StreamEntry < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account, inverse_of: :stream_entries
+ end
+
+ class Status < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account, inverse_of: :statuses
+ has_many :favourites, inverse_of: :status, dependent: :destroy
+ has_many :mentions, dependent: :destroy, inverse_of: :status
+ end
+
+ class Favourite < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account, inverse_of: :favourites
+ belongs_to :status, inverse_of: :favourites
+ end
+
+ class Mention < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account, inverse_of: :mentions
+ belongs_to :status
+ end
+
+ class Notification < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account, optional: true
+ belongs_to :from_account, class_name: 'Account', optional: true
+ belongs_to :activity, polymorphic: true, optional: true
+ end
+
+ class Account < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ has_many :stream_entries, inverse_of: :account, dependent: :destroy
+ has_many :statuses, inverse_of: :account, dependent: :destroy
+ has_many :favourites, inverse_of: :account, dependent: :destroy
+ has_many :mentions, inverse_of: :account, dependent: :destroy
+ has_many :notifications, inverse_of: :account, dependent: :destroy
+ end
+
def up
local_domain = Rails.configuration.x.local_domain
diff --git a/db/migrate/20190715164535_add_instance_actor.rb b/db/migrate/20190715164535_add_instance_actor.rb
index a26d549493..8c0301d69d 100644
--- a/db/migrate/20190715164535_add_instance_actor.rb
+++ b/db/migrate/20190715164535_add_instance_actor.rb
@@ -1,4 +1,9 @@
class AddInstanceActor < ActiveRecord::Migration[5.2]
+ class Account < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ validates :username, uniqueness: { scope: :domain, case_sensitive: false }
+ end
+
def up
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
end
diff --git a/db/migrate/20191007013357_update_pt_locales.rb b/db/migrate/20191007013357_update_pt_locales.rb
index b7288d38a0..9e8f8b4241 100644
--- a/db/migrate/20191007013357_update_pt_locales.rb
+++ b/db/migrate/20191007013357_update_pt_locales.rb
@@ -1,4 +1,8 @@
class UpdatePtLocales < ActiveRecord::Migration[5.2]
+ class User < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ end
+
disable_ddl_transaction!
def up
diff --git a/db/migrate/20200508212852_reset_unique_jobs_locks.rb b/db/migrate/20200508212852_reset_unique_jobs_locks.rb
index 304e493223..d717ffc547 100644
--- a/db/migrate/20200508212852_reset_unique_jobs_locks.rb
+++ b/db/migrate/20200508212852_reset_unique_jobs_locks.rb
@@ -3,7 +3,7 @@ class ResetUniqueJobsLocks < ActiveRecord::Migration[5.2]
def up
# We do this to clean up unique job digests that were not properly
- # disposed of prior to https://github.com/tootsuite/mastodon/pull/13361
+ # disposed of prior to https://github.com/mastodon/mastodon/pull/13361
until SidekiqUniqueJobs::Digests.new.delete_by_pattern('*').nil?; end
end
diff --git a/db/migrate/20210416200740_create_canonical_email_blocks.rb b/db/migrate/20210416200740_create_canonical_email_blocks.rb
index a1f1660bff..32c44646c3 100644
--- a/db/migrate/20210416200740_create_canonical_email_blocks.rb
+++ b/db/migrate/20210416200740_create_canonical_email_blocks.rb
@@ -2,7 +2,7 @@ class CreateCanonicalEmailBlocks < ActiveRecord::Migration[6.1]
def change
create_table :canonical_email_blocks do |t|
t.string :canonical_email_hash, null: false, default: '', index: { unique: true }
- t.belongs_to :reference_account, null: false, foreign_key: { on_cascade: :delete, to_table: 'accounts' }
+ t.belongs_to :reference_account, null: false, foreign_key: { to_table: 'accounts' }
t.timestamps
end
diff --git a/db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb b/db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb
new file mode 100644
index 0000000000..64cf84448b
--- /dev/null
+++ b/db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb
@@ -0,0 +1,13 @@
+class FixCanonicalEmailBlocksForeignKey < ActiveRecord::Migration[6.1]
+ def up
+ safety_assured do
+ execute 'ALTER TABLE canonical_email_blocks DROP CONSTRAINT fk_rails_1ecb262096, ADD CONSTRAINT fk_rails_1ecb262096 FOREIGN KEY (reference_account_id) REFERENCES accounts(id) ON DELETE CASCADE;'
+ end
+ end
+
+ def down
+ safety_assured do
+ execute 'ALTER TABLE canonical_email_blocks DROP CONSTRAINT fk_rails_1ecb262096, ADD CONSTRAINT fk_rails_1ecb262096 FOREIGN KEY (reference_account_id) REFERENCES accounts(id);'
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 37dda654e1..387ddb066e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_05_26_193025) do
+ActiveRecord::Schema.define(version: 2021_06_30_000137) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1009,7 +1009,7 @@ ActiveRecord::Schema.define(version: 2021_05_26_193025) do
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
add_foreign_key "bookmarks", "accounts", on_delete: :cascade
add_foreign_key "bookmarks", "statuses", on_delete: :cascade
- add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id"
+ add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
diff --git a/db/views/follow_recommendations_v01.sql b/db/views/follow_recommendations_v01.sql
index 799abeaee5..8295bbc0f0 100644
--- a/db/views/follow_recommendations_v01.sql
+++ b/db/views/follow_recommendations_v01.sql
@@ -20,7 +20,7 @@ FROM (
HAVING count(follows.id) >= 5
UNION ALL
SELECT accounts.id AS account_id,
- sum(reblogs_count + favourites_count) / (1.0 + sum(reblogs_count + favourites_count)) AS rank,
+ sum(status_stats.reblogs_count + status_stats.favourites_count) / (1.0 + sum(status_stats.reblogs_count + status_stats.favourites_count)) AS rank,
'most_interactions' AS reason
FROM status_stats
INNER JOIN statuses ON statuses.id = status_stats.status_id
@@ -32,7 +32,7 @@ FROM (
AND accounts.locked = 'f'
AND accounts.discoverable = 't'
GROUP BY accounts.id
- HAVING sum(reblogs_count + favourites_count) >= 5
+ HAVING sum(status_stats.reblogs_count + status_stats.favourites_count) >= 5
) t0
GROUP BY account_id
ORDER BY rank DESC
diff --git a/db/views/follow_recommendations_v02.sql b/db/views/follow_recommendations_v02.sql
index 673c5cc85d..f67c6eecf1 100644
--- a/db/views/follow_recommendations_v02.sql
+++ b/db/views/follow_recommendations_v02.sql
@@ -18,7 +18,7 @@ FROM (
HAVING count(follows.id) >= 5
UNION ALL
SELECT account_summaries.account_id AS account_id,
- sum(reblogs_count + favourites_count) / (1.0 + sum(reblogs_count + favourites_count)) AS rank,
+ sum(status_stats.reblogs_count + status_stats.favourites_count) / (1.0 + sum(status_stats.reblogs_count + status_stats.favourites_count)) AS rank,
'most_interactions' AS reason
FROM status_stats
INNER JOIN statuses ON statuses.id = status_stats.status_id
@@ -28,7 +28,7 @@ FROM (
AND account_summaries.sensitive = 'f'
AND follow_recommendation_suppressions.id IS NULL
GROUP BY account_summaries.account_id
- HAVING sum(reblogs_count + favourites_count) >= 5
+ HAVING sum(status_stats.reblogs_count + status_stats.favourites_count) >= 5
) t0
GROUP BY account_id
ORDER BY rank DESC
diff --git a/docker-compose.yml b/docker-compose.yml
index 52eea7a74f..288a4fdf69 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -43,7 +43,7 @@ services:
web:
build: .
- image: tootsuite/mastodon
+ image: tootsuite/mastodon:v3.4.5
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@@ -63,7 +63,7 @@ services:
streaming:
build: .
- image: tootsuite/mastodon
+ image: tootsuite/mastodon:v3.4.5
restart: always
env_file: .env.production
command: node ./streaming
@@ -80,7 +80,7 @@ services:
sidekiq:
build: .
- image: tootsuite/mastodon
+ image: tootsuite/mastodon:v3.4.5
restart: always
env_file: .env.production
command: bundle exec sidekiq
diff --git a/lib/cli.rb b/lib/cli.rb
index 3f1658566b..8815e137ad 100644
--- a/lib/cli.rb
+++ b/lib/cli.rb
@@ -94,17 +94,22 @@ module Mastodon
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
- prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
- prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
- prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
+ unless options[:dry_run]
+ prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
+ prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
+ prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
- exit(1) if prompt.no?('Are you sure you want to proceed?')
+ exit(1) if prompt.no?('Are you sure you want to proceed?')
+ end
inboxes = Account.inboxes
processed = 0
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
+ Setting.registrations_mode = 'none' unless options[:dry_run]
+
if inboxes.empty?
+ Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless options[:dry_run]
prompt.ok('It seems like your server has not federated with anything')
prompt.ok('You can shut it down and delete it any time')
return
@@ -112,9 +117,7 @@ module Mastodon
prompt.warn('Do NOT interrupt this process...')
- Setting.registrations_mode = 'none'
-
- Account.local.without_suspended.find_each do |account|
+ delete_account = ->(account) do
payload = ActiveModelSerializers::SerializableResource.new(
account,
serializer: ActivityPub::DeleteActorSerializer,
@@ -128,12 +131,15 @@ module Mastodon
[json, account.id, inbox_url]
end
- account.suspend!
+ account.suspend!(block_email: false)
end
processed += 1
end
+ Account.local.without_suspended.find_each { |account| delete_account.call(account) }
+ Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) }
+
prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}")
prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data')
rescue TTY::Reader::InputInterrupt
diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb
index 59c118500a..36ca71844f 100644
--- a/lib/mastodon/media_cli.rb
+++ b/lib/mastodon/media_cli.rb
@@ -230,6 +230,7 @@ module Mastodon
processed, aggregate = parallelize_with_progress(scope) do |media_attachment|
next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?)
+ next if DomainBlock.reject_media?(media_attachment.account.domain)
unless options[:dry_run]
media_attachment.reset_file!
diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb
index 39a6e06808..5bc903349a 100644
--- a/lib/mastodon/migration_helpers.rb
+++ b/lib/mastodon/migration_helpers.rb
@@ -295,7 +295,7 @@ module Mastodon
table = Arel::Table.new(table_name)
total = estimate_rows_in_table(table_name).to_i
- if total == 0
+ if total < 1
count_arel = table.project(Arel.star.count.as('count'))
count_arel = yield table, count_arel if block_given?
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 5f9499f6a5..06103454e6 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
end
def patch
- 1
+ 5
end
def flags
diff --git a/lib/paperclip/response_with_limit_adapter.rb b/lib/paperclip/response_with_limit_adapter.rb
index 17a2abd25f..deb89717a4 100644
--- a/lib/paperclip/response_with_limit_adapter.rb
+++ b/lib/paperclip/response_with_limit_adapter.rb
@@ -17,9 +17,9 @@ module Paperclip
def cache_current_values
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
- @size = @target.response.content_length
@tempfile = copy_to_tempfile(@target)
@content_type = ContentTypeDetector.new(@tempfile.path).detect
+ @size = File.size(@tempfile)
end
def copy_to_tempfile(source)
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 72bacb5eb2..d905a07dad 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -333,8 +333,12 @@ namespace :mastodon do
prompt.say 'This configuration will be written to .env.production'
if prompt.yes?('Save configuration?')
+ incompatible_syntax = false
+
env_contents = env.each_pair.map do |key, value|
if value.is_a?(String) && value =~ /[\s\#\\"]/
+ incompatible_syntax = true
+
if value =~ /[']/
value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
"#{key}=\"#{value}\""
@@ -346,12 +350,19 @@ namespace :mastodon do
end
end.join("\n")
- File.write(Rails.root.join('.env.production'), "# Generated with mastodon:setup on #{Time.now.utc}\n\n" + env_contents + "\n")
+ generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
+
+ if incompatible_syntax
+ generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
+ generated_header << "# using docker-compose or not.\n\n"
+ end
+
+ File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
if using_docker
prompt.ok 'Below is your configuration, save it to an .env.production file outside Docker:'
prompt.say "\n"
- prompt.say File.read(Rails.root.join('.env.production'))
+ prompt.say "#{generated_header}#{env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n")}"
prompt.say "\n"
prompt.ok 'It is also saved within this container so you can proceed with this wizard.'
end
diff --git a/lib/tasks/repo.rake b/lib/tasks/repo.rake
index 86c358a94e..d004c5751b 100644
--- a/lib/tasks/repo.rake
+++ b/lib/tasks/repo.rake
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-REPOSITORY_NAME = 'tootsuite/mastodon'
+REPOSITORY_NAME = 'mastodon/mastodon'
namespace :repo do
desc 'Generate the AUTHORS.md file'
@@ -34,7 +34,7 @@ namespace :repo do
file << <<~FOOTER
- This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead.
+ This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/mastodon/mastodon/graphs/contributors) instead.
FOOTER
end
diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake
new file mode 100644
index 0000000000..0f38b50e39
--- /dev/null
+++ b/lib/tasks/tests.rake
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+namespace :tests do
+ namespace :migrations do
+ desc 'Populate the database with test data for 2.0.0'
+ task populate_v2: :environment do
+ admin_key = OpenSSL::PKey::RSA.new(2048)
+ user_key = OpenSSL::PKey::RSA.new(2048)
+ remote_key = OpenSSL::PKey::RSA.new(2048)
+ remote_key2 = OpenSSL::PKey::RSA.new(2048)
+ remote_key3 = OpenSSL::PKey::RSA.new(2048)
+ admin_private_key = ActiveRecord::Base.connection.quote(admin_key.to_pem)
+ admin_public_key = ActiveRecord::Base.connection.quote(admin_key.public_key.to_pem)
+ user_private_key = ActiveRecord::Base.connection.quote(user_key.to_pem)
+ user_public_key = ActiveRecord::Base.connection.quote(user_key.public_key.to_pem)
+ remote_public_key = ActiveRecord::Base.connection.quote(remote_key.public_key.to_pem)
+ remote_public_key2 = ActiveRecord::Base.connection.quote(remote_key2.public_key.to_pem)
+ remote_public_key_ap = ActiveRecord::Base.connection.quote(remote_key3.public_key.to_pem)
+ local_domain = ActiveRecord::Base.connection.quote(Rails.configuration.x.local_domain)
+
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ -- accounts
+
+ INSERT INTO "accounts"
+ (id, username, domain, private_key, public_key, created_at, updated_at)
+ VALUES
+ (1, 'admin', NULL, #{admin_private_key}, #{admin_public_key}, now(), now()),
+ (2, 'user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
+
+ INSERT INTO "accounts"
+ (id, username, domain, private_key, public_key, created_at, updated_at, remote_url, salmon_url)
+ VALUES
+ (3, 'remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
+ 'https://remote.com/@remote', 'https://remote.com/salmon/1'),
+ (4, 'Remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
+ 'https://remote.com/@Remote', 'https://remote.com/salmon/1'),
+ (5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now(), now(),
+ 'https://remote.com/stale/@REMOTE', 'https://remote.com/stale/salmon/1');
+
+ INSERT INTO "accounts"
+ (id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url)
+ VALUES
+ (6, 'bob', 'activitypub.com', NULL, #{remote_public_key_ap}, now(), now(),
+ 1, 'https://activitypub.com/users/bob/inbox', 'https://activitypub.com/users/bob/outbox', 'https://activitypub.com/users/bob/followers');
+
+ INSERT INTO "accounts"
+ (id, username, domain, private_key, public_key, created_at, updated_at)
+ VALUES
+ (7, 'user', #{local_domain}, #{user_private_key}, #{user_public_key}, now(), now()),
+ (8, 'pt_user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
+
+ -- users
+
+ INSERT INTO "users"
+ (id, account_id, email, created_at, updated_at, admin)
+ VALUES
+ (1, 1, 'admin@localhost', now(), now(), true),
+ (2, 2, 'user@localhost', now(), now(), false);
+
+ INSERT INTO "users"
+ (id, account_id, email, created_at, updated_at, admin, locale)
+ VALUES
+ (3, 7, 'ptuser@localhost', now(), now(), false, 'pt');
+
+ -- statuses
+
+ INSERT INTO "statuses"
+ (id, account_id, text, created_at, updated_at)
+ VALUES
+ (1, 1, 'test', now(), now()),
+ (2, 1, '@remote@remote.com hello', now(), now()),
+ (3, 1, '@Remote@remote.com hello', now(), now()),
+ (4, 1, '@REMOTE@remote.com hello', now(), now());
+
+ INSERT INTO "statuses"
+ (id, account_id, text, created_at, updated_at, uri, local)
+ VALUES
+ (5, 1, 'activitypub status', now(), now(), 'https://localhost/users/admin/statuses/4', true);
+
+ INSERT INTO "statuses"
+ (id, account_id, text, created_at, updated_at)
+ VALUES
+ (6, 3, 'test', now(), now());
+
+ INSERT INTO "statuses"
+ (id, account_id, text, created_at, updated_at, in_reply_to_id, in_reply_to_account_id)
+ VALUES
+ (7, 4, '@admin hello', now(), now(), 3, 1);
+
+ INSERT INTO "statuses"
+ (id, account_id, text, created_at, updated_at)
+ VALUES
+ (8, 5, 'test', now(), now());
+
+ INSERT INTO "statuses"
+ (id, account_id, reblog_of_id, created_at, updated_at)
+ VALUES
+ (9, 1, 2, now(), now());
+
+ -- mentions (from previous statuses)
+
+ INSERT INTO "mentions"
+ (status_id, account_id, created_at, updated_at)
+ VALUES
+ (2, 3, now(), now()),
+ (3, 4, now(), now()),
+ (4, 5, now(), now());
+
+ -- stream entries
+
+ INSERT INTO "stream_entries"
+ (activity_id, account_id, activity_type, created_at, updated_at)
+ VALUES
+ (1, 1, 'status', now(), now()),
+ (2, 1, 'status', now(), now()),
+ (3, 1, 'status', now(), now()),
+ (4, 1, 'status', now(), now()),
+ (5, 1, 'status', now(), now()),
+ (6, 3, 'status', now(), now()),
+ (7, 4, 'status', now(), now()),
+ (8, 5, 'status', now(), now()),
+ (9, 1, 'status', now(), now());
+
+
+ -- custom emoji
+
+ INSERT INTO "custom_emojis"
+ (shortcode, created_at, updated_at)
+ VALUES
+ ('test', now(), now()),
+ ('Test', now(), now()),
+ ('blobcat', now(), now());
+
+ INSERT INTO "custom_emojis"
+ (shortcode, domain, uri, created_at, updated_at)
+ VALUES
+ ('blobcat', 'remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
+ ('blobcat', 'Remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
+ ('Blobcat', 'remote.org', 'https://remote.org/emoji/Blobcat', now(), now());
+
+ -- favourites
+
+ INSERT INTO "favourites"
+ (account_id, status_id, created_at, updated_at)
+ VALUES
+ (1, 1, now(), now()),
+ (1, 7, now(), now()),
+ (4, 1, now(), now()),
+ (3, 1, now(), now()),
+ (5, 1, now(), now());
+
+ -- pinned statuses
+
+ INSERT INTO "status_pins"
+ (account_id, status_id, created_at, updated_at)
+ VALUES
+ (1, 1, now(), now()),
+ (3, 6, now(), now()),
+ (4, 7, now(), now());
+
+ -- follows
+
+ INSERT INTO "follows"
+ (account_id, target_account_id, created_at, updated_at)
+ VALUES
+ (1, 5, now(), now()),
+ (6, 2, now(), now()),
+ (5, 2, now(), now()),
+ (6, 1, now(), now());
+
+ -- follow requests
+
+ INSERT INTO "follow_requests"
+ (account_id, target_account_id, created_at, updated_at)
+ VALUES
+ (2, 5, now(), now()),
+ (5, 1, now(), now());
+ SQL
+ end
+ end
+end
diff --git a/package.json b/package.json
index 94d809eb47..0d415352e0 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "@tootsuite/mastodon",
+ "name": "@mastodon/mastodon",
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=12"
@@ -18,7 +18,7 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/tootsuite/mastodon.git"
+ "url": "https://github.com/mastodon/mastodon.git"
},
"browserslist": [
"last 2 versions",
diff --git a/scalingo.json b/scalingo.json
index 324356df0c..51d9b5b9f5 100644
--- a/scalingo.json
+++ b/scalingo.json
@@ -1,8 +1,8 @@
{
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
- "repository": "https://github.com/tootsuite/mastodon",
- "logo": "https://github.com/tootsuite.png",
+ "repository": "https://github.com/mastodon/mastodon",
+ "logo": "https://github.com/mastodon.png",
"env": {
"LOCAL_DOMAIN": {
"description": "The domain that your Mastodon instance will run on (this can be appname.scalingo.io or a custom domain)",
diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
index d373f56bdb..3a382ff27e 100644
--- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
+++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
@@ -5,11 +5,13 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
let!(:follower_1) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') }
let!(:follower_2) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') }
let!(:follower_3) { Fabricate(:account, domain: 'foo.com', uri: 'https://foo.com/users/a') }
+ let!(:follower_4) { Fabricate(:account, username: 'instance-actor', domain: 'example.com', uri: 'https://example.com') }
before do
follower_1.follow!(account)
follower_2.follow!(account)
follower_3.follow!(account)
+ follower_4.follow!(account)
end
before do
@@ -45,7 +47,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
it 'returns orderedItems with followers from example.com' do
expect(body[:orderedItems]).to be_an Array
- expect(body[:orderedItems].sort).to eq [follower_1.uri, follower_2.uri]
+ expect(body[:orderedItems].sort).to eq [follower_4.uri, follower_1.uri, follower_2.uri]
end
it 'returns private Cache-Control header' do
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index d23f2c17cb..1722690db1 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -55,6 +55,10 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
it_behaves_like 'cachable response'
+ it 'does not have a Vary header' do
+ expect(response.headers['Vary']).to be_nil
+ end
+
context 'when account is permanently suspended' do
before do
account.suspend!
@@ -96,6 +100,10 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
it_behaves_like 'cachable response'
+ it 'returns Vary header with Signature' do
+ expect(response.headers['Vary']).to include 'Signature'
+ end
+
context 'when account is permanently suspended' do
before do
account.suspend!
@@ -144,7 +152,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
it 'returns private Cache-Control header' do
- expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+ expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end
end
@@ -170,7 +178,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
it 'returns private Cache-Control header' do
- expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+ expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end
end
@@ -195,7 +203,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
it 'returns private Cache-Control header' do
- expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+ expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end
end
@@ -220,7 +228,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
it 'returns private Cache-Control header' do
- expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
+ expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end
end
end
diff --git a/spec/controllers/api/v1/accounts/notes_controller_spec.rb b/spec/controllers/api/v1/accounts/notes_controller_spec.rb
new file mode 100644
index 0000000000..0a2957fede
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/notes_controller_spec.rb
@@ -0,0 +1,48 @@
+require 'rails_helper'
+
+describe Api::V1::Accounts::NotesController do
+ render_views
+
+ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:accounts') }
+ let(:account) { Fabricate(:account) }
+ let(:comment) { 'foo' }
+
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'POST #create' do
+ subject do
+ post :create, params: { account_id: account.id, comment: comment }
+ end
+
+ context 'when account note has reasonable length' do
+ let(:comment) { 'foo' }
+
+ it 'returns http success' do
+ subject
+ expect(response).to have_http_status(200)
+ end
+
+ it 'updates account note' do
+ subject
+ expect(AccountNote.find_by(account_id: user.account.id, target_account_id: account.id).comment).to eq comment
+ end
+ end
+
+ context 'when account note exceends allowed length' do
+ let(:comment) { 'a' * 2_001 }
+
+ it 'returns 422' do
+ subject
+ expect(response).to have_http_status(422)
+ end
+
+ it 'does not create account note' do
+ subject
+ expect(AccountNote.where(account_id: user.account.id, target_account_id: account.id).exists?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index d03ae51e86..f718f5dd92 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -206,6 +206,38 @@ RSpec.describe Auth::SessionsController, type: :controller do
end
end
+ context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do
+ let!(:other_user) do
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ end
+
+ before do
+ post :create, params: { user: { email: other_user.email, password: other_user.password } }
+ post :create, params: { user: { email: user.email, password: user.password } }
+ end
+
+ it 'renders two factor authentication page' do
+ expect(controller).to render_template("two_factor")
+ expect(controller).to render_template(partial: "_otp_authentication_form")
+ end
+ end
+
+ context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do
+ let!(:other_user) do
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
+ end
+
+ before do
+ post :create, params: { user: { email: other_user.email, password: other_user.password } }
+ post :create, params: { user: { email: user.email, password: user.password } }
+ end
+
+ it 'renders two factor authentication page' do
+ expect(controller).to render_template("two_factor")
+ expect(controller).to render_template(partial: "_otp_authentication_form")
+ end
+ end
+
context 'using upcase email and password' do
before do
post :create, params: { user: { email: user.email.upcase, password: user.password } }
@@ -231,6 +263,21 @@ RSpec.describe Auth::SessionsController, type: :controller do
end
end
+ context 'using a valid OTP, attempting to leverage previous half-login to bypass password auth' do
+ let!(:other_user) do
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
+ end
+
+ before do
+ post :create, params: { user: { email: other_user.email, password: other_user.password } }
+ post :create, params: { user: { email: user.email, otp_attempt: user.current_otp } }, session: { attempt_user_updated_at: user.updated_at.to_s }
+ end
+
+ it "doesn't log the user in" do
+ expect(controller.current_user).to be_nil
+ end
+ end
+
context 'when the server has an decryption error' do
before do
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
@@ -380,6 +427,52 @@ RSpec.describe Auth::SessionsController, type: :controller do
end
end
+ context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do
+ let!(:other_user) do
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ end
+
+ before do
+ post :create, params: { user: { email: other_user.email, password: other_user.password } }
+ post :create, params: { user: { email: user.email, password: user.password } }
+ end
+
+ it 'renders sign in token authentication page' do
+ expect(controller).to render_template("sign_in_token")
+ end
+
+ it 'generates sign in token' do
+ expect(user.reload.sign_in_token).to_not be_nil
+ end
+
+ it 'sends sign in token e-mail' do
+ expect(UserMailer).to have_received(:sign_in_token)
+ end
+ end
+
+ context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do
+ let!(:other_user) do
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
+ end
+
+ before do
+ post :create, params: { user: { email: other_user.email, password: other_user.password } }
+ post :create, params: { user: { email: user.email, password: user.password } }
+ end
+
+ it 'renders sign in token authentication page' do
+ expect(controller).to render_template("sign_in_token")
+ end
+
+ it 'generates sign in token' do
+ expect(user.reload.sign_in_token).to_not be_nil
+ end
+
+ it 'sends sign in token e-mail' do
+ expect(UserMailer).to have_received(:sign_in_token).with(user, any_args)
+ end
+ end
+
context 'using a valid sign in token' do
before do
user.generate_sign_in_token && user.save
@@ -395,6 +488,22 @@ RSpec.describe Auth::SessionsController, type: :controller do
end
end
+ context 'using a valid sign in token, attempting to leverage previous half-login to bypass password auth' do
+ let!(:other_user) do
+ Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
+ end
+
+ before do
+ user.generate_sign_in_token && user.save
+ post :create, params: { user: { email: other_user.email, password: other_user.password } }
+ post :create, params: { user: { email: user.email, sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_updated_at: user.updated_at.to_s }
+ end
+
+ it "doesn't log the user in" do
+ expect(controller.current_user).to be_nil
+ end
+ end
+
context 'using an invalid sign in token' do
before do
post :create, params: { user: { sign_in_token_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
@@ -410,4 +519,33 @@ RSpec.describe Auth::SessionsController, type: :controller do
end
end
end
+
+ describe 'GET #webauthn_options' do
+ context 'with WebAuthn and OTP enabled as second factor' do
+ let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
+
+ let(:fake_client) { WebAuthn::FakeClient.new(domain) }
+
+ let!(:user) do
+ Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
+ end
+
+ before do
+ user.update(webauthn_id: WebAuthn.generate_user_id)
+ public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
+ user.webauthn_credentials.create(
+ nickname: 'SecurityKeyNickname',
+ external_id: public_key_credential.id,
+ public_key: public_key_credential.public_key,
+ sign_count: '1000'
+ )
+ post :create, params: { user: { email: user.email, password: user.password } }
+ end
+
+ it 'returns http success' do
+ get :webauthn_options
+ expect(response).to have_http_status :ok
+ end
+ end
+ end
end
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index f6d55f6932..0062741691 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe FollowerAccountsController do
render_views
- let(:alice) { Fabricate(:account, username: 'alice') }
+ let(:alice) { Fabricate(:user).account }
let(:follower0) { Fabricate(:account) }
let(:follower1) { Fabricate(:account) }
@@ -101,6 +101,23 @@ describe FollowerAccountsController do
expect(body['partOf']).to be_blank
end
+ context 'when account hides their network' do
+ before do
+ alice.user.settings.hide_network = true
+ end
+
+ it 'returns followers count' do
+ expect(body['totalItems']).to eq 2
+ end
+
+ it 'does not return items' do
+ expect(body['items']).to be_blank
+ expect(body['orderedItems']).to be_blank
+ expect(body['first']).to be_blank
+ expect(body['last']).to be_blank
+ end
+ end
+
context 'when account is permanently suspended' do
before do
alice.suspend!
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index 0fc0967a63..7ec0e3d069 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe FollowingAccountsController do
render_views
- let(:alice) { Fabricate(:account, username: 'alice') }
+ let(:alice) { Fabricate(:user).account }
let(:followee0) { Fabricate(:account) }
let(:followee1) { Fabricate(:account) }
@@ -101,6 +101,23 @@ describe FollowingAccountsController do
expect(body['partOf']).to be_blank
end
+ context 'when account hides their network' do
+ before do
+ alice.user.settings.hide_network = true
+ end
+
+ it 'returns followers count' do
+ expect(body['totalItems']).to eq 2
+ end
+
+ it 'does not return items' do
+ expect(body['items']).to be_blank
+ expect(body['orderedItems']).to be_blank
+ expect(body['first']).to be_blank
+ expect(body['last']).to be_blank
+ end
+ end
+
context 'when account is permanently suspended' do
before do
alice.suspend!
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
index 1075456f33..8574d369d1 100644
--- a/spec/controllers/well_known/webfinger_controller_spec.rb
+++ b/spec/controllers/well_known/webfinger_controller_spec.rb
@@ -24,6 +24,10 @@ describe WellKnown::WebfingerController, type: :controller do
expect(response).to have_http_status(200)
end
+ it 'does not set a Vary header' do
+ expect(response.headers['Vary']).to be_nil
+ end
+
it 'returns application/jrd+json' do
expect(response.media_type).to eq 'application/jrd+json'
end
diff --git a/spec/fixtures/requests/oembed_youtube.html b/spec/fixtures/requests/oembed_youtube.html
new file mode 100644
index 0000000000..1508e4dd95
--- /dev/null
+++ b/spec/fixtures/requests/oembed_youtube.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/spec/fixtures/xml/mastodon.atom b/spec/fixtures/xml/mastodon.atom
deleted file mode 100644
index 92921a9380..0000000000
--- a/spec/fixtures/xml/mastodon.atom
+++ /dev/null
@@ -1,261 +0,0 @@
-
-
- http://kickass.zone/users/localhost.atom
- ::1
- 2016-10-10T13:29:56Z
- http://kickass.zone/system/accounts/avatars/000/000/001/medium/eris.png
-
- http://activitystrea.ms/schema/1.0/person
- http://kickass.zone/users/localhost
- localhost
- localhost@kickass.zone
-
-
-
-
- localhost
- ::1
-
-
-
-
-
-
- tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow
- 2016-10-10T13:29:56Z
- 2016-10-10T13:29:56Z
- localhost started following kat@mastodon.social
- localhost started following kat@mastodon.social
- http://activitystrea.ms/schema/1.0/follow
-
-
- http://activitystrea.ms/schema/1.0/activity
-
- http://activitystrea.ms/schema/1.0/person
- https://mastodon.social/users/kat
- kat
- kat@mastodon.social
- #trans #queer
-
-
-
-
- kat
- Kat
- #trans #queer
-
-
-
- tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite
- 2016-10-10T13:29:26Z
- 2016-10-10T13:29:26Z
- localhost favourited a status by kat@mastodon.social
- localhost favourited a status by kat@mastodon.social
- http://activitystrea.ms/schema/1.0/favorite
-
-
- http://activitystrea.ms/schema/1.0/activity
-
-
- http://activitystrea.ms/schema/1.0/comment
- tag:mastodon.social,2016-10-10:objectId=22833:objectType=Status
- @localhost oooh more mastodons ❤
-
- <p><a href="http://kickass.zone/users/localhost">@localhost</a> oooh more mastodons ❤</p>
- http://activitystrea.ms/schema/1.0/post
- 2016-10-10T13:23:35Z
- 2016-10-10T13:23:35Z
-
- http://activitystrea.ms/schema/1.0/person
- https://mastodon.social/users/kat
- kat
- kat@mastodon.social
- #trans #queer
-
-
-
-
- kat
- Kat
- #trans #queer
-
-
-
-
-
- tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite
- 2016-10-10T13:13:15Z
- 2016-10-10T13:13:15Z
- localhost favourited a status by Gargron@mastodon.social
- localhost favourited a status by Gargron@mastodon.social
- http://activitystrea.ms/schema/1.0/favorite
-
-
- http://activitystrea.ms/schema/1.0/activity
-
-
- http://activitystrea.ms/schema/1.0/note
- tag:mastodon.social,2016-10-10:objectId=22825:objectType=Status
- Deployed some fixes
-
- <p>Deployed some fixes</p>
- http://activitystrea.ms/schema/1.0/post
- 2016-10-10T13:10:37Z
- 2016-10-10T13:10:37Z
-
- http://activitystrea.ms/schema/1.0/person
- https://mastodon.social/users/Gargron
- Gargron
- Gargron@mastodon.social
- Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon
-
-
-
-
- Gargron
- Eugen
- Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon
-
-
-
-
- tag:kickass.zone,2016-10-10:objectId=17:objectType=Status
- 2016-10-10T00:41:31Z
- 2016-10-10T00:41:31Z
- Social media needs MOAR cats! http://kickass.zone/media/3
- <p>Social media needs MOAR cats! <a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/3">http://kickass.zone/media/3</a></p>
- http://activitystrea.ms/schema/1.0/post
-
-
- http://activitystrea.ms/schema/1.0/note
-
-
-
- tag:kickass.zone,2016-10-10:objectId=14:objectType=Status
- 2016-10-10T00:38:39Z
- 2016-10-10T00:38:39Z
- http://kickass.zone/media/2
- <p><a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/2">http://kickass.zone/media/2</a></p>
- http://activitystrea.ms/schema/1.0/post
-
-
- http://activitystrea.ms/schema/1.0/note
-
-
-
- tag:kickass.zone,2016-10-10:objectId=12:objectType=Status
- 2016-10-10T00:37:49Z
- 2016-10-10T00:37:49Z
-
- http://activitystrea.ms/schema/1.0/delete
-
-
- http://activitystrea.ms/schema/1.0/activity
-
-
- tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow
- 2016-10-10T00:23:07Z
- 2016-10-10T00:23:07Z
- localhost started following bignimbus@mastodon.social
- localhost started following bignimbus@mastodon.social
- http://activitystrea.ms/schema/1.0/follow
-
-
- http://activitystrea.ms/schema/1.0/activity
-
- http://activitystrea.ms/schema/1.0/person
- https://mastodon.social/users/bignimbus
- bignimbus
- bignimbus@mastodon.social
- jdauriemma.com
-
-
-
-
- bignimbus
- Jeff Auriemma
- jdauriemma.com
-
-
-
- tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow
- 2016-10-10T00:14:18Z
- 2016-10-10T00:14:18Z
- localhost started following Gargron@mastodon.social
- localhost started following Gargron@mastodon.social
- http://activitystrea.ms/schema/1.0/follow
-
-
- http://activitystrea.ms/schema/1.0/activity
-
- http://activitystrea.ms/schema/1.0/person
- https://mastodon.social/users/Gargron
- Gargron
- Gargron@mastodon.social
- Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon
-
-
-
-
- Gargron
- Eugen
- Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon
-
-
-
- tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow
- 2016-10-10T00:09:09Z
- 2016-10-10T00:09:09Z
- localhost started following abc@mastodon.social
- localhost started following abc@mastodon.social
- http://activitystrea.ms/schema/1.0/follow
-
-
- http://activitystrea.ms/schema/1.0/activity
-
- http://activitystrea.ms/schema/1.0/person
- https://mastodon.social/users/abc
- abc
- abc@mastodon.social
-
-
-
-
- abc
- abc
-
-
-
- tag:kickass.zone,2016-10-10:objectId=3:objectType=Status
- 2016-10-10T00:02:47Z
- 2016-10-10T00:02:47Z
-
- http://activitystrea.ms/schema/1.0/delete
-
-
- http://activitystrea.ms/schema/1.0/activity
-
-
- tag:kickass.zone,2016-10-10:objectId=2:objectType=Status
- 2016-10-10T00:02:18Z
- 2016-10-10T00:02:18Z
- Yes, that was the obligatory first post. :)
- <p>Yes, that was the obligatory first post. :)</p>
- http://activitystrea.ms/schema/1.0/post
-
-
- http://activitystrea.ms/schema/1.0/comment
-
-
-
- tag:kickass.zone,2016-10-10:objectId=1:objectType=Status
- 2016-10-10T00:01:56Z
- 2016-10-10T00:01:56Z
- Hello, world!
- <p>Hello, world!</p>
- http://activitystrea.ms/schema/1.0/post
-
-
- http://activitystrea.ms/schema/1.0/note
-
-
diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb
index 1c5c6f0edd..606a1de2e5 100644
--- a/spec/lib/activitypub/tag_manager_spec.rb
+++ b/spec/lib/activitypub/tag_manager_spec.rb
@@ -42,6 +42,14 @@ RSpec.describe ActivityPub::TagManager do
expect(subject.to(status)).to eq [subject.uri_for(mentioned)]
end
+ it "returns URIs of mentioned group's followers for direct statuses to groups" do
+ status = Fabricate(:status, visibility: :direct)
+ mentioned = Fabricate(:account, domain: 'remote.org', uri: 'https://remote.org/group', followers_url: 'https://remote.org/group/followers', actor_type: 'Group')
+ status.mentions.create(account: mentioned)
+ expect(subject.to(status)).to include(subject.uri_for(mentioned))
+ expect(subject.to(status)).to include(subject.followers_uri_for(mentioned))
+ end
+
it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do
bob = Fabricate(:account, username: 'bob')
alice = Fabricate(:account, username: 'alice')
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 03d6f5fb0f..65e6714c0e 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -5,6 +5,37 @@ RSpec.describe Account, type: :model do
let(:bob) { Fabricate(:account, username: 'bob') }
subject { Fabricate(:account) }
+ describe '#suspend!' do
+ it 'marks the account as suspended' do
+ subject.suspend!
+ expect(subject.suspended?).to be true
+ end
+
+ it 'creates a deletion request' do
+ subject.suspend!
+ expect(AccountDeletionRequest.where(account: subject).exists?).to be true
+ end
+
+ context 'when the account is of a local user' do
+ let!(:subject) { Fabricate(:account, user: Fabricate(:user, email: 'foo+bar@domain.org')) }
+
+ it 'creates a canonical domain block' do
+ subject.suspend!
+ expect(CanonicalEmailBlock.block?(subject.user_email)).to be true
+ end
+
+ context 'when a canonical domain block already exists for that email' do
+ before do
+ Fabricate(:canonical_email_block, email: subject.user_email)
+ end
+
+ it 'does not raise an error' do
+ expect { subject.suspend! }.not_to raise_error
+ end
+ end
+ end
+ end
+
describe '#follow!' do
it 'creates a follow' do
follow = subject.follow!(bob)
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
index 85fbf7e79c..ca243ebc5e 100644
--- a/spec/models/concerns/account_interactions_spec.rb
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -539,46 +539,57 @@ describe AccountInteractions do
end
end
- describe '#followers_hash' do
+ describe '#remote_followers_hash' do
let(:me) { Fabricate(:account, username: 'Me') }
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
- let(:remote_3) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
+ let(:remote_3) { Fabricate(:account, username: 'instance-actor', domain: 'example.org', uri: 'https://example.org') }
+ let(:remote_4) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
before do
remote_1.follow!(me)
remote_2.follow!(me)
remote_3.follow!(me)
+ remote_4.follow!(me)
me.follow!(remote_1)
end
- context 'on a local user' do
- it 'returns correct hash for remote domains' do
- expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
- expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
- end
+ it 'returns correct hash for remote domains' do
+ expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
+ expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
+ expect(me.remote_followers_hash('https://foo.org.evil.com/')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
+ expect(me.remote_followers_hash('https://foo')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
+ end
- it 'invalidates cache as needed when removing or adding followers' do
- expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
- remote_1.unfollow!(me)
- expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
- remote_1.follow!(me)
- expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
- end
+ it 'invalidates cache as needed when removing or adding followers' do
+ expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
+ remote_3.unfollow!(me)
+ expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
+ remote_1.unfollow!(me)
+ expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
+ remote_1.follow!(me)
+ expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
end
+ end
- context 'on a remote user' do
- it 'returns correct hash for remote domains' do
- expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
- end
+ describe '#local_followers_hash' do
+ let(:me) { Fabricate(:account, username: 'Me') }
+ let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
- it 'invalidates cache as needed when removing or adding followers' do
- expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
- me.unfollow!(remote_1)
- expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
- me.follow!(remote_1)
- expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
- end
+ before do
+ me.follow!(remote_1)
+ end
+
+ it 'returns correct hash for local users' do
+ expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
+ end
+
+ it 'invalidates cache as needed when removing or adding followers' do
+ expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
+ me.unfollow!(remote_1)
+ expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
+ me.follow!(remote_1)
+ expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
end
end
diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb
index 93a4e88e41..973b3e23c8 100644
--- a/spec/presenters/instance_presenter_spec.rb
+++ b/spec/presenters/instance_presenter_spec.rb
@@ -91,8 +91,8 @@ describe InstancePresenter do
end
describe '#source_url' do
- it 'returns "https://github.com/tootsuite/mastodon"' do
- expect(instance_presenter.source_url).to eq('https://github.com/tootsuite/mastodon')
+ it 'returns "https://github.com/mastodon/mastodon"' do
+ expect(instance_presenter.source_url).to eq('https://github.com/mastodon/mastodon')
end
end
diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb
index 56e7f83211..1b1d878a7e 100644
--- a/spec/services/activitypub/process_account_service_spec.rb
+++ b/spec/services/activitypub/process_account_service_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
attachment: [
{ type: 'PropertyValue', name: 'Pronouns', value: 'They/them' },
{ type: 'PropertyValue', name: 'Occupation', value: 'Unit test' },
+ { type: 'PropertyValue', name: 'non-string', value: ['foo', 'bar'] },
],
}.with_indifferent_access
end
diff --git a/spec/services/fetch_oembed_service_spec.rb b/spec/services/fetch_oembed_service_spec.rb
index a4262b0408..88f0113edd 100644
--- a/spec/services/fetch_oembed_service_spec.rb
+++ b/spec/services/fetch_oembed_service_spec.rb
@@ -13,6 +13,32 @@ describe FetchOEmbedService, type: :service do
describe 'discover_provider' do
context 'when status code is 200 and MIME type is text/html' do
+ context 'when OEmbed endpoint contains URL as parameter' do
+ before do
+ stub_request(:get, 'https://www.youtube.com/watch?v=IPSbNdBmWKE').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_youtube.html'),
+ )
+ stub_request(:get, 'https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIPSbNdBmWKE').to_return(
+ status: 200,
+ headers: { 'Content-Type': 'text/html' },
+ body: request_fixture('oembed_json_empty.html')
+ )
+ end
+
+ it 'returns new OEmbed::Provider for JSON provider' do
+ subject.call('https://www.youtube.com/watch?v=IPSbNdBmWKE')
+ expect(subject.endpoint_url).to eq 'https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIPSbNdBmWKE'
+ expect(subject.format).to eq :json
+ end
+
+ it 'stores URL template' do
+ subject.call('https://www.youtube.com/watch?v=IPSbNdBmWKE')
+ expect(Rails.cache.read('oembed_endpoint:www.youtube.com')[:endpoint]).to eq 'https://www.youtube.com/oembed?format=json&url={url}'
+ end
+ end
+
context 'Both of JSON and XML provider are discoverable' do
before do
stub_request(:get, 'https://host.test/oembed.html').to_return(
@@ -33,6 +59,11 @@ describe FetchOEmbedService, type: :service do
expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
expect(subject.format).to eq :xml
end
+
+ it 'does not cache OEmbed endpoint' do
+ subject.call('https://host.test/oembed.html', format: :xml)
+ expect(Rails.cache.exist?('oembed_endpoint:host.test')).to eq false
+ end
end
context 'JSON provider is discoverable while XML provider is not' do
@@ -49,6 +80,11 @@ describe FetchOEmbedService, type: :service do
expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
expect(subject.format).to eq :json
end
+
+ it 'does not cache OEmbed endpoint' do
+ subject.call('https://host.test/oembed.html')
+ expect(Rails.cache.exist?('oembed_endpoint:host.test')).to eq false
+ end
end
context 'XML provider is discoverable while JSON provider is not' do
@@ -65,6 +101,11 @@ describe FetchOEmbedService, type: :service do
expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
expect(subject.format).to eq :xml
end
+
+ it 'does not cache OEmbed endpoint' do
+ subject.call('https://host.test/oembed.html')
+ expect(Rails.cache.exist?('oembed_endpoint:host.test')).to eq false
+ end
end
context 'Invalid XML provider is discoverable while JSON provider is not' do
diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb
index f2cb22c5ef..7433866b76 100644
--- a/spec/services/notify_service_spec.rb
+++ b/spec/services/notify_service_spec.rb
@@ -64,8 +64,9 @@ RSpec.describe NotifyService, type: :service do
is_expected.to_not change(Notification, :count)
end
- context 'if the message chain initiated by recipient, but is not direct message' do
+ context 'if the message chain is initiated by recipient, but is not direct message' do
let(:reply_to) { Fabricate(:status, account: recipient) }
+ let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
it 'does not notify' do
@@ -73,8 +74,20 @@ RSpec.describe NotifyService, type: :service do
end
end
- context 'if the message chain initiated by recipient and is direct message' do
+ context 'if the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
+ let(:reply_to) { Fabricate(:status, account: recipient) }
+ let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
+ let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) }
+ let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) }
+
+ it 'does not notify' do
+ is_expected.to_not change(Notification, :count)
+ end
+ end
+
+ context 'if the message chain is initiated by the recipient with a mention to the sender' do
let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
+ let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
it 'does notify' do
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 147a59fc31..d21270c793 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -25,29 +25,33 @@ RSpec.describe PostStatusService, type: :service do
expect(status.thread).to eq in_reply_to_status
end
- it 'schedules a status' do
- account = Fabricate(:account)
- future = Time.now.utc + 2.hours
-
- status = subject.call(account, text: 'Hi future!', scheduled_at: future)
-
- expect(status).to be_a ScheduledStatus
- expect(status.scheduled_at).to eq future
- expect(status.params['text']).to eq 'Hi future!'
- end
-
- it 'does not immediately create a status when scheduling a status' do
- account = Fabricate(:account)
- media = Fabricate(:media_attachment)
- future = Time.now.utc + 2.hours
-
- status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future)
-
- expect(status).to be_a ScheduledStatus
- expect(status.scheduled_at).to eq future
- expect(status.params['text']).to eq 'Hi future!'
- expect(media.reload.status).to be_nil
- expect(Status.where(text: 'Hi future!').exists?).to be_falsey
+ context 'when scheduling a status' do
+ let!(:account) { Fabricate(:account) }
+ let!(:future) { Time.now.utc + 2.hours }
+ let!(:previous_status) { Fabricate(:status, account: account) }
+
+ it 'schedules a status' do
+ status = subject.call(account, text: 'Hi future!', scheduled_at: future)
+ expect(status).to be_a ScheduledStatus
+ expect(status.scheduled_at).to eq future
+ expect(status.params['text']).to eq 'Hi future!'
+ end
+
+ it 'does not immediately create a status' do
+ media = Fabricate(:media_attachment, account: account)
+ status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future)
+
+ expect(status).to be_a ScheduledStatus
+ expect(status.scheduled_at).to eq future
+ expect(status.params['text']).to eq 'Hi future!'
+ expect(status.params['media_ids']).to eq [media.id]
+ expect(media.reload.status).to be_nil
+ expect(Status.where(text: 'Hi future!').exists?).to be_falsey
+ end
+
+ it 'does not change statuses count' do
+ expect { subject.call(account, text: 'Hi future!', scheduled_at: future, thread: previous_status) }.not_to change { [account.statuses_count, previous_status.replies_count] }
+ end
end
it 'creates response to the original status of boost' do
diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb
index 3b2f9d6984..d74e8dc624 100644
--- a/spec/services/process_mentions_service_spec.rb
+++ b/spec/services/process_mentions_service_spec.rb
@@ -42,6 +42,24 @@ RSpec.describe ProcessMentionsService, type: :service do
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
end
end
+
+ context 'with an IDN TLD' do
+ let(:remote_user) { Fabricate(:account, username: 'foo', protocol: :activitypub, domain: 'xn--y9a3aq.xn--y9a3aq', inbox_url: 'http://example.com/inbox') }
+ let(:status) { Fabricate(:status, account: account, text: "Hello @foo@հայ.հայ") }
+
+ before do
+ stub_request(:post, remote_user.inbox_url)
+ subject.call(status)
+ end
+
+ it 'creates a mention' do
+ expect(remote_user.mentions.where(status: status).count).to eq 1
+ end
+
+ it 'sends activity to the inbox' do
+ expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
+ end
+ end
end
context 'Temporarily-unreachable ActivityPub user' do
diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb
index c75c287598..1c2f5eee95 100644
--- a/spec/views/about/show.html.haml_spec.rb
+++ b/spec/views/about/show.html.haml_spec.rb
@@ -17,7 +17,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
site_short_description: 'something',
site_description: 'something',
version_number: '1.0',
- source_url: 'https://github.com/tootsuite/mastodon',
+ source_url: 'https://github.com/mastodon/mastodon',
open_registrations: false,
thumbnail: nil,
hero: nil,
diff --git a/spec/workers/activitypub/delivery_worker_spec.rb b/spec/workers/activitypub/delivery_worker_spec.rb
index f4633731e5..d39393d507 100644
--- a/spec/workers/activitypub/delivery_worker_spec.rb
+++ b/spec/workers/activitypub/delivery_worker_spec.rb
@@ -11,7 +11,7 @@ describe ActivityPub::DeliveryWorker do
let(:payload) { 'test' }
before do
- allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/').and_return('somehash')
+ allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
end
describe 'perform' do
diff --git a/spec/workers/move_worker_spec.rb b/spec/workers/move_worker_spec.rb
index 8ab4f182fd..82449b0c7e 100644
--- a/spec/workers/move_worker_spec.rb
+++ b/spec/workers/move_worker_spec.rb
@@ -9,7 +9,8 @@ describe MoveWorker do
let(:source_account) { Fabricate(:account, protocol: :activitypub, domain: 'example.com') }
let(:target_account) { Fabricate(:account, protocol: :activitypub, domain: 'example.com') }
let(:local_user) { Fabricate(:user) }
- let!(:account_note) { Fabricate(:account_note, account: local_user.account, target_account: source_account) }
+ let(:comment) { 'old note prior to move' }
+ let!(:account_note) { Fabricate(:account_note, account: local_user.account, target_account: source_account, comment: comment) }
let(:block_service) { double }
@@ -26,19 +27,37 @@ describe MoveWorker do
end
shared_examples 'user note handling' do
- it 'copies user note' do
- subject.perform(source_account.id, target_account.id)
- expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(source_account.acct)
- expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
+ context 'when user notes are short enough' do
+ it 'copies user note with prelude' do
+ subject.perform(source_account.id, target_account.id)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(source_account.acct)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
+ end
+
+ it 'merges user notes when needed' do
+ new_account_note = AccountNote.create!(account: account_note.account, target_account: target_account, comment: 'new note prior to move')
+
+ subject.perform(source_account.id, target_account.id)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(source_account.acct)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(new_account_note.comment)
+ end
end
- it 'merges user notes when needed' do
- new_account_note = AccountNote.create!(account: account_note.account, target_account: target_account, comment: 'new note prior to move')
+ context 'when user notes are too long' do
+ let(:comment) { 'abc' * 333 }
- subject.perform(source_account.id, target_account.id)
- expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(source_account.acct)
- expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
- expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(new_account_note.comment)
+ it 'copies user note without prelude' do
+ subject.perform(source_account.id, target_account.id)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
+ end
+
+ it 'keeps user notes unchanged' do
+ new_account_note = AccountNote.create!(account: account_note.account, target_account: target_account, comment: 'new note prior to move')
+
+ subject.perform(source_account.id, target_account.id)
+ expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(new_account_note.comment)
+ end
end
end
diff --git a/spec/workers/publish_scheduled_announcement_worker_spec.rb b/spec/workers/publish_scheduled_announcement_worker_spec.rb
new file mode 100644
index 0000000000..0977bba1ee
--- /dev/null
+++ b/spec/workers/publish_scheduled_announcement_worker_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PublishScheduledAnnouncementWorker do
+ subject { described_class.new }
+
+ let!(:remote_account) { Fabricate(:account, domain: 'domain.com', username: 'foo', uri: 'https://domain.com/users/foo') }
+ let!(:remote_status) { Fabricate(:status, uri: 'https://domain.com/users/foo/12345', account: remote_account) }
+ let!(:local_status) { Fabricate(:status) }
+ let(:scheduled_announcement) { Fabricate(:announcement, text: "rebooting very soon, see #{ActivityPub::TagManager.instance.uri_for(remote_status)} and #{ActivityPub::TagManager.instance.uri_for(local_status)}") }
+
+ describe 'perform' do
+ before do
+ service = double
+ allow(FetchRemoteStatusService).to receive(:new).and_return(service)
+ allow(service).to receive(:call).with('https://domain.com/users/foo/12345') { remote_status.reload }
+
+ subject.perform(scheduled_announcement.id)
+ end
+
+ it 'updates the linked statuses' do
+ expect(scheduled_announcement.reload.status_ids).to eq [remote_status.id, local_status.id]
+ end
+ end
+end