Merge commit 'b85c387c5c0527b0ad31c27031a09d361826c5fc' into glitch-soc/merge-upstream
Conflicts: - `config/initializers/content_security_policy.rb`: Kept our version, it was not affected by upstream's bug.
This commit is contained in:
		
						commit
						c48ec9cb8c
					
				
					 178 changed files with 1616 additions and 1109 deletions
				
			
		
							
								
								
									
										114
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,114 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  $schema: 'https://docs.renovatebot.com/renovate-schema.json',
 | 
				
			||||||
 | 
					  extends: [
 | 
				
			||||||
 | 
					    'config:base',
 | 
				
			||||||
 | 
					    ':dependencyDashboard',
 | 
				
			||||||
 | 
					    ':labels(dependencies)',
 | 
				
			||||||
 | 
					    ':maintainLockFilesMonthly', // update non-direct dependencies monthly
 | 
				
			||||||
 | 
					    ':prConcurrentLimit10', // only 10 open PRs at the same time
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it
 | 
				
			||||||
 | 
					  // packageRules order is important, they are applied from top to bottom and are merged,
 | 
				
			||||||
 | 
					  // so for example grouping rules needs to be at the bottom
 | 
				
			||||||
 | 
					  packageRules: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Ignore major version bumps for these node packages
 | 
				
			||||||
 | 
					      matchManagers: ['npm'],
 | 
				
			||||||
 | 
					      matchPackageNames: [
 | 
				
			||||||
 | 
					        '@rails/ujs', // Needs to match the major Rails version
 | 
				
			||||||
 | 
					        'tesseract.js', // Requires code changes
 | 
				
			||||||
 | 
					        'react-hotkeys', // Requires code changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Requires Webpacker upgrade or replacement
 | 
				
			||||||
 | 
					        '@types/webpack',
 | 
				
			||||||
 | 
					        'babel-loader',
 | 
				
			||||||
 | 
					        'compression-webpack-plugin',
 | 
				
			||||||
 | 
					        'css-loader',
 | 
				
			||||||
 | 
					        'imports-loader',
 | 
				
			||||||
 | 
					        'mini-css-extract-plugin',
 | 
				
			||||||
 | 
					        'postcss-loader',
 | 
				
			||||||
 | 
					        'sass-loader',
 | 
				
			||||||
 | 
					        'terser-webpack-plugin',
 | 
				
			||||||
 | 
					        'webpack',
 | 
				
			||||||
 | 
					        'webpack-assets-manifest',
 | 
				
			||||||
 | 
					        'webpack-bundle-analyzer',
 | 
				
			||||||
 | 
					        'webpack-dev-server',
 | 
				
			||||||
 | 
					        'webpack-cli',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // react-router: Requires manual upgrade
 | 
				
			||||||
 | 
					        'history',
 | 
				
			||||||
 | 
					        'react-router-dom',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['major'],
 | 
				
			||||||
 | 
					      enabled: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Ignore major version bumps for these Ruby packages
 | 
				
			||||||
 | 
					      matchManagers: ['bundler'],
 | 
				
			||||||
 | 
					      matchPackageNames: [
 | 
				
			||||||
 | 
					        'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x
 | 
				
			||||||
 | 
					        'strong_migrations', // Requires manual upgrade
 | 
				
			||||||
 | 
					        'sidekiq', // Requires manual upgrade
 | 
				
			||||||
 | 
					        'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
 | 
				
			||||||
 | 
					        'redis', // Requires manual upgrade and sync with Sidekiq version
 | 
				
			||||||
 | 
					        'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Needs major Rails version bump
 | 
				
			||||||
 | 
					        'rack',
 | 
				
			||||||
 | 
					        'rails',
 | 
				
			||||||
 | 
					        'rails-i18n',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['major'],
 | 
				
			||||||
 | 
					      enabled: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Update Github Actions and Docker images weekly
 | 
				
			||||||
 | 
					      matchManagers: ['github-actions', 'dockerfile', 'docker-compose'],
 | 
				
			||||||
 | 
					      extends: ['schedule:weekly'],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version
 | 
				
			||||||
 | 
					      matchManagers: ['dockerfile'],
 | 
				
			||||||
 | 
					      matchPackageNames: ['moritzheiber/ruby-jemalloc'],
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['minor', 'major'],
 | 
				
			||||||
 | 
					      enabled: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Ignore major bump for the node image, this needs to be synced with .nvmrc
 | 
				
			||||||
 | 
					      matchManagers: ['dockerfile'],
 | 
				
			||||||
 | 
					      matchPackageNames: ['node'],
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['major'],
 | 
				
			||||||
 | 
					      enabled: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Ignore major postgres bumps in the docker-compose file, as those break dev environments
 | 
				
			||||||
 | 
					      matchManagers: ['docker-compose'],
 | 
				
			||||||
 | 
					      matchPackageNames: ['postgres'],
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['major'],
 | 
				
			||||||
 | 
					      enabled: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Update devDependencies every week, with one grouped PR
 | 
				
			||||||
 | 
					      matchDepTypes: 'devDependencies',
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['patch', 'minor'],
 | 
				
			||||||
 | 
					      excludePackageNames: [
 | 
				
			||||||
 | 
					        'typescript', // Typescript has many changes in minor versions, needs to be checked every time
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      groupName: 'devDependencies (non-major)',
 | 
				
			||||||
 | 
					      extends: ['schedule:weekly'],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Update @types/* packages every week, with one grouped PR
 | 
				
			||||||
 | 
					      matchPackagePrefixes: '@types/',
 | 
				
			||||||
 | 
					      matchUpdateTypes: ['patch', 'minor'],
 | 
				
			||||||
 | 
					      groupName: 'DefinitelyTyped types (non-major)',
 | 
				
			||||||
 | 
					      extends: ['schedule:weekly'],
 | 
				
			||||||
 | 
					      addLabels: ['typescript'],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    // Add labels depending on package manager
 | 
				
			||||||
 | 
					    { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] },
 | 
				
			||||||
 | 
					    { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] },
 | 
				
			||||||
 | 
					    { matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] },
 | 
				
			||||||
 | 
					    { matchManagers: ['github-actions'], addLabels: ['github_actions'] },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-css.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-css.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - 'package.json'
 | 
					      - 'package.json'
 | 
				
			||||||
      - 'yarn.lock'
 | 
					      - 'yarn.lock'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-haml.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-haml.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - '.github/workflows/haml-lint-problem-matcher.json'
 | 
					      - '.github/workflows/haml-lint-problem-matcher.json'
 | 
				
			||||||
      - '.github/workflows/lint-haml.yml'
 | 
					      - '.github/workflows/lint-haml.yml'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-js.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - 'package.json'
 | 
					      - 'package.json'
 | 
				
			||||||
      - 'yarn.lock'
 | 
					      - 'yarn.lock'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-json.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-json.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - 'package.json'
 | 
					      - 'package.json'
 | 
				
			||||||
      - 'yarn.lock'
 | 
					      - 'yarn.lock'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-md.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-md.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - '.github/workflows/lint-md.yml'
 | 
					      - '.github/workflows/lint-md.yml'
 | 
				
			||||||
      - '.nvmrc'
 | 
					      - '.nvmrc'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-ruby.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-ruby.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - 'Gemfile*'
 | 
					      - 'Gemfile*'
 | 
				
			||||||
      - '.rubocop*.yml'
 | 
					      - '.rubocop*.yml'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/lint-yml.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/lint-yml.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - 'package.json'
 | 
					      - 'package.json'
 | 
				
			||||||
      - 'yarn.lock'
 | 
					      - 'yarn.lock'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								.github/workflows/rebase-needed.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/rebase-needed.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -4,10 +4,12 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
      - 'l10n_main'
 | 
					      - 'l10n_main'
 | 
				
			||||||
  pull_request_target:
 | 
					  pull_request_target:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
      - 'l10n_main'
 | 
					      - 'l10n_main'
 | 
				
			||||||
    types: [synchronize]
 | 
					    types: [synchronize]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/test-js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/test-js.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - 'package.json'
 | 
					      - 'package.json'
 | 
				
			||||||
      - 'yarn.lock'
 | 
					      - 'yarn.lock'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/workflows/test-ruby.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/test-ruby.yml
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -4,6 +4,7 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches-ignore:
 | 
					    branches-ignore:
 | 
				
			||||||
      - 'dependabot/**'
 | 
					      - 'dependabot/**'
 | 
				
			||||||
 | 
					      - 'renovate/**'
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -239,79 +239,6 @@ RSpec/AnyInstance:
 | 
				
			||||||
    - 'spec/workers/activitypub/delivery_worker_spec.rb'
 | 
					    - 'spec/workers/activitypub/delivery_worker_spec.rb'
 | 
				
			||||||
    - 'spec/workers/web/push_notification_worker_spec.rb'
 | 
					    - 'spec/workers/web/push_notification_worker_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: SkipBlocks, EnforcedStyle.
 | 
					 | 
				
			||||||
# SupportedStyles: described_class, explicit
 | 
					 | 
				
			||||||
RSpec/DescribedClass:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/cache_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/challengable_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/entity_cache_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/extractor_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/feed_manager_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/hash_object_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/ostatus/tag_manager_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/request_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/tag_manager_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/webfinger_resource_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/mailers/notification_mailer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/mailers/user_mailer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_conversation_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_migration_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/email_domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/export_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/favourite_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/follow_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/identity_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/import_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/media_attachment_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/notification_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/relationship_filter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/report_filter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/session_activation_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/setting_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/site_upload_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_pin_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/user_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/account_moderation_note_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/presenters/account_relationships_presenter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/presenters/status_relationships_presenter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/serializers/activitypub/note_serializer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/serializers/rest/account_serializer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/activitypub/fetch_remote_account_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/activitypub/fetch_remote_actor_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/activitypub/fetch_remote_key_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/after_block_domain_from_account_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/authorize_follow_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/batched_remove_status_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/block_domain_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/block_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/bootstrap_timeline_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/clear_domain_media_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/favourite_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/follow_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/import_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/post_status_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/precompute_feed_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/process_mentions_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/purge_domain_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/reblog_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/reject_follow_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/remove_from_followers_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/remove_status_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/unallow_domain_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/unblock_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/unfollow_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/unmute_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/update_account_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/note_length_validator_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
RSpec/EmptyExampleGroup:
 | 
					RSpec/EmptyExampleGroup:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -468,30 +395,6 @@ RSpec/MessageSpies:
 | 
				
			||||||
    - 'spec/spec_helper.rb'
 | 
					    - 'spec/spec_helper.rb'
 | 
				
			||||||
    - 'spec/validators/status_length_validator_spec.rb'
 | 
					    - 'spec/validators/status_length_validator_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RSpec/MissingExampleGroupArgument:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/users/roles_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/registrations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/features/log_in_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/activity/undo_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/status_reach_finder_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/email_domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/trends/statuses_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/trends/tags_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/user_role_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/user_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/fetch_link_card_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/notify_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/process_mentions_service_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RSpec/MultipleExpectations:
 | 
					RSpec/MultipleExpectations:
 | 
				
			||||||
  Max: 19
 | 
					  Max: 19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1336,11 +1239,6 @@ Style/GlobalStdStream:
 | 
				
			||||||
    - 'config/environments/development.rb'
 | 
					    - 'config/environments/development.rb'
 | 
				
			||||||
    - 'config/environments/production.rb'
 | 
					    - 'config/environments/production.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: AllowedVariables.
 | 
					 | 
				
			||||||
Style/GlobalVars:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'config/initializers/statsd.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
 | 
					# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
 | 
				
			||||||
Style/GuardClause:
 | 
					Style/GuardClause:
 | 
				
			||||||
| 
						 | 
					@ -1474,7 +1372,6 @@ Style/RedundantConstantBase:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'config/environments/production.rb'
 | 
					    - 'config/environments/production.rb'
 | 
				
			||||||
    - 'config/initializers/sidekiq.rb'
 | 
					    - 'config/initializers/sidekiq.rb'
 | 
				
			||||||
    - 'config/initializers/statsd.rb'
 | 
					 | 
				
			||||||
    - 'config/locales/sr-Latn.rb'
 | 
					    - 'config/locales/sr-Latn.rb'
 | 
				
			||||||
    - 'config/locales/sr.rb'
 | 
					    - 'config/locales/sr.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1488,52 +1385,6 @@ Style/RedundantFetchBlock:
 | 
				
			||||||
    - 'config/initializers/paperclip.rb'
 | 
					    - 'config/initializers/paperclip.rb'
 | 
				
			||||||
    - 'config/puma.rb'
 | 
					    - 'config/puma.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
Style/RedundantRegexpCharacterClass:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/link_details_extractor.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/tag_manager.rb'
 | 
					 | 
				
			||||||
    - 'app/models/domain_allow.rb'
 | 
					 | 
				
			||||||
    - 'app/models/domain_block.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fetch_oembed_service.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/rack_attack.rb'
 | 
					 | 
				
			||||||
    - 'lib/tasks/emojis.rake'
 | 
					 | 
				
			||||||
    - 'lib/tasks/mastodon.rake'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
Style/RedundantRegexpEscape:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/webfinger_resource.rb'
 | 
					 | 
				
			||||||
    - 'app/models/account.rb'
 | 
					 | 
				
			||||||
    - 'app/models/tag.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fetch_link_card_service.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/twitter_regex.rb'
 | 
					 | 
				
			||||||
    - 'lib/paperclip/color_extractor.rb'
 | 
					 | 
				
			||||||
    - 'lib/tasks/mastodon.rake'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
 | 
					 | 
				
			||||||
# SupportedStyles: slashes, percent_r, mixed
 | 
					 | 
				
			||||||
Style/RegexpLiteral:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/link_details_extractor.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/plain_text_formatter.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/tag_manager.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/text_formatter.rb'
 | 
					 | 
				
			||||||
    - 'app/models/account.rb'
 | 
					 | 
				
			||||||
    - 'app/models/domain_allow.rb'
 | 
					 | 
				
			||||||
    - 'app/models/domain_block.rb'
 | 
					 | 
				
			||||||
    - 'app/models/site_upload.rb'
 | 
					 | 
				
			||||||
    - 'app/models/tag.rb'
 | 
					 | 
				
			||||||
    - 'app/services/backup_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fetch_oembed_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/search_service.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/rack_attack.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/twitter_regex.rb'
 | 
					 | 
				
			||||||
    - 'config/routes.rb'
 | 
					 | 
				
			||||||
    - 'lib/mastodon/premailer_webpack_strategy.rb'
 | 
					 | 
				
			||||||
    - 'lib/tasks/mastodon.rake'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
 | 
					# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
 | 
				
			||||||
# AllowedMethods: present?, blank?, presence, try, try!
 | 
					# AllowedMethods: present?, blank?, presence, try, try!
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,7 +71,7 @@ module Admin
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def resource_params
 | 
					    def resource_params
 | 
				
			||||||
      params.require(:webhook).permit(:url, events: [])
 | 
					      params.require(:webhook).permit(:url, :template, events: [])
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge';
 | 
				
			||||||
import { me } from '../initial_state';
 | 
					import { me } from '../initial_state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Avatar } from './avatar';
 | 
					import { Avatar } from './avatar';
 | 
				
			||||||
 | 
					import Button from './button';
 | 
				
			||||||
import { DisplayName } from './display_name';
 | 
					import { DisplayName } from './display_name';
 | 
				
			||||||
import { IconButton } from './icon_button';
 | 
					import { IconButton } from './icon_button';
 | 
				
			||||||
import { RelativeTimestamp } from './relative_timestamp';
 | 
					import { RelativeTimestamp } from './relative_timestamp';
 | 
				
			||||||
| 
						 | 
					@ -23,13 +24,13 @@ import { RelativeTimestamp } from './relative_timestamp';
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
					  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
				
			||||||
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
					  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
				
			||||||
  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
 | 
					  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
 | 
				
			||||||
  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
 | 
					  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
 | 
				
			||||||
  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
 | 
					  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
 | 
				
			||||||
  mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
 | 
					  mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
 | 
				
			||||||
  unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
 | 
					  unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
 | 
				
			||||||
  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
 | 
					  mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
 | 
				
			||||||
  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
 | 
					  block: { id: 'account.block_short', defaultMessage: 'Block' },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Account extends ImmutablePureComponent {
 | 
					class Account extends ImmutablePureComponent {
 | 
				
			||||||
| 
						 | 
					@ -96,39 +97,39 @@ class Account extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let buttons;
 | 
					    let buttons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (actionIcon) {
 | 
					    if (actionIcon && onActionClick) {
 | 
				
			||||||
      if (onActionClick) {
 | 
					      buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
 | 
				
			||||||
        buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
 | 
					    } else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (account.get('id') !== me && account.get('relationship', null) !== null) {
 | 
					 | 
				
			||||||
      const following = account.getIn(['relationship', 'following']);
 | 
					      const following = account.getIn(['relationship', 'following']);
 | 
				
			||||||
      const requested = account.getIn(['relationship', 'requested']);
 | 
					      const requested = account.getIn(['relationship', 'requested']);
 | 
				
			||||||
      const blocking  = account.getIn(['relationship', 'blocking']);
 | 
					      const blocking  = account.getIn(['relationship', 'blocking']);
 | 
				
			||||||
      const muting  = account.getIn(['relationship', 'muting']);
 | 
					      const muting  = account.getIn(['relationship', 'muting']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (requested) {
 | 
					      if (requested) {
 | 
				
			||||||
        buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
 | 
					        buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
 | 
				
			||||||
      } else if (blocking) {
 | 
					      } else if (blocking) {
 | 
				
			||||||
        buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
 | 
					        buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
 | 
				
			||||||
      } else if (muting) {
 | 
					      } else if (muting) {
 | 
				
			||||||
        let hidingNotificationsButton;
 | 
					        let hidingNotificationsButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (account.getIn(['relationship', 'muting_notifications'])) {
 | 
					        if (account.getIn(['relationship', 'muting_notifications'])) {
 | 
				
			||||||
          hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
 | 
					          hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
 | 
					          hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        buttons = (
 | 
					        buttons = (
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
 | 
					            <Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
 | 
				
			||||||
            {hidingNotificationsButton}
 | 
					            {hidingNotificationsButton}
 | 
				
			||||||
          </>
 | 
					          </>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } else if (defaultAction === 'mute') {
 | 
					      } else if (defaultAction === 'mute') {
 | 
				
			||||||
        buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
 | 
					        buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
 | 
				
			||||||
      } else if (defaultAction === 'block') {
 | 
					      } else if (defaultAction === 'block') {
 | 
				
			||||||
        buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
 | 
					        buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
 | 
				
			||||||
      } else if (!account.get('moved') || following) {
 | 
					      } else if (!account.get('moved') || following) {
 | 
				
			||||||
        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
 | 
					        buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -121,10 +121,10 @@ class DropdownMenu extends PureComponent {
 | 
				
			||||||
      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
 | 
					      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { text, href = '#', target = '_blank', method } = option;
 | 
					    const { text, href = '#', target = '_blank', method, dangerous } = option;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <li className='dropdown-menu__item' key={`${text}-${i}`}>
 | 
					      <li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
 | 
				
			||||||
        <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
 | 
					        <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
 | 
				
			||||||
          {text}
 | 
					          {text}
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,28 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
import { PureComponent } from 'react';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class LoadMore extends PureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    onClick: PropTypes.func,
 | 
					 | 
				
			||||||
    disabled: PropTypes.bool,
 | 
					 | 
				
			||||||
    visible: PropTypes.bool,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static defaultProps = {
 | 
					 | 
				
			||||||
    visible: true,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render() {
 | 
					 | 
				
			||||||
    const { disabled, visible } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <button type='button' className='load-more' disabled={disabled || !visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
 | 
					 | 
				
			||||||
        <FormattedMessage id='status.load_more' defaultMessage='Load more' />
 | 
					 | 
				
			||||||
      </button>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										24
									
								
								app/javascript/mastodon/components/load_more.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/javascript/mastodon/components/load_more.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  onClick: (event: React.MouseEvent) => void;
 | 
				
			||||||
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					  visible?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export const LoadMore: React.FC<Props> = ({
 | 
				
			||||||
 | 
					  onClick,
 | 
				
			||||||
 | 
					  disabled,
 | 
				
			||||||
 | 
					  visible = true,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      type='button'
 | 
				
			||||||
 | 
					      className='load-more'
 | 
				
			||||||
 | 
					      disabled={disabled || !visible}
 | 
				
			||||||
 | 
					      style={{ visibility: visible ? 'visible' : 'hidden' }}
 | 
				
			||||||
 | 
					      onClick={onClick}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <FormattedMessage id='status.load_more' defaultMessage='Load more' />
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ import IntersectionObserverArticleContainer from '../containers/intersection_obs
 | 
				
			||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
 | 
					import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
 | 
				
			||||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
 | 
					import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import LoadMore from './load_more';
 | 
					import { LoadMore } from './load_more';
 | 
				
			||||||
import LoadPending from './load_pending';
 | 
					import LoadPending from './load_pending';
 | 
				
			||||||
import LoadingIndicator from './loading_indicator';
 | 
					import LoadingIndicator from './loading_indicator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -280,8 +280,8 @@ class StatusActionBar extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (writtenByMe) {
 | 
					    if (writtenByMe) {
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
 | 
				
			||||||
| 
						 | 
					@ -290,22 +290,22 @@ class StatusActionBar extends ImmutablePureComponent {
 | 
				
			||||||
      if (relationship && relationship.get('muting')) {
 | 
					      if (relationship && relationship.get('muting')) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (relationship && relationship.get('blocking')) {
 | 
					      if (relationship && relationship.get('blocking')) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!this.props.onFilter) {
 | 
					      if (!this.props.onFilter) {
 | 
				
			||||||
        menu.push(null);
 | 
					        menu.push(null);
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
 | 
				
			||||||
        menu.push(null);
 | 
					        menu.push(null);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
 | 
					      menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (account.get('acct') !== account.get('username')) {
 | 
					      if (account.get('acct') !== account.get('username')) {
 | 
				
			||||||
        const domain = account.get('acct').split('@')[1];
 | 
					        const domain = account.get('acct').split('@')[1];
 | 
				
			||||||
| 
						 | 
					@ -315,7 +315,7 @@ class StatusActionBar extends ImmutablePureComponent {
 | 
				
			||||||
        if (relationship && relationship.get('domain_blocking')) {
 | 
					        if (relationship && relationship.get('domain_blocking')) {
 | 
				
			||||||
          menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
 | 
					          menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
 | 
					          menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -332,16 +332,16 @@ class Header extends ImmutablePureComponent {
 | 
				
			||||||
      if (account.getIn(['relationship', 'muting'])) {
 | 
					      if (account.getIn(['relationship', 'muting'])) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
 | 
					        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
 | 
					        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (account.getIn(['relationship', 'blocking'])) {
 | 
					      if (account.getIn(['relationship', 'blocking'])) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
 | 
					        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
 | 
					        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
 | 
					      menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (signedIn && isRemote) {
 | 
					    if (signedIn && isRemote) {
 | 
				
			||||||
| 
						 | 
					@ -350,7 +350,7 @@ class Header extends ImmutablePureComponent {
 | 
				
			||||||
      if (account.getIn(['relationship', 'domain_blocking'])) {
 | 
					      if (account.getIn(['relationship', 'domain_blocking'])) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
 | 
					        menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain });
 | 
					        menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
 | 
					import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
 | 
				
			||||||
import { openModal } from 'mastodon/actions/modal';
 | 
					import { openModal } from 'mastodon/actions/modal';
 | 
				
			||||||
import ColumnBackButton from 'mastodon/components/column_back_button';
 | 
					import ColumnBackButton from 'mastodon/components/column_back_button';
 | 
				
			||||||
import LoadMore from 'mastodon/components/load_more';
 | 
					import { LoadMore } from 'mastodon/components/load_more';
 | 
				
			||||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
 | 
					import LoadingIndicator from 'mastodon/components/loading_indicator';
 | 
				
			||||||
import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
					import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
				
			||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 | 
					import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Icon }  from 'mastodon/components/icon';
 | 
					import { Icon }  from 'mastodon/components/icon';
 | 
				
			||||||
import LoadMore from 'mastodon/components/load_more';
 | 
					import { LoadMore } from 'mastodon/components/load_more';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
 | 
					import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
 | 
				
			||||||
import AccountContainer from '../../../containers/account_container';
 | 
					import AccountContainer from '../../../containers/account_container';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodo
 | 
				
			||||||
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 | 
					import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 | 
				
			||||||
import Column from 'mastodon/components/column';
 | 
					import Column from 'mastodon/components/column';
 | 
				
			||||||
import ColumnHeader from 'mastodon/components/column_header';
 | 
					import ColumnHeader from 'mastodon/components/column_header';
 | 
				
			||||||
import LoadMore from 'mastodon/components/load_more';
 | 
					import { LoadMore } from 'mastodon/components/load_more';
 | 
				
			||||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
 | 
					import LoadingIndicator from 'mastodon/components/loading_indicator';
 | 
				
			||||||
import { RadioButton } from 'mastodon/components/radio_button';
 | 
					import { RadioButton } from 'mastodon/components/radio_button';
 | 
				
			||||||
import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
					import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { expandSearch } from 'mastodon/actions/search';
 | 
					import { expandSearch } from 'mastodon/actions/search';
 | 
				
			||||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
 | 
					import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
 | 
				
			||||||
import LoadMore from 'mastodon/components/load_more';
 | 
					import { LoadMore } from 'mastodon/components/load_more';
 | 
				
			||||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
 | 
					import LoadingIndicator from 'mastodon/components/loading_indicator';
 | 
				
			||||||
import Account from 'mastodon/containers/account_container';
 | 
					import Account from 'mastodon/containers/account_container';
 | 
				
			||||||
import Status from 'mastodon/containers/status_container';
 | 
					import Status from 'mastodon/containers/status_container';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,8 @@ const messages = defineMessages({
 | 
				
			||||||
  dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
 | 
					  dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
 | 
				
			||||||
  spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
 | 
					  spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
 | 
				
			||||||
  spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
 | 
					  spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
 | 
				
			||||||
 | 
					  legal: { id: 'report.reasons.legal', defaultMessage: 'It\'s illegal' },
 | 
				
			||||||
 | 
					  legal_description: { id: 'report.reasons.legal_description', defaultMessage: 'You believe it violates the law of your or the server\'s country' },
 | 
				
			||||||
  violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
 | 
					  violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
 | 
				
			||||||
  violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
 | 
					  violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
 | 
				
			||||||
  other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
 | 
					  other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
 | 
				
			||||||
| 
						 | 
					@ -69,11 +71,13 @@ class Category extends PureComponent {
 | 
				
			||||||
    const options = rules.size > 0 ? [
 | 
					    const options = rules.size > 0 ? [
 | 
				
			||||||
      'dislike',
 | 
					      'dislike',
 | 
				
			||||||
      'spam',
 | 
					      'spam',
 | 
				
			||||||
 | 
					      'legal',
 | 
				
			||||||
      'violation',
 | 
					      'violation',
 | 
				
			||||||
      'other',
 | 
					      'other',
 | 
				
			||||||
    ] : [
 | 
					    ] : [
 | 
				
			||||||
      'dislike',
 | 
					      'dislike',
 | 
				
			||||||
      'spam',
 | 
					      'spam',
 | 
				
			||||||
 | 
					      'legal',
 | 
				
			||||||
      'other',
 | 
					      'other',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -219,8 +219,8 @@ class ActionBar extends PureComponent {
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
 | 
					      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
 | 
				
			||||||
      menu.push(null);
 | 
					      menu.push(null);
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
 | 
					      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
 | 
				
			||||||
      menu.push(null);
 | 
					      menu.push(null);
 | 
				
			||||||
| 
						 | 
					@ -228,16 +228,16 @@ class ActionBar extends PureComponent {
 | 
				
			||||||
      if (relationship && relationship.get('muting')) {
 | 
					      if (relationship && relationship.get('muting')) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (relationship && relationship.get('blocking')) {
 | 
					      if (relationship && relationship.get('blocking')) {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
 | 
					        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
 | 
					      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (account.get('acct') !== account.get('username')) {
 | 
					      if (account.get('acct') !== account.get('username')) {
 | 
				
			||||||
        const domain = account.get('acct').split('@')[1];
 | 
					        const domain = account.get('acct').split('@')[1];
 | 
				
			||||||
| 
						 | 
					@ -247,7 +247,7 @@ class ActionBar extends PureComponent {
 | 
				
			||||||
        if (relationship && relationship.get('domain_blocking')) {
 | 
					        if (relationship && relationship.get('domain_blocking')) {
 | 
				
			||||||
          menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
 | 
					          menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
 | 
					          menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,9 +17,10 @@
 | 
				
			||||||
  "account.badges.group": "Group",
 | 
					  "account.badges.group": "Group",
 | 
				
			||||||
  "account.block": "Block @{name}",
 | 
					  "account.block": "Block @{name}",
 | 
				
			||||||
  "account.block_domain": "Block domain {domain}",
 | 
					  "account.block_domain": "Block domain {domain}",
 | 
				
			||||||
 | 
					  "account.block_short": "Block",
 | 
				
			||||||
  "account.blocked": "Blocked",
 | 
					  "account.blocked": "Blocked",
 | 
				
			||||||
  "account.browse_more_on_origin_server": "Browse more on the original profile",
 | 
					  "account.browse_more_on_origin_server": "Browse more on the original profile",
 | 
				
			||||||
  "account.cancel_follow_request": "Withdraw follow request",
 | 
					  "account.cancel_follow_request": "Cancel follow",
 | 
				
			||||||
  "account.direct": "Privately mention @{name}",
 | 
					  "account.direct": "Privately mention @{name}",
 | 
				
			||||||
  "account.disable_notifications": "Stop notifying me when @{name} posts",
 | 
					  "account.disable_notifications": "Stop notifying me when @{name} posts",
 | 
				
			||||||
  "account.domain_blocked": "Domain blocked",
 | 
					  "account.domain_blocked": "Domain blocked",
 | 
				
			||||||
| 
						 | 
					@ -48,7 +49,8 @@
 | 
				
			||||||
  "account.mention": "Mention @{name}",
 | 
					  "account.mention": "Mention @{name}",
 | 
				
			||||||
  "account.moved_to": "{name} has indicated that their new account is now:",
 | 
					  "account.moved_to": "{name} has indicated that their new account is now:",
 | 
				
			||||||
  "account.mute": "Mute @{name}",
 | 
					  "account.mute": "Mute @{name}",
 | 
				
			||||||
  "account.mute_notifications": "Mute notifications from @{name}",
 | 
					  "account.mute_notifications_short": "Mute notifications",
 | 
				
			||||||
 | 
					  "account.mute_short": "Mute",
 | 
				
			||||||
  "account.muted": "Muted",
 | 
					  "account.muted": "Muted",
 | 
				
			||||||
  "account.open_original_page": "Open original page",
 | 
					  "account.open_original_page": "Open original page",
 | 
				
			||||||
  "account.posts": "Posts",
 | 
					  "account.posts": "Posts",
 | 
				
			||||||
| 
						 | 
					@ -65,7 +67,7 @@
 | 
				
			||||||
  "account.unendorse": "Don't feature on profile",
 | 
					  "account.unendorse": "Don't feature on profile",
 | 
				
			||||||
  "account.unfollow": "Unfollow",
 | 
					  "account.unfollow": "Unfollow",
 | 
				
			||||||
  "account.unmute": "Unmute @{name}",
 | 
					  "account.unmute": "Unmute @{name}",
 | 
				
			||||||
  "account.unmute_notifications": "Unmute notifications from @{name}",
 | 
					  "account.unmute_notifications_short": "Unmute notifications",
 | 
				
			||||||
  "account.unmute_short": "Unmute",
 | 
					  "account.unmute_short": "Unmute",
 | 
				
			||||||
  "account_note.placeholder": "Click to add note",
 | 
					  "account_note.placeholder": "Click to add note",
 | 
				
			||||||
  "admin.dashboard.daily_retention": "User retention rate by day after sign-up",
 | 
					  "admin.dashboard.daily_retention": "User retention rate by day after sign-up",
 | 
				
			||||||
| 
						 | 
					@ -530,6 +532,8 @@
 | 
				
			||||||
  "report.placeholder": "Additional comments",
 | 
					  "report.placeholder": "Additional comments",
 | 
				
			||||||
  "report.reasons.dislike": "I don't like it",
 | 
					  "report.reasons.dislike": "I don't like it",
 | 
				
			||||||
  "report.reasons.dislike_description": "It is not something you want to see",
 | 
					  "report.reasons.dislike_description": "It is not something you want to see",
 | 
				
			||||||
 | 
					  "report.reasons.legal": "It's illegal",
 | 
				
			||||||
 | 
					  "report.reasons.legal_description": "You believe it violates the law of your or the server's country",
 | 
				
			||||||
  "report.reasons.other": "It's something else",
 | 
					  "report.reasons.other": "It's something else",
 | 
				
			||||||
  "report.reasons.other_description": "The issue does not fit into other categories",
 | 
					  "report.reasons.other_description": "The issue does not fit into other categories",
 | 
				
			||||||
  "report.reasons.spam": "It's spam",
 | 
					  "report.reasons.spam": "It's spam",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1526,6 +1526,7 @@ body > [data-popper-placement] {
 | 
				
			||||||
.account__wrapper {
 | 
					.account__wrapper {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 10px;
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.account__avatar {
 | 
					.account__avatar {
 | 
				
			||||||
| 
						 | 
					@ -1594,108 +1595,10 @@ a .account__avatar {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.account__relationship {
 | 
					.account__relationship {
 | 
				
			||||||
  height: 18px;
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  white-space: nowrap;
 | 
					  white-space: nowrap;
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.account__disclaimer {
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  border-top: 1px solid lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
  color: $dark-text-color;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  strong {
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @each $lang in $cjk-langs {
 | 
					 | 
				
			||||||
      &:lang(#{$lang}) {
 | 
					 | 
				
			||||||
        font-weight: 700;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  a {
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    color: inherit;
 | 
					 | 
				
			||||||
    text-decoration: underline;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:hover,
 | 
					 | 
				
			||||||
    &:focus,
 | 
					 | 
				
			||||||
    &:active {
 | 
					 | 
				
			||||||
      text-decoration: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.account__action-bar {
 | 
					 | 
				
			||||||
  border-top: 1px solid lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
  border-bottom: 1px solid lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
  line-height: 36px;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  flex: 0 0 auto;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
}
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 4px;
 | 
				
			||||||
.account__action-bar-dropdown {
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .icon-button {
 | 
					 | 
				
			||||||
    vertical-align: middle;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .dropdown--active {
 | 
					 | 
				
			||||||
    .dropdown__content.dropdown__right {
 | 
					 | 
				
			||||||
      inset-inline-start: 6px;
 | 
					 | 
				
			||||||
      inset-inline-end: initial;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &::after {
 | 
					 | 
				
			||||||
      bottom: initial;
 | 
					 | 
				
			||||||
      margin-inline-start: 11px;
 | 
					 | 
				
			||||||
      margin-top: -7px;
 | 
					 | 
				
			||||||
      inset-inline-end: initial;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.account__action-bar-links {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex: 1 1 auto;
 | 
					 | 
				
			||||||
  line-height: 18px;
 | 
					 | 
				
			||||||
  text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.account__action-bar__tab {
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  flex: 0 1 100%;
 | 
					 | 
				
			||||||
  border-inline-end: 1px solid lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
  padding: 10px 0;
 | 
					 | 
				
			||||||
  border-bottom: 4px solid transparent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.active {
 | 
					 | 
				
			||||||
    border-bottom: 4px solid $ui-highlight-color;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  & > span {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    text-transform: uppercase;
 | 
					 | 
				
			||||||
    font-size: 11px;
 | 
					 | 
				
			||||||
    color: $darker-text-color;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  strong {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    font-size: 15px;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    color: $primary-text-color;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @each $lang in $cjk-langs {
 | 
					 | 
				
			||||||
      &:lang(#{$lang}) {
 | 
					 | 
				
			||||||
        font-weight: 700;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.account-authorize {
 | 
					.account-authorize {
 | 
				
			||||||
| 
						 | 
					@ -2049,36 +1952,18 @@ a.account__display-name {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-animation {
 | 
					.dropdown-animation {
 | 
				
			||||||
  animation: dropdown 300ms cubic-bezier(0.1, 0.7, 0.1, 1);
 | 
					  animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @keyframes dropdown {
 | 
					  @keyframes dropdown {
 | 
				
			||||||
    from {
 | 
					    from {
 | 
				
			||||||
      opacity: 0;
 | 
					      opacity: 0;
 | 
				
			||||||
      transform: scaleX(0.85) scaleY(0.75);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    to {
 | 
					    to {
 | 
				
			||||||
      opacity: 1;
 | 
					      opacity: 1;
 | 
				
			||||||
      transform: scaleX(1) scaleY(1);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.top {
 | 
					 | 
				
			||||||
    transform-origin: bottom;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.right {
 | 
					 | 
				
			||||||
    transform-origin: left;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.bottom {
 | 
					 | 
				
			||||||
    transform-origin: top;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.left {
 | 
					 | 
				
			||||||
    transform-origin: right;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .reduce-motion & {
 | 
					  .reduce-motion & {
 | 
				
			||||||
    animation: none;
 | 
					    animation: none;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -2094,16 +1979,17 @@ a.account__display-name {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu__separator {
 | 
					.dropdown-menu__separator {
 | 
				
			||||||
  border-bottom: 1px solid darken($ui-secondary-color, 8%);
 | 
					  border-bottom: 1px solid var(--dropdown-border-color);
 | 
				
			||||||
  margin: 5px 7px 6px;
 | 
					  margin: 5px 0;
 | 
				
			||||||
  height: 0;
 | 
					  height: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu {
 | 
					.dropdown-menu {
 | 
				
			||||||
  background: $ui-secondary-color;
 | 
					  background: var(--dropdown-background-color);
 | 
				
			||||||
  padding: 4px 0;
 | 
					  border: 1px solid var(--dropdown-border-color);
 | 
				
			||||||
 | 
					  padding: 4px;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
					  box-shadow: var(--dropdown-shadow);
 | 
				
			||||||
  z-index: 9999;
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__text-button {
 | 
					  &__text-button {
 | 
				
			||||||
| 
						 | 
					@ -2124,12 +2010,13 @@ a.account__display-name {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__container {
 | 
					  &__container {
 | 
				
			||||||
    &__header {
 | 
					    &__header {
 | 
				
			||||||
      border-bottom: 1px solid darken($ui-secondary-color, 8%);
 | 
					      border-bottom: 1px solid var(--dropdown-border-color);
 | 
				
			||||||
      padding: 4px 14px;
 | 
					      padding: 10px 14px;
 | 
				
			||||||
      padding-bottom: 8px;
 | 
					      padding-bottom: 14px;
 | 
				
			||||||
 | 
					      margin-bottom: 4px;
 | 
				
			||||||
      font-size: 13px;
 | 
					      font-size: 13px;
 | 
				
			||||||
      line-height: 18px;
 | 
					      line-height: 18px;
 | 
				
			||||||
      color: $inverted-text-color;
 | 
					      color: $darker-text-color;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__list {
 | 
					    &__list {
 | 
				
			||||||
| 
						 | 
					@ -2166,103 +2053,43 @@ a.account__display-name {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu__arrow {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &::before {
 | 
					 | 
				
			||||||
    content: '';
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    width: 14px;
 | 
					 | 
				
			||||||
    height: 5px;
 | 
					 | 
				
			||||||
    background-color: $ui-secondary-color;
 | 
					 | 
				
			||||||
    mask-image: url("data:image/svg+xml;utf8,<svg width='14' height='5' xmlns='http://www.w3.org/2000/svg'><path d='M7 0L0 5h14L7 0z' fill='white'/></svg>");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.top {
 | 
					 | 
				
			||||||
    bottom: -5px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &::before {
 | 
					 | 
				
			||||||
      transform: rotate(180deg);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.right {
 | 
					 | 
				
			||||||
    inset-inline-start: -9px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &::before {
 | 
					 | 
				
			||||||
      transform: rotate(-90deg);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.bottom {
 | 
					 | 
				
			||||||
    top: -5px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.left {
 | 
					 | 
				
			||||||
    inset-inline-end: -9px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &::before {
 | 
					 | 
				
			||||||
      transform: rotate(90deg);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown-menu__item {
 | 
					.dropdown-menu__item {
 | 
				
			||||||
  font-size: 13px;
 | 
					  font-size: 13px;
 | 
				
			||||||
  line-height: 18px;
 | 
					  line-height: 18px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  color: $inverted-text-color;
 | 
					
 | 
				
			||||||
 | 
					  &--dangerous {
 | 
				
			||||||
 | 
					    color: $error-value-color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  a,
 | 
					  a,
 | 
				
			||||||
  button {
 | 
					  button {
 | 
				
			||||||
    font-family: inherit;
 | 
					    font: inherit;
 | 
				
			||||||
    font-size: inherit;
 | 
					 | 
				
			||||||
    line-height: inherit;
 | 
					 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    padding: 4px 14px;
 | 
					    padding: 10px 14px;
 | 
				
			||||||
    border: 0;
 | 
					    border: 0;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    background: transparent;
 | 
				
			||||||
    box-sizing: border-box;
 | 
					    box-sizing: border-box;
 | 
				
			||||||
    text-decoration: none;
 | 
					    text-decoration: none;
 | 
				
			||||||
    background: $ui-secondary-color;
 | 
					 | 
				
			||||||
    color: inherit;
 | 
					    color: inherit;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
    text-overflow: ellipsis;
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
    white-space: nowrap;
 | 
					    white-space: nowrap;
 | 
				
			||||||
    text-align: inherit;
 | 
					    text-align: inherit;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:focus,
 | 
					    &:focus,
 | 
				
			||||||
    &:hover,
 | 
					    &:hover,
 | 
				
			||||||
    &:active {
 | 
					    &:active {
 | 
				
			||||||
      background: $ui-highlight-color;
 | 
					      background: var(--dropdown-border-color);
 | 
				
			||||||
      color: $secondary-text-color;
 | 
					 | 
				
			||||||
      outline: 0;
 | 
					      outline: 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu__item--text {
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  text-overflow: ellipsis;
 | 
					 | 
				
			||||||
  white-space: nowrap;
 | 
					 | 
				
			||||||
  padding: 4px 14px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown-menu__item.edited-timestamp__history__item {
 | 
					 | 
				
			||||||
  border-bottom: 1px solid darken($ui-secondary-color, 8%);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:last-child {
 | 
					 | 
				
			||||||
    border-bottom: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.dropdown-menu__item--text,
 | 
					 | 
				
			||||||
  a,
 | 
					 | 
				
			||||||
  button {
 | 
					 | 
				
			||||||
    padding: 8px 14px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.inline-account {
 | 
					.inline-account {
 | 
				
			||||||
  display: inline-flex;
 | 
					  display: inline-flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
| 
						 | 
					@ -2278,62 +2105,6 @@ a.account__display-name {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown--active .dropdown__content {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  line-height: 18px;
 | 
					 | 
				
			||||||
  max-width: 311px;
 | 
					 | 
				
			||||||
  inset-inline-end: 0;
 | 
					 | 
				
			||||||
  text-align: start;
 | 
					 | 
				
			||||||
  z-index: 9999;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  & > ul {
 | 
					 | 
				
			||||||
    list-style: none;
 | 
					 | 
				
			||||||
    background: $ui-secondary-color;
 | 
					 | 
				
			||||||
    padding: 4px 0;
 | 
					 | 
				
			||||||
    border-radius: 4px;
 | 
					 | 
				
			||||||
    box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
 | 
					 | 
				
			||||||
    min-width: 140px;
 | 
					 | 
				
			||||||
    position: relative;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.dropdown__right {
 | 
					 | 
				
			||||||
    inset-inline-end: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.dropdown__left {
 | 
					 | 
				
			||||||
    & > ul {
 | 
					 | 
				
			||||||
      inset-inline-start: -98px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  & > ul > li > a {
 | 
					 | 
				
			||||||
    font-size: 13px;
 | 
					 | 
				
			||||||
    line-height: 18px;
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    padding: 4px 14px;
 | 
					 | 
				
			||||||
    box-sizing: border-box;
 | 
					 | 
				
			||||||
    text-decoration: none;
 | 
					 | 
				
			||||||
    background: $ui-secondary-color;
 | 
					 | 
				
			||||||
    color: $inverted-text-color;
 | 
					 | 
				
			||||||
    overflow: hidden;
 | 
					 | 
				
			||||||
    text-overflow: ellipsis;
 | 
					 | 
				
			||||||
    white-space: nowrap;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:focus {
 | 
					 | 
				
			||||||
      outline: 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:hover {
 | 
					 | 
				
			||||||
      background: $ui-highlight-color;
 | 
					 | 
				
			||||||
      color: $secondary-text-color;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown__icon {
 | 
					 | 
				
			||||||
  vertical-align: middle;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.columns-area {
 | 
					.columns-area {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex: 1 1 auto;
 | 
					  flex: 1 1 auto;
 | 
				
			||||||
| 
						 | 
					@ -3111,10 +2882,10 @@ $ui-header-height: 55px;
 | 
				
			||||||
.compose-form__highlightable {
 | 
					.compose-form__highlightable {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  flex: 0 1 auto;
 | 
					  flex: 0 1 auto;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  transition: box-shadow 300ms linear;
 | 
					  transition: box-shadow 300ms linear;
 | 
				
			||||||
 | 
					  min-height: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.active {
 | 
					  &.active {
 | 
				
			||||||
    transition: none;
 | 
					    transition: none;
 | 
				
			||||||
| 
						 | 
					@ -3156,7 +2927,6 @@ $ui-header-height: 55px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .compose-form {
 | 
					  .compose-form {
 | 
				
			||||||
    flex: 1;
 | 
					    flex: 1;
 | 
				
			||||||
    overflow-y: hidden;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    min-height: 310px;
 | 
					    min-height: 310px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,3 +61,10 @@ $no-gap-breakpoint: 1175px;
 | 
				
			||||||
$font-sans-serif: 'mastodon-font-sans-serif' !default;
 | 
					$font-sans-serif: 'mastodon-font-sans-serif' !default;
 | 
				
			||||||
$font-display: 'mastodon-font-display' !default;
 | 
					$font-display: 'mastodon-font-display' !default;
 | 
				
			||||||
$font-monospace: 'mastodon-font-monospace' !default;
 | 
					$font-monospace: 'mastodon-font-monospace' !default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:root {
 | 
				
			||||||
 | 
					  --dropdown-border-color: #{lighten($ui-base-color, 12%)};
 | 
				
			||||||
 | 
					  --dropdown-background-color: #{lighten($ui-base-color, 4%)};
 | 
				
			||||||
 | 
					  --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
 | 
				
			||||||
 | 
					    0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -25,33 +27,25 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure
 | 
				
			||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    account_matching_sql = begin
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }]
 | 
				
			||||||
      if params[:include_subdomains]
 | 
					  end
 | 
				
			||||||
        "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))"
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        'accounts.domain = $3::text'
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_accounts AS (
 | 
					        WITH new_accounts AS (
 | 
				
			||||||
          SELECT accounts.id
 | 
					          SELECT accounts.id
 | 
				
			||||||
          FROM accounts
 | 
					          FROM accounts
 | 
				
			||||||
          WHERE date_trunc('day', accounts.created_at)::date = axis.period
 | 
					          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
 | 
					        SELECT count(*) FROM new_accounts
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def time_period
 | 
					  def time_period
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur
 | 
				
			||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    account_matching_sql = begin
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }]
 | 
				
			||||||
      if params[:include_subdomains]
 | 
					  end
 | 
				
			||||||
        "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))"
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        'accounts.domain = $3::text'
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_followers AS (
 | 
					        WITH new_followers AS (
 | 
				
			||||||
          SELECT follows.id
 | 
					          SELECT follows.id
 | 
				
			||||||
          FROM follows
 | 
					          FROM follows
 | 
				
			||||||
          INNER JOIN accounts ON follows.account_id = accounts.id
 | 
					          INNER JOIN accounts ON follows.account_id = accounts.id
 | 
				
			||||||
          WHERE date_trunc('day', follows.created_at)::date = axis.period
 | 
					          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
 | 
					        SELECT count(*) FROM new_followers
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def time_period
 | 
					  def time_period
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure:
 | 
				
			||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    account_matching_sql = begin
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }]
 | 
				
			||||||
      if params[:include_subdomains]
 | 
					  end
 | 
				
			||||||
        "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))"
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        'accounts.domain = $3::text'
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_follows AS (
 | 
					        WITH new_follows AS (
 | 
				
			||||||
          SELECT follows.id
 | 
					          SELECT follows.id
 | 
				
			||||||
          FROM follows
 | 
					          FROM follows
 | 
				
			||||||
          INNER JOIN accounts ON follows.target_account_id = accounts.id
 | 
					          INNER JOIN accounts ON follows.target_account_id = accounts.id
 | 
				
			||||||
          WHERE date_trunc('day', follows.created_at)::date = axis.period
 | 
					          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
 | 
					        SELECT count(*) FROM new_follows
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def time_period
 | 
					  def time_period
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
  include ActionView::Helpers::NumberHelper
 | 
					  include ActionView::Helpers::NumberHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
| 
						 | 
					@ -35,34 +36,26 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
 | 
				
			||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    account_matching_sql = begin
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }]
 | 
				
			||||||
      if params[:include_subdomains]
 | 
					  end
 | 
				
			||||||
        "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))"
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        'accounts.domain = $3::text'
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_media_attachments AS (
 | 
					        WITH new_media_attachments AS (
 | 
				
			||||||
          SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size
 | 
					          SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size
 | 
				
			||||||
          FROM media_attachments
 | 
					          FROM media_attachments
 | 
				
			||||||
          INNER JOIN accounts ON accounts.id = media_attachments.account_id
 | 
					          INNER JOIN accounts ON accounts.id = media_attachments.account_id
 | 
				
			||||||
          WHERE date_trunc('day', media_attachments.created_at)::date = axis.period
 | 
					          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
 | 
					        SELECT SUM(size) FROM new_media_attachments
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def time_period
 | 
					  def time_period
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure:
 | 
				
			||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    account_matching_sql = begin
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }]
 | 
				
			||||||
      if params[:include_subdomains]
 | 
					  end
 | 
				
			||||||
        "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))"
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        'accounts.domain = $3::text'
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_reports AS (
 | 
					        WITH new_reports AS (
 | 
				
			||||||
          SELECT reports.id
 | 
					          SELECT reports.id
 | 
				
			||||||
          FROM reports
 | 
					          FROM reports
 | 
				
			||||||
          INNER JOIN accounts ON accounts.id = reports.target_account_id
 | 
					          INNER JOIN accounts ON accounts.id = reports.target_account_id
 | 
				
			||||||
          WHERE date_trunc('day', reports.created_at)::date = axis.period
 | 
					          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
 | 
					        SELECT count(*) FROM new_reports
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def time_period
 | 
					  def time_period
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -25,35 +27,35 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure
 | 
				
			||||||
    nil
 | 
					    nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    account_matching_sql = begin
 | 
					    [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 }]
 | 
				
			||||||
      if params[:include_subdomains]
 | 
					  end
 | 
				
			||||||
        "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $5::text))"
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        'accounts.domain = $5::text'
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_statuses AS (
 | 
					        WITH new_statuses AS (
 | 
				
			||||||
          SELECT statuses.id
 | 
					          SELECT statuses.id
 | 
				
			||||||
          FROM statuses
 | 
					          FROM statuses
 | 
				
			||||||
          INNER JOIN accounts ON accounts.id = statuses.account_id
 | 
					          INNER JOIN accounts ON accounts.id = statuses.account_id
 | 
				
			||||||
          WHERE statuses.id BETWEEN $3 AND $4
 | 
					          WHERE statuses.id BETWEEN :earliest_status_id AND :latest_status_id
 | 
				
			||||||
            AND #{account_matching_sql}
 | 
					            AND #{account_domain_sql(params[:include_subdomains])}
 | 
				
			||||||
            AND date_trunc('day', statuses.created_at)::date = axis.period
 | 
					            AND date_trunc('day', statuses.created_at)::date = axis.period
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        SELECT count(*) FROM new_statuses
 | 
					        SELECT count(*) FROM new_statuses
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def time_period
 | 
					  def time_period
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def key
 | 
					  def key
 | 
				
			||||||
    'new_users'
 | 
					    'new_users'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -15,8 +17,12 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe
 | 
				
			||||||
    User.where(created_at: previous_time_period).count
 | 
					    User.where(created_at: previous_time_period).count
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at }]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_users AS (
 | 
					        WITH new_users AS (
 | 
				
			||||||
          SELECT users.id
 | 
					          SELECT users.id
 | 
				
			||||||
| 
						 | 
					@ -26,12 +32,8 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe
 | 
				
			||||||
        SELECT count(*) FROM new_users
 | 
					        SELECT count(*) FROM new_users
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def key
 | 
					  def key
 | 
				
			||||||
    'opened_reports'
 | 
					    'opened_reports'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -15,8 +17,12 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B
 | 
				
			||||||
    Report.where(created_at: previous_time_period).count
 | 
					    Report.where(created_at: previous_time_period).count
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at }]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH new_reports AS (
 | 
					        WITH new_reports AS (
 | 
				
			||||||
          SELECT reports.id
 | 
					          SELECT reports.id
 | 
				
			||||||
| 
						 | 
					@ -26,12 +32,8 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B
 | 
				
			||||||
        SELECT count(*) FROM new_reports
 | 
					        SELECT count(*) FROM new_reports
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										25
									
								
								app/lib/admin/metrics/measure/query_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/lib/admin/metrics/measure/query_helper.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def key
 | 
					  def key
 | 
				
			||||||
    'resolved_reports'
 | 
					    'resolved_reports'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -15,8 +17,12 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
 | 
				
			||||||
    Report.resolved.where(action_taken_at: previous_time_period).count
 | 
					    Report.resolved.where(action_taken_at: previous_time_period).count
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					    [sql_query_string, { start_at: @start_at, end_at: @end_at }]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def sql_query_string
 | 
				
			||||||
 | 
					    <<~SQL.squish
 | 
				
			||||||
      SELECT axis.*, (
 | 
					      SELECT axis.*, (
 | 
				
			||||||
        WITH resolved_reports AS (
 | 
					        WITH resolved_reports AS (
 | 
				
			||||||
          SELECT reports.id
 | 
					          SELECT reports.id
 | 
				
			||||||
| 
						 | 
					@ -26,12 +32,8 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
 | 
				
			||||||
        SELECT count(*) FROM resolved_reports
 | 
					        SELECT count(*) FROM resolved_reports
 | 
				
			||||||
      ) AS value
 | 
					      ) AS value
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) AS axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
					class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::BaseMeasure
 | 
				
			||||||
 | 
					  include Admin::Metrics::Measure::QueryHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.with_params?
 | 
					  def self.with_params?
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  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')
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform_data_query
 | 
					  def sql_array
 | 
				
			||||||
    sql = <<-SQL.squish
 | 
					    [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 axis.*, (
 | 
				
			||||||
        SELECT count(distinct accounts.domain) AS value
 | 
					        SELECT count(distinct accounts.domain) AS value
 | 
				
			||||||
        FROM statuses
 | 
					        FROM statuses
 | 
				
			||||||
        INNER JOIN statuses_tags ON statuses.id = statuses_tags.status_id
 | 
					        INNER JOIN statuses_tags ON statuses.id = statuses_tags.status_id
 | 
				
			||||||
        INNER JOIN accounts ON statuses.account_id = accounts.id
 | 
					        INNER JOIN accounts ON statuses.account_id = accounts.id
 | 
				
			||||||
        WHERE statuses_tags.tag_id = $1
 | 
					        WHERE statuses_tags.tag_id = :tag_id
 | 
				
			||||||
          AND statuses.id BETWEEN $2 AND $3
 | 
					          AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id
 | 
				
			||||||
          AND date_trunc('day', statuses.created_at)::date = axis.day
 | 
					          AND date_trunc('day', statuses.created_at)::date = axis.day
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      FROM (
 | 
					      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
 | 
					      ) as axis
 | 
				
			||||||
    SQL
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def tag
 | 
					  def tag
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,15 +7,15 @@ class LinkDetailsExtractor
 | 
				
			||||||
  # Some publications wrap their JSON-LD data in their <script> tags
 | 
					  # Some publications wrap their JSON-LD data in their <script> tags
 | 
				
			||||||
  # in commented-out CDATA blocks, they need to be removed before
 | 
					  # in commented-out CDATA blocks, they need to be removed before
 | 
				
			||||||
  # attempting to parse JSON
 | 
					  # attempting to parse JSON
 | 
				
			||||||
  CDATA_JUNK_PATTERN = %r{^[\s]*(
 | 
					  CDATA_JUNK_PATTERN = %r{^\s*(
 | 
				
			||||||
    (/\*[\s]*<!\[CDATA\[[\s]*\*/) # Block comment style opening
 | 
					    (/\*\s*<!\[CDATA\[\s*\*/) # Block comment style opening
 | 
				
			||||||
    |
 | 
					    |
 | 
				
			||||||
    (//[\s]*<!\[CDATA\[) # Single-line comment style opening
 | 
					    (//\s*<!\[CDATA\[) # Single-line comment style opening
 | 
				
			||||||
    |
 | 
					    |
 | 
				
			||||||
    (/\*[\s]*\]\]>[\s]*\*/) # Block comment style closing
 | 
					    (/\*\s*\]\]>\s*\*/) # Block comment style closing
 | 
				
			||||||
    |
 | 
					    |
 | 
				
			||||||
    (//[\s]*\]\]>) # Single-line comment style closing
 | 
					    (//\s*\]\]>) # Single-line comment style closing
 | 
				
			||||||
  )[\s]*$}x
 | 
					  )\s*$}x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  class StructuredData
 | 
					  class StructuredData
 | 
				
			||||||
    SUPPORTED_TYPES = %w(
 | 
					    SUPPORTED_TYPES = %w(
 | 
				
			||||||
| 
						 | 
					@ -204,7 +204,7 @@ class LinkDetailsExtractor
 | 
				
			||||||
  def host_to_url(str)
 | 
					  def host_to_url(str)
 | 
				
			||||||
    return if str.blank?
 | 
					    return if str.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    str.start_with?(/https?:\/\//) ? str : "http://#{str}"
 | 
					    str.start_with?(%r{https?://}) ? str : "http://#{str}"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def valid_url_or_nil(str, same_origin_only: false)
 | 
					  def valid_url_or_nil(str, same_origin_only: false)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
class PlainTextFormatter
 | 
					class PlainTextFormatter
 | 
				
			||||||
  include ActionView::Helpers::TextHelper
 | 
					  include ActionView::Helpers::TextHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NEWLINE_TAGS_RE = /(<br \/>|<br>|<\/p>)+/
 | 
					  NEWLINE_TAGS_RE = %r{(<br />|<br>|</p>)+}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attr_reader :text, :local
 | 
					  attr_reader :text, :local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,18 +7,18 @@ class TagManager
 | 
				
			||||||
  include RoutingHelper
 | 
					  include RoutingHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def web_domain?(domain)
 | 
					  def web_domain?(domain)
 | 
				
			||||||
    domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
 | 
					    domain.nil? || domain.delete('/').casecmp(Rails.configuration.x.web_domain).zero?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def local_domain?(domain)
 | 
					  def local_domain?(domain)
 | 
				
			||||||
    domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
 | 
					    domain.nil? || domain.delete('/').casecmp(Rails.configuration.x.local_domain).zero?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def normalize_domain(domain)
 | 
					  def normalize_domain(domain)
 | 
				
			||||||
    return if domain.nil?
 | 
					    return if domain.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    uri = Addressable::URI.new
 | 
					    uri = Addressable::URI.new
 | 
				
			||||||
    uri.host = domain.gsub(/[\/]/, '')
 | 
					    uri.host = domain.delete('/')
 | 
				
			||||||
    uri.normalized_host
 | 
					    uri.normalized_host
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ class TextFormatter
 | 
				
			||||||
  include ERB::Util
 | 
					  include ERB::Util
 | 
				
			||||||
  include RoutingHelper
 | 
					  include RoutingHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  URL_PREFIX_REGEX = /\A(https?:\/\/(www\.)?|xmpp:)/
 | 
					  URL_PREFIX_REGEX = %r{\A(https?://(www\.)?|xmpp:)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  DEFAULT_REL = %w(nofollow noopener noreferrer).freeze
 | 
					  DEFAULT_REL = %w(nofollow noopener noreferrer).freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ class WebfingerResource
 | 
				
			||||||
    case resource
 | 
					    case resource
 | 
				
			||||||
    when /\Ahttps?/i
 | 
					    when /\Ahttps?/i
 | 
				
			||||||
      username_from_url
 | 
					      username_from_url
 | 
				
			||||||
    when /\@/
 | 
					    when /@/
 | 
				
			||||||
      username_from_acct
 | 
					      username_from_acct
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      raise InvalidRequest
 | 
					      raise InvalidRequest
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										67
									
								
								app/lib/webhooks/payload_renderer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								app/lib/webhooks/payload_renderer.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Webhooks::PayloadRenderer
 | 
				
			||||||
 | 
					  class DocumentTraverser
 | 
				
			||||||
 | 
					    INT_REGEX = /[0-9]+/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def initialize(document)
 | 
				
			||||||
 | 
					      @document = document.with_indifferent_access
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(path)
 | 
				
			||||||
 | 
					      value  = @document.dig(*parse_path(path))
 | 
				
			||||||
 | 
					      string = Oj.dump(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # We want to make sure people can use the variable inside
 | 
				
			||||||
 | 
					      # other strings, so it can't be wrapped in quotes.
 | 
				
			||||||
 | 
					      if value.is_a?(String)
 | 
				
			||||||
 | 
					        string[1...-1]
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        string
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse_path(path)
 | 
				
			||||||
 | 
					      path.split('.').filter_map do |segment|
 | 
				
			||||||
 | 
					        if segment.match(INT_REGEX)
 | 
				
			||||||
 | 
					          segment.to_i
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          segment.presence
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  class TemplateParser < Parslet::Parser
 | 
				
			||||||
 | 
					    rule(:dot) { str('.') }
 | 
				
			||||||
 | 
					    rule(:digit) { match('[0-9]') }
 | 
				
			||||||
 | 
					    rule(:property_name) { match('[a-z_]').repeat(1) }
 | 
				
			||||||
 | 
					    rule(:array_index) { digit.repeat(1) }
 | 
				
			||||||
 | 
					    rule(:segment) { (property_name | array_index) }
 | 
				
			||||||
 | 
					    rule(:path) { property_name >> (dot >> segment).repeat }
 | 
				
			||||||
 | 
					    rule(:variable) { (str('}}').absent? >> path).repeat.as(:variable) }
 | 
				
			||||||
 | 
					    rule(:expression) { str('{{') >> variable >> str('}}') }
 | 
				
			||||||
 | 
					    rule(:text) { (str('{{').absent? >> any).repeat(1) }
 | 
				
			||||||
 | 
					    rule(:text_with_expressions) { (text.as(:text) | expression).repeat.as(:text) }
 | 
				
			||||||
 | 
					    root(:text_with_expressions)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  EXPRESSION_REGEXP = /
 | 
				
			||||||
 | 
					    \{\{
 | 
				
			||||||
 | 
					      [a-z_]+
 | 
				
			||||||
 | 
					      (\.
 | 
				
			||||||
 | 
					        ([a-z_]+|[0-9]+)
 | 
				
			||||||
 | 
					      )*
 | 
				
			||||||
 | 
					    \}\}
 | 
				
			||||||
 | 
					  /iox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize(json)
 | 
				
			||||||
 | 
					    @document = DocumentTraverser.new(Oj.load(json))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def render(template)
 | 
				
			||||||
 | 
					    template.gsub(EXPRESSION_REGEXP) { |match| @document.get(match[2...-2]) }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -62,9 +62,9 @@ class Account < ApplicationRecord
 | 
				
			||||||
    trust_level
 | 
					    trust_level
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  USERNAME_RE   = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
 | 
					  USERNAME_RE   = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
 | 
				
			||||||
  MENTION_RE    = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
 | 
					  MENTION_RE    = %r{(?<=^|[^/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
 | 
				
			||||||
  URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
 | 
					  URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
 | 
				
			||||||
  USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
 | 
					  USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  include Attachmentable
 | 
					  include Attachmentable
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ class DomainAllow < ApplicationRecord
 | 
				
			||||||
    def rule_for(domain)
 | 
					    def rule_for(domain)
 | 
				
			||||||
      return if domain.blank?
 | 
					      return if domain.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') }
 | 
					      uri = Addressable::URI.new.tap { |u| u.host = domain.delete('/') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      find_by(domain: uri.normalized_host)
 | 
					      find_by(domain: uri.normalized_host)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ class DomainBlock < ApplicationRecord
 | 
				
			||||||
    def rule_for(domain)
 | 
					    def rule_for(domain)
 | 
				
			||||||
      return if domain.blank?
 | 
					      return if domain.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      uri      = Addressable::URI.new.tap { |u| u.host = domain.strip.gsub(/[\/]/, '') }
 | 
					      uri      = Addressable::URI.new.tap { |u| u.host = domain.strip.delete('/') }
 | 
				
			||||||
      segments = uri.normalized_host.split('.')
 | 
					      segments = uri.normalized_host.split('.')
 | 
				
			||||||
      variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }
 | 
					      variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,6 +51,7 @@ class Report < ApplicationRecord
 | 
				
			||||||
  enum category: {
 | 
					  enum category: {
 | 
				
			||||||
    other: 0,
 | 
					    other: 0,
 | 
				
			||||||
    spam: 1_000,
 | 
					    spam: 1_000,
 | 
				
			||||||
 | 
					    legal: 1_500,
 | 
				
			||||||
    violation: 2_000,
 | 
					    violation: 2_000,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ class SiteUpload < ApplicationRecord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_attached_file :file, styles: ->(file) { STYLES[file.instance.var.to_sym] }, convert_options: { all: '-coalesce +profile "!icc,*" +set modify-date +set create-date' }, processors: [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
 | 
					  has_attached_file :file, styles: ->(file) { STYLES[file.instance.var.to_sym] }, convert_options: { all: '-coalesce +profile "!icc,*" +set modify-date +set create-date' }, processors: [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/
 | 
					  validates_attachment_content_type :file, content_type: %r{\Aimage/.*\z}
 | 
				
			||||||
  validates :file, presence: true
 | 
					  validates :file, presence: true
 | 
				
			||||||
  validates :var, presence: true, uniqueness: true
 | 
					  validates :var, presence: true, uniqueness: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ class Tag < ApplicationRecord
 | 
				
			||||||
  HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
 | 
					  HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
 | 
				
			||||||
  HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
 | 
					  HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_PAT})/i
 | 
					  HASHTAG_RE = %r{(?:^|[^/)\w])#(#{HASHTAG_NAME_PAT})}i
 | 
				
			||||||
  HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
 | 
					  HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
 | 
				
			||||||
  HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/
 | 
					  HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@
 | 
				
			||||||
#  enabled    :boolean          default(TRUE), not null
 | 
					#  enabled    :boolean          default(TRUE), not null
 | 
				
			||||||
#  created_at :datetime         not null
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
#  updated_at :datetime         not null
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  template   :text
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Webhook < ApplicationRecord
 | 
					class Webhook < ApplicationRecord
 | 
				
			||||||
| 
						 | 
					@ -30,6 +31,7 @@ class Webhook < ApplicationRecord
 | 
				
			||||||
  validates :events, presence: true
 | 
					  validates :events, presence: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validate :validate_events
 | 
					  validate :validate_events
 | 
				
			||||||
 | 
					  validate :validate_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_validation :strip_events
 | 
					  before_validation :strip_events
 | 
				
			||||||
  before_validation :generate_secret
 | 
					  before_validation :generate_secret
 | 
				
			||||||
| 
						 | 
					@ -49,7 +51,18 @@ class Webhook < ApplicationRecord
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def validate_events
 | 
					  def validate_events
 | 
				
			||||||
    errors.add(:events, :invalid) if events.any? { |e| !EVENTS.include?(e) }
 | 
					    errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def validate_template
 | 
				
			||||||
 | 
					    return if template.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    begin
 | 
				
			||||||
 | 
					      parser = Webhooks::PayloadRenderer::TemplateParser.new
 | 
				
			||||||
 | 
					      parser.parse(template)
 | 
				
			||||||
 | 
					    rescue Parslet::ParseFailed
 | 
				
			||||||
 | 
					      errors.add(:template, :invalid)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def strip_events
 | 
					  def strip_events
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,8 +77,8 @@ class BackupService < BaseService
 | 
				
			||||||
        path = m.file&.path
 | 
					        path = m.file&.path
 | 
				
			||||||
        next unless path
 | 
					        next unless path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        path = path.gsub(/\A.*\/system\//, '')
 | 
					        path = path.gsub(%r{\A.*/system/}, '')
 | 
				
			||||||
        path = path.gsub(/\A\/+/, '')
 | 
					        path = path.gsub(%r{\A/+}, '')
 | 
				
			||||||
        download_to_zip(zipfile, m.file, path)
 | 
					        download_to_zip(zipfile, m.file, path)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ class FetchLinkCardService < BaseService
 | 
				
			||||||
  URL_PATTERN = %r{
 | 
					  URL_PATTERN = %r{
 | 
				
			||||||
    (#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]})                                                                #   $1 preceding chars
 | 
					    (#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]})                                                                #   $1 preceding chars
 | 
				
			||||||
    (                                                                                                                           #   $2 URL
 | 
					    (                                                                                                                           #   $2 URL
 | 
				
			||||||
      (https?:\/\/)                                                                                                             #   $3 Protocol (required)
 | 
					      (https?://)                                                                                                               #   $3 Protocol (required)
 | 
				
			||||||
      (#{Twitter::TwitterText::Regex[:valid_domain]})                                                                           #   $4 Domain(s)
 | 
					      (#{Twitter::TwitterText::Regex[:valid_domain]})                                                                           #   $4 Domain(s)
 | 
				
			||||||
      (?::(#{Twitter::TwitterText::Regex[:valid_port_number]}))?                                                                #   $5 Port number (optional)
 | 
					      (?::(#{Twitter::TwitterText::Regex[:valid_port_number]}))?                                                                #   $5 Port number (optional)
 | 
				
			||||||
      (/#{Twitter::TwitterText::Regex[:valid_url_path]}*)?                                                                      #   $6 URL Path and anchor
 | 
					      (/#{Twitter::TwitterText::Regex[:valid_url_path]}*)?                                                                      #   $6 URL Path and anchor
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FetchOEmbedService
 | 
					class FetchOEmbedService
 | 
				
			||||||
  ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
 | 
					  ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
 | 
				
			||||||
  URL_REGEX                 = /(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i
 | 
					  URL_REGEX                 = %r{(=(https?(%3A|:)(//|%2F%2F)))([^&]*)}i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attr_reader :url, :options, :format, :endpoint_url
 | 
					  attr_reader :url, :options, :format, :endpoint_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,7 +70,7 @@ class SearchService < BaseService
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def url_query?
 | 
					  def url_query?
 | 
				
			||||||
    @resolve && /\Ahttps?:\/\//.match?(@query)
 | 
					    @resolve && %r{\Ahttps?://}.match?(@query)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def url_resource_results
 | 
					  def url_resource_results
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,5 +7,8 @@
 | 
				
			||||||
  .fields-group
 | 
					  .fields-group
 | 
				
			||||||
    = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 | 
					    = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .fields-group
 | 
				
			||||||
 | 
					    = f.input :template, wrapper: :with_block_label, input_html: { placeholder: '{ "content": "Hello {{object.username}}" }' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .actions
 | 
					  .actions
 | 
				
			||||||
    = f.button :button, @webhook.new_record? ? t('admin.webhooks.add_new') : t('generic.save_changes'), type: :submit
 | 
					    = f.button :button, @webhook.new_record? ? t('admin.webhooks.add_new') : t('generic.save_changes'), type: :submit
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,14 +2,14 @@
 | 
				
			||||||
  = t('admin.webhooks.title')
 | 
					  = t('admin.webhooks.title')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- content_for :heading do
 | 
					- content_for :heading do
 | 
				
			||||||
  %h2
 | 
					  .content__heading__row
 | 
				
			||||||
    %small
 | 
					    %h2
 | 
				
			||||||
      = fa_icon 'inbox'
 | 
					      %small
 | 
				
			||||||
      = t('admin.webhooks.webhook')
 | 
					        = fa_icon 'inbox'
 | 
				
			||||||
    = @webhook.url
 | 
					        = t('admin.webhooks.webhook')
 | 
				
			||||||
 | 
					      = @webhook.url
 | 
				
			||||||
- content_for :heading_actions do
 | 
					    .content__heading__actions
 | 
				
			||||||
  = link_to t('admin.webhooks.edit'), edit_admin_webhook_path, class: 'button' if can?(:update, @webhook)
 | 
					      = link_to t('admin.webhooks.edit'), edit_admin_webhook_path, class: 'button' if can?(:update, @webhook)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.table-wrapper
 | 
					.table-wrapper
 | 
				
			||||||
  %table.table.horizontal-table
 | 
					  %table.table.horizontal-table
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ class Webhooks::DeliveryWorker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(webhook_id, body)
 | 
					  def perform(webhook_id, body)
 | 
				
			||||||
    @webhook   = Webhook.find(webhook_id)
 | 
					    @webhook   = Webhook.find(webhook_id)
 | 
				
			||||||
    @body      = body
 | 
					    @body      = @webhook.template.blank? ? body : Webhooks::PayloadRenderer.new(body).render(@webhook.template)
 | 
				
			||||||
    @response  = nil
 | 
					    @response  = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    perform_request
 | 
					    perform_request
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,7 +79,7 @@ class Rack::Attack
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req|
 | 
					  throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req|
 | 
				
			||||||
    req.authenticated_user_id if req.post? && req.path.match?(/\A\/api\/v\d+\/media\z/i)
 | 
					    req.authenticated_user_id if req.post? && req.path.match?(%r{\A/api/v\d+/media\z}i)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('throttle_media_proxy', limit: 30, period: 10.minutes) do |req|
 | 
					  throttle('throttle_media_proxy', limit: 30, period: 10.minutes) do |req|
 | 
				
			||||||
| 
						 | 
					@ -98,8 +98,8 @@ class Rack::Attack
 | 
				
			||||||
    req.throttleable_remote_ip if req.paging_request? && req.unauthenticated?
 | 
					    req.throttleable_remote_ip if req.paging_request? && req.unauthenticated?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog\z/
 | 
					  API_DELETE_REBLOG_REGEX = %r{\A/api/v1/statuses/\d+/unreblog\z}
 | 
				
			||||||
  API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+\z/
 | 
					  API_DELETE_STATUS_REGEX = %r{\A/api/v1/statuses/\d+\z}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
 | 
					  throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
 | 
				
			||||||
    req.authenticated_user_id if (req.post? && req.path.match?(API_DELETE_REBLOG_REGEX)) || (req.delete? && req.path.match?(API_DELETE_STATUS_REGEX))
 | 
					    req.authenticated_user_id if (req.post? && req.path.match?(API_DELETE_REBLOG_REGEX)) || (req.delete? && req.path.match?(API_DELETE_STATUS_REGEX))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
StrongMigrations.start_after = 2017_09_24_022025
 | 
					StrongMigrations.start_after = 2017_09_24_022025
 | 
				
			||||||
 | 
					StrongMigrations.target_version = 10
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,8 +6,8 @@ module Twitter::TwitterText
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  class Regex
 | 
					  class Regex
 | 
				
			||||||
    REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou
 | 
					    REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>()?]/iou
 | 
				
			||||||
    REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
 | 
					    REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}()?!*"'「」<>;:=,.$%\[\]~&|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
 | 
				
			||||||
    REGEXEN[:valid_url_balanced_parens] = /
 | 
					    REGEXEN[:valid_url_balanced_parens] = /
 | 
				
			||||||
      \(
 | 
					      \(
 | 
				
			||||||
        (?:
 | 
					        (?:
 | 
				
			||||||
| 
						 | 
					@ -25,20 +25,20 @@ module Twitter::TwitterText
 | 
				
			||||||
      \)
 | 
					      \)
 | 
				
			||||||
    /iox
 | 
					    /iox
 | 
				
			||||||
    UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
 | 
					    UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
 | 
				
			||||||
    REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@\^#{UCHARS}]/iou
 | 
					    REGEXEN[:valid_url_query_chars] = %r{[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|@\^#{UCHARS}]}iou
 | 
				
			||||||
    REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
 | 
					    REGEXEN[:valid_url_query_ending_chars] = %r{[a-z0-9_&=#/\-#{UCHARS}]}iou
 | 
				
			||||||
    REGEXEN[:valid_url_path] = /(?:
 | 
					    REGEXEN[:valid_url_path] = %r{(?:
 | 
				
			||||||
      (?:
 | 
					      (?:
 | 
				
			||||||
        #{REGEXEN[:valid_general_url_path_chars]}*
 | 
					        #{REGEXEN[:valid_general_url_path_chars]}*
 | 
				
			||||||
        (?:#{REGEXEN[:valid_url_balanced_parens]} #{REGEXEN[:valid_general_url_path_chars]}*)*
 | 
					        (?:#{REGEXEN[:valid_url_balanced_parens]} #{REGEXEN[:valid_general_url_path_chars]}*)*
 | 
				
			||||||
        #{REGEXEN[:valid_url_path_ending_chars]}
 | 
					        #{REGEXEN[:valid_url_path_ending_chars]}
 | 
				
			||||||
      )|(?:#{REGEXEN[:valid_general_url_path_chars]}+\/)
 | 
					      )|(?:#{REGEXEN[:valid_general_url_path_chars]}+/)
 | 
				
			||||||
    )/iox
 | 
					    )}iox
 | 
				
			||||||
    REGEXEN[:valid_url] = %r{
 | 
					    REGEXEN[:valid_url] = %r{
 | 
				
			||||||
      (                                                                                     #   $1 total match
 | 
					      (                                                                                     #   $1 total match
 | 
				
			||||||
        (#{REGEXEN[:valid_url_preceding_chars]})                                            #   $2 Preceding character
 | 
					        (#{REGEXEN[:valid_url_preceding_chars]})                                            #   $2 Preceding character
 | 
				
			||||||
        (                                                                                   #   $3 URL
 | 
					        (                                                                                   #   $3 URL
 | 
				
			||||||
          ((?:https?|dat|dweb|ipfs|ipns|ssb|gopher|gemini):\/\/)?                           #   $4 Protocol (optional)
 | 
					          ((?:https?|dat|dweb|ipfs|ipns|ssb|gopher|gemini)://)?                             #   $4 Protocol (optional)
 | 
				
			||||||
          (#{REGEXEN[:valid_domain]})                                                       #   $5 Domain(s)
 | 
					          (#{REGEXEN[:valid_domain]})                                                       #   $5 Domain(s)
 | 
				
			||||||
          (?::(#{REGEXEN[:valid_port_number]}))?                                            #   $6 Port number (optional)
 | 
					          (?::(#{REGEXEN[:valid_port_number]}))?                                            #   $6 Port number (optional)
 | 
				
			||||||
          (/#{REGEXEN[:valid_url_path]}*)?                                                  #   $7 URL Path and anchor
 | 
					          (/#{REGEXEN[:valid_url_path]}*)?                                                  #   $7 URL Path and anchor
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,6 +131,7 @@ en:
 | 
				
			||||||
        position: Higher role decides conflict resolution in certain situations. Certain actions can only be performed on roles with a lower priority
 | 
					        position: Higher role decides conflict resolution in certain situations. Certain actions can only be performed on roles with a lower priority
 | 
				
			||||||
      webhook:
 | 
					      webhook:
 | 
				
			||||||
        events: Select events to send
 | 
					        events: Select events to send
 | 
				
			||||||
 | 
					        template: Compose your own JSON payload using variable interpolation. Leave blank for default JSON.
 | 
				
			||||||
        url: Where events will be sent to
 | 
					        url: Where events will be sent to
 | 
				
			||||||
    labels:
 | 
					    labels:
 | 
				
			||||||
      account:
 | 
					      account:
 | 
				
			||||||
| 
						 | 
					@ -304,6 +305,7 @@ en:
 | 
				
			||||||
        position: Priority
 | 
					        position: Priority
 | 
				
			||||||
      webhook:
 | 
					      webhook:
 | 
				
			||||||
        events: Enabled events
 | 
					        events: Enabled events
 | 
				
			||||||
 | 
					        template: Payload template
 | 
				
			||||||
        url: Endpoint URL
 | 
					        url: Endpoint URL
 | 
				
			||||||
    'no': 'No'
 | 
					    'no': 'No'
 | 
				
			||||||
    not_recommended: Not recommended
 | 
					    not_recommended: Not recommended
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -116,21 +116,21 @@ Rails.application.routes.draw do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get '/:encoded_at(*path)', to: redirect("/@%{path}"), constraints: { encoded_at: /%40/ }
 | 
					  get '/:encoded_at(*path)', to: redirect("/@%{path}"), constraints: { encoded_at: /%40/ }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constraints(username: /[^@\/.]+/) do
 | 
					  constraints(username: %r{[^@/.]+}) do
 | 
				
			||||||
    get '/@:username', to: 'accounts#show', as: :short_account
 | 
					    get '/@:username', to: 'accounts#show', as: :short_account
 | 
				
			||||||
    get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
 | 
					    get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
 | 
				
			||||||
    get '/@:username/media', to: 'accounts#show', as: :short_account_media
 | 
					    get '/@:username/media', to: 'accounts#show', as: :short_account_media
 | 
				
			||||||
    get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
 | 
					    get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constraints(account_username: /[^@\/.]+/) do
 | 
					  constraints(account_username: %r{[^@/.]+}) do
 | 
				
			||||||
    get '/@:account_username/following', to: 'following_accounts#index'
 | 
					    get '/@:account_username/following', to: 'following_accounts#index'
 | 
				
			||||||
    get '/@:account_username/followers', to: 'follower_accounts#index'
 | 
					    get '/@:account_username/followers', to: 'follower_accounts#index'
 | 
				
			||||||
    get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
 | 
					    get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
 | 
				
			||||||
    get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
 | 
					    get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false
 | 
					  get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, format: false
 | 
				
			||||||
  get '/settings', to: redirect('/settings/profile')
 | 
					  get '/settings', to: redirect('/settings/profile')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  draw(:settings)
 | 
					  draw(:settings)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								db/migrate/20230129023109_add_template_to_webhooks.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/migrate/20230129023109_add_template_to_webhooks.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddTemplateToWebhooks < ActiveRecord::Migration[6.1]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :webhooks, :template, :text
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,17 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require Rails.root.join('lib', 'mastodon', 'migration_helpers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AddExclusiveToLists < ActiveRecord::Migration[6.1]
 | 
					class AddExclusiveToLists < ActiveRecord::Migration[6.1]
 | 
				
			||||||
  def change
 | 
					  include Mastodon::MigrationHelpers
 | 
				
			||||||
    add_column :lists, :exclusive, :boolean, null: false, default: false
 | 
					
 | 
				
			||||||
 | 
					  disable_ddl_transaction!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def up
 | 
				
			||||||
 | 
					    safety_assured { add_column_with_default :lists, :exclusive, :boolean, default: false, allow_null: false }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def down
 | 
				
			||||||
 | 
					    remove_column :lists, :exclusive
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -567,7 +567,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
 | 
				
			||||||
    t.datetime "created_at", null: false
 | 
					    t.datetime "created_at", null: false
 | 
				
			||||||
    t.datetime "updated_at", null: false
 | 
					    t.datetime "updated_at", null: false
 | 
				
			||||||
    t.integer "replies_policy", default: 0, null: false
 | 
					    t.integer "replies_policy", default: 0, null: false
 | 
				
			||||||
    t.boolean "exclusive", default: false
 | 
					    t.boolean "exclusive", default: false, null: false
 | 
				
			||||||
    t.index ["account_id"], name: "index_lists_on_account_id"
 | 
					    t.index ["account_id"], name: "index_lists_on_account_id"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1139,6 +1139,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
 | 
				
			||||||
    t.boolean "enabled", default: true, null: false
 | 
					    t.boolean "enabled", default: true, null: false
 | 
				
			||||||
    t.datetime "created_at", precision: 6, null: false
 | 
					    t.datetime "created_at", precision: 6, null: false
 | 
				
			||||||
    t.datetime "updated_at", precision: 6, null: false
 | 
					    t.datetime "updated_at", precision: 6, null: false
 | 
				
			||||||
 | 
					    t.text "template"
 | 
				
			||||||
    t.index ["url"], name: "index_webhooks_on_url", unique: true
 | 
					    t.index ["url"], name: "index_webhooks_on_url", unique: true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ module PremailerWebpackStrategy
 | 
				
			||||||
            Rails.public_path.join(url).read
 | 
					            Rails.public_path.join(url).read
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    css.gsub(/url\(\//, "url(#{asset_host}/")
 | 
					    css.gsub(%r{url\(/}, "url(#{asset_host}/")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  module_function :load
 | 
					  module_function :load
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -171,7 +171,7 @@ module Paperclip
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def palette_from_histogram(result, quantity)
 | 
					    def palette_from_histogram(result, quantity)
 | 
				
			||||||
      frequencies       = result.scan(/([0-9]+)\:/).flatten.map(&:to_f)
 | 
					      frequencies       = result.scan(/([0-9]+):/).flatten.map(&:to_f)
 | 
				
			||||||
      hex_values        = result.scan(/\#([0-9A-Fa-f]{6,8})/).flatten
 | 
					      hex_values        = result.scan(/\#([0-9A-Fa-f]{6,8})/).flatten
 | 
				
			||||||
      total_frequencies = frequencies.sum.to_f
 | 
					      total_frequencies = frequencies.sum.to_f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ def gen_border(codepoint, color)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def codepoints_to_filename(codepoints)
 | 
					def codepoints_to_filename(codepoints)
 | 
				
			||||||
  codepoints.downcase.gsub(/\A[0]+/, '').tr(' ', '-')
 | 
					  codepoints.downcase.gsub(/\A0+/, '').tr(' ', '-')
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def codepoints_to_unicode(codepoints)
 | 
					def codepoints_to_unicode(codepoints)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ namespace :mastodon do
 | 
				
			||||||
      env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
 | 
					      env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
 | 
				
			||||||
        q.required true
 | 
					        q.required true
 | 
				
			||||||
        q.modify :strip
 | 
					        q.modify :strip
 | 
				
			||||||
        q.validate(/\A[a-z0-9\.\-]+\z/i)
 | 
					        q.validate(/\A[a-z0-9.-]+\z/i)
 | 
				
			||||||
        q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here'
 | 
					        q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here'
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -240,7 +240,7 @@ namespace :mastodon do
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
 | 
					          env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
 | 
				
			||||||
          env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '')
 | 
					          env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(%r{\Ahttps?://}, '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q|
 | 
					          env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q|
 | 
				
			||||||
            q.required true
 | 
					            q.required true
 | 
				
			||||||
| 
						 | 
					@ -269,7 +269,7 @@ namespace :mastodon do
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
 | 
					          env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
 | 
				
			||||||
          env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '')
 | 
					          env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(%r{\Ahttps?://}, '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          env['S3_BUCKET'] = prompt.ask('Storj DCS bucket name:') do |q|
 | 
					          env['S3_BUCKET'] = prompt.ask('Storj DCS bucket name:') do |q|
 | 
				
			||||||
            q.required true
 | 
					            q.required true
 | 
				
			||||||
| 
						 | 
					@ -573,7 +573,7 @@ def dotenv_escape(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # As long as the value doesn't include single quotes, we can safely
 | 
					  # As long as the value doesn't include single quotes, we can safely
 | 
				
			||||||
  # rely on single quotes
 | 
					  # rely on single quotes
 | 
				
			||||||
  return "'#{value}'" unless /[']/.match?(value)
 | 
					  return "'#{value}'" unless value.include?("'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # If the value contains the string '\n' or '\r' we simply can't use
 | 
					  # If the value contains the string '\n' or '\r' we simply can't use
 | 
				
			||||||
  # a double-quoted string, because Dotenv will expand \n or \r no
 | 
					  # a double-quoted string, because Dotenv will expand \n or \r no
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								spec/controllers/.rubocop.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/controllers/.rubocop.yml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					inherit_from: ../../.rubocop.yml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Anonymous controllers in specs cannot access described_class
 | 
				
			||||||
 | 
					# https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/cop/rspec/described_class.rb#L36-L39
 | 
				
			||||||
 | 
					RSpec/DescribedClass:
 | 
				
			||||||
 | 
					  SkipBlocks: true
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ RSpec.describe AccountsController do
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context do
 | 
					      context 'with a normal account in an HTML request' do
 | 
				
			||||||
        before do
 | 
					        before do
 | 
				
			||||||
          get :show, params: { username: account.username, format: format }
 | 
					          get :show, params: { username: account.username, format: format }
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					@ -173,7 +173,7 @@ RSpec.describe AccountsController do
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context do
 | 
					      context 'with a normal account in a JSON request' do
 | 
				
			||||||
        before do
 | 
					        before do
 | 
				
			||||||
          get :show, params: { username: account.username, format: format }
 | 
					          get :show, params: { username: account.username, format: format }
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					@ -314,7 +314,7 @@ RSpec.describe AccountsController do
 | 
				
			||||||
        it_behaves_like 'cacheable response'
 | 
					        it_behaves_like 'cacheable response'
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context do
 | 
					      context 'with a normal account in an RSS request' do
 | 
				
			||||||
        before do
 | 
					        before do
 | 
				
			||||||
          get :show, params: { username: account.username, format: format }
 | 
					          get :show, params: { username: account.username, format: format }
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,7 +88,7 @@ RSpec.describe ActivityPub::CollectionsController do
 | 
				
			||||||
      context 'with signature' do
 | 
					      context 'with signature' do
 | 
				
			||||||
        let(:remote_account) { Fabricate(:account, domain: 'example.com') }
 | 
					        let(:remote_account) { Fabricate(:account, domain: 'example.com') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        context do
 | 
					        context 'when getting a featured resource' do
 | 
				
			||||||
          before do
 | 
					          before do
 | 
				
			||||||
            get :show, params: { id: 'featured', account_username: account.username }
 | 
					            get :show, params: { id: 'featured', account_username: account.username }
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,4 +20,16 @@ describe Admin::AccountActionsController do
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    let(:account) { Fabricate(:account) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'records the account action' do
 | 
				
			||||||
 | 
					      expect do
 | 
				
			||||||
 | 
					        post :create, params: { account_id: account.id, admin_account_action: { type: 'silence' } }
 | 
				
			||||||
 | 
					      end.to change { account.strikes.count }.by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(admin_account_path(account.id))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -309,4 +309,128 @@ RSpec.describe Admin::AccountsController do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #unsensitive' do
 | 
				
			||||||
 | 
					    subject { post :unsensitive, params: { id: account.id } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:current_user) { Fabricate(:user, role: role) }
 | 
				
			||||||
 | 
					    let(:account) { Fabricate(:account, sensitized_at: 1.year.ago) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.find_by(name: 'Admin') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'marks accounts not sensitized' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(account.reload).to_not be_sensitized
 | 
				
			||||||
 | 
					        expect(response).to redirect_to admin_account_path(account.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is not admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.everyone }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'fails to change account' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status 403
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #unsilence' do
 | 
				
			||||||
 | 
					    subject { post :unsilence, params: { id: account.id } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:current_user) { Fabricate(:user, role: role) }
 | 
				
			||||||
 | 
					    let(:account) { Fabricate(:account, silenced_at: 1.year.ago) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.find_by(name: 'Admin') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'marks accounts not silenced' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(account.reload).to_not be_silenced
 | 
				
			||||||
 | 
					        expect(response).to redirect_to admin_account_path(account.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is not admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.everyone }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'fails to change account' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status 403
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #unsuspend' do
 | 
				
			||||||
 | 
					    subject { post :unsuspend, params: { id: account.id } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:current_user) { Fabricate(:user, role: role) }
 | 
				
			||||||
 | 
					    let(:account) { Fabricate(:account) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      account.suspend!
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.find_by(name: 'Admin') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'marks accounts not suspended' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(account.reload).to_not be_suspended
 | 
				
			||||||
 | 
					        expect(response).to redirect_to admin_account_path(account.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is not admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.everyone }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'fails to change account' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status 403
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #destroy' do
 | 
				
			||||||
 | 
					    subject { post :destroy, params: { id: account.id } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:current_user) { Fabricate(:user, role: role) }
 | 
				
			||||||
 | 
					    let(:account) { Fabricate(:account) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      account.suspend!
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.find_by(name: 'Admin') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        allow(Admin::AccountDeletionWorker).to receive(:perform_async).with(account.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'destroys the account' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(Admin::AccountDeletionWorker).to have_received(:perform_async).with(account.id)
 | 
				
			||||||
 | 
					        expect(response).to redirect_to admin_account_path(account.id)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when user is not admin' do
 | 
				
			||||||
 | 
					      let(:role) { UserRole.everyone }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'fails to change account' do
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status 403
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,4 +73,30 @@ describe Admin::AnnouncementsController do
 | 
				
			||||||
      expect(flash.notice).to match(I18n.t('admin.announcements.destroyed_msg'))
 | 
					      expect(flash.notice).to match(I18n.t('admin.announcements.destroyed_msg'))
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #publish' do
 | 
				
			||||||
 | 
					    subject { post :publish, params: { id: announcement.id } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:announcement) { Fabricate(:announcement, published_at: nil) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'marks announcement published' do
 | 
				
			||||||
 | 
					      subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(announcement.reload).to be_published
 | 
				
			||||||
 | 
					      expect(response).to redirect_to admin_announcements_path
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #unpublish' do
 | 
				
			||||||
 | 
					    subject { post :unpublish, params: { id: announcement.id } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:announcement) { Fabricate(:announcement, published_at: 4.days.ago) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'marks announcement as not published' do
 | 
				
			||||||
 | 
					      subject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(announcement.reload).to_not be_published
 | 
				
			||||||
 | 
					      expect(response).to redirect_to admin_announcements_path
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,4 +56,45 @@ describe Admin::RelaysController do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'DELETE #destroy' do
 | 
				
			||||||
 | 
					    let(:relay) { Fabricate(:relay) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'deletes an existing relay' do
 | 
				
			||||||
 | 
					      delete :destroy, params: { id: relay.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect { relay.reload }.to raise_error(ActiveRecord::RecordNotFound)
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(admin_relays_path)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #enable' do
 | 
				
			||||||
 | 
					    let(:relay) { Fabricate(:relay, state: :idle) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      stub_request(:post, /example.com/).to_return(status: 200)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'updates a relay from idle to pending' do
 | 
				
			||||||
 | 
					      post :enable, params: { id: relay.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(relay.reload).to be_pending
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(admin_relays_path)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #disable' do
 | 
				
			||||||
 | 
					    let(:relay) { Fabricate(:relay, state: :pending) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      stub_request(:post, /example.com/).to_return(status: 200)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'updates a relay from pending to idle' do
 | 
				
			||||||
 | 
					      post :disable, params: { id: relay.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(relay.reload).to be_idle
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(admin_relays_path)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ describe Admin::StatusesController do
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'GET #index' do
 | 
					  describe 'GET #index' do
 | 
				
			||||||
    context do
 | 
					    context 'with a valid account' do
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        get :index, params: { account_id: account.id }
 | 
					        get :index, params: { account_id: account.id }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,16 @@ describe Admin::StatusesController do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #show' do
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      get :show, params: { account_id: account.id, id: status.id }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'returns http success' do
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(200)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'POST #batch' do
 | 
					  describe 'POST #batch' do
 | 
				
			||||||
    before do
 | 
					    before do
 | 
				
			||||||
      post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } }
 | 
					      post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@ describe Admin::Users::RolesController do
 | 
				
			||||||
      put :update, params: { user_id: user.id, user: { role_id: selected_role.id } }
 | 
					      put :update, params: { user_id: user.id, user: { role_id: selected_role.id } }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context do
 | 
					    context 'with manage roles permissions' do
 | 
				
			||||||
      let(:permissions) { UserRole::FLAGS[:manage_roles] }
 | 
					      let(:permissions) { UserRole::FLAGS[:manage_roles] }
 | 
				
			||||||
      let(:position) { 1 }
 | 
					      let(:position) { 1 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,4 +18,68 @@ describe Admin::WarningPresetsController do
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #edit' do
 | 
				
			||||||
 | 
					    let(:account_warning_preset) { Fabricate(:account_warning_preset) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'returns http success and renders edit' do
 | 
				
			||||||
 | 
					      get :edit, params: { id: account_warning_preset.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      expect(response).to render_template(:edit)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    context 'with valid data' do
 | 
				
			||||||
 | 
					      it 'creates a new account_warning_preset and redirects' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { account_warning_preset: { text: 'The account_warning_preset text.' } }
 | 
				
			||||||
 | 
					        end.to change(AccountWarningPreset, :count).by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_warning_presets_path)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with invalid data' do
 | 
				
			||||||
 | 
					      it 'does creates a new account_warning_preset and renders index' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { account_warning_preset: { text: '' } }
 | 
				
			||||||
 | 
					        end.to_not change(AccountWarningPreset, :count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to render_template(:index)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'PUT #update' do
 | 
				
			||||||
 | 
					    let(:account_warning_preset) { Fabricate(:account_warning_preset, text: 'Original text') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with valid data' do
 | 
				
			||||||
 | 
					      it 'updates the account_warning_preset and redirects' do
 | 
				
			||||||
 | 
					        put :update, params: { id: account_warning_preset.id, account_warning_preset: { text: 'Updated text.' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_warning_presets_path)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with invalid data' do
 | 
				
			||||||
 | 
					      it 'does not update the account_warning_preset and renders index' do
 | 
				
			||||||
 | 
					        put :update, params: { id: account_warning_preset.id, account_warning_preset: { text: '' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to render_template(:edit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'DELETE #destroy' do
 | 
				
			||||||
 | 
					    let!(:account_warning_preset) { Fabricate(:account_warning_preset) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'destroys the account_warning_preset and redirects' do
 | 
				
			||||||
 | 
					      delete :destroy, params: { id: account_warning_preset.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect { account_warning_preset.reload }.to raise_error(ActiveRecord::RecordNotFound)
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(admin_warning_presets_path)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,7 +73,7 @@ RSpec.describe Api::V1::AccountsController do
 | 
				
			||||||
    let(:scopes) { 'write:follows' }
 | 
					    let(:scopes) { 'write:follows' }
 | 
				
			||||||
    let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) }
 | 
					    let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context do
 | 
					    context 'when posting to an other account' do
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        post :follow, params: { id: other_account.id }
 | 
					        post :follow, params: { id: other_account.id }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@ RSpec.describe Api::V1::Admin::AccountActionsController do
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'POST #create' do
 | 
					  describe 'POST #create' do
 | 
				
			||||||
    context do
 | 
					    context 'with type of disable' do
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        post :create, params: { account_id: account.id, type: 'disable' }
 | 
					        post :create, params: { account_id: account.id, type: 'disable' }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,7 +96,7 @@ RSpec.describe Api::V1::Admin::DomainAllowsController do
 | 
				
			||||||
  describe 'POST #create' do
 | 
					  describe 'POST #create' do
 | 
				
			||||||
    let!(:domain_allow) { Fabricate(:domain_allow, domain: 'example.com') }
 | 
					    let!(:domain_allow) { Fabricate(:domain_allow, domain: 'example.com') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context do
 | 
					    context 'with a valid domain' do
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        post :create, params: { domain: 'foo.bar.com' }
 | 
					        post :create, params: { domain: 'foo.bar.com' }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,7 +120,7 @@ RSpec.describe Api::V1::StatusesController do
 | 
				
			||||||
    describe 'POST #create' do
 | 
					    describe 'POST #create' do
 | 
				
			||||||
      let(:scopes) { 'write:statuses' }
 | 
					      let(:scopes) { 'write:statuses' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context do
 | 
					      context 'with a basic status body' do
 | 
				
			||||||
        before do
 | 
					        before do
 | 
				
			||||||
          post :create, params: { status: 'Hello world' }
 | 
					          post :create, params: { status: 'Hello world' }
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,7 +79,7 @@ RSpec.describe Auth::RegistrationsController do
 | 
				
			||||||
      request.env['devise.mapping'] = Devise.mappings[:user]
 | 
					      request.env['devise.mapping'] = Devise.mappings[:user]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context do
 | 
					    context 'with open registrations' do
 | 
				
			||||||
      around do |example|
 | 
					      around do |example|
 | 
				
			||||||
        registrations_mode = Setting.registrations_mode
 | 
					        registrations_mode = Setting.registrations_mode
 | 
				
			||||||
        example.run
 | 
					        example.run
 | 
				
			||||||
| 
						 | 
					@ -111,7 +111,7 @@ RSpec.describe Auth::RegistrationsController do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context do
 | 
					    context 'when an accept language is present in headers' do
 | 
				
			||||||
      subject do
 | 
					      subject do
 | 
				
			||||||
        Setting.registrations_mode = 'open'
 | 
					        Setting.registrations_mode = 'open'
 | 
				
			||||||
        request.headers['Accept-Language'] = accept_language
 | 
					        request.headers['Accept-Language'] = accept_language
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@ describe 'Log in' do
 | 
				
			||||||
    expect(subject).to have_css('.flash-message', text: failure_message('invalid'))
 | 
					    expect(subject).to have_css('.flash-message', text: failure_message('invalid'))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  context do
 | 
					  context 'when confirmed at is nil' do
 | 
				
			||||||
    let(:confirmed_at) { nil }
 | 
					    let(:confirmed_at) { nil }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'A unconfirmed user is able to log in' do
 | 
					    it 'A unconfirmed user is able to log in' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ RSpec.describe ActivityPub::Activity::Undo do
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context do
 | 
					      context 'when not atomUri' do
 | 
				
			||||||
        before do
 | 
					        before do
 | 
				
			||||||
          Fabricate(:status, reblog: status, account: sender, uri: 'bar')
 | 
					          Fabricate(:status, reblog: status, account: sender, uri: 'bar')
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								spec/lib/admin/metrics/measure/active_users_measure_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								spec/lib/admin/metrics/measure/active_users_measure_spec.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -37,4 +37,10 @@ describe Admin::Metrics::Measure::InstanceAccountsMeasure do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#data' do
 | 
				
			||||||
 | 
					    it 'runs data query without error' do
 | 
				
			||||||
 | 
					      expect { measure.data }.to_not raise_error
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,4 +39,10 @@ describe Admin::Metrics::Measure::InstanceFollowersMeasure do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#data' do
 | 
				
			||||||
 | 
					    it 'runs data query without error' do
 | 
				
			||||||
 | 
					      expect { measure.data }.to_not raise_error
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,4 +39,10 @@ describe Admin::Metrics::Measure::InstanceFollowsMeasure do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#data' do
 | 
				
			||||||
 | 
					    it 'runs data query without error' do
 | 
				
			||||||
 | 
					      expect { measure.data }.to_not raise_error
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,4 +40,10 @@ describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#data' do
 | 
				
			||||||
 | 
					    it 'runs data query without error' do
 | 
				
			||||||
 | 
					      expect { measure.data }.to_not raise_error
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,4 +36,10 @@ describe Admin::Metrics::Measure::InstanceReportsMeasure do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#data' do
 | 
				
			||||||
 | 
					    it 'runs data query without error' do
 | 
				
			||||||
 | 
					      expect { measure.data }.to_not raise_error
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,4 +36,10 @@ describe Admin::Metrics::Measure::InstanceStatusesMeasure do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#data' do
 | 
				
			||||||
 | 
					    it 'runs data query without error' do
 | 
				
			||||||
 | 
					      expect { measure.data }.to_not raise_error
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								spec/lib/admin/metrics/measure/interactions_measure_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								spec/lib/admin/metrics/measure/interactions_measure_spec.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
							
								
								
									
										17
									
								
								spec/lib/admin/metrics/measure/new_users_measure_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								spec/lib/admin/metrics/measure/new_users_measure_spec.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
		Reference in a new issue