commit
3a58d82dc0
17
CHANGELOG.md
17
CHANGELOG.md
@ -3,6 +3,23 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [3.4.4] - 2021-11-26
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036))
|
||||||
|
- Fix overflow of long profile fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17010))
|
||||||
|
- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986))
|
||||||
|
- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998))
|
||||||
|
- Fix logout link not working in Safari ([noellabo](https://github.com/mastodon/mastodon/pull/16574))
|
||||||
|
- Fix “open” link of media modal not closing modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16524))
|
||||||
|
- Fix replying from modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16516))
|
||||||
|
- Fix `mastodon:setup` command crashing in some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042))
|
||||||
|
- Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041))
|
||||||
|
|
||||||
## [3.4.3] - 2021-11-06
|
## [3.4.3] - 2021-11-06
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -6,6 +6,10 @@ import { multiply } from 'color-blend';
|
|||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
onConfirm: () => logOut(),
|
onConfirm: () => logOut(),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@ -86,6 +86,7 @@ class Compose extends React.PureComponent {
|
|||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
onConfirm: () => logOut(),
|
onConfirm: () => logOut(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -118,7 +118,11 @@ class Footer extends ImmutablePureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status } = this.props;
|
const { status, onClose } = this.props;
|
||||||
|
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
router.history.push(`/statuses/${status.get('id')}`);
|
router.history.push(`/statuses/${status.get('id')}`);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ const makeMapStateToProps = () => {
|
|||||||
ancestorsIds = ancestorsIds.withMutations(mutable => {
|
ancestorsIds = ancestorsIds.withMutations(mutable => {
|
||||||
let id = statusId;
|
let id = statusId;
|
||||||
|
|
||||||
while (id) {
|
while (id && !mutable.includes(id)) {
|
||||||
mutable.unshift(id);
|
mutable.unshift(id);
|
||||||
id = inReplyTos.get(id);
|
id = inReplyTos.get(id);
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ const makeMapStateToProps = () => {
|
|||||||
const ids = [statusId];
|
const ids = [statusId];
|
||||||
|
|
||||||
while (ids.length > 0) {
|
while (ids.length > 0) {
|
||||||
let id = ids.shift();
|
let id = ids.pop();
|
||||||
const replies = contextReplies.get(id);
|
const replies = contextReplies.get(id);
|
||||||
|
|
||||||
if (statusId !== id) {
|
if (statusId !== id) {
|
||||||
@ -115,7 +115,7 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
if (replies) {
|
if (replies) {
|
||||||
replies.reverse().forEach(reply => {
|
replies.reverse().forEach(reply => {
|
||||||
ids.unshift(reply);
|
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent {
|
|||||||
onConfirm: PropTypes.func.isRequired,
|
onConfirm: PropTypes.func.isRequired,
|
||||||
secondary: PropTypes.string,
|
secondary: PropTypes.string,
|
||||||
onSecondary: PropTypes.func,
|
onSecondary: PropTypes.func,
|
||||||
|
closeWhenConfirm: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
closeWhenConfirm: true,
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.button.focus();
|
this.button.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
|
if (this.props.closeWhenConfirm) {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
|
}
|
||||||
this.props.onConfirm();
|
this.props.onConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
onConfirm: () => logOut(),
|
onConfirm: () => logOut(),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@ -829,6 +829,7 @@ a.name-tag,
|
|||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
flex: 1 0 50%;
|
flex: 1 0 50%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__header__fields,
|
.account__header__fields,
|
||||||
|
@ -46,7 +46,9 @@ class Webfinger
|
|||||||
def body_from_webfinger(url = standard_url, use_fallback = true)
|
def body_from_webfinger(url = standard_url, use_fallback = true)
|
||||||
webfinger_request(url).perform do |res|
|
webfinger_request(url).perform do |res|
|
||||||
if res.code == 200
|
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
|
elsif res.code == 404 && use_fallback
|
||||||
body_from_host_meta
|
body_from_host_meta
|
||||||
elsif res.code == 410
|
elsif res.code == 410
|
||||||
|
@ -15,7 +15,7 @@ class CanonicalEmailBlock < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :reference_account, class_name: 'Account'
|
belongs_to :reference_account, class_name: 'Account'
|
||||||
|
|
||||||
validates :canonical_email_hash, presence: true
|
validates :canonical_email_hash, presence: true, uniqueness: true
|
||||||
|
|
||||||
def email=(email)
|
def email=(email)
|
||||||
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
||||||
|
@ -67,8 +67,49 @@ class NotifyService < BaseService
|
|||||||
message? && @notification.target_status.direct_visibility?
|
message? && @notification.target_status.direct_visibility?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns true if the sender has been mentionned by the recipient up the thread
|
||||||
def response_to_recipient?
|
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
|
end
|
||||||
|
|
||||||
def from_staff?
|
def from_staff?
|
||||||
|
@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
3
|
4
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
@ -17,9 +17,9 @@ module Paperclip
|
|||||||
|
|
||||||
def cache_current_values
|
def cache_current_values
|
||||||
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
||||||
@size = @target.response.content_length
|
|
||||||
@tempfile = copy_to_tempfile(@target)
|
@tempfile = copy_to_tempfile(@target)
|
||||||
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
||||||
|
@size = File.size(@tempfile)
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy_to_tempfile(source)
|
def copy_to_tempfile(source)
|
||||||
|
@ -350,11 +350,11 @@ namespace :mastodon do
|
|||||||
end
|
end
|
||||||
end.join("\n")
|
end.join("\n")
|
||||||
|
|
||||||
generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n"
|
generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
|
||||||
|
|
||||||
if incompatible_syntax
|
if incompatible_syntax
|
||||||
generated_header << "Some variables in this file will be interpreted differently whether you are\n"
|
generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
|
||||||
generated_header << "using docker-compose or not.\n\n"
|
generated_header << "# using docker-compose or not.\n\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
|
File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
|
||||||
|
@ -5,6 +5,37 @@ RSpec.describe Account, type: :model do
|
|||||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||||
subject { Fabricate(:account) }
|
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
|
describe '#follow!' do
|
||||||
it 'creates a follow' do
|
it 'creates a follow' do
|
||||||
follow = subject.follow!(bob)
|
follow = subject.follow!(bob)
|
||||||
|
@ -64,8 +64,9 @@ RSpec.describe NotifyService, type: :service do
|
|||||||
is_expected.to_not change(Notification, :count)
|
is_expected.to_not change(Notification, :count)
|
||||||
end
|
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(: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)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||||
|
|
||||||
it 'does not notify' do
|
it 'does not notify' do
|
||||||
@ -73,8 +74,20 @@ RSpec.describe NotifyService, type: :service do
|
|||||||
end
|
end
|
||||||
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(: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)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||||
|
|
||||||
it 'does notify' do
|
it 'does notify' do
|
||||||
|
Loading…
Reference in New Issue
Block a user