merge with catstodon/main

develop
anna 3 weeks ago
parent 60d54db810
commit df00e5e6cb
Signed by: fef
GPG Key ID: 2585C2DC6D79B485

@ -1,7 +1,9 @@
[production]
defaults
not IE 11
> 0.2%
ios >= 15.6
not dead
not OperaMini all
[development]
supports es6-module

@ -5,7 +5,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/sshd:1": {},
"ghcr.io/devcontainers/features/sshd:1": {}
},
"runServices": ["app", "db", "redis"],
@ -15,16 +15,16 @@
"portsAttributes": {
"3000": {
"label": "web",
"onAutoForward": "notify",
"onAutoForward": "notify"
},
"4000": {
"label": "stream",
"onAutoForward": "silent",
},
"onAutoForward": "silent"
}
},
"otherPortsAttributes": {
"onAutoForward": "silent",
"onAutoForward": "silent"
},
"remoteEnv": {
@ -33,7 +33,7 @@
"STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev",
"DISABLE_FORGERY_REQUEST_PROTECTION": "true",
"ES_ENABLED": "",
"LIBRE_TRANSLATE_ENDPOINT": "",
"LIBRE_TRANSLATE_ENDPOINT": ""
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
@ -43,7 +43,7 @@
"customizations": {
"vscode": {
"settings": {},
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"],
},
},
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
}
}
}

@ -5,7 +5,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/sshd:1": {},
"ghcr.io/devcontainers/features/sshd:1": {}
},
"forwardPorts": [3000, 4000],
@ -14,17 +14,17 @@
"3000": {
"label": "web",
"onAutoForward": "notify",
"requireLocalPort": true,
"requireLocalPort": true
},
"4000": {
"label": "stream",
"onAutoForward": "silent",
"requireLocalPort": true,
},
"requireLocalPort": true
}
},
"otherPortsAttributes": {
"onAutoForward": "silent",
"onAutoForward": "silent"
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
@ -34,7 +34,7 @@
"customizations": {
"vscode": {
"settings": {},
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"],
},
},
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
}
}
}

@ -70,7 +70,7 @@ services:
hard: -1
libretranslate:
image: libretranslate/libretranslate:v1.5.4
image: libretranslate/libretranslate:v1.5.6
restart: unless-stopped
volumes:
- lt-data:/home/libretranslate/.local

