From 4a5464f36051ea9cbc0c7afa8c885f4f2bdd9245 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 5 Jun 2023 01:42:17 +0200 Subject: [PATCH 01/15] Change "Follow 7 people" to "Find at least 7 people to follow" in web UI (#24954) --- app/javascript/mastodon/features/onboarding/index.jsx | 2 +- app/javascript/mastodon/locales/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index ecebdb6965..79291b3d08 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -120,7 +120,7 @@ class Onboarding extends ImmutablePureComponent {
0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} /> - = 7} icon='user-plus' label={} description={} /> + = 7} icon='user-plus' label={} description={} /> = 1} icon='pencil-square-o' label={} description={} /> } description={} />
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 5ed793cdba..f6d6daa3e5 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -460,8 +460,8 @@ "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.skip": "Want to skip right ahead?", "onboarding.start.title": "You've made it!", - "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.", - "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}", + "onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.", + "onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow", "onboarding.steps.publish_status.body": "Say hello to the world.", "onboarding.steps.publish_status.title": "Make your first post", "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", From e49819142f98a94ba44e24aa093815bf74afab05 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sun, 4 Jun 2023 19:57:05 -0400 Subject: [PATCH 02/15] Remove unmaintained `nsa` gem (#25265) --- Gemfile | 1 - Gemfile.lock | 7 ------- config/initializers/statsd.rb | 15 --------------- 3 files changed, 23 deletions(-) delete mode 100644 config/initializers/statsd.rb diff --git a/Gemfile b/Gemfile index 62e45a5f30..cff8cb1f88 100644 --- a/Gemfile +++ b/Gemfile @@ -60,7 +60,6 @@ gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' -gem 'nsa', '~> 0.2' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' diff --git a/Gemfile.lock b/Gemfile.lock index 7d04d875c5..bb209b3841 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,11 +442,6 @@ GEM nokogiri (1.15.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.2.8) - activesupport (>= 4.2, < 7) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) oj (3.14.3) omniauth (1.9.2) hashie (>= 3.4.6) @@ -682,7 +677,6 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) stackprof (0.2.25) - statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) strong_migrations (0.8.0) @@ -831,7 +825,6 @@ DEPENDENCIES net-http (~> 0.3.2) net-ldap (~> 0.18) nokogiri (~> 1.15) - nsa (~> 0.2) oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb deleted file mode 100644 index 93ea1d1e4a..0000000000 --- a/config/initializers/statsd.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -if ENV['STATSD_ADDR'].present? - host, port = ENV['STATSD_ADDR'].split(':') - - $statsd = ::Statsd.new(host, port) - $statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } - - ::NSA.inform_statsd($statsd) do |informant| - informant.collect(:action_controller, :web) - informant.collect(:active_record, :db) - informant.collect(:active_support_cache, :cache) - informant.collect(:sidekiq, :sidekiq) - end -end From 03a707f6a6c8bbefec630c04d183fc9f22b23d9d Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 5 Jun 2023 03:16:12 -0300 Subject: [PATCH 03/15] Add test coverage for `Mastodon::CLI::Accounts#merge` (#25199) --- spec/lib/mastodon/cli/accounts_spec.rb | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index ba49e480ad..50066572c6 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -998,4 +998,115 @@ describe Mastodon::CLI::Accounts do end end end + + describe '#merge' do + shared_examples 'an account not found' do |acct| + it 'exits with an error message indicating that there is no such account' do + expect { cli.invoke(:merge, arguments) }.to output( + a_string_including("No such account (#{acct})") + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when "from_account" is not found' do + let(:to_account) { Fabricate(:account, domain: 'example.com') } + let(:arguments) { ['non_existent_username@domain.com', "#{to_account.username}@#{to_account.domain}"] } + + it_behaves_like 'an account not found', 'non_existent_username@domain.com' + end + + context 'when "from_account" is a local account' do + let(:from_account) { Fabricate(:account, domain: nil, username: 'bob') } + let(:to_account) { Fabricate(:account, domain: 'example.com') } + let(:arguments) { [from_account.username, "#{to_account.username}@#{to_account.domain}"] } + + it_behaves_like 'an account not found', 'bob' + end + + context 'when "to_account" is not found' do + let(:from_account) { Fabricate(:account, domain: 'example.com') } + let(:arguments) { ["#{from_account.username}@#{from_account.domain}", 'non_existent_username'] } + + it_behaves_like 'an account not found', 'non_existent_username' + end + + context 'when "to_account" is local' do + let(:from_account) { Fabricate(:account, domain: 'example.com') } + let(:to_account) { Fabricate(:account, domain: nil, username: 'bob') } + let(:arguments) do + ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] + end + + it_behaves_like 'an account not found', 'bob@' + end + + context 'when "from_account" and "to_account" public keys do not match' do + let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'from_account') } + let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'to_account') } + let(:arguments) do + ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] + end + + before do + allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account) + allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account) + end + + it 'exits with an error message indicating that the accounts do not have the same pub key' do + expect { cli.invoke(:merge, arguments) }.to output( + a_string_including("Accounts don't have the same public key, might not be duplicates!\nOverride with --force") + ).to_stdout + .and raise_error(SystemExit) + end + + context 'with --force option' do + let(:options) { { force: true } } + + before do + allow(to_account).to receive(:merge_with!) + allow(from_account).to receive(:destroy) + end + + it 'merges "from_account" into "to_account"' do + cli.invoke(:merge, arguments, options) + + expect(to_account).to have_received(:merge_with!).with(from_account).once + end + + it 'deletes "from_account"' do + cli.invoke(:merge, arguments, options) + + expect(from_account).to have_received(:destroy).once + end + end + end + + context 'when "from_account" and "to_account" public keys match' do + let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'pub_key') } + let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'pub_key') } + let(:arguments) do + ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] + end + + before do + allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account) + allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account) + allow(to_account).to receive(:merge_with!) + allow(from_account).to receive(:destroy) + end + + it 'merges "from_account" into "to_account"' do + cli.invoke(:merge, arguments) + + expect(to_account).to have_received(:merge_with!).with(from_account).once + end + + it 'deletes "from_account"' do + cli.invoke(:merge, arguments) + + expect(from_account).to have_received(:destroy) + end + end + end end From 5c87c6650424c0ec759257b0c8e71fa2d51eedc9 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 02:20:18 -0400 Subject: [PATCH 04/15] Add coverage for CLI::CanonicalEmailBlocks command (#25239) --- .../cli/canonical_email_blocks_spec.rb | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb index fb481e8a82..eb57a3cd15 100644 --- a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb +++ b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb @@ -4,9 +4,57 @@ require 'rails_helper' require 'mastodon/cli/canonical_email_blocks' describe Mastodon::CLI::CanonicalEmailBlocks do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#find' do + let(:arguments) { ['user@example.com'] } + + context 'when a block is present' do + before { Fabricate(:canonical_email_block, email: 'user@example.com') } + + it 'announces the presence of the block' do + expect { cli.invoke(:find, arguments) }.to output( + a_string_including('user@example.com is blocked') + ).to_stdout + end + end + + context 'when a block is not present' do + it 'announces the absence of the block' do + expect { cli.invoke(:find, arguments) }.to output( + a_string_including('user@example.com is not blocked') + ).to_stdout + end + end + end + + describe '#remove' do + let(:arguments) { ['user@example.com'] } + + context 'when a block is present' do + before { Fabricate(:canonical_email_block, email: 'user@example.com') } + + it 'removes the block' do + expect { cli.invoke(:remove, arguments) }.to output( + a_string_including('Unblocked user@example.com') + ).to_stdout + + expect(CanonicalEmailBlock.matching_email('user@example.com')).to be_empty + end + end + + context 'when a block is not present' do + it 'announces the absence of the block' do + expect { cli.invoke(:remove, arguments) }.to output( + a_string_including('user@example.com is not blocked') + ).to_stdout + end + end + end end From 0daf78f903ba81a2fd1566fd9845cea623e8324c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 02:22:03 -0400 Subject: [PATCH 05/15] Add `allow_other_host: true` to backups controller (#25266) --- app/controllers/backups_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5891da6f6d..205df48d44 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -11,15 +11,15 @@ class BackupsController < ApplicationController def download case Paperclip::Attachment.default_options[:storage] when :s3 - redirect_to @backup.dump.expiring_url(10) + redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10) + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true else - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end when :filesystem - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end end From bacb6749217767de83120ad1dea6a59f69ee66d9 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 5 Jun 2023 00:37:02 -0700 Subject: [PATCH 06/15] Add exclusive lists (#22048) Co-authored-by: Liam Cooke Co-authored-by: John Holdun Co-authored-by: Effy Elden Co-authored-by: Lina Reyne Co-authored-by: Lina <20880695+necropolina@users.noreply.github.com> Co-authored-by: Claire --- app/controllers/api/v1/lists_controller.rb | 2 +- app/javascript/mastodon/actions/lists.js | 4 +- .../mastodon/features/list_timeline/index.jsx | 18 ++++++++- app/javascript/mastodon/locales/en.json | 1 + .../mastodon/reducers/list_editor.js | 2 + app/lib/feed_manager.rb | 26 +++++++------ app/models/list.rb | 1 + app/serializers/rest/list_serializer.rb | 2 +- .../20230605085710_add_exclusive_to_lists.rb | 7 ++++ db/schema.rb | 3 +- spec/lib/feed_manager_spec.rb | 37 +++++++++++++++++++ 11 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20230605085710_add_exclusive_to_lists.rb diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2b..4bbbed2673 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index 2faa54b955..b0789cd426 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -151,10 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index f41e8e6f23..f9f3a7c315 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; +import Toggle from 'react-toggle'; + import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { fetchList, deleteList, updateList } from 'mastodon/actions/lists'; import { openModal } from 'mastodon/actions/modal'; @@ -145,7 +147,13 @@ class ListTimeline extends PureComponent { handleRepliesPolicyChange = ({ target }) => { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(updateList(id, undefined, false, target.value)); + dispatch(updateList(id, undefined, false, undefined, target.value)); + }; + + onExclusiveToggle = ({ target }) => { + const { dispatch } = this.props; + const { id } = this.props.params; + dispatch(updateList(id, undefined, false, target.checked, undefined)); }; render () { @@ -154,6 +162,7 @@ class ListTimeline extends PureComponent { const pinned = !!columnId; const title = list ? list.get('title') : id; const replies_policy = list ? list.get('replies_policy') : undefined; + const isExclusive = list ? list.get('exclusive') : undefined; if (typeof list === 'undefined') { return ( @@ -191,6 +200,13 @@ class ListTimeline extends PureComponent { +
+ + +
+ { replies_policy !== undefined && (
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f6d6daa3e5..09282de7c8 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -356,6 +356,7 @@ "lists.delete": "Delete list", "lists.edit": "Edit list", "lists.edit.submit": "Change title", + "lists.exclusive": "Hide these posts from home", "lists.new.create": "Add list", "lists.new.title_placeholder": "New list title", "lists.replies_policy.followed": "Any followed user", diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js index ceceb27c7a..d3fd62adec 100644 --- a/app/javascript/mastodon/reducers/list_editor.js +++ b/app/javascript/mastodon/reducers/list_editor.js @@ -25,6 +25,7 @@ const initialState = ImmutableMap({ isSubmitting: false, isChanged: false, title: '', + isExclusive: false, accounts: ImmutableMap({ items: ImmutableList(), @@ -46,6 +47,7 @@ export default function listEditorReducer(state = initialState, action) { return state.withMutations(map => { map.set('listId', action.list.get('id')); map.set('title', action.list.get('title')); + map.set('isExclusive', action.list.get('is_exclusive')); map.set('isSubmitting', false); }); case LIST_EDITOR_TITLE_CHANGE: diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 643e6828d2..7423d2d092 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -40,9 +40,9 @@ class FeedManager def filter?(timeline_type, status, receiver) case timeline_type when :home - filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status])) + filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), :home) when :list - filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status])) + filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list) when :mentions filter_from_mentions?(status, receiver.id) when :tags @@ -351,10 +351,11 @@ class FeedManager # @param [Integer] receiver_id # @param [Hash] crutches # @return [Boolean] - def filter_from_home?(status, receiver_id, crutches) + def filter_from_home?(status, receiver_id, crutches, timeline_type = :home) return false if receiver_id == status.account_id return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) - return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) + return true if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present? + return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) check_for_blocks = crutches[:active_mentions][status.id] || [] check_for_blocks.push(status.account_id) @@ -543,13 +544,16 @@ class FeedManager arr end - crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true) - crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h - crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) - crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) - crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) - crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) - crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) + lists = List.where(account_id: receiver_id, exclusive: true) + + crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true) + crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h + crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) + crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) + crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) + crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) + crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) + crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true) crutches end diff --git a/app/models/list.rb b/app/models/list.rb index bd1bdbd24d..7dc96f01b3 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -10,6 +10,7 @@ # created_at :datetime not null # updated_at :datetime not null # replies_policy :integer default("list"), not null +# exclusive :boolean default(FALSE) # class List < ApplicationRecord diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb index 3e87f71196..6a1b6ea3eb 100644 --- a/app/serializers/rest/list_serializer.rb +++ b/app/serializers/rest/list_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::ListSerializer < ActiveModel::Serializer - attributes :id, :title, :replies_policy + attributes :id, :title, :replies_policy, :exclusive def id object.id.to_s diff --git a/db/migrate/20230605085710_add_exclusive_to_lists.rb b/db/migrate/20230605085710_add_exclusive_to_lists.rb new file mode 100644 index 0000000000..cc21a3e315 --- /dev/null +++ b/db/migrate/20230605085710_add_exclusive_to_lists.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddExclusiveToLists < ActiveRecord::Migration[6.1] + def change + add_column :lists, :exclusive, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 98fa5d6004..35fbb8d2ef 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: 2023_05_31_154811) do +ActiveRecord::Schema.define(version: 2023_06_05_085710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -567,6 +567,7 @@ ActiveRecord::Schema.define(version: 2023_05_31_154811) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "replies_policy", default: 0, null: false + t.boolean "exclusive", default: false t.index ["account_id"], name: "index_lists_on_account_id" end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 79d1f5249e..31b53fd879 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -26,6 +26,7 @@ RSpec.describe FeedManager do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } let(:jeff) { Fabricate(:account, username: 'jeff') } + let(:list) { Fabricate(:list, account: alice) } context 'with home feed' do it 'returns false for followee\'s status' do @@ -153,6 +154,42 @@ RSpec.describe FeedManager do status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de') expect(FeedManager.instance.filter?(:home, status, alice)).to be false end + + it 'returns true for post from followee on exclusive list' do + list.exclusive = true + alice.follow!(bob) + list.accounts << bob + allow(List).to receive(:where).and_return(list) + status = Fabricate(:status, text: 'I post a lot', account: bob) + expect(FeedManager.instance.filter?(:home, status, alice)).to be true + end + + it 'returns true for reblog from followee on exclusive list' do + list.exclusive = true + alice.follow!(jeff) + list.accounts << jeff + allow(List).to receive(:where).and_return(list) + status = Fabricate(:status, text: 'I post a lot', account: bob) + reblog = Fabricate(:status, reblog: status, account: jeff) + expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true + end + + it 'returns false for post from followee on non-exclusive list' do + list.exclusive = false + alice.follow!(bob) + list.accounts << bob + status = Fabricate(:status, text: 'I post a lot', account: bob) + expect(FeedManager.instance.filter?(:home, status, alice)).to be false + end + + it 'returns false for reblog from followee on non-exclusive list' do + list.exclusive = false + alice.follow!(jeff) + list.accounts << jeff + status = Fabricate(:status, text: 'I post a lot', account: bob) + reblog = Fabricate(:status, reblog: status, account: jeff) + expect(FeedManager.instance.filter?(:home, reblog, alice)).to be false + end end context 'with mentions feed' do From c2c396157fa76160b2bb28fd224b1eac40c9c220 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jun 2023 09:52:36 +0200 Subject: [PATCH 07/15] Fix design issues with recent react-intl upgrade (#25272) --- app/javascript/mastodon/locales/intl_provider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/locales/intl_provider.tsx b/app/javascript/mastodon/locales/intl_provider.tsx index 1ea77c798e..4fa8b2247c 100644 --- a/app/javascript/mastodon/locales/intl_provider.tsx +++ b/app/javascript/mastodon/locales/intl_provider.tsx @@ -48,6 +48,7 @@ export const IntlProvider: React.FC< locale={locale} messages={messages} onError={onProviderError} + textComponent='span' {...props} > {children} From a8310b15ed95dc262084940d79edb2f456f96742 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Mon, 5 Jun 2023 08:40:35 -0400 Subject: [PATCH 08/15] Update kt-paperclip 7.2 from sha (#25274) --- Gemfile | 2 +- Gemfile.lock | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index cff8cb1f88..ad164af1e4 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'dotenv-rails', '~> 2.8' gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' +gem 'kt-paperclip', '~> 7.2' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' diff --git a/Gemfile.lock b/Gemfile.lock index bb209b3841..a9919bd3a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,18 +7,6 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) -GIT - remote: https://github.com/kreeti/kt-paperclip.git - revision: 11abf222dc31bff71160a1d138b445214f434b2b - ref: 11abf222dc31bff71160a1d138b445214f434b2b - specs: - kt-paperclip (7.1.1) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - marcel (~> 1.0.1) - mime-types - terrapin (~> 0.6.0) - GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -380,6 +368,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + kt-paperclip (7.2.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + marcel (~> 1.0.1) + mime-types + terrapin (~> 0.6.0) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -813,7 +807,7 @@ DEPENDENCIES json-ld-preloaded (~> 3.2) json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1)! + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) From a6c898f50160c322eb53252a9a2fff8b7b93a0fb Mon Sep 17 00:00:00 2001 From: "S.H" Date: Mon, 5 Jun 2023 21:49:51 +0900 Subject: [PATCH 09/15] Fix not shown announcements in hometimeline. (#25251) --- app/javascript/mastodon/actions/importer/normalizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 3232e12a2b..9ed6b583b5 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -138,7 +138,7 @@ export function normalizePollOptionTranslation(translation, poll) { export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap.emojis(normalAnnouncement); + const emojiMap = makeEmojiMap(normalAnnouncement.emojis); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); From 70cd2d600029bb43e98994886b4e528c565fafd6 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 5 Jun 2023 09:51:25 -0300 Subject: [PATCH 10/15] Add test coverage for `Mastodon::CLI::Accounts#cull` (#25250) --- spec/lib/mastodon/cli/accounts_spec.rb | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index 50066572c6..cf1d612f3d 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -1109,4 +1109,143 @@ describe Mastodon::CLI::Accounts do end end end + + describe '#cull' do + let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } + let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com') } + let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org') } + let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net') } + let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com') } + let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net') } + + before do + allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) + end + + context 'when no domain is specified' do + let(:scope) { Account.remote.where(protocol: :activitypub).partitioned } + + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(tom) + .and_yield(bob) + .and_yield(gon) + .and_yield(ana) + .and_yield(tales) + .and_return([5, 3]) + stub_request(:head, 'https://example.org/users/bob').to_return(status: 404) + stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) + stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) + end + + it 'deletes all inactive remote accounts that longer exist in the origin server' do + cli.cull + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once + expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once + end + + it 'does not delete any active remote account that still exists in the origin server' do + cli.cull + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false) + expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false) + expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) + end + + it 'touches inactive remote accounts that have not been deleted' do + allow(tales).to receive(:touch) + + cli.cull + + expect(tales).to have_received(:touch).once + end + + it 'displays the summary correctly' do + expect { cli.cull }.to output( + a_string_including('Visited 5 accounts, removed 3') + ).to_stdout + end + end + + context 'when a domain is specified' do + let(:domain) { 'example.net' } + let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned } + + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(gon) + .and_yield(tales) + .and_return([2, 2]) + stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) + stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) + end + + it 'deletes inactive remote accounts that longer exist in the specified domain' do + cli.cull(domain) + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once + expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once + end + + it 'displays the summary correctly' do + expect { cli.cull }.to output( + a_string_including('Visited 2 accounts, removed 2') + ).to_stdout + end + end + + context 'when a domain is unavailable' do + shared_examples 'an unavailable domain' do + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0]) + end + + it 'skips accounts from the unavailable domain' do + cli.cull + + expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) + end + + it 'displays the summary correctly' do + expect { cli.cull }.to output( + a_string_including("Visited 1 accounts, removed 0\nThe following domains were not available during the check:\n example.net") + ).to_stdout + end + end + + context 'when a connection timeout occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_timeout + end + + it_behaves_like 'an unavailable domain' + end + + context 'when a connection error occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_raise(HTTP::ConnectionError) + end + + it_behaves_like 'an unavailable domain' + end + + context 'when an ssl error occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_raise(OpenSSL::SSL::SSLError) + end + + it_behaves_like 'an unavailable domain' + end + + context 'when a private network address error occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_raise(Mastodon::PrivateNetworkAddressError) + end + + it_behaves_like 'an unavailable domain' + end + end + end end From 3b21c13dcc80bad4d8d1ec7c7c52470c5d3942aa Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 10:52:33 -0400 Subject: [PATCH 11/15] Rails 7 compatibility fix for `Admin::Metrics::Dimension` classes (#25277) --- .../dimension/instance_accounts_dimension.rb | 19 +++++++----- .../dimension/instance_languages_dimension.rb | 25 ++++++++++++---- .../metrics/dimension/languages_dimension.rb | 19 +++++++----- .../admin/metrics/dimension/query_helper.rb | 13 ++++++++ .../metrics/dimension/servers_dimension.rb | 24 +++++++++++---- .../metrics/dimension/sources_dimension.rb | 20 ++++++++----- .../dimension/tag_languages_dimension.rb | 29 ++++++++++++++---- .../dimension/tag_servers_dimension.rb | 30 +++++++++++++++---- .../instance_accounts_dimension_spec.rb | 18 +++++++++++ .../instance_languages_dimension_spec.rb | 18 +++++++++++ .../dimension/languages_dimension_spec.rb | 18 +++++++++++ .../dimension/servers_dimension_spec.rb | 18 +++++++++++ .../software_versions_dimension_spec.rb | 18 +++++++++++ .../dimension/sources_dimension_spec.rb | 18 +++++++++++ .../dimension/space_usage_dimension_spec.rb | 18 +++++++++++ .../dimension/tag_languages_dimension_spec.rb | 18 +++++++++++ .../dimension/tag_servers_dimension_spec.rb | 18 +++++++++++ 17 files changed, 297 insertions(+), 44 deletions(-) create mode 100644 app/lib/admin/metrics/dimension/query_helper.rb create mode 100644 spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/servers_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/sources_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb diff --git a/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb b/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb index 4eac8e611e..f8eb9d7bfb 100644 --- a/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb +++ b/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def self.with_params? @@ -14,19 +15,23 @@ class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dim protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { domain: params[:domain], limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT accounts.username, count(follows.*) AS value FROM accounts LEFT JOIN follows ON follows.target_account_id = accounts.id - WHERE accounts.domain = $1 + WHERE accounts.domain = :domain GROUP BY accounts.id, follows.target_account_id ORDER BY value DESC - LIMIT $2 + LIMIT :limit SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, @limit]]) - - rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } } end def params diff --git a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb index 1ede1a56e4..b478213808 100644 --- a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def self.with_params? @@ -14,21 +15,33 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE accounts.domain = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE accounts.domain = :domain + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id AND statuses.reblog_of_id IS NULL GROUP BY COALESCE(statuses.language, 'und') ORDER BY count(*) DESC - LIMIT $4 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end - rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def params diff --git a/app/lib/admin/metrics/dimension/languages_dimension.rb b/app/lib/admin/metrics/dimension/languages_dimension.rb index f1cf82cf27..100692a17b 100644 --- a/app/lib/admin/metrics/dimension/languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/languages_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def key @@ -10,18 +11,22 @@ class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension: protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['locale'], human_key: standard_locale_name(row['locale']), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT locale, count(*) AS value FROM users - WHERE current_sign_in_at BETWEEN $1 AND $2 + WHERE current_sign_in_at BETWEEN :start_at AND :end_at AND locale IS NOT NULL GROUP BY locale ORDER BY count(*) DESC - LIMIT $3 + LIMIT :limit SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]]) - - rows.map { |row| { key: row['locale'], human_key: standard_locale_name(row['locale']), value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/dimension/query_helper.rb b/app/lib/admin/metrics/dimension/query_helper.rb new file mode 100644 index 0000000000..9fc953cb3e --- /dev/null +++ b/app/lib/admin/metrics/dimension/query_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Admin::Metrics::Dimension::QueryHelper + protected + + def dimension_data_rows + ActiveRecord::Base.connection.select_all(sanitized_sql_string) + end + + def sanitized_sql_string + ActiveRecord::Base.sanitize_sql_array(sql_array) + end +end diff --git a/app/lib/admin/metrics/dimension/servers_dimension.rb b/app/lib/admin/metrics/dimension/servers_dimension.rb index 91bcce6551..42aba8e213 100644 --- a/app/lib/admin/metrics/dimension/servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/servers_dimension.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper + def key 'servers' end @@ -8,18 +10,30 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT accounts.domain, count(*) AS value FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE statuses.id BETWEEN $1 AND $2 + WHERE statuses.id BETWEEN :earliest_status_id AND :latest_status_id GROUP BY accounts.domain ORDER BY count(*) DESC - LIMIT $3 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, Mastodon::Snowflake.id_at(@start_at)], [nil, Mastodon::Snowflake.id_at(@end_at)], [nil, @limit]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at) + end - rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at) end end diff --git a/app/lib/admin/metrics/dimension/sources_dimension.rb b/app/lib/admin/metrics/dimension/sources_dimension.rb index 122807cdcd..a14c3e7c16 100644 --- a/app/lib/admin/metrics/dimension/sources_dimension.rb +++ b/app/lib/admin/metrics/dimension/sources_dimension.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper + def key 'sources' end @@ -8,18 +10,22 @@ class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::B protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT oauth_applications.name, count(*) AS value FROM users LEFT JOIN oauth_applications ON oauth_applications.id = users.created_by_application_id - WHERE users.created_at BETWEEN $1 AND $2 + WHERE users.created_at BETWEEN :start_at AND :end_at GROUP BY oauth_applications.name ORDER BY count(*) DESC - LIMIT $3 + LIMIT :limit SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]]) - - rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb index e1349c2294..cd077ff863 100644 --- a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def self.with_params? @@ -14,20 +15,36 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value FROM statuses INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id - WHERE statuses_tags.tag_id = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE statuses_tags.tag_id = :tag_id + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id GROUP BY COALESCE(statuses.language, 'und') ORDER BY count(*) DESC - LIMIT $4 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]]) + def tag_id + params[:id] + end - rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end + + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def params diff --git a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb index 7ddf3378cd..fc5e49a966 100644 --- a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper + def self.with_params? true end @@ -12,21 +14,37 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<-SQL.squish SELECT accounts.domain, count(*) AS value FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id - WHERE statuses_tags.tag_id = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE statuses_tags.tag_id = :tag_id + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id GROUP BY accounts.domain ORDER BY count(*) DESC - LIMIT $4 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]]) + def tag_id + params[:id] + end - rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end + + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def params diff --git a/spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb b/spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb new file mode 100644 index 0000000000..106717f97b --- /dev/null +++ b/spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::InstanceAccountsDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb b/spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb new file mode 100644 index 0000000000..f9f6430ca0 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::InstanceLanguagesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/languages_dimension_spec.rb b/spec/lib/admin/metrics/dimension/languages_dimension_spec.rb new file mode 100644 index 0000000000..1722c4c616 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/languages_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::LanguagesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/servers_dimension_spec.rb b/spec/lib/admin/metrics/dimension/servers_dimension_spec.rb new file mode 100644 index 0000000000..7e2bb9ac0b --- /dev/null +++ b/spec/lib/admin/metrics/dimension/servers_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::ServersDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb b/spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb new file mode 100644 index 0000000000..ee14917330 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::SoftwareVersionsDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/sources_dimension_spec.rb b/spec/lib/admin/metrics/dimension/sources_dimension_spec.rb new file mode 100644 index 0000000000..d6b581a9bb --- /dev/null +++ b/spec/lib/admin/metrics/dimension/sources_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::SourcesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb b/spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb new file mode 100644 index 0000000000..65d04cfedd --- /dev/null +++ b/spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::SpaceUsageDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb b/spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb new file mode 100644 index 0000000000..721d24fa18 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::TagLanguagesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb b/spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb new file mode 100644 index 0000000000..3054716816 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::TagServersDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end From 1483a3ddfe74e4fb81d87447a1781943eab86c60 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jun 2023 17:32:24 +0200 Subject: [PATCH 12/15] Add data-nosnippet so Google doesn't use trending posts in snippets for / (#25279) --- app/javascript/mastodon/features/explore/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx index dbc0400e8e..185db0732a 100644 --- a/app/javascript/mastodon/features/explore/index.jsx +++ b/app/javascript/mastodon/features/explore/index.jsx @@ -67,7 +67,7 @@ class Explore extends PureComponent {
-
+
{isSearching ? ( ) : ( From e428670e614b59048431c79e849d18315da98d72 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jun 2023 17:35:05 +0200 Subject: [PATCH 13/15] Fix CSP headers when S3_ALIAS_HOST includes a path component (#25273) --- config/initializers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index f4f9177996..a05b67440c 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -3,7 +3,7 @@ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy def host_to_url(str) - "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}" if str.present? + "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}".split('/').first if str.present? end base_host = Rails.configuration.x.web_domain From af135bddd0a17be9a1345216db7986b593d51151 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 14:46:04 -0400 Subject: [PATCH 14/15] Update `Admin::Metrics::Measure` classes for Rails 7 (#25236) --- .../measure/instance_accounts_measure.rb | 24 ++++++--------- .../measure/instance_followers_measure.rb | 24 ++++++--------- .../measure/instance_follows_measure.rb | 24 ++++++--------- .../instance_media_attachments_measure.rb | 23 +++++--------- .../measure/instance_reports_measure.rb | 24 ++++++--------- .../measure/instance_statuses_measure.rb | 30 ++++++++++--------- .../metrics/measure/new_users_measure.rb | 16 +++++----- .../metrics/measure/opened_reports_measure.rb | 16 +++++----- app/lib/admin/metrics/measure/query_helper.rb | 25 ++++++++++++++++ .../measure/resolved_reports_measure.rb | 16 +++++----- .../metrics/measure/tag_servers_measure.rb | 24 ++++++++++----- .../measure/active_users_measure_spec.rb | 17 +++++++++++ .../measure/instance_accounts_measure_spec.rb | 6 ++++ .../instance_followers_measure_spec.rb | 6 ++++ .../measure/instance_follows_measure_spec.rb | 6 ++++ ...instance_media_attachments_measure_spec.rb | 6 ++++ .../measure/instance_reports_measure_spec.rb | 6 ++++ .../measure/instance_statuses_measure_spec.rb | 6 ++++ .../measure/interactions_measure_spec.rb | 17 +++++++++++ .../metrics/measure/new_users_measure_spec.rb | 17 +++++++++++ .../measure/opened_reports_measure_spec.rb | 17 +++++++++++ .../measure/resolved_reports_measure_spec.rb | 17 +++++++++++ .../measure/tag_accounts_measure_spec.rb | 19 ++++++++++++ .../measure/tag_servers_measure_spec.rb | 19 ++++++++++++ .../metrics/measure/tag_uses_measure_spec.rb | 19 ++++++++++++ 25 files changed, 307 insertions(+), 117 deletions(-) create mode 100644 app/lib/admin/metrics/measure/query_helper.rb create mode 100644 spec/lib/admin/metrics/measure/active_users_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/interactions_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/new_users_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb diff --git a/app/lib/admin/metrics/measure/instance_accounts_measure.rb b/app/lib/admin/metrics/measure/instance_accounts_measure.rb index 14a61de88c..3d081fdd90 100644 --- a/app/lib/admin/metrics/measure/instance_accounts_measure.rb +++ b/app/lib/admin/metrics/measure/instance_accounts_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,33 +27,25 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_accounts AS ( SELECT accounts.id FROM accounts WHERE date_trunc('day', accounts.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_accounts ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_followers_measure.rb b/app/lib/admin/metrics/measure/instance_followers_measure.rb index dc0f5492c9..378c6754d9 100644 --- a/app/lib/admin/metrics/measure/instance_followers_measure.rb +++ b/app/lib/admin/metrics/measure/instance_followers_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_followers AS ( SELECT follows.id FROM follows INNER JOIN accounts ON follows.account_id = accounts.id WHERE date_trunc('day', follows.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_followers ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_follows_measure.rb b/app/lib/admin/metrics/measure/instance_follows_measure.rb index f2088ffb30..e213348fbc 100644 --- a/app/lib/admin/metrics/measure/instance_follows_measure.rb +++ b/app/lib/admin/metrics/measure/instance_follows_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_follows AS ( SELECT follows.id FROM follows INNER JOIN accounts ON follows.target_account_id = accounts.id WHERE date_trunc('day', follows.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_follows ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb index 779883e031..2d4b5f56b0 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper include ActionView::Helpers::NumberHelper def self.with_params? @@ -35,34 +36,26 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_media_attachments AS ( SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size FROM media_attachments INNER JOIN accounts ON accounts.id = media_attachments.account_id WHERE date_trunc('day', media_attachments.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT SUM(size) FROM new_media_attachments ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_reports_measure.rb b/app/lib/admin/metrics/measure/instance_reports_measure.rb index c1f7189bfe..9da3d53e34 100644 --- a/app/lib/admin/metrics/measure/instance_reports_measure.rb +++ b/app/lib/admin/metrics/measure/instance_reports_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_reports AS ( SELECT reports.id FROM reports INNER JOIN accounts ON accounts.id = reports.target_account_id WHERE date_trunc('day', reports.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index 1b38b40c55..8c71c66145 100644 --- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb +++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,35 +27,35 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $5::text))" - else - 'accounts.domain = $5::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_statuses AS ( SELECT statuses.id FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE statuses.id BETWEEN $3 AND $4 - AND #{account_matching_sql} + WHERE statuses.id BETWEEN :earliest_status_id AND :latest_status_id + AND #{account_domain_sql(params[:include_subdomains])} AND date_trunc('day', statuses.created_at)::date = axis.period ) SELECT count(*) FROM new_statuses ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, params[:domain]]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end - rows.map { |row| { date: row['period'], value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def time_period diff --git a/app/lib/admin/metrics/measure/new_users_measure.rb b/app/lib/admin/metrics/measure/new_users_measure.rb index 71191f1a22..6837c14c82 100644 --- a/app/lib/admin/metrics/measure/new_users_measure.rb +++ b/app/lib/admin/metrics/measure/new_users_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def key 'new_users' end @@ -15,8 +17,12 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe User.where(created_at: previous_time_period).count end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_users AS ( SELECT users.id @@ -26,12 +32,8 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe SELECT count(*) FROM new_users ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/measure/opened_reports_measure.rb b/app/lib/admin/metrics/measure/opened_reports_measure.rb index 4b80a0c8c3..c395c46341 100644 --- a/app/lib/admin/metrics/measure/opened_reports_measure.rb +++ b/app/lib/admin/metrics/measure/opened_reports_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def key 'opened_reports' end @@ -15,8 +17,12 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B Report.where(created_at: previous_time_period).count end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_reports AS ( SELECT reports.id @@ -26,12 +32,8 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B SELECT count(*) FROM new_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/measure/query_helper.rb b/app/lib/admin/metrics/measure/query_helper.rb new file mode 100644 index 0000000000..969065f73f --- /dev/null +++ b/app/lib/admin/metrics/measure/query_helper.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Admin::Metrics::Measure::QueryHelper + protected + + def perform_data_query + measurement_data_rows.map { |row| { date: row['period'], value: row['value'].to_s } } + end + + def measurement_data_rows + ActiveRecord::Base.connection.select_all(sanitized_sql_string) + end + + def sanitized_sql_string + ActiveRecord::Base.sanitize_sql_array(sql_array) + end + + def account_domain_sql(include_subdomains) + if include_subdomains + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || :domain::text))" + else + 'accounts.domain = :domain::text' + end + end +end diff --git a/app/lib/admin/metrics/measure/resolved_reports_measure.rb b/app/lib/admin/metrics/measure/resolved_reports_measure.rb index 4ab746c8fa..780db75a10 100644 --- a/app/lib/admin/metrics/measure/resolved_reports_measure.rb +++ b/app/lib/admin/metrics/measure/resolved_reports_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def key 'resolved_reports' end @@ -15,8 +17,12 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure: Report.resolved.where(action_taken_at: previous_time_period).count end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH resolved_reports AS ( SELECT reports.id @@ -26,12 +32,8 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure: SELECT count(*) FROM resolved_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/measure/tag_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb index 11f229602e..e6378b8021 100644 --- a/app/lib/admin/metrics/measure/tag_servers_measure.rb +++ b/app/lib/admin/metrics/measure/tag_servers_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -19,25 +21,33 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base tag.statuses.where('statuses.id BETWEEN ? AND ?', Mastodon::Snowflake.id_at(@start_at - length_of_period, with_random: false), Mastodon::Snowflake.id_at(@end_at - length_of_period, with_random: false)).joins(:account).count('distinct accounts.domain') end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( SELECT count(distinct accounts.domain) AS value FROM statuses INNER JOIN statuses_tags ON statuses.id = statuses_tags.status_id INNER JOIN accounts ON statuses.account_id = accounts.id - WHERE statuses_tags.tag_id = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE statuses_tags.tag_id = :tag_id + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id AND date_trunc('day', statuses.created_at)::date = axis.day ) FROM ( - SELECT generate_series(date_trunc('day', $4::timestamp)::date, date_trunc('day', $5::timestamp)::date, ('1 day')::interval) AS day + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, ('1 day')::interval) AS day ) as axis SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id].to_i], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @start_at], [nil, @end_at]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end - rows.map { |row| { date: row['day'], value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def tag diff --git a/spec/lib/admin/metrics/measure/active_users_measure_spec.rb b/spec/lib/admin/metrics/measure/active_users_measure_spec.rb new file mode 100644 index 0000000000..55164ed88a --- /dev/null +++ b/spec/lib/admin/metrics/measure/active_users_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::ActiveUsersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb index 29a157491e..8e414963f3 100644 --- a/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb @@ -37,4 +37,10 @@ describe Admin::Metrics::Measure::InstanceAccountsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb index ebf789c1b3..c627e6cede 100644 --- a/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb @@ -39,4 +39,10 @@ describe Admin::Metrics::Measure::InstanceFollowersMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb index 335e3c7321..42f33dfc35 100644 --- a/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb @@ -39,4 +39,10 @@ describe Admin::Metrics::Measure::InstanceFollowsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb index 711a2aff05..c103307f97 100644 --- a/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb @@ -40,4 +40,10 @@ describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb index f0ffd39cfb..62fcf84ac1 100644 --- a/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb @@ -36,4 +36,10 @@ describe Admin::Metrics::Measure::InstanceReportsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb index c1425ecdb9..df4cfe207b 100644 --- a/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb @@ -36,4 +36,10 @@ describe Admin::Metrics::Measure::InstanceStatusesMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/interactions_measure_spec.rb b/spec/lib/admin/metrics/measure/interactions_measure_spec.rb new file mode 100644 index 0000000000..e98c830598 --- /dev/null +++ b/spec/lib/admin/metrics/measure/interactions_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InteractionsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/new_users_measure_spec.rb b/spec/lib/admin/metrics/measure/new_users_measure_spec.rb new file mode 100644 index 0000000000..fe82f8219b --- /dev/null +++ b/spec/lib/admin/metrics/measure/new_users_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::NewUsersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb new file mode 100644 index 0000000000..deed64ae88 --- /dev/null +++ b/spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::OpenedReportsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb new file mode 100644 index 0000000000..cb98df2dc2 --- /dev/null +++ b/spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::ResolvedReportsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb b/spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb new file mode 100644 index 0000000000..938b67afa3 --- /dev/null +++ b/spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::TagAccountsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let!(:tag) { Fabricate(:tag) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new(id: tag.id) } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb b/spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb new file mode 100644 index 0000000000..e09a2b04e5 --- /dev/null +++ b/spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::TagServersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let!(:tag) { Fabricate(:tag) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new(id: tag.id) } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb b/spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb new file mode 100644 index 0000000000..869e937445 --- /dev/null +++ b/spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::TagUsesMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let!(:tag) { Fabricate(:tag) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new(id: tag.id) } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end From 0ddc89528213859bf4caeefd475a93ed973ab0e0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 6 Jun 2023 04:14:28 +0200 Subject: [PATCH 15/15] Change follow button in account row to be more obvious in web UI (#24956) --- .../mastodon/components/account.jsx | 41 ++++++++++--------- app/javascript/mastodon/locales/en.json | 8 ++-- .../styles/mastodon/components.scss | 6 ++- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index ea863f5d18..0f3b85388c 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -16,6 +16,7 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { me } from '../initial_state'; import { Avatar } from './avatar'; +import Button from './button'; import { DisplayName } from './display_name'; import { IconButton } from './icon_button'; import { RelativeTimestamp } from './relative_timestamp'; @@ -23,13 +24,13 @@ import { RelativeTimestamp } from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' }, + unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, + mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, + block: { id: 'account.block_short', defaultMessage: 'Block' }, }); class Account extends ImmutablePureComponent { @@ -96,39 +97,39 @@ class Account extends ImmutablePureComponent { let buttons; - if (actionIcon) { - if (onActionClick) { - buttons = ; - } - } else if (account.get('id') !== me && account.get('relationship', null) !== null) { + if (actionIcon && onActionClick) { + buttons = ; + } else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) { const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); const blocking = account.getIn(['relationship', 'blocking']); const muting = account.getIn(['relationship', 'muting']); if (requested) { - buttons = ; + buttons =