{ancestors}
diff --git a/app/javascript/mastodon/features/ui/components/audio_modal.js b/app/javascript/mastodon/features/ui/components/audio_modal.js
index 0676bd9cf..c46fefce8 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 1227fa453..65d97ca16 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 447862499..ab4260a5e 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 08da10330..061776e24 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 2f13a175a..7e8e1329d 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 2d27180f7..ad1e8a2ee 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 c1c6ac739..756a69293 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 cb53887c7..ea81b4332 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 06cd8c5e8..48772ae7f 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 25ebc19b0..2744c6c6c 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 7ca027e81..36c1461b0 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 9a2960507..d70ce6103 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 f6b5e10d3..f6b9741fa 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 f6eb01913..9d5bd67e8 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 e0e022cea..1ffb5b4bf 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 1a5feb349..2f2a55b55 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 bf61df923..b338bc92f 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 a8546d65a..be8c45bfe 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 958f6c78e..763567f42 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 0cda8ad0a..338fb8d14 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 afa7cc0ea..fee32cba2 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 dafe8f55b..4786aa760 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 16dcc7ad6..7d385a00f 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 60be9b9dc..4cbaa04c6 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 fc187db40..e78c74d1e 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 0a383d6a3..85aaec4d6 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 5400612bf..b266c019e 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 949c670aa..7e52a7594 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 6ad509551..fc664e105 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 cd93b0f58..f4429622c 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 9cc72785e..d367b2155 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 cae5a5ac9..d9966723a 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 1a81b96f6..72e9c6611 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 e8a2b46fd..05b585ab6 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 af5a4aaf7..00196dd01 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 990cf9ec8..dc81007ac 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 adf4ca7b2..ac0c72816 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 7975ee999..04639e32c 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 f521aff22..0fa3cffb5 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 297379893..65de7f8f3 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 7463b266f..11ca79592 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 6c5a576a7..788f2cf80 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 39e321316..4a900e3b8 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 024d40573..9f7e85b73 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 f844cc871..f642e6e59 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 ef612e177..5232e6cfd 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 3ff2aa9e5..84c09ff35 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 6274031dc..9d5185fe6 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 bd4f4c2a3..40537e9c9 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 a26d54949..8c0301d69 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 b7288d38a..9e8f8b424 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 304e49322..d717ffc54 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 a1f1660bf..32c44646c 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 000000000..64cf84448
--- /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 37dda654e..387ddb066 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 799abeaee..8295bbc0f 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 673c5cc85..f67c6eecf 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 52eea7a74..288a4fdf6 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 3f1658566..8815e137a 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 59c118500..36ca71844 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 39a6e0680..5bc903349 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 5f9499f6a..06103454e 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 17a2abd25..deb89717a 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 72bacb5eb..d905a07da 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 86c358a94..d004c5751 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 000000000..0f38b50e3
--- /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 94d809eb4..0d415352e 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 324356df0..51d9b5b9f 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 d373f56bd..3a382ff27 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 d23f2c17c..1722690db 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 000000000..0a2957fed
--- /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 d03ae51e8..f718f5dd9 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 f6d55f693..006274169 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 0fc0967a6..7ec0e3d06 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 1075456f3..8574d369d 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 000000000..1508e4dd9
--- /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 92921a938..000000000
--- 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 1c5c6f0ed..606a1de2e 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 03d6f5fb0..65e6714c0 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 85fbf7e79..ca243ebc5 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 93a4e88e4..973b3e23c 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 56e7f8321..1b1d878a7 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 a4262b040..88f0113ed 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 f2cb22c5e..7433866b7 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 147a59fc3..d21270c79 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 3b2f9d698..d74e8dc62 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 c75c28759..1c2f5eee9 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 f4633731e..d39393d50 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 8ab4f182f..82449b0c7 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 000000000..0977bba1e
--- /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