@ -123,7 +123,7 @@ module.exports = defineConfig({
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
'react/self-closing-comp': 'error',
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off',
@ -165,7 +165,7 @@ module.exports = defineConfig({
// },
// ],
'jsx-a11y/no-noninteractive-tabindex': 'off',
'jsx-a11y/no-onchange': 'warn',
'jsx-a11y/no-onchange': 'off',
// recommended is full 'error'
'jsx-a11y/no-static-element-interactions': [
'warn',
@ -176,7 +176,7 @@ module.exports = defineConfig({
},
],
// See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js
// See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js
'import/extensions': [
'error',
'always',
@ -355,7 +355,6 @@ module.exports = defineConfig({
'plugin:import/typescript',
'plugin:promise/recommended',
'plugin:jsdoc/recommended-typescript',
'plugin:prettier/recommended',
],
parserOptions: {
@ -364,6 +363,9 @@ module.exports = defineConfig({
},
rules: {
// Disable formatting rules that have been enabled in the base config
'indent': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],

@ -23,7 +23,7 @@ runs:
shell: bash
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

@ -1,3 +1,4 @@
comment: false # Do not leave PR comments
coverage:
status:
project:
@ -8,6 +9,3 @@ coverage:
default:
# Github status check is not blocking
informational: true
comment:
# Only write a comment in PR if there are changes
require_changes: true

@ -125,6 +125,22 @@
],
groupName: null, // We dont want them to belong to any group
},
{
// Group all RuboCop packages with `rubocop` in the same PR
matchManagers: ['bundler'],
matchPackageNames: ['rubocop'],
matchPackagePrefixes: ['rubocop-'],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'RuboCop (non-major)',
},
{
// Group all RSpec packages with `rspec` in the same PR
matchManagers: ['bundler'],
matchPackageNames: ['rspec'],
matchPackagePrefixes: ['rspec-'],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'RSpec (non-major)',
},
// Add labels depending on package manager
{ matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] },
{ matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] },

@ -53,7 +53,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5.0.2
uses: peter-evans/create-pull-request@v6.0.2
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'

@ -0,0 +1,18 @@
name: Check formatting
on:
push:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Check formatting with Prettier
run: yarn format:check

@ -43,4 +43,4 @@ jobs:
- run: echo "::add-matcher::.github/stylelint-matcher.json"
- name: Stylelint
run: yarn lint:sass
run: yarn lint:css

@ -36,4 +36,4 @@ jobs:
- name: Run haml-lint
run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
bundle exec haml-lint
bundle exec haml-lint --reporter github

@ -1,38 +0,0 @@
name: JSON Linting
on:
push:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '.prettier*'
- '**/*.json'
- '.github/workflows/lint-json.yml'
- '!app/javascript/mastodon/locales/*.json'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '.prettier*'
- '**/*.json'
- '.github/workflows/lint-json.yml'
- '!app/javascript/mastodon/locales/*.json'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Prettier
run: yarn lint:json

@ -1,38 +0,0 @@
name: Markdown Linting
on:
push:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
paths:
- '.github/workflows/lint-md.yml'
- '.nvmrc'
- '.prettier*'
- '**/*.md'
- '!AUTHORS.md'
- 'package.json'
- 'yarn.lock'
pull_request:
paths:
- '.github/workflows/lint-md.yml'
- '.nvmrc'
- '.prettier*'
- '**/*.md'
- '!AUTHORS.md'
- 'package.json'
- 'yarn.lock'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Prettier
run: yarn lint:md

@ -1,40 +0,0 @@
name: YML Linting
on:
push:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '.prettier*'
- '**/*.yaml'
- '**/*.yml'
- '.github/workflows/lint-yml.yml'
- '!config/locales/*.yml'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '.prettier*'
- '**/*.yaml'
- '**/*.yml'
- '.github/workflows/lint-yml.yml'
- '!config/locales/*.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Prettier
run: yarn lint:yml

@ -114,6 +114,7 @@ jobs:
- '3.0'
- '3.1'
- '.ruby-version'
- '3.3'
steps:
- uses: actions/checkout@v4
@ -139,7 +140,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
files: coverage/lcov/mastodon.lcov
@ -189,6 +190,7 @@ jobs:
- '3.0'
- '3.1'
- '.ruby-version'
- '3.3'
steps:
- uses: actions/checkout@v4
@ -224,7 +226,7 @@ jobs:
if: failure()
with:
name: e2e-screenshots
path: tmp/screenshots/
path: tmp/capybara/
test-search:
name: Elastic Search integration testing
@ -288,6 +290,7 @@ jobs:
- '3.0'
- '3.1'
- '.ruby-version'
- '3.3'
search-image:
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
include:
@ -328,4 +331,4 @@ jobs:
if: failure()
with:
name: test-search-screenshots
path: tmp/screenshots/
path: tmp/capybara/

@ -1,5 +1,3 @@
inherits_from: .haml-lint_todo.yml
exclude:
- 'vendor/**/*'
- lib/templates/haml/scaffold/_form.html.haml
@ -14,3 +12,5 @@ linters:
enabled: true
LineLength:
max: 320
ViewLength:
max: 200 # Override default value of 100 inherited from rubocop

@ -1,13 +0,0 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2024-01-09 11:30:07 -0500 using Haml-Lint version 0.53.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 1
LineLength:
exclude:
- 'app/views/admin/roles/_form.html.haml'

@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged

@ -54,6 +54,13 @@
# Ignore Docker option files
docker-compose.override.yml
# Ignore public
/public/assets
/public/emoji
/public/packs
/public/packs-test
/public/system
# Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json
@ -74,6 +81,7 @@ app/javascript/styles/mastodon/reset.scss
# Ignore the generated AUTHORS.md
AUTHORS.md
# Process a few selected JS files
!lint-staged.config.js
# Ignore glitch-soc emoji map file

@ -96,13 +96,6 @@ Rails/FilePath:
Rails/HttpStatus:
EnforcedStyle: numeric
# Reason: Allowed in `tootctl` CLI code and in boot ENV checker
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsexit
Rails/Exit:
Exclude:
- 'config/boot.rb'
- 'lib/mastodon/cli/*.rb'
# Reason: Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railslexicallyscopedactionfilter
Rails/LexicallyScopedActionFilter:
@ -135,6 +128,11 @@ Rails/UnusedIgnoredColumns:
Rails/NegateInclude:
Enabled: false
# Reason: Enforce default limit, but allow some elements to span lines
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecexamplelength
RSpec/ExampleLength:
CountAsOne: ['array', 'heredoc', 'method_call']
# Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath
RSpec/FilePath:
@ -175,6 +173,15 @@ Style/ClassAndModuleChildren:
Style/Documentation:
Enabled: false
# Reason: Route redirects are not token-formatted and must be skipped
# https://docs.rubocop.org/rubocop/cops_style.html#styleformatstringtoken
Style/FormatStringToken:
inherit_mode:
merge:
- AllowedMethods # The rubocop-rails config adds `redirect`
AllowedMethods:
- redirect_with_vary
# Reason: Enforce modern Ruby style
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
Style/HashSyntax:
@ -203,11 +210,6 @@ Style/RedundantBegin:
Style/RescueStandardError:
EnforcedStyle: implicit
# Reason: Simplify some spec layouts
# https://docs.rubocop.org/rubocop/cops_style.html#stylesemicolon
Style/Semicolon:
AllowAsExpressionSeparator: true
# Reason: Originally disabled for CodeClimate, and no config consensus has been found
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
Style/SymbolArray:

@ -36,10 +36,10 @@ Metrics/PerceivedComplexity:
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 22
Max: 20 # Override default of 5
RSpec/MultipleExpectations:
Max: 8
Max: 7
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -20,7 +20,7 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.2.0-nightly.2023.11.09+something
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
ARG MASTODON_VERSION_METADATA=""
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"

@ -58,7 +58,9 @@ gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.1'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.6.2'
gem 'i18n', '1.14.1' # TODO: Remove version when resolved: https://github.com/glebm/i18n-tasks/issues/552 / https://github.com/ruby-i18n/i18n/pull/688
gem 'idn-ruby', require: 'idn'
gem 'inline_svg'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
@ -88,7 +90,7 @@ gem 'sidekiq-bulk', '~> 0.2.0'
gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2'
gem 'stoplight', '~> 3.0.1'
gem 'strong_migrations', '1.7.0'
gem 'strong_migrations', '1.8.0'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023'
@ -112,7 +114,7 @@ group :test do
# RSpec helpers for email specs
gem 'email_spec'
# Extra RSpec extenion methods and helpers for sidekiq
# Extra RSpec extension methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 4.0'
# Browser integration testing
@ -125,12 +127,6 @@ group :test do
# Used to mock environment variables
gem 'climate_control'
# Generating fake data for specs
gem 'faker', '~> 3.2'
# Generate test objects for specs
gem 'fabrication', '~> 2.30'
# Add back helpers functions removed in Rails 5.1
gem 'rails-controller-testing', '~> 1.0'
@ -182,6 +178,12 @@ group :development, :test do
# Interactive Debugging tools
gem 'debug', '~> 1.8'
# Generate fake data values
gem 'faker', '~> 3.2'
# Generate factory objects
gem 'fabrication', '~> 2.30'
# Profiling tools
gem 'memory_profiler', require: false
gem 'ruby-prof', require: false

@ -10,35 +10,35 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
actioncable (7.1.3.2)
actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
actionmailbox (7.1.3.2)
actionpack (= 7.1.3.2)
activejob (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.3)
actionpack (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activesupport (= 7.1.3)
actionmailer (7.1.3.2)
actionpack (= 7.1.3.2)
actionview (= 7.1.3.2)
activejob (= 7.1.3.2)
activesupport (= 7.1.3.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (7.1.3)
actionview (= 7.1.3)
activesupport (= 7.1.3)
actionpack (7.1.3.2)
actionview (= 7.1.3.2)
activesupport (= 7.1.3.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@ -46,15 +46,15 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.3)
actionpack (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
actiontext (7.1.3.2)
actionpack (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.3)
activesupport (= 7.1.3)
actionview (7.1.3.2)
activesupport (= 7.1.3.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@ -64,22 +64,22 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.1.3)
activesupport (= 7.1.3)
activejob (7.1.3.2)
activesupport (= 7.1.3.2)
globalid (>= 0.3.6)
activemodel (7.1.3)
activesupport (= 7.1.3)
activerecord (7.1.3)
activemodel (= 7.1.3)
activesupport (= 7.1.3)
activemodel (7.1.3.2)
activesupport (= 7.1.3.2)
activerecord (7.1.3.2)
activemodel (= 7.1.3.2)
activesupport (= 7.1.3.2)
timeout (>= 0.4.0)
activestorage (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activesupport (= 7.1.3)
activestorage (7.1.3.2)
actionpack (= 7.1.3.2)
activejob (= 7.1.3.2)
activerecord (= 7.1.3.2)
activesupport (= 7.1.3.2)
marcel (~> 1.0)
activesupport (7.1.3)
activesupport (7.1.3.2)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@ -139,7 +139,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.6)
bigdecimal (3.1.7)
bindata (2.4.15)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
@ -213,20 +213,19 @@ GEM
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.5.0)
diff-lcs (1.5.1)
discard (1.3.0)
activerecord (>= 4.2, < 8)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.8)
doorkeeper (5.6.9)
railties (>= 5)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
drb (2.2.0)
ruby2_keywords
drb (2.2.1)
ed25519 (1.3.0)
elasticsearch (7.13.3)
elasticsearch-api (= 7.13.3)
@ -309,7 +308,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.56.0)
haml_lint (0.57.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@ -333,7 +332,7 @@ GEM
http-form_data (2.3.0)
http_accept_language (2.1.1)
httpclient (2.8.3)
httplog (1.6.2)
httplog (1.6.3)
rack (>= 2.0)
rainbow (>= 2.0.0)
i18n (1.14.1)
@ -350,14 +349,17 @@ GEM
rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1)
idn-ruby (0.1.5)
inline_svg (1.9.0)
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.7.2)
irb (1.11.2)
irb (1.12.0)
rdoc
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.1)
json-canonicalization (1.0.0)
json-jwt (1.15.3)
json-jwt (1.15.3.1)
activesupport (>= 4.2)
aes_key_wrap
bindata
@ -372,7 +374,7 @@ GEM
json-ld-preloaded (3.3.0)
json-ld (~> 3.3)
rdf (~> 3.3)
json-schema (4.1.1)
json-schema (4.2.0)
addressable (>= 2.8)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
@ -435,7 +437,7 @@ GEM
mime-types-data (3.2023.1205)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.21.2)
minitest (5.22.3)
msgpack (1.7.2)
multi_json (1.15.0)
multipart-post (2.3.0)
@ -444,7 +446,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.9.1)
net-imap (0.4.10)
date
net-protocol
net-ldap (0.19.0)
@ -455,7 +457,7 @@ GEM
net-smtp (0.4.0.1)
net-protocol
nio4r (2.5.9)
nokogiri (1.16.2)
nokogiri (1.16.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nsa (0.3.0)
@ -465,11 +467,11 @@ GEM
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3)
bigdecimal (>= 3.0)
omniauth (2.1.1)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-cas (3.0.0.beta.1)
omniauth-cas (3.0.0)
addressable (~> 2.8)
nokogiri (~> 1.12)
omniauth (~> 2.1)
@ -497,7 +499,7 @@ GEM
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
orm_adapter (0.5.0)
ox (2.14.17)
ox (2.14.18)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
@ -505,7 +507,7 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.5.5)
pg (1.5.6)
pghero (3.4.1)
activerecord (>= 6)
posix-spawn (0.3.15)
@ -532,10 +534,10 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.3)
rack (2.2.8)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.1)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-oauth2 (1.21.3)
activesupport
@ -543,8 +545,9 @@ GEM
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.0.5)
rack
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack
rack-session (1.0.2)
@ -554,20 +557,20 @@ GEM
rackup (1.0.0)
rack (< 3)
webrick
rails (7.1.3)
actioncable (= 7.1.3)
actionmailbox (= 7.1.3)
actionmailer (= 7.1.3)
actionpack (= 7.1.3)
actiontext (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activemodel (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
rails (7.1.3.2)
actioncable (= 7.1.3.2)
actionmailbox (= 7.1.3.2)
actionmailer (= 7.1.3.2)
actionpack (= 7.1.3.2)
actiontext (= 7.1.3.2)
actionview (= 7.1.3.2)
activejob (= 7.1.3.2)
activemodel (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
bundler (>= 1.15.0)
railties (= 7.1.3)
railties (= 7.1.3.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -579,12 +582,12 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (7.0.8)
rails-i18n (7.0.9)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
railties (7.1.3.2)
actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@ -597,7 +600,7 @@ GEM
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.6.2)
rdoc (6.6.3.1)
psych (>= 4.0.0)
redcarpet (3.6.0)
redis (4.8.1)
@ -606,7 +609,7 @@ GEM
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.0)
reline (0.4.2)
reline (0.4.3)
io-console (~> 0.5)
request_store (1.5.1)
rack (>= 1.4)
@ -621,31 +624,31 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (~> 3.13.0)
rspec-github (2.4.0)
rspec-core (~> 3.0)
rspec-mocks (3.12.6)
rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.1.1)
rspec-support (~> 3.13.0)
rspec-rails (6.1.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-sidekiq (4.1.0)
rspec-core (~> 3.0)
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.12.1)
rubocop (1.60.2)
rspec-support (3.13.1)
rubocop (1.62.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@ -653,24 +656,24 @@ GEM
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-ast (1.31.2)
parser (>= 3.3.0.4)
rubocop-capybara (2.20.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.25.0)
rubocop (~> 1.33)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
rubocop-performance (1.20.2)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.23.1)
rubocop-rails (2.24.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rspec (2.26.1)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.27.1)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
@ -691,7 +694,7 @@ GEM
scenic (1.7.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.17.0)
selenium-webdriver (4.18.1)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@ -731,7 +734,7 @@ GEM
stoplight (3.0.2)
redlock (~> 1.0)
stringio (3.1.0)
strong_migrations (1.7.0)
strong_migrations (1.8.0)
activerecord (>= 5.2)
swd (1.3.0)
activesupport (>= 3)
@ -743,8 +746,8 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1)
climate_control
test-prof (1.3.1)
thor (1.3.0)
test-prof (1.3.2)
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
tpm-key_attestation (0.12.0)
@ -793,7 +796,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.20.0)
webmock (3.22.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -811,7 +814,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.12)
zeitwerk (2.6.13)
PLATFORMS
ruby
@ -862,8 +865,10 @@ DEPENDENCIES
http (~> 5.1)
http_accept_language (~> 2.1)
httplog (~> 1.6.2)
i18n (= 1.14.1)
i18n-tasks (~> 1.0)
idn-ruby
inline_svg
irb (~> 1.8)
json-ld
json-ld-preloaded (~> 3.2)
@ -935,7 +940,7 @@ DEPENDENCIES
simplecov-lcov (~> 0.8)
stackprof
stoplight (~> 3.0.1)
strong_migrations (= 1.7.0)
strong_migrations (= 1.8.0)
test-prof
thor (~> 1.2)
tty-prompt (~> 0.23)

