From fcb9350ff8cdc83388f75de6b031410df8aa8a56 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Sep 2020 00:07:19 +0200 Subject: [PATCH 01/81] Change web UI to show empty profile for suspended accounts (#14766) --- .../features/account/components/header.js | 60 ++++++++++--------- .../features/account_gallery/index.js | 29 ++++++--- .../features/account_timeline/index.js | 8 ++- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 61ecf045d18..02217b62c52 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -140,6 +140,8 @@ class Header extends ImmutablePureComponent { return null; } + const suspended = account.get('suspended'); + let info = []; let actionBtn = ''; let lockedIcon = ''; @@ -268,7 +270,7 @@ class Header extends ImmutablePureComponent {
- {info} + {!suspended && info}
@@ -282,11 +284,13 @@ class Header extends ImmutablePureComponent {
-
- {actionBtn} + {!suspended && ( +
+ {actionBtn} - -
+ +
+ )}
@@ -298,7 +302,7 @@ class Header extends ImmutablePureComponent {
- { (fields.size > 0 || identity_proofs.size > 0) && ( + {(fields.size > 0 || identity_proofs.size > 0) && (
{identity_proofs.map((proof, i) => (
@@ -324,33 +328,35 @@ class Header extends ImmutablePureComponent {
)} - {account.get('id') !== me && } + {account.get('id') !== me && !suspended && } {account.get('note').length > 0 && account.get('note') !== '

' &&
}
-
- - - + {!suspended && ( +
+ + + - - - + + + - - - -
+ + + +
+ )}
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index fc5aead4868..e5caec0bc24 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -15,12 +15,15 @@ import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from 'mastodon/components/load_more'; import MissingIndicator from 'mastodon/components/missing_indicator'; import { openModal } from 'mastodon/actions/modal'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = (state, props) => ({ isAccount: !!state.getIn(['accounts', props.params.accountId]), attachments: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), + suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false), + blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), }); class LoadMoreMedia extends ImmutablePureComponent { @@ -56,6 +59,8 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + blockedBy: PropTypes.bool, + suspended: PropTypes.bool, multiColumn: PropTypes.bool, }; @@ -119,7 +124,7 @@ class AccountGallery extends ImmutablePureComponent { } render () { - const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props; + const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props; const { width } = this.state; if (!isAccount) { @@ -152,15 +157,21 @@ class AccountGallery extends ImmutablePureComponent {
-
- {attachments.map((attachment, index) => attachment === null ? ( - 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> - ) : ( - - ))} + {(suspended || blockedBy) ? ( +
+ +
+ ) : ( +
+ {attachments.map((attachment, index) => attachment === null ? ( + 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> + ) : ( + + ))} - {loadOlder} -
+ {loadOlder} +
+ )} {isLoading && attachments.size === 0 && (
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index b9a616266b3..cbc8598051a 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -31,6 +31,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false }) featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), + suspended: state.getIn(['accounts', accountId, 'suspended'], false), blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), }; }; @@ -57,6 +58,7 @@ class AccountTimeline extends ImmutablePureComponent { withReplies: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + suspended: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, multiColumn: PropTypes.bool, @@ -113,7 +115,7 @@ class AccountTimeline extends ImmutablePureComponent { } render () { - const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props; + const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props; if (!isAccount) { return ( @@ -134,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent { let emptyMessage; - if (blockedBy) { + if (suspended || blockedBy) { emptyMessage = ; } else if (remote && statusIds.isEmpty()) { emptyMessage = ; @@ -153,7 +155,7 @@ class AccountTimeline extends ImmutablePureComponent { alwaysPrepend append={remoteMessage} scrollKey='account_timeline' - statusIds={blockedBy ? emptyList : statusIds} + statusIds={(suspended || blockedBy) ? emptyList : statusIds} featuredStatusIds={featuredStatusIds} isLoading={isLoading} hasMore={hasMore} From 91eecd1b3c95807be00535b58ebfd85e549d77e0 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 10 Sep 2020 19:08:03 +0200 Subject: [PATCH 02/81] =?UTF-8?q?Add=20border=20around=20=F0=9F=95=BA=20em?= =?UTF-8?q?oji=20(#14769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #14768 --- .../mastodon/features/emoji/emoji.js | 2 +- lib/tasks/emojis.rake | 2 +- public/emoji/1f57a_border.svg | 31 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 public/emoji/1f57a_border.svg diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 5237b25f059..5d9dad0978b 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => { }; // Emoji requiring extra borders depending on theme -const darkEmoji = emojiFilenames(['🎱', '🐜', 'âšĢ', '🖤', 'âŦ›', 'â—ŧī¸', '◾', 'â—ŧī¸', '✒ī¸', 'â–Ēī¸', 'đŸ’Ŗ', 'đŸŽŗ', '📷', '📸', 'â™Ŗī¸', 'đŸ•ļī¸', '✴ī¸', '🔌', '💂‍♀ī¸', 'đŸ“Ŋī¸', 'đŸŗ', 'đŸĻ', '💂', 'đŸ”Ē', 'đŸ•ŗī¸', '🕹ī¸', '🕋', '🖊ī¸', '🖋ī¸', '💂‍♂ī¸', '🎤', '🎓', 'đŸŽĨ', 'đŸŽŧ', '♠ī¸', '🎩', 'đŸĻƒ', 'đŸ“ŧ', '📹', '🎮', '🐃', '🏴', '🐞']); +const darkEmoji = emojiFilenames(['🎱', '🐜', 'âšĢ', '🖤', 'âŦ›', 'â—ŧī¸', '◾', 'â—ŧī¸', '✒ī¸', 'â–Ēī¸', 'đŸ’Ŗ', 'đŸŽŗ', '📷', '📸', 'â™Ŗī¸', 'đŸ•ļī¸', '✴ī¸', '🔌', '💂‍♀ī¸', 'đŸ“Ŋī¸', 'đŸŗ', 'đŸĻ', '💂', 'đŸ”Ē', 'đŸ•ŗī¸', '🕹ī¸', '🕋', '🖊ī¸', '🖋ī¸', '💂‍♂ī¸', '🎤', '🎓', 'đŸŽĨ', 'đŸŽŧ', '♠ī¸', '🎩', 'đŸĻƒ', 'đŸ“ŧ', '📹', '🎮', '🐃', '🏴', '🐞', 'đŸ•ē']); const lightEmoji = emojiFilenames(['đŸ‘Ŋ', '⚾', '🐔', '☁ī¸', '💨', '🕊ī¸', '👀', 'đŸĨ', 'đŸ‘ģ', '🐐', '❕', '❔', '⛸ī¸', '🌩ī¸', '🔊', '🔇', '📃', '🌧ī¸', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠ī¸', '🌨ī¸', '🔉', '🔈', 'đŸ’Ŧ', '💭', '🏐', 'đŸŗī¸', 'âšĒ', 'âŦœ', 'â—Ŋ', 'â—ģī¸', 'â–Ģī¸']); const emojiFilename = (filename) => { diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake index 2ac8bc05995..d0b8fa890b3 100644 --- a/lib/tasks/emojis.rake +++ b/lib/tasks/emojis.rake @@ -91,7 +91,7 @@ namespace :emojis do desc 'Generate emoji variants with white borders' task :generate_borders do src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json') - emojis = '🎱🐜âšĢ🖤âŦ›â—ŧī¸â—žâ—ŧī¸âœ’ī¸â–Ēī¸đŸ’ŖđŸŽŗ📷📸â™Ŗī¸đŸ•ļī¸âœ´ī¸đŸ”ŒđŸ’‚‍♀ī¸đŸ“Ŋī¸đŸŗđŸĻđŸ’‚đŸ”ĒđŸ•ŗī¸đŸ•šī¸đŸ•‹đŸ–Šī¸đŸ–‹ī¸đŸ’‚‍♂ī¸đŸŽ¤đŸŽ“đŸŽĨđŸŽŧ♠ī¸đŸŽŠđŸĻƒđŸ“ŧ📹🎮🐃🏴🐞đŸ‘Ŋ⚾🐔☁ī¸đŸ’¨đŸ•Šī¸đŸ‘€đŸĨđŸ‘ģ🐐❕❔⛸ī¸đŸŒŠī¸đŸ”ŠđŸ”‡đŸ“ƒđŸŒ§ī¸đŸđŸšđŸ™đŸ“đŸ‘đŸ’€â˜ ī¸đŸŒ¨ī¸đŸ”‰đŸ”ˆđŸ’Ŧ💭🏐đŸŗī¸âšĒâŦœâ—Ŋâ—ģī¸â–Ģī¸' + emojis = '🎱🐜âšĢ🖤âŦ›â—ŧī¸â—žâ—ŧī¸âœ’ī¸â–Ēī¸đŸ’ŖđŸŽŗ📷📸â™Ŗī¸đŸ•ļī¸âœ´ī¸đŸ”ŒđŸ’‚‍♀ī¸đŸ“Ŋī¸đŸŗđŸĻđŸ’‚đŸ”ĒđŸ•ŗī¸đŸ•šī¸đŸ•‹đŸ–Šī¸đŸ–‹ī¸đŸ’‚‍♂ī¸đŸŽ¤đŸŽ“đŸŽĨđŸŽŧ♠ī¸đŸŽŠđŸĻƒđŸ“ŧ📹🎮🐃🏴🐞đŸ•ēđŸ‘Ŋ⚾🐔☁ī¸đŸ’¨đŸ•Šī¸đŸ‘€đŸĨđŸ‘ģ🐐❕❔⛸ī¸đŸŒŠī¸đŸ”ŠđŸ”‡đŸ“ƒđŸŒ§ī¸đŸđŸšđŸ™đŸ“đŸ‘đŸ’€â˜ ī¸đŸŒ¨ī¸đŸ”‰đŸ”ˆđŸ’Ŧ💭🏐đŸŗī¸âšĒâŦœâ—Ŋâ—ģī¸â–Ģī¸' map = Oj.load(File.read(src)) diff --git a/public/emoji/1f57a_border.svg b/public/emoji/1f57a_border.svg new file mode 100644 index 00000000000..7d3729976cf --- /dev/null +++ b/public/emoji/1f57a_border.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e6d67f85e288a5c3137c9fc09f99c9051544d87c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2020 21:06:53 +0900 Subject: [PATCH 03/81] Bump node-fetch from 2.6.0 to 2.6.1 (#14772) Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/bitinn/node-fetch/releases) - [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 18f2120040c..2b4818ed683 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7181,9 +7181,9 @@ nice-try@^1.0.4: integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== node-fetch@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== node-forge@0.9.0: version "0.9.0" From e6b272e5c9c227cfbbe375a893f567c5967d669c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Sep 2020 15:16:29 +0200 Subject: [PATCH 04/81] Change REST API to return empty data for suspended accounts (#14765) --- .../activitypub/outboxes_controller.rb | 3 +- .../v1/accounts/featured_tags_controller.rb | 2 +- .../accounts/follower_accounts_controller.rb | 2 +- .../accounts/following_accounts_controller.rb | 2 +- .../v1/accounts/identity_proofs_controller.rb | 2 +- .../api/v1/accounts/lists_controller.rb | 2 +- .../v1/accounts/relationships_controller.rb | 2 +- .../api/v1/accounts/statuses_controller.rb | 2 +- app/controllers/api/v1/accounts_controller.rb | 5 -- app/controllers/api/v1/blocks_controller.rb | 2 + .../api/v1/endorsements_controller.rb | 2 +- .../api/v1/follow_requests_controller.rb | 2 +- .../api/v1/lists/accounts_controller.rb | 4 +- app/controllers/api/v1/mutes_controller.rb | 2 + .../api/v1/notifications_controller.rb | 4 +- .../favourited_by_accounts_controller.rb | 1 + .../reblogged_by_accounts_controller.rb | 2 +- app/models/notification.rb | 3 + app/policies/status_policy.rb | 2 + app/serializers/rest/account_serializer.rb | 55 ++++++++++++++++--- lib/paperclip/attachment_extensions.rb | 4 ++ lib/paperclip/url_generator_extensions.rb | 4 ++ 22 files changed, 79 insertions(+), 30 deletions(-) diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index e066860bfed..5fd735ad6af 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -57,9 +57,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_statuses return unless page_requested? - @statuses = @account.statuses.permitted_for(@account, signed_request_account) @statuses = cache_collection_paginated_by_id( - @statuses, + @account.statuses.permitted_for(@account, signed_request_account), Status, LIMIT, params_slice(:max_id, :min_id, :since_id) diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb index d6277261d45..014d7195671 100644 --- a/app/controllers/api/v1/accounts/featured_tags_controller.rb +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController end def set_featured_tags - @featured_tags = @account.featured_tags + @featured_tags = @account.suspended? ? @account.featured_tags : [] end end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 2277067c9f4..a665863ebf4 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def hide_results? - (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 93d4bd3a4a1..7d885a212f2 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def hide_results? - (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 8dad6fee962..4b5f6902c7d 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController before_action :set_account def index - @proofs = @account.identity_proofs.active + @proofs = @account.suspended? ? [] : @account.identity_proofs.active render json: @proofs, each_serializer: REST::IdentityProofSerializer end diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb index ccb751f8f7d..c92f1f8a08d 100644 --- a/app/controllers/api/v1/accounts/lists_controller.rb +++ b/app/controllers/api/v1/accounts/lists_controller.rb @@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController before_action :set_account def index - @lists = @account.lists.where(account: current_account) + @lists = @account.suspended? ? [] : @account.lists.where(account: current_account) render json: @lists, each_serializer: REST::ListSerializer end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 1d3992a2857..503f85c97d7 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - accounts = Account.where(id: account_ids).select('id') + accounts = Account.without_suspended.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. @accounts = accounts.index_by(&:id).values_at(*account_ids).compact diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 85a9133e3ac..92ccb80615d 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - cached_account_statuses + @account.suspended? ? [] : cached_account_statuses end def cached_account_statuses diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0080faf3307..61dcb87c23d 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController before_action :require_user!, except: [:show, :create] before_action :set_account, except: [:create] - before_action :check_account_suspension, only: [:show] before_action :check_enabled_registrations, only: [:create] skip_before_action :require_authenticated_user!, only: :create @@ -73,10 +72,6 @@ class Api::V1::AccountsController < Api::BaseController AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) end - def check_account_suspension - gone if @account.suspended? - end - def account_params params.permit(:username, :email, :password, :agreement, :locale, :reason) end diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index a2baeef900c..586cdfca9d4 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController def paginated_blocks @paginated_blocks ||= Block.eager_load(target_account: :account_stat) + .joins(:target_account) + .merge(Account.without_suspended) .where(account: current_account) .paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index c87dbc4ce83..9e80f468a78 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat) + current_account.endorsed_accounts.includes(:account_stat).without_suspended end def insert_pagination_headers diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 0ee6e531f07..0420b7bef92 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.includes(:follow_requests, :account_stat).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) end def paginated_follow_requests diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 23078263e7a..b66ea9bfe60 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController def load_accounts if unlimited? - @list.accounts.includes(:account_stat).all + @list.accounts.without_suspended.includes(:account_stat).all else - @list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 65439fe9bc1..805d0dee2ab 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -18,6 +18,8 @@ class Api::V1::MutesController < Api::BaseController def paginated_mutes @paginated_mutes ||= Mute.eager_load(:target_account) + .joins(:target_account) + .merge(Account.without_suspended) .where(account: current_account) .paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 9d03cb879d6..522c35ba548 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController end def show - @notification = current_account.notifications.find(params[:id]) + @notification = current_account.notifications.without_suspended.find(params[:id]) render json: @notification, serializer: REST::NotificationSerializer end @@ -40,7 +40,7 @@ class Api::V1::NotificationsController < Api::BaseController end def browserable_account_notifications - current_account.notifications.browserable(exclude_types, from_account) + current_account.notifications.without_suspended.browserable(exclude_types, from_account) end def target_statuses_from_notifications diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 8229786d6cc..2b614a83756 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def default_accounts Account + .without_suspended .includes(:favourites, :account_stat) .references(:favourites) .where(favourites: { status_id: @status.id }) diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 6c9e49d903a..24db30fcc01 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def default_accounts - Account.includes(:statuses, :account_stat).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) end def paginated_statuses diff --git a/app/models/notification.rb b/app/models/notification.rb index ad7528f505c..4d7a392b1b7 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -41,8 +41,11 @@ class Notification < ApplicationRecord validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] } validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values } + scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) } + scope :browserable, ->(exclude_types = [], account_id = nil) { types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types) + if account_id.nil? where(activity_type: types) else diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 3d4e50d3719..bcf9c3395ca 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -12,6 +12,8 @@ class StatusPolicy < ApplicationPolicy end def show? + return false if author.suspended? + if requires_mention? owned? || mention_exists? elsif private? diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 0db1916b074..189a62d0eec 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -8,8 +8,11 @@ class REST::AccountSerializer < ActiveModel::Serializer :followers_count, :following_count, :statuses_count, :last_status_at has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + has_many :emojis, serializer: REST::CustomEmojiSerializer + attribute :suspended, if: :suspended? + class FieldSerializer < ActiveModel::Serializer attributes :name, :value, :verified_at @@ -29,7 +32,7 @@ class REST::AccountSerializer < ActiveModel::Serializer end def note - Formatter.instance.simplified_format(object) + object.suspended? ? '' : Formatter.instance.simplified_format(object) end def url @@ -37,26 +40,60 @@ class REST::AccountSerializer < ActiveModel::Serializer end def avatar - full_asset_url(object.avatar_original_url) + full_asset_url(object.suspended? ? object.avatar.default_url : object.avatar_original_url) end def avatar_static - full_asset_url(object.avatar_static_url) + full_asset_url(object.suspended? ? object.avatar.default_url : object.avatar_static_url) end def header - full_asset_url(object.header_original_url) + full_asset_url(object.suspended? ? object.header.default_url : object.header_original_url) end def header_static - full_asset_url(object.header_static_url) - end - - def moved_and_not_nested? - object.moved? && object.moved_to_account.moved_to_account_id.nil? + full_asset_url(object.suspended? ? object.header.default_url : object.header_static_url) end def last_status_at object.last_status_at&.to_date&.iso8601 end + + def display_name + object.suspended? ? '' : object.display_name + end + + def locked + object.suspended? ? false : object.locked + end + + def bot + object.suspended? ? false : object.bot + end + + def discoverable + object.suspended? ? false : object.discoverable + end + + def moved_to_account + object.suspended? ? nil : object.moved_to_account + end + + def emojis + object.suspended? ? [] : object.emojis + end + + def fields + object.suspended? ? [] : object.fields + end + + def suspended + object.suspended? + end + + delegate :suspended?, to: :object + + def moved_and_not_nested? + object.moved? && object.moved_to_account.moved_to_account_id.nil? + end end diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index 93df0a326f0..752e79e65ed 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -35,6 +35,10 @@ module Paperclip formats.include?(other_extension.delete('.')) && File.basename(other_filename, other_extension) == File.basename(original_filename, File.extname(original_filename)) end + + def default_url(style_name = default_style) + @url_generator.for_as_default(style_name) + end end end diff --git a/lib/paperclip/url_generator_extensions.rb b/lib/paperclip/url_generator_extensions.rb index 1079efdbc4f..e1d6df2c299 100644 --- a/lib/paperclip/url_generator_extensions.rb +++ b/lib/paperclip/url_generator_extensions.rb @@ -11,6 +11,10 @@ module Paperclip Addressable::URI.parse(url).normalize.to_str.gsub(escape_regex) { |m| "%#{m.ord.to_s(16).upcase}" } end end + + def for_as_default(style_name) + attachment_options[:interpolator].interpolate(default_url, @attachment, style_name) + end end end From 4e4b3a0c8e69a724e229f028896ce774ef26df3b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 Sep 2020 20:56:35 +0200 Subject: [PATCH 05/81] Refactor settings controllers (#14767) - Disallow suspended accounts from revoking sessions and apps - Allow suspended accounts to access exports --- .../concerns/export_controller_concern.rb | 5 ---- .../authorized_applications_controller.rb | 5 ++++ .../settings/aliases_controller.rb | 4 +-- .../settings/applications_controller.rb | 3 -- app/controllers/settings/base_controller.rb | 7 +++++ .../settings/deletes_controller.rb | 9 ++---- .../exports/blocked_accounts_controller.rb | 2 +- .../exports/blocked_domains_controller.rb | 2 +- .../exports/following_accounts_controller.rb | 2 +- .../settings/exports/lists_controller.rb | 2 +- .../exports/muted_accounts_controller.rb | 2 +- .../settings/exports_controller.rb | 11 -------- .../settings/featured_tags_controller.rb | 3 -- .../settings/identity_proofs_controller.rb | 3 -- .../settings/imports_controller.rb | 3 -- .../migration/redirects_controller.rb | 11 ++------ .../settings/migrations_controller.rb | 9 +----- .../settings/pictures_controller.rb | 1 - .../settings/preferences_controller.rb | 4 --- .../settings/profiles_controller.rb | 3 -- .../settings/sessions_controller.rb | 6 ++-- .../confirmations_controller.rb | 5 +--- .../otp_authentication_controller.rb | 5 +--- .../recovery_codes_controller.rb | 7 ++--- .../webauthn_credentials_controller.rb | 3 +- ...actor_authentication_methods_controller.rb | 5 +--- .../auth/registrations/_sessions.html.haml | 2 +- app/views/auth/registrations/edit.html.haml | 27 +++++++++--------- .../authorized_applications/index.html.haml | 2 +- config/navigation.rb | 2 +- .../settings/deletes_controller_spec.rb | 28 +++++++++---------- 31 files changed, 65 insertions(+), 118 deletions(-) diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb index bfe990c827d..24cfc7a0124 100644 --- a/app/controllers/concerns/export_controller_concern.rb +++ b/app/controllers/concerns/export_controller_concern.rb @@ -5,7 +5,6 @@ module ExportControllerConcern included do before_action :authenticate_user! - before_action :require_not_suspended! before_action :load_export skip_before_action :require_functional! @@ -30,8 +29,4 @@ module ExportControllerConcern def export_filename "#{controller_name}.csv" end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index fb8389034b9..45151cdd775 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! + before_action :require_not_suspended!, only: :destroy before_action :set_body_classes skip_before_action :require_functional! @@ -25,4 +26,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def store_current_location store_location_for(:user, request.url) end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index b7c9a409d1f..a421b8ede31 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class Settings::AliasesController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! + before_action :require_not_suspended! before_action :set_aliases, except: :destroy before_action :set_alias, only: :destroy diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index ed3f82a8e06..d3ac268d862 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ApplicationsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_application, only: [:show, :update, :destroy, :regenerate] before_action :prepare_scopes, only: [:create, :update] diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 3c404cfff20..8311538a569 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class Settings::BaseController < ApplicationController + layout 'admin' + + before_action :authenticate_user! before_action :set_body_classes before_action :set_cache_headers @@ -13,4 +16,8 @@ class Settings::BaseController < ApplicationController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 15a59c999df..7d4844e60f1 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true class Settings::DeletesController < Settings::BaseController - layout 'admin' - - before_action :check_enabled_deletion - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! + before_action :require_not_suspended! + before_action :check_enabled_deletion + def show @confirmation = Form::DeleteConfirmation.new end diff --git a/app/controllers/settings/exports/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb index 2092104e01d..2190caa3619 100644 --- a/app/controllers/settings/exports/blocked_accounts_controller.rb +++ b/app/controllers/settings/exports/blocked_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class BlockedAccountsController < ApplicationController + class BlockedAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/blocked_domains_controller.rb b/app/controllers/settings/exports/blocked_domains_controller.rb index 6676ce34014..bee4b2431e3 100644 --- a/app/controllers/settings/exports/blocked_domains_controller.rb +++ b/app/controllers/settings/exports/blocked_domains_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class BlockedDomainsController < ApplicationController + class BlockedDomainsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb index 74281ddca26..acefcb15da6 100644 --- a/app/controllers/settings/exports/following_accounts_controller.rb +++ b/app/controllers/settings/exports/following_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class FollowingAccountsController < ApplicationController + class FollowingAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/lists_controller.rb b/app/controllers/settings/exports/lists_controller.rb index cf5a9de44b3..bc65f56a0ef 100644 --- a/app/controllers/settings/exports/lists_controller.rb +++ b/app/controllers/settings/exports/lists_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class ListsController < ApplicationController + class ListsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/muted_accounts_controller.rb b/app/controllers/settings/exports/muted_accounts_controller.rb index e511619ca6a..50b7bf1f791 100644 --- a/app/controllers/settings/exports/muted_accounts_controller.rb +++ b/app/controllers/settings/exports/muted_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class MutedAccountsController < ApplicationController + class MutedAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 0e93d07a9b2..30138d29ed6 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -3,11 +3,6 @@ class Settings::ExportsController < Settings::BaseController include Authorization - layout 'admin' - - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! def show @@ -16,8 +11,6 @@ class Settings::ExportsController < Settings::BaseController end def create - raise Mastodon::NotPermittedError unless user_signed_in? - backup = nil RedisLock.acquire(lock_options) do |lock| @@ -37,8 +30,4 @@ class Settings::ExportsController < Settings::BaseController def lock_options { redis: Redis.current, key: "backup:#{current_user.id}" } end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index e9861da56cf..e805527d07c 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::FeaturedTagsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_featured_tags, only: :index before_action :set_featured_tag, except: [:index, :create] before_action :set_recently_used_tags, only: :index diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index 3a90b7c4df0..bf2899da66f 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::IdentityProofsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :check_required_params, only: :new def index diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 7b8c4ae2350..d4516526eee 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ImportsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_account def show diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index 97193ade02d..6d469f3842c 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true class Settings::Migration::RedirectsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! + before_action :require_not_suspended! + def new @redirect = Form::Redirect.new end @@ -38,8 +35,4 @@ class Settings::Migration::RedirectsController < Settings::BaseController def resource_params params.require(:form_redirect).permit(:acct, :current_password, :current_username) end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 68304bb513f..62603aba819 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true class Settings::MigrationsController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_not_suspended! before_action :set_migrations before_action :set_cooldown - skip_before_action :require_functional! - def show @migration = current_account.migrations.build end @@ -44,8 +41,4 @@ class Settings::MigrationsController < Settings::BaseController def on_cooldown? @cooldown.present? end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index df2a6eed3ec..28df65f8ffa 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -2,7 +2,6 @@ module Settings class PicturesController < BaseController - before_action :authenticate_user! before_action :set_account before_action :set_picture diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index bac9b329d46..be4dc904d4d 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Settings::PreferencesController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - def show; end def update diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 19a7ce157f0..0c15447a6c1 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ProfilesController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_account def show diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index df5ace80368..ee2fc5dc80f 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class Settings::SessionsController < Settings::BaseController - before_action :authenticate_user! - before_action :set_session, only: :destroy - skip_before_action :require_functional! + before_action :require_not_suspended! + before_action :set_session, only: :destroy + def destroy @session.destroy! flash[:notice] = I18n.t('sessions.revoke_success') diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index 9f23011a7d4..1a0afe58b05 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -5,14 +5,11 @@ module Settings class ConfirmationsController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_challenge! before_action :ensure_otp_secret - skip_before_action :require_functional! - def new prepare_two_factor_form end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index 6836f7ef622..cbba842a980 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -5,14 +5,11 @@ module Settings class OtpAuthenticationController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :verify_otp_not_enabled, only: [:show] before_action :require_challenge!, only: [:create] - skip_before_action :require_functional! - def show @confirmation = Form::TwoFactorConfirmation.new end diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index 0c4f5bff762..6ec53224d38 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -5,13 +5,10 @@ module Settings class RecoveryCodesController < BaseController include ChallengableConcern - layout 'admin' - - before_action :authenticate_user! - before_action :require_challenge!, on: :create - skip_before_action :require_functional! + before_action :require_challenge!, on: :create + def create @recovery_codes = current_user.generate_otp_backup_codes! current_user.save! diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index a19c604f3bb..1c557092ba5 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -3,9 +3,8 @@ module Settings module TwoFactorAuthentication class WebauthnCredentialsController < BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_otp_enabled before_action :require_webauthn_enabled, only: [:index, :destroy] diff --git a/app/controllers/settings/two_factor_authentication_methods_controller.rb b/app/controllers/settings/two_factor_authentication_methods_controller.rb index 224d3a45cac..205933ea814 100644 --- a/app/controllers/settings/two_factor_authentication_methods_controller.rb +++ b/app/controllers/settings/two_factor_authentication_methods_controller.rb @@ -4,14 +4,11 @@ module Settings class TwoFactorAuthenticationMethodsController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_challenge!, only: :disable before_action :require_otp_enabled - skip_before_action :require_functional! - def index; end def disable diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 395e36a9fd6..d3a04c00e7e 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -27,5 +27,5 @@ - else %time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) %td - - if current_session.session_id != session.session_id + - if current_session.session_id != session.session_id && !current_account.suspended? = table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 4a46b27a93c..a3445b421aa 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -30,18 +30,19 @@ = render 'sessions' -%hr.spacer/ - -%h3= t('auth.migrate_account') -%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) - -%hr.spacer/ - -%h3= t('migrations.incoming_migrations') -%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) - -- if open_deletion? && !current_account.suspended? +- unless current_account.suspended? %hr.spacer/ - %h3= t('auth.delete_account') - %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) + %h3= t('auth.migrate_account') + %p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) + + %hr.spacer/ + + %h3= t('migrations.incoming_migrations') + %p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) + + - if open_deletion? + %hr.spacer/ + + %h3= t('auth.delete_account') + %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 7b77108a93c..fbb733db49f 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -20,5 +20,5 @@ %th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ') %td= l application.created_at %td - - unless application.superapp? + - unless application.superapp? || current_account.suspended? = table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } diff --git a/config/navigation.rb b/config/navigation.rb index ece41d4bf9e..c113a3c3eed 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -21,7 +21,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases} - s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/security_keys} + s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys} s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url end diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb index 996872efd1a..8d5c4774fd8 100644 --- a/spec/controllers/settings/deletes_controller_spec.rb +++ b/spec/controllers/settings/deletes_controller_spec.rb @@ -77,6 +77,20 @@ describe Settings::DeletesController do expect(response).to redirect_to settings_delete_path end end + + context 'when account deletions are disabled' do + around do |example| + open_deletion = Setting.open_deletion + example.run + Setting.open_deletion = open_deletion + end + + it 'redirects' do + Setting.open_deletion = false + delete :destroy + expect(response).to redirect_to root_path + end + end end context 'when not signed in' do @@ -85,19 +99,5 @@ describe Settings::DeletesController do expect(response).to redirect_to '/auth/sign_in' end end - - context do - around do |example| - open_deletion = Setting.open_deletion - example.run - Setting.open_deletion = open_deletion - end - - it 'redirects' do - Setting.open_deletion = false - delete :destroy - expect(response).to redirect_to root_path - end - end end end From fa0c71f0d92ed5587859710dde3b076ec64b1498 Mon Sep 17 00:00:00 2001 From: tateisu Date: Sun, 13 Sep 2020 00:09:49 +0900 Subject: [PATCH 06/81] allow pagination by min_id and max_id (#14776) * allow pagination by min_id and max_id * also AccountConversation allows min_id,max_id pair * also home,list TL allows min_id,max_id pair --- app/models/account_conversation.rb | 5 +++-- app/models/concerns/paginable.rb | 5 +++-- app/models/feed.rb | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb index 5e2ddd0839f..56fd13543c8 100644 --- a/app/models/account_conversation.rb +++ b/app/models/account_conversation.rb @@ -38,15 +38,16 @@ class AccountConversation < ApplicationRecord class << self def to_a_paginated_by_id(limit, options = {}) if options[:min_id] - paginate_by_min_id(limit, options[:min_id]).reverse + paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse else paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a end end - def paginate_by_min_id(limit, min_id = nil) + def paginate_by_min_id(limit, min_id = nil, max_id = nil) query = order(arel_table[:last_status_id].asc).limit(limit) query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present? + query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present? query end diff --git a/app/models/concerns/paginable.rb b/app/models/concerns/paginable.rb index 760cc3df4d4..62e39f6714a 100644 --- a/app/models/concerns/paginable.rb +++ b/app/models/concerns/paginable.rb @@ -14,15 +14,16 @@ module Paginable # Differs from :paginate_by_max_id in that it gives the results immediately following min_id, # whereas since_id gives the items with largest id, but with since_id as a cutoff. # Results will be in ascending order by id. - scope :paginate_by_min_id, ->(limit, min_id = nil) { + scope :paginate_by_min_id, ->(limit, min_id = nil, max_id = nil) { query = reorder(arel_table[:id]).limit(limit) query = query.where(arel_table[:id].gt(min_id)) if min_id.present? + query = query.where(arel_table[:id].lt(max_id)) if max_id.present? query } def self.to_a_paginated_by_id(limit, options = {}) if options[:min_id].present? - paginate_by_min_id(limit, options[:min_id]).reverse + paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse else paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a end diff --git a/app/models/feed.rb b/app/models/feed.rb index 36e0c1e0a09..f51dcfab1da 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -20,12 +20,12 @@ class Feed protected def from_redis(limit, max_id, since_id, min_id) + max_id = '+inf' if max_id.blank? if min_id.blank? - max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) else - unhydrated = redis.zrangebyscore(key, "(#{min_id}", '+inf', limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) + unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) end Status.where(id: unhydrated).cache_ids From 42c4322ce72f33a12bffdc42c7ffe27a08dcba44 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Sep 2020 12:52:17 +0200 Subject: [PATCH 07/81] Fix reported statuses not being included in warning e-mail (#14778) --- app/models/admin/account_action.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index b30a823699e..9edd152f57b 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -142,7 +142,7 @@ class Admin::AccountAction end def status_ids - @report.status_ids if @report && include_statuses + report.status_ids if report && include_statuses end def reports From cd4ec7cd74c0975c7ff9aa832ed7e1bb10966439 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 14 Sep 2020 13:04:29 +0200 Subject: [PATCH 08/81] Do not serve account actors at all in limited federation mode (#14800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Do not serve account actors at all in limited federation mode When an account is fetched without a signature from an allowed instance, return an error. This isn't really an improvement in security, as the only information that was previously returned was required protocol-level info, and the only personal bit was the existence of the account. The existence of the account can still be checked by issuing a webfinger query, as those are accepted without signatures. However, this change makes it so that unallowed instances won't create account records on their end when they find a reference to an unknown account. The previous behavior of rendering a limited list of fields, instead of not rendering the actor at all, was in order to prevent situations in which two instances in Authorized Fetch mode or Limited Federation mode would fail to reach each other because resolving an account would require a signed queryâ€Ļ from an account which can only be fetched with a signed query itself. However, this should now be fine as fetching accounts is done by signing on behalf of the special instance actor, which does not require any kind of valid signature to be fetched. * Fix tests --- app/controllers/accounts_controller.rb | 11 ++--------- spec/controllers/accounts_controller_spec.rb | 20 ++------------------ 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index d97d88fd928..6d711afd0a3 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,6 +7,7 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication + before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers before_action :set_body_classes @@ -48,7 +49,7 @@ class AccountsController < ApplicationController format.json do expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) - render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to + render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter end end end @@ -153,12 +154,4 @@ class AccountsController < ApplicationController def params_slice(*keys) params.slice(*keys).permit(*keys) end - - def restrict_fields_to - if signed_request_account.present? || public_fetch_mode? - # Return all fields - else - %i(id type preferred_username inbox public_key endpoints) - end - end end diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 93bf2c83f40..b04f4650bc4 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -348,24 +348,8 @@ RSpec.describe AccountsController, type: :controller do context 'in authorized fetch mode' do let(:authorized_fetch_mode) { true } - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns application/activity+json' do - expect(response.content_type).to eq 'application/activity+json' - end - - it_behaves_like 'cachable response' - - it 'returns Vary header with Signature' do - expect(response.headers['Vary']).to include 'Signature' - end - - it 'renders bare minimum account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey) - expect(json).to_not include(:name, :summary) + it 'returns http unauthorized' do + expect(response).to have_http_status(401) end end end From 3df3e633638fe8a468d1e5726639ae15e71033ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 22:02:42 +0900 Subject: [PATCH 09/81] Bump aws-sdk-s3 from 1.79.1 to 1.80.0 (#14789) Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.79.1 to 1.80.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index f549f447a65..52b7471874d 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.7' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.79', require: false +gem 'aws-sdk-s3', '~> 1.80', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 21c85766936..b8edfdca781 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,7 +79,7 @@ GEM cocaine (~> 0.5.3) awrence (1.1.1) aws-eventstream (1.1.0) - aws-partitions (1.365.0) + aws-partitions (1.368.0) aws-sdk-core (3.105.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) @@ -88,7 +88,7 @@ GEM aws-sdk-kms (1.37.0) aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.79.1) + aws-sdk-s3 (1.80.0) aws-sdk-core (~> 3, >= 3.104.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) @@ -680,7 +680,7 @@ DEPENDENCIES active_record_query_trace (~> 1.7) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.79) + aws-sdk-s3 (~> 1.80) better_errors (~> 2.7) binding_of_caller (~> 0.7) blurhash (~> 0.1) From 7d390ef4d0ccb8f8d37564f4b8252e7a9bbb84f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 22:03:11 +0900 Subject: [PATCH 10/81] Bump @babel/preset-env from 7.11.0 to 7.11.5 (#14794) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.11.0 to 7.11.5. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.11.5/packages/babel-preset-env) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a1d393fb7ea..7e50ce6abda 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-transform-react-inline-elements": "^7.10.4", "@babel/plugin-transform-runtime": "^7.11.5", - "@babel/preset-env": "^7.11.0", + "@babel/preset-env": "^7.11.5", "@babel/preset-react": "^7.10.4", "@babel/runtime": "^7.11.2", "@clusterws/cws": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 2b4818ed683..27a870738b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -842,10 +842,10 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-env@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" - integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== +"@babel/preset-env@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" + integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== dependencies: "@babel/compat-data" "^7.11.0" "@babel/helper-compilation-targets" "^7.10.4" @@ -909,7 +909,7 @@ "@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.0" + "@babel/types" "^7.11.5" browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" From e975877d9c17c3c1ad7a1643462f6ef78d5294f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 22:04:00 +0900 Subject: [PATCH 11/81] Bump caniuse-lite from 1.0.30001124 to 1.0.30001129 (#14781) Bumps [caniuse-lite](https://github.com/ben-eb/caniuse-lite) from 1.0.30001124 to 1.0.30001129. - [Release notes](https://github.com/ben-eb/caniuse-lite/releases) - [Changelog](https://github.com/ben-eb/caniuse-lite/blob/master/CHANGELOG.md) - [Commits](https://github.com/ben-eb/caniuse-lite/compare/v1.0.30001124...v1.0.30001129) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a870738b6..4fa611c836f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2677,9 +2677,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001124: - version "1.0.30001124" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz#5d9998190258e11630d674fc50ea8e579ae0ced2" - integrity sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA== + version "1.0.30001129" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001129.tgz#e6514b94c0ef50f98cf7476daa91228ddd2ef7bc" + integrity sha512-9945fTVKS810DZITpsAbuhQG7Lam0tEfVbZlsBaCFZaszepbryrArS05PWmJSBQ6mta+v9iz0pUIAbW1eBILIg== capture-exit@^2.0.0: version "2.0.0" From b67caf9be48294bef290eea69e90d98223fcf3eb Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 14 Sep 2020 15:05:22 +0200 Subject: [PATCH 12/81] Add paragraph about browser add-ons when encountering some errors (#14801) * Add paragraph about browser add-ons when encountering some errors When a crash is caused by a NotFoundError exception, add a paragraph to the error page mentioning browser add-ons. Indeed, crashes with NotFoundError are often caused by browser extensions messing with the DOM in ways React.JS can't recover from (e.g. issues #13325 and #14731). * Reword error messages --- .../mastodon/components/error_boundary.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js index ca3012276bc..ca4a2cfe14b 100644 --- a/app/javascript/mastodon/components/error_boundary.js +++ b/app/javascript/mastodon/components/error_boundary.js @@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent { } render() { - const { hasError, copied } = this.state; + const { hasError, copied, errorMessage } = this.state; if (!hasError) { return this.props.children; } + const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError'); + return (
-

-

+

+ { likelyBrowserAddonIssue ? ( + + ) : ( + + )} +

+

+ { likelyBrowserAddonIssue ? ( + + ) : ( + + )} +

Mastodon v{version} ¡ ¡

From b6985fdb6cd9c318deec306dfde820fc5827ef78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 22:08:13 +0900 Subject: [PATCH 13/81] Bump rails from 5.2.4.3 to 5.2.4.4 (#14792) Bumps [rails](https://github.com/rails/rails) from 5.2.4.3 to 5.2.4.4. - [Release notes](https://github.com/rails/rails/releases) - [Commits](https://github.com/rails/rails/compare/v5.2.4.3...v5.2.4.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 74 ++++++++++++++++++++++++++-------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Gemfile b/Gemfile index 52b7471874d..106bbc3cec1 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.0.0' gem 'pkg-config', '~> 1.4' gem 'puma', '~> 4.3' -gem 'rails', '~> 5.2.4.3' +gem 'rails', '~> 5.2.4.4' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 0.20' gem 'rack', '~> 2.2.3' diff --git a/Gemfile.lock b/Gemfile.lock index b8edfdca781..032a9766511 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,25 +16,25 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (5.2.4.3) - actionpack (= 5.2.4.3) + actioncable (5.2.4.4) + actionpack (= 5.2.4.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.4.3) - actionpack (= 5.2.4.3) - actionview (= 5.2.4.3) - activejob (= 5.2.4.3) + actionmailer (5.2.4.4) + actionpack (= 5.2.4.4) + actionview (= 5.2.4.4) + activejob (= 5.2.4.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.4.3) - actionview (= 5.2.4.3) - activesupport (= 5.2.4.3) + actionpack (5.2.4.4) + actionview (= 5.2.4.4) + activesupport (= 5.2.4.4) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.4.3) - activesupport (= 5.2.4.3) + actionview (5.2.4.4) + activesupport (= 5.2.4.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -45,20 +45,20 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.7) - activejob (5.2.4.3) - activesupport (= 5.2.4.3) + activejob (5.2.4.4) + activesupport (= 5.2.4.4) globalid (>= 0.3.6) - activemodel (5.2.4.3) - activesupport (= 5.2.4.3) - activerecord (5.2.4.3) - activemodel (= 5.2.4.3) - activesupport (= 5.2.4.3) + activemodel (5.2.4.4) + activesupport (= 5.2.4.4) + activerecord (5.2.4.4) + activemodel (= 5.2.4.4) + activesupport (= 5.2.4.4) arel (>= 9.0) - activestorage (5.2.4.3) - actionpack (= 5.2.4.3) - activerecord (= 5.2.4.3) + activestorage (5.2.4.4) + actionpack (= 5.2.4.4) + activerecord (= 5.2.4.4) marcel (~> 0.3.1) - activesupport (5.2.4.3) + activesupport (5.2.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -441,18 +441,18 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.4.3) - actioncable (= 5.2.4.3) - actionmailer (= 5.2.4.3) - actionpack (= 5.2.4.3) - actionview (= 5.2.4.3) - activejob (= 5.2.4.3) - activemodel (= 5.2.4.3) - activerecord (= 5.2.4.3) - activestorage (= 5.2.4.3) - activesupport (= 5.2.4.3) + rails (5.2.4.4) + actioncable (= 5.2.4.4) + actionmailer (= 5.2.4.4) + actionpack (= 5.2.4.4) + actionview (= 5.2.4.4) + activejob (= 5.2.4.4) + activemodel (= 5.2.4.4) + activerecord (= 5.2.4.4) + activestorage (= 5.2.4.4) + activesupport (= 5.2.4.4) bundler (>= 1.3.0) - railties (= 5.2.4.3) + railties (= 5.2.4.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -468,9 +468,9 @@ GEM railties (>= 5.0, < 6) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (5.2.4.3) - actionpack (= 5.2.4.3) - activesupport (= 5.2.4.3) + railties (5.2.4.4) + actionpack (= 5.2.4.4) + activesupport (= 5.2.4.4) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -766,7 +766,7 @@ DEPENDENCIES rack (~> 2.2.3) rack-attack (~> 6.3) rack-cors (~> 1.1) - rails (~> 5.2.4.3) + rails (~> 5.2.4.4) rails-controller-testing (~> 1.0) rails-i18n (~> 5.1) rails-settings-cached (~> 0.6) From bbcbf12215a5ec69362a769c1bae9c630eda0ed4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Sep 2020 09:24:24 +0200 Subject: [PATCH 14/81] Fix unreadable placeholder text color in high contrast theme in web UI (#14803) Fix #14717 --- app/javascript/styles/contrast/diff.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index 5a40e7d79a9..841ed66480c 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -75,3 +75,8 @@ .public-layout .public-account-header__tabs__tabs .counter.active::after { border-bottom: 4px solid $ui-highlight-color; } + +.compose-form .autosuggest-textarea__textarea::placeholder, +.compose-form .spoiler-input__input::placeholder { + color: $inverted-text-color; +} From ed099d8bdc5b3d9e7df7ce5358441887e6bb7e48 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 15 Sep 2020 14:37:58 +0200 Subject: [PATCH 15/81] Change account suspensions to be reversible by default (#14726) --- app/controllers/admin/accounts_controller.rb | 31 +-- app/controllers/api/base_controller.rb | 4 +- .../api/v1/admin/accounts_controller.rb | 9 +- .../settings/deletes_controller.rb | 2 +- app/lib/activitypub/activity/delete.rb | 2 +- app/mailers/notification_mailer.rb | 16 +- app/mailers/user_mailer.rb | 28 +-- app/models/account.rb | 9 +- app/models/account_deletion_request.rb | 20 ++ app/models/admin/account_action.rb | 2 +- app/models/concerns/account_associations.rb | 3 + app/models/form/account_batch.rb | 2 +- app/models/invite.rb | 2 +- app/models/user.rb | 4 +- app/policies/account_policy.rb | 4 + app/services/after_unallow_domain_service.rb | 2 +- app/services/block_domain_service.rb | 2 +- app/services/delete_account_service.rb | 180 +++++++++++++++++ app/services/suspend_account_service.rb | 185 +++--------------- app/services/unsuspend_account_service.rb | 52 +++++ app/views/admin/accounts/show.html.haml | 106 +++++----- app/workers/account_deletion_worker.rb | 13 ++ app/workers/admin/account_deletion_worker.rb | 13 ++ app/workers/admin/suspension_worker.rb | 6 +- app/workers/admin/unsuspension_worker.rb | 13 ++ .../scheduler/user_cleanup_scheduler.rb | 13 ++ config/locales/en.yml | 31 ++- config/locales/simple_form.en.yml | 8 +- config/routes.rb | 4 +- ...193330_create_account_deletion_requests.rb | 8 + db/schema.rb | 10 +- lib/mastodon/accounts_cli.rb | 4 +- lib/mastodon/domains_cli.rb | 2 +- .../auth/registrations_controller_spec.rb | 3 +- .../export_controller_concern_spec.rb | 1 + .../account_deletion_request_fabricator.rb | 3 + spec/models/account_deletion_request_spec.rb | 4 + spec/models/invite_spec.rb | 2 +- ...spec.rb => delete_account_service_spec.rb} | 2 +- 39 files changed, 526 insertions(+), 279 deletions(-) create mode 100644 app/models/account_deletion_request.rb create mode 100644 app/services/delete_account_service.rb create mode 100644 app/services/unsuspend_account_service.rb create mode 100644 app/workers/account_deletion_worker.rb create mode 100644 app/workers/admin/account_deletion_worker.rb create mode 100644 app/workers/admin/unsuspension_worker.rb create mode 100644 db/migrate/20200908193330_create_account_deletion_requests.rb create mode 100644 spec/fabricators/account_deletion_request_fabricator.rb create mode 100644 spec/models/account_deletion_request_spec.rb rename spec/services/{suspend_account_service_spec.rb => delete_account_service_spec.rb} (98%) diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 7b178354299..b9b75727dd9 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,7 +2,7 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] + before_action :set_account, except: [:index] before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] @@ -14,49 +14,58 @@ module Admin def show authorize @account, :show? + @deletion_request = @account.deletion_request @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @moderation_notes = @account.targeted_moderation_notes.latest @warnings = @account.targeted_account_warnings.latest.custom + @domain_block = DomainBlock.rule_for(@account.domain) end def memorialize authorize @account, :memorialize? @account.memorialize! log_action :memorialize, @account - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct) end def enable authorize @account.user, :enable? @account.user.enable! log_action :enable, @account.user - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct) end def approve authorize @account.user, :approve? @account.user.approve! - redirect_to admin_pending_accounts_path + redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) end def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) - redirect_to admin_pending_accounts_path + DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) + redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) + end + + def destroy + authorize @account, :destroy? + Admin::AccountDeletionWorker.perform_async(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct) end def unsilence authorize @account, :unsilence? @account.unsilence! log_action :unsilence, @account - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct) end def unsuspend authorize @account, :unsuspend? @account.unsuspend! + Admin::UnsuspensionWorker.perform_async(@account.id) log_action :unsuspend, @account - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct) end def redownload @@ -65,7 +74,7 @@ module Admin @account.update!(last_webfingered_at: nil) ResolveAccountService.new.call(@account) - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct) end def remove_avatar @@ -76,7 +85,7 @@ module Admin log_action :remove_avatar, @account.user - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct) end def remove_header @@ -87,7 +96,7 @@ module Admin log_action :remove_header, @account.user - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct) end private diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 4672255475f..e962c4e97f6 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -96,12 +96,12 @@ class Api::BaseController < ApplicationController def require_user! if !current_user render json: { error: 'This method requires an authenticated user' }, status: 422 - elsif current_user.disabled? - render json: { error: 'Your login is currently disabled' }, status: 403 elsif !current_user.confirmed? render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403 elsif !current_user.approved? render json: { error: 'Your login is currently pending approval' }, status: 403 + elsif !current_user.functional? + render json: { error: 'Your login is currently disabled' }, status: 403 else set_user_activity end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 24c7fbef12f..3af572f25ef 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) + DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def destroy + authorize @account, :destroy? + Admin::AccountDeletionWorker.perform_async(@account.id) render json: @account, serializer: REST::Admin::AccountSerializer end @@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController def unsuspend authorize @account, :unsuspend? @account.unsuspend! + Admin::UnsuspensionWorker.perform_async(@account.id) log_action :unsuspend, @account render json: @account, serializer: REST::Admin::AccountSerializer end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 7d4844e60f1..f96c83b8025 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -43,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController def destroy_account! current_account.suspend! - Admin::SuspensionWorker.perform_async(current_user.account_id, true) + AccountDeletionWorker.perform_async(current_user.account_id) sign_out end end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index dc9ff580c1a..09b9e5e0e83 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -13,7 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_person lock_or_return("delete_in_progress:#{@account.id}") do - SuspendAccountService.new.call(@account, reserve_username: false) + DeleteAccountService.new.call(@account, reserve_username: false) end end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9d8a7886c27..54db892ccc6 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -10,7 +10,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status - return if @me.user.disabled? || @status.nil? + return unless @me.user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -22,7 +22,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account - return if @me.user.disabled? + return unless @me.user.functional? locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) @@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? || @status.nil? + return unless @me.user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -47,7 +47,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? || @status.nil? + return unless @me.user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -59,7 +59,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account - return if @me.user.disabled? + return unless @me.user.functional? locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) @@ -67,7 +67,7 @@ class NotificationMailer < ApplicationMailer end def digest(recipient, **opts) - return if recipient.user.disabled? + return unless recipient.user.functional? @me = recipient @since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max @@ -88,8 +88,10 @@ class NotificationMailer < ApplicationMailer def thread_by_conversation(conversation) return if conversation.nil? + msg_id = "" + headers['In-Reply-To'] = msg_id - headers['References'] = msg_id + headers['References'] = msg_id end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index b557685516c..95996ba3ff9 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -15,7 +15,7 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.unconfirmed_email.presence || @resource.email, @@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject') @@ -40,7 +40,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject') @@ -51,7 +51,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject') @@ -62,7 +62,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject') @@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject') @@ -84,7 +84,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject') @@ -95,7 +95,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject') @@ -106,7 +106,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject') @@ -118,7 +118,7 @@ class UserMailer < Devise::Mailer @instance = Rails.configuration.x.local_domain @webauthn_credential = webauthn_credential - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject') @@ -130,7 +130,7 @@ class UserMailer < Devise::Mailer @instance = Rails.configuration.x.local_domain @webauthn_credential = webauthn_credential - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject') @@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject') @@ -153,7 +153,7 @@ class UserMailer < Devise::Mailer @instance = Rails.configuration.x.local_domain @backup = backup - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject') @@ -181,7 +181,7 @@ class UserMailer < Devise::Mailer @detection = Browser.new(user_agent) @timestamp = timestamp.to_time.utc - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, diff --git a/app/models/account.rb b/app/models/account.rb index 6b7ebda9e67..5acc8d621cb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -222,23 +222,20 @@ class Account < ApplicationRecord def suspend!(date = Time.now.utc) transaction do - user&.disable! if local? + create_deletion_request! update!(suspended_at: date) end end def unsuspend! transaction do - user&.enable! if local? + deletion_request&.destroy! update!(suspended_at: nil) end end def memorialize! - transaction do - user&.disable! if local? - update!(memorial: true) - end + update!(memorial: true) end def sign? diff --git a/app/models/account_deletion_request.rb b/app/models/account_deletion_request.rb new file mode 100644 index 00000000000..7d0c346cc27 --- /dev/null +++ b/app/models/account_deletion_request.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: account_deletion_requests +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# created_at :datetime not null +# updated_at :datetime not null +# +class AccountDeletionRequest < ApplicationRecord + DELAY_TO_DELETION = 30.days.freeze + + belongs_to :account + + def due_at + created_at + DELAY_TO_DELETION + end +end diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index 9edd152f57b..c4ac09520ef 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -134,7 +134,7 @@ class Admin::AccountAction end def process_email! - UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable? + UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable? end def warnable? diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index cca3a17fa41..98849f8fcfa 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -60,5 +60,8 @@ module AccountAssociations # Hashtags has_and_belongs_to_many :tags has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account + + # Account deletion requests + has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy end end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 0b285fde92c..7b9e40f6855 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -69,6 +69,6 @@ class Form::AccountBatch records = accounts.includes(:user) records.each { |account| authorize(account.user, :reject?) } - .each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) } + .each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) } end end diff --git a/app/models/invite.rb b/app/models/invite.rb index 29d25eae801..7ea4e2f9841 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -28,7 +28,7 @@ class Invite < ApplicationRecord before_validation :set_code def valid_for_use? - (max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?) + (max_uses.nil? || uses < max_uses) && !expired? && user&.functional? end private diff --git a/app/models/user.rb b/app/models/user.rb index dbee0898824..6b21d6ed674 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -168,7 +168,7 @@ class User < ApplicationRecord end def active_for_authentication? - true + !account.memorial? end def suspicious_sign_in?(ip) @@ -176,7 +176,7 @@ class User < ApplicationRecord end def functional? - confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil? + confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil? end def unconfirmed_or_pending? diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index 9c145979d6c..1b105e92aa8 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -17,6 +17,10 @@ class AccountPolicy < ApplicationPolicy staff? && !record.user&.staff? end + def destroy? + record.suspended? && record.deletion_request.present? && admin? + end + def unsuspend? staff? end diff --git a/app/services/after_unallow_domain_service.rb b/app/services/after_unallow_domain_service.rb index ccd0b8ae919..d3008a1052f 100644 --- a/app/services/after_unallow_domain_service.rb +++ b/app/services/after_unallow_domain_service.rb @@ -3,7 +3,7 @@ class AfterUnallowDomainService < BaseService def call(domain) Account.where(domain: domain).find_each do |account| - SuspendAccountService.new.call(account, reserve_username: false) + DeleteAccountService.new.call(account, reserve_username: false) end end end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index dc23ef8d8a8..1cf3382b352 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -36,7 +36,7 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at) blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account| - SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at) + DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at) end end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb new file mode 100644 index 00000000000..15bdd13e3d4 --- /dev/null +++ b/app/services/delete_account_service.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +class DeleteAccountService < BaseService + include Payloadable + + ASSOCIATIONS_ON_SUSPEND = %w( + account_pins + active_relationships + block_relationships + blocked_by_relationships + conversation_mutes + conversations + custom_filters + domain_blocks + favourites + follow_requests + list_accounts + mute_relationships + muted_by_relationships + notifications + owned_lists + passive_relationships + report_notes + scheduled_statuses + status_pins + ).freeze + + ASSOCIATIONS_ON_DESTROY = %w( + reports + targeted_moderation_notes + targeted_reports + ).freeze + + # Suspend or remove an account and remove as much of its data + # as possible. If it's a local account and it has not been confirmed + # or never been approved, then side effects are skipped and both + # the user and account records are removed fully. Otherwise, + # it is controlled by options. + # @param [Account] + # @param [Hash] options + # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts + # @option [Boolean] :reserve_username Keep account record + # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads + # @option [Time] :suspended_at Only applicable when :reserve_username is true + def call(account, **options) + @account = account + @options = { reserve_username: true, reserve_email: true }.merge(options) + + if @account.local? && @account.user_unconfirmed_or_pending? + @options[:reserve_email] = false + @options[:reserve_username] = false + @options[:skip_side_effects] = true + end + + reject_follows! + purge_user! + purge_profile! + purge_content! + fulfill_deletion_request! + end + + private + + def reject_follows! + return if @account.local? || !@account.activitypub? + + ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow| + [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url] + end + end + + def purge_user! + return if !@account.local? || @account.user.nil? + + if @options[:reserve_email] + @account.user.disable! + @account.user.invites.where(uses: 0).destroy_all + else + @account.user.destroy + end + end + + def purge_content! + distribute_delete_actor! if @account.local? && !@options[:skip_side_effects] + + @account.statuses.reorder(nil).find_in_batches do |statuses| + statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username] + BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects]) + end + + @account.media_attachments.reorder(nil).find_each do |media_attachment| + next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id) + + media_attachment.destroy + end + + @account.polls.reorder(nil).find_each do |poll| + next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) + + poll.destroy + end + + associations_for_destruction.each do |association_name| + destroy_all(@account.public_send(association_name)) + end + + @account.destroy unless @options[:reserve_username] + end + + def purge_profile! + # If the account is going to be destroyed + # there is no point wasting time updating + # its values first + + return unless @options[:reserve_username] + + @account.silenced_at = nil + @account.suspended_at = @options[:suspended_at] || Time.now.utc + @account.locked = false + @account.memorial = false + @account.discoverable = false + @account.display_name = '' + @account.note = '' + @account.fields = [] + @account.statuses_count = 0 + @account.followers_count = 0 + @account.following_count = 0 + @account.moved_to_account = nil + @account.trust_level = :untrusted + @account.avatar.destroy + @account.header.destroy + @account.save! + end + + def fulfill_deletion_request! + @account.deletion_request&.destroy + end + + def destroy_all(association) + association.in_batches.destroy_all + end + + def distribute_delete_actor! + ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url| + [delete_actor_json, @account.id, inbox_url] + end + + ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url| + [delete_actor_json, @account.id, inbox_url] + end + end + + def delete_actor_json + @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account)) + end + + def build_reject_json(follow) + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + end + + def delivery_inboxes + @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url) + end + + def low_priority_delivery_inboxes + Account.inboxes - delivery_inboxes + end + + def reported_status_ids + @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq + end + + def associations_for_destruction + if @options[:reserve_username] + ASSOCIATIONS_ON_SUSPEND + else + ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY + end + end +end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index ecc893931d5..5a079c3acdd 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -1,175 +1,52 @@ # frozen_string_literal: true class SuspendAccountService < BaseService - include Payloadable - - ASSOCIATIONS_ON_SUSPEND = %w( - account_pins - active_relationships - block_relationships - blocked_by_relationships - conversation_mutes - conversations - custom_filters - domain_blocks - favourites - follow_requests - list_accounts - mute_relationships - muted_by_relationships - notifications - owned_lists - passive_relationships - report_notes - scheduled_statuses - status_pins - ).freeze - - ASSOCIATIONS_ON_DESTROY = %w( - reports - targeted_moderation_notes - targeted_reports - ).freeze - - # Suspend or remove an account and remove as much of its data - # as possible. If it's a local account and it has not been confirmed - # or never been approved, then side effects are skipped and both - # the user and account records are removed fully. Otherwise, - # it is controlled by options. - # @param [Account] - # @param [Hash] options - # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts - # @option [Boolean] :reserve_username Keep account record - # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads - # @option [Time] :suspended_at Only applicable when :reserve_username is true - def call(account, **options) + def call(account) @account = account - @options = { reserve_username: true, reserve_email: true }.merge(options) - if @account.local? && @account.user_unconfirmed_or_pending? - @options[:reserve_email] = false - @options[:reserve_username] = false - @options[:skip_side_effects] = true - end - - reject_follows! - purge_user! - purge_profile! - purge_content! + suspend! + unmerge_from_home_timelines! + unmerge_from_list_timelines! + privatize_media_attachments! end private - def reject_follows! - return if @account.local? || !@account.activitypub? + def suspend! + @account.suspend! unless @account.suspended? + end - ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow| - [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url] + def unmerge_from_home_timelines! + @account.followers_for_local_distribution.find_each do |follower| + FeedManager.instance.unmerge_from_timeline(@account, follower) end end - def purge_user! - return if !@account.local? || @account.user.nil? - - if @options[:reserve_email] - @account.user.disable! - @account.user.invites.where(uses: 0).destroy_all - else - @account.user.destroy + def unmerge_from_list_timelines! + @account.lists_for_local_distribution.find_each do |list| + FeedManager.instance.unmerge_from_list(@account, list) end end - def purge_content! - distribute_delete_actor! if @account.local? && !@options[:skip_side_effects] + def privatize_media_attachments! + attachment_names = MediaAttachment.attachment_definitions.keys - @account.statuses.reorder(nil).find_in_batches do |statuses| - statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username] - BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects]) - end + @account.media_attachments.find_each do |media_attachment| + attachment_names.each do |attachment_name| + attachment = media_attachment.public_send(attachment_name) + styles = [:original] | attachment.styles.keys - @account.media_attachments.reorder(nil).find_each do |media_attachment| - next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id) - - media_attachment.destroy - end - - @account.polls.reorder(nil).find_each do |poll| - next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) - - poll.destroy - end - - associations_for_destruction.each do |association_name| - destroy_all(@account.public_send(association_name)) - end - - @account.destroy unless @options[:reserve_username] - end - - def purge_profile! - # If the account is going to be destroyed - # there is no point wasting time updating - # its values first - - return unless @options[:reserve_username] - - @account.silenced_at = nil - @account.suspended_at = @options[:suspended_at] || Time.now.utc - @account.locked = false - @account.memorial = false - @account.discoverable = false - @account.display_name = '' - @account.note = '' - @account.fields = [] - @account.statuses_count = 0 - @account.followers_count = 0 - @account.following_count = 0 - @account.moved_to_account = nil - @account.trust_level = :untrusted - @account.avatar.destroy - @account.header.destroy - @account.save! - end - - def destroy_all(association) - association.in_batches.destroy_all - end - - def distribute_delete_actor! - ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url| - [delete_actor_json, @account.id, inbox_url] - end - - ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url| - [delete_actor_json, @account.id, inbox_url] - end - end - - def delete_actor_json - @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account)) - end - - def build_reject_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) - end - - def delivery_inboxes - @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url) - end - - def low_priority_delivery_inboxes - Account.inboxes - delivery_inboxes - end - - def reported_status_ids - @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq - end - - def associations_for_destruction - if @options[:reserve_username] - ASSOCIATIONS_ON_SUSPEND - else - ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY + styles.each do |style| + case Paperclip::Attachment.default_options[:storage] + when :s3 + attachment.s3_object(style).acl.put(:private) + when :fog + # Not supported + when :filesystem + FileUtils.chmod(0o600 & ~File.umask, attachment.path(style)) + end + end + end end end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb new file mode 100644 index 00000000000..3e731ddd9f7 --- /dev/null +++ b/app/services/unsuspend_account_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class UnsuspendAccountService < BaseService + def call(account) + @account = account + + unsuspend! + merge_into_home_timelines! + merge_into_list_timelines! + publish_media_attachments! + end + + private + + def unsuspend! + @account.unsuspend! if @account.suspended? + end + + def merge_into_home_timelines! + @account.followers_for_local_distribution.find_each do |follower| + FeedManager.instance.merge_into_timeline(@account, follower) + end + end + + def merge_into_list_timelines! + @account.lists_for_local_distribution.find_each do |list| + FeedManager.instance.merge_into_list(@account, list) + end + end + + def publish_media_attachments! + attachment_names = MediaAttachment.attachment_definitions.keys + + @account.media_attachments.find_each do |media_attachment| + attachment_names.each do |attachment_name| + attachment = media_attachment.public_send(attachment_name) + styles = [:original] | attachment.styles.keys + + styles.each do |style| + case Paperclip::Attachment.default_options[:storage] + when :s3 + attachment.s3_object(style).acl.put(Paperclip::Attachment.default_options[:s3_permissions]) + when :fog + # Not supported + when :filesystem + FileUtils.chmod(0o666 & ~File.umask, attachment.path(style)) + end + end + end + end + end +end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index e6461aad04d..2c48692b7ba 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -56,19 +56,21 @@ = link_to admin_action_logs_path(target_account_id: @account.id) do .dashboard__counters__text - if @account.local? && @account.user.nil? - %span.neutral= t('admin.accounts.deleted') + = t('admin.accounts.deleted') + - elsif @account.memorial? + = t('admin.accounts.memorialized') - elsif @account.suspended? - %span.red= t('admin.accounts.suspended') + = t('admin.accounts.suspended') - elsif @account.silenced? - %span.red= t('admin.accounts.silenced') + = t('admin.accounts.silenced') - elsif @account.local? && @account.user&.disabled? - %span.red= t('admin.accounts.disabled') + = t('admin.accounts.disabled') - elsif @account.local? && !@account.user&.confirmed? - %span.neutral= t('admin.accounts.confirming') + = t('admin.accounts.confirming') - elsif @account.local? && !@account.user_approved? - %span.neutral= t('admin.accounts.pending') + = t('admin.accounts.pending') - else - %span.neutral= t('admin.accounts.no_limits_imposed') + = t('admin.accounts.no_limits_imposed') .dashboard__counters__label= t 'admin.accounts.login_status' - unless @account.local? && @account.user.nil? @@ -122,19 +124,6 @@ = t('admin.accounts.confirming') %td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user) - %tr - %th= t('admin.accounts.login_status') - %td - - if @account.user&.disabled? - = t('admin.accounts.disabled') - - else - = t('admin.accounts.enabled') - %td - - if @account.user&.disabled? - = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user) - - elsif @account.user_approved? - = table_link_to 'lock', t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable') if can?(:disable, @account.user) - %tr %th= t('simple_form.labels.defaults.locale') %td= @account.user_locale @@ -172,49 +161,62 @@ %td = @account.inbox_url = fa_icon DeliveryFailureTracker.available?(@account.inbox_url) ? 'check' : 'times' + %td + = table_link_to 'search', @domain_block.present? ? t('admin.domain_blocks.view') : t('admin.accounts.view_domain'), admin_instance_path(@account.domain) %tr %th= t('admin.accounts.shared_inbox_url') %td = @account.shared_inbox_url = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times' + %td + - if @domain_block.nil? + = table_link_to 'ban', t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain) - %div.action-buttons - %div - - if @account.local? && @account.user_approved? - = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - - if @account.silenced? - = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account) - - elsif !@account.local? || @account.user_approved? - = link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account) + - if @account.suspended? + %hr.spacer/ - - if @account.local? - - if @account.user_pending? - = link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user) - = link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user) + %p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible') - - unless @account.user_confirmed? - = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) + = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account) - - if @account.suspended? - = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account) - - elsif !@account.local? || @account.user_approved? - = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account) + - if @deletion_request.present? + = link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :destroy, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account) + - else + %div.action-buttons + %div + - if @account.local? && @account.user_approved? + = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - - unless @account.local? - - if DomainBlock.rule_for(@account.domain) - = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button' + - if @account.user_disabled? + = link_to t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post, class: 'button' if can?(:enable, @account.user) + - else + = link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user) + + - if @account.silenced? + = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account) + - elsif !@account.local? || @account.user_approved? + = link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button' if can?(:silence, @account) + + - if @account.local? + - if @account.user_pending? + = link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user) + = link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user) + + - unless @account.user_confirmed? + = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) + + - if !@account.local? || @account.user_approved? + = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button' if can?(:suspend, @account) + + %div + - if @account.local? + = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) + - if @account.user&.otp_required_for_login? + = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) + - if !@account.memorial? && @account.user_approved? + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) - else - = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' - - %div - - if @account.local? - = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) - - if @account.user&.otp_required_for_login? - = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) - - if !@account.memorial? && @account.user_approved? - = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) - - else - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) %hr.spacer/ diff --git a/app/workers/account_deletion_worker.rb b/app/workers/account_deletion_worker.rb new file mode 100644 index 00000000000..0f6be71e193 --- /dev/null +++ b/app/workers/account_deletion_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AccountDeletionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: false) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/admin/account_deletion_worker.rb b/app/workers/admin/account_deletion_worker.rb new file mode 100644 index 00000000000..82f269ad6fb --- /dev/null +++ b/app/workers/admin/account_deletion_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Admin::AccountDeletionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: true) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb index 83c815efd7c..35c57033609 100644 --- a/app/workers/admin/suspension_worker.rb +++ b/app/workers/admin/suspension_worker.rb @@ -5,7 +5,9 @@ class Admin::SuspensionWorker sidekiq_options queue: 'pull' - def perform(account_id, remove_user = false) - SuspendAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: !remove_user) + def perform(account_id) + SuspendAccountService.new.call(Account.find(account_id)) + rescue ActiveRecord::RecordNotFound + true end end diff --git a/app/workers/admin/unsuspension_worker.rb b/app/workers/admin/unsuspension_worker.rb new file mode 100644 index 00000000000..7cb2349b16b --- /dev/null +++ b/app/workers/admin/unsuspension_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Admin::UnsuspensionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + UnsuspendAccountService.new.call(Account.find(account_id)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb index 6113edde17b..8571b59e140 100644 --- a/app/workers/scheduler/user_cleanup_scheduler.rb +++ b/app/workers/scheduler/user_cleanup_scheduler.rb @@ -6,9 +6,22 @@ class Scheduler::UserCleanupScheduler sidekiq_options lock: :until_executed, retry: 0 def perform + clean_unconfirmed_accounts! + clean_suspended_accounts! + end + + private + + def clean_unconfirmed_accounts! User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch| Account.where(id: batch.map(&:account_id)).delete_all User.where(id: batch.map(&:id)).delete_all end end + + def clean_suspended_accounts! + AccountDeletionRequest.where('created_at <= ?', AccountDeletionRequest::DELAY_TO_DELETION.ago).reorder(nil).find_each do |deletion_request| + Admin::AccountDeletionWorker.perform_async(deletion_request.account_id) + end + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index ab96074fd56..427b2c3fca0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -98,6 +98,7 @@ en: add_email_domain_block: Block e-mail domain approve: Approve approve_all: Approve all + approved_msg: Successfully approved %{username}'s sign-up application are_you_sure: Are you sure? avatar: Avatar by_domain: Domain @@ -111,18 +112,21 @@ en: confirm: Confirm confirmed: Confirmed confirming: Confirming + delete: Delete data deleted: Deleted demote: Demote - disable: Disable + destroyed_msg: "%{username}'s data is now queued to be deleted imminently" + disable: Freeze disable_two_factor_authentication: Disable 2FA - disabled: Disabled + disabled: Frozen display_name: Display name domain: Domain edit: Edit email: Email email_status: Email status - enable: Enable + enable: Unfreeze enabled: Enabled + enabled_msg: Successfully unfroze %{username}'s account followers: Followers follows: Follows header: Header @@ -138,6 +142,8 @@ en: login_status: Login status media_attachments: Media attachments memorialize: Turn into memoriam + memorialized: Memorialized + memorialized_msg: Successfully turned %{username} into a memorial account moderation: active: Active all: All @@ -158,10 +164,14 @@ en: public: Public push_subscription_expires: PuSH subscription expires redownload: Refresh profile + redownloaded_msg: Successfully refreshed %{username}'s profile from origin reject: Reject reject_all: Reject all + rejected_msg: Successfully rejected %{username}'s sign-up application remove_avatar: Remove avatar remove_header: Remove header + removed_avatar_msg: Successfully removed %{username}'s avatar image + removed_header_msg: Successfully removed %{username}'s header image resend_confirmation: already_confirmed: This user is already confirmed send: Resend confirmation email @@ -182,18 +192,23 @@ en: show: created_reports: Made reports targeted_reports: Reported by others - silence: Silence - silenced: Silenced + silence: Limit + silenced: Limited statuses: Statuses subscribe: Subscribe suspended: Suspended + suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had. + suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below. time_in_queue: Waiting in queue %{time} title: Accounts unconfirmed_email: Unconfirmed email undo_silenced: Undo silence undo_suspension: Undo suspension + unsilenced_msg: Successfully unlimited %{username}'s account unsubscribe: Unsubscribe + unsuspended_msg: Successfully unsuspended %{username}'s account username: Username + view_domain: View summary for domain warn: Warn web: Web whitelisted: Allowed for federation @@ -1304,9 +1319,9 @@ en: title: Sign in attempt warning: explanation: - disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked. - silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you. - suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers. + disable: You can no longer login to your account or use it in any other way, but your profile and other data remains intact. + silence: You can still use your account but only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you. + suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed, but we will retain some data to prevent you from evading the suspension. get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}. review_server_policies: Review server policies statuses: 'Specifically, for:' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4ab0d1871d5..910e77ec2a3 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -90,10 +90,10 @@ en: text: Custom warning type: Action types: - disable: Disable login - none: Do nothing - silence: Silence - suspend: Suspend and irreversibly delete account data + disable: Freeze + none: Send a warning + silence: Limit + suspend: Suspend warning_preset_id: Use a warning preset announcement: all_day: All-day event diff --git a/config/routes.rb b/config/routes.rb index c281a86e348..8d9bc317b52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -232,7 +232,7 @@ Rails.application.routes.draw do resources :report_notes, only: [:create, :destroy] - resources :accounts, only: [:index, :show] do + resources :accounts, only: [:index, :show, :destroy] do member do post :enable post :unsilence @@ -466,7 +466,7 @@ Rails.application.routes.draw do end namespace :admin do - resources :accounts, only: [:index, :show] do + resources :accounts, only: [:index, :show, :destroy] do member do post :enable post :unsilence diff --git a/db/migrate/20200908193330_create_account_deletion_requests.rb b/db/migrate/20200908193330_create_account_deletion_requests.rb new file mode 100644 index 00000000000..e03183ae457 --- /dev/null +++ b/db/migrate/20200908193330_create_account_deletion_requests.rb @@ -0,0 +1,8 @@ +class CreateAccountDeletionRequests < ActiveRecord::Migration[5.2] + def change + create_table :account_deletion_requests do |t| + t.references :account, foreign_key: { on_delete: :cascade } + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e37aae96212..038e391302a 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: 2020_06_30_190544) do +ActiveRecord::Schema.define(version: 2020_09_08_193330) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -36,6 +36,13 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id" end + create_table "account_deletion_requests", force: :cascade do |t| + t.bigint "account_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_account_deletion_requests_on_account_id" + end + create_table "account_domain_blocks", force: :cascade do |t| t.string "domain" t.datetime "created_at", null: false @@ -926,6 +933,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do add_foreign_key "account_aliases", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade + add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 8c91c301358..8f9279a3c05 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -87,7 +87,7 @@ module Mastodon say('Use --force to reattach it anyway and delete the other user') return elsif account.user.present? - account.user.destroy! + DeleteAccountService.new.call(account, reserve_email: false) end end @@ -192,7 +192,7 @@ module Mastodon end say("Deleting user with #{account.statuses_count} statuses, this might take a while...") - SuspendAccountService.new.call(account, reserve_email: false) + DeleteAccountService.new.call(account, reserve_email: false) say('OK', :green) end diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index 558737c273e..5433ddd9d7e 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -42,7 +42,7 @@ module Mastodon end processed, = parallelize_with_progress(scope) do |account| - SuspendAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] + DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] end DomainBlock.where(domain: domains).destroy_all unless options[:dry_run] diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index c2e9f33a82a..bef82276372 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -199,9 +199,10 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end subject do + inviter = Fabricate(:user, confirmed_at: 2.days.ago) Setting.registrations_mode = 'approved' request.headers["Accept-Language"] = accept_language - invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now) + invite = Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now) post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code, agreement: 'true' } } end diff --git a/spec/controllers/concerns/export_controller_concern_spec.rb b/spec/controllers/concerns/export_controller_concern_spec.rb index e5861c801f5..fce129bee22 100644 --- a/spec/controllers/concerns/export_controller_concern_spec.rb +++ b/spec/controllers/concerns/export_controller_concern_spec.rb @@ -5,6 +5,7 @@ require 'rails_helper' describe ApplicationController, type: :controller do controller do include ExportControllerConcern + def index send_export_file end diff --git a/spec/fabricators/account_deletion_request_fabricator.rb b/spec/fabricators/account_deletion_request_fabricator.rb new file mode 100644 index 00000000000..08a82ba3c33 --- /dev/null +++ b/spec/fabricators/account_deletion_request_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:account_deletion_request) do + account +end diff --git a/spec/models/account_deletion_request_spec.rb b/spec/models/account_deletion_request_spec.rb new file mode 100644 index 00000000000..afaecbe2289 --- /dev/null +++ b/spec/models/account_deletion_request_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe AccountDeletionRequest, type: :model do +end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 30abfb86bf4..b0596c56123 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Invite, type: :model do it 'returns false when invite creator has been disabled' do invite = Fabricate(:invite, max_uses: nil, expires_at: nil) - SuspendAccountService.new.call(invite.user.account) + invite.user.account.suspend! expect(invite.valid_for_use?).to be false end end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/delete_account_service_spec.rb similarity index 98% rename from spec/services/suspend_account_service_spec.rb rename to spec/services/delete_account_service_spec.rb index 32726d76397..d208b25b849 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/delete_account_service_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe SuspendAccountService, type: :service do +RSpec.describe DeleteAccountService, type: :service do describe '#call on local account' do before do stub_request(:post, "https://alice.com/inbox").to_return(status: 201) From b677ae9e6f816da7f13749bee097ff799a5aa9e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:13:12 +0900 Subject: [PATCH 16/81] Bump sidekiq from 6.1.1 to 6.1.2 (#14782) Bumps [sidekiq](https://github.com/mperham/sidekiq) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/mperham/sidekiq/releases) - [Changelog](https://github.com/mperham/sidekiq/blob/master/Changes.md) - [Commits](https://github.com/mperham/sidekiq/compare/v6.1.1...v6.1.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 032a9766511..0b3245a8d20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -564,7 +564,7 @@ GEM nokogumbo (~> 2.0) securecompare (1.0.0) semantic_range (2.3.0) - sidekiq (6.1.1) + sidekiq (6.1.2) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) From 7a54779c72a489b7ffbcc4013e723296ce10be1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:13:31 +0900 Subject: [PATCH 17/81] Bump redis from 4.2.1 to 4.2.2 (#14784) Bumps [redis](https://github.com/redis/redis-rb) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/redis/redis-rb/releases) - [Changelog](https://github.com/redis/redis-rb/blob/master/CHANGELOG.md) - [Commits](https://github.com/redis/redis-rb/compare/v4.2.1...v4.2.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0b3245a8d20..fb25a6fcf5e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -481,7 +481,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.4.0) rdf (~> 3.1) - redis (4.2.1) + redis (4.2.2) redis-actionpack (5.2.0) actionpack (>= 5, < 7) redis-rack (>= 2.1.0, < 3) From acdae79f33edf000d3fdf0691db6975687fb8b7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:14:15 +0900 Subject: [PATCH 18/81] Bump pghero from 2.7.0 to 2.7.2 (#14786) Bumps [pghero](https://github.com/ankane/pghero) from 2.7.0 to 2.7.2. - [Release notes](https://github.com/ankane/pghero/releases) - [Changelog](https://github.com/ankane/pghero/blob/master/CHANGELOG.md) - [Commits](https://github.com/ankane/pghero/compare/v2.7.0...v2.7.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fb25a6fcf5e..cf593c8bdcc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -406,7 +406,7 @@ GEM pastel (0.8.0) tty-color (~> 0.5) pg (1.2.3) - pghero (2.7.0) + pghero (2.7.2) activerecord (>= 5) pkg-config (1.4.2) posix-spawn (0.3.15) From cbc45378ea64c9530ceb4f0a108ee88eca82d8da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:14:39 +0900 Subject: [PATCH 19/81] Bump ox from 2.13.3 to 2.13.4 (#14787) Bumps [ox](https://github.com/ohler55/ox) from 2.13.3 to 2.13.4. - [Release notes](https://github.com/ohler55/ox/releases) - [Changelog](https://github.com/ohler55/ox/blob/develop/CHANGELOG.md) - [Commits](https://github.com/ohler55/ox/compare/v2.13.3...v2.13.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cf593c8bdcc..a436ba6c6db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -387,7 +387,7 @@ GEM openssl (2.2.0) openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.13.3) + ox (2.13.4) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) From f4ed6e36a4e5d183b30050fb52d631a07da74d11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:15:10 +0900 Subject: [PATCH 20/81] Bump node-releases from 1.1.60 to 1.1.61 (#14799) Bumps [node-releases](https://github.com/chicoxyzzy/node-releases) from 1.1.60 to 1.1.61. - [Release notes](https://github.com/chicoxyzzy/node-releases/releases) - [Commits](https://github.com/chicoxyzzy/node-releases/compare/v1.1.60...v1.1.61) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4fa611c836f..246d1421441 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7242,9 +7242,9 @@ node-notifier@^8.0.0: which "^2.0.2" node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== + version "1.1.61" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" + integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" From 8c8cb7f1f8de634b01211a7364e350276b93bcd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:16:57 +0900 Subject: [PATCH 21/81] Bump sprockets-rails from 3.2.1 to 3.2.2 (#14788) Bumps [sprockets-rails](https://github.com/rails/sprockets-rails) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/rails/sprockets-rails/releases) - [Commits](https://github.com/rails/sprockets-rails/compare/v3.2.1...v3.2.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a436ba6c6db..f4fd2fef574 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -593,7 +593,7 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.1) + sprockets-rails (3.2.2) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) From c08daac304340574733dfe2f58469cb48e64a207 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:17:32 +0900 Subject: [PATCH 22/81] Bump electron-to-chromium from 1.3.562 to 1.3.567 (#14798) Bumps [electron-to-chromium](https://github.com/kilian/electron-to-chromium) from 1.3.562 to 1.3.567. - [Release notes](https://github.com/kilian/electron-to-chromium/releases) - [Changelog](https://github.com/Kilian/electron-to-chromium/blob/master/CHANGELOG.md) - [Commits](https://github.com/kilian/electron-to-chromium/compare/v1.3.562...v1.3.567) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 246d1421441..add8bf74ab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3800,9 +3800,9 @@ ejs@^2.3.4, ejs@^2.6.1: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== electron-to-chromium@^1.3.562: - version "1.3.562" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.562.tgz#79c20277ee1c8d0173a22af00e38433b752bc70f" - integrity sha512-WhRe6liQ2q/w1MZc8mD8INkenHivuHdrr4r5EQHNomy3NJux+incP6M6lDMd0paShP3MD0WGe5R1TWmEClf+Bg== + version "1.3.567" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.567.tgz#7a404288952ac990e447a7a86470d460ea953b8f" + integrity sha512-1aKkw0Hha1Bw9JA5K5PT5eFXC/TXbkJvUfNSNEciPUMgSIsRJZM1hF2GUEAGZpAbgvd8En21EA+Lv820KOhvqA== elliptic@^6.5.3: version "6.5.3" From 4351228338a54bfd8b1080148a51f6f7afa22689 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:20:05 +0900 Subject: [PATCH 23/81] Bump detect-passive-events from 1.0.4 to 1.0.5 (#14796) Bumps [detect-passive-events](https://github.com/rafgraph/detect-passive-events) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/rafgraph/detect-passive-events/releases) - [Commits](https://github.com/rafgraph/detect-passive-events/compare/v1.0.4...v1.0.5) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7e50ce6abda..7dc935d63c4 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "cross-env": "^7.0.2", "css-loader": "^4.2.2", "cssnano": "^4.1.10", - "detect-passive-events": "^1.0.2", + "detect-passive-events": "^1.0.5", "dotenv": "^8.2.0", "emoji-mart": "Gargron/emoji-mart#build", "es6-symbol": "^3.1.3", diff --git a/yarn.lock b/yarn.lock index add8bf74ab0..c5b2cab27d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3630,10 +3630,10 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== -detect-passive-events@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a" - integrity sha1-btR35uW863kHlzXc01d4nTf5qRo= +detect-passive-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.5.tgz#ce324db665123bef9e368b8059ff95d95217cc05" + integrity sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA== diff-sequences@^25.2.6: version "25.2.6" From 2cfbde41bc99d4dc24a10916e895a46b25670273 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 12:45:52 +0900 Subject: [PATCH 24/81] Bump webmock from 3.8.3 to 3.9.1 (#14780) Bumps [webmock](https://github.com/bblimke/webmock) from 3.8.3 to 3.9.1. - [Release notes](https://github.com/bblimke/webmock/releases) - [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md) - [Commits](https://github.com/bblimke/webmock/compare/v3.8.3...v3.9.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 106bbc3cec1..6aca1274a1f 100644 --- a/Gemfile +++ b/Gemfile @@ -126,7 +126,7 @@ group :test do gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.19', require: false - gem 'webmock', '~> 3.8' + gem 'webmock', '~> 3.9' gem 'parallel_tests', '~> 3.2' gem 'rspec_junit_formatter', '~> 0.4' end diff --git a/Gemfile.lock b/Gemfile.lock index f4fd2fef574..4f742ba5761 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -653,7 +653,7 @@ GEM safety_net_attestation (~> 0.4.0) securecompare (~> 1.0) tpm-key_attestation (~> 0.9.0) - webmock (3.8.3) + webmock (3.9.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -801,6 +801,6 @@ DEPENDENCIES twitter-text (~> 1.14) tzinfo-data (~> 1.2020) webauthn (~> 3.0.0.alpha1) - webmock (~> 3.8) + webmock (~> 3.9) webpacker (~> 5.2) webpush From a3bafccccd08e8319b6ea6ec1b035d8a92271846 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 12:47:02 +0900 Subject: [PATCH 25/81] Bump css-loader from 4.2.2 to 4.3.0 (#14793) Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 4.2.2 to 4.3.0. - [Release notes](https://github.com/webpack-contrib/css-loader/releases) - [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/css-loader/compare/v4.2.2...v4.3.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7dc935d63c4..44dcad66f89 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "classnames": "^2.2.5", "compression-webpack-plugin": "^5.0.1", "cross-env": "^7.0.2", - "css-loader": "^4.2.2", + "css-loader": "^4.3.0", "cssnano": "^4.1.10", "detect-passive-events": "^1.0.5", "dotenv": "^8.2.0", diff --git a/yarn.lock b/yarn.lock index c5b2cab27d0..5793ed9ad06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3253,10 +3253,10 @@ css-list-helpers@^1.0.1: dependencies: tcomb "^2.5.0" -css-loader@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" - integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg== +css-loader@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e" + integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg== dependencies: camelcase "^6.0.0" cssesc "^3.0.0" @@ -3268,7 +3268,7 @@ css-loader@^4.2.2: postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" postcss-value-parser "^4.1.0" - schema-utils "^2.7.0" + schema-utils "^2.7.1" semver "^7.3.2" css-select-base-adapter@^0.1.1: From 9b74f6aca6aa3d130895c4737d1fa1db4fcffb5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 12:48:12 +0900 Subject: [PATCH 26/81] Bump yargs from 15.4.1 to 16.0.3 (#14797) Bumps [yargs](https://github.com/yargs/yargs) from 15.4.1 to 16.0.3. - [Release notes](https://github.com/yargs/yargs/releases) - [Changelog](https://github.com/yargs/yargs/blob/master/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs/compare/v15.4.1...v16.0.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 45 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 44dcad66f89..f4a12e2d393 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "react-test-renderer": "^16.13.1", "sass-lint": "^1.13.1", "webpack-dev-server": "^3.11.0", - "yargs": "^15.4.1" + "yargs": "^16.0.3" }, "resolutions": { "kind-of": "^6.0.3" diff --git a/yarn.lock b/yarn.lock index 5793ed9ad06..ef36a14896f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2873,6 +2873,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" + integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4872,7 +4881,7 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -11145,6 +11154,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -11206,6 +11224,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571" + integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -11237,6 +11260,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.0.0: + version "20.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.0.0.tgz#c65a1daaa977ad63cebdd52159147b789a4e19a9" + integrity sha512-8eblPHTL7ZWRkyjIZJjnGf+TijiKJSwA24svzLRVvtgoi/RZiKa9fFQTrlx0OKLnyHSdt/enrdadji6WFfESVA== + yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -11253,7 +11281,7 @@ yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^15.3.1, yargs@^15.4.1: +yargs@^15.3.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== @@ -11270,6 +11298,19 @@ yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" + integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== + dependencies: + cliui "^7.0.0" + escalade "^3.0.2" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.1" + yargs-parser "^20.0.0" + zlibjs@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554" From 5d3c8baa9a0aa42c68cf0485c187652a435891ab Mon Sep 17 00:00:00 2001 From: kawaguchi Date: Thu, 17 Sep 2020 03:16:46 +0900 Subject: [PATCH 27/81] Fix validates :sign_count of WebauthnCredential (#14806) --- app/models/webauthn_credential.rb | 2 +- spec/models/webauthn_credentials_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/webauthn_credential.rb b/app/models/webauthn_credential.rb index 4129ce53971..7d423e38d71 100644 --- a/app/models/webauthn_credential.rb +++ b/app/models/webauthn_credential.rb @@ -18,5 +18,5 @@ class WebauthnCredential < ApplicationRecord validates :external_id, uniqueness: true validates :nickname, uniqueness: { scope: :user_id } validates :sign_count, - numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 } + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**63 - 1 } end diff --git a/spec/models/webauthn_credentials_spec.rb b/spec/models/webauthn_credentials_spec.rb index 9289c371e65..a63ae6cd248 100644 --- a/spec/models/webauthn_credentials_spec.rb +++ b/spec/models/webauthn_credentials_spec.rb @@ -69,8 +69,8 @@ RSpec.describe WebauthnCredential, type: :model do expect(webauthn_credential).to model_have_error_on_field(:sign_count) end - it 'is invalid if sign_count is greater 2**32 - 1' do - webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**32) + it 'is invalid if sign_count is greater 2**63 - 1' do + webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**63) webauthn_credential.valid? From aab867b0e8119ecee78dabe8007f3c033e734b6d Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 16 Sep 2020 20:17:16 +0200 Subject: [PATCH 28/81] Fix notification filter bar incorrectly filtering gaps (#14808) --- app/javascript/mastodon/features/notifications/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index d16a0f33a4e..6ff3767803d 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -32,7 +32,7 @@ const getNotifications = createSelector([ // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); } - return notifications.filter(item => item !== null && allowedType === item.get('type')); + return notifications.filter(item => item === null || allowedType === item.get('type')); }); const mapStateToProps = state => ({ From eaea2311aaaf030e4a2f5d03be6131d0716fdaf7 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 16 Sep 2020 20:17:40 +0200 Subject: [PATCH 29/81] Fix home TL marker code mishandling gaps (#14809) --- app/javascript/mastodon/actions/markers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js index 37d1ddccfee..6cb09fe96ce 100644 --- a/app/javascript/mastodon/actions/markers.js +++ b/app/javascript/mastodon/actions/markers.js @@ -57,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { const _buildParams = (state) => { const params = {}; - const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]); + const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null); const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']); if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { From 18c03c18f1cfa15bd919067da89a335255e0b271 Mon Sep 17 00:00:00 2001 From: mayaeh Date: Thu, 17 Sep 2020 03:33:18 +0900 Subject: [PATCH 30/81] Bump selfsigned from 1.10.7 to 1.10.8 and bump node-forge from 0.9.0 to 0.10.0. (#14807) https://github.com/jfromaniello/selfsigned/compare/v1.10.7...v1.10.8 https://github.com/digitalbazaar/forge/compare/0.9.0...0.10.0 --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index ef36a14896f..b1e39d11de5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7194,10 +7194,10 @@ node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-forge@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" - integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-int64@^0.4.0: version "0.4.0" @@ -9448,11 +9448,11 @@ select-hose@^2.0.0: integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= selfsigned@^1.10.7: - version "1.10.7" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" - integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== + version "1.10.8" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== dependencies: - node-forge "0.9.0" + node-forge "^0.10.0" "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" From 5d8660fc3cfb21ec896b70c3058a32806e7ae06c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 20:34:10 +0200 Subject: [PATCH 31/81] Bump axios from 0.19.2 to 0.20.0 (#14791) Bumps [axios](https://github.com/axios/axios) from 0.19.2 to 0.20.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.19.2...v0.20.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 36 ++++++++++-------------------------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index f4a12e2d393..ae05617344f 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "array-includes": "^3.1.1", "arrow-key-navigation": "^1.2.0", "autoprefixer": "^9.8.6", - "axios": "^0.19.2", + "axios": "^0.20.0", "babel-loader": "^8.1.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-preval": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index b1e39d11de5..3faf08dcb65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2083,12 +2083,12 @@ axe-core@^3.5.4: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== -axios@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" axobject-query@^2.1.2: version "2.2.0" @@ -3478,14 +3478,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: +debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -4710,19 +4703,10 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -follow-redirects@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb" - integrity sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA== - dependencies: - debug "^3.0.0" +follow-redirects@^1.0.0, follow-redirects@^1.10.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== font-awesome@^4.7.0: version "4.7.0" From 4de893113b893140d5c48024ca697793d9ab73b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 20:39:05 +0200 Subject: [PATCH 32/81] Bump rubocop-ast from 0.3.0 to 0.4.0 (#14785) Bumps [rubocop-ast](https://github.com/rubocop-hq/rubocop-ast) from 0.3.0 to 0.4.0. - [Release notes](https://github.com/rubocop-hq/rubocop-ast/releases) - [Changelog](https://github.com/rubocop-hq/rubocop-ast/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop-hq/rubocop-ast/compare/v0.3.0...v0.4.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4f742ba5761..60b317ed366 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -544,7 +544,7 @@ GEM rubocop-ast (>= 0.3.0, < 1.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.3.0) + rubocop-ast (0.4.0) parser (>= 2.7.1.4) rubocop-rails (2.8.0) activesupport (>= 4.2.0) From 75e4bd9413143ee208d00814c728fc2bf0c58cf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 20:40:12 +0200 Subject: [PATCH 33/81] Bump thor from 0.20.3 to 1.0.1 (#14783) Bumps [thor](https://github.com/erikhuda/thor) from 0.20.3 to 1.0.1. - [Release notes](https://github.com/erikhuda/thor/releases) - [Changelog](https://github.com/erikhuda/thor/blob/master/CHANGELOG.md) - [Commits](https://github.com/erikhuda/thor/compare/v0.20.3...v1.0.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 6aca1274a1f..9ce6b18e548 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'pkg-config', '~> 1.4' gem 'puma', '~> 4.3' gem 'rails', '~> 5.2.4.4' gem 'sprockets', '~> 3.7.2' -gem 'thor', '~> 0.20' +gem 'thor', '~> 1.0' gem 'rack', '~> 2.2.3' gem 'thwait', '~> 0.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 60b317ed366..8884186fb01 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -612,7 +612,7 @@ GEM unicode-display_width (~> 1.1, >= 1.1.1) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (0.20.3) + thor (1.0.1) thread_safe (0.3.6) thwait (0.2.0) e2mmap @@ -795,7 +795,7 @@ DEPENDENCIES stoplight (~> 2.2.1) streamio-ffmpeg (~> 3.0) strong_migrations (~> 0.7) - thor (~> 0.20) + thor (~> 1.0) thwait (~> 0.2.0) tty-prompt (~> 0.22) twitter-text (~> 1.14) From 974b1b79ce58e6799e5e5bb576e630ca783150de Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 18 Sep 2020 17:26:45 +0200 Subject: [PATCH 34/81] Add option to be notified when a followed user posts (#13546) * Add bell button Fix #4890 * Remove duplicate type from post-deployment migration * Fix legacy class type mappings * Improve query performance with better index * Fix validation * Remove redundant index from notifications --- app/controllers/api/v1/accounts_controller.rb | 5 +- .../api/v1/follow_requests_controller.rb | 2 +- app/javascript/mastodon/actions/accounts.js | 4 +- .../mastodon/actions/notifications.js | 2 +- .../features/account/components/header.js | 12 ++- .../account_timeline/components/header.js | 5 + .../containers/header_container.js | 12 ++- .../notifications/components/filter_bar.js | 8 ++ .../notifications/components/notification.js | 35 +++++++ .../styles/mastodon/components.scss | 4 + app/lib/activitypub/activity.rb | 4 +- app/lib/activitypub/activity/follow.rb | 4 +- app/lib/activitypub/activity/like.rb | 2 +- app/models/concerns/account_interactions.rb | 26 +++-- app/models/follow.rb | 3 +- app/models/follow_request.rb | 3 +- app/models/notification.rb | 44 +++++---- .../rest/notification_serializer.rb | 2 +- .../rest/relationship_serializer.rb | 12 ++- app/services/favourite_service.rb | 2 +- app/services/follow_service.rb | 15 +-- app/services/import_service.rb | 6 +- app/services/notify_service.rb | 8 +- app/services/process_mentions_service.rb | 2 +- app/services/reblog_service.rb | 2 +- app/workers/feed_insert_worker.rb | 15 ++- app/workers/local_notification_worker.rb | 4 +- app/workers/poll_expiration_notify_worker.rb | 4 +- app/workers/refollow_worker.rb | 3 +- app/workers/unfollow_follow_worker.rb | 5 +- .../20200917192924_add_notify_to_follows.rb | 19 ++++ ...0200917193034_add_type_to_notifications.rb | 5 + ...7222316_add_index_notifications_on_type.rb | 7 ++ ...200917193528_migrate_notifications_type.rb | 22 +++++ ...index_notifications_on_account_activity.rb | 15 +++ db/schema.rb | 8 +- .../api/v1/accounts_controller_spec.rb | 96 ++++++++++++------- .../concerns/account_interactions_spec.rb | 2 +- spec/models/follow_request_spec.rb | 2 +- spec/services/import_service_spec.rb | 1 + spec/services/notify_service_spec.rb | 6 +- spec/workers/refollow_worker_spec.rb | 4 +- 42 files changed, 330 insertions(+), 112 deletions(-) create mode 100644 db/migrate/20200917192924_add_notify_to_follows.rb create mode 100644 db/migrate/20200917193034_add_type_to_notifications.rb create mode 100644 db/migrate/20200917222316_add_index_notifications_on_type.rb create mode 100644 db/post_migrate/20200917193528_migrate_notifications_type.rb create mode 100644 db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 61dcb87c23d..aef51a6479a 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -30,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController end def follow - FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true) - - options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } + follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true) + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 0420b7bef92..b34c76f29e9 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController def authorize AuthorizeFollowService.new.call(account, current_account) - NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account)) + NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account)) render json: account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index d28f7dad88b..723c04e554d 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -109,14 +109,14 @@ export function fetchAccountFail(id, error) { }; }; -export function followAccount(id, reblogs = true) { +export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); dispatch(followAccountRequest(id, locked)); - api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { dispatch(followAccountFail(error, locked)); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index a26844f8482..099e42f6c54 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -59,7 +59,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (notification.type === 'mention') { + if (['mention', 'status'].includes(notification.type)) { const dropRegex = filters[0]; const regex = filters[1]; const searchIndex = searchTextFromRawStatus(notification.status); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 02217b62c52..2b97af4e675 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { autoPlayGif, me, isStaff } from 'mastodon/initial_state'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import IconButton from 'mastodon/components/icon_button'; import Avatar from 'mastodon/components/avatar'; import { counterRenderer } from 'mastodon/components/common_counter'; import ShortNumber from 'mastodon/components/short_number'; @@ -35,6 +36,8 @@ const messages = defineMessages({ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, + enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' }, + disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, @@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent { onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, onReblogToggle: PropTypes.func.isRequired, + onNotifyToggle: PropTypes.func.isRequired, + onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, @@ -144,6 +148,7 @@ class Header extends ImmutablePureComponent { let info = []; let actionBtn = ''; + let bellBtn = ''; let lockedIcon = ''; let menu = []; @@ -173,6 +178,10 @@ class Header extends ImmutablePureComponent { actionBtn = + + ); + } + return ( diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 553cb33659d..5a0cf3b9060 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -16,7 +16,7 @@ import { expandNotifications } from '../../actions/notifications'; import { fetchFilters } from '../../actions/filters'; import { clearHeight } from '../../actions/height_cache'; import { focusApp, unfocusApp } from 'mastodon/actions/app'; -import { synchronouslySubmitMarkers } from 'mastodon/actions/markers'; +import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -265,6 +265,7 @@ class UI extends React.PureComponent { handleWindowFocus = () => { this.props.dispatch(focusApp()); + this.props.dispatch(submitMarkers()); } handleWindowBlur = () => { @@ -368,6 +369,7 @@ class UI extends React.PureComponent { window.setTimeout(() => Notification.requestPermission(), 120 * 1000); } + this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index ed1ba027267..b01db806fa5 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -9,6 +9,7 @@ import { NOTIFICATIONS_LOAD_PENDING, NOTIFICATIONS_MOUNT, NOTIFICATIONS_UNMOUNT, + NOTIFICATIONS_MARK_AS_READ, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -16,6 +17,13 @@ import { FOLLOW_REQUEST_AUTHORIZE_SUCCESS, FOLLOW_REQUEST_REJECT_SUCCESS, } from '../actions/accounts'; +import { + MARKERS_FETCH_SUCCESS, +} from '../actions/markers'; +import { + APP_FOCUS, + APP_UNFOCUS, +} from '../actions/app'; import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -26,8 +34,11 @@ const initialState = ImmutableMap({ items: ImmutableList(), hasMore: true, top: false, - mounted: false, + mounted: 0, unread: 0, + lastReadId: '0', + readMarkerId: '0', + isTabVisible: true, isLoading: false, }); @@ -46,8 +57,10 @@ const normalizeNotification = (state, notification, usePendingItems) => { return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1); } - if (!top) { + if (shouldCountUnreadNotifications(state)) { state = state.update('unread', unread => unread + 1); + } else { + state = state.set('lastReadId', notification.id); } return state.update('items', list => { @@ -60,6 +73,7 @@ const normalizeNotification = (state, notification, usePendingItems) => { }; const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { + const lastReadId = state.get('lastReadId'); let items = ImmutableList(); notifications.forEach((n, i) => { @@ -87,6 +101,15 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece mutable.set('hasMore', false); } + if (shouldCountUnreadNotifications(state)) { + mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0)); + } else { + const mostRecent = items.find(item => item !== null); + if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) { + mutable.set('lastReadId', mostRecent.get('id')); + } + } + mutable.set('isLoading', false); }); }; @@ -96,21 +119,92 @@ const filterNotifications = (state, accountIds, type) => { return state.update('items', helper).update('pendingItems', helper); }; +const clearUnread = (state) => { + state = state.set('unread', state.get('pendingItems').size); + const lastNotification = state.get('items').find(item => item !== null); + return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0'); +}; + const updateTop = (state, top) => { - if (top) { - state = state.set('unread', state.get('pendingItems').size); + state = state.set('top', top); + + if (!shouldCountUnreadNotifications(state)) { + state = clearUnread(state); } - return state.set('top', top); + return state; }; const deleteByStatus = (state, statusId) => { + const lastReadId = state.get('lastReadId'); + + if (shouldCountUnreadNotifications(state)) { + const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); + } + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); return state.update('items', helper).update('pendingItems', helper); }; +const updateMounted = (state) => { + state = state.update('mounted', count => count + 1); + if (!shouldCountUnreadNotifications(state)) { + state = state.set('readMarkerId', state.get('lastReadId')); + state = clearUnread(state); + } + return state; +}; + +const updateVisibility = (state, visibility) => { + state = state.set('isTabVisible', visibility); + if (!shouldCountUnreadNotifications(state)) { + state = state.set('readMarkerId', state.get('lastReadId')); + state = clearUnread(state); + } + return state; +}; + +const shouldCountUnreadNotifications = (state) => { + const isTabVisible = state.get('isTabVisible'); + const isOnTop = state.get('top'); + const isMounted = state.get('mounted') > 0; + const lastReadId = state.get('lastReadId'); + const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0); + + return !(isTabVisible && isOnTop && isMounted && lastItemReached); +}; + +const recountUnread = (state, last_read_id) => { + return state.withMutations(mutable => { + if (compareId(last_read_id, mutable.get('lastReadId')) > 0) { + mutable.set('lastReadId', last_read_id); + } + + if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) { + mutable.set('readMarkerId', last_read_id); + } + + if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) { + mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0)); + } + }); +}; + export default function notifications(state = initialState, action) { switch(action.type) { + case MARKERS_FETCH_SUCCESS: + return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state; + case NOTIFICATIONS_MOUNT: + return updateMounted(state); + case NOTIFICATIONS_UNMOUNT: + return state.update('mounted', count => count - 1); + case APP_FOCUS: + return updateVisibility(state, true); + case APP_UNFOCUS: + return updateVisibility(state, false); case NOTIFICATIONS_LOAD_PENDING: return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: @@ -144,10 +238,9 @@ export default function notifications(state = initialState, action) { return action.timeline === 'home' ? state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; - case NOTIFICATIONS_MOUNT: - return state.set('mounted', true); - case NOTIFICATIONS_UNMOUNT: - return state.set('mounted', false); + case NOTIFICATIONS_MARK_AS_READ: + const lastNotification = state.get('items').find(item => item !== null); + return lastNotification ? recountUnread(state, lastNotification.get('id')) : state; default: return state; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 7defa0d162c..5e79b4a1135 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7015,3 +7015,22 @@ noscript { } } } + +.notification, +.status__wrapper { + position: relative; + + &.unread { + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + pointer-events: 0; + width: 100%; + height: 100%; + border-left: 2px solid $highlight-text-color; + pointer-events: none; + } + } +} From d60290044ed3f6f5cb84e1dafd317250d5c97d92 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 26 Sep 2020 20:57:39 +0200 Subject: [PATCH 66/81] Add environment variable to allow requests to some private addresses (#14722) --- app/lib/request.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index 89fbeaf9529..38048dad7b5 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -253,7 +253,15 @@ class Request alias new open def check_private_address(address) - raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) + addr = IPAddr.new(address.to_s) + return if private_address_exceptions.any? { |range| range.include?(addr) } + raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(addr) + end + + def private_address_exceptions + @private_address_exceptions = begin + (ENV['ALLOWED_PRIVATE_ADDRESSES'] || '').split(',').map { |addr| IPAddr.new(addr) } + end end end end From 5bbc9a4f78cce2ba519b061e77b166417b044235 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 27 Sep 2020 22:00:43 +0200 Subject: [PATCH 67/81] Fix downloading remote media files when server returns empty filename (#14867) Fixes #14817 --- lib/paperclip/response_with_limit_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/paperclip/response_with_limit_adapter.rb b/lib/paperclip/response_with_limit_adapter.rb index 8711b13497a..17a2abd25f2 100644 --- a/lib/paperclip/response_with_limit_adapter.rb +++ b/lib/paperclip/response_with_limit_adapter.rb @@ -16,7 +16,7 @@ module Paperclip private def cache_current_values - @original_filename = filename_from_content_disposition || filename_from_path || 'data' + @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 From d88a79b4566869ede24958fbff946e357bbb3cb9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 28 Sep 2020 13:29:43 +0200 Subject: [PATCH 68/81] Add pop-out player for audio/video in web UI (#14870) Fix #11160 --- .../mastodon/actions/picture_in_picture.js | 38 +++++ .../mastodon/components/animated_number.js | 17 ++- .../mastodon/components/icon_button.js | 11 +- .../picture_in_picture_placeholder.js | 69 +++++++++ app/javascript/mastodon/components/status.js | 19 ++- .../mastodon/components/status_action_bar.js | 13 +- .../mastodon/containers/status_container.js | 6 + .../mastodon/features/audio/index.js | 47 +++++- .../picture_in_picture/components/footer.js | 137 ++++++++++++++++++ .../picture_in_picture/components/header.js | 40 +++++ .../features/picture_in_picture/index.js | 85 +++++++++++ .../status/components/detailed_status.js | 8 +- .../mastodon/features/status/index.js | 5 +- .../features/ui/components/media_modal.js | 2 +- .../features/ui/components/video_modal.js | 4 +- app/javascript/mastodon/features/ui/index.js | 2 + .../mastodon/features/video/index.js | 53 +++++-- app/javascript/mastodon/reducers/index.js | 2 + .../mastodon/reducers/picture_in_picture.js | 22 +++ .../styles/mastodon/components.scss | 126 +++++++++++++--- 20 files changed, 648 insertions(+), 58 deletions(-) create mode 100644 app/javascript/mastodon/actions/picture_in_picture.js create mode 100644 app/javascript/mastodon/components/picture_in_picture_placeholder.js create mode 100644 app/javascript/mastodon/features/picture_in_picture/components/footer.js create mode 100644 app/javascript/mastodon/features/picture_in_picture/components/header.js create mode 100644 app/javascript/mastodon/features/picture_in_picture/index.js create mode 100644 app/javascript/mastodon/reducers/picture_in_picture.js diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js new file mode 100644 index 00000000000..4085cb59e00 --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.js @@ -0,0 +1,38 @@ +// @ts-check + +export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY'; +export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; + +/** + * @typedef MediaProps + * @property {string} src + * @property {boolean} muted + * @property {number} volume + * @property {number} currentTime + * @property {string} poster + * @property {string} backgroundColor + * @property {string} foregroundColor + * @property {string} accentColor + */ + +/** + * @param {string} statusId + * @param {string} accountId + * @param {string} playerType + * @param {MediaProps} props + * @return {object} + */ +export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({ + type: PICTURE_IN_PICTURE_DEPLOY, + statusId, + accountId, + playerType, + props, +}); + +/* + * @return {object} + */ +export const removePictureInPicture = () => ({ + type: PICTURE_IN_PICTURE_REMOVE, +}); diff --git a/app/javascript/mastodon/components/animated_number.js b/app/javascript/mastodon/components/animated_number.js index f3127c88ef1..fbe948c5b02 100644 --- a/app/javascript/mastodon/components/animated_number.js +++ b/app/javascript/mastodon/components/animated_number.js @@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; import { reduceMotion } from 'mastodon/initial_state'; +const obfuscatedCount = count => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + export default class AnimatedNumber extends React.PureComponent { static propTypes = { value: PropTypes.number.isRequired, + obfuscate: PropTypes.bool, }; state = { @@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent { } render () { - const { value } = this.props; + const { value, obfuscate } = this.props; const { direction } = this.state; if (reduceMotion) { - return ; + return obfuscate ? obfuscatedCount(value) : ; } const styles = [{ @@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } ))} )} diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index fd715bc3c83..7f83dc1b91e 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import AnimatedNumber from 'mastodon/components/animated_number'; export default class IconButton extends React.PureComponent { @@ -24,6 +25,8 @@ export default class IconButton extends React.PureComponent { animate: PropTypes.bool, overlay: PropTypes.bool, tabIndex: PropTypes.string, + counter: PropTypes.number, + obfuscateCount: PropTypes.bool, }; static defaultProps = { @@ -97,6 +100,8 @@ export default class IconButton extends React.PureComponent { pressed, tabIndex, title, + counter, + obfuscateCount, } = this.props; const { @@ -113,6 +118,10 @@ export default class IconButton extends React.PureComponent { overlayed: overlay, }); + if (typeof counter !== 'undefined') { + style.width = 'auto'; + } + return ( ); } diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.js b/app/javascript/mastodon/components/picture_in_picture_placeholder.js new file mode 100644 index 00000000000..19d15c18b16 --- /dev/null +++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'mastodon/components/icon'; +import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; +import { connect } from 'react-redux'; +import { debounce } from 'lodash'; +import { FormattedMessage } from 'react-intl'; + +export default @connect() +class PictureInPicturePlaceholder extends React.PureComponent { + + static propTypes = { + width: PropTypes.number, + dispatch: PropTypes.func.isRequired, + }; + + state = { + width: this.props.width, + height: this.props.width && (this.props.width / (16/9)), + }; + + handleClick = () => { + const { dispatch } = this.props; + dispatch(removePictureInPicture()); + } + + setRef = c => { + this.node = c; + + if (this.node) { + this._setDimensions(); + } + } + + _setDimensions () { + const width = this.node.offsetWidth; + const height = width / (16/9); + + this.setState({ width, height }); + } + + componentDidMount () { + window.addEventListener('resize', this.handleResize, { passive: true }); + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + handleResize = debounce(() => { + if (this.node) { + this._setDimensions(); + } + }, 250, { + trailing: true, + }); + + render () { + const { height } = this.state; + + return ( +
+ + +
+ ); + } + +} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index a1d6f27a64f..c1e1cd172a1 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -17,6 +17,7 @@ import { HotKeys } from 'react-hotkeys'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import { displayMedia } from '../initial_state'; +import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -95,6 +96,8 @@ class Status extends ImmutablePureComponent { cacheMediaWidth: PropTypes.func, cachedMediaWidth: PropTypes.number, scrollKey: PropTypes.string, + deployPictureInPicture: PropTypes.func, + usingPiP: PropTypes.bool, }; // Avoid checking props that are functions (and whose equality will always @@ -105,6 +108,7 @@ class Status extends ImmutablePureComponent { 'muted', 'hidden', 'unread', + 'usingPiP', ]; state = { @@ -206,6 +210,13 @@ class Status extends ImmutablePureComponent { } } + handleDeployPictureInPicture = (type, mediaProps) => { + const { deployPictureInPicture } = this.props; + const status = this._properStatus(); + + deployPictureInPicture(status, type, mediaProps); + } + handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this._properStatus(), this.context.router.history); @@ -266,7 +277,7 @@ class Status extends ImmutablePureComponent { let media = null; let statusAvatar, prepend, rebloggedByText; - const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props; + const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, usingPiP } = this.props; let { status, account, ...other } = this.props; @@ -337,7 +348,9 @@ class Status extends ImmutablePureComponent { status = status.get('reblog'); } - if (status.get('media_attachments').size > 0) { + if (usingPiP) { + media = ; + } else if (status.get('media_attachments').size > 0) { if (this.props.muted) { media = ( )} @@ -383,6 +397,7 @@ class Status extends ImmutablePureComponent { sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} cacheWidth={this.props.cacheMediaWidth} + deployPictureInPicture={this.handleDeployPictureInPicture} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} /> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index b7babd4adc9..66b5a17ac22 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -43,16 +43,6 @@ const messages = defineMessages({ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); -const obfuscatedCount = count => { - if (count < 0) { - return 0; - } else if (count <= 1) { - return count; - } else { - return '1+'; - } -}; - const mapStateToProps = (state, { status }) => ({ relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), }); @@ -329,9 +319,10 @@ class StatusActionBar extends ImmutablePureComponent { return (
-
{obfuscatedCount(status.get('replies_count'))}
+ + {shareButton}
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index decf7279f9a..7bfd66d3eaa 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -37,6 +37,7 @@ import { initMuteModal } from '../actions/mutes'; import { initBlockModal } from '../actions/blocks'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; +import { deployPictureInPicture } from '../actions/picture_in_picture'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, deleteModal } from '../initial_state'; import { showAlertForError } from '../actions/alerts'; @@ -56,6 +57,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props), + usingPiP: state.get('picture_in_picture').statusId === props.id, }); return mapStateToProps; @@ -207,6 +209,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(unblockDomain(domain)); }, + deployPictureInPicture (status, type, mediaProps) { + dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js index 5b817269458..6954d2a4c1a 100644 --- a/app/javascript/mastodon/features/audio/index.js +++ b/app/javascript/mastodon/features/audio/index.js @@ -37,7 +37,11 @@ class Audio extends React.PureComponent { backgroundColor: PropTypes.string, foregroundColor: PropTypes.string, accentColor: PropTypes.string, + currentTime: PropTypes.number, autoPlay: PropTypes.bool, + volume: PropTypes.number, + muted: PropTypes.bool, + deployPictureInPicture: PropTypes.func, }; state = { @@ -64,6 +68,19 @@ class Audio extends React.PureComponent { } } + _pack() { + return { + src: this.props.src, + volume: this.audio.volume, + muted: this.audio.muted, + currentTime: this.audio.currentTime, + poster: this.props.poster, + backgroundColor: this.props.backgroundColor, + foregroundColor: this.props.foregroundColor, + accentColor: this.props.accentColor, + }; + } + _setDimensions () { const width = this.player.offsetWidth; const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9)); @@ -112,6 +129,10 @@ class Audio extends React.PureComponent { componentWillUnmount () { window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.handleResize); + + if (!this.state.paused && this.audio && this.props.deployPictureInPicture) { + this.props.deployPictureInPicture('audio', this._pack()); + } } togglePlay = () => { @@ -248,7 +269,13 @@ class Audio extends React.PureComponent { const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0); if (!this.state.paused && !inView) { - this.setState({ paused: true }, () => this.audio.pause()); + this.audio.pause(); + + if (this.props.deployPictureInPicture) { + this.props.deployPictureInPicture('audio', this._pack()); + } + + this.setState({ paused: true }); } }, 150, { trailing: true }); @@ -261,10 +288,22 @@ class Audio extends React.PureComponent { } handleLoadedData = () => { - const { autoPlay } = this.props; + const { autoPlay, currentTime, volume, muted } = this.props; + + if (currentTime) { + this.audio.currentTime = currentTime; + } + + if (volume !== undefined) { + this.audio.volume = volume; + } + + if (muted !== undefined) { + this.audio.muted = muted; + } if (autoPlay) { - this.audio.play(); + this.togglePlay(); } } @@ -350,7 +389,7 @@ class Audio extends React.PureComponent { render () { const { src, intl, alt, editable, autoPlay } = this.props; const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state; - const progress = (currentTime / duration) * 100; + const progress = Math.min((currentTime / duration) * 100, 100); return (
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js new file mode 100644 index 00000000000..086cda954eb --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -0,0 +1,137 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'mastodon/components/icon_button'; +import classNames from 'classnames'; +import { me, boostModal } from 'mastodon/initial_state'; +import { defineMessages, injectIntl } from 'react-intl'; +import { replyCompose } from 'mastodon/actions/compose'; +import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; +import { makeGetStatus } from 'mastodon/selectors'; +import { openModal } from 'mastodon/actions/modal'; + +const messages = defineMessages({ + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, + reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, + cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, + cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, +}); + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, { statusId }) => ({ + status: getStatus(state, { id: statusId }), + askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, + }); + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) +@injectIntl +class Footer extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + statusId: PropTypes.string.isRequired, + status: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + askReplyConfirmation: PropTypes.bool, + }; + + _performReply = () => { + const { dispatch, status } = this.props; + dispatch(replyCompose(status, this.context.router.history)); + }; + + handleReplyClick = () => { + const { dispatch, askReplyConfirmation, intl } = this.props; + + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + })); + } else { + this._performReply(); + } + }; + + handleFavouriteClick = () => { + const { dispatch, status } = this.props; + + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } + }; + + _performReblog = () => { + const { dispatch, status } = this.props; + dispatch(reblog(status)); + } + + handleReblogClick = e => { + const { dispatch, status } = this.props; + + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else if ((e && e.shiftKey) || !boostModal) { + this._performReblog(); + } else { + dispatch(openModal('BOOST', { status, onReblog: this._performReblog })); + } + }; + + render () { + const { status, intl } = this.props; + + const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; + + let replyIcon, replyTitle; + + if (status.get('in_reply_to_id', null) === null) { + replyIcon = 'reply'; + replyTitle = intl.formatMessage(messages.reply); + } else { + replyIcon = 'reply-all'; + replyTitle = intl.formatMessage(messages.replyAll); + } + + let reblogTitle = ''; + + if (status.get('reblogged')) { + reblogTitle = intl.formatMessage(messages.cancel_reblog_private); + } else if (publicStatus) { + reblogTitle = intl.formatMessage(messages.reblog); + } else if (reblogPrivate) { + reblogTitle = intl.formatMessage(messages.reblog_private); + } else { + reblogTitle = intl.formatMessage(messages.cannot_reblog); + } + + return ( +
+ + + +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.js b/app/javascript/mastodon/features/picture_in_picture/components/header.js new file mode 100644 index 00000000000..4cb6de1a40f --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'mastodon/components/icon_button'; +import { Link } from 'react-router-dom'; +import Avatar from 'mastodon/components/avatar'; +import DisplayName from 'mastodon/components/display_name'; + +const mapStateToProps = (state, { accountId }) => ({ + account: state.getIn(['accounts', accountId]), +}); + +export default @connect(mapStateToProps) +class Header extends ImmutablePureComponent { + + static propTypes = { + accountId: PropTypes.string.isRequired, + statusId: PropTypes.string.isRequired, + account: ImmutablePropTypes.map.isRequired, + onClose: PropTypes.func.isRequired, + }; + + render () { + const { account, statusId, onClose } = this.props; + + return ( +
+ + + + + + +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/picture_in_picture/index.js b/app/javascript/mastodon/features/picture_in_picture/index.js new file mode 100644 index 00000000000..1e59fbcd337 --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Video from 'mastodon/features/video'; +import Audio from 'mastodon/features/audio'; +import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; +import Header from './components/header'; +import Footer from './components/footer'; + +const mapStateToProps = state => ({ + ...state.get('picture_in_picture'), +}); + +export default @connect(mapStateToProps) +class PictureInPicture extends React.Component { + + static propTypes = { + statusId: PropTypes.string, + accountId: PropTypes.string, + type: PropTypes.string, + src: PropTypes.string, + muted: PropTypes.bool, + volume: PropTypes.number, + currentTime: PropTypes.number, + poster: PropTypes.string, + backgroundColor: PropTypes.string, + foregroundColor: PropTypes.string, + accentColor: PropTypes.string, + dispatch: PropTypes.func.isRequired, + }; + + handleClose = () => { + const { dispatch } = this.props; + dispatch(removePictureInPicture()); + } + + render () { + const { type, src, currentTime, accountId, statusId } = this.props; + + if (!currentTime) { + return null; + } + + let player; + + if (type === 'video') { + player = ( +
- {(!onCloseVideo && !editable && !fullscreen) && } + {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && } {(!fullscreen && onOpenVideo) && } {onCloseVideo && } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3823bb05e09..a8fb69c2748 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -36,6 +36,7 @@ import trends from './trends'; import missed_updates from './missed_updates'; import announcements from './announcements'; import markers from './markers'; +import picture_in_picture from './picture_in_picture'; const reducers = { announcements, @@ -75,6 +76,7 @@ const reducers = { trends, missed_updates, markers, + picture_in_picture, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/picture_in_picture.js b/app/javascript/mastodon/reducers/picture_in_picture.js new file mode 100644 index 00000000000..06cd8c5e875 --- /dev/null +++ b/app/javascript/mastodon/reducers/picture_in_picture.js @@ -0,0 +1,22 @@ +import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture'; + +const initialState = { + statusId: null, + accountId: null, + type: null, + src: null, + muted: false, + volume: 0, + currentTime: 0, +}; + +export default function pictureInPicture(state = initialState, action) { + switch(action.type) { + case PICTURE_IN_PICTURE_DEPLOY: + return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props }; + case PICTURE_IN_PICTURE_REMOVE: + return { ...initialState }; + default: + return state; + } +}; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5e79b4a1135..a20265bb9f0 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -163,7 +163,8 @@ } .icon-button { - display: inline-block; + display: inline-flex; + align-items: center; padding: 0; color: $action-button-color; border: 0; @@ -245,6 +246,14 @@ background: rgba($base-overlay-background, 0.9); } } + + &__counter { + display: inline-block; + width: 14px; + margin-left: 4px; + font-size: 12px; + font-weight: 500; + } } .text-icon-button { @@ -1139,24 +1148,6 @@ align-items: center; display: flex; margin-top: 8px; - - &__counter { - display: inline-flex; - margin-right: 11px; - align-items: center; - - .status__action-bar-button { - margin-right: 4px; - } - - &__label { - display: inline-block; - width: 14px; - font-size: 12px; - font-weight: 500; - color: $action-button-color; - } - } } .status__action-bar-button { @@ -7034,3 +7025,100 @@ noscript { } } } + +.picture-in-picture { + position: fixed; + bottom: 20px; + right: 20px; + width: 300px; + + &__footer { + border-radius: 0 0 4px 4px; + background: lighten($ui-base-color, 4%); + padding: 10px; + padding-top: 12px; + display: flex; + justify-content: space-between; + } + + &__header { + border-radius: 4px 4px 0 0; + background: lighten($ui-base-color, 4%); + padding: 10px; + display: flex; + justify-content: space-between; + + &__account { + display: flex; + text-decoration: none; + } + + .account__avatar { + margin-right: 10px; + } + + .display-name { + color: $primary-text-color; + text-decoration: none; + + strong, + span { + display: block; + text-overflow: ellipsis; + overflow: hidden; + } + + span { + color: $darker-text-color; + } + } + } + + .video-player, + .audio-player { + border-radius: 0; + } + + @media screen and (max-width: 415px) { + width: 210px; + bottom: 10px; + right: 10px; + + &__footer { + display: none; + } + + .video-player, + .audio-player { + border-radius: 0 0 4px 4px; + } + } +} + +.picture-in-picture-placeholder { + box-sizing: border-box; + border: 2px dashed lighten($ui-base-color, 8%); + background: $base-shadow-color; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 10px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + color: $darker-text-color; + + i { + display: block; + font-size: 24px; + font-weight: 400; + margin-bottom: 10px; + } + + &:hover, + &:focus, + &:active { + border-color: lighten($ui-base-color, 12%); + } +} From d31792a2a671e22daa51f3360dc440d8bfea4574 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 10 Sep 2020 19:08:03 +0200 Subject: [PATCH 69/81] =?UTF-8?q?[Glitch]=20Add=20border=20around=20?= =?UTF-8?q?=F0=9F=95=BA=20emoji?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port 91eecd1b3c95807be00535b58ebfd85e549d77e0 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/emoji/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index a59e17ddb77..233ec25e3a4 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => { }; // Emoji requiring extra borders depending on theme -const darkEmoji = emojiFilenames(['🎱', '🐜', 'âšĢ', '🖤', 'âŦ›', 'â—ŧī¸', '◾', 'â—ŧī¸', '✒ī¸', 'â–Ēī¸', 'đŸ’Ŗ', 'đŸŽŗ', '📷', '📸', 'â™Ŗī¸', 'đŸ•ļī¸', '✴ī¸', '🔌', '💂‍♀ī¸', 'đŸ“Ŋī¸', 'đŸŗ', 'đŸĻ', '💂', 'đŸ”Ē', 'đŸ•ŗī¸', '🕹ī¸', '🕋', '🖊ī¸', '🖋ī¸', '💂‍♂ī¸', '🎤', '🎓', 'đŸŽĨ', 'đŸŽŧ', '♠ī¸', '🎩', 'đŸĻƒ', 'đŸ“ŧ', '📹', '🎮', '🐃', '🏴', '🐞']); +const darkEmoji = emojiFilenames(['🎱', '🐜', 'âšĢ', '🖤', 'âŦ›', 'â—ŧī¸', '◾', 'â—ŧī¸', '✒ī¸', 'â–Ēī¸', 'đŸ’Ŗ', 'đŸŽŗ', '📷', '📸', 'â™Ŗī¸', 'đŸ•ļī¸', '✴ī¸', '🔌', '💂‍♀ī¸', 'đŸ“Ŋī¸', 'đŸŗ', 'đŸĻ', '💂', 'đŸ”Ē', 'đŸ•ŗī¸', '🕹ī¸', '🕋', '🖊ī¸', '🖋ī¸', '💂‍♂ī¸', '🎤', '🎓', 'đŸŽĨ', 'đŸŽŧ', '♠ī¸', '🎩', 'đŸĻƒ', 'đŸ“ŧ', '📹', '🎮', '🐃', '🏴', '🐞', 'đŸ•ē']); const lightEmoji = emojiFilenames(['đŸ‘Ŋ', '⚾', '🐔', '☁ī¸', '💨', '🕊ī¸', '👀', 'đŸĨ', 'đŸ‘ģ', '🐐', '❕', '❔', '⛸ī¸', '🌩ī¸', '🔊', '🔇', '📃', '🌧ī¸', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠ī¸', '🌨ī¸', '🔉', '🔈', 'đŸ’Ŧ', '💭', '🏐', 'đŸŗī¸', 'âšĒ', 'âŦœ', 'â—Ŋ', 'â—ģī¸', 'â–Ģī¸']); const emojiFilename = (filename) => { From 45862024485273476f5cbbd7162a7fadefd4b3b5 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 16 Sep 2020 20:17:16 +0200 Subject: [PATCH 70/81] [Glitch] Fix notification filter bar incorrectly filtering gaps Port aab867b0e8119ecee78dabe8007f3c033e734b6d to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/notifications/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 68132386004..475968caa6d 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -48,7 +48,7 @@ const getNotifications = createSelector([ // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); } - return notifications.filter(item => item !== null && allowedType === item.get('type')); + return notifications.filter(item => item === null || allowedType === item.get('type')); }); const mapStateToProps = state => ({ From 572d1a1bf8366f65f3b7633b347386604ebcacfc Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 16 Sep 2020 20:17:40 +0200 Subject: [PATCH 71/81] [Glitch] Fix home TL marker code mishandling gaps Port eaea2311aaaf030e4a2f5d03be6131d0716fdaf7 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/actions/markers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 6b49ebf8877..80bcada6eda 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -60,7 +60,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { const _buildParams = (state) => { const params = {}; - const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]); + const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null); const lastNotificationId = state.getIn(['notifications', 'lastReadId']); if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { From 6775de3fc98c46e26a07ed69f6f2459e6b1fce70 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 10 Sep 2020 00:07:19 +0200 Subject: [PATCH 72/81] [Glitch] Change web UI to show empty profile for suspended accounts Port fcb9350ff8cdc83388f75de6b031410df8aa8a56 to glitch-soc Signed-off-by: Thibaut Girka --- .../features/account/components/header.js | 72 ++++++++++--------- .../glitch/features/account_gallery/index.js | 26 ++++--- .../glitch/features/account_timeline/index.js | 12 +++- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 0af0935e6c8..88c578b59ce 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -134,6 +134,8 @@ class Header extends ImmutablePureComponent { const accountNote = account.getIn(['relationship', 'note']); + const suspended = account.get('suspended'); + let info = []; let actionBtn = ''; let lockedIcon = ''; @@ -170,17 +172,21 @@ class Header extends ImmutablePureComponent { actionBtn = ''; } + if (suspended && !account.getIn(['relationship', 'following'])) { + actionBtn = ''; + } + if (account.get('locked')) { lockedIcon = ; } - if (account.get('id') !== me) { + if (account.get('id') !== me && !suspended) { menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); menu.push(null); } - if ('share' in navigator) { + if ('share' in navigator && !suspended) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push(null); } @@ -297,39 +303,41 @@ class Header extends ImmutablePureComponent { -
-
- { (fields.size > 0 || identity_proofs.size > 0) && ( -
- {identity_proofs.map((proof, i) => ( -
-
+ {!suspended && ( +
+
+ { (fields.size > 0 || identity_proofs.size > 0) && ( +
+ {identity_proofs.map((proof, i) => ( +
+
-
- - - - -
-
- ))} - {fields.map((pair, i) => ( -
-
- -
- {pair.get('verified_at') && } -
-
- ))} -
- )} +
+ + + + +
+
+ ))} + {fields.map((pair, i) => ( +
+
+ +
+ {pair.get('verified_at') && } +
+
+ ))} +
+ )} - {account.get('note').length > 0 && account.get('note') !== '

' &&
} + {account.get('note').length > 0 && account.get('note') !== '

' &&
} +
-
-
-
+ )} +
+
); } diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 040741c2a67..fda8082cc45 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -21,6 +21,7 @@ const mapStateToProps = (state, props) => ({ attachments: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), + suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false), }); class LoadMoreMedia extends ImmutablePureComponent { @@ -56,6 +57,7 @@ class AccountGallery extends ImmutablePureComponent { hasMore: PropTypes.bool, isAccount: PropTypes.bool, multiColumn: PropTypes.bool, + suspended: PropTypes.bool, }; state = { @@ -131,7 +133,7 @@ class AccountGallery extends ImmutablePureComponent { } render () { - const { attachments, isLoading, hasMore, isAccount, multiColumn } = this.props; + const { attachments, isLoading, hasMore, isAccount, multiColumn, suspended } = this.props; const { width } = this.state; if (!isAccount) { @@ -164,15 +166,21 @@ class AccountGallery extends ImmutablePureComponent {
-
- {attachments.map((attachment, index) => attachment === null ? ( - 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> - ) : ( - - ))} + {suspended ? ( +
+ +
+ ) : ( +
+ {attachments.map((attachment, index) => attachment === null ? ( + 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> + ) : ( + + ))} - {loadOlder} -
+ {loadOlder} +
+ )} {isLoading && attachments.size === 0 && (
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 5558ba2a308..c56cc9b8e07 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -17,6 +17,8 @@ import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; +const emptyList = ImmutableList(); + const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const path = withReplies ? `${accountId}:with_replies` : accountId; @@ -28,6 +30,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false }) featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), + suspended: state.getIn(['accounts', accountId, 'suspended'], false), }; }; @@ -51,6 +54,7 @@ class AccountTimeline extends ImmutablePureComponent { hasMore: PropTypes.bool, withReplies: PropTypes.bool, isAccount: PropTypes.bool, + suspended: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, multiColumn: PropTypes.bool, @@ -91,7 +95,7 @@ class AccountTimeline extends ImmutablePureComponent { } render () { - const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, multiColumn, remote, remoteUrl } = this.props; + const { statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props; if (!isAccount) { return ( @@ -112,7 +116,9 @@ class AccountTimeline extends ImmutablePureComponent { let emptyMessage; - if (remote && statusIds.isEmpty()) { + if (suspended) { + emptyMessage = ; + } else if (remote && statusIds.isEmpty()) { emptyMessage = ; } else { emptyMessage = ; @@ -129,7 +135,7 @@ class AccountTimeline extends ImmutablePureComponent { alwaysPrepend append={remoteMessage} scrollKey='account_timeline' - statusIds={statusIds} + statusIds={suspended ? emptyList : statusIds} featuredStatusIds={featuredStatusIds} isLoading={isLoading} hasMore={hasMore} From 0a069bffd9d1318f5a1b28f03afe92e96a61fdf3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 8 Nov 2018 21:05:42 +0100 Subject: [PATCH 73/81] [Glitch] Optimize the process of following someone Port front-end changes from 6d59dfa15d873da75c731b79367ab6b3d1b2f5a5 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/accounts.js | 18 ++++++++++++++---- .../flavours/glitch/reducers/relationships.js | 12 ++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index e1012a80bc6..1b4bff48735 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -129,12 +129,14 @@ export function fetchAccountFail(id, error) { export function followAccount(id, reblogs = true) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); - dispatch(followAccountRequest(id)); + const locked = getState().getIn(['accounts', id, 'locked'], false); + + dispatch(followAccountRequest(id, locked)); api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { - dispatch(followAccountFail(error)); + dispatch(followAccountFail(error, locked)); }); }; }; @@ -151,10 +153,12 @@ export function unfollowAccount(id) { }; }; -export function followAccountRequest(id) { +export function followAccountRequest(id, locked) { return { type: ACCOUNT_FOLLOW_REQUEST, id, + locked, + skipLoading: true, }; }; @@ -163,13 +167,16 @@ export function followAccountSuccess(relationship, alreadyFollowing) { type: ACCOUNT_FOLLOW_SUCCESS, relationship, alreadyFollowing, + skipLoading: true, }; }; -export function followAccountFail(error) { +export function followAccountFail(error, locked) { return { type: ACCOUNT_FOLLOW_FAIL, error, + locked, + skipLoading: true, }; }; @@ -177,6 +184,7 @@ export function unfollowAccountRequest(id) { return { type: ACCOUNT_UNFOLLOW_REQUEST, id, + skipLoading: true, }; }; @@ -185,6 +193,7 @@ export function unfollowAccountSuccess(relationship, statuses) { type: ACCOUNT_UNFOLLOW_SUCCESS, relationship, statuses, + skipLoading: true, }; }; @@ -192,6 +201,7 @@ export function unfollowAccountFail(error) { return { type: ACCOUNT_UNFOLLOW_FAIL, error, + skipLoading: true, }; }; diff --git a/app/javascript/flavours/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js index dcaeefcae84..33eb5b425e7 100644 --- a/app/javascript/flavours/glitch/reducers/relationships.js +++ b/app/javascript/flavours/glitch/reducers/relationships.js @@ -1,6 +1,10 @@ import { ACCOUNT_FOLLOW_SUCCESS, + ACCOUNT_FOLLOW_REQUEST, + ACCOUNT_FOLLOW_FAIL, ACCOUNT_UNFOLLOW_SUCCESS, + ACCOUNT_UNFOLLOW_REQUEST, + ACCOUNT_UNFOLLOW_FAIL, ACCOUNT_BLOCK_SUCCESS, ACCOUNT_UNBLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, @@ -40,6 +44,14 @@ const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { + case ACCOUNT_FOLLOW_REQUEST: + return state.setIn([action.id, action.locked ? 'requested' : 'following'], true); + case ACCOUNT_FOLLOW_FAIL: + return state.setIn([action.id, action.locked ? 'requested' : 'following'], false); + case ACCOUNT_UNFOLLOW_REQUEST: + return state.setIn([action.id, 'following'], false); + case ACCOUNT_UNFOLLOW_FAIL: + return state.setIn([action.id, 'following'], true); case ACCOUNT_FOLLOW_SUCCESS: case ACCOUNT_UNFOLLOW_SUCCESS: case ACCOUNT_BLOCK_SUCCESS: From 14869ee656d03313882a912978e5478c628512f3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 18 Sep 2020 17:26:45 +0200 Subject: [PATCH 74/81] [Glitch] Add option to be notified when a followed user posts Port 974b1b79ce58e6799e5e5bb576e630ca783150de to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/accounts.js | 4 +-- .../flavours/glitch/actions/notifications.js | 2 +- .../flavours/glitch/components/status.js | 1 + .../glitch/components/status_prepend.js | 30 ++++++++++++++++++- .../features/account/components/header.js | 12 +++++++- .../account_timeline/components/header.js | 5 ++++ .../containers/header_container.js | 12 ++++++-- .../notifications/components/filter_bar.js | 8 +++++ .../notifications/components/notification.js | 22 ++++++++++++++ .../glitch/styles/components/accounts.scss | 4 +++ 10 files changed, 93 insertions(+), 7 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 1b4bff48735..428b62f68da 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -126,14 +126,14 @@ export function fetchAccountFail(id, error) { }; }; -export function followAccount(id, reblogs = true) { +export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); dispatch(followAccountRequest(id, locked)); - api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { dispatch(followAccountFail(error, locked)); diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index ccc427c2968..7f311153bb7 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -73,7 +73,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (notification.type === 'mention') { + if (['mention', 'status'].includes(notification.type)) { const dropRegex = filters[0]; const regex = filters[1]; const searchIndex = searchTextFromRawStatus(notification.status); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 8da5db96194..fc7940e5a0e 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -680,6 +680,7 @@ class Status extends ImmutablePureComponent { favourite: 'favourited', reblog: 'boosted', reblogged_by: 'boosted', + status: 'posted', }[prepend]; selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`; diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index 637c4f23aa4..0ba55d5d8b5 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -64,6 +64,14 @@ export default class StatusPrepend extends React.PureComponent { values={{ name : link }} /> ); + case 'status': + return ( + + ); case 'poll': if (me === account.get('id')) { return ( @@ -88,12 +96,32 @@ export default class StatusPrepend extends React.PureComponent { const { Message } = this; const { type } = this.props; + let iconId; + + switch(type) { + case 'favourite': + iconId = 'star'; + break; + case 'featured': + iconId = 'thumb-tack'; + break; + case 'poll': + iconId = 'tasks'; + break; + case 'reblogged_by': + iconId = 'retweet'; + break; + case 'status': + iconId = 'bell'; + break; + }; + return !type ? null : (