From 5fb1c3e934a1a782972ac2732ce7f0208c341ac2 Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Thu, 15 Dec 2022 14:47:06 +0000 Subject: Revoke all authorized applications on password reset (#21325) * Clear sessions on password change * Rename User::clear_sessions to revoke_access for a clearer meaning * Add reset paassword controller test * Use User.find instead of User.find_for_authentication for reset password test * Use redirect and render for better test meaning in reset password Co-authored-by: Effy Elden --- app/models/user.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'app/models') diff --git a/app/models/user.rb b/app/models/user.rb index 5530a9070..ca98a0afa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -377,6 +377,15 @@ class User < ApplicationRecord super end + def revoke_access! + Doorkeeper::AccessGrant.by_resource_owner(self).update_all(revoked_at: Time.now.utc) + + Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| + batch.update_all(revoked_at: Time.now.utc) + Web::PushSubscription.where(access_token_id: batch).delete_all + end + end + def reset_password! # First, change password to something random and deactivate all sessions transaction do @@ -385,12 +394,7 @@ class User < ApplicationRecord end # Then, remove all authorized applications and connected push subscriptions - Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc) - - Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| - batch.update_all(revoked_at: Time.now.utc) - Web::PushSubscription.where(access_token_id: batch).delete_all - end + revoke_access! # Finally, send a reset password prompt to the user send_reset_password_instructions -- cgit From d412147d02e84cb76b252706a5357fe5d434c3db Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Fri, 16 Dec 2022 01:11:14 +0900 Subject: Save avatar or header correctly even if other one fails (#18465) * Save avatar or header correctly if other one fails * Fix test --- app/models/account.rb | 12 +++++++++--- spec/models/account_spec.rb | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index fc7359cfc..a7bda15d3 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -341,9 +341,15 @@ class Account < ApplicationRecord def save_with_optional_media! save! - rescue ActiveRecord::RecordInvalid - self.avatar = nil - self.header = nil + rescue ActiveRecord::RecordInvalid => e + errors = e.record.errors.errors + errors.each do |err| + if err.attribute == :avatar + self.avatar = nil + elsif err.attribute == :header + self.header = nil + end + end save! end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index edae05f9d..c9d782cee 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -160,7 +160,7 @@ RSpec.describe Account, type: :model do expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar' expect(account.header_remote_url).to eq expectation.header_remote_url expect(account.avatar_file_name).to eq nil - expect(account.header_file_name).to eq nil + expect(account.header_file_name).to eq expectation.header_file_name end end end -- cgit From 3656a6b9cc353f7f08a2d8f00c1b3f2fd8e3fb21 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Fri, 16 Dec 2022 01:30:47 +0900 Subject: Add "disabled" user filter for admin/accounts UI (#21282) --- app/models/account_filter.rb | 2 +- app/views/admin/accounts/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app/models') diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index 3a4ac0492..d27bb46fc 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -81,7 +81,7 @@ class AccountFilter when 'suspended' Account.suspended when 'disabled' - accounts_with_users.merge(User.disabled) + accounts_with_users.merge(User.disabled).without_suspended when 'silenced' Account.silenced when 'sensitized' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index f33f788ed..d0897221d 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -13,7 +13,7 @@ .filter-subset.filter-subset--with-select %strong= t('admin.accounts.moderation.title') .input.select.optional - = select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all') + = select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.disabled'), 'disabled'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all') .filter-subset.filter-subset--with-select %strong= t('admin.accounts.role') .input.select.optional -- cgit From 2644a28cb30dfb57b9543dd045657e8ed660876a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 18:09:48 +0100 Subject: Change remote media files to be downloaded outside of transactions (#21796) --- app/models/media_attachment.rb | 2 ++ .../activitypub/process_status_update_service.rb | 42 ++++++++++++---------- 2 files changed, 26 insertions(+), 18 deletions(-) (limited to 'app/models') diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 51b256482..5916b0b4b 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -210,6 +210,8 @@ class MediaAttachment < ApplicationRecord default_scope { order(id: :asc) } + attr_accessor :skip_download + def local? remote_url.blank? end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index fad19f87f..11b38ab92 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -45,6 +45,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService create_edits! end + download_media_files! queue_poll_notifications! next unless significant_changes? @@ -66,12 +67,12 @@ class ActivityPub::ProcessStatusUpdateService < BaseService def update_media_attachments! previous_media_attachments = @status.media_attachments.to_a previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id) - next_media_attachments = [] + @next_media_attachments = [] as_array(@json['attachment']).each do |attachment| media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment) - next if media_attachment_parser.remote_url.blank? || next_media_attachments.size > 4 + next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > 4 begin media_attachment = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url } @@ -87,34 +88,39 @@ class ActivityPub::ProcessStatusUpdateService < BaseService media_attachment.focus = media_attachment_parser.focus media_attachment.thumbnail_remote_url = media_attachment_parser.thumbnail_remote_url media_attachment.blurhash = media_attachment_parser.blurhash + media_attachment.status_id = @status.id + media_attachment.skip_download = unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download? media_attachment.save! - next_media_attachments << media_attachment - - next if unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download? - - begin - media_attachment.download_file! if media_attachment.remote_url_previously_changed? - media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed? - media_attachment.save - rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError - RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) - end + @next_media_attachments << media_attachment rescue Addressable::URI::InvalidURIError => e Rails.logger.debug "Invalid URL in attachment: #{e}" end end - added_media_attachments = next_media_attachments - previous_media_attachments + added_media_attachments = @next_media_attachments - previous_media_attachments - MediaAttachment.where(id: added_media_attachments.map(&:id)).update_all(status_id: @status.id) - - @status.ordered_media_attachment_ids = next_media_attachments.map(&:id) - @status.media_attachments.reload + @status.ordered_media_attachment_ids = @next_media_attachments.map(&:id) @media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids end + def download_media_files! + @next_media_attachments.each do |media_attachment| + next if media_attachment.skip_download + + media_attachment.download_file! if media_attachment.remote_url_previously_changed? + media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed? + media_attachment.save + rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError + RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) + rescue Seahorse::Client::NetworkingError => e + Rails.logger.warn "Error storing media attachment: #{e}" + end + + @status.media_attachments.reload + end + def update_poll!(allow_significant_changes: true) previous_poll = @status.preloadable_poll @previous_expires_at = previous_poll&.expires_at -- cgit From 70415714f14e067aba518a105c96475db31fa124 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 18:50:11 +0100 Subject: Add follow request banner on account header (#20785) * Add requested_by to relationship maps * Display whether an account has requested to follow you on their profile --- .../account/components/follow_request_note.js | 37 ++++++++++++++++++++++ .../mastodon/features/account/components/header.js | 3 ++ .../containers/follow_request_note_container.js | 15 +++++++++ app/javascript/mastodon/reducers/relationships.js | 11 +++++++ app/javascript/styles/mastodon/components.scss | 32 ++++++++++++++++++- app/models/concerns/account_interactions.rb | 4 +++ app/presenters/account_relationships_presenter.rb | 6 +++- app/serializers/rest/relationship_serializer.rb | 8 +++-- spec/models/account_spec.rb | 6 ++++ .../account_relationships_presenter_spec.rb | 9 ++++++ 10 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 app/javascript/mastodon/features/account/components/follow_request_note.js create mode 100644 app/javascript/mastodon/features/account/containers/follow_request_note_container.js (limited to 'app/models') diff --git a/app/javascript/mastodon/features/account/components/follow_request_note.js b/app/javascript/mastodon/features/account/components/follow_request_note.js new file mode 100644 index 000000000..300ae4266 --- /dev/null +++ b/app/javascript/mastodon/features/account/components/follow_request_note.js @@ -0,0 +1,37 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; + +export default class FollowRequestNote extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account, onAuthorize, onReject } = this.props; + + return ( +
+
+ }} /> +
+ +
+ + + +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index f117412be..dddbf4dd4 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -14,6 +14,7 @@ import ShortNumber from 'mastodon/components/short_number'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; +import FollowRequestNoteContainer from '../containers/follow_request_note_container'; import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; import { Helmet } from 'react-helmet'; @@ -311,6 +312,8 @@ class Header extends ImmutablePureComponent { return (
+ {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && } +
{!suspended && info} diff --git a/app/javascript/mastodon/features/account/containers/follow_request_note_container.js b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js new file mode 100644 index 000000000..c33c3de59 --- /dev/null +++ b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FollowRequestNote from '../components/follow_request_note'; +import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts'; + +const mapDispatchToProps = (dispatch, { account }) => ({ + onAuthorize () { + dispatch(authorizeFollowRequest(account.get('id'))); + }, + + onReject () { + dispatch(rejectFollowRequest(account.get('id'))); + }, +}); + +export default connect(null, mapDispatchToProps)(FollowRequestNote); diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index 53949258a..850ece351 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -1,3 +1,6 @@ +import { + NOTIFICATIONS_UPDATE, +} from '../actions/notifications'; import { ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_FOLLOW_REQUEST, @@ -12,6 +15,8 @@ import { ACCOUNT_PIN_SUCCESS, ACCOUNT_UNPIN_SUCCESS, RELATIONSHIPS_FETCH_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_REJECT_SUCCESS, } from '../actions/accounts'; import { DOMAIN_BLOCK_SUCCESS, @@ -44,6 +49,12 @@ const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { + case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: + return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false); + case FOLLOW_REQUEST_REJECT_SUCCESS: + return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false); + case NOTIFICATIONS_UPDATE: + return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state; case ACCOUNT_FOLLOW_REQUEST: return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true); case ACCOUNT_FOLLOW_FAIL: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 15fc6aa69..6a22f6009 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -166,6 +166,30 @@ &:disabled { opacity: 0.5; } + + &.button--confirmation { + color: $valid-value-color; + border-color: $valid-value-color; + + &:active, + &:focus, + &:hover { + background: $valid-value-color; + color: $primary-text-color; + } + } + + &.button--destructive { + color: $error-value-color; + border-color: $error-value-color; + + &:active, + &:focus, + &:hover { + background: $error-value-color; + color: $primary-text-color; + } + } } &.button--block { @@ -6722,7 +6746,8 @@ noscript { } } -.moved-account-banner { +.moved-account-banner, +.follow-request-banner { padding: 20px; background: lighten($ui-base-color, 4%); display: flex; @@ -6745,6 +6770,7 @@ noscript { justify-content: space-between; align-items: center; gap: 15px; + width: 100%; } .detailed-status__display-name { @@ -6752,6 +6778,10 @@ noscript { } } +.follow-request-banner .button { + width: 100%; +} + .column-inline-form { padding: 15px; display: flex; diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 15c49f2fe..de8bf338f 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -44,6 +44,10 @@ module AccountInteractions end end + def requested_by_map(target_account_ids, account_id) + follow_mapping(FollowRequest.where(account_id: target_account_ids, target_account_id: account_id), :account_id) + end + def endorsed_map(target_account_ids, account_id) follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id) end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index d662380f6..ab8bac412 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -2,7 +2,7 @@ class AccountRelationshipsPresenter attr_reader :following, :followed_by, :blocking, :blocked_by, - :muting, :requested, :domain_blocking, + :muting, :requested, :requested_by, :domain_blocking, :endorsed, :account_note def initialize(account_ids, current_account_id, **options) @@ -15,6 +15,7 @@ class AccountRelationshipsPresenter @blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id)) @muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id)) @requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id)) + @requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id)) @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id)) @endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id)) @account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id)) @@ -27,6 +28,7 @@ class AccountRelationshipsPresenter @blocked_by.merge!(options[:blocked_by_map] || {}) @muting.merge!(options[:muting_map] || {}) @requested.merge!(options[:requested_map] || {}) + @requested_by.merge!(options[:requested_by_map] || {}) @domain_blocking.merge!(options[:domain_blocking_map] || {}) @endorsed.merge!(options[:endorsed_map] || {}) @account_note.merge!(options[:account_note_map] || {}) @@ -44,6 +46,7 @@ class AccountRelationshipsPresenter blocked_by: {}, muting: {}, requested: {}, + requested_by: {}, domain_blocking: {}, endorsed: {}, account_note: {}, @@ -73,6 +76,7 @@ class AccountRelationshipsPresenter blocked_by: { account_id => blocked_by[account_id] }, muting: { account_id => muting[account_id] }, requested: { account_id => requested[account_id] }, + requested_by: { account_id => requested_by[account_id] }, domain_blocking: { account_id => domain_blocking[account_id] }, endorsed: { account_id => endorsed[account_id] }, account_note: { account_id => account_note[account_id] }, diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb index 31fc60eb2..b53387401 100644 --- a/app/serializers/rest/relationship_serializer.rb +++ b/app/serializers/rest/relationship_serializer.rb @@ -2,8 +2,8 @@ class REST::RelationshipSerializer < ActiveModel::Serializer attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by, - :blocking, :blocked_by, :muting, :muting_notifications, :requested, - :domain_blocking, :endorsed, :note + :blocking, :blocked_by, :muting, :muting_notifications, + :requested, :requested_by, :domain_blocking, :endorsed, :note def id object.id.to_s @@ -54,6 +54,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer instance_options[:relationships].requested[object.id] ? true : false end + def requested_by + instance_options[:relationships].requested_by[object.id] ? true : false + end + def domain_blocking instance_options[:relationships].domain_blocking[object.id] || false end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index c9d782cee..6cd769dc8 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do end end + describe '.requested_by_map' do + it 'returns an hash' do + expect(Account.requested_by_map([], 1)).to be_a Hash + end + end + describe 'MENTION_RE' do subject { Account::MENTION_RE } diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb index edfbbb354..8a485d2b9 100644 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -10,6 +10,7 @@ RSpec.describe AccountRelationshipsPresenter do allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:requested_by_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) end @@ -71,6 +72,14 @@ RSpec.describe AccountRelationshipsPresenter do end end + context 'options[:requested_by_map] is set' do + let(:options) { { requested_by_map: { 6 => true } } } + + it 'sets @requested merged with default_map and options[:requested_by_map]' do + expect(presenter.requested_by).to eq default_map.merge(options[:requested_by_map]) + end + end + context 'options[:domain_blocking_map] is set' do let(:options) { { domain_blocking_map: { 7 => true } } } -- cgit