@ -3,7 +3,6 @@
## Introduction
This Mastodon fork is based on the [glitch-soc Fork of Mastodon](https://github.com/glitch-soc/mastodon), with changes made to suit [CatCatNya~](https://catcatnya.com).
The aforementioned instance is running the `develop` branch.
I intend to contribute some useful differences back to [glitch-soc](https://github.com/glitch-soc/mastodon) and [vanilla Mastodon](https://github.com/mastodon/mastodon).
To install, take a look at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/). The instructions and features are the same, except for the differences outlined below.
@ -22,15 +21,15 @@ instead, use merge (fast-forward, if possible, with merge commit otherwise).
- sounds/boop.mp3
- sounds/boop.ogg
<br>You might want to revert these to the upstream files (or your own versions!) if you decide to use this fork for your own instance.
- The web frontend emoji picker is a blobcat instead of the joy emoji.
- The rate limits for authenticated users have been relaxed a bit.
- The API endpoint `/api/v1/custom_emojis` is no longer affected by AUTHORIZED_FETCH, allowing anyone to copy custom emojis.
- Allow higher resolution images. (4096x4096 instead of the previous limit of 1920x1080)
- Allow higher resolution images. (4096x4096 instead of the previous limit of 3840x2160)
- Allow posting polls with only one poll option (if `MIN_POLL_OPTIONS` is set to 1 on your instance).
- Added oatstodon flavour (taken from [types.pl fork](https://github.com/ralsei/types.pl), by [@oat@hellsite.site](https://hellsite.site/@oat))
- Added oatstodon flavour (taken from [types.pl fork](https://github.com/ralsei/types.pl), by [@oat@hellsite.site](https://hellsite.site/@oat)), with slight adjustments since.
- Emoji reactions on statuses (with both Unicode and custom emojis, same as for announcements), a feature originally developed for [Nyastodon](https://git.bsd.gay/fef/nyastodon).
Ended up as a Catstodon-maintained patch after its initial two Pull Requests to glitch-soc, but was handed over to [Essem's fork, Chuckya](https://github.com/TheEssem/mastodon) and is now pending [its fourth attempt of merging into glitch-soc](https://github.com/glitch-soc/mastodon/pull/2462).
- Lifts the "only federate local favourites" restriction on favourites/likes and emoji reactions.
- Cherry-picks the [activity filter branch](https://github.com/chikorita157/mastodon-sakura/tree/newmain-tmp3-noellabo-filtering) from [Sakurajima Mastodon](https://github.com/chikorita157/mastodon-sakura).
## Previous differences now merged into glitch-soc

2
Vagrantfile vendored

@ -188,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.post_up_message = <<MESSAGE
To start server
$ vagrant ssh -c "cd /vagrant && foreman start"
$ vagrant ssh -c "cd /vagrant && bin/dev"
MESSAGE
end

@ -1,6 +1,9 @@
# frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :require_authenticated_user!
skip_before_action :require_not_suspended!
skip_around_action :set_locale

@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :authenticate_user!
before_action :require_account_signature!

@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?

@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!

@ -1,9 +1,7 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < ActivityPub::BaseController
include SignatureVerification
include JsonLdHelper
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
before_action :require_actor_signature!
@ -62,11 +60,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil?
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
params = SignatureParamsTransformer.new.apply(tree)
params = SignatureParser.parse(raw_params)
ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params)
rescue Parslet::ParseFailed
rescue SignatureParser::ParsingError
Rails.logger.warn 'Error parsing Collection-Synchronization header'
end

@ -3,9 +3,6 @@
class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }
before_action :require_account_signature!, if: :authorized_fetch_mode?

@ -1,9 +1,7 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureVerification
include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60

@ -128,7 +128,7 @@ module Admin
def unblock_email
authorize @account, :unblock_email?
CanonicalEmailBlock.where(reference_account: @account).delete_all
CanonicalEmailBlock.matching_account(@account).delete_all
log_action :unblock_email, @account

@ -53,7 +53,7 @@ module Admin
end
def resource_params
params.require(:rule).permit(:text, :priority)
params.require(:rule).permit(:text, :hint, :priority)
end
end
end

@ -8,6 +8,7 @@ class Api::BaseController < ApplicationController
include Api::AccessTokenTrackingConcern
include Api::CachingConcern
include Api::ContentSecurityPolicy
include Api::ErrorHandling
skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -18,51 +19,6 @@ class Api::BaseController < ApplicationController
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
end
rescue_from ActiveRecord::RecordNotUnique do
render json: { error: 'Duplicate record' }, status: 422
end
rescue_from Date::Error do
render json: { error: 'Invalid date supplied' }, status: 422
end
rescue_from ActiveRecord::RecordNotFound do
render json: { error: 'Record not found' }, status: 404
end
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
render json: { error: 'Remote data could not be fetched' }, status: 503
end
rescue_from OpenSSL::SSL::SSLError do
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
end
rescue_from Mastodon::NotPermittedError do
render json: { error: 'This action is not allowed' }, status: 403
end
rescue_from Seahorse::Client::NetworkingError do |e|
Rails.logger.warn "Storage server error: #{e}"
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RateLimitExceededError do
render json: { error: I18n.t('errors.429') }, status: 429
end
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
render json: { error: e.to_s }, status: 400
end
def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: error.try(:description) || 'Not authorized' } }
end
@ -73,6 +29,14 @@ class Api::BaseController < ApplicationController
protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
@ -140,6 +104,10 @@ class Api::BaseController < ApplicationController
private
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_options_invalid?
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
end

@ -41,10 +41,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_account_followers_url pagination_params(max_id: pagination_max_id) if records_continue?
end

@ -41,10 +41,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end

@ -4,7 +4,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_account
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
after_action :insert_pagination_headers
def index
cache_if_unauthenticated!
@ -35,10 +35,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -51,11 +47,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
def pagination_collection
@statuses
end
end

@ -125,10 +125,6 @@ class Api::V1::Admin::AccountsController < Api::BaseController
translated_params
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -137,12 +133,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
def pagination_collection
@accounts
end
def records_continue?

@ -65,10 +65,6 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
@canonical_email_block = CanonicalEmailBlock.find(params[:id])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -77,12 +73,8 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
end
def pagination_max_id
@canonical_email_blocks.last.id
end
def pagination_since_id
@canonical_email_blocks.first.id
def pagination_collection
@canonical_email_blocks
end
def records_continue?

@ -61,10 +61,6 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
DomainAllow.all
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -73,12 +69,8 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
end
def pagination_max_id
@domain_allows.last.id
end
def pagination_since_id
@domain_allows.first.id
def pagination_collection
@domain_allows
end
def records_continue?

@ -72,10 +72,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -84,12 +80,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty?
end
def pagination_max_id
@domain_blocks.last.id
end
def pagination_since_id
@domain_blocks.first.id
def pagination_collection
@domain_blocks
end
def records_continue?

@ -58,10 +58,6 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
params.permit(:domain, :allow_with_approval)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -70,12 +66,8 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
end
def pagination_max_id
@email_domain_blocks.last.id
end
def pagination_since_id
@email_domain_blocks.first.id
def pagination_collection
@email_domain_blocks
end
def records_continue?

@ -63,10 +63,6 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
params.permit(:ip, :severity, :comment, :expires_in)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -75,12 +71,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
end
def pagination_max_id
@ip_blocks.last.id
end
def pagination_since_id
@ip_blocks.first.id
def pagination_collection
@ip_blocks
end
def records_continue?

@ -35,6 +35,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController
def update
authorize @report, :update?
@report.update!(report_params)
log_action :update, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
@ -88,10 +89,6 @@ class Api::V1::Admin::ReportsController < Api::BaseController
params.permit(*FILTER_PARAMS)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -100,12 +97,8 @@ class Api::V1::Admin::ReportsController < Api::BaseController
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
end
def pagination_max_id
@reports.last.id
end
def pagination_since_id
@reports.first.id
def pagination_collection
@reports
end
def records_continue?

@ -44,10 +44,6 @@ class Api::V1::Admin::TagsController < Api::BaseController
params.permit(:display_name, :trendable, :usable, :listable)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -56,12 +52,8 @@ class Api::V1::Admin::TagsController < Api::BaseController
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
end
def pagination_max_id
@tags.last.id
end
def pagination_since_id
@tags.first.id
def pagination_collection
@tags
end
def records_continue?

@ -42,10 +42,6 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
@providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -54,12 +50,8 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
end
def pagination_max_id
@providers.last.id
end
def pagination_since_id
@providers.first.id
def pagination_collection
@providers
end
def records_continue?

@ -28,10 +28,6 @@ class Api::V1::BlocksController < Api::BaseController
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_blocks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -40,12 +36,8 @@ class Api::V1::BlocksController < Api::BaseController
api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty?
end
def pagination_max_id
paginated_blocks.last.id
end
def pagination_since_id
paginated_blocks.first.id
def pagination_collection
paginated_blocks
end
def records_continue?

@ -31,10 +31,6 @@ class Api::V1::BookmarksController < Api::BaseController
current_account.bookmarks
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -43,12 +39,8 @@ class Api::V1::BookmarksController < Api::BaseController
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
def pagination_max_id
results.last.id
end
def pagination_since_id
results.first.id
def pagination_collection
results
end
def records_continue?

@ -53,10 +53,6 @@ class Api::V1::ConversationsController < Api::BaseController
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_conversations_url pagination_params(max_id: pagination_max_id) if records_continue?
end

@ -29,10 +29,6 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -41,12 +37,8 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end
def pagination_max_id
@encrypted_messages.last.id
end
def pagination_since_id
@encrypted_messages.first.id
def pagination_collection
@encrypted_messages
end
def records_continue?

@ -38,10 +38,6 @@ class Api::V1::DomainBlocksController < Api::BaseController
current_account.domain_blocks
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -50,12 +46,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty?
end
def pagination_max_id
@blocks.last.id
end
def pagination_since_id
@blocks.first.id
def pagination_collection
@blocks
end
def records_continue?

@ -28,10 +28,6 @@ class Api::V1::EndorsementsController < Api::BaseController
current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
return if unlimited?
@ -44,12 +40,8 @@ class Api::V1::EndorsementsController < Api::BaseController
api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
def pagination_collection
@accounts
end
def records_continue?

@ -31,10 +31,6 @@ class Api::V1::FavouritesController < Api::BaseController
current_account.favourites
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_favourites_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -43,12 +39,8 @@ class Api::V1::FavouritesController < Api::BaseController
api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
def pagination_max_id
results.last.id
end
def pagination_since_id
results.first.id
def pagination_collection
results
end
def records_continue?

@ -12,6 +12,10 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
private
def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
end
def featured_tag_ids
current_account.featured_tags.pluck(:tag_id)
end
end

@ -48,10 +48,6 @@ class Api::V1::FollowRequestsController < Api::BaseController
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) if records_continue?
end

@ -22,10 +22,6 @@ class Api::V1::FollowedTagsController < Api::BaseController
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_followed_tags_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -34,12 +30,8 @@ class Api::V1::FollowedTagsController < Api::BaseController
api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty?
end
def pagination_max_id
@results.last.id
end
def pagination_since_id
@results.first.id
def pagination_collection
@results
end
def records_continue?

@ -55,10 +55,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
params.permit(account_ids: [])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
return if unlimited?
@ -71,12 +67,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
def pagination_collection
@accounts
end
def records_continue?

@ -28,10 +28,6 @@ class Api::V1::MutesController < Api::BaseController
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_mutes_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -40,12 +36,8 @@ class Api::V1::MutesController < Api::BaseController
api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty?
end
def pagination_max_id
paginated_mutes.last.id
end
def pagination_since_id
paginated_mutes.first.id
def pagination_collection
paginated_mutes
end
def records_continue?

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Api::V1::Notifications::PoliciesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update
before_action :require_user!
before_action :set_policy
def show
render json: @policy, serializer: REST::NotificationPolicySerializer
end
def update
@policy.update!(resource_params)
render json: @policy, serializer: REST::NotificationPolicySerializer
end
private
def set_policy
@policy = NotificationPolicy.find_or_initialize_by(account: current_account)
with_read_replica do
@policy.summarize!
end
end
def resource_params
params.permit(
:filter_not_following,
:filter_not_followers,
:filter_new_accounts,
:filter_private_mentions
)
end
end

@ -0,0 +1,75 @@
# frozen_string_literal: true
class Api::V1::Notifications::RequestsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
before_action :require_user!
before_action :set_request, except: :index
after_action :insert_pagination_headers, only: :index
def index
with_read_replica do
@requests = load_requests
@relationships = relationships
end
render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
end
def show
render json: @request, serializer: REST::NotificationRequestSerializer
end
def accept
AcceptNotificationRequestService.new.call(@request)
render_empty
end
def dismiss
@request.update!(dismissed: true)
render_empty
end
private
def load_requests
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
NotificationRequest.preload_cache_collection(requests) do |statuses|
cache_collection(statuses, Status)
end
end
def relationships
StatusRelationshipsPresenter.new(@requests.map(&:last_status), current_user&.account_id)
end
def set_request
@request = NotificationRequest.where(account: current_account).find(params[:id])
end
def next_path
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
end
def prev_path
api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty?
end
def pagination_max_id
@requests.last.id
end
def pagination_since_id
@requests.first.id
end
def pagination_params(core_params)
params.slice(:dismissed).permit(:dismissed).merge(core_params)
end
end

@ -58,7 +58,8 @@ class Api::V1::NotificationsController < Api::BaseController
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),
exclude_types: Array(browserable_params[:exclude_types]),
from_account_id: browserable_params[:account_id]
from_account_id: browserable_params[:account_id],
include_filtered: truthy_param?(:include_filtered)
)
end
@ -66,10 +67,6 @@ class Api::V1::NotificationsController < Api::BaseController
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty?
end
@ -78,19 +75,15 @@ class Api::V1::NotificationsController < Api::BaseController
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end
def pagination_max_id
@notifications.last.id
end
def pagination_since_id
@notifications.first.id
def pagination_collection
@notifications
end
def browserable_params
params.permit(:account_id, types: [], exclude_types: [])
params.permit(:account_id, :include_filtered, types: [], exclude_types: [])
end
def pagination_params(core_params)
params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params)
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
end
end

@ -47,10 +47,6 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
params.slice(:limit).permit(:limit).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) if records_continue?
end
@ -63,11 +59,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
def pagination_collection
@statuses
end
end

@ -34,10 +34,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end

@ -30,10 +30,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end

@ -72,13 +72,9 @@ class Api::V1::StatusesController < Api::BaseController
with_rate_limit: true
)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
render json: @status, serializer: serializer_for_status
rescue PostStatusService::UnexpectedMentionsError => e
unexpected_accounts = ActiveModel::Serializer::CollectionSerializer.new(
e.accounts,
serializer: REST::AccountSerializer
)
render json: { error: e.message, unexpected_accounts: unexpected_accounts }, status: 422
render json: unexpected_accounts_error_json(e), status: 422
end
def update
@ -158,6 +154,21 @@ class Api::V1::StatusesController < Api::BaseController
)
end
def serializer_for_status
@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
def unexpected_accounts_error_json(error)
{
error: error.message,
unexpected_accounts: serialized_accounts(error.accounts),
}
end
def serialized_accounts(accounts)
ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end

@ -5,16 +5,8 @@ class Api::V1::Timelines::BaseController < Api::BaseController
private
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
def pagination_collection
@statuses
end
def next_path_params

@ -34,10 +34,6 @@ class Api::V1::Trends::LinksController < Api::BaseController
scope
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end

@ -33,10 +33,6 @@ class Api::V1::Trends::StatusesController < Api::BaseController
scope
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end

@ -30,10 +30,6 @@ class Api::V1::Trends::TagsController < Api::BaseController
Trends.tags.query.allowed
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end

@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
end
def use_seamless_external_login?
@ -180,7 +180,7 @@ class ApplicationController < ActionController::Base
use_pack 'error'
render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html]
end
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code }
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 }
end
end

@ -188,7 +188,9 @@ class Auth::SessionsController < Devise::SessionsController
)
# Only send a notification email every hour at most
return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present?
return if redis.get("2fa_failure_notification:#{user.id}").present?
redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour)
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
end

@ -0,0 +1,52 @@
# frozen_string_literal: true
module Api::ErrorHandling
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
end
rescue_from ActiveRecord::RecordNotUnique do
render json: { error: 'Duplicate record' }, status: 422
end
rescue_from Date::Error do
render json: { error: 'Invalid date supplied' }, status: 422
end
rescue_from ActiveRecord::RecordNotFound do
render json: { error: 'Record not found' }, status: 404
end
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
render json: { error: 'Remote data could not be fetched' }, status: 503
end
rescue_from OpenSSL::SSL::SSLError do
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
end
rescue_from Mastodon::NotPermittedError do
render json: { error: 'This action is not allowed' }, status: 403
end
rescue_from Seahorse::Client::NetworkingError do |e|
Rails.logger.warn "Storage server error: #{e}"
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RateLimitExceededError do
render json: { error: I18n.t('errors.429') }, status: 429
end
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
render json: { error: e.to_s }, status: 400
end
end
end

@ -12,39 +12,6 @@ module SignatureVerification
class SignatureVerificationError < StandardError; end
class SignatureParamsParser < Parslet::Parser
rule(:token) { match("[0-9a-zA-Z!#$%&'*+.^_`|~-]").repeat(1).as(:token) }
rule(:quoted_string) { str('"') >> (qdtext | quoted_pair).repeat.as(:quoted_string) >> str('"') }
# qdtext and quoted_pair are not exactly according to spec but meh
rule(:qdtext) { match('[^\\\\"]') }
rule(:quoted_pair) { str('\\') >> any }
rule(:bws) { match('\s').repeat }
rule(:param) { (token.as(:key) >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:param) }
rule(:comma) { bws >> str(',') >> bws }
# Old versions of node-http-signature add an incorrect "Signature " prefix to the header
rule(:buggy_prefix) { str('Signature ') }
rule(:params) { buggy_prefix.maybe >> (param >> (comma >> param).repeat).as(:params) }
root(:params)
end
class SignatureParamsTransformer < Parslet::Transform
rule(params: subtree(:param)) do
(param.is_a?(Array) ? param : [param]).each_with_object({}) { |(key, value), hash| hash[key] = value }
end
rule(param: { key: simple(:key), value: simple(:val) }) do
[key, val]
end
rule(quoted_string: simple(:string)) do
string.to_s
end
rule(token: simple(:string)) do
string.to_s
end
end
def require_account_signature!
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
@ -135,12 +102,8 @@ module SignatureVerification
end
def signature_params
@signature_params ||= begin
raw_signature = request.headers['Signature']
tree = SignatureParamsParser.new.parse(raw_signature)
SignatureParamsTransformer.new.apply(tree)
end
rescue Parslet::ParseFailed
@signature_params ||= SignatureParser.parse(request.headers['Signature'])
rescue SignatureParser::ParsingError
raise SignatureVerificationError, 'Error parsing signature parameters'
end

@ -16,6 +16,6 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/Appli
helper_method :custom_css_styles
def set_user_roles
@user_roles = UserRole.where(highlighted: true).where.not(color: [nil, ''])
@user_roles = UserRole.providing_styles
end
end

@ -6,6 +6,8 @@ class InstanceActorsController < ActivityPub::BaseController
serialization_scope nil
before_action :set_account
skip_before_action :authenticate_user! # From `AccountOwnedConcern`
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
@ -16,6 +18,11 @@ class InstanceActorsController < ActivityPub::BaseController
private
# Skips various `before_action` from `AccountOwnedConcern`
def account_required?
false
end
def set_account
@account = Account.representative
end

@ -1,27 +1,26 @@
# frozen_string_literal: true
class IntentsController < ApplicationController
before_action :check_uri
EXPECTED_SCHEME = 'web+mastodon'
before_action :handle_invalid_uri, unless: :valid_uri?
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show
if uri.scheme == 'web+mastodon'
case uri.host
when 'follow'
return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:'))
when 'share'
return redirect_to share_path(text: uri.query_values['text'])
end
case uri.host
when 'follow'
redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:'))
when 'share'
redirect_to share_path(text: uri.query_values['text'])
else
handle_invalid_uri
end
not_found
end
private
def check_uri
not_found if uri.blank?
def valid_uri?
uri.present? && uri.scheme == EXPECTED_SCHEME
end
def handle_invalid_uri

@ -0,0 +1,61 @@
# frozen_string_literal: true
class SeveredRelationshipsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers
before_action :set_event, only: [:following, :followers]
def index
@events = AccountRelationshipSeveranceEvent.where(account: current_account)
end
def following
respond_to do |format|
format.csv { send_data following_data, filename: "following-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
end
end
def followers
respond_to do |format|
format.csv { send_data followers_data, filename: "followers-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
end
end
private
def set_event
@event = AccountRelationshipSeveranceEvent.find(params[:id])
end
def following_data
CSV.generate(headers: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], write_headers: true) do |csv|
@event.severed_relationships.active.about_local_account(current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
csv << [acct(follow.target_account), follow.show_reblogs, follow.notify, follow.languages&.join(', ')]
end
end
end
def followers_data
CSV.generate(headers: ['Account address'], write_headers: true) do |csv|
@event.severed_relationships.passive.about_local_account(current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
csv << [acct(follow.account)]
end
end
end
def acct(account)
account.local? ? account.local_username_and_domain : account.acct
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end

@ -28,14 +28,6 @@ module ApplicationHelper
number_to_human(number, **options)
end
def active_nav_class(*paths)
paths.any? { |path| current_page?(path) } ? 'active' : ''
end
def show_landing_strip?
!user_signed_in? && !single_user_mode?
end
def open_registrations?
Setting.registrations_mode == 'open'
end
@ -122,7 +114,7 @@ module ApplicationHelper
end
def check_icon
content_tag(:svg, tag.path('fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor')
inline_svg_tag 'check.svg'
end
def visibility_icon(status)
@ -214,7 +206,7 @@ module ApplicationHelper
state_params[:moved_to_account] = current_account.moved_to_account
end
state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode?
state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode?
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
# rubocop:disable Rails/OutputSafety

@ -21,15 +21,4 @@ module BrandingHelper
def render_logo
image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon')
end
def render_symbol(version = :icon)
path = case version
when :icon
'logo-symbol-icon.svg'
when :wordmark
'logo-symbol-wordmark.svg'
end
render(file: Rails.root.join('app', 'javascript', 'images', path)).html_safe # rubocop:disable Rails/OutputSafety
end
end

@ -25,12 +25,21 @@ module ContextHelper
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: {
'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId',
'toot' => 'http://joinmastodon.org/ns#',
'Device' => 'toot:Device',
'Ed25519Signature' => 'toot:Ed25519Signature',
'Ed25519Key' => 'toot:Ed25519Key',
'Curve25519Key' => 'toot:Curve25519Key',
'EncryptedMessage' => 'toot:EncryptedMessage',
'publicKeyBase64' => 'toot:publicKeyBase64',
'deviceId' => 'toot:deviceId',
'claim' => { '@type' => '@id', '@id' => 'toot:claim' },
'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' },
'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' },
'devices' => { '@type' => '@id', '@id' => 'toot:devices' },
'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText'
'messageFranking' => 'toot:messageFranking',
'messageType' => 'toot:messageType',
'cipherText' => 'toot:cipherText',
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze

@ -109,6 +109,7 @@ module LanguagesHelper
mn: ['Mongolian', 'Монгол хэл'].freeze,
mr: ['Marathi', 'मराठी'].freeze,
ms: ['Malay', 'Bahasa Melayu'].freeze,
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
mt: ['Maltese', 'Malti'].freeze,
my: ['Burmese', 'ဗမာစာ'].freeze,
na: ['Nauru', 'Ekakairũ Naoero'].freeze,
@ -127,7 +128,7 @@ module LanguagesHelper
om: ['Oromo', 'Afaan Oromoo'].freeze,
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
os: ['Ossetian', 'ирон æвзаг'].freeze,
pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze,
pi: ['Pāli', 'पाऴि'].freeze,
pl: ['Polish', 'Polski'].freeze,
ps: ['Pashto', 'پښتو'].freeze,
@ -191,15 +192,20 @@ module LanguagesHelper
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
cnr: ['Montenegrin', 'crnogorski'].freeze,
csb: ['Kashubian', 'Kaszëbsczi'].freeze,
jbo: ['Lojban', 'la .lojban.'].freeze,
kab: ['Kabyle', 'Taqbaylit'].freeze,
ldn: ['Láadan', 'Láadan'].freeze,
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
nds: ['Low German', 'Plattdüütsch'].freeze,
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
sco: ['Scots', 'Scots'].freeze,
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze,
vai: ['Vai', 'ꕙꔤ'].freeze,
xal: ['Kalmyk', 'Хальмг келн'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,

@ -15,9 +15,20 @@ module ReactComponentHelper
div_tag_with_data(data)
end
def serialized_media_attachments(media_attachments)
media_attachments.map { |attachment| serialized_attachment(attachment) }
end
private
def div_tag_with_data(data)
content_tag(:div, nil, data: data)
end
def serialized_attachment(attachment)
ActiveModelSerializers::SerializableResource.new(
attachment,
serializer: REST::MediaAttachmentSerializer
).as_json
end
end

@ -4,14 +4,6 @@ module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed'
def link_to_newer(url)
link_to t('statuses.show_newer'), url, class: 'load-more load-gap'
end
def link_to_older(url)
link_to t('statuses.show_older'), url, class: 'load-more load-gap'
end
def nothing_here(extra_classes = '')
content_tag(:div, class: "nothing-here #{extra_classes}") do
t('accounts.nothing_here')

@ -1,228 +0,0 @@
// This file will be loaded on admin pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
import ready from '../mastodon/ready';
const setAnnouncementEndsAttributes = (target) => {
const valid = target?.value && target?.validity?.valid;
const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at');
if (valid) {
element.classList.remove('optional');
element.required = true;
element.min = target.value;
} else {
element.classList.add('optional');
element.removeAttribute('required');
element.removeAttribute('min');
}
};
Rails.delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
setAnnouncementEndsAttributes(target);
});
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
selectAllMatchingElement.classList.add('active');
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
const hiddenField = document.querySelector('#select_all_matching');
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
selectAllMatchingElement.classList.remove('active');
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
hiddenField.value = '0';
};
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector('#select_all_matching');
const active = hiddenField.value === '1';
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
if (active) {
hiddenField.value = '0';
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
} else {
hiddenField.value = '1';
notSelectedMsg.classList.remove('active');
selectedMsg.classList.add('active');
}
});
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector('#batch_checkbox_all');
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});
Rails.delegate(document, '.media-spoiler-show-button', 'click', () => {
[].forEach.call(document.querySelectorAll('button.media-spoiler'), (element) => {
element.click();
});
});
Rails.delegate(document, '.media-spoiler-hide-button', 'click', () => {
[].forEach.call(document.querySelectorAll('.spoiler-button.spoiler-button--visible button'), (element) => {
element.click();
});
});
Rails.delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
target.form.submit();
});
const onDomainBlockSeverityChange = (target) => {
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
if (rejectMediaDiv) {
rejectMediaDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
}
if (rejectReportsDiv) {
rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
}
};
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
const onEnableBootstrapTimelineAccountsChange = (target) => {
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
bootstrapTimelineAccountsField.parentElement.parentElement.classList.remove('disabled');
} else {
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
bootstrapTimelineAccountsField.parentElement.parentElement.classList.add('disabled');
}
}
};
Rails.delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
const onChangeRegistrationMode = (target) => {
const enabled = target.value === 'approved';
[].forEach.call(document.querySelectorAll('#form_admin_settings_require_invite_text'), (input) => {
input.disabled = !enabled;
if (enabled) {
let element = input;
do {
element.classList.remove('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
} else {
let element = input;
do {
element.classList.add('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
}
});
};
const convertUTCDateTimeToLocal = (value) => {
const date = new Date(value + 'Z');
const twoChars = (x) => (x.toString().padStart(2, '0'));
return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
};
const convertLocalDatetimeToUTC = (value) => {
const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
const match = re.exec(value);
const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
const fullISO8601 = date.toISOString();
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
};
Rails.delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
ready(() => {
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
if (registrationMode) onChangeRegistrationMode(registrationMode);
const checkAllElement = document.querySelector('#batch_checkbox_all');
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
}
document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => {
const domain = document.querySelector('input[type="text"]#by_domain')?.value;
if (domain) {
const url = new URL(event.target.href);
url.searchParams.set('_domain', domain);
e.target.href = url;
}
});
[].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => {
if (element.value) {
element.value = convertUTCDateTimeToLocal(element.value);
}
if (element.placeholder) {
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
}
});
Rails.delegate(document, 'form', 'submit', ({ target }) => {
[].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
if (element.value && element.validity.valid) {
element.value = convertLocalDatetimeToUTC(element.value);
}
});
});
const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at');
if (announcementStartsAt) {
setAnnouncementEndsAttributes(announcementStartsAt);
}
});

@ -0,0 +1,340 @@
// This file will be loaded on admin pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
import ready from '../mastodon/ready';
const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
const valid = target.value && target.validity.valid;
const element = document.querySelector<HTMLInputElement>(
'input[type="datetime-local"]#announcement_ends_at',
);
if (!element) return;
if (valid) {
element.classList.remove('optional');
element.required = true;
element.min = target.value;
} else {
element.classList.add('optional');
element.removeAttribute('required');
element.removeAttribute('min');
}
};
Rails.delegate(
document,
'input[type="datetime-local"]#announcement_starts_at',
'change',
({ target }) => {
if (target instanceof HTMLInputElement)
setAnnouncementEndsAttributes(target);
},
);
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
selectAllMatchingElement?.classList.add('active');
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
const hiddenField = document.querySelector<HTMLInputElement>(
'input#select_all_matching',
);
const selectedMsg = document.querySelector(
'.batch-table__select-all .selected',
);
const notSelectedMsg = document.querySelector(
'.batch-table__select-all .not-selected',
);
selectAllMatchingElement?.classList.remove('active');
selectedMsg?.classList.remove('active');
notSelectedMsg?.classList.add('active');
if (hiddenField) hiddenField.value = '0';
};
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
document
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
.forEach((content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector<HTMLInputElement>(
'#select_all_matching',
);
if (!hiddenField) return;
const active = hiddenField.value === '1';
const selectedMsg = document.querySelector(
'.batch-table__select-all .selected',
);
const notSelectedMsg = document.querySelector(
'.batch-table__select-all .not-selected',
);
if (!selectedMsg || !notSelectedMsg) return;
if (active) {
hiddenField.value = '0';
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
} else {
hiddenField.value = '1';
notSelectedMsg.classList.remove('active');
selectedMsg.classList.add('active');
}
});
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector<HTMLInputElement>(
'input#batch_checkbox_all',
);
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
if (checkAllElement) {
const allCheckboxes = Array.from(
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
);
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
checkAllElement.indeterminate =
!checkAllElement.checked &&
allCheckboxes.some((content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});
Rails.delegate(
document,
'.filter-subset--with-select select',
'change',
({ target }) => {
if (target instanceof HTMLSelectElement) target.form?.submit();
},
);
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
const rejectMediaDiv = document.querySelector(
'.input.with_label.domain_block_reject_media',
);
const rejectReportsDiv = document.querySelector(
'.input.with_label.domain_block_reject_reports',
);
if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
rejectMediaDiv.style.display =
target.value === 'suspend' ? 'none' : 'block';
}
if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
rejectReportsDiv.style.display =
target.value === 'suspend' ? 'none' : 'block';
}
};
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
});
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
const bootstrapTimelineAccountsField =
document.querySelector<HTMLInputElement>(
'#form_admin_settings_bootstrap_timeline_accounts',
);
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement?.classList.remove(
'disabled',
);
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
'disabled',
);
} else {
bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
'disabled',
);
}
}
};
Rails.delegate(
document,
'#form_admin_settings_enable_bootstrap_timeline_accounts',
'change',
({ target }) => {
if (target instanceof HTMLInputElement)
onEnableBootstrapTimelineAccountsChange(target);
},
);
const onChangeRegistrationMode = (target: HTMLSelectElement) => {
const enabled = target.value === 'approved';
document
.querySelectorAll<HTMLElement>(
'.form_admin_settings_registrations_mode .warning-hint',
)
.forEach((warning_hint) => {
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
});
document
.querySelectorAll<HTMLInputElement>(
'input#form_admin_settings_require_invite_text',
)
.forEach((input) => {
input.disabled = !enabled;
if (enabled) {
let element: HTMLElement | null = input;
do {
element.classList.remove('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
} else {
let element: HTMLElement | null = input;
do {
element.classList.add('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
}
});
};
const convertUTCDateTimeToLocal = (value: string) => {
const date = new Date(value + 'Z');
const twoChars = (x: number) => x.toString().padStart(2, '0');
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
};
function convertLocalDatetimeToUTC(value: string) {
const date = new Date(value);
const fullISO8601 = date.toISOString();
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
}
Rails.delegate(
document,
'#form_admin_settings_registrations_mode',
'change',
({ target }) => {
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
},
);
ready(() => {
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
'select#domain_block_severity',
);
if (domainBlockSeveritySelect)
onDomainBlockSeverityChange(domainBlockSeveritySelect);
const enableBootstrapTimelineAccounts =
document.querySelector<HTMLInputElement>(
'input#form_admin_settings_enable_bootstrap_timeline_accounts',
);
if (enableBootstrapTimelineAccounts)
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
const registrationMode = document.querySelector<HTMLSelectElement>(
'select#form_admin_settings_registrations_mode',
);
if (registrationMode) onChangeRegistrationMode(registrationMode);
const checkAllElement = document.querySelector<HTMLInputElement>(
'input#batch_checkbox_all',
);
if (checkAllElement) {
const allCheckboxes = Array.from(
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
);
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
checkAllElement.indeterminate =
!checkAllElement.checked &&
allCheckboxes.some((content) => content.checked);
}
document
.querySelector('a#add-instance-button')
?.addEventListener('click', (e) => {
const domain = document.querySelector<HTMLInputElement>(
'input[type="text"]#by_domain',
)?.value;
if (domain && e.target instanceof HTMLAnchorElement) {
const url = new URL(e.target.href);
url.searchParams.set('_domain', domain);
e.target.href = url.toString();
}
});
document
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
.forEach((element) => {
if (element.value) {
element.value = convertUTCDateTimeToLocal(element.value);
}
if (element.placeholder) {
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
}
});
Rails.delegate(document, 'form', 'submit', ({ target }) => {
if (target instanceof HTMLFormElement)
target
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
.forEach((element) => {
if (element.value && element.validity.valid) {
element.value = convertLocalDatetimeToUTC(element.value);
}
});
});
const announcementStartsAt = document.querySelector<HTMLInputElement>(
'input[type="datetime-local"]#announcement_starts_at',
);
if (announcementStartsAt) {
setAnnouncementEndsAttributes(announcementStartsAt);
}
}).catch((reason) => {
throw reason;
});

@ -1,25 +0,0 @@
// This file will be loaded on embed pages, regardless of theme.
import 'packs/public-path';
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
function setEmbedHeight () {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
}
if (['interactive', 'complete'].includes(document.readyState)) {
setEmbedHeight();
} else {
document.addEventListener('DOMContentLoaded', setEmbedHeight);
}
});

@ -0,0 +1,41 @@
// This file will be loaded on embed pages, regardless of theme.
import 'packs/public-path';
import ready from '../mastodon/ready';
interface SetHeightMessage {
type: 'setHeight';
id: string;
height: number;
}
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
if (
data &&
typeof data === 'object' &&
'type' in data &&
data.type === 'setHeight'
)
return true;
else return false;
}
window.addEventListener('message', (e) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
const data = e.data;
ready(() => {
window.parent.postMessage(
{
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
},
'*',
);
}).catch((e) => {
console.error('Error in setHeightMessage postMessage', e);
});
});

@ -1,44 +0,0 @@
// This file will be loaded on settings pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
const avatar = document.getElementById(target.id + '-preview');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
const input = target.parentNode.querySelector('.input-copy__wrapper input');
const oldReadOnly = input.readonly;
input.readonly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
target.parentNode.classList.add('copied');
setTimeout(() => {
target.parentNode.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readonly = oldReadOnly;
});

@ -0,0 +1,70 @@
// This file will be loaded on settings pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
Rails.delegate(
document,
'#edit_profile input[type=file]',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const avatar = document.querySelector<HTMLImageElement>(
`img#${target.id}-preview`,
);
if (!avatar) return;
let file: File | undefined;
if (target.files) file = target.files[0];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
if (url) avatar.src = url;
},
);
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!(target instanceof HTMLButtonElement)) return;
const input = target.parentNode?.querySelector<HTMLInputElement>(
'.input-copy__wrapper input',
);
if (!input) return;
const oldReadOnly = input.readOnly;
input.readOnly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement;
if (!parent) return;
parent.classList.add('copied');
setTimeout(() => {
parent.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly;
});

@ -2,12 +2,12 @@
# theme.
pack:
about:
admin: admin.js
admin: admin.ts
auth: auth.js
common:
filename: common.js
stylesheet: true
embed: embed.js
embed: embed.ts
error:
home:
inert:
@ -18,7 +18,7 @@ pack:
stylesheet: true
modal:
public:
settings: settings.js
settings: settings.ts
sign_up:
share:
remote_interaction_helper: remote_interaction_helper.ts

@ -66,11 +66,9 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
@ -93,11 +91,6 @@ export * from './accounts_typed';
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {

@ -12,8 +12,6 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
export function fetchBlocks() {
return (dispatch, getState) => {
dispatch(fetchBlocksRequest());
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) {
return dispatch => {
dispatch({
type: BLOCKS_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'BLOCK' }));
dispatch(openModal({
modalType: 'BLOCK',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

@ -21,7 +21,6 @@ let fetchComposeSuggestionsAccountsController;
let fetchComposeSuggestionsTagsController;
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
@ -59,7 +58,7 @@ export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@ -82,6 +81,7 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
@ -117,12 +117,6 @@ export function changeCompose(text) {
};
}
export function cycleElefriendCompose() {
return {
type: COMPOSE_CYCLE_ELEFRIEND,
};
}
export function replyCompose(status, routerHistory) {
return (dispatch, getState) => {
const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
@ -148,13 +142,13 @@ export function resetCompose() {
};
}
export const focusCompose = (routerHistory, defaultText) => dispatch => {
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(routerHistory);
ensureComposeIsVisible(getState, routerHistory);
};
export function mentionCompose(account, routerHistory) {
@ -179,7 +173,7 @@ export function directCompose(account, routerHistory) {
};
}
export function submitCompose(routerHistory) {
export function submitCompose(routerHistory, overridePrivacy = null) {
return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@ -228,7 +222,7 @@ export function submitCompose(routerHistory) {
media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
spoiler_text: spoilerText,
visibility: getState().getIn(['compose', 'privacy']),
visibility: overridePrivacy || getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
},
@ -246,11 +240,6 @@ export function submitCompose(routerHistory) {
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// If the response has no data then we can't do anything else.
if (!response.data) {
return;
}
// To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = timelineId => {
@ -278,12 +267,14 @@ export function submitCompose(routerHistory) {
insertIfOnline('direct');
}
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
if (getState().getIn(['local_settings', 'show_published_toast'])) {
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@ -660,15 +651,19 @@ export const readyComposeSuggestionsTags = (token, tags) => ({
export function selectComposeSuggestion(position, token, suggestion, path) {
return (dispatch, getState) => {
let completion;
let completion, startPosition;
if (suggestion.type === 'emoji') {
completion = suggestion.native || suggestion.colons;
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
completion = `#${suggestion.name}`;
startPosition = position - 1;
} else if (suggestion.type === 'account') {
completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']);
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
startPosition = position;
}
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
@ -676,7 +671,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) {
dispatch({
type: COMPOSE_SUGGESTION_SELECT,
position,
position: startPosition,
token,
completion,
path,
@ -684,7 +679,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
} else {
dispatch({
type: COMPOSE_SUGGESTION_IGNORE,
position,
position: startPosition,
token,
completion,
path,
@ -786,18 +781,26 @@ export function changeComposeVisibility(value) {
};
}
export function changeComposeContentType(value) {
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
type: COMPOSE_CONTENT_TYPE_CHANGE,
type: COMPOSE_EMOJI_INSERT,
position,
emoji,
needsSpace,
};
}
export function changeComposing(value) {
return {
type: COMPOSE_COMPOSING_CHANGE,
value,
};
}
export function insertEmojiCompose(position, emoji) {
export function changeComposeContentType(value) {
return {
type: COMPOSE_EMOJI_INSERT,
position,
emoji,
type: COMPOSE_CONTENT_TYPE_CHANGE,
value,
};
}
@ -820,11 +823,12 @@ export function addPollOption(title) {
};
}
export function changePollOption(index, title) {
export function changePollOption(index, title, maxOptions) {
return {
type: COMPOSE_POLL_OPTION_CHANGE,
index,
title,
maxOptions,
};
}
@ -842,3 +846,9 @@ export function changePollSettings(expiresIn, isMultiple) {
isMultiple,
};
}
export const changeMediaOrder = (a, b) => ({
type: COMPOSE_CHANGE_MEDIA_ORDER,
a,
b,
});

@ -1,6 +1,8 @@
import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
import { openModal } from './modal';
export * from "./domain_blocks_typed";
@ -150,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error,
};
}
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
domain: account.get('acct').split('@')[1],
acct: account.get('acct'),
accountId: account.get('id'),
},
}));

@ -12,10 +12,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'MUTE' }));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
dispatch(openModal({
modalType: 'MUTE',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save