diff --git a/.annotaterb.yml b/.annotaterb.yml deleted file mode 100644 index df8e92b247..0000000000 --- a/.annotaterb.yml +++ /dev/null @@ -1,59 +0,0 @@ ---- -:position: before -:position_in_additional_file_patterns: before -:position_in_class: before -:position_in_factory: before -:position_in_fixture: before -:position_in_routes: before -:position_in_serializer: before -:position_in_test: before -:classified_sort: true -:exclude_controllers: true -:exclude_factories: true -:exclude_fixtures: true -:exclude_helpers: true -:exclude_scaffolds: true -:exclude_serializers: true -:exclude_sti_subclasses: true -:exclude_tests: true -:force: false -:format_markdown: false -:format_rdoc: false -:format_yard: false -:frozen: false -:ignore_model_sub_dir: false -:ignore_unknown_models: false -:include_version: false -:show_complete_foreign_keys: false -:show_foreign_keys: false -:show_indexes: false -:simple_indexes: false -:sort: false -:timestamp: false -:trace: false -:with_comment: true -:with_column_comments: true -:with_table_comments: true -:active_admin: false -:command: -:debug: false -:hide_default_column_types: '' -:hide_limit_column_types: 'integer,boolean' -:ignore_columns: -:ignore_routes: -:models: true -:routes: false -:skip_on_db_migrate: false -:target_action: :do_annotations -:wrapper: -:wrapper_close: -:wrapper_open: -:classes_default_to_s: [] -:additional_file_patterns: [] -:model_dir: - - app/models -:require: [] -:root_dir: - - '' - -:show_check_constraints: false diff --git a/.browserslistrc b/.browserslistrc index 0135379d6e..54dd3aaf34 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,6 +1,7 @@ +[production] defaults -> 0.2% -firefox >= 78 -ios >= 15.6 +not IE 11 not dead -not OperaMini all + +[development] +supports es6-module diff --git a/.bundler-audit.yml b/.bundler-audit.yml new file mode 100644 index 0000000000..c867b1abf0 --- /dev/null +++ b/.bundler-audit.yml @@ -0,0 +1,10 @@ +--- +ignore: + # devise-two-factor advisory about brute-forcing TOTP + # We have rate-limits on authentication endpoints in place (including second + # factor verification) since Mastodon v3.2.0 + - CVE-2024-0227 + # devise-two-factor advisory about generated secrets being weaker than expected + # We call `generate_otp_secret` ourselves with a requested length of 32 characters, + # which exceeds the recommended remediation of 26 characters, so we're safe + - CVE-2024-8796 diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3aa0bbf7da..b3b1d97a24 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,18 +1,20 @@ # For details, see https://github.com/devcontainers/images/tree/main/src/ruby -FROM mcr.microsoft.com/devcontainers/ruby:1-3.3-bookworm +FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye -# Install node version from .nvmrc -WORKDIR /app -COPY .nvmrc . -RUN /bin/bash --login -i -c "nvm install" +# Install Rails +# RUN gem install rails webdrivers -# Install additional OS packages -RUN apt-get update && \ - export DEBIAN_FRONTEND=noninteractive && \ - apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev +ARG NODE_VERSION="16" +RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" -# Disable download prompt for Corepack -ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +# [Optional] Uncomment this section to install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libpam-dev -# Move welcome message to where VS Code expects it -COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt +# [Optional] Uncomment this line to install additional gems. +RUN gem install foreman + +# [Optional] Uncomment this line to install global node packages. +RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 + +COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json index d2358657f6..ca9156fdaa 100644 --- a/.devcontainer/codespaces/devcontainer.json +++ b/.devcontainer/codespaces/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Mastodon on GitHub Codespaces", - "dockerComposeFile": "../compose.yaml", + "dockerComposeFile": "../docker-compose.yml", "service": "app", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -23,8 +23,6 @@ } }, - "remoteUser": "root", - "otherPortsAttributes": { "onAutoForward": "silent" }, @@ -39,7 +37,7 @@ }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": "bin/setup", + "postCreateCommand": ".devcontainer/post-create.sh", "waitFor": "postCreateCommand", "customizations": { diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml deleted file mode 100644 index 5da1ec3a24..0000000000 --- a/.devcontainer/compose.yaml +++ /dev/null @@ -1,91 +0,0 @@ -services: - app: - working_dir: /workspaces/mastodon/ - build: - context: .. - dockerfile: .devcontainer/Dockerfile - volumes: - - ..:/workspaces/mastodon:cached - environment: - RAILS_ENV: development - NODE_ENV: development - BIND: 0.0.0.0 - BOOTSNAP_CACHE_DIR: /tmp - REDIS_HOST: redis - REDIS_PORT: '6379' - DB_HOST: db - DB_USER: postgres - DB_PASS: postgres - DB_PORT: '5432' - ES_ENABLED: 'true' - ES_HOST: es - ES_PORT: '9200' - LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 - LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000} - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - ports: - - '3000:3000' - - '3035:3035' - - '4000:4000' - networks: - - external_network - - internal_network - - db: - image: postgres:14-alpine - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_HOST_AUTH_METHOD: trust - networks: - - internal_network - - redis: - image: redis:7-alpine - restart: unless-stopped - volumes: - - redis-data:/data - networks: - - internal_network - - es: - image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 - restart: unless-stopped - environment: - ES_JAVA_OPTS: -Xms512m -Xmx512m - cluster.name: es-mastodon - discovery.type: single-node - bootstrap.memory_lock: 'true' - volumes: - - es-data:/usr/share/elasticsearch/data - networks: - - internal_network - ulimits: - memlock: - soft: -1 - hard: -1 - - libretranslate: - image: libretranslate/libretranslate:v1.6.2 - restart: unless-stopped - volumes: - - lt-data:/home/libretranslate/.local - networks: - - external_network - - internal_network - -volumes: - postgres-data: - redis-data: - es-data: - lt-data: - -networks: - external_network: - internal_network: - internal: true diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fb88f7801f..fa8d6542c1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Mastodon on local machine", - "dockerComposeFile": "compose.yaml", + "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -23,14 +23,12 @@ } }, - "remoteUser": "root", - "otherPortsAttributes": { "onAutoForward": "silent" }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": "bin/setup", + "postCreateCommand": ".devcontainer/post-create.sh", "waitFor": "postCreateCommand", "customizations": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..20aecd71d6 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,90 @@ +version: '3' + +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ../..:/workspaces:cached + environment: + RAILS_ENV: development + NODE_ENV: development + BIND: 0.0.0.0 + REDIS_HOST: redis + REDIS_PORT: '6379' + DB_HOST: db + DB_USER: postgres + DB_PASS: postgres + DB_PORT: '5432' + ES_ENABLED: 'true' + ES_HOST: es + ES_PORT: '9200' + LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + ports: + - '127.0.0.1:3000:3000' + - '127.0.0.1:3035:3035' + - '127.0.0.1:4000:4000' + networks: + - external_network + - internal_network + + db: + image: postgres:14-alpine + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_HOST_AUTH_METHOD: trust + networks: + - internal_network + + redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - redis-data:/data + networks: + - internal_network + + es: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 + restart: unless-stopped + environment: + ES_JAVA_OPTS: -Xms512m -Xmx512m + cluster.name: es-mastodon + discovery.type: single-node + bootstrap.memory_lock: 'true' + volumes: + - es-data:/usr/share/elasticsearch/data + networks: + - internal_network + ulimits: + memlock: + soft: -1 + hard: -1 + + libretranslate: + image: libretranslate/libretranslate:v1.3.11 + restart: unless-stopped + volumes: + - lt-data:/home/libretranslate/.local + networks: + - external_network + - internal_network + +volumes: + postgres-data: + redis-data: + es-data: + lt-data: + +networks: + external_network: + internal_network: + internal: true diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000000..a075cc7b3b --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e # Fail the whole script on first error + +# Fetch Ruby gem dependencies +bundle config path 'vendor/bundle' +bundle config with 'development test' +bundle install + +# Make Gemfile.lock pristine again +git checkout -- Gemfile.lock + +# Fetch Javascript dependencies +yarn --frozen-lockfile + +# [re]create, migrate, and seed the test database +RAILS_ENV=test ./bin/rails db:setup + +# [re]create, migrate, and seed the development database +RAILS_ENV=development ./bin/rails db:setup + +# Precompile assets for development +RAILS_ENV=development ./bin/rails assets:precompile + +# Precompile assets for test +RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt index dbc19c910c..488cf92857 100644 --- a/.devcontainer/welcome-message.txt +++ b/.devcontainer/welcome-message.txt @@ -1,7 +1,8 @@ -👋 Welcome to your Mastodon Dev Container! +👋 Welcome to "Mastodon" in GitHub Codespaces! -ðŸ› ï¸ Your environment is fully setup with all the required software. +ðŸ› ï¸ Your environment is fully setup with all the required software. -💥 Run `bin/dev` to start the application processes. +🔠To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1). + +📠Edit away, run your app as usual, and we'll automatically make it available for you to access. -🥼 Run `RAILS_ENV=test bin/rails assets:precompile && RAILS_ENV=test bin/rspec` to run the test suite. diff --git a/.dockerignore b/.dockerignore index 9d990ab9ce..fedbea236d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,7 +8,6 @@ public/system public/assets public/packs -public/packs-test node_modules neo4j vendor/bundle @@ -20,9 +19,3 @@ postgres14 redis elasticsearch chart -.yarn/ -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions diff --git a/.env.development b/.env.development deleted file mode 100644 index 0330da8377..0000000000 --- a/.env.development +++ /dev/null @@ -1,4 +0,0 @@ -# Required by ActiveRecord encryption feature -ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR -ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E -ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr diff --git a/.env.production.sample b/.env.production.sample index a311ad5f8d..0bf01bdc36 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -1,5 +1,5 @@ # This is a sample configuration file. You can generate your configuration -# with the `bundle exec rails mastodon:setup` interactive setup wizard, but to customize +# with the `rake mastodon:setup` interactive setup wizard, but to customize # your setup even further, you'll need to edit it manually. This sample does # not demonstrate all available configuration options. Please look at # https://docs.joinmastodon.org/admin/config/ for the full documentation. @@ -40,25 +40,14 @@ ES_PASS=password # Secrets # ------- -# Make sure to use `bundle exec rails secret` to generate secrets +# Make sure to use `rake secret` to generate secrets # ------- SECRET_KEY_BASE= OTP_SECRET= -# Encryption secrets -# ------------------ -# Must be available (and set to same values) for all server processes -# These are private/secret values, do not share outside hosting environment -# Use `bin/rails db:encryption:init` to generate fresh secrets -# Do NOT change these secrets once in use, as this would cause data loss and other issues -# ------------------ -# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY= -# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT= -# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY= - # Web Push # -------- -# Generate with `bundle exec rails mastodon:webpush:generate_vapid_key` +# Generate with `rake mastodon:webpush:generate_vapid_key` # -------- VAPID_PRIVATE_KEY= VAPID_PUBLIC_KEY= @@ -79,9 +68,6 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= S3_ALIAS_HOST=files.example.com -# Optional list of hosts that are allowed to serve media for your instance -# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com - # IP and session retention # ----------------------- # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml @@ -89,27 +75,3 @@ S3_ALIAS_HOST=files.example.com # ----------------------- IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952 - -# Fetch All Replies Behavior -# -------------------------- -# When a user expands a post (DetailedStatus view), fetch all of its replies -# (default: false) -FETCH_REPLIES_ENABLED=false - -# Period to wait between fetching replies (in minutes) -FETCH_REPLIES_COOLDOWN_MINUTES=15 - -# Period to wait after a post is first created before fetching its replies (in minutes) -FETCH_REPLIES_INITIAL_WAIT_MINUTES=5 - -# Max number of replies to fetch - total, recursively through a whole reply tree -FETCH_REPLIES_MAX_GLOBAL=1000 - -# Max number of replies to fetch - for a single post -FETCH_REPLIES_MAX_SINGLE=500 - -# Max number of replies Collection pages to fetch - total -FETCH_REPLIES_MAX_PAGES=500 - -# Maximum allowed character count -MAX_CHARS=5555 diff --git a/.env.test b/.env.test index 52379687b4..def5fbd8d3 100644 --- a/.env.test +++ b/.env.test @@ -1,13 +1,10 @@ -# In test, compile the NodeJS code as if we are in production -NODE_ENV=production +# Node.js +NODE_ENV=tests # Federation LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_HTTPS=true # Elasticsearch +ES_ENABLED=false +ES_HOST=localhost +ES_PORT=9200 ES_PREFIX=test - -# Secret values required by ActiveRecord encryption feature -# Use `bin/rails db:encryption:init` to generate fresh secrets -ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=test_determinist_key_DO_NOT_USE_IN_PRODUCTION -ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=test_salt_DO_NOT_USE_IN_PRODUCTION -ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=test_primary_key_DO_NOT_USE_IN_PRODUCTION diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..d4930e1f52 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,13 @@ +/build/** +/coverage/** +/db/** +/lib/** +/log/** +/node_modules/** +/nonobox/** +/public/** +!/public/embed.js +/spec/** +/tmp/** +/vendor/** +!.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..d5f0ae1ac5 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,384 @@ +module.exports = { + root: true, + + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', + ], + + env: { + browser: true, + node: true, + es6: true, + }, + + globals: { + ATTACHMENT_HOST: false, + }, + + parser: '@typescript-eslint/parser', + + plugins: [ + 'react', + 'jsx-a11y', + 'import', + 'promise', + '@typescript-eslint', + 'formatjs', + ], + + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2021, + requireConfigFile: false, + babelOptions: { + configFile: false, + presets: ['@babel/react', '@babel/env'], + }, + }, + + settings: { + react: { + version: 'detect', + }, + 'import/ignore': [ + 'node_modules', + '\\.(css|scss|json)$', + ], + 'import/resolver': { + typescript: {}, + }, + }, + + rules: { + 'consistent-return': 'error', + 'dot-notation': 'error', + eqeqeq: ['error', 'always', { 'null': 'ignore' }], + 'jsx-quotes': ['error', 'prefer-single'], + 'no-case-declarations': 'off', + 'no-catch-shadow': 'error', + 'no-console': [ + 'warn', + { + allow: [ + 'error', + 'warn', + ], + }, + ], + 'no-empty': 'off', + 'no-restricted-properties': [ + 'error', + { property: 'substring', message: 'Use .slice instead of .substring.' }, + { property: 'substr', message: 'Use .slice instead of .substr.' }, + ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], + 'no-self-assign': 'off', + 'no-unused-expressions': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'after-used', + destructuredArrayIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + 'valid-typeof': 'error', + + 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], + 'react/jsx-boolean-value': 'error', + 'react/display-name': 'off', + 'react/jsx-fragments': ['error', 'syntax'], + 'react/jsx-equals-spacing': 'error', + 'react/jsx-no-bind': 'error', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-no-target-blank': 'off', + 'react/jsx-tag-spacing': 'error', + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/jsx-wrap-multilines': 'error', + 'react/no-deprecated': 'off', + 'react/no-unknown-property': 'off', + '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 + 'jsx-a11y/accessible-emoji': 'warn', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/label-has-associated-control': 'off', + 'jsx-a11y/media-has-caption': 'off', + 'jsx-a11y/no-autofocus': 'off', + // recommended rule is: + // 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ + // 'error', + // { + // tr: ['none', 'presentation'], + // canvas: ['img'], + // }, + // ], + 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', + // recommended rule is: + // 'jsx-a11y/no-noninteractive-element-interactions': [ + // 'error', + // { + // body: ['onError', 'onLoad'], + // iframe: ['onError', 'onLoad'], + // img: ['onError', 'onLoad'], + // }, + // ], + 'jsx-a11y/no-noninteractive-element-interactions': [ + 'warn', + { + handlers: [ + 'onClick', + ], + }, + ], + // recommended rule is: + // 'jsx-a11y/no-noninteractive-tabindex': [ + // 'error', + // { + // tags: [], + // roles: ['tabpanel'], + // allowExpressionValues: true, + // }, + // ], + 'jsx-a11y/no-noninteractive-tabindex': 'off', + 'jsx-a11y/no-onchange': 'warn', + // recommended is full 'error' + 'jsx-a11y/no-static-element-interactions': [ + 'warn', + { + handlers: [ + 'onClick', + ], + }, + ], + + // See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js + 'import/extensions': [ + 'error', + 'always', + { + js: 'never', + jsx: 'never', + mjs: 'never', + ts: 'never', + tsx: 'never', + }, + ], + 'import/first': 'error', + 'import/newline-after-import': 'error', + 'import/no-anonymous-default-export': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + 'config/webpack/**', + 'app/javascript/mastodon/performance.js', + 'app/javascript/mastodon/test_setup.js', + 'app/javascript/**/__tests__/**', + ], + }, + ], + 'import/no-amd': 'error', + 'import/no-commonjs': 'error', + 'import/no-import-module-exports': 'error', + 'import/no-relative-packages': 'error', + 'import/no-self-import': 'error', + 'import/no-useless-path-segments': 'error', + 'import/no-webpack-loader-syntax': 'error', + + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always', + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + ['index', 'sibling'], + 'object', + ], + pathGroups: [ + // React core packages + { + pattern: '{react,react-dom,react-dom/client,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: '{react-intl,intl-messageformat}', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet,react-router-dom}', + group: 'external', + position: 'before', + }, + // Immutable / Redux / data store + { + pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', + group: 'external', + position: 'before', + }, + // Internal packages + { + pattern: '{mastodon/**}', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: [], + }, + ], + + 'promise/always-return': 'off', + 'promise/catch-or-return': [ + 'error', + { + allowFinally: true, + }, + ], + 'promise/no-callback-in-promise': 'off', + 'promise/no-nesting': 'off', + 'promise/no-promise-in-callback': 'off', + + 'formatjs/blocklist-elements': 'error', + 'formatjs/enforce-default-message': ['error', 'literal'], + 'formatjs/enforce-description': 'off', // description values not currently used + 'formatjs/enforce-id': 'off', // Explicit IDs are used in the project + 'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx + 'formatjs/enforce-plural-rules': 'error', + 'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming + 'formatjs/no-complex-selectors': 'error', + 'formatjs/no-emoji': 'error', + 'formatjs/no-id': 'off', // IDs are used for translation keys + 'formatjs/no-invalid-icu': 'error', + 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings + 'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx + 'formatjs/no-multiple-whitespaces': 'error', + 'formatjs/no-offset': 'error', + 'formatjs/no-useless-message': 'error', + 'formatjs/prefer-formatted-message': 'error', + 'formatjs/prefer-pound-in-plural': 'error', + + 'jsdoc/check-types': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns': 'off', + }, + + overrides: [ + { + files: [ + '*.config.js', + '.*rc.js', + 'ide-helper.js', + 'config/webpack/**/*', + 'config/formatjs-formatter.js', + ], + + env: { + commonjs: true, + }, + + parserOptions: { + sourceType: 'script', + }, + + rules: { + 'import/no-commonjs': 'off', + }, + }, + { + files: [ + '**/*.ts', + '**/*.tsx', + ], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/strict-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended-typescript', + 'plugin:prettier/recommended', + ], + + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, + + rules: { + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + "@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}], + + 'jsdoc/require-jsdoc': 'off', + + // Those rules set stricter rules for TS files + // to enforce better practices when converting from JS + 'import/no-default-export': 'warn', + 'react/prefer-stateless-function': 'warn', + 'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }], + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform + 'react/prop-types': 'off', + }, + }, + { + files: [ + '**/__tests__/*.js', + '**/__tests__/*.jsx', + ], + + env: { + jest: true, + }, + }, + { + files: [ + 'streaming/**/*', + ], + rules: { + 'import/no-commonjs': 'off', + }, + }, + ], +}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fa7a0c5353..be750a5e41 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,3 @@ -custom: https://fantia.jp/fanclubs/484677 +patreon: mastodon +open_collective: mastodon +custom: https://sponsor.joinmastodon.org diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml deleted file mode 100644 index 10421eed7b..0000000000 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: ãƒã‚°å ±å‘Š -description: kmyblueã®ãƒã‚°å ±å‘Šï¼ˆãŸã ã—情報改竄ã€ç§˜å¯†æƒ…å ±ã®æ¼æ´©ã€ã‚·ã‚¹ãƒ†ãƒ ã®ç ´æãªã©ãŒç™ºç”Ÿã™ã‚‹ãƒã‚°ã¯ã€ã“ã¡ã‚‰ã§ã¯ãªã「Securityã€ã‚¿ãƒ–よりセキュリティインシデントã¨ã—ã¦å ±å‘Šã—ã¦ãã ã•ã„) -labels: [bug] -body: - - type: textarea - attributes: - label: ãƒã‚°ã®å†ç¾æ‰‹é † - description: ã©ã®ã‚ˆã†ã«æ“作ã—ãŸã‚‰ãƒã‚°ãŒç™ºç”Ÿã—ãŸã®ã‹ã€ãƒã‚°ãŒç™ºç”Ÿã™ã‚‹ç›´å‰ã¾ã§ã®æ‰‹é †ã‚’順番ã«è©³ã—ãæ•™ãˆã¦ãã ã•ã„ - value: | - 1. - 2. - 3. - ... - validations: - required: true - - type: textarea - attributes: - label: 期待ã™ã‚‹å‹•作 - description: ã©ã®ã‚ˆã†ã«å‹•ã„ã¦ã»ã—ã‹ã£ãŸã§ã™ã‹ï¼Ÿ - validations: - required: true - - type: textarea - attributes: - label: 実際ã®å‹•作 - description: ã©ã®ã‚ˆã†ãªãƒã‚°ãŒç™ºç”Ÿã—ã¾ã—ãŸã‹ï¼Ÿ - validations: - required: true - - type: textarea - attributes: - label: 詳ã—ã„æƒ…å ± - validations: - required: false - - type: input - attributes: - label: ãƒã‚°ãŒç™ºç”Ÿã—ãŸkmyblueサーãƒãƒ¼ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ - description: サーãƒãƒ¼å›ºæœ‰ã®å•題ã®å¯èƒ½æ€§ã‚‚ã‚りã¾ã™ã®ã§ã€ãƒ—ライãƒã‚·ãƒ¼ä¸Šå¯èƒ½ãªç¯„囲内ã§ã€ã§ãã‚‹ã ã‘書ã„ã¦ãã ã•ã„ - placeholder: kmy.blue - validations: - required: false - - type: input - attributes: - label: ãƒã‚°ãŒç™ºç”Ÿã—ãŸkmyblueã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ - description: | - Mastodonã§ã¯ãªãkmyblueã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’記述ã—ã¦ãã ã•ã„。例ãˆã°ãƒãƒ¼ã‚¸ãƒ§ãƒ³è¡¨è¨˜ãŒ `v4.2.0+kmyblue.5.1-LTS` ã®å ´åˆã€ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¯ `5.1`ã«ãªã‚Šã¾ã™ - - ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¯ã€PCã ã¨ç”»é¢å·¦ä¸‹ã€ã‚¹ãƒžãƒ›ã ã¨æ¦‚è¦ç”»é¢ã®ä¸€ç•ªä¸‹ã«æ›¸ã„ã¦ã‚りã¾ã™ - placeholder: '5.1' - validations: - required: true - - type: input - attributes: - label: ブラウザã®åå‰ - description: | - ブラウザã®åå‰ã‚’書ã„ã¦ãã ã•ã„。å¯èƒ½ã§ã‚れã°ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚‚併記ã—ã¦ãã ã•ã„ - placeholder: Firefox 105.0.3 - validations: - required: false - - type: input - attributes: - label: OS - description: | - ã‚ãªãŸã®OSã¨ã€ã§ãれã°ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚‚æ•™ãˆã¦ãã ã•ã„。スマホã®å ´åˆã¯ã€ã€ŒAndroidã€ã€ŒiPhoneã€ã«ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’ã¤ã‘ã¦ãã ã•ã„ - placeholder: Windows11 - validations: - required: false - - type: textarea - attributes: - label: ãã®ä»–ã®è©³ç´°æƒ…å ± - description: | - ã‚ãªãŸã®ç’°å¢ƒãŒç‰¹æ®Šãªå ´åˆã€è©³ã—ã„ã“ã¨ã‚’æ•™ãˆã¦ãã ã•ã„(例: VPSã€torã€å­¦å†…LANãªã©ï¼‰ - - サーãƒãƒ¼ç®¡ç†è€…ã®å ´åˆã¯ã€Rubyã€Node.jsã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€Cloudflareã®ä½¿ç”¨å¯å¦ãªã©ã‚‚å¯èƒ½ãªã‚‰æ›¸ã„ã¦ãã ã•ã„ - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/1.web_bug_report.yml b/.github/ISSUE_TEMPLATE/1.web_bug_report.yml new file mode 100644 index 0000000000..20e27d103c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.web_bug_report.yml @@ -0,0 +1,76 @@ +name: Bug Report (Web Interface) +description: If you are using Mastodon's web interface and something is not working as expected +labels: [bug, 'status/to triage', 'area/web interface'] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: true + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` + placeholder: v4.1.2 + validations: + required: true + - type: input + attributes: + label: Browser name and version + description: | + What browser are you using when getting this bug? Please specify the version as well. + placeholder: Firefox 105.0.3 + validations: + required: true + - type: input + attributes: + label: Operating system + description: | + What OS are you running? Please specify the version as well. + placeholder: macOS 13.4.1 + validations: + required: true + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have. This can include the full error log, inspector's output… + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2.feature_request.yml b/.github/ISSUE_TEMPLATE/2.feature_request.yml deleted file mode 100644 index 10fb4bb23b..0000000000 --- a/.github/ISSUE_TEMPLATE/2.feature_request.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: æ©Ÿèƒ½è¦æœ› -description: æ©Ÿèƒ½ã®ææ¡ˆ -labels: [enhancement] -body: - - type: textarea - attributes: - label: 欲ã—ã„æ©Ÿèƒ½ - description: 欲ã—ã„æ©Ÿèƒ½ã®è©³ç´°ã‚’書ã„ã¦ãã ã•ã„ - validations: - required: true - - type: textarea - attributes: - label: å¿…è¦æ€§ - description: ã“ã®æ©Ÿèƒ½ã¯ã‚ãªãŸã«ã¨ã£ã¦ãªãœå¿…è¦ã§ã—ょã†ã‹ï¼Ÿã©ã†ã„ã£ãŸçжæ³ã§ä½¿ã‚れるもã®ã§ã™ã‹ï¼Ÿ - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/2.server_bug_report.yml b/.github/ISSUE_TEMPLATE/2.server_bug_report.yml new file mode 100644 index 0000000000..49d5f57209 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2.server_bug_report.yml @@ -0,0 +1,65 @@ +name: Bug Report (server / API) +description: | + If something is not working as expected, but is not from using the web interface. +labels: [bug, 'status/to triage'] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: false + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` + placeholder: v4.1.2 + validations: + required: false + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have, like logs or error traces + value: | + If this is happening on your own Mastodon server, please fill out those: + - Ruby version: (from `ruby --version`, eg. v3.1.2) + - Node.js version: (from `node --version`, eg. v18.16.0) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/3.feature_request.yml b/.github/ISSUE_TEMPLATE/3.feature_request.yml new file mode 100644 index 0000000000..2cabcf61e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3.feature_request.yml @@ -0,0 +1,22 @@ +name: Feature Request +description: I have a suggestion +labels: [suggestion] +body: + - type: markdown + attributes: + value: | + Please use a concise and distinct title for the issue. + + Consider: Could it be implemented as a 3rd party app using the REST API instead? + - type: textarea + attributes: + label: Pitch + description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before. + validations: + required: true + - type: textarea + attributes: + label: Motivation + description: Why do you think this feature is needed? Who would benefit from it? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/3.spec_change_request.yml b/.github/ISSUE_TEMPLATE/3.spec_change_request.yml deleted file mode 100644 index e71befe859..0000000000 --- a/.github/ISSUE_TEMPLATE/3.spec_change_request.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: ä»•æ§˜å¤‰æ›´ãƒ»æ”¹å–„è¦æœ› -description: 既存ã®ä»•様や挙動変更ã®è¦æœ› -labels: [specchange] -body: - - type: markdown - attributes: - value: æ„図ã—ãŸã‚‚ã®ã¨ã¯æ˜Žã‚‰ã‹ã«ç•°ãªã‚‹æŒ™å‹•ã‚’ã—ã¦ã„ã‚‹ã‚‚ã®ã¯ãƒã‚°ã¨ã—ã¦ã€ã‚‚ã¨ã‚‚ã¨ä»•様ã¨ã—ã¦æ±ºã‚られãŸå‹•ãã‚’ã—ã¦ã„ã‚‹ã‚‚ã®ã‚’変更ã—ãŸã„ã¨ãã¯ã“ã¡ã‚‰ã§ãŠé¡˜ã„ã—ã¾ã™ - - type: textarea - attributes: - label: 挙動を変更ã—ã¦ã»ã—ã„æ©Ÿèƒ½ã‚„動作 - validations: - required: true - - type: textarea - attributes: - label: ç¾åœ¨ã®æŒ™å‹• - validations: - required: true - - type: textarea - attributes: - label: 変更ã—ã¦ã»ã—ã„æ–°ã—ã„æŒ™å‹• - validations: - required: true - - type: textarea - attributes: - label: å¿…è¦æ€§ - description: ã“ã®å¤‰æ›´ã¯ã‚ãªãŸã«ã¨ã£ã¦ãªãœå¿…è¦ã§ã—ょã†ã‹ï¼Ÿã©ã†ã„ã£ãŸçжæ³ã§ä½¿ã‚れるもã®ã§ã™ã‹ï¼Ÿ - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml deleted file mode 100644 index fa9bfc7c80..0000000000 --- a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Deployment troubleshooting -description: | - You are a server administrator and you are encountering a technical issue during installation, upgrade or operations of Mastodon. -labels: ['status/to triage'] -type: 'Troubleshooting' -body: - - type: markdown - attributes: - value: | - Make sure that you are submitting a new bug that was not previously reported or already fixed. - - Please use a concise and distinct title for the issue. - - type: textarea - attributes: - label: Steps to reproduce the problem - description: What were you trying to do? - value: | - 1. - 2. - 3. - ... - validations: - required: true - - type: input - attributes: - label: Expected behaviour - description: What should have happened? - validations: - required: true - - type: input - attributes: - label: Actual behaviour - description: What happened? - validations: - required: true - - type: textarea - attributes: - label: Detailed description - validations: - required: false - - type: input - attributes: - label: Mastodon instance - description: The address of the Mastodon instance where you experienced the issue - placeholder: mastodon.social - validations: - required: true - - type: input - attributes: - label: Mastodon version - description: | - This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1` - placeholder: v4.3.0 - validations: - required: false - - type: textarea - attributes: - label: Environment - description: | - Details about your environment, like how Mastodon is deployed, if containers are used, version numbers, etc. - value: | - Please at least include those informations: - - Operating system: (eg. Ubuntu 22.04) - - Ruby version: (from `ruby --version`, eg. v3.4.1) - - Node.js version: (from `node --version`, eg. v20.18.0) - validations: - required: false - - type: textarea - attributes: - label: Technical details - description: | - Any additional technical details you may have, like logs or error traces - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358db1..f5d3196528 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ -blank_issues_enabled: true +blank_issues_enabled: false +contact_links: + - name: GitHub Discussions + url: https://github.com/mastodon/mastodon/discussions + about: Please ask and answer questions here. diff --git a/.github/actions/setup-javascript/action.yml b/.github/actions/setup-javascript/action.yml deleted file mode 100644 index 808adc7de6..0000000000 --- a/.github/actions/setup-javascript/action.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: 'Setup Javascript' -description: 'Setup a Javascript environment ready to run the Mastodon code' -inputs: - onlyProduction: - description: Only install production dependencies - default: 'false' - -runs: - using: 'composite' - steps: - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - # The following is needed because we can not use `cache: true` for `setup-node`, as it does not support Corepack yet and mess up with the cache location if ran after Node is installed - - name: Enable corepack - shell: bash - run: corepack enable - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - shell: bash - run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - - - 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 }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install all yarn packages - shell: bash - run: yarn install --immutable - if: inputs.onlyProduction == 'false' - - - name: Install all production yarn packages - shell: bash - run: yarn workspaces focus --production - if: inputs.onlyProduction != 'false' diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml deleted file mode 100644 index 3e232f134c..0000000000 --- a/.github/actions/setup-ruby/action.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: 'Setup RUby' -description: 'Setup a Ruby environment ready to run the Mastodon code' -inputs: - ruby-version: - description: The Ruby version to install - default: '.ruby-version' - additional-system-dependencies: - description: 'Additional packages to install' - -runs: - using: 'composite' - steps: - - name: Install system dependencies - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }} - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ inputs.ruby-version }} - bundler-cache: true diff --git a/.github/codecov.yml b/.github/codecov.yml deleted file mode 100644 index 21af6d0d45..0000000000 --- a/.github/codecov.yml +++ /dev/null @@ -1,13 +0,0 @@ -comment: false # Do not leave PR comments -coverage: - status: - project: - default: - # GitHub status check is not blocking - informational: true - patch: - default: - # GitHub status check is not blocking - informational: true -github_checks: - annotations: false diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e638b9c548..879a564e1c 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -2,21 +2,17 @@ $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ 'config:recommended', - 'customManagers:dockerfileVersions', ':labels(dependencies)', + ':maintainLockFilesMonthly', // update non-direct dependencies monthly ':prConcurrentLimitNone', // Remove limit for open PRs at any time. ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. ], - rebaseWhen: 'conflicted', minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it // packageRules order is important, they are applied from top to bottom and are merged, // meaning the most important ones must be at the bottom, for example grouping rules // If we do not want a package to be grouped with others, we need to set its groupName // to `null` after any other rule set it to something. dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).', - postUpdateOptions: ['yarnDedupeHighest'], - // The types are now included in recent versions,we ignore them here until we upgrade and remove the dependency - ignoreDeps: ['@types/emoji-mart'], packageRules: [ { // Require Dependency Dashboard Approval for major version bumps of these node packages @@ -26,7 +22,6 @@ 'react-hotkeys', // Requires code changes // Requires Webpacker upgrade or replacement - '@svgr/webpack', '@types/webpack', 'babel-loader', 'compression-webpack-plugin', @@ -54,6 +49,7 @@ matchManagers: ['bundler'], matchPackageNames: [ 'rack', // Needs to be synced with Rails version + 'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x 'strong_migrations', // Requires manual upgrade 'sidekiq', // Requires manual upgrade 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version @@ -63,7 +59,7 @@ dependencyDashboardApproval: true, }, { - // Update GitHub Actions and Docker images weekly + // Update Github Actions and Docker images weekly matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], extends: ['schedule:weekly'], }, @@ -90,7 +86,6 @@ }, { // Update devDependencies every week, with one grouped PR - matchManagers: ['npm'], matchDepTypes: 'devDependencies', matchUpdateTypes: ['patch', 'minor'], groupName: 'devDependencies (non-major)', @@ -99,30 +94,14 @@ { // Group all eslint-related packages with `eslint` in the same PR matchManagers: ['npm'], - matchPackageNames: [ - 'eslint', - 'eslint-*', - 'typescript-eslint', - '@eslint/*', - 'globals', - ], + matchPackageNames: ['eslint'], + matchPackagePrefixes: ['eslint-', '@typescript-eslint/'], matchUpdateTypes: ['patch', 'minor'], groupName: 'eslint (non-major)', }, - { - // Group actions/*-artifact in the same PR - matchManagers: ['github-actions'], - matchPackageNames: [ - 'actions/download-artifact', - 'actions/upload-artifact', - ], - matchUpdateTypes: ['major'], - groupName: 'artifact actions (major)', - }, { // Update @types/* packages every week, with one grouped PR - matchManagers: ['npm'], - matchPackageNames: '@types/*', + matchPackagePrefixes: '@types/', matchUpdateTypes: ['patch', 'minor'], groupName: 'DefinitelyTyped types (non-major)', extends: ['schedule:weekly'], @@ -136,27 +115,6 @@ ], 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', 'rubocop-*'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'RuboCop (non-major)', - }, - { - // Group all RSpec packages with `rspec` in the same PR - matchManagers: ['bundler'], - matchPackageNames: ['rspec', 'rspec-*'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'RSpec (non-major)', - }, - { - // Group all opentelemetry-ruby packages in the same PR - matchManagers: ['bundler'], - matchPackageNames: ['opentelemetry-*'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'opentelemetry-ruby (non-major)', - }, // Add labels depending on package manager { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, diff --git a/.github/stylelint-matcher.json b/.github/stylelint-matcher.json new file mode 100644 index 0000000000..cdfd4086bd --- /dev/null +++ b/.github/stylelint-matcher.json @@ -0,0 +1,21 @@ +{ + "problemMatcher": [ + { + "owner": "stylelint", + "pattern": [ + { + "regexp": "^([^\\s].*)$", + "file": 1 + }, + { + "regexp": "^\\s+((\\d+):(\\d+))?\\s+(✖|×)\\s+(.*)\\s{2,}(.*)$", + "line": 2, + "column": 3, + "message": 5, + "code": 6, + "loop": true + } + ] + } + ] +} diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml deleted file mode 100644 index d3cb4e5e0a..0000000000 --- a/.github/workflows/build-security.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Build security nightly container image -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - compute-suffix: - runs-on: ubuntu-latest - if: github.repository == 'mastodon/mastodon' - steps: - - id: version_vars - env: - TZ: Etc/UTC - run: | - echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT - outputs: - prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} - - build-image: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: Dockerfile - cache: false - push_to_images: | - tootsuite/mastodon - ghcr.io/mastodon/mastodon - version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - flavor: | - latest=auto - tags: | - type=raw,value=edge - type=raw,value=nightly - type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} - secrets: inherit - - build-image-streaming: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: streaming/Dockerfile - cache: false - push_to_images: | - tootsuite/mastodon-streaming - ghcr.io/mastodon/mastodon-streaming - version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - flavor: | - latest=auto - tags: | - type=raw,value=edge - type=raw,value=nightly - type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} - secrets: inherit diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml index c96e4429af..bfb93a36cd 100644 --- a/.github/workflows/bundler-audit.yml +++ b/.github/workflows/bundler-audit.yml @@ -1,22 +1,19 @@ name: Bundler Audit on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' paths: - 'Gemfile*' - '.ruby-version' + - '.bundler-audit.yml' - '.github/workflows/bundler-audit.yml' pull_request: paths: - 'Gemfile*' - '.ruby-version' + - '.bundler-audit.yml' - '.github/workflows/bundler-audit.yml' schedule: @@ -26,17 +23,18 @@ jobs: security: runs-on: ubuntu-latest - env: - BUNDLE_ONLY: development - steps: - name: Clone repository uses: actions/checkout@v4 + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: + ruby-version: .ruby-version bundler-cache: true - name: Run bundler-audit - run: bin/bundler-audit check --update + run: bundle exec bundler-audit diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 63529e4f16..39cf32ddc4 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -2,19 +2,9 @@ name: Check i18n on: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches: [main] pull_request: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches: [main] env: RAILS_ENV: test @@ -24,16 +14,30 @@ permissions: jobs: check-i18n: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile - name: Check for missing strings in English JSON run: | @@ -41,18 +45,18 @@ jobs: git diff --exit-code - name: Check locale file normalization - run: bin/i18n-tasks check-normalized + run: bundle exec i18n-tasks check-normalized - name: Check for unused strings - run: bin/i18n-tasks unused + run: bundle exec i18n-tasks unused - name: Check for missing strings in English YML run: | - bin/i18n-tasks add-missing -l en + bundle exec i18n-tasks add-missing -l en git diff --exit-code - name: Check for wrong string interpolations - run: bin/i18n-tasks check-consistent-interpolations + run: bundle exec i18n-tasks check-consistent-interpolations - name: Check that all required locale files exist - run: bin/rake repo:check_locales_files + run: bundle exec rake repo:check_locales_files diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 827629d8e6..3b40c3fd07 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,21 +1,11 @@ name: 'CodeQL' on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches: ['main'] pull_request: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + # The branches below must be a subset of the branches above + branches: ['main'] schedule: - cron: '22 6 * * 1' @@ -41,7 +31,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +44,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v2 # â„¹ï¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -67,6 +57,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v2 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml deleted file mode 100644 index 6d9a058629..0000000000 --- a/.github/workflows/crowdin-download-stable.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Crowdin / Download translations (stable branches) -on: - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - download-translations-stable: - runs-on: ubuntu-latest - if: github.repository == 'mastodon/mastodon' - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Increase Git http.postBuffer - # This is needed due to a bug in Ubuntu's cURL version? - # See https://github.com/orgs/community/discussions/55820 - run: | - git config --global http.version HTTP/1.1 - git config --global http.postBuffer 157286400 - - # Download the translation files from Crowdin - - name: crowdin action - uses: crowdin/github-action@v2 - with: - upload_sources: false - upload_translations: false - download_translations: true - crowdin_branch_name: ${{ github.base_ref || github.ref_name }} - push_translations: false - create_pull_request: false - env: - CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - # As the files are extracted from a Docker container, they belong to root:root - # We need to fix this before the next steps - - name: Fix file permissions - run: sudo chown -R runner:docker . - - # This is needed to run the normalize step - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Run i18n normalize task - run: bin/i18n-tasks normalize - - # Create or update the pull request - - name: Create Pull Request - uses: peter-evans/create-pull-request@v7.0.6 - with: - commit-message: 'New Crowdin translations' - title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' - author: 'GitHub Actions ' - body: | - New Crowdin translations, automated with GitHub Actions - - See `.github/workflows/crowdin-download.yml` - - This PR will be updated every day with new translations. - - Due to a limitation in GitHub Actions, checks are not running on this PR without manual action. - If you want to run the checks, then close and re-open it. - branch: i18n/crowdin/translations-${{ github.base_ref || github.ref_name }} - base: ${{ github.base_ref || github.ref_name }} - labels: i18n diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml deleted file mode 100644 index a190ece7df..0000000000 --- a/.github/workflows/format-check.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Check formatting -on: - merge_group: - push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' - 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 diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index ffab4880e1..bd775dba20 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -1,13 +1,9 @@ name: CSS Linting on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -39,8 +35,18 @@ jobs: - name: Clone repository uses: actions/checkout@v4 - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - uses: xt0rted/stylelint-problem-matcher@v1 + + - run: echo "::add-matcher::.github/stylelint-matcher.json" - name: Stylelint - run: yarn lint:css --custom-formatter @csstools/stylelint-formatter-github + run: yarn lint:sass diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index c596261eb0..ca9bd66a4a 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -1,13 +1,9 @@ name: Haml Linting on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' + - 'renovate/**' paths: - '.github/workflows/haml-lint-problem-matcher.json' - '.github/workflows/lint-haml.yml' @@ -30,20 +26,22 @@ on: jobs: lint: runs-on: ubuntu-latest - - env: - BUNDLE_ONLY: development - steps: - name: Clone repository uses: actions/checkout@v4 + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: + ruby-version: .ruby-version bundler-cache: true - name: Run haml-lint run: | echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bin/haml-lint --reporter github + bundle exec haml-lint diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 13468e7799..67d28589cb 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -1,20 +1,16 @@ name: JavaScript Linting on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' - 'tsconfig.json' - '.nvmrc' - '.prettier*' - - 'eslint.config.mjs' + - '.eslint*' - '**/*.js' - '**/*.jsx' - '**/*.ts' @@ -28,7 +24,7 @@ on: - 'tsconfig.json' - '.nvmrc' - '.prettier*' - - 'eslint.config.mjs' + - '.eslint*' - '**/*.js' - '**/*.jsx' - '**/*.ts' @@ -43,11 +39,17 @@ jobs: - name: Clone repository uses: actions/checkout@v4 - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile - name: ESLint - run: yarn workspaces foreach --all --parallel run lint:js --max-warnings 0 + run: yarn lint:js --max-warnings 0 - name: Typecheck run: yarn typecheck diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml new file mode 100644 index 0000000000..1d98c52673 --- /dev/null +++ b/.github/workflows/lint-json.yml @@ -0,0 +1,44 @@ +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 Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml new file mode 100644 index 0000000000..1b3f92c972 --- /dev/null +++ b/.github/workflows/lint-md.yml @@ -0,0 +1,44 @@ +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 Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn lint:md diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index 5bb67b108c..92882a084d 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -1,18 +1,13 @@ name: Ruby Linting on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' + - 'renovate/**' paths: - 'Gemfile*' - '.rubocop*.yml' - '.ruby-version' - - 'bin/rubocop' - 'config/brakeman.ignore' - '**/*.rb' - '**/*.rake' @@ -23,7 +18,6 @@ on: - 'Gemfile*' - '.rubocop*.yml' - '.ruby-version' - - 'bin/rubocop' - 'config/brakeman.ignore' - '**/*.rb' - '**/*.rake' @@ -33,24 +27,25 @@ jobs: lint: runs-on: ubuntu-latest - env: - BUNDLE_ONLY: development - steps: - name: Clone repository uses: actions/checkout@v4 + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: + ruby-version: .ruby-version bundler-cache: true - name: Set-up RuboCop Problem Matcher uses: r7kamura/rubocop-problem-matchers-action@v1 - name: Run rubocop - run: bin/rubocop + run: bundle exec rubocop - name: Run brakeman if: always() # Run both checks, even if the first failed - run: bin/brakeman + run: bundle exec brakeman diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml new file mode 100644 index 0000000000..e77cc98891 --- /dev/null +++ b/.github/workflows/lint-yml.yml @@ -0,0 +1,46 @@ +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 Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn lint:yml diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml index f0fc8b0db7..06d835c090 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -10,7 +10,6 @@ permissions: jobs: label-rebase-needed: runs-on: ubuntu-latest - if: github.repository == 'mastodon/mastodon' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -18,7 +17,7 @@ jobs: steps: - name: Check for merge conflicts - uses: eps1lon/actions-label-merge-conflict@v3 + uses: eps1lon/actions-label-merge-conflict@releases/2.x with: dirtyLabel: 'rebase needed :construction:' repoToken: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index f962f5c36f..0ef1d9b7c8 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -1,13 +1,9 @@ name: JavaScript Testing on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -39,8 +35,14 @@ jobs: - name: Clone repository uses: actions/checkout@v4 - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' - - name: JavaScript testing + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Jest testing run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml new file mode 100644 index 0000000000..59287e88cf --- /dev/null +++ b/.github/workflows/test-migrations-one-step.yml @@ -0,0 +1,111 @@ +name: Test one step migrations +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +jobs: + pre_job: + runs-on: ubuntu-latest + + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml", "lib/tasks/tests.rake"]' + + test: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + + services: + postgres: + image: postgres:${{ matrix.postgres}} + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + CONTINUOUS_INTEGRATION: true + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + BUNDLE_WITHOUT: 'development production' + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 + + steps: + - uses: actions/checkout@v4 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Create database + run: './bin/rails db:create' + + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + + - name: Run all remaining migrations + run: './bin/rails db:migrate' + + - name: Check migration result + run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml new file mode 100644 index 0000000000..8f3c84d8f3 --- /dev/null +++ b/.github/workflows/test-migrations-two-step.yml @@ -0,0 +1,119 @@ +name: Test two step migrations +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +jobs: + pre_job: + runs-on: ubuntu-latest + + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml", "lib/tasks/tests.rake"]' + + test: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + + services: + postgres: + image: postgres:${{ matrix.postgres}} + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + CONTINUOUS_INTEGRATION: true + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + BUNDLE_WITHOUT: 'development production' + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 + + steps: + - uses: actions/checkout@v4 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Create database + run: './bin/rails db:create' + + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run pre-deployment migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + + - name: Run all remaining pre-deployment migrations + run: './bin/rails db:migrate' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Run all post-deployment migrations + run: './bin/rails db:migrate' + + - name: Check migration result + run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml deleted file mode 100644 index c4a716e8f9..0000000000 --- a/.github/workflows/test-migrations.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Historical data migration test - -on: - merge_group: - push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' - paths: - - 'Gemfile*' - - '.ruby-version' - - '**/*.rb' - - '.github/workflows/test-migrations.yml' - - 'lib/tasks/tests.rake' - - 'lib/tasks/db.rake' - - pull_request: - paths: - - 'Gemfile*' - - '.ruby-version' - - '**/*.rb' - - '.github/workflows/test-migrations.yml' - - 'lib/tasks/tests.rake' - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - - matrix: - postgres: - - 14-alpine - - 15-alpine - - 16-alpine - - 17-alpine - - services: - postgres: - image: postgres:${{ matrix.postgres}} - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - RAILS_ENV: test - BUNDLE_CLEAN: true - BUNDLE_FROZEN: true - BUNDLE_WITHOUT: 'development:production' - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Ensure no errors with `db:prepare` - run: | - bin/rails db:drop - bin/rails db:prepare - bin/rails db:migrate - - - name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS - run: | - bin/rails db:drop - SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare - bin/rails db:migrate - - - name: Test "one step migration" flow - run: | - bin/rails db:drop - bin/rails db:create - bin/rails tests:migrations:prepare_database - bin/rails db:migrate - bin/rails tests:migrations:check_database - - - name: Test "two step migration" flow - run: | - bin/rails db:drop - bin/rails db:create - SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database - - # Migrate up to v4.2.0 breakpoint - bin/rails db:migrate VERSION=20230907150100 - - # Migrate the rest - SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate - bin/rails db:migrate - bin/rails tests:migrations:check_database diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index fd4c666059..12b991f762 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -1,14 +1,10 @@ name: Ruby Testing on: - merge_group: push: - branches: - - 'main' - - 'kb*' - - 'upstream-*' - - 'releases/*' - - 'stable-*' + branches-ignore: + - 'dependabot/**' + - 'renovate/**' pull_request: env: @@ -32,47 +28,42 @@ jobs: env: RAILS_ENV: ${{ matrix.mode }} BUNDLE_WITH: ${{ matrix.mode }} - SECRET_KEY_BASE_DUMMY: 1 + OTP_SECRET: precompile_placeholder + SECRET_KEY_BASE: precompile_placeholder steps: - uses: actions/checkout@v4 - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - name: Set up Node.js + uses: actions/setup-node@v3 with: - onlyProduction: 'true' + cache: yarn + node-version-file: '.nvmrc' - - name: Cache assets from compilation - uses: actions/cache@v4 - with: - path: | - public/assets - public/packs - public/packs-test - tmp/cache/webpacker - key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }} - restore-keys: | - ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }} - ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }} - ${{ matrix.mode }}-assets-main - ${{ matrix.mode }}-assets - - - name: Precompile assets - run: |- - bin/rails assets:precompile - - - name: Archive asset artifacts + - name: Install native Ruby dependencies run: | - tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev - - uses: actions/upload-artifact@v4 + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - run: yarn --frozen-lockfile --production + - name: Precompile assets + # Previously had set this, but it's not supported + # export NODE_OPTIONS=--openssl-legacy-provider + run: |- + ./bin/rails assets:precompile + + - uses: actions/upload-artifact@v3 if: matrix.mode == 'test' with: path: |- - ./artifacts.tar.gz + ./public/assets + ./public/packs-test name: ${{ github.sha }} retention-days: 0 @@ -90,9 +81,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 5432:5432 @@ -100,9 +91,9 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 6379:6379 @@ -110,7 +101,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} + DISABLE_SIMPLECOV: true RAILS_ENV: test ALLOW_NOPAM: true PAM_ENABLED: true @@ -121,148 +112,48 @@ jobs: SAML_ENABLED: true CAS_ENABLED: true BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} + CI_JOBS: ${{ matrix.ci_job }}/4 ES_ENABLED: false strategy: fail-fast: false matrix: ruby-version: - - '3.2' - - '3.3' + - '3.0' + - '3.1' - '.ruby-version' + ci_job: + - 1 + - 2 + - 3 + - 4 steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: - path: './' + path: './public' name: ${{ github.sha }} - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz + - name: Update package index + run: sudo apt-get update - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick libpam-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev - - - name: Load database schema - run: | - bin/rails db:setup - bin/flatware fan bin/rails db:test:prepare - - - name: Cache RSpec persistence file - uses: actions/cache@v4 - with: - path: | - tmp/rspec/examples.txt - key: rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }} - restore-keys: | - rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}-${{ matrix.ruby-version }} - rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }} - rspec-persistence-${{ github.head_ref || github.ref_name }} - rspec-persistence-main - rspec-persistence - - - run: bin/flatware rspec -r ./spec/flatware_helper.rb - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v5 - with: - files: coverage/lcov/*.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - test-libvips: - name: Libvips tests - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - MASTODON_USE_LIBVIPS: true - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.2' - - '3.3' - - '.ruby-version' - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg libpam-dev + bundler-cache: true - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' - - run: bin/rspec --tag attachment_processing - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v5 - with: - files: coverage/lcov/mastodon.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - run: bundle exec rake rspec_chunked test-e2e: name: End to End testing @@ -279,9 +170,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 5432:5432 @@ -289,9 +180,9 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 6379:6379 @@ -299,62 +190,71 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres + DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test ES_ENABLED: false - LOCAL_DOMAIN: localhost:3000 - LOCAL_HTTPS: false strategy: fail-fast: false matrix: ruby-version: - - '3.2' - - '3.3' + - '3.0' + - '3.1' - '.ruby-version' steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: - path: './' + path: './public' name: ${{ github.sha }} - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz + - name: Update package index + run: sudo apt-get update - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + bundler-cache: true - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - run: yarn --frozen-lockfile - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' - - run: bin/rspec spec/system --tag streaming --tag js + - run: bundle exec rake spec:system - name: Archive logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: e2e-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: e2e-screenshots - path: tmp/capybara/ + path: tmp/screenshots/ test-search: - name: Elastic Search integration testing + name: Testing search runs-on: ubuntu-latest needs: @@ -368,9 +268,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 5432:5432 @@ -378,36 +278,22 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 6379:6379 elasticsearch: - image: ${{ contains(matrix.search-image, 'elasticsearch') && matrix.search-image || '' }} + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13 env: discovery.type: single-node xpack.security.enabled: false options: >- --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 2s - --health-timeout 3s - --health-retries 50 - ports: - - 9200:9200 - - opensearch: - image: ${{ contains(matrix.search-image, 'opensearch') && matrix.search-image || '' }} - env: - discovery.type: single-node - DISABLE_INSTALL_DEMO_CONFIG: true - DISABLE_SECURITY_PLUGIN: true - options: >- - --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 2s - --health-timeout 3s - --health-retries 50 + --health-interval 10s + --health-timeout 5s + --health-retries 10 ports: - 9200:9200 @@ -415,6 +301,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres + DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test ES_ENABLED: true @@ -425,139 +312,56 @@ jobs: fail-fast: false matrix: ruby-version: - - '3.2' - - '3.3' + - '3.0' + - '3.1' - '.ruby-version' - search-image: - - docker.elastic.co/elasticsearch/elasticsearch:7.17.13 - include: - - ruby-version: '.ruby-version' - search-image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2 - - ruby-version: '.ruby-version' - search-image: opensearchproject/opensearch:2 steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: - path: './' + path: './public' name: ${{ github.sha }} - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Update package index + run: sudo apt-get update + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + bundler-cache: true - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript + - run: yarn --frozen-lockfile - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' - - run: bin/rspec --tag search + - run: bundle exec rake spec:search - name: Archive logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: test-search-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: test-search-screenshots - path: tmp/capybara/ - - test-back-and-return: - name: Back to original and return test - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - ES_ENABLED: false - BACK_UPSTREAM_FORCE: true - - strategy: - fail-fast: false - matrix: - ruby-version: - - '.ruby-version' - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - name: Back to upstream schema - run: 'bundle exec rake dangerous:back_upstream' - - - name: Return to kmyblue - run: './bin/rails db:migrate' - - - run: bin/rspec - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v3 - with: - files: coverage/lcov/mastodon-back-ret.lcov + path: tmp/screenshots/ diff --git a/.gitignore b/.gitignore index 7d60baebf8..2bc8b18c8f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,15 +24,16 @@ /public/packs-test .env .env.production +.env.development /node_modules/ /build/ -# Ignore elasticsearch config -/.elasticsearch.yml - # Ignore Vagrant files .vagrant/ +# Ignore Capistrano customizations +/config/deploy/* + # Ignore IDE files .vscode/ .idea/ @@ -57,23 +58,8 @@ npm-debug.log yarn-error.log yarn-debug.log -# From https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - # Ignore vagrant log files *-cloudimg-console.log # Ignore Docker option files docker-compose.override.yml - -# Ignore dotenv .local files -.env*.local - -# Ignore local-only rspec configuration -.rspec-local diff --git a/.haml-lint.yml b/.haml-lint.yml index 7dbc88e9db..d1ed30b260 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -2,6 +2,7 @@ inherits_from: .haml-lint_todo.yml exclude: - 'vendor/**/*' + - lib/templates/haml/scaffold/_form.html.haml require: - ./lib/linter/haml_middle_dot.rb @@ -11,7 +12,3 @@ linters: enabled: true MiddleDot: enabled: true - LineLength: - max: 300 - ViewLength: - max: 200 # Override default value of 100 inherited from rubocop diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 841561291f..a839e3789f 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -1,36 +1,49 @@ # 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. +# on 2023-07-20 09:47:50 -0400 using Haml-Lint version 0.48.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 + # Offense count: 951 LineLength: - exclude: - - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - - 'app/views/admin/roles/_form.html.haml' + enabled: false - # Offense count: 9 + # Offense count: 22 + UnnecessaryStringOutput: + enabled: false + + # Offense count: 57 RuboCop: - exclude: - - 'app/views/home/index.html.haml' + enabled: false + # Offense count: 3 ViewLength: exclude: - - 'app/views/admin/accounts/index.html.haml' + - 'app/views/admin/accounts/show.html.haml' - 'app/views/admin/instances/show.html.haml' - - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - - 'app/views/admin/settings/discovery/show.html.haml' - - 'app/views/settings/preferences/appearance/show.html.haml' - - 'app/views/settings/preferences/other/show.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + # Offense count: 32 InstanceVariables: exclude: + - 'app/views/admin/reports/_actions.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/webhooks/_form.html.haml' + - 'app/views/auth/registrations/_status.html.haml' + - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' + - 'app/views/authorize_interactions/_post_follow_actions.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/relationships/_account.html.haml' + - 'app/views/shared/_og.html.haml' - 'app/views/application/_sidebar.html.haml' - - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - - 'app/views/admin/ng_words/keywords/_ng_word.html.haml' - - 'app/views/admin/ng_words/white_list/_specified_domain.html.haml' - - 'app/views/admin/sensitive_words/_sensitive_word.html.haml' + + # Offense count: 3 + IdNames: + exclude: + - 'app/views/authorize_interactions/error.html.haml' + - 'app/views/oauth/authorizations/error.html.haml' + - 'app/views/shared/_error_messages.html.haml' diff --git a/.husky/pre-commit b/.husky/pre-commit index 3723623171..d2ae35e84b 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + yarn lint-staged diff --git a/.nanoignore b/.nanoignore new file mode 100644 index 0000000000..80e9397035 --- /dev/null +++ b/.nanoignore @@ -0,0 +1,19 @@ +.DS_Store +.git/ +.gitignore + +.bundle/ +.cache/ +config/deploy/* +coverage +docs/ +.env +log/*.log +neo4j/ +node_modules/ +public/assets/ +public/system/ +spec/ +tmp/ +.vagrant/ +vendor/bundle/ diff --git a/.nvmrc b/.nvmrc index 744ca17ec0..b1b396bcfa 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.14 +20.7 diff --git a/.prettierignore b/.prettierignore index 80b4c0159e..91029f665d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -31,6 +31,9 @@ # Ignore Vagrant files .vagrant/ +# Ignore Capistrano customizations +/config/deploy/* + # Ignore IDE files .vscode/ .idea/ @@ -54,16 +57,8 @@ # 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 -/app/javascript/mastodon/features/emoji/emoji_sheet.json # Ignore locale files /app/javascript/mastodon/locales/*.json @@ -81,6 +76,3 @@ app/javascript/styles/mastodon/reset.scss # Ignore the generated AUTHORS.md AUTHORS.md - -# Process a few selected JS files -!lint-staged.config.js diff --git a/.prettierrc.js b/.prettierrc.js index 65ec869c33..af39b253f6 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,4 +1,4 @@ module.exports = { singleQuote: true, jsxSingleQuote: true -}; +} diff --git a/.profile b/.profile new file mode 100644 index 0000000000..f4826ea303 --- /dev/null +++ b/.profile @@ -0,0 +1 @@ +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread diff --git a/.rspec b/.rspec index 83e16f8044..9a8e706d09 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --color --require spec_helper +--format Fuubar diff --git a/.rubocop.yml b/.rubocop.yml index 1bbba515af..ef40e95a2a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,36 +1,214 @@ ---- -AllCops: - CacheRootDirectory: tmp - DisplayStyleGuide: true - Exclude: - - Vagrantfile - - config/initializers/json_ld* - - lib/mastodon/migration_helpers.rb - ExtraDetails: true - NewCops: enable - TargetRubyVersion: 3.2 # Oldest supported ruby version - -inherit_from: - - .rubocop/layout.yml - - .rubocop/metrics.yml - - .rubocop/naming.yml - - .rubocop/rails.yml - - .rubocop/rspec_rails.yml - - .rubocop/rspec.yml - - .rubocop/style.yml - - .rubocop/i18n.yml - - .rubocop/custom.yml - - .rubocop_todo.yml - - .rubocop/strict.yml +# Can be removed once all rules are addressed or moved to this file as documented overrides +inherit_from: .rubocop_todo.yml +# Used for merging with exclude lists with .rubocop_todo.yml inherit_mode: merge: - Exclude -plugins: - - rubocop-capybara - - rubocop-i18n - - rubocop-performance +require: - rubocop-rails - rubocop-rspec - - rubocop-rspec_rails + - rubocop-performance + - rubocop-capybara + - ./lib/linter/rubocop_middle_dot + +AllCops: + TargetRubyVersion: 3.0 # Set to minimum supported version of CI + DisplayCopNames: true + DisplayStyleGuide: true + ExtraDetails: true + UseCache: true + CacheRootDirectory: tmp + NewCops: enable # Opt-in to newly added rules + Exclude: + - 'db/schema.rb' + - 'bin/*' + - 'node_modules/**/*' + - 'Vagrantfile' + - 'vendor/**/*' + - 'lib/json_ld/*' # Generated files + - 'lib/templates/**/*' + +# Reason: Prefer Hashes without extreme indentation +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutfirsthashelementindentation +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength +Layout/LineLength: + Max: 320 # Default of 120 causes a duplicate entry in generated todo file + +# Reason: +# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier +Lint/UselessAccessModifier: + ContextCreatingMethods: + - class_methods + +## Disable most Metrics/*Length cops +# Reason: those are often triggered and force significant refactors when this happend +# but the team feel they are not really improving the code quality. + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength +Metrics/BlockLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength +Metrics/ClassLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength +Metrics/MethodLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength +Metrics/ModuleLength: + Enabled: false + +## End Disable Metrics/*Length cops + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize +Metrics/AbcSize: + Exclude: + - 'app/serializers/initial_state_serializer.rb' + - 'lib/mastodon/cli/*.rb' + - db/*migrate/**/* + +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting +Metrics/BlockNesting: + Exclude: + - 'lib/mastodon/cli/*.rb' + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity +Metrics/CyclomaticComplexity: + Exclude: + - 'app/lib/feed_manager.rb' + - 'app/policies/status_policy.rb' + - 'app/services/activitypub/process_account_service.rb' + - 'app/services/delivery_antenna_service.rb' + - 'app/services/post_status_service.rb' + - lib/mastodon/cli/*.rb + - db/*migrate/**/* + +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists +Metrics/ParameterLists: + CountKeywordArgs: false + +Metrics/PerceivedComplexity: + Exclude: + - 'app/policies/status_policy.rb' + - 'app/services/delivery_antenna_service.rb' + - 'app/services/post_status_service.rb' + +# Reason: Prevailing style is argument file paths +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath +Rails/FilePath: + EnforcedStyle: arguments + +# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus +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: Some single letter camel case files shouldn't be split +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath +RSpec/FilePath: + CustomTransform: + ActivityPub: activitypub # Ignore the snake_case due to the amount of files to rename + DeepL: deepl + FetchOEmbedService: fetch_oembed_service + JsonLdHelper: jsonld_helper + OEmbedController: oembed_controller + OStatus: ostatus + NodeInfoController: nodeinfo_controller # NodeInfo isn't snake_cased for any of the instances + Exclude: + - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder + - 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder + +# Reason: +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject +RSpec/NamedSubject: + EnforcedStyle: named_only + +# Reason: Prevailing style choice +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnottonot +RSpec/NotToNot: + EnforcedStyle: to_not + +# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus +RSpec/Rails/HttpStatus: + EnforcedStyle: numeric + +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren +Style/ClassAndModuleChildren: + Enabled: false + +# Reason: Classes mostly self-document with their names +# https://docs.rubocop.org/rubocop/cops_style.html#styledocumentation +Style/Documentation: + Enabled: false + +# Reason: Enforce modern Ruby style +# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax +Style/HashSyntax: + EnforcedStyle: ruby19_no_mixed_keys + +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals +Style/NumericLiterals: + AllowedPatterns: + - \d{4}_\d{2}_\d{2}_\d{6} # For DB migration date version number readability + +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylepercentliteraldelimiters +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%i': '()' + '%w': '()' + +# Reason: Prefer less indentation in conditional assignments +# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin +Style/RedundantBegin: + Enabled: false + +# Reason: Overridden to reduce implicit StandardError rescues +# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror +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: + Enabled: false + +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainarrayliteral +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: 'comma' + +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: 'comma' + +Style/MiddleDot: + Enabled: true diff --git a/.rubocop/custom.yml b/.rubocop/custom.yml deleted file mode 100644 index 63035837f8..0000000000 --- a/.rubocop/custom.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -require: - - ../lib/linter/rubocop_middle_dot - -Style/MiddleDot: - Enabled: true diff --git a/.rubocop/i18n.yml b/.rubocop/i18n.yml deleted file mode 100644 index de395d3a79..0000000000 --- a/.rubocop/i18n.yml +++ /dev/null @@ -1,12 +0,0 @@ -I18n/RailsI18n: - Enabled: true - Exclude: - - 'config/**/*' - - 'db/**/*' - - 'lib/**/*' - - 'spec/**/*' -I18n/GetText: - Enabled: false - -I18n/RailsI18n/DecorateStringFormattingUsingInterpolation: - Enabled: false diff --git a/.rubocop/layout.yml b/.rubocop/layout.yml deleted file mode 100644 index 487879ca2c..0000000000 --- a/.rubocop/layout.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -Layout/FirstHashElementIndentation: - EnforcedStyle: consistent - -Layout/LineLength: - Max: 300 # Default of 120 causes a duplicate entry in generated todo file diff --git a/.rubocop/metrics.yml b/.rubocop/metrics.yml deleted file mode 100644 index 2828d5e4a3..0000000000 --- a/.rubocop/metrics.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -Metrics/AbcSize: - Exclude: - - 'app/serializers/initial_state_serializer.rb' - - lib/mastodon/cli/*.rb - -Metrics/BlockLength: - Enabled: false - -Metrics/ClassLength: - Enabled: false - -Metrics/CyclomaticComplexity: - Exclude: - - 'app/lib/feed_manager.rb' - - 'app/policies/status_policy.rb' - - 'app/services/activitypub/process_account_service.rb' - - 'app/services/delivery_antenna_service.rb' - - 'app/services/post_status_service.rb' - - lib/mastodon/cli/*.rb - -Metrics/MethodLength: - Enabled: false - -Metrics/ModuleLength: - Enabled: false - -Metrics/ParameterLists: - CountKeywordArgs: false diff --git a/.rubocop/naming.yml b/.rubocop/naming.yml deleted file mode 100644 index da6ad4ac57..0000000000 --- a/.rubocop/naming.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -Naming/BlockForwarding: - EnforcedStyle: explicit diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml deleted file mode 100644 index bbd172e656..0000000000 --- a/.rubocop/rails.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -Rails/BulkChangeTable: - Enabled: false # Conflicts with strong_migrations features - -Rails/Delegate: - Enabled: false - -Rails/FilePath: - EnforcedStyle: arguments - -Rails/HttpStatus: - EnforcedStyle: numeric - -Rails/NegateInclude: - Enabled: false - -Rails/RakeEnvironment: - Exclude: # Tasks are doing local work which do not need full env loaded - - lib/tasks/auto_annotate_models.rake - - lib/tasks/emojis.rake - - lib/tasks/mastodon.rake - - lib/tasks/repo.rake - - lib/tasks/statistics.rake - -Rails/SkipsModelValidations: - Enabled: false diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml deleted file mode 100644 index 940701c722..0000000000 --- a/.rubocop/rspec.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -RSpec/ExampleLength: - CountAsOne: ['array', 'heredoc', 'method_call'] - Max: 20 # Override default of 5 - -RSpec/MultipleExpectations: - Max: 10 # Overrides default of 1 - -RSpec/MultipleMemoizedHelpers: - Max: 20 # Overrides default of 5 - Exclude: - - 'spec/services/delete_account_service_spec.rb' - -RSpec/NamedSubject: - EnforcedStyle: named_only - -RSpec/NestedGroups: - Max: 10 # Overrides default of 3 - -RSpec/NotToNot: - EnforcedStyle: to_not - -RSpec/SpecFilePathFormat: - CustomTransform: - ActivityPub: activitypub - DeepL: deepl - FetchOEmbedService: fetch_oembed_service - OEmbedController: oembed_controller - OStatus: ostatus diff --git a/.rubocop/rspec_rails.yml b/.rubocop/rspec_rails.yml deleted file mode 100644 index 993a5689ad..0000000000 --- a/.rubocop/rspec_rails.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -RSpecRails/HttpStatus: - EnforcedStyle: numeric diff --git a/.rubocop/strict.yml b/.rubocop/strict.yml deleted file mode 100644 index c2655a1470..0000000000 --- a/.rubocop/strict.yml +++ /dev/null @@ -1,24 +0,0 @@ -Lint/Debugger: # Remove any `binding.pry` - Enabled: true - Exclude: [] - -RSpec/Focus: # Require full spec run on CI - Enabled: true - Exclude: [] - -Rails/Output: # Remove any `puts` debugging - inherit_mode: - merge: - - Include - Enabled: true - Exclude: [] - Include: - - spec/**/*.rb - -Rails/FindEach: # Using `each` could impact performance, use `find_each` - Enabled: true - Exclude: [] - -Rails/UniqBeforePluck: # Require `uniq.pluck` and not `pluck.uniq` - Enabled: true - Exclude: [] diff --git a/.rubocop/style.yml b/.rubocop/style.yml deleted file mode 100644 index f59340d452..0000000000 --- a/.rubocop/style.yml +++ /dev/null @@ -1,63 +0,0 @@ ---- -Style/ArrayIntersect: - Enabled: false - -Style/ClassAndModuleChildren: - Enabled: false - -Style/Documentation: - Enabled: false - -Style/FormatStringToken: - AllowedMethods: - - redirect_with_vary # Route redirects are not token-formatted - inherit_mode: - merge: - - AllowedMethods - -Style/HashAsLastArrayItem: - Enabled: false - -Style/HashSyntax: - EnforcedShorthandSyntax: either - EnforcedStyle: ruby19_no_mixed_keys - -Style/IfUnlessModifier: - Exclude: - - '**/*.haml' - -Style/KeywordArgumentsMerging: - Enabled: false - -Style/NumericLiterals: - AllowedPatterns: - - \d{4}_\d{2}_\d{2}_\d{6} - -Style/PercentLiteralDelimiters: - PreferredDelimiters: - '%i': () - '%w': () - -Style/RedundantBegin: - Enabled: false - -Style/RedundantFetchBlock: - Enabled: false - -Style/RescueStandardError: - EnforcedStyle: implicit - -Style/SafeNavigationChainLength: - Enabled: false - -Style/SymbolArray: - Enabled: false - -Style/TrailingCommaInArrayLiteral: - EnforcedStyleForMultiline: comma - -Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: comma - -Style/WordArray: - MinSize: 3 # Override default of 2 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 13fb25d333..831e518af5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,20 +1,113 @@ # This configuration was generated by -# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.75.2. +# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` +# using RuboCop version 1.56.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. +# Include: **/*.gemfile, **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Exclude: + - 'Gemfile' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_argument, with_fixed_indentation +Layout/ArgumentAlignment: + Exclude: + - 'config/initializers/cors.rb' + - 'config/initializers/session_store.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/rack_attack.rb' + - 'config/routes.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. +Layout/LeadingCommentSpace: + Exclude: + - 'config/application.rb' + - 'config/initializers/3_omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Exclude: + - 'app/models/account.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_no_space, require_space +Layout/SpaceInLambdaLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'spec/controllers/api/v2/search_controller_spec.rb' + - 'spec/fabricators/access_token_fabricator.rb' + - 'spec/fabricators/conversation_fabricator.rb' + - 'spec/fabricators/system_key_fabricator.rb' + - 'spec/lib/activitypub/adapter_spec.rb' + - 'spec/models/user_role_spec.rb' + Lint/NonLocalExitFromIterator: Exclude: - - 'app/helpers/json_ld_helper.rb' + - 'app/helpers/jsonld_helper.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/OrAssignmentToConstant: + Exclude: + - 'lib/sanitize_ext/sanitize_config.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/simple_form.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/UselessAssignment: + Exclude: + - 'app/services/activitypub/process_status_update_service.rb' + - 'config/initializers/3_omniauth.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' + - 'spec/controllers/api/v1/favourites_controller_spec.rb' + - 'spec/controllers/concerns/account_controller_concern_spec.rb' + - 'spec/helpers/jsonld_helper_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/domain_block_spec.rb' + - 'spec/models/status_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/models/webauthn_credentials_spec.rb' + - 'spec/services/account_search_service_spec.rb' + - 'spec/services/post_status_service_spec.rb' + - 'spec/services/precompute_feed_service_spec.rb' + - 'spec/services/resolve_url_service_spec.rb' + - 'spec/views/statuses/show.html.haml_spec.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 82 + Max: 144 -# Configuration parameters: CountBlocks, CountModifierForms, Max. +# Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: Exclude: - 'lib/tasks/mastodon.rake' @@ -26,46 +119,597 @@ Metrics/CyclomaticComplexity: # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 27 + +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +Naming/VariableNumber: Exclude: - - 'app/policies/status_policy.rb' - - 'app/services/delivery_antenna_service.rb' - - 'app/services/post_status_service.rb' + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20190820003045_update_statuses_index.rb' + - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' + - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' + - 'db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/domain_block_spec.rb' + - 'spec/models/user_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeMultiline. +Performance/DeletePrefix: + Exclude: + - 'app/models/featured_tag.rb' + +Performance/MapMethodChain: + Exclude: + - 'app/models/feed.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'spec/services/bulk_import_service_spec.rb' + - 'spec/services/import_service_spec.rb' + +RSpec/AnyInstance: + Exclude: + - 'spec/controllers/activitypub/inboxes_controller_spec.rb' + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/admin/resets_controller_spec.rb' + - 'spec/controllers/admin/settings/branding_controller_spec.rb' + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb' + - 'spec/lib/request_spec.rb' + - 'spec/lib/status_filter_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/setting_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/validators/follow_limit_validator_spec.rb' + - 'spec/workers/activitypub/delivery_worker_spec.rb' + - 'spec/workers/web/push_notification_worker_spec.rb' + +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 22 + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: implicit, each, example +RSpec/HookArgument: + Exclude: + - 'spec/controllers/api/v1/streaming_controller_spec.rb' + - 'spec/controllers/well_known/webfinger_controller_spec.rb' + - 'spec/helpers/instance_helper_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/rails_helper.rb' + - 'spec/serializers/activitypub/note_serializer_spec.rb' + - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' + - 'spec/services/import_service_spec.rb' + +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/controllers/api/v1/streaming_controller_spec.rb' + - 'spec/controllers/auth/confirmations_controller_spec.rb' + - 'spec/controllers/auth/passwords_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/concerns/export_controller_concern_spec.rb' + - 'spec/controllers/home_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' + - 'spec/controllers/statuses_cleanup_controller_spec.rb' + - 'spec/models/concerns/account_finder_concern_spec.rb' + - 'spec/models/concerns/account_interactions_spec.rb' + - 'spec/models/public_feed_spec.rb' + - 'spec/serializers/activitypub/note_serializer_spec.rb' + - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' + - 'spec/services/remove_status_service_spec.rb' + - 'spec/services/search_service_spec.rb' + - 'spec/services/unblock_domain_service_spec.rb' + +RSpec/LetSetup: + Exclude: + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/admin/action_logs_controller_spec.rb' + - 'spec/controllers/admin/instances_controller_spec.rb' + - 'spec/controllers/admin/reports/actions_controller_spec.rb' + - 'spec/controllers/admin/statuses_controller_spec.rb' + - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' + - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/v1/filters_controller_spec.rb' + - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' + - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' + - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' + - 'spec/controllers/auth/confirmations_controller_spec.rb' + - 'spec/controllers/auth/passwords_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/follower_accounts_controller_spec.rb' + - 'spec/controllers/following_accounts_controller_spec.rb' + - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' + - 'spec/controllers/oauth/tokens_controller_spec.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/lib/activitypub/activity/delete_spec.rb' + - 'spec/lib/vacuum/applications_vacuum_spec.rb' + - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/account_statuses_cleanup_policy_spec.rb' + - 'spec/models/canonical_email_block_spec.rb' + - 'spec/models/status_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/services/account_statuses_cleanup_service_spec.rb' + - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' + - 'spec/services/activitypub/fetch_remote_status_service_spec.rb' + - 'spec/services/activitypub/process_account_service_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/services/batched_remove_status_service_spec.rb' + - 'spec/services/block_domain_service_spec.rb' + - 'spec/services/bulk_import_service_spec.rb' + - 'spec/services/delete_account_service_spec.rb' + - 'spec/services/import_service_spec.rb' + - 'spec/services/notify_service_spec.rb' + - 'spec/services/remove_status_service_spec.rb' + - 'spec/services/report_service_spec.rb' + - 'spec/services/resolve_account_service_spec.rb' + - 'spec/services/suspend_account_service_spec.rb' + - 'spec/services/unallow_domain_service_spec.rb' + - 'spec/services/unsuspend_account_service_spec.rb' + - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' + +RSpec/MessageChain: + Exclude: + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/models/concerns/remotable_spec.rb' + - 'spec/models/session_activation_spec.rb' + - 'spec/models/setting_spec.rb' + +# Configuration parameters: EnforcedStyle. +# SupportedStyles: have_received, receive +RSpec/MessageSpies: + Exclude: + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' + - 'spec/lib/webfinger_resource_spec.rb' + - 'spec/models/admin/account_action_spec.rb' + - 'spec/models/concerns/remotable_spec.rb' + - 'spec/models/follow_request_spec.rb' + - 'spec/models/identity_spec.rb' + - 'spec/models/session_activation_spec.rb' + - 'spec/models/setting_spec.rb' + - 'spec/services/activitypub/fetch_replies_service_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/spec_helper.rb' + - 'spec/validators/status_length_validator_spec.rb' + +RSpec/MultipleExpectations: + Max: 8 + +# Configuration parameters: AllowSubject. +RSpec/MultipleMemoizedHelpers: + Max: 21 + +# Configuration parameters: AllowedGroups. +RSpec/NestedGroups: + Max: 6 + +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/ApplicationController: + Exclude: + - 'app/controllers/health_controller.rb' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/CreateTableWithTimestamps: + Exclude: + - 'db/migrate/20170508230434_create_conversation_mutes.rb' + - 'db/migrate/20170823162448_create_status_pins.rb' + - 'db/migrate/20171116161857_create_list_accounts.rb' + - 'db/migrate/20180929222014_create_account_conversations.rb' + - 'db/migrate/20181007025445_create_pghero_space_stats.rb' + - 'db/migrate/20190103124649_create_scheduled_statuses.rb' + - 'db/migrate/20220824233535_create_status_trends.rb' + - 'db/migrate/20221006061337_create_preview_card_trends.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity. +Rails/DuplicateAssociation: + Exclude: + - 'app/serializers/activitypub/collection_serializer.rb' + - 'app/serializers/activitypub/note_serializer.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasAndBelongsToMany: + Exclude: + - 'app/models/concerns/account_associations.rb' + - 'app/models/preview_card.rb' + - 'app/models/status.rb' + - 'app/models/tag.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasManyOrHasOneDependent: + Exclude: + - 'app/models/concerns/account_counters.rb' + - 'app/models/conversation.rb' + - 'app/models/custom_emoji.rb' + - 'app/models/custom_emoji_category.rb' + - 'app/models/domain_block.rb' + - 'app/models/invite.rb' + - 'app/models/status.rb' + - 'app/models/user.rb' + - 'app/models/web/push_subscription.rb' + +Rails/I18nLocaleTexts: + Exclude: + - 'lib/tasks/mastodon.rake' + - 'spec/helpers/flashes_helper_spec.rb' + +# Configuration parameters: Include. +# Include: app/controllers/**/*.rb, app/mailers/**/*.rb +Rails/LexicallyScopedActionFilter: + Exclude: + - 'app/controllers/auth/passwords_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/controllers/auth/sessions_controller.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/NegateInclude: + Exclude: + - 'app/controllers/concerns/signature_verification.rb' + - 'app/helpers/jsonld_helper.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/activitypub/activity/move.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/link_details_extractor.rb' + - 'app/models/concerns/attachmentable.rb' + - 'app/models/concerns/remotable.rb' + - 'app/models/custom_filter.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/fetch_link_card_service.rb' + - 'app/services/search_service.rb' + - 'app/workers/web/push_notification_worker.rb' + - 'lib/paperclip/color_extractor.rb' Rails/OutputSafety: Exclude: - 'config/initializers/simple_form.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Include. +# Include: **/Rakefile, **/*.rake +Rails/RakeEnvironment: + Exclude: + - 'lib/tasks/auto_annotate_models.rake' + - 'lib/tasks/db.rake' + - 'lib/tasks/emojis.rake' + - 'lib/tasks/mastodon.rake' + - 'lib/tasks/repo.rake' + - 'lib/tasks/statistics.rake' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ReversibleMigration: + Exclude: + - 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb' + - 'db/migrate/20161122163057_remove_unneeded_indexes.rb' + - 'db/migrate/20170205175257_remove_devices.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170609145826_remove_default_language_from_statuses.rb' + - 'db/migrate/20170711225116_fix_null_booleans.rb' + - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' + - 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb' + - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180617162849_remove_unused_indexes.rb' + - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb' + +# Configuration parameters: ForbiddenMethods, AllowedMethods. +# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all +Rails/SkipsModelValidations: + Exclude: + - 'app/controllers/admin/invites_controller.rb' + - 'app/controllers/concerns/session_tracking_concern.rb' + - 'app/models/concerns/account_merging.rb' + - 'app/models/concerns/expireable.rb' + - 'app/models/status.rb' + - 'app/models/trends/links.rb' + - 'app/models/trends/preview_card_batch.rb' + - 'app/models/trends/preview_card_provider_batch.rb' + - 'app/models/trends/status_batch.rb' + - 'app/models/trends/statuses.rb' + - 'app/models/trends/tag_batch.rb' + - 'app/models/trends/tags.rb' + - 'app/models/user.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/approve_appeal_service.rb' + - 'app/services/block_domain_service.rb' + - 'app/services/delete_account_service.rb' + - 'app/services/process_mentions_service.rb' + - 'app/services/unallow_domain_service.rb' + - 'app/services/unblock_domain_service.rb' + - 'app/services/update_status_service.rb' + - 'app/workers/activitypub/post_upgrade_worker.rb' + - 'app/workers/move_worker.rb' + - 'app/workers/scheduler/ip_cleanup_scheduler.rb' + - 'app/workers/scheduler/scheduled_statuses_scheduler.rb' + - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' + - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' + - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' + - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' + - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/migrate/20191007013357_update_pt_locales.rb' + - 'db/migrate/20220316233212_update_kurdish_locales.rb' + - 'db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb' + - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' + - 'db/post_migrate/20200917193528_migrate_notifications_type.rb' + - 'db/post_migrate/20201017234926_fill_account_suspension_origin.rb' + - 'db/post_migrate/20220617202502_migrate_roles.rb' + - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' + - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/main.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' + - 'spec/lib/activitypub/activity/follow_spec.rb' + - 'spec/services/follow_service_spec.rb' + - 'spec/services/update_account_service_spec.rb' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ThreeStateBooleanColumn: + Exclude: + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + - 'db/migrate/20230314021909_add_group_message_following_only_to_accounts.rb' + - 'db/migrate/20230314081013_add_group_allow_private_message_to_accounts.rb' + - 'db/migrate/20230412005311_add_markdown_to_statuses.rb' + - 'db/migrate/20230412073021_add_markdown_to_status_edits.rb' + - 'db/migrate/20230428111230_add_emoji_reaction_streaming_to_accounts.rb' + - 'db/migrate/20230510004621_remove_stop_emoji_reaction_streaming_from_accounts.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UniqueValidationWithoutIndex: + Exclude: + - 'app/models/account_alias.rb' + - 'app/models/custom_filter_status.rb' + - 'app/models/identity.rb' + - 'app/models/webauthn_credential.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UnusedIgnoredColumns: + Exclude: + - 'app/models/account.rb' + - 'app/models/account_stat.rb' + - 'app/models/admin/action_log.rb' + - 'app/models/custom_filter.rb' + - 'app/models/email_domain_block.rb' + - 'app/models/report.rb' + - 'app/models/status_edit.rb' + - 'app/models/user.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: exists, where +Rails/WhereExists: + Exclude: + - 'app/controllers/activitypub/inboxes_controller.rb' + - 'app/controllers/admin/email_domain_blocks_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/delivery_failure_tracker.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/status_cache_hydrator.rb' + - 'app/lib/suspicious_sign_in_detector.rb' + - 'app/models/concerns/account_interactions.rb' + - 'app/models/featured_tag.rb' + - 'app/models/poll.rb' + - 'app/models/session_activation.rb' + - 'app/models/status.rb' + - 'app/models/user.rb' + - 'app/policies/status_policy.rb' + - 'app/serializers/rest/announcement_serializer.rb' + - 'app/serializers/rest/tag_serializer.rb' + - 'app/services/activitypub/fetch_remote_status_service.rb' + - 'app/services/app_sign_up_service.rb' + - 'app/services/vote_service.rb' + - 'app/validators/reaction_validator.rb' + - 'app/validators/vote_validator.rb' + - 'app/workers/move_worker.rb' + - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' + - 'lib/tasks/tests.rake' + - 'spec/models/account_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/services/purge_domain_service_spec.rb' + - 'spec/services/unallow_domain_service_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnConstant, AllowOnSelfClass. +Style/CaseEquality: + Exclude: + - 'config/initializers/trusted_proxies.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. +# AllowedMethods: ==, equal?, eql? +Style/ClassEqualityComparison: + Exclude: + - 'app/helpers/jsonld_helper.rb' + - 'app/serializers/activitypub/outbox_serializer.rb' + +Style/ClassVars: + Exclude: + - 'config/initializers/devise.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CombinableLoops: + Exclude: + - 'app/models/form/custom_emoji_batch.rb' + - 'app/models/form/ip_block_batch.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: + - 'app/lib/redis_configuration.rb' + - 'app/lib/translation_service.rb' + - 'config/environments/development.rb' - 'config/environments/production.rb' - 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/3_omniauth.rb' + - 'config/initializers/blacklists.rb' - 'config/initializers/cache_buster.rb' + - 'config/initializers/content_security_policy.rb' - 'config/initializers/devise.rb' - 'config/initializers/paperclip.rb' - 'config/initializers/vapid.rb' + - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/mastodon/redis_config.rb' - 'lib/tasks/repo.rake' + - 'spec/features/profile_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated # AllowedMethods: redirect Style/FormatStringToken: Exclude: + - 'app/models/privacy_policy.rb' - 'config/initializers/devise.rb' - 'lib/paperclip/color_extractor.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/GlobalStdStream: + Exclude: + - 'config/boot.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: - Enabled: false + Exclude: + - 'app/controllers/admin/confirmations_controller.rb' + - 'app/controllers/auth/confirmations_controller.rb' + - 'app/controllers/auth/passwords_controller.rb' + - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' + - 'app/lib/activitypub/activity/block.rb' + - 'app/lib/request.rb' + - 'app/lib/request_pool.rb' + - 'app/lib/webfinger.rb' + - 'app/lib/webfinger_resource.rb' + - 'app/models/concerns/account_counters.rb' + - 'app/models/concerns/ldap_authenticable.rb' + - 'app/models/tag.rb' + - 'app/models/user.rb' + - 'app/services/fan_out_on_write_service.rb' + - 'app/services/post_status_service.rb' + - 'app/services/process_hashtags_service.rb' + - 'app/workers/move_worker.rb' + - 'app/workers/redownload_avatar_worker.rb' + - 'app/workers/redownload_header_worker.rb' + - 'app/workers/redownload_media_worker.rb' + - 'app/workers/remote_account_refresh_worker.rb' + - 'config/initializers/devise.rb' + - 'db/migrate/20170901141119_truncate_preview_cards.rb' + - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' + - 'lib/devise/two_factor_ldap_authenticatable.rb' + - 'lib/devise/two_factor_pam_authenticatable.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'lib/mastodon/cli/media.rb' + - 'lib/paperclip/attachment_extensions.rb' + - 'lib/tasks/repo.rake' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: braces, no_braces +Style/HashAsLastArrayItem: + Exclude: + - 'app/controllers/admin/statuses_controller.rb' + - 'app/controllers/api/v1/statuses_controller.rb' + - 'app/models/concerns/account_counters.rb' + - 'app/models/concerns/status_threading_concern.rb' + - 'app/models/status.rb' + - 'app/services/batched_remove_status_service.rb' + - 'app/services/notify_service.rb' + - 'db/migrate/20181024224956_migrate_account_conversations.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashTransformValues: + Exclude: + - 'app/serializers/rest/web_push_subscription_serializer.rb' + - 'app/services/import_service.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/ffmpeg.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: InverseMethods, InverseBlocks. +Style/InverseMethods: + Exclude: + - 'app/models/custom_filter.rb' + - 'app/services/update_account_service.rb' + - 'spec/controllers/activitypub/replies_controller_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: line_count_dependent, lambda, literal +Style/Lambda: + Exclude: + - 'config/initializers/simple_form.rb' + - 'config/routes.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: + Exclude: + - 'app/models/status.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'app/models/tag.rb' + - 'app/services/delete_account_service.rb' + - 'lib/mastodon/migration_warning.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/NilLambda: + Exclude: + - 'config/initializers/paperclip.rb' # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: + - 'app/helpers/admin/account_moderation_notes_helper.rb' + - 'app/helpers/jsonld_helper.rb' - 'app/lib/admin/system_check/message.rb' - 'app/lib/request.rb' - 'app/lib/webfinger.rb' @@ -73,6 +717,14 @@ Style/OptionalBooleanParameter: - 'app/services/fetch_resource_service.rb' - 'app/workers/domain_block_worker.rb' - 'app/workers/unfollow_follow_worker.rb' + - 'lib/mastodon/redis_config.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'config/deploy.rb' + - 'config/initializers/doorkeeper.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. @@ -86,3 +738,114 @@ Style/RedundantConstantBase: Exclude: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeForConstants. +Style/RedundantFetchBlock: + Exclude: + - 'config/initializers/1_hosts.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/paperclip.rb' + - 'config/puma.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Exclude: + - 'app/controllers/api/v1/directories_controller.rb' + - 'app/controllers/auth/confirmations_controller.rb' + - 'app/lib/ostatus/tag_manager.rb' + - 'app/models/form/import.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'app/models/concerns/account_finder_concern.rb' + - 'app/models/status.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: only_raise, only_fail, semantic +Style/SignalException: + Exclude: + - 'lib/devise/two_factor_ldap_authenticatable.rb' + - 'lib/devise/two_factor_pam_authenticatable.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SingleArgumentDig: + Exclude: + - 'lib/webpacker/manifest_extensions.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_parentheses, require_no_parentheses +Style/StabbyLambdaParentheses: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/StderrPuts: + Exclude: + - 'config/boot.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/rack_attack.rb' + - 'config/initializers/webauthn.rb' + - 'config/routes.rb' + - 'db/schema.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to +Style/SymbolProc: + Exclude: + - 'config/initializers/3_omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'config/environments/development.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInHashLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/environments/test.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + Exclude: + - 'app/helpers/languages_helper.rb' + - 'config/initializers/cors.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/models/form/import_spec.rb' diff --git a/.ruby-version b/.ruby-version index 6cb9d3dd0d..b347b11eac 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.3 +3.2.3 diff --git a/.watchmanconfig b/.watchmanconfig deleted file mode 100644 index 29e4f231e9..0000000000 --- a/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["node_modules/", "public/"] -} diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz new file mode 100644 index 0000000000..4dc538b2ca Binary files /dev/null and b/.yarn/install-state.gz differ diff --git a/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch b/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch deleted file mode 100644 index 0b3f94d09e..0000000000 --- a/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/lib/index.js b/lib/index.js -index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644 ---- a/lib/index.js -+++ b/lib/index.js -@@ -99,7 +99,7 @@ function lodash(_ref) { - - var node = _ref3; - -- if ((0, _types.isModuleDeclaration)(node)) { -+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) { - isModule = true; - break; - } diff --git a/.yarnclean b/.yarnclean new file mode 100644 index 0000000000..21eb734a6c --- /dev/null +++ b/.yarnclean @@ -0,0 +1,49 @@ +# test directories +__tests__ +test +tests +powered-test + +# asset directories +docs +doc +website +images +# assets + +# examples +example +examples + +# code coverage directories +coverage +.nyc_output + +# build scripts +Makefile +Gulpfile.js +Gruntfile.js + +# configs +.tern-project +.gitattributes +.editorconfig +.*ignore +.eslintrc +.jshintrc +.flowconfig +.documentup.json +.yarn-metadata.json +.*.yml +*.yml + +# misc +*.gz +*.md + +# for specific ignore +!.svgo.yml +!sass-lint/**/*.yml + +# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384 +!**/yaml/dist/**/doc diff --git a/.yarnrc.yml b/.yarnrc.yml deleted file mode 100644 index 3186f3f079..0000000000 --- a/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/AUTHORS_KB.md b/AUTHORS_KB.md deleted file mode 100644 index 2cda5fd31d..0000000000 --- a/AUTHORS_KB.md +++ /dev/null @@ -1,18 +0,0 @@ -# Authors for kmyblue fork - -## 貢献者 - -kmyblueフォークã¯ã€ä»¥ä¸‹ã®æ–¹ã®è²¢çŒ®ã«ã‚ˆã£ã¦æˆã‚Šç«‹ã£ã¦ã„ã¾ã™ã€‚ -本家Mastodonã®è²¢çŒ®è€…ã«ã¤ã„ã¦ã¯ã€`AUTHORS.md`ã‚’ã”覧ãã ã•ã„。 - -- [aoisensi](https://github.com/aoisensi) -- [KMY](https://github.com/kmycode) -- [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS) -- [Yuicho](https://github.com/yuicho) - -## 特記 - -kmyblueフォークã®é–‹ç™ºã«ã‚ãŸã£ã¦ã€API・Activity仕様ã®è¨­è¨ˆï¼ˆä¸€éƒ¨æ©Ÿèƒ½ã«ã¤ã„ã¦ã¯å†…部仕様)策定ã®éŽç¨‹ã§ä¸‹è¨˜ãƒªãƒã‚¸ãƒˆãƒªã®ã‚³ãƒ¼ãƒ‰ã‚’å‚考ã«ã—ã¾ã—ãŸã€‚ -kmyblueフォークã«ç›´æŽ¥è²¢çŒ®ã—ãŸã‚ã‘ã§ã¯ã‚りã¾ã›ã‚“ãŒã€ä»¥ä¸‹ã®ãƒªãƒã‚¸ãƒˆãƒªã«ã‚る絵文字リアクション機能・検索範囲機能ã®ã‚³ãƒ¼ãƒ‰ã®ã†ã¡ä¸€éƒ¨ã«kmyblueã¸è»¢å†™ã—ãŸç®‡æ‰€ãŒã”ã–ã„ã¾ã™ãŸã‚ã€ãŠåå‰è¨˜è¼‰ã•ã›ã¦ã„ãŸã ãã¾ã™ã€‚ - -- [Fedibird](https://github.com/fedibird/mastodon) diff --git a/Aptfile b/Aptfile index 06c91d4c7b..5e033f1365 100644 --- a/Aptfile +++ b/Aptfile @@ -1,5 +1,5 @@ -libidn12 -# for idn-ruby on heroku-24 stack - -# use https://github.com/heroku/heroku-buildpack-activestorage-preview -# in place for ffmpeg and its dependent packages to reduce slag size +ffmpeg +libopenblas0-pthread +libpq-dev +libxdamage1 +libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd4783597..f3e364320b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,544 +2,32 @@ All notable changes to this project will be documented in this file. -## [4.3.7] - 2025-04-02 - -### Add - -- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire) -- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire) - -### Changed - -- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire) -- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski) -- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire) - -### Fixed - -- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire) -- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire) -- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire) -- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire) -- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire) -- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire) -- Fix “x†hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire) - -## [4.3.6] - 2025-03-13 +## [4.2.13] - 2024-09-30 ### Security -- Update dependency `omniauth-saml` -- Update dependency `rack` - -### Fixed - -- Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126 by @ClearlyClaire) - -## [4.3.5] - 2025-03-10 - -### Changed - -- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire) - -### Fixed - -- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap) -- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire) -- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire) -- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire) - -## [4.3.4] - 2025-02-27 - -### Security - -- Update dependencies -- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf)) -- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h)) -- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825)) - -### Changed - -- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire) -- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire) -- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski) -- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski) - -### Fixed - -- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire) -- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke) -- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire) -- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire) -- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire) -- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire) -- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire) -- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire) -- Fix polls not being validated on edition (#33755 by @ClearlyClaire) -- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire) -- Fix preview card sizing in “Author attribution†in profile settings (#33482 by @ClearlyClaire) -- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire) -- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski) -- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron) -- Fix accounts table long display name (#29316 by @WebCoder49) -- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan) - -## [4.3.3] - 2025-01-16 - -### Security - -- Fix insufficient validation of account URIs ([GHSA-5wxh-3p65-r4g6](https://github.com/mastodon/mastodon/security/advisories/GHSA-5wxh-3p65-r4g6)) -- Update dependencies - -### Fixed - -- Fix `libyaml` missing from `Dockerfile` build stage (#33591 by @vmstan) -- Fix incorrect notification settings migration for non-followers (#33348 by @ClearlyClaire) -- Fix down clause for notification policy v2 migrations (#33340 by @jesseplusplus) -- Fix error decrementing status count when `FeaturedTags#last_status_at` is `nil` (#33320 by @ClearlyClaire) -- Fix last paginated notification group only including data on a single notification (#33271 by @ClearlyClaire) -- Fix processing of mentions for post edits with an existing corresponding silent mention (#33227 by @ClearlyClaire) -- Fix deletion of unconfirmed users with Webauthn set (#33186 by @ClearlyClaire) -- Fix empty authors preview card serialization (#33151, #33466 by @mjankowski and @ClearlyClaire) - -## [4.3.2] - 2024-12-03 - -### Added - -- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire) -- Add error message when user tries to follow their own account (#31910 by @lenikadali) -- Add client_secret_expires_at to OAuth Applications (#30317 by @ThisIsMissEm) - -### Changed - -- Change design of Content Warnings and filters (#32543 by @ClearlyClaire) - -### Fixed - -- Fix processing incoming post edits with mentions to unresolvable accounts (#33129 by @ClearlyClaire) -- Fix error when including multiple instances of `embed.js` (#33107 by @YKWeyer) -- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire) -- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire) -- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire) -- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron) -- Fix duplicate notifications in notification groups when using slow mode (#33014 by @ClearlyClaire) -- Fix posts made in the future being allowed to trend (#32996 by @ClearlyClaire) -- Fix uploading higher-than-wide GIF profile picture with libvips enabled (#32911 by @ClearlyClaire) -- Fix domain attribution field having autocorrect and autocapitalize enabled (#32903 by @ClearlyClaire) -- Fix titles being escaped twice (#32889 by @ClearlyClaire) -- Fix list creation limit check (#32869 by @ClearlyClaire) -- Fix error in `tootctl email_domain_blocks` when supplying `--with-dns-records` (#32863 by @mjankowski) -- Fix `min_id` and `max_id` causing error in search API (#32857 by @Gargron) -- Fix inefficiencies when processing removal of posts that use featured tags (#32787 by @ClearlyClaire) -- Fix alt-text pop-in not using the translated description (#32766 by @ClearlyClaire) -- Fix preview cards with long titles erroneously causing layout changes (#32678 by @ClearlyClaire) -- Fix embed modal layout on mobile (#32641 by @DismalShadowX) -- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro) -- Fix blocks not being applied on link timeline (#32625 by @tribela) -- Fix follow counters being incorrectly changed (#32622 by @oneiros) -- Fix 'unknown' media attachment type rendering (#32613 and #32713 by @ThisIsMissEm and @renatolond) -- Fix tl language native name (#32606 by @seav) - -### Security - -- Update dependencies - -## [4.3.1] - 2024-10-21 - -### Added - -- Add more explicit explanations about author attribution and `fediverse:creator` (#32383 by @ClearlyClaire) -- Add ability to group follow notifications in WebUI, can be disabled in the column settings (#32520 by @renchap) -- Add back a 6 hours mute duration option (#32522 by @renchap) -- Add note about not changing ActiveRecord encryption secrets once they are set (#32413, #32476, #32512, and #32537 by @ClearlyClaire and @mjankowski) - -### Changed - -- Change translation feature to translate to selected regional variant (e.g. pt-BR) if available (#32428 by @c960657) - -### Removed - -- Remove ability to get embed code for remote posts (#32578 by @ClearlyClaire)\ - Getting the embed code is only reliable for local posts.\ - It never worked for non-Mastodon servers, and stopped working correctly with the changes made in 4.3.0.\ - We have therefore decided to remove the menu entry while we investigate solutions. - -### Fixed - -- Fix follow recommendation moderation page default language when using regional variant (#32580 by @ClearlyClaire) -- Fix column-settings spacing in local timeline in advanced view (#32567 by @lindwurm) -- Fix broken i18n in text welcome mailer tags area (#32571 by @mjankowski) -- Fix missing or incorrect cache-control headers for Streaming server (#32551 by @ThisIsMissEm) -- Fix only the first paragraph being displayed in some notifications (#32348 by @ClearlyClaire) -- Fix reblog icons on account media view (#32506 by @tribela) -- Fix Content-Security-Policy not allowing OpenStack SWIFT object storage URI (#32439 by @kenkiku1021) -- Fix back arrow pointing to the incorrect direction in RTL languages (#32485 by @renchap) -- Fix streaming server using `REDIS_USERNAME` instead of `REDIS_USER` (#32493 by @ThisIsMissEm) -- Fix follow recommendation carrousel scrolling on RTL layouts (#32462 and #32505 by @ClearlyClaire) -- Fix follow recommendation suppressions not applying immediately (#32392 by @ClearlyClaire) -- Fix language of push notifications (#32415 by @ClearlyClaire) -- Fix mute duration not being shown in list of muted accounts in web UI (#32388 by @ClearlyClaire) -- Fix “Mark every notification as read†not updating the read marker if scrolled down (#32385 by @ClearlyClaire) -- Fix “Mention†appearing for otherwise filtered posts (#32356 by @ClearlyClaire) -- Fix notification requests from suspended accounts still being listed (#32354 by @ClearlyClaire) -- Fix list edition modal styling (#32358 and #32367 by @ClearlyClaire and @vmstan) -- Fix 4 columns barely not fitting on 1920px screen (#32361 by @ClearlyClaire) -- Fix icon alignment in applications list (#32293 by @mjankowski) - -## [4.3.0] - 2024-10-08 - -The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by @mjankowski. - -### Security - -- **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\ - This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared. - Fix ReDoS vulnerability on some Ruby versions ([GHSA-jpxp-r43f-rhvx](https://github.com/mastodon/mastodon/security/advisories/GHSA-jpxp-r43f-rhvx)) -- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 and #32241 by @ClearlyClaire) - Update dependencies ### Added -- **Add server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610, #31929, #32089, #32085, #32243, #32179 and #32254 by @ClearlyClaire, @Gargron, @mgmn, and @renchap)\ - Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\ - This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\ - As part of this, the visual design of the entire notifications feature has been revamped.\ - This feature is intended to eventually replace the existing notifications column, but for this first beta, users will have to enable it in the “Experimental features†section of the notifications column settings.\ - The API is not final yet, but it consists of: - - a new `group_key` attribute to `Notification` entities - - `GET /api/v2/notifications`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped - - `GET /api/v2/notifications/:group_key`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group - - `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts - - `POST /api/v2/notifications/:group_key/dismiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group - - `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count -- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723, #32062 and #32281 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\ - The old “Block notifications from non-followersâ€, “Block notifications from people you don't follow†and “Block direct messages from people you don't follow†notification settings have been replaced by a new set of settings found directly in the notification column.\ - You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\ - Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications†box that you can review separately without it clogging your main notifications.\ - This adds the following REST API endpoints: - - - `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy - - `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications - - `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests - - `GET /api/v1/notifications/requests/:id`: https://docs.joinmastodon.org/methods/notifications/#get-one-request - - `POST /api/v1/notifications/requests/:id/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-request - - `POST /api/v1/notifications/requests/:id/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-request - - `POST /api/v1/notifications/requests/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-multiple-requests - - `POST /api/v1/notifications/requests/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-multiple-requests - - `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged - - In addition, accepting one or more notification requests generates a new streaming event: - - - `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed - -- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\ - Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\ - Note that this does not notify remote users.\ - This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event). -- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\ - Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\ - This can be disabled in the “Animations and accessibility†section of the preferences. -- **Add "system" theme setting (light/dark theme depending on user system preference)** (#29748, #29553, #29795, #29918, #30839, and #30861 by @nshki, @ErikUden, @mjankowski, @renchap, and @vmstan)\ - Add a “system†theme that automatically switch between default dark and light themes depending on the user's system preferences.\ - Also changes the default server theme to this new “system†theme so that automatic theme selection happens even when logged out. -- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\ - You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\ - This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link -- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, #30846, #31819, #31900 and #32188 by @Gargron, @mjankowski and @oneiros)\ - This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\ - Articles hosted outside the fediverse can indicate a fediverse author with a meta tag: - ```html - - ``` - On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors \ - Users can allow arbitrary domains to use `fediverse:creator` to credit them by visiting `/settings/verification`.\ - This is federated as a new `attributionDomains` property in the `http://joinmastodon.org/ns` namespace, containing an array of domain names: https://docs.joinmastodon.org/spec/activitypub/#properties-used-1 -- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\ - In addition to email notifications, also notify users of moderation actions or warnings against them directly within the app, so they are less likely to miss important communication from their moderators.\ - This adds the `moderation_warning` notification type to the REST API and streaming, with a new [`moderation_warning` attribute](https://docs.joinmastodon.org/entities/Notification/#moderation_warning). -- **Add domain information to profiles in web UI** (#29602 by @Gargron)\ - Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation. -- **Add support for Redis sentinel** (#31694, #31623, #31744, #31767, and #31768 by @ThisIsMissEm and @oneiros)\ - See https://docs.joinmastodon.org/admin/scaling/#redis-sentinel -- **Add ability to reorder uploaded media before posting in web UI** (#28456 and #32093 by @Gargron) - Add “A Mastodon update is available.†message on admin dashboard for non-bugfix updates (#32106 by @ClearlyClaire) -- Add ability to view alt text by clicking the ALT badge in web UI (#32058 by @Gargron) -- Add preview of followers removed in domain block modal in web UI (#32032 and #32105 by @ClearlyClaire and @Gargron) -- Add reblogs and favourites counts to statuses in ActivityPub (#32007 by @Gargron) -- Add moderation interface for searching hashtags (#30880 by @ThisIsMissEm) -- Add ability for admins to configure instance favicon and logo (#30040, #30208, #30259, #30375, #30734, #31016, and #30205 by @ClearlyClaire, @FawazFarid, @JasonPunyon, @mgmn, and @renchap)\ - This is also exposed through the REST API: https://docs.joinmastodon.org/entities/Instance/#icon -- Add `api_versions` to `/api/v2/instance` (#31354 by @ClearlyClaire)\ - Add API version number to make it easier for clients to detect compatible features going forward.\ - See API documentation at https://docs.joinmastodon.org/entities/Instance/#api-versions -- Add quick links to Administration and Moderation Reports from Web UI (#24838 by @ThisIsMissEm) -- Add link to `/admin/roles` in moderation interface when changing someone's role (#31791 by @ClearlyClaire) -- Add recent audit log entries in federation moderation interface (#27386 by @ThisIsMissEm) -- Add profile setup to onboarding in web UI (#27829, #27876, and #28453 by @Gargron) -- Add prominent share/copy button on profiles in web UI (#27865 and #27889 by @ClearlyClaire and @Gargron) -- Add optional hints for server rules (#29539 and #29758 by @ClearlyClaire and @Gargron)\ - Server rules can now be broken into a short rule name and a longer explanation of the rule.\ - This adds a new [`hint` attribute](https://docs.joinmastodon.org/entities/Rule/#hint) to `Rule` entities in the REST API. -- Add support for PKCE in OAuth flow (#31129 by @ThisIsMissEm) -- Add CDN cache busting on media deletion (#31353 and #31414 by @ClearlyClaire and @tribela) -- Add the OAuth application used in local reports (#30539 by @ThisIsMissEm) -- Add hint to user that other remote statuses may be missing (#26910, #31387, and #31516 by @Gargron, @audiodude, and @renchap) -- Add lang attribute on preview card title (#31303 by @c960657) -- Add check for `Content-Length` in `ResponseWithLimitAdapter` (#31285 by @c960657) -- Add `Accept-Language` header to fetch preview cards in the server's default language (#31232 by @c960657) -- Add support for PKCE Extension in OmniAuth OIDC through the `OIDC_USE_PKCE` environment variable (#31131 by @ThisIsMissEm) -- Add API endpoints for unread notifications count (#31191 by @ClearlyClaire)\ - This adds the following REST API endpoints: - - `GET /api/v1/notifications/unread_count`: https://docs.joinmastodon.org/methods/notifications/#unread-count -- Add `/` keyboard shortcut to focus the search field (#29921 by @ClearlyClaire) -- Add button to view the Hashtag on the instance from Hashtags in Moderation UI (#31533 by @ThisIsMissEm) -- Add list of pending releases directly in mail notifications for version updates (#29436 and #30035 by @ClearlyClaire) -- Add “Appeals†link under “Moderation†navigation category in moderation interface (#31071 by @ThisIsMissEm) -- Add badge on account card in report moderation interface when account is already suspended (#29592 by @ClearlyClaire) -- Add admin comments directly to the `admin/instances` page (#29240 by @tribela) -- Add ability to require approval when users sign up using specific email domains (#28468, #28732, #28607, and #28608 by @ClearlyClaire) -- Add banner for forwarded reports made by remote users about remote content (#27549 by @ClearlyClaire) -- Add support HTML ruby tags in remote posts for east-asian languages (#30897 by @ThisIsMissEm) -- Add link to manage warning presets in admin navigation (#26199 by @vmstan) -- Add volume saving/reuse to video player (#27488 by @thehydrogen) -- Add Elasticsearch index size, ffmpeg and ImageMagick versions to the admin dashboard (#27301, #30710, #31130, and #30845 by @vmstan) -- Add `MASTODON_SIDEKIQ_READY_FILENAME` environment variable to use a file for Sidekiq to signal it is ready to process jobs (#30971 and #30988 by @renchap)\ - In the official Docker image, this is set to `sidekiq_process_has_started_and_will_begin_processing_jobs` so that Sidekiq will touch `tmp/sidekiq_process_has_started_and_will_begin_processing_jobs` to signal readiness. -- Add `S3_RETRY_LIMIT` environment variable to make S3 retries configurable (#23215 by @smiba) -- Add `S3_KEY_PREFIX` environment variable (#30181 by @S0yKaf) -- Add support for multiple `redirect_uris` when creating OAuth 2.0 Applications (#29192 by @ThisIsMissEm) -- Add Interlingue and Interlingua to interface languages (#28630 and #30828 by @Dhghomon and @renchap) -- Add Kashubian, Pennsylvania Dutch, Vai, Jawi Malay, Mohawk and Low German to posting languages (#26024, #26634, #27136, #29098, #27115, and #27434 by @EngineerDali, @HelgeKrueger, and @gunchleoc) -- Add option to use native Ruby driver for Redis through `REDIS_DRIVER=ruby` (#30717 by @vmstan) -- Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, #30858 and #32104 by @ClearlyClaire, @Gargron, and @mjankowski)\ - Server admins can now use libvips as a faster and lighter alternative to ImageMagick for processing user-uploaded images.\ - This requires libvips 8.13 or newer, and needs to be enabled with `MASTODON_USE_LIBVIPS=true`.\ - This is enabled by default in the official Docker images, and is intended to completely replace ImageMagick in the future. -- Add validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm) -- Add anchors to each authorized application in `/oauth/authorized_applications` (#31677 by @fowl2) -- Add active animation to header settings button (#30221, #30307, and #30388 by @daudix) -- Add OpenTelemetry instrumentation (#30130, #30322, #30353, #30350 and #31998 by @julianocosta89, @renchap, @robbkidd and @timetinytim)\ - See https://docs.joinmastodon.org/admin/config/#otel for documentation -- Add API to get multiple accounts and statuses (#27871 and #30465 by @ClearlyClaire)\ - This adds `GET /api/v1/accounts` and `GET /api/v1/statuses` to the REST API, see https://docs.joinmastodon.org/methods/accounts/#index and https://docs.joinmastodon.org/methods/statuses/#index -- Add support for CORS to `POST /oauth/revoke` (#31743 by @ClearlyClaire) -- Add redirection back to previous page after site upload deletion (#30141 by @FawazFarid) -- Add RFC8414 OAuth 2.0 server metadata (#29191 by @ThisIsMissEm) -- Add loading indicator and empty result message to advanced interface search (#30085 by @ClearlyClaire) -- Add `profile` OAuth 2.0 scope, allowing more limited access to user data (#29087 and #30357 by @ThisIsMissEm) -- Add the role ID to the badge component (#29707 by @renchap) -- Add diagnostic message for failure during CLI search deploy (#29462 by @mjankowski) -- Add pagination `Link` headers on API accounts/statuses when pinned true (#29442 by @mjankowski) -- Add support for specifying custom CA cert for Elasticsearch through `ES_CA_FILE` (#29122 and #29147 by @ClearlyClaire) -- Add groundwork for annual reports for accounts (#28693 by @Gargron)\ - This lays the groundwork for a “year-in-reviewâ€/“wrapped†style report for local users, but is currently not in use. -- Add notification email on invalid second authenticator (#28822 by @ClearlyClaire) -- Add date of account deletion in list of accounts in the admin interface (#25640 by @tribela) -- Add new emojis from `jdecked/twemoji` 15.0 (#28404 by @TheEssem) -- Add configurable error handling in attachment batch deletion (#28184 by @vmstan)\ - This makes the S3 batch size configurable through the `S3_BATCH_DELETE_LIMIT` environment variable (defaults to 1000), and adds some retry logic, configurable through the `S3_BATCH_DELETE_RETRY` environment variable (defaults to 3). -- Add VAPID public key to instance serializer (#28006 by @ThisIsMissEm) -- Add support for serving JRD `/.well-known/host-meta.json` in addition to XRD host-meta (#32206 by @c960657) -- Add `nodeName` and `nodeDescription` to nodeinfo `metadata` (#28079 by @6543) -- Add Thai diacritics and tone marks in `HASHTAG_INVALID_CHARS_RE` (#26576 by @ppnplus) -- Add variable delay before link verification of remote account links (#27774 by @ClearlyClaire) -- Add support for invite codes in the registration API (#27805 by @ClearlyClaire) -- Add HTML lang attribute to preview card descriptions (#27503 by @srapilly) -- Add display of relevant account warnings to report action logs (#27425 by @ClearlyClaire) -- Add validation of allowed schemes on preview card URLs (#27485 by @mjankowski) -- Add token introspection without read scope to `/api/v1/apps/verify_credentials` (#27142 by @ThisIsMissEm) -- Add support for cross-origin request to `/nodeinfo/2.0` (#27413 by @palant) -- Add variable delay before link verification of remote account links (#27351 by @ClearlyClaire) -- Add PWA shortcut to `/explore` page (#27235 by @jake-anto) ### Changed -- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, #31525, #32153, and #32201 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\ - This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\ - In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state. -- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, #29659, #31889 and #32033 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\ - The compose form has been completely redesigned for a more modern and consistent look, as well as spelling out the chosen privacy setting and language name at all times.\ - As part of this, the “Unlisted†privacy setting has been renamed to “Quiet publicâ€. -- **Change design of modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, #31399, #31555, #31752, #31801, #31883, #31844, #31864, and #31943 by @ClearlyClaire, @Gargron, @tribela and @vmstan)\ - The mute, block, and domain block confirmation modals have been completely redesigned to be clearer and include more detailed information on the action to be performed.\ - They also have a more modern and consistent design, along with other confirmation modals in the application. -- **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, #31510 and #32128 by @ClearlyClaire, @Gargron, @mjankowski, @renchap, and @vmstan) -- **Change onboarding prompt to follow suggestions carousel in web UI** (#28878, #29272, and #31912 by @Gargron) -- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, #29879, #32073 and #32132 by @c960657, @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\ - All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to read and keeping maximum compatibility across mail clients. -- **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\ - This replaces the “past interactions†recommendation algorithm with a “friends of friends†algorithm that suggests accounts followed by people you follow, and a “similar profiles†algorithm that suggests accounts with a profile similar to your most recent follows.\ - In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\ - This change deprecates the `source` attribute in `Suggestion` entities in the REST API, and replaces it with the new [`sources` attribute](https://docs.joinmastodon.org/entities/Suggestion/#sources). -- Change account search algorithm (#30803 by @Gargron) -- **Change streaming server to use its own dependencies and its own docker image** (#24702, #27967, #26850, #28112, #28115, #28137, #28138, #28497, #28548, #30795, #31612, and #31615 by @TheEssem, @ThisIsMissEm, @jippi, @renchap, @timetinytim, and @vmstan)\ - In order to reduce the amount of runtime dependencies, the streaming server has been moved into a separate package and Docker image.\ - The `mastodon` image does not contain the streaming server anymore, as it has been moved to its own `mastodon-streaming` image.\ - Administrators may need to update their setup accordingly. -- Change how content warnings and filters are displayed in web UI (#31365, and #31761 by @Gargron) -- Change preview card processing to ignore `undefined` as canonical url (#31882 by @oneiros) -- Change embedded posts to use web UI (#31766, #32135 and #32271 by @Gargron) -- Change inner borders in media galleries in web UI (#31852 by @Gargron) -- Change design of media attachments and profile media tab in web UI (#31807, #32048, #31967, #32217, #32224 and #32257 by @ClearlyClaire and @Gargron) -- Change labels on thread indicators in web UI (#31806 by @Gargron) -- Change label of "Data export" menu item in settings interface (#32099 by @c960657) -- Change responsive break points on navigation panel in web UI (#32034 by @Gargron) -- Change cursor to `not-allowed` on disabled buttons (#32076 by @mjankowski) -- Change OAuth authorization prompt to not refer to apps as “third-party†(#32005 by @Gargron) - Change Mastodon to issue correct HTTP signatures by default (#31994 by @ClearlyClaire) -- Change zoom icon in web UI (#29683 by @Gargron) -- Change directory page to use URL query strings for options (#31980, #31977 and #31984 by @ClearlyClaire and @renchap) -- Change report action buttons to be disabled when action has already been taken (#31773, #31822, and #31899 by @ClearlyClaire and @ThisIsMissEm) -- Change width of columns in advanced web UI (#31762 by @Gargron) -- Change design of unread conversations in web UI (#31763 by @Gargron) -- Change Web UI to allow viewing and severing relationships with suspended accounts (#27667 by @ClearlyClaire)\ - This also adds a `with_suspended` parameter to `GET /api/v1/accounts/relationships` in the REST API. -- Change preview card image size limit from 2MB to 8MB when using libvips (#31904 by @ClearlyClaire) -- Change avatars border radius (#31390 by @renchap) -- Change counters to be displayed on profile timelines in web UI (#30525 by @Gargron) -- Change disabled buttons color in light mode to make the difference more visible (#30998 by @renchap) -- Change design of people tab on explore in web UI (#30059 by @Gargron) -- Change sidebar text in web UI (#30696 by @Gargron) -- Change "Follow" to "Follow back" and "Mutual" when appropriate in web UI (#28452, #28465, and #31934 by @ClearlyClaire, @Gargron and @renchap) -- Change media to be hidden/blurred by default in report modal (#28522 by @ClearlyClaire) -- Change order of the "muting" and "blocking" list options in “Data Exports†(#26088 by @fixermark) -- Change admin and moderation notes character limit from 500 to 2000 characters (#30288 by @ThisIsMissEm) -- Change mute options to be in dropdown on muted users list in web UI (#30049 and #31315 by @ClearlyClaire and @Gargron) -- Change out-of-band hashtags design in web UI (#29732 by @Gargron) -- Change design of metadata underneath detailed posts in web UI (#29585, #29605, and #29648 by @ClearlyClaire and @Gargron) -- Change action button to be last on profiles in web UI (#29533 and #29923 by @ClearlyClaire and @Gargron) -- Change confirmation prompts in trending moderation interface to be more specific (#19626 by @tribela) -- Change “Trends†moderation menu to “Recommendations & Trends†and move follow recommendations there (#31292 by @ThisIsMissEm) -- Change irrelevant fields in account cleanup settings to be disabled unless automatic cleanup is enabled (#26562 by @c960657) -- Change dropdown menu icon to not be replaced by close icon when open in web UI (#29532 by @Gargron) -- Change back button to always appear in advanced web UI (#29551 and #29669 by @Gargron) -- Change border of active compose field search inputs (#29832 and #29839 by @vmstan) -- Change instances of Nokogiri HTML4 parsing to HTML5 (#31812, #31815, #31813, and #31814 by @flavorjones) -- Change link detection to allow `@` at the end of an URL (#31124 by @adamniedzielski) -- Change User-Agent to use Mastodon as the product, and http.rb as platform details (#31192 by @ClearlyClaire) -- Change layout and wording of the Content Retention server settings page (#27733 by @vmstan) -- Change unconfirmed users to be kept for one week instead of two days (#30285 by @renchap) -- Change maximum page size for Admin Domain Management APIs from 200 to 500 (#31253 by @ThisIsMissEm) -- Change database pool size to default to Sidekiq concurrency settings in Sidekiq processes (#26488 by @sinoru) -- Change alt text to empty string for avatars (#21875 by @jasminjohal) -- Change Docker images to use custom-built libvips and ffmpeg (#30571, #30569, and #31498 by @vmstan) -- Change external links in the admin audit log to plain text or local administration pages (#27139 and #27150 by @ClearlyClaire and @ThisIsMissEm) -- Change YJIT to be enabled when available (#30310 and #27283 by @ClearlyClaire and @mjankowski)\ - Enable Ruby's built-in just-in-time compiler. This improves performances substantially, at the cost of a slightly increased memory usage. -- Change `.env` file loading from deprecated `dotenv-rails` gem to `dotenv` gem (#29173 and #30121 by @mjankowski)\ - This should have no effect except in the unlikely case an environment variable included a newline. -- Change “Panjabi†language name to the more common spelling “Punjabi†(#27117 by @gunchleoc) -- Change encryption of OTP secrets to use ActiveRecord Encryption (#29831, #28325, #30151, #30202, #30340, and #30344 by @ClearlyClaire and @mjankowski)\ - This requires a manual step from administrators of existing servers. Indeed, they need to generate new secrets, which can be done using `bundle exec rails db:encryption:init`.\ - Furthermore, there is a risk that the introduced migration fails if the server was misconfigured in the past. If that happens, the migration error will include the relevant information. -- Change `/api/v1/announcements` to return regular `Status` entities (#26736 by @ClearlyClaire) -- Change imports to convert case-insensitive fields to lowercase (#29739 and #29740 by @ThisIsMissEm) -- Change stats in the admin interface to be inclusive of the full selected range, from beginning of day to end of day (#29416 and #29841 by @mjankowski) -- Change materialized views to be refreshed concurrently to avoid locks (#29015 by @Gargron) -- Change compose form to use server-provided post character and poll options limits (#28928 and #29490 by @ClearlyClaire and @renchap) -- Change streaming server logging from `npmlog` to `pino` and `pino-http` (#27828 by @ThisIsMissEm)\ - This changes the Mastodon streaming server log format, so this might be considered a breaking change if you were parsing the logs. -- Change media “ALT†label to use a specific CSS class (#28777 by @ClearlyClaire) -- Change streaming API host to not be overridden to localhost in development mode (#28557 by @ClearlyClaire) -- Change cookie rotator to use SHA1 digest for new cookies (#27392 by @ClearlyClaire)\ - Note that this requires that no pre-4.2.0 Mastodon web server is running when this code is deployed, as those would not understand the new cookies.\ - Therefore, zero-downtime updates are only supported if you're coming from 4.2.0 or newer. If you want to skip Mastodon 4.2, you will need to completely stop Mastodon services before updating. -- Change preview card deletes to be done using batch method (#28183 by @vmstan) -- Change `img-src` and `media-src` CSP directives to not include `https:` (#28025 and #28561 by @ClearlyClaire) -- Change self-destruct procedure (#26439, #29049, and #29420 by @ClearlyClaire and @zunda)\ - Instead of enqueuing deletion jobs immediately, `tootctl self-destruct` now outputs a value for the `SELF_DESTRUCT` environment variable, which puts a server in self-destruct mode, processing deletions in the background, while giving users access to their export archives. - -### Removed - -- Remove unused E2EE messaging code and related `crypto` OAuth scope (#31193, #31945, #31963, and #31964 by @ClearlyClaire and @mjankowski) -- Remove StatsD integration (replaced by OpenTelemetry) (#30240 by @mjankowski) -- Remove `CacheBuster` default options (#30718 by @mjankowski) -- Remove home marker updates from the Web UI (#22721 by @davbeck)\ - The web interface was unconditionally updating the home marker to the most recent received post, discarding any value set by other clients, thus making the feature unreliable. -- Remove support for Ruby 3.0 (reaching EOL) (#29702 by @mjankowski) -- Remove setting for unfollow confirmation modal (#29373 by @ClearlyClaire)\ - Instead, the unfollow confirmation modal will always be displayed. -- Remove support for Capistrano (#27295 and #30009 by @mjankowski and @renchap) ### Fixed -- **Fix link preview cards not always preserving the original URL from the status** (#27312 by @Gargron) -- Fix log out from user menu not working on Safari (#31402 by @renchap) -- Fix various issues when in link preview card generation (#28748, #30017, #30362, #30173, #30853, #30929, #30933, #30957, #30987, and #31144 by @adamniedzielski, @oneiros, @phocks, @timothyjrogers, and @tribela) -- Fix handling of missing links in Webfinger responses (#31030 by @adamniedzielski) -- Fix error when accepting an appeal for sensitive posts deleted in the meantime (#32037 by @ClearlyClaire) -- Fix error when encountering reblog of deleted post in feed rebuild (#32001 by @ClearlyClaire) -- Fix Safari browser glitch related to horizontal scrolling (#31960 by @Gargron) -- Fix unresolvable mentions sometimes preventing processing incoming posts (#29215 by @tribela and @ClearlyClaire) -- Fix too many requests caused by relationship look-ups in web UI (#32042 by @Gargron) -- Fix links for reblogs in moderation interface (#31979 by @ClearlyClaire) -- Fix the appearance of avatars when they do not load (#31966 and #32270 by @Gargron and @renchap) -- Fix spurious error notifications for aborted requests in web UI (#31952 by @c960657) -- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil) +- Fix replies collection being cached improperly - Fix security context sometimes not being added in LD-Signed activities (#31871 by @ClearlyClaire) -- Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77) -- Fix wrapping in dashboard quick access buttons (#32043 by @renchap) -- Fix recently used tags hint being displayed in profile edition page when there is none (#32120 by @mjankowski) -- Fix checkbox lists on narrow screens in the settings interface (#32112 by @mjankowski) -- Fix the position of status action buttons being affected by interaction counters (#32084 by @renchap) -- Fix the summary of converted ActivityPub object types to be treated as HTML (#28629 by @Menrath) -- Fix cutoff of instance name in sign-up form (#30598 by @oneiros) -- Fix invalid date searches returning 503 errors (#31526 by @notchairmk) -- Fix invalid `visibility` values in `POST /api/v1/statuses` returning 500 errors (#31571 by @c960657) -- Fix some components re-rendering spuriously in web UI (#31879 and #31881 by @ClearlyClaire and @Gargron) -- Fix sort order of moderation notes on Reports and Accounts (#31528 by @ThisIsMissEm) -- Fix email language when recipient has no selected locale (#31747 by @ClearlyClaire) -- Fix frequently-used languages not correctly updating in the web UI (#31386 by @c960657) -- Fix `POST /api/v1/statuses` silently ignoring invalid `media_ids` parameter (#31681 by @c960657) -- Fix handling of the `BIND` environment variable in the streaming server (#31624 by @ThisIsMissEm) -- Fix empty `aria-hidden` attribute value in logo resources area (#30570 by @mjankowski) -- Fix “Redirect URI†field not being marked as required in “New application†form (#30311 by @ThisIsMissEm) -- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire) -- Fix rack attack `match_type` value typo in logging config (#30514 by @mjankowski) -- Fix various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, #31445, #32091, #32147 and #32137 by @ClearlyClaire, @mjankowski, @valtlai and @vmstan) -- Fix editing description of media uploads with custom thumbnails (#32221 by @ClearlyClaire) -- Fix race condition in `POST /api/v1/push/subscription` (#30166 by @ClearlyClaire) -- Fix post deletion not being delayed when those are part of an account warning (#30163 by @ClearlyClaire) -- Fix rendering error on `/start` when not logged in (#30023 by @timothyjrogers) -- Fix unneeded requests to blocked domains when receiving relayed signed activities from them (#31161 by @ClearlyClaire) -- Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787 by @ClearlyClaire) -- Fix notification-related records not being reattributed when merging accounts (#29694 by @ClearlyClaire) -- Fix results/query in `api/v1/featured_tags/suggestions` (#29597 by @mjankowski) -- Fix distracting and confusing always-showing scrollbar track in boost confirmation modal (#31524 by @ClearlyClaire) -- Fix being able to upload more than 4 media attachments in some cases (#29183 by @mashirozx) -- Fix preview card player getting embedded when clicking on the external link button (#29457 by @ClearlyClaire) -- Fix full date display not respecting the locale 12/24h format (#29448 by @renchap) -- Fix filters title and keywords overflow (#29396 by @GeopJr) -- Fix incorrect date format in “Follows and followers†(#29390 by @JasonPunyon) -- Fix navigation item active highlight for some paths (#32159 by @mjankowski) -- Fix “Edit media†modal sizing and layout when space-constrained (#27095 by @ronilaukkarinen) -- Fix modal container bounds (#29185 by @nico3333fr) -- Fix inefficient HTTP signature parsing using regexps and `StringScanner` (#29133 by @ClearlyClaire) -- Fix moderation report updates through `PUT /api/v1/admin/reports/:id` not being logged in the audit log (#29044, #30342, and #31033 by @mjankowski, @tribela, and @vmstan) -- Fix moderation interface allowing to select rule violation when there are no server rules (#31458 by @ThisIsMissEm) -- Fix redirection from paths with url-encoded `@` to their decoded form (#31184 by @timothyjrogers) -- Fix Trending Tags pending review having an unstable sort order (#31473 by @ThisIsMissEm) -- Fix the emoji dropdown button always opening the dropdown instead of behaving like a toggle (#29012 by @jh97uk) -- Fix processing of incoming posts with bearcaps (#26527 by @kmycode) -- Fix support for IPv6 redis connections in streaming (#31229 by @ThisIsMissEm) -- Fix search form re-rendering spuriously in web UI (#28876 by @Gargron) -- Fix `RedownloadMediaWorker` not being called on transient S3 failure (#28714 by @ClearlyClaire) -- Fix ISO code for Canadian French from incorrect `fr-QC` to `fr-CA` (#26015 by @gunchleoc) -- Fix `.opus` file uploads being misidentified by Paperclip (#28580 by @vmstan) -- Fix loading local accounts with extraneous domain part in WebUI (#28559 by @ClearlyClaire) -- Fix destructive actions in dropdowns not using error color in light theme (#28484 by @logicalmoody) -- Fix call to inefficient `delete_matched` cache method in domain blocks (#28374 by @ClearlyClaire) -- Fix status edits not always being streamed to mentioned users (#28324 by @ClearlyClaire) -- Fix onboarding step descriptions being truncated on narrow screens (#28021 by @ClearlyClaire) -- Fix duplicate IDs in relationships and familiar_followers APIs (#27982 by @KevinBongart) -- Fix modal content not being selectable (#27813 by @pajowu) -- Fix Web UI not displaying appropriate explanation when a user hides their follows/followers (#27791 by @ClearlyClaire) -- Fix format-dependent redirects being cached regardless of requested format (#27632 by @ClearlyClaire) -- Fix confusing screen when visiting a confirmation link for an already-confirmed email (#27368 by @ClearlyClaire) -- Fix explore page reloading when you navigate back to it in web UI (#27489 by @Gargron) -- Fix missing redirection from `/home` to `/deck/home` in the advanced interface (#27378 by @Signez) -- Fix empty environment variables not using default nil value (#27400 by @renchap) -- Fix language sorting in settings (#27158 by @gunchleoc) +- Fix error when encountering reblog of deleted post in feed rebuild (#32001 by @ClearlyClaire) + +## [4.2.12] - 2024-08-19 + +### Fixed + +- Fix broken notifications for mentions from local moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31484)) ## [4.2.11] - 2024-08-16 @@ -1691,4 +1179,2331 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix rate limiting for paths with formats ([Gargron](https://github.com/mastodon/mastodon/pull/20675)) - Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388)) -_For previous changes, review the [stable-3.5 branch](https://github.com/mastodon/mastodon/blob/stable-3.5/CHANGELOG.md)_ +## [3.5.3] - 2022-05-26 + +### Added + +- **Add language dropdown to compose form in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18420), [ykzts](https://github.com/mastodon/mastodon/pull/18460)) +- **Add warning for limited accounts in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18344)) +- Add `limited` attribute to accounts in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18344)) + +### Changed + +- **Change RSS feeds** ([Gargron](https://github.com/mastodon/mastodon/pull/18356), [tribela](https://github.com/mastodon/mastodon/pull/18406)) + - Titles are now date and time of post + - Bodies now render all content faithfully, including polls and emojis + - All media attachments are included with Media RSS +- Change "dangerous" to "sensitive" in privacy policy and web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18515)) +- Change unconfirmed accounts to not be visible in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17530)) +- Change `tootctl search deploy` to improve performance ([Gargron](https://github.com/mastodon/mastodon/pull/18463), [Gargron](https://github.com/mastodon/mastodon/pull/18514)) +- Change search indexing to use batches to minimize resource usage ([Gargron](https://github.com/mastodon/mastodon/pull/18451)) + +### Fixed + +- Fix follower and other counters being able to go negative ([Gargron](https://github.com/mastodon/mastodon/pull/18517)) +- Fix unnecessary query on when creating a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17901)) +- Fix warning an account outside of a report closing all reports for that account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18387)) +- Fix error when resolving a link that redirects to a local post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18314)) +- Fix preferred posting language returning unusable value in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18428)) +- Fix race condition error when external status is reblogged ([ykzts](https://github.com/mastodon/mastodon/pull/18424)) +- Fix missing string for appeal validation error ([Gargron](https://github.com/mastodon/mastodon/pull/18410)) +- Fix block/mute lists showing a follow button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18364)) +- Fix Redis configuration not being changed by `mastodon:setup` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18383)) +- Fix streaming notifications not using quick filter logic in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18316)) +- Fix ambiguous wording on appeal actions in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18328)) +- Fix floating action button obscuring last element in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18332)) +- Fix account warnings not being recorded in audit log ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18338)) +- Fix leftover icons for direct visibility statuses ([Steffo99](https://github.com/mastodon/mastodon/pull/18305)) +- Fix link verification requiring case sensitivity on links ([sgolemon](https://github.com/mastodon/mastodon/pull/18320)) +- Fix embeds not setting their height correctly ([rinsuki](https://github.com/mastodon/mastodon/pull/18301)) + +### Security + +- Fix concurrent unfollowing decrementing follower count more than once ([Gargron](https://github.com/mastodon/mastodon/pull/18527)) +- Fix being able to appeal a strike unlimited times ([Gargron](https://github.com/mastodon/mastodon/pull/18529)) +- Fix being able to report otherwise inaccessible statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18528)) +- Fix empty votes arbitrarily increasing voters count in polls ([Gargron](https://github.com/mastodon/mastodon/pull/18526)) +- Fix moderator identity leak when approving appeal of sensitive marked statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18525)) +- Fix suspended users being able to access APIs that don't require a user ([Gargron](https://github.com/mastodon/mastodon/pull/18524)) +- Fix confirmation redirect to app without `Location` header ([Gargron](https://github.com/mastodon/mastodon/pull/18523)) + +## [3.5.2] - 2022-05-04 + +### Added + +- Add warning on direct messages screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18289)) + - We already had a warning when composing a direct message, it has now been reworded to be more clear + - Same warning is now displayed when viewing sent and received direct messages +- Add ability to set approval-based registration through tootctl ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18248)) +- Add pre-filling of domain from search filter in domain allow/block admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18172)) + +## Changed + +- Change name of “Direct†visibility to “Mentioned people only†in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18146), [Gargron](https://github.com/mastodon/mastodon/pull/18289), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18291)) +- Change trending posts to only show one post from each account ([Gargron](https://github.com/mastodon/mastodon/pull/18181)) +- Change half-life of trending posts from 6 hours to 2 hours ([Gargron](https://github.com/mastodon/mastodon/pull/18182)) +- Change full-text search feature to also include polls you have voted in ([tribela](https://github.com/mastodon/mastodon/pull/18070)) +- Change Redis from using one connection per process, to using a connection pool ([Gargron](https://github.com/mastodon/mastodon/pull/18135), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18160), [Gargron](https://github.com/mastodon/mastodon/pull/18171)) + - Different threads no longer have to wait on a mutex over a single connection + - However, this does increase the number of Redis connections by a fair amount + - We are planning to optimize Redis use so that the pool can be made smaller in the future + +## Removed + +- Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190)) + - The IPs of the blocked e-mail domain or its MX records are no longer checked + - Previously it was too easy to block e-mail providers by mistake + +## Fixed + +- Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260)) +- Fix error when looking up handle with surrounding spaces in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18225)) +- Fix double render error when authorizing interaction ([Gargron](https://github.com/mastodon/mastodon/pull/18203)) +- Fix error when a post references an invalid media attachment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18211)) +- Fix error when trying to revoke OAuth token without supplying a token ([Gargron](https://github.com/mastodon/mastodon/pull/18205)) +- Fix error caused by missing subject in Webfinger response ([Gargron](https://github.com/mastodon/mastodon/pull/18204)) +- Fix error on attempting to delete an account moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18196)) +- Fix light-mode emoji borders in web UI ([Gaelan](https://github.com/mastodon/mastodon/pull/18131)) +- Fix being able to scroll away from the loading bar in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18170)) +- Fix error when a bookmark or favorite has been reported and deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18174)) +- Fix being offered empty “Server rules violation†report option in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18165)) +- Fix temporary network errors preventing from authorizing interactions with remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18161)) +- Fix incorrect link in "new trending tags" email ([cdzombak](https://github.com/mastodon/mastodon/pull/18156)) +- Fix missing indexes on some foreign keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18157)) +- Fix n+1 query on feed merge and populate operations ([Gargron](https://github.com/mastodon/mastodon/pull/18111)) +- Fix feed unmerge worker being exceptionally slow in some conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18110)) +- Fix PeerTube videos appearing with an erroneous “Edited at†marker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18100)) +- Fix instance actor being created incorrectly when running through migrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18109)) +- Fix web push notifications containing HTML entities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18071)) +- Fix inconsistent parsing of `TRUSTED_PROXY_IP` ([ykzts](https://github.com/mastodon/mastodon/pull/18051)) +- Fix error when fetching pinned posts ([tribela](https://github.com/mastodon/mastodon/pull/18030)) +- Fix wrong optimization in feed populate operation ([dogelover911](https://github.com/mastodon/mastodon/pull/18009)) +- Fix error in alias settings page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18004)) + +## [3.5.1] - 2022-04-08 + +### Added + +- Add pagination for trending statuses in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17976)) + +### Changed + +- Change e-mail notifications to only be sent when recipient is offline ([Gargron](https://github.com/mastodon/mastodon/pull/17984)) + - Send e-mails for mentions and follows by default again + - But only when recipient does not have push notifications through an app +- Change `website` attribute to be nullable on `Application` entity in REST API ([rinsuki](https://github.com/mastodon/mastodon/pull/17962)) + +### Removed + +- Remove sign-in token authentication, instead send e-mail about new sign-in ([Gargron](https://github.com/mastodon/mastodon/pull/17970)) + - You no longer need to enter a security code sent through e-mail + - Instead you get an e-mail about a new sign-in from an unfamiliar IP address + +### Fixed + +- Fix error responses for `from` search prefix ([single-right-quote](https://github.com/mastodon/mastodon/pull/17963)) +- Fix dangling language-specific trends ([Gargron](https://github.com/mastodon/mastodon/pull/17997)) +- Fix extremely rare race condition when deleting a status or account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17994)) +- Fix trends returning less results per page when filtered in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17996)) +- Fix pagination header on empty trends responses in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17986)) +- Fix cookies secure flag being set when served over Tor ([Gargron](https://github.com/mastodon/mastodon/pull/17992)) +- Fix migration error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17991)) +- Fix error when re-running some migrations if they get interrupted at the wrong moment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17989)) +- Fix potentially missing statuses when reconnecting to streaming API in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17981), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17987), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17980)) +- Fix error when sending warning emails with custom text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17983)) +- Fix unset `SMTP_RETURN_PATH` environment variable causing e-mail not to send ([Gargron](https://github.com/mastodon/mastodon/pull/17982)) +- Fix possible duplicate statuses in timelines in some edge cases in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17971)) +- Fix spurious edits and require incoming edits to be explicitly marked as such ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17918)) +- Fix error when encountering invalid pinned statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17964)) +- Fix inconsistency in error handling when removing a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17974)) +- Fix admin API unconditionally requiring CSRF token ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17975)) +- Fix trending tags endpoint missing `offset` param in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17973)) +- Fix unusual number formatting in some locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17929)) +- Fix `S3_FORCE_SINGLE_REQUEST` environment variable not working ([HolgerHuo](https://github.com/mastodon/mastodon/pull/17922)) +- Fix failure to build assets with OpenSSL 3 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17930)) +- Fix PWA manifest using outdated routes ([HolgerHuo](https://github.com/mastodon/mastodon/pull/17921)) +- Fix error when indexing statuses into Elasticsearch ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17912)) + +## [3.5.0] - 2022-03-30 + +### Added + +- **Add support for incoming edited posts** ([Gargron](https://github.com/mastodon/mastodon/pull/16697), [Gargron](https://github.com/mastodon/mastodon/pull/17727), [Gargron](https://github.com/mastodon/mastodon/pull/17728), [Gargron](https://github.com/mastodon/mastodon/pull/17320), [Gargron](https://github.com/mastodon/mastodon/pull/17404), [Gargron](https://github.com/mastodon/mastodon/pull/17390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17335), [Gargron](https://github.com/mastodon/mastodon/pull/17696), [Gargron](https://github.com/mastodon/mastodon/pull/17745), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17740), [Gargron](https://github.com/mastodon/mastodon/pull/17697), [Gargron](https://github.com/mastodon/mastodon/pull/17648), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17531), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17499), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17498), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17380), [Gargron](https://github.com/mastodon/mastodon/pull/17373), [Gargron](https://github.com/mastodon/mastodon/pull/17334), [Gargron](https://github.com/mastodon/mastodon/pull/17333), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17699), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17748)) + - Previous versions remain available for perusal and comparison + - People who reblogged a post are notified when it's edited + - New REST APIs: + - `PUT /api/v1/statuses/:id` + - `GET /api/v1/statuses/:id/history` + - `GET /api/v1/statuses/:id/source` + - New streaming API event: + - `status.update` +- **Add appeals for moderator decisions** ([Gargron](https://github.com/mastodon/mastodon/pull/17364), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17725), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17566), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17652), [Gargron](https://github.com/mastodon/mastodon/pull/17616), [Gargron](https://github.com/mastodon/mastodon/pull/17615), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17554), [Gargron](https://github.com/mastodon/mastodon/pull/17523)) + - All default moderator decisions now notify the affected user by e-mail + - They now link to an appeal page instead of suggesting replying to the e-mail + - They can now be found in account settings and not just e-mail + - Users can submit one appeal within 20 days of the decision + - Moderators can approve or reject the appeal +- **Add notifications for posts deleted by moderators** ([Gargron](https://github.com/mastodon/mastodon/pull/17204), [Gargron](https://github.com/mastodon/mastodon/pull/17668), [Gargron](https://github.com/mastodon/mastodon/pull/17746), [Gargron](https://github.com/mastodon/mastodon/pull/17679), [Gargron](https://github.com/mastodon/mastodon/pull/17487)) + - New, redesigned report view in admin UI + - Common report actions now only take one click to complete + - Deleting posts or marking as sensitive from report now notifies user + - Reports can be categorized by reason and specific rules violated + - The reasons are automatically cited in the notifications, except for spam + - Marking posts as sensitive now federates using post editing +- **Add explore page with trending posts and links** ([Gargron](https://github.com/mastodon/mastodon/pull/17123), [Gargron](https://github.com/mastodon/mastodon/pull/17431), [Gargron](https://github.com/mastodon/mastodon/pull/16917), [Gargron](https://github.com/mastodon/mastodon/pull/17677), [Gargron](https://github.com/mastodon/mastodon/pull/16938), [Gargron](https://github.com/mastodon/mastodon/pull/17044), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16978), [Gargron](https://github.com/mastodon/mastodon/pull/16979), [tribela](https://github.com/mastodon/mastodon/pull/17066), [Gargron](https://github.com/mastodon/mastodon/pull/17072), [Gargron](https://github.com/mastodon/mastodon/pull/17403), [noiob](https://github.com/mastodon/mastodon/pull/17624), [mayaeh](https://github.com/mastodon/mastodon/pull/17755), [mayaeh](https://github.com/mastodon/mastodon/pull/17757), [Gargron](https://github.com/mastodon/mastodon/pull/17760), [mayaeh](https://github.com/mastodon/mastodon/pull/17762)) + - Hashtag trends algorithm is extended to work for posts and links + - Links are only considered if they have an adequate preview card + - Preview card generation has been improved to support structured data + - Links can only trend if the publisher (domain) has been approved + - Posts can only trend if the author has been approved + - Individual approval and rejection for posts and links is also available + - Moderators are notified about pending trends at most once every 2 hours + - Posts and link trends are language-specific + - Search page is redesigned into explore page in web UI + - Discovery tab is coming soon in official iOS and Android apps + - New REST APIs: + - `GET /api/v1/trends/links` + - `GET /api/v1/trends/statuses` + - `GET /api/v1/trends/tags` (alias of `GET /api/v1/trends`) + - `GET /api/v1/admin/trends/links` + - `GET /api/v1/admin/trends/statuses` + - `GET /api/v1/admin/trends/tags` +- **Add graphs and retention metrics to admin dashboard** ([Gargron](https://github.com/mastodon/mastodon/pull/16829), [Gargron](https://github.com/mastodon/mastodon/pull/17617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17570), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16910), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16909), [mashirozx](https://github.com/mastodon/mastodon/pull/16884), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16854)) + - Dashboard shows more numbers with development over time + - Other data such as most used interface languages and sign-up sources + - User retention graph shows how many new users stick around + - New REST APIs: + - `POST /api/v1/admin/measures` + - `POST /api/v1/admin/dimensions` + - `POST /api/v1/admin/retention` +- Add `GET /api/v1/accounts/familiar_followers` to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17700)) +- Add `POST /api/v1/accounts/:id/remove_from_followers` to REST API ([noellabo](https://github.com/mastodon/mastodon/pull/16864)) +- Add `category` and `rule_ids` params to `POST /api/v1/reports` IN REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17492), [Gargron](https://github.com/mastodon/mastodon/pull/17682), [Gargron](https://github.com/mastodon/mastodon/pull/17713)) + - `category` can be one of: `spam`, `violation`, `other` (default) + - `rule_ids` must reference `rules` returned in `GET /api/v1/instance` +- Add global `lang` param to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17464), [Gargron](https://github.com/mastodon/mastodon/pull/17592)) +- Add `types` param to `GET /api/v1/notifications` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17767)) +- **Add notifications for moderators about new sign-ups** ([Gargron](https://github.com/mastodon/mastodon/pull/16953), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17629)) + - When a new user confirms e-mail, moderators receive a notification + - New notification type: + - `admin.sign_up` +- Add authentication history ([Gargron](https://github.com/mastodon/mastodon/pull/16408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16428), [baby-gnu](https://github.com/mastodon/mastodon/pull/16654)) +- Add ability to automatically delete old posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16529), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17691), [tribela](https://github.com/mastodon/mastodon/pull/16653)) +- Add ability to pin private posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16954), [tribela](https://github.com/mastodon/mastodon/pull/17326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17304), [MitarashiDango](https://github.com/mastodon/mastodon/pull/17647)) +- Add ability to filter search results by author using `from:` syntax ([tribela](https://github.com/mastodon/mastodon/pull/16526)) +- Add ability to delete canonical email blocks in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16644)) +- Add ability to purge undeliverable domains in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16686), [tribela](https://github.com/mastodon/mastodon/pull/17210), [tribela](https://github.com/mastodon/mastodon/pull/17741), [tribela](https://github.com/mastodon/mastodon/pull/17209)) +- Add ability to disable e-mail token authentication for specific users in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/16427)) +- **Add ability to suspend accounts in batches in admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/17009), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17301), [Gargron](https://github.com/mastodon/mastodon/pull/17444)) + - New, redesigned accounts list in admin UI + - Batch suspensions are meant to help clean up spam and bot accounts + - They do not generate notifications +- Add ability to filter reports by origin of target account in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/16487)) +- Add support for login through OpenID Connect ([chandrn7](https://github.com/mastodon/mastodon/pull/16221)) +- Add lazy loading for emoji picker in web UI ([mashirozx](https://github.com/mastodon/mastodon/pull/16907), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17011)) +- Add single option votes tooltip in polls in web UI ([Brawaru](https://github.com/mastodon/mastodon/pull/16849)) +- Add confirmation modal when closing media edit modal with unsaved changes in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16518)) +- Add hint about missing media attachment description in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17845)) +- Add support for fetching Create and Announce activities by URI in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16383)) +- Add `S3_FORCE_SINGLE_REQUEST` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16866)) +- Add `OMNIAUTH_ONLY` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17288), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17345)) +- Add `ES_USER` and `ES_PASS` environment variables for Elasticsearch authentication ([tribela](https://github.com/mastodon/mastodon/pull/16890)) +- Add `CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED` environment variable ([baby-gnu](https://github.com/mastodon/mastodon/pull/16655)) +- Add ability to pass specific domains to `tootctl accounts cull` ([tribela](https://github.com/mastodon/mastodon/pull/16511)) +- Add `--by-uri` option to `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16434)) +- Add `--batch-size` option to `tootctl search deploy` ([aquarla](https://github.com/mastodon/mastodon/pull/17049)) +- Add `--remove-orphans` option to `tootctl statuses remove` ([noellabo](https://github.com/mastodon/mastodon/pull/17067)) + +### Changed + +- Change design of federation pages in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/17704), [noellabo](https://github.com/mastodon/mastodon/pull/17735), [Gargron](https://github.com/mastodon/mastodon/pull/17765)) +- Change design of account cards in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17689)) +- Change `follow` scope to be covered by `read` and `write` scopes in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17678)) +- Change design of authorized applications page ([Gargron](https://github.com/mastodon/mastodon/pull/17656), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17686)) +- Change e-mail domain blocks to block IPs dynamically ([Gargron](https://github.com/mastodon/mastodon/pull/17635), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17650), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17649)) +- Change report modal to include category selection in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17565), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17734), [Gargron](https://github.com/mastodon/mastodon/pull/17654), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17632)) +- Change reblogs to not count towards hashtag trends anymore ([Gargron](https://github.com/mastodon/mastodon/pull/17501)) +- Change languages to be listed under standard instead of native name in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/17485)) +- Change routing paths to use usernames in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/16171), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16772), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16773), [mashirozx](https://github.com/mastodon/mastodon/pull/16793), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17060)) +- Change list title input design in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17092)) +- Change "Opt-in to profile directory" preference to be general discoverability preference ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16637)) +- Change API rate limits to use /64 masking on IPv6 addresses ([tribela](https://github.com/mastodon/mastodon/pull/17588), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17600), [zunda](https://github.com/mastodon/mastodon/pull/17590)) +- Change allowed formats for locally uploaded custom emojis to include GIF ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17706), [Gargron](https://github.com/mastodon/mastodon/pull/17759)) +- Change error message when chosen password is too long ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17082)) +- Change minimum required Elasticsearch version from 6 to 7 ([noellabo](https://github.com/mastodon/mastodon/pull/16915)) + +### Removed + +- Remove profile directory link from main navigation panel in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17688)) +- **Remove language detection through cld3** ([Gargron](https://github.com/mastodon/mastodon/pull/17478), [ykzts](https://github.com/mastodon/mastodon/pull/17539), [Gargron](https://github.com/mastodon/mastodon/pull/17496), [Gargron](https://github.com/mastodon/mastodon/pull/17722)) + - cld3 is very inaccurate on short-form content even with unique alphabets + - Post language can be overridden individually using `language` param + - Otherwise, it defaults to the user's interface language +- Remove support for `OAUTH_REDIRECT_AT_SIGN_IN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17287)) + - Use `OMNIAUTH_ONLY` instead +- Remove Keybase integration ([Gargron](https://github.com/mastodon/mastodon/pull/17045)) +- Remove old columns and indexes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17245), [Gargron](https://github.com/mastodon/mastodon/pull/16409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17191)) +- Remove shortcodes from newly-created media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16730), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16763)) + +### Deprecated + +- `GET /api/v1/trends` → `GET /api/v1/trends/tags` +- OAuth `follow` scope → `read` and/or `write` +- `text` attribute on `DELETE /api/v1/statuses/:id` → `GET /api/v1/statuses/:id/source` + +### Fixed + +- Fix IDN domains not being rendered correctly in a few left-over places ([Gargron](https://github.com/mastodon/mastodon/pull/17848)) +- Fix Sanskrit translation not being used in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17820)) +- Fix Kurdish languages having the wrong language codes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17812)) +- Fix pghero making database schema suggestions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17807)) +- Fix encoding glitch in the OpenGraph description of a profile page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17821)) +- Fix web manifest not permitting PWA usage from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714)) +- Fix not being able to edit media attachments for scheduled posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17690)) +- Fix subscribed relay activities being recorded as boosts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17571)) +- Fix streaming API server error messages when JSON parsing fails not specifying the source ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17559)) +- Fix browsers autofilling new password field with old password ([mashirozx](https://github.com/mastodon/mastodon/pull/17702)) +- Fix text being invisible before fonts load in web UI ([tribela](https://github.com/mastodon/mastodon/pull/16330)) +- Fix public profile pages of unconfirmed users being accessible ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17385), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17457)) +- Fix nil error when trying to fetch key for signature verification ([Gargron](https://github.com/mastodon/mastodon/pull/17747)) +- Fix null values being included in some indexes ([Gargron](https://github.com/mastodon/mastodon/pull/17711)) +- Fix `POST /api/v1/emails/confirmations` not being available after sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/17743)) +- Fix rare race condition when reblogged post is deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17693), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17730)) +- Fix being able to add more than 4 hashtags to hashtag column in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17729)) +- Fix data integrity of featured tags ([Gargron](https://github.com/mastodon/mastodon/pull/17712)) +- Fix performance of account timelines ([Gargron](https://github.com/mastodon/mastodon/pull/17709)) +- Fix returning empty `

` tag for blank account `note` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17687)) +- Fix leak of existence of otherwise inaccessible posts in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17684)) +- Fix not showing loading indicator when searching in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17655)) +- Fix media modal footer's “external link†not being a link ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17561)) +- Fix reply button on media modal not giving focus to compose form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17626)) +- Fix some media attachments being converted with too high framerates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17619)) +- Fix sign in token and warning emails failing to send when contact e-mail address is malformed ([helloworldstack](https://github.com/mastodon/mastodon/pull/17589)) +- Fix opening the emoji picker scrolling the single-column view to the top ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17579)) +- Fix edge case where settings/admin page sidebar would be incorrectly hidden ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17580)) +- Fix performance of server-side filtering ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17575)) +- Fix privacy policy link not being visible on small screens ([Gargron](https://github.com/mastodon/mastodon/pull/17533)) +- Fix duplicate accounts when searching by IP range in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/17524), [tribela](https://github.com/mastodon/mastodon/pull/17150)) +- Fix error when performing a batch action on posts in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17532)) +- Fix deletes not being signed in authorized fetch mode ([Gargron](https://github.com/mastodon/mastodon/pull/17484)) +- Fix Undo Announce sometimes inlining the originally Announced status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17516)) +- Fix localization of cold-start follow recommendations ([Gargron](https://github.com/mastodon/mastodon/pull/17479), [Gargron](https://github.com/mastodon/mastodon/pull/17486)) +- Fix replies collection incorrectly looping ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17462)) +- Fix errors when multiple Delete are received for a given actor ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17460)) +- Fixed prototype pollution bug and only allow trusted origin ([r0hanSH](https://github.com/mastodon/mastodon/pull/17420)) +- Fix text being incorrectly pre-selected in composer textarea on /share ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17339)) +- Fix SMTP_ENABLE_STARTTLS_AUTO/SMTP_TLS/SMTP_SSL environment variables don't work ([kgtkr](https://github.com/mastodon/mastodon/pull/17216)) +- Fix media upload specific rate limits only being applied to v1 endpoint in REST API ([tribela](https://github.com/mastodon/mastodon/pull/17272)) +- Fix media descriptions not being used for client-side filtering ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17206)) +- Fix cold-start follow recommendation favouring older accounts due to wrong sorting ([noellabo](https://github.com/mastodon/mastodon/pull/17126)) +- Fix not redirect to the right page after authenticating with WebAuthn ([heguro](https://github.com/mastodon/mastodon/pull/17098)) +- Fix searching for additional hashtags in hashtag column ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17054)) +- Fix color of hashtag column settings inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17058)) +- Fix performance of `tootctl statuses remove` ([noellabo](https://github.com/mastodon/mastodon/pull/17052)) +- Fix `tootctl accounts cull` not excluding domains on timeouts and certificate issues ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16433)) +- Fix 404 error when filtering admin action logs by non-existent target account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16643)) +- Fix error when accessing streaming API without any OAuth scopes ([Brawaru](https://github.com/mastodon/mastodon/pull/16823)) +- Fix follow request count not updating when new follow requests arrive over streaming API in web UI ([matildepark](https://github.com/mastodon/mastodon/pull/16652)) +- Fix error when unsuspending a local account ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16605)) +- Fix crash when a notification contains a not yet processed media attachment in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16573)) +- Fix wrong color of download button in audio player in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16572)) +- Fix notes for others accounts not being deleted when an account is deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16579)) +- Fix error when logging occurrence of unsupported video file ([noellabo](https://github.com/mastodon/mastodon/pull/16581)) +- Fix wrong elements in trends widget being hidden on smaller screens in web UI ([tribela](https://github.com/mastodon/mastodon/pull/16570)) +- Fix link to about page being displayed in limited federation mode ([weex](https://github.com/mastodon/mastodon/pull/16432)) +- Fix styling of boost button in media modal not reflecting ability to boost ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16387)) +- Fix OCR failure when erroneous lang data is in cache ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16386)) +- Fix downloading media from blocked domains in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/16914)) +- Fix login form being displayed on landing page when already logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17348)) +- Fix polling for media processing status too frequently in web UI ([tribela](https://github.com/mastodon/mastodon/pull/17271)) +- Fix hashtag autocomplete overriding user-typed case ([weex](https://github.com/mastodon/mastodon/pull/16460)) +- Fix WebAuthn authentication setup to not prompt for PIN ([truongnmt](https://github.com/mastodon/mastodon/pull/16545)) + +### Security + +- Fix being able to post URLs longer than 4096 characters ([Gargron](https://github.com/mastodon/mastodon/pull/17908)) +- Fix being able to bypass e-mail restrictions ([Gargron](https://github.com/mastodon/mastodon/pull/17909)) + +## [3.4.6] - 2022-02-03 + +### Fixed + +- Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338)) +- Fix spurious errors when receiving an Add activity for a private post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17425)) + +### Security + +- Fix error-prone SQL queries ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15828)) +- Fix not compacting incoming signed JSON-LD activities ([puckipedia](https://github.com/mastodon/mastodon/pull/17426), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17428)) (CVE-2022-24307) +- Fix insufficient sanitization of report comments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17430)) +- Fix stop condition of a Common Table Expression ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17427)) +- Disable legacy XSS filtering ([Wonderfall](https://github.com/mastodon/mastodon/pull/17289)) + +## [3.4.5] - 2022-01-31 + +### Added + +- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393)) +- Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000)) + +### Fixed + +- Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394)) +- Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398)) +- Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188)) +- Fix Ruby 3.0 dependencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16723)) +- Fix followers synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16510)) + +## [3.4.4] - 2021-11-26 + +### Fixed + +- Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036)) +- Fix overflow of long profile fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17010)) +- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986)) +- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998)) +- Fix logout link not working in Safari ([noellabo](https://github.com/mastodon/mastodon/pull/16574)) +- Fix “open†link of media modal not closing modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16524)) +- Fix replying from modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16516)) +- Fix `mastodon:setup` command crashing in some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976)) + +### Security + +- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042)) +- Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041)) + +## [3.4.3] - 2021-11-06 + +### Fixed + +- Fix login being broken due to inaccurately applied backport fix in 3.4.2 ([Gargron](https://github.com/mastodon/mastodon/commit/5c47a18c8df3231aa25c6d1f140a71a7fac9cbf9)) + +## [3.4.2] - 2021-11-06 + +### Added + +- Add `configuration` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/16485)) + +### Fixed + +- Fix handling of back button with modal windows in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16499)) +- Fix pop-in player when author has long username in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16468)) +- Fix crash when a status with a playing video gets deleted in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16384)) +- Fix crash with Microsoft Translate in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16525)) +- Fix PWA not being usable from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714)) +- Fix locale-specific number rounding errors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16469)) +- Fix scheduling a status decreasing status count ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16791)) +- Fix user's canonical email address being blocked when user deletes own account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16503)) +- Fix not being able to suspend users that already have their canonical e-mail blocked ([Gargron](https://github.com/mastodon/mastodon/pull/16455)) +- Fix anonymous access to outbox not being cached by the reverse proxy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16458)) +- Fix followers synchronization mechanism not working when URI has empty path ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16744)) +- Fix serialization of counts in REST API when user hides their network ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16418)) +- Fix inefficiencies in auto-linking code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16506)) +- Fix `tootctl self-destruct` not sending delete activities for recently-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16688)) +- Fix suspicious sign-in e-mail text being out of date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16690)) +- Fix some frameworks being unnecessarily loaded ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16725)) +- Fix canonical e-mail blocks missing foreign key constraints ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16448)) +- Fix inconsistent order on account's statuses page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/16937)) +- Fix media from blocked domains being redownloaded by `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/16914)) +- Fix `mastodon:setup` generated env-file syntax ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16896)) +- Fix link previews being incorrectly generated from earlier links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16885)) +- Fix wrong `to`/`cc` values for remote groups in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16700)) +- Fix mentions with non-ascii TLDs not being processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16689)) +- Fix authentication failures halfway through a sign-in attempt ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16792)) +- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628)) +- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598)) +- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583)) +- Fix newlines being added to account notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576)) +- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941)) +- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943)) + +### Security + +- Fix user notes not having a length limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16942)) +- Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943)) + +## [3.4.1] - 2021-06-03 + +### Added + +- Add new emoji assets from Twemoji 13.1.0 ([Gargron](https://github.com/mastodon/mastodon/pull/16345)) + +### Fixed + +- Fix some ActivityPub identifiers in server actor outbox ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16343)) +- Fix custom CSS path setting cookies and being uncacheable due to it ([tribela](https://github.com/mastodon/mastodon/pull/16314)) +- Fix unread notification count when polling in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16272)) +- Fix health check not being accessible through localhost ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16270)) +- Fix some redis locks auto-releasing too fast ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16276), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16291)) +- Fix e-mail confirmations API not working correctly ([Gargron](https://github.com/mastodon/mastodon/pull/16348)) +- Fix migration script not being able to run if it fails midway ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16312)) +- Fix account deletion sometimes failing because of optimistic locks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16317)) +- Fix deprecated slash as division in SASS files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16347)) +- Fix `tootctl search deploy` compatibility error on Ruby 3 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16346)) +- Fix mailer jobs for deleted notifications erroring out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16294)) + +## [3.4.0] - 2021-05-16 + +### Added + +- **Add follow recommendations for onboarding** ([Gargron](https://github.com/mastodon/mastodon/pull/15945), [Gargron](https://github.com/mastodon/mastodon/pull/16161), [Gargron](https://github.com/mastodon/mastodon/pull/16060), [Gargron](https://github.com/mastodon/mastodon/pull/16077), [Gargron](https://github.com/mastodon/mastodon/pull/16078), [Gargron](https://github.com/mastodon/mastodon/pull/16160), [Gargron](https://github.com/mastodon/mastodon/pull/16079), [noellabo](https://github.com/mastodon/mastodon/pull/16044), [noellabo](https://github.com/mastodon/mastodon/pull/16045), [Gargron](https://github.com/mastodon/mastodon/pull/16152), [Gargron](https://github.com/mastodon/mastodon/pull/16153), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16082), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16173), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16159), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16189)) + - Tutorial on first web UI launch has been replaced with follow suggestions + - Follow suggestions take user locale into account and are a mix of accounts most followed by currently active local users, and accounts that wrote the most shared/favourited posts in the last 30 days + - Only accounts that have opted-in to being discoverable from their profile settings, and that do not require follow requests, will be suggested + - Moderators can review suggestions for every supported locale and suppress specific suggestions from appearing and admins can ensure certain accounts always show up in suggestions from the settings area + - New users no longer automatically follow admins +- **Add server rules** ([Gargron](https://github.com/mastodon/mastodon/pull/15769), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15778)) + - Admins can create and edit itemized server rules + - They are available through the REST API and on the about page +- **Add canonical e-mail blocks for suspended accounts** ([Gargron](https://github.com/mastodon/mastodon/pull/16049)) + - Normally, people can make multiple accounts using the same e-mail address using the `+` trick or by inserting or removing `.` characters from the first part of their address + - Once an account is suspended, it will no longer be possible for the e-mail address used by that account to be used for new sign-ups in any of its forms +- Add management of delivery availability in admin UI ([noellabo](https://github.com/mastodon/mastodon/pull/15771)) +- **Add system checks to dashboard in admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/15989), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15954), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16002)) + - The dashboard will now warn you if you some Sidekiq queues are not being processed, if you have not defined any server rules, or if you forgot to run database migrations from the latest Mastodon upgrade +- Add inline description of moderation actions in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15792)) +- Add "recommended" label to activity/peers API toggles in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/16081)) +- Add joined date to profiles in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/16169), [rinsuki](https://github.com/mastodon/mastodon/pull/16186)) +- Add transition to media modal background in web UI ([mkljczk](https://github.com/mastodon/mastodon/pull/15843)) +- Add option to opt-out of unread notification markers in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15842)) +- Add borders to 📱, 🚲, and 📲 emojis in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15794), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16035)) +- Add dropdown for boost privacy in boost confirmation modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15704)) +- Add support for Ruby 3.0 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16046), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16174)) +- Add `Message-ID` header to outgoing emails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16076)) + - Some e-mail spam filters penalize e-mails that have a `Message-ID` header that uses a different domain name than the sending e-mail address. Now, the same domain will be used +- Add `af`, `gd` and `si` locales ([Gargron](https://github.com/mastodon/mastodon/pull/16090)) +- Add guard against DNS rebinding attacks ([noellabo](https://github.com/mastodon/mastodon/pull/16087), [noellabo](https://github.com/mastodon/mastodon/pull/16095)) +- Add HTTP header to explicitly opt-out of FLoC by default ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16036)) +- Add missing push notification title for polls and statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15929), [mkljczk](https://github.com/mastodon/mastodon/pull/15564), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15931)) +- Add `POST /api/v1/emails/confirmations` to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15816), [Gargron](https://github.com/mastodon/mastodon/pull/15949)) + - This method allows an app through which a user signed-up to request a new confirmation e-mail to be sent, or to change the e-mail of the account before it is confirmed +- Add `GET /api/v1/accounts/lookup` to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15740), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15750)) + - This method allows to quickly convert a username of a known account to an ID that can be used with the REST API, or to check if a username is available + for sign-up +- Add `policy` param to `POST /api/v1/push/subscriptions` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/16040)) + - This param allows an app to control from whom notifications should be delivered as push notifications to the app +- Add `details` to error response for `POST /api/v1/accounts` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15803)) + - This attribute allows an app to display more helpful information to the user about why the sign-up did not succeed +- Add `SIDEKIQ_REDIS_URL` and related environment variables to optionally use a separate Redis server for Sidekiq ([noellabo](https://github.com/mastodon/mastodon/pull/16188)) + +### Changed + +- Change trending hashtags to be affected be reblogs ([Gargron](https://github.com/mastodon/mastodon/pull/16164)) + - Previously, only original posts contributed to a hashtag's trending score + - Now, reblogs of posts will also contribute to that hashtag's trending score +- Change e-mail confirmation link to always redirect to web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16151)) +- Change log level of worker lifecycle to WARN in streaming API ([Gargron](https://github.com/mastodon/mastodon/pull/16110)) + - Since running with INFO log level in production is not always desirable, it is easy to miss when a worker is shutdown and a new one is started +- Change the nouns "toot" and "status" to "post" in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/16080), [Gargron](https://github.com/mastodon/mastodon/pull/16089)) + - To be clear, the button still says "Toot!" +- Change order of dropdown menu on posts to be more intuitive in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/15647)) +- Change description of keyboard shortcuts in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/16129)) +- Change option labels on edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/16041)) + - "Lock account" is now "Require follow requests" + - "List this account on the directory" is now "Suggest account to others" + - "Hide your network" is now "Hide your social graph" +- Change newly generated account IDs to not be enumerable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15844)) +- Change Web Push API deliveries to use request pooling ([Gargron](https://github.com/mastodon/mastodon/pull/16014)) +- Change multiple mentions with same username to render with domain ([Gargron](https://github.com/mastodon/mastodon/pull/15718), [noellabo](https://github.com/mastodon/mastodon/pull/16038)) + - When a post contains mentions of two or more users who have the same username, but on different domains, render their names with domain to help disambiguate them + - Always render the domain of usernames used in profile metadata +- Change health check endpoint to reveal less information ([Gargron](https://github.com/mastodon/mastodon/pull/15988)) +- Change account counters to use upsert (requires Postgres >= 9.5) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15913)) +- Change `mastodon:setup` to not call `assets:precompile` in Docker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13942)) +- **Change max. image dimensions to 1920x1080px (1080p)** ([Gargron](https://github.com/mastodon/mastodon/pull/15690)) + - Previously, this was 1280x1280px + - This is the amount of pixels that original images get downsized to +- Change custom emoji to be animated when hovering container in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15637)) +- Change streaming API from deprecated ClusterWS/cws to ws ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15932)) +- Change systemd configuration to add sandboxing features ([Izorkin](https://github.com/mastodon/mastodon/pull/15937), [Izorkin](https://github.com/mastodon/mastodon/pull/16103), [Izorkin](https://github.com/mastodon/mastodon/pull/16127)) +- Change nginx configuration to make running Onion service easier ([cohosh](https://github.com/mastodon/mastodon/pull/15498)) +- Change Helm configuration ([dunn](https://github.com/mastodon/mastodon/pull/15722), [dunn](https://github.com/mastodon/mastodon/pull/15728), [dunn](https://github.com/mastodon/mastodon/pull/15748), [dunn](https://github.com/mastodon/mastodon/pull/15749), [dunn](https://github.com/mastodon/mastodon/pull/15767)) +- Change Docker configuration ([SuperSandro2000](https://github.com/mastodon/mastodon/pull/10823), [mashirozx](https://github.com/mastodon/mastodon/pull/15978)) + +### Removed + +- Remove PubSubHubbub-related columns from accounts table ([Gargron](https://github.com/mastodon/mastodon/pull/16170), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15857)) +- Remove dependency on @babel/plugin-proposal-class-properties ([ykzts](https://github.com/mastodon/mastodon/pull/16155)) +- Remove dependency on pluck_each gem ([Gargron](https://github.com/mastodon/mastodon/pull/16012)) +- Remove spam check and dependency on nilsimsa gem ([Gargron](https://github.com/mastodon/mastodon/pull/16011)) +- Remove MySQL-specific code from Mastodon::MigrationHelpers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15924)) +- Remove IE11 from supported browsers target ([gol-cha](https://github.com/mastodon/mastodon/pull/15779)) + +### Fixed + +- Fix "You might be interested in" flashing while searching in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/16162)) +- Fix display of posts without text content in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15665)) +- Fix Google Translate breaking web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15610), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15611)) +- Fix web UI crashing when SVG support is disabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15809)) +- Fix web UI crash when a status opened in the media modal is deleted ([kaias1jp](https://github.com/mastodon/mastodon/pull/15701)) +- Fix OCR language data failing to load in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15519)) +- Fix footer links not being clickable in Safari in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/15496)) +- Fix autofocus/autoselection not working on mobile in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15555), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15985)) +- Fix media redownload worker retrying on unexpected response codes ([Gargron](https://github.com/mastodon/mastodon/pull/16111)) +- Fix thread resolve worker retrying when status no longer exists ([Gargron](https://github.com/mastodon/mastodon/pull/16109)) +- Fix n+1 queries when rendering statuses in REST API ([abcang](https://github.com/mastodon/mastodon/pull/15641)) +- Fix n+1 queries when rendering notifications in REST API ([abcang](https://github.com/mastodon/mastodon/pull/15640)) +- Fix delete of local reply to local parent not being forwarded ([Gargron](https://github.com/mastodon/mastodon/pull/16096)) +- Fix remote reporters not receiving suspend/unsuspend activities ([Gargron](https://github.com/mastodon/mastodon/pull/16050)) +- Fix understanding (not fully qualified) `as:Public` and `Public` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15948)) +- Fix actor update not being distributed on profile picture deletion ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15461)) +- Fix processing of incoming Delete activities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16084)) +- Fix processing of incoming Block activities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15546)) +- Fix processing of incoming Update activities of unknown accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15514)) +- Fix URIs of repeat follow requests not being recorded ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15662)) +- Fix error on requests with no `Digest` header ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15782)) +- Fix activity object not requiring signature in secure mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15592)) +- Fix database serialization failure returning HTTP 500 ([Gargron](https://github.com/mastodon/mastodon/pull/16101)) +- Fix media processing getting stuck on too much stdin/stderr ([Gargron](https://github.com/mastodon/mastodon/pull/16136)) +- Fix some inefficient array manipulations ([007lva](https://github.com/mastodon/mastodon/pull/15513), [007lva](https://github.com/mastodon/mastodon/pull/15527)) +- Fix some inefficient regex matching ([007lva](https://github.com/mastodon/mastodon/pull/15528)) +- Fix some inefficient SQL queries ([abcang](https://github.com/mastodon/mastodon/pull/16104), [abcang](https://github.com/mastodon/mastodon/pull/16106), [abcang](https://github.com/mastodon/mastodon/pull/16105)) +- Fix trying to fetch key from empty URI when verifying HTTP signature ([Gargron](https://github.com/mastodon/mastodon/pull/16100)) +- Fix `tootctl maintenance fix-duplicates` failures ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15923), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15515)) +- Fix error when removing status caused by race condition ([Gargron](https://github.com/mastodon/mastodon/pull/16099)) +- Fix blocking someone not clearing up list feeds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16205)) +- Fix misspelled URLs character counting ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15382)) +- Fix Sidekiq hanging forever due to a Resolv bug in Ruby 2.7.3 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16157)) +- Fix edge case where follow limit interferes with accepting a follow ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16098)) +- Fix inconsistent lead text style in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/16052), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16086)) +- Fix reports of already suspended accounts being recorded ([Gargron](https://github.com/mastodon/mastodon/pull/16047)) +- Fix sign-up restrictions based on IP addresses not being enforced ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15607)) +- Fix YouTube embeds failing due to YouTube serving wrong OEmbed URLs ([Gargron](https://github.com/mastodon/mastodon/pull/15716)) +- Fix error when rendering public pages with media without meta ([Gargron](https://github.com/mastodon/mastodon/pull/16112)) +- Fix misaligned logo on follow button on public pages ([noellabo](https://github.com/mastodon/mastodon/pull/15458)) +- Fix video modal not working on public pages ([noellabo](https://github.com/mastodon/mastodon/pull/15469)) +- Fix race conditions on account migration creation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15597)) +- Fix not being able to change world filter expiration back to “Never†([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15858)) +- Fix `.env.vagrant` not setting `RAILS_ENV` variable ([chandrn7](https://github.com/mastodon/mastodon/pull/15709)) +- Fix error when muting users with `duration` in REST API ([Tak](https://github.com/mastodon/mastodon/pull/15516)) +- Fix border padding on front page in light theme ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15926)) +- Fix wrong URL to custom CSS when `CDN_HOST` is used ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15927)) +- Fix `tootctl accounts unfollow` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15639)) +- Fix `tootctl emoji import` wasting time on MacOS shadow files ([cortices](https://github.com/mastodon/mastodon/pull/15430)) +- Fix `tootctl emoji import` not treating shortcodes as case-insensitive ([angristan](https://github.com/mastodon/mastodon/pull/15738)) +- Fix some issues with SAML account creation ([Gargron](https://github.com/mastodon/mastodon/pull/15222), [kaiyou](https://github.com/mastodon/mastodon/pull/15511)) +- Fix MX validation applying for explicitly allowed e-mail domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15930)) +- Fix share page not using configured custom mascot ([tribela](https://github.com/mastodon/mastodon/pull/15687)) +- Fix instance actor not being automatically created if it wasn't seeded properly ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15693)) +- Fix HTTPS enforcement preventing Mastodon from being run as an Onion service ([cohosh](https://github.com/mastodon/mastodon/pull/15560), [jtracey](https://github.com/mastodon/mastodon/pull/15741), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15712), [cohosh](https://github.com/mastodon/mastodon/pull/15725)) +- Fix app name, website and redirect URIs not having a maximum length ([Gargron](https://github.com/mastodon/mastodon/pull/16042)) + +## [3.3.0] - 2020-12-27 + +### Added + +- **Add hotkeys for audio/video control in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/15158), [Gargron](https://github.com/mastodon/mastodon/pull/15198)) + - `Space` and `k` to toggle playback + - `m` to toggle mute + - `f` to toggle fullscreen + - `j` and `l` to go back and forward by 10 seconds + - `.` and `,` to go back and forward by a frame (video only) +- Add expand/compress button on media modal in web UI ([mashirozx](https://github.com/mastodon/mastodon/pull/15068), [mashirozx](https://github.com/mastodon/mastodon/pull/15088), [mashirozx](https://github.com/mastodon/mastodon/pull/15094)) +- Add border around 🕺 emoji in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14769)) +- Add border around 🞠emoji in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14712)) +- Add home link to the getting started column when home isn't mounted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14707)) +- Add option to disable swiping motions across the web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13885)) +- **Add pop-out player for audio/video in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/14870), [Gargron](https://github.com/mastodon/mastodon/pull/15157), [Gargron](https://github.com/mastodon/mastodon/pull/14915), [noellabo](https://github.com/mastodon/mastodon/pull/15309)) + - Continue watching/listening when you scroll away + - Action bar to interact with/open toot from the pop-out player +- Add unread notification markers in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14818), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14960), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14954), [noellabo](https://github.com/mastodon/mastodon/pull/14897), [noellabo](https://github.com/mastodon/mastodon/pull/14907)) +- Add paragraph about browser add-ons when encountering errors in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14801)) +- Add import and export for bookmarks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14956)) +- Add cache buster feature for media files ([Gargron](https://github.com/mastodon/mastodon/pull/15155)) + - If you have a proxy cache in front of object storage, deleted files will persist until the cache expires + - If enabled, cache buster will make a special request to the proxy to signal a cache reset +- Add duration option to the mute function ([aquarla](https://github.com/mastodon/mastodon/pull/13831)) +- Add replies policy option to the list function ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9205), [trwnh](https://github.com/mastodon/mastodon/pull/15304)) +- Add `og:published_time` OpenGraph tags on toots ([nornagon](https://github.com/mastodon/mastodon/pull/14865)) +- **Add option to be notified when a followed user posts** ([Gargron](https://github.com/mastodon/mastodon/pull/13546), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14896), [Gargron](https://github.com/mastodon/mastodon/pull/14822)) + - If you don't want to miss a toot, click the bell button! +- Add client-side validation in password change forms ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14564)) +- Add client-side validation in the registration form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14560), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14599)) +- Add support for Gemini URLs ([joshleeb](https://github.com/mastodon/mastodon/pull/15013)) +- Add app shortcuts to web app manifest ([mkljczk](https://github.com/mastodon/mastodon/pull/15234)) +- Add WebAuthn as an alternative 2FA method ([santiagorodriguez96](https://github.com/mastodon/mastodon/pull/14466), [jiikko](https://github.com/mastodon/mastodon/pull/14806)) +- Add honeypot fields and minimum fill-out time for sign-up form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15276)) +- Add icon for mutual relationships in relationship manager ([noellabo](https://github.com/mastodon/mastodon/pull/15149)) +- Add follow selected followers button in relationship manager ([noellabo](https://github.com/mastodon/mastodon/pull/15148)) +- **Add subresource integrity for JS and CSS assets** ([Gargron](https://github.com/mastodon/mastodon/pull/15096)) + - If you use a CDN for static assets (JavaScript, CSS, and so on), you have to trust that the CDN does not modify the assets maliciously + - Subresource integrity compares server-generated asset digests with what's actually served from the CDN and prevents such attacks +- Add `ku`, `sa`, `sc`, `zgh` to available locales ([ykzts](https://github.com/mastodon/mastodon/pull/15138)) +- Add ability to force an account to mark media as sensitive ([noellabo](https://github.com/mastodon/mastodon/pull/14361)) +- **Add ability to block access or limit sign-ups from chosen IPs** ([Gargron](https://github.com/mastodon/mastodon/pull/14963), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15263)) + - Add rules for IPs or CIDR ranges that automatically expire after a configurable amount of time + - Choose the severity of the rule, either blocking all access or merely limiting sign-ups +- **Add support for reversible suspensions through ActivityPub** ([Gargron](https://github.com/mastodon/mastodon/pull/14989)) + - Servers can signal that one of their accounts has been suspended + - During suspension, the account can only delete its own content + - A reversal of the suspension can be signalled the same way + - A local suspension always overrides a remote one +- Add indication to admin UI of whether a report has been forwarded ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13237)) +- Add display of reasons for joining of an account in admin UI ([mashirozx](https://github.com/mastodon/mastodon/pull/15265)) +- Add option to obfuscate domain name in public list of domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/15355)) +- Add option to make reasons for joining required on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15358), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15385), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15405)) +- Add ActivityPub follower synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14510), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15026)) +- Add outbox attribute to instance actor ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14721)) +- Add featured hashtags as an ActivityPub collection ([Gargron](https://github.com/mastodon/mastodon/pull/11595), [noellabo](https://github.com/mastodon/mastodon/pull/15277)) +- Add support for dereferencing objects through bearcaps ([Gargron](https://github.com/mastodon/mastodon/pull/14683), [noellabo](https://github.com/mastodon/mastodon/pull/14981)) +- Add `S3_READ_TIMEOUT` environment variable ([tateisu](https://github.com/mastodon/mastodon/pull/14952)) +- Add `ALLOWED_PRIVATE_ADDRESSES` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14722)) +- Add `--fix-permissions` option to `tootctl media remove-orphans` ([Gargron](https://github.com/mastodon/mastodon/pull/14383), [uist1idrju3i](https://github.com/mastodon/mastodon/pull/14715)) +- Add `tootctl accounts merge` ([Gargron](https://github.com/mastodon/mastodon/pull/15201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15264), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15256)) + - Has someone changed their domain or subdomain thereby creating two accounts where there should be one? + - This command will fix it on your end +- Add `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14860), [Gargron](https://github.com/mastodon/mastodon/pull/15223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15373)) + - Index corruption in the database? + - This command is for you +- **Add support for managing multiple stream subscriptions in a single connection** ([Gargron](https://github.com/mastodon/mastodon/pull/14524), [Gargron](https://github.com/mastodon/mastodon/pull/14566), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14859), [zunda](https://github.com/mastodon/mastodon/pull/14608)) + - Previously, getting live updates for multiple timelines required opening a HTTP or WebSocket connection for each + - More connections means more resource consumption on both ends, not to mention the (ever so slight) delay when establishing a new connection + - Now, with just a single WebSocket connection you can subscribe and unsubscribe to and from multiple streams +- Add support for limiting results by both `min_id` and `max_id` at the same time in REST API ([tateisu](https://github.com/mastodon/mastodon/pull/14776)) +- Add `GET /api/v1/accounts/:id/featured_tags` to REST API ([noellabo](https://github.com/mastodon/mastodon/pull/11817), [noellabo](https://github.com/mastodon/mastodon/pull/15270)) +- Add stoplight for object storage failures, return HTTP 503 in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/13043)) +- Add optional `tootctl remove media` cronjob in Helm chart ([dunn](https://github.com/mastodon/mastodon/pull/14396)) +- Add clean error message when `RAILS_ENV` is unset ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15381)) + +### Changed + +- **Change media modals look in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/15217), [Gargron](https://github.com/mastodon/mastodon/pull/15221), [Gargron](https://github.com/mastodon/mastodon/pull/15284), [Gargron](https://github.com/mastodon/mastodon/pull/15283), [Kjwon15](https://github.com/mastodon/mastodon/pull/15308), [noellabo](https://github.com/mastodon/mastodon/pull/15305), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15417)) + - Background of the overlay matches the color of the image + - Action bar to interact with or open the toot from the modal +- Change order of announcements in admin UI to be newest-first ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15091)) +- **Change account suspensions to be reversible by default** ([Gargron](https://github.com/mastodon/mastodon/pull/14726), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15152), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15106), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15099), [noellabo](https://github.com/mastodon/mastodon/pull/14855), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15380), [Gargron](https://github.com/mastodon/mastodon/pull/15420), [Gargron](https://github.com/mastodon/mastodon/pull/15414)) + - Suspensions no longer equal deletions + - A suspended account can be unsuspended with minimal consequences for 30 days + - Immediate deletion of data is still available as an explicit option + - Suspended accounts can request an archive of their data through the UI +- Change REST API to return empty data for suspended accounts (14765) +- Change web UI to show empty profile for suspended accounts ([Gargron](https://github.com/mastodon/mastodon/pull/14766), [Gargron](https://github.com/mastodon/mastodon/pull/15345)) +- Change featured hashtag suggestions to be recently used instead of most used ([abcang](https://github.com/mastodon/mastodon/pull/14760)) +- Change direct toots to appear in the home feed again ([Gargron](https://github.com/mastodon/mastodon/pull/14711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15182), [noellabo](https://github.com/mastodon/mastodon/pull/14727)) + - Return to treating all toots the same instead of trying to retrofit direct visibility into an instant messaging model +- Change email address validation to return more specific errors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14565)) +- Change HTTP signature requirements to include `Digest` header on `POST` requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15069)) +- Change click area of video/audio player buttons to be bigger in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/15049)) +- Change order of filters by alphabetic by "keyword or phrase" ([ariasuni](https://github.com/mastodon/mastodon/pull/15050)) +- Change suspension of remote accounts to also undo outgoing follows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15188)) +- Change string "Home" to "Home and lists" in the filter creation screen ([ariasuni](https://github.com/mastodon/mastodon/pull/15139)) +- Change string "Boost to original audience" to "Boost with original visibility" in web UI ([3n-k1](https://github.com/mastodon/mastodon/pull/14598)) +- Change string "Show more" to "Show newer" and "Show older" on public pages ([ariasuni](https://github.com/mastodon/mastodon/pull/15052)) +- Change order of announcements to be reverse chronological in web UI ([dariusk](https://github.com/mastodon/mastodon/pull/15065), [dariusk](https://github.com/mastodon/mastodon/pull/15070)) +- Change RTL detection to rely on unicode-bidi paragraph by paragraph in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14573)) +- Change visibility icon next to timestamp to be clickable in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/15053), [mayaeh](https://github.com/mastodon/mastodon/pull/15055)) +- Change public thread view to hide "Show thread" link ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15266)) +- Change number format on about page from full to shortened ([Gargron](https://github.com/mastodon/mastodon/pull/15327)) +- Change how scheduled tasks run in multi-process environments ([noellabo](https://github.com/mastodon/mastodon/pull/15314)) + - New dedicated queue `scheduler` + - Runs by default when Sidekiq is executed with no options + - Has to be added manually in a multi-process environment + +### Removed + +- Remove fade-in animation from modals in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/15199)) +- Remove auto-redirect to direct messages in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/15142)) +- Remove obsolete IndexedDB operations from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14730)) +- Remove dependency on unused and unmaintained http_parser.rb gem ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14574)) + +### Fixed + +- Fix layout on about page when contact account has a long username ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15357)) +- Fix follow limit preventing re-following of a moved account ([Gargron](https://github.com/mastodon/mastodon/pull/14207), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15384)) +- **Fix deletes not reaching every server that interacted with toot** ([Gargron](https://github.com/mastodon/mastodon/pull/15200)) + - Previously, delete of a toot would be primarily sent to the followers of its author, people mentioned in the toot, and people who reblogged the toot + - Now, additionally, it is ensured that it is sent to people who replied to it, favourited it, and to the person it replies to even if that person is not mentioned +- Fix resolving an account through its non-canonical form (i.e. alternate domain) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15187)) +- Fix sending redundant ActivityPub events when processing remote account deletion ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15104)) +- Fix Move handler not being triggered when failing to fetch target account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15107)) +- Fix downloading remote media files when server returns empty filename ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14867)) +- Fix account processing failing because of large collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15027)) +- Fix not being able to unfavorite toots one has lost access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15192)) +- Fix not being able to unbookmark toots one has lost access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14604)) +- Fix possible casing inconsistencies in hashtag search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14906)) +- Fix updating account counters when association is not yet created ([Gargron](https://github.com/mastodon/mastodon/pull/15108)) +- Fix cookies not having a SameSite attribute ([Gargron](https://github.com/mastodon/mastodon/pull/15098)) +- Fix poll ending notifications being created for each vote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15071)) +- Fix multiple boosts of a same toot erroneously appearing in TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14759)) +- Fix asset builds not picking up `CDN_HOST` change ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14381)) +- Fix desktop notifications permission prompt in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14985), [Gargron](https://github.com/mastodon/mastodon/pull/15141), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13543), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15176)) + - Some time ago, browsers added a requirement that desktop notification prompts could only be displayed in response to a user-generated event (such as a click) + - This means that for some time, users who haven't already given the permission before were not getting a prompt and as such were not receiving desktop notifications +- Fix "Mark media as sensitive" string not supporting pluralizations in other languages in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/15051)) +- Fix glitched image uploads when canvas read access is blocked in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15180)) +- Fix some account gallery items having empty labels in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15073)) +- Fix alt-key hotkeys activating while typing in a text field in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14942)) +- Fix wrong seek bar width on media player in web UI ([mfmfuyu](https://github.com/mastodon/mastodon/pull/15060)) +- Fix logging out on mobile in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14901)) +- Fix wrong click area for GIFVs in media modal in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14615)) +- Fix unreadable placeholder text color in high contrast theme in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14803)) +- Fix scrolling issues when closing some dropdown menus in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14606)) +- Fix notification filter bar incorrectly filtering gaps in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14808)) +- Fix disabled boost icon being replaced by private boost icon on hover in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14456)) +- Fix hashtag detection in compose form being different to server-side in web UI ([kedamaDQ](https://github.com/mastodon/mastodon/pull/14484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14513)) +- Fix home last read marker mishandling gaps in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14809)) +- Fix unnecessary re-rendering of various components when typing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/15286)) +- Fix notifications being unnecessarily re-rendered in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15312)) +- Fix column swiping animation logic in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15301)) +- Fix inefficiency when fetching hashtag timeline ([noellabo](https://github.com/mastodon/mastodon/pull/14861), [akihikodaki](https://github.com/mastodon/mastodon/pull/14662)) +- Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/mastodon/mastodon/pull/14674)) +- Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/mastodon/mastodon/pull/14673)) +- Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/mastodon/mastodon/pull/14675)) +- Fix inefficiency when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421)) +- Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/mastodon/mastodon/pull/14534)) +- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287)) +- Fix performance on instances list in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/15282)) +- Fix server actor appearing in list of accounts in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14567)) +- Fix "bootstrap timeline accounts" toggle in site settings in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15325)) +- Fix PostgreSQL secret name for cronjob in Helm chart ([metal3d](https://github.com/mastodon/mastodon/pull/15072)) +- Fix Procfile not being compatible with herokuish ([acuteaura](https://github.com/mastodon/mastodon/pull/12685)) +- Fix installation of tini being split into multiple steps in Dockerfile ([ryncsn](https://github.com/mastodon/mastodon/pull/14686)) + +### Security + +- Fix streaming API allowing connections to persist after access token invalidation ([Gargron](https://github.com/mastodon/mastodon/pull/15111)) +- Fix 2FA/sign-in token sessions being valid after password change ([Gargron](https://github.com/mastodon/mastodon/pull/14802)) +- Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15364)) + +## [3.2.2] - 2020-12-19 + +### Added + +- Add `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14860), [Gargron](https://github.com/mastodon/mastodon/pull/15223)) + - Index corruption in the database? + - This command is for you + +### Removed + +- Remove dependency on unused and unmaintained http_parser.rb gem ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14574)) + +### Fixed + +- Fix Move handler not being triggered when failing to fetch target account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15107)) +- Fix downloading remote media files when server returns empty filename ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14867)) +- Fix possible casing inconsistencies in hashtag search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14906)) +- Fix updating account counters when association is not yet created ([Gargron](https://github.com/mastodon/mastodon/pull/15108)) +- Fix account processing failing because of large collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15027)) +- Fix resolving an account through its non-canonical form (i.e. alternate domain) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15187)) +- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287)) + +### Security + +- Fix 2FA/sign-in token sessions being valid after password change ([Gargron](https://github.com/mastodon/mastodon/pull/14802)) +- Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15364)) + +## [3.2.1] - 2020-10-19 + +### Added + +- Add support for latest HTTP Signatures spec draft ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14556)) +- Add support for inlined objects in ActivityPub `to`/`cc` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14514)) + +### Changed + +- Change actors to not be served at all without authentication in limited federation mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14800)) + - Previously, a bare version of an actor was served when not authenticated, i.e. username and public key + - Because all actor fetch requests are signed using a separate system actor, that is no longer required + +### Fixed + +- Fix `tootctl media` commands not recognizing very large IDs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14536)) +- Fix crash when failing to load emoji picker in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14525)) +- Fix contrast requirements in thumbnail color extraction ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14464)) +- Fix audio/video player not using `CDN_HOST` on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14486)) +- Fix private boost icon not being used on public pages ([OmmyZhang](https://github.com/mastodon/mastodon/pull/14471)) +- Fix audio player on Safari in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14465)) +- Fix dereferencing remote statuses not using the correct account for signature when receiving a targeted inbox delivery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14656)) +- Fix nil error in `tootctl media remove` ([noellabo](https://github.com/mastodon/mastodon/pull/14657)) +- Fix videos with near-60 fps being rejected ([Gargron](https://github.com/mastodon/mastodon/pull/14684)) +- Fix reported statuses not being included in warning e-mail ([Gargron](https://github.com/mastodon/mastodon/pull/14778)) +- Fix `Reject` activities of `Follow` objects not correctly destroying a follow relationship ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14479)) +- Fix inefficiencies in fan-out-on-write service ([Gargron](https://github.com/mastodon/mastodon/pull/14682), [noellabo](https://github.com/mastodon/mastodon/pull/14709)) +- Fix timeout errors when trying to webfinger some IPv6 configurations ([Gargron](https://github.com/mastodon/mastodon/pull/14919)) +- Fix files served as `application/octet-stream` being rejected without attempting mime type detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14452)) + +## [3.2.0] - 2020-07-27 + +### Added + +- Add `SMTP_SSL` environment variable ([OmmyZhang](https://github.com/mastodon/mastodon/pull/14309)) +- Add hotkey for toggling content warning input in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13987)) +- **Add e-mail-based sign in challenge for users with disabled 2FA** ([Gargron](https://github.com/mastodon/mastodon/pull/14013)) + - If user tries signing in after: + - Being inactive for a while + - With a previously unknown IP + - Without 2FA being enabled + - Require to enter a token sent via e-mail before sigining in +- Add `limit` param to RSS feeds ([noellabo](https://github.com/mastodon/mastodon/pull/13743)) +- Add `visibility` param to share page ([noellabo](https://github.com/mastodon/mastodon/pull/13023)) +- Add blurhash to link previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13984), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14143), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14278), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14126), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14260)) + - In web UI, toots cannot be marked as sensitive unless there is media attached + - However, it's possible to do via API or ActivityPub + - Thumbnails of link previews of such posts now use blurhash in web UI + - The Card entity in REST API has a new `blurhash` attribute +- Add support for `summary` field for media description in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13763)) +- Add hints about incomplete remote content to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14031), [noellabo](https://github.com/mastodon/mastodon/pull/14195)) +- **Add personal notes for accounts** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14148), [Gargron](https://github.com/mastodon/mastodon/pull/14208), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14251)) + - To clarify, these are notes only you can see, to help you remember details + - Notes can be viewed and edited from profiles in web UI + - New REST API: `POST /api/v1/accounts/:id/note` with `comment` param + - The Relationship entity in REST API has a new `note` attribute +- Add Helm chart ([dunn](https://github.com/mastodon/mastodon/pull/14090), [dunn](https://github.com/mastodon/mastodon/pull/14256), [dunn](https://github.com/mastodon/mastodon/pull/14245)) +- **Add customizable thumbnails for audio and video attachments** ([Gargron](https://github.com/mastodon/mastodon/pull/14145), [Gargron](https://github.com/mastodon/mastodon/pull/14244), [Gargron](https://github.com/mastodon/mastodon/pull/14273), [Gargron](https://github.com/mastodon/mastodon/pull/14203), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14255), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14306), [noellabo](https://github.com/mastodon/mastodon/pull/14358), [noellabo](https://github.com/mastodon/mastodon/pull/14357)) + - Metadata (album, artist, etc) is no longer stripped from audio files + - Album art is automatically extracted from audio files + - Thumbnail can be manually uploaded for both audio and video attachments + - Media upload APIs now support `thumbnail` param + - On `POST /api/v1/media` and `POST /api/v2/media` + - And on `PUT /api/v1/media/:id` + - ActivityPub representation of media attachments represents custom thumbnails with an `icon` attribute + - The Media Attachment entity in REST API now has a `preview_remote_url` to its `preview_url`, equivalent to `remote_url` to its `url` +- **Add color extraction for thumbnails** ([Gargron](https://github.com/mastodon/mastodon/pull/14209), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14264)) + - The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent` + - The background color is chosen from the most dominant color around the edges of the thumbnail + - The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm + - The most saturated color of the two is designated as the accent color + - The one with the highest W3C contrast is designated as the foreground color + - If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern +- Add a visibility indicator to toots in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14123), [highemerly](https://github.com/mastodon/mastodon/pull/14292)) +- Add `tootctl email_domain_blocks` ([tateisu](https://github.com/mastodon/mastodon/pull/13589), [Gargron](https://github.com/mastodon/mastodon/pull/14147)) +- Add "Add new domain block" to header of federation page in admin UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13934)) +- Add ability to keep emoji picker open with ctrl+click in web UI ([bclindner](https://github.com/mastodon/mastodon/pull/13896), [noellabo](https://github.com/mastodon/mastodon/pull/14096)) +- Add custom icon for private boosts in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14380)) +- Add support for Create and Update activities that don't inline objects in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14359)) +- Add support for Undo activities that don't inline activities in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14346)) + +### Changed + +- Change `.env.production.sample` to be leaner and cleaner ([Gargron](https://github.com/mastodon/mastodon/pull/14206)) + - It was overloaded as de-facto documentation and getting quite crowded + - Defer to the actual documentation while still giving a minimal example +- Change `tootctl search deploy` to work faster and display progress ([Gargron](https://github.com/mastodon/mastodon/pull/14300)) +- Change User-Agent of link preview fetching service to include "Bot" ([Gargron](https://github.com/mastodon/mastodon/pull/14248)) + - Some websites may not render OpenGraph tags into HTML if that's not the case +- Change behaviour to carry blocks over when someone migrates their followers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14144)) +- Change volume control and download buttons in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14122)) +- **Change design of audio players in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/14095), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14281), [Gargron](https://github.com/mastodon/mastodon/pull/14282), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14118), [Gargron](https://github.com/mastodon/mastodon/pull/14199), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14338)) +- Change reply filter to never filter own toots in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14128)) +- Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14132), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14373)) +- Change contrast of flash messages ([cchoi12](https://github.com/mastodon/mastodon/pull/13892)) +- Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13834)) +- Change appearance of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938)) +- Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13954)) +- Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/mastodon/mastodon/pull/13773), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13772), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14020), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14015)) +- Change structure of unavailable content section on about page ([ariasuni](https://github.com/mastodon/mastodon/pull/13930)) +- Change behaviour to accept ActivityPub activities relayed through group actor ([noellabo](https://github.com/mastodon/mastodon/pull/14279)) +- Change amount of processing retries for ActivityPub activities ([noellabo](https://github.com/mastodon/mastodon/pull/14355)) + +### Removed + +- Remove the terms "blacklist" and "whitelist" from UX ([Gargron](https://github.com/mastodon/mastodon/pull/14149), [mayaeh](https://github.com/mastodon/mastodon/pull/14192)) + - Environment variables changed (old versions continue to work): + - `WHITELIST_MODE` → `LIMITED_FEDERATION_MODE` + - `EMAIL_DOMAIN_BLACKLIST` → `EMAIL_DOMAIN_DENYLIST` + - `EMAIL_DOMAIN_WHITELIST` → `EMAIL_DOMAIN_ALLOWLIST` + - CLI option changed: + - `tootctl domains purge --whitelist-mode` → `tootctl domains purge --limited-federation-mode` +- Remove some unnecessary database indexes ([lfuelling](https://github.com/mastodon/mastodon/pull/13695), [noellabo](https://github.com/mastodon/mastodon/pull/14259)) +- Remove unnecessary Node.js version upper bound ([ykzts](https://github.com/mastodon/mastodon/pull/14139)) + +### Fixed + +- Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/mastodon/mastodon/pull/14394)) +- Fix sometimes occurring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378)) +- Fix RSS feeds not being cacheable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368)) +- Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/mastodon/mastodon/pull/14365)) +- Fix boosted toots from blocked account not being retroactively removed from TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14339)) +- Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14061)) +- Fix streaming server trying to use empty password to connect to Redis when `REDIS_PASSWORD` is given but blank ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14135)) +- Fix being unable to unboost posts when blocked by their author ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14308)) +- Fix account domain block not properly unfollowing accounts from domain ([Gargron](https://github.com/mastodon/mastodon/pull/14304)) +- Fix removing a domain allow wiping known accounts in open federation mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14298)) +- Fix blocks and mutes pagination in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14275)) +- Fix new posts pushing down origin of opened dropdown in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14348)) +- Fix timeline markers not being saved sometimes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13887), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13889), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14155)) +- Fix CSV uploads being rejected ([noellabo](https://github.com/mastodon/mastodon/pull/13835)) +- Fix incompatibility with Elasticsearch 7.x ([noellabo](https://github.com/mastodon/mastodon/pull/13828)) +- Fix being able to search posts where you're in the target audience but not actively mentioned ([noellabo](https://github.com/mastodon/mastodon/pull/13829)) +- Fix non-local posts appearing on local-only hashtag timelines in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/13827)) +- Fix `tootctl media remove-orphans` choking on unknown files in storage ([Gargron](https://github.com/mastodon/mastodon/pull/13765)) +- Fix `tootctl upgrade storage-schema` misbehaving ([Gargron](https://github.com/mastodon/mastodon/pull/13761), [angristan](https://github.com/mastodon/mastodon/pull/13768)) + - Fix it marking records as upgraded even though no files were moved + - Fix it not working with S3 storage + - Fix it not working with custom emojis +- Fix GIF reader raising incorrect exceptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13760)) +- Fix hashtag search performing account search as well ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13758)) +- Fix Webfinger returning wrong status code on malformed or missing param ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13759)) +- Fix `rake mastodon:setup` error when some environment variables are set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13928)) +- Fix admin page crashing when trying to block an invalid domain name in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13884)) +- Fix unsent toot confirmation dialog not popping up in single column mode in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13888)) +- Fix performance of follow import ([noellabo](https://github.com/mastodon/mastodon/pull/13836)) + - Reduce timeout of Webfinger requests to that of other requests + - Use circuit breakers to stop hitting unresponsive servers + - Avoid hitting servers that are already known to be generally unavailable +- Fix filters ignoring media descriptions ([BenLubar](https://github.com/mastodon/mastodon/pull/13837)) +- Fix some actions on custom emojis leading to cryptic errors in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13951)) +- Fix ActivityPub serialization of replies when some of them are URIs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13957)) +- Fix `rake mastodon:setup` choking on environment variables containing `%` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13940)) +- Fix account redirect confirmation message talking about moved followers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13950)) +- Fix avatars having the wrong size on public detailed status pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14140)) +- Fix various issues around OpenGraph representation of media ([Gargron](https://github.com/mastodon/mastodon/pull/14133)) + - Pages containing audio no longer say "Attached: 1 image" in description + - Audio attachments now represented as OpenGraph `og:audio` + - The `twitter:player` page now uses Mastodon's proper audio/video player + - Audio/video buffered bars now display correctly in audio/video player + - Volume and progress bars now respond to movement/move smoother +- Fix audio/video/images/cards not reacting to window resizes in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14130)) +- Fix very wide media attachments resulting in too thin a thumbnail in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14127)) +- Fix crash when merging posts into home feed after following someone ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14129)) +- Fix unique username constraint for local users not being enforced in database ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14099)) +- Fix unnecessary gap under video modal in web UI ([mfmfuyu](https://github.com/mastodon/mastodon/pull/14098)) +- Fix 2FA and sign in token pages not respecting user locale ([mfmfuyu](https://github.com/mastodon/mastodon/pull/14087)) +- Fix unapproved users being able to view profiles when in limited-federation mode _and_ requiring approval for sign-ups ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14093)) +- Fix initial audio volume not corresponding to what's displayed in audio player in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14057)) +- Fix timelines sometimes jumping when closing modals in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14019)) +- Fix memory usage of downloading remote files ([Gargron](https://github.com/mastodon/mastodon/pull/14184), [Gargron](https://github.com/mastodon/mastodon/pull/14181), [noellabo](https://github.com/mastodon/mastodon/pull/14356)) + - Don't read entire file (up to 40 MB) into memory + - Read and write it to temp file in small chunks +- Fix inconsistent account header padding in web UI ([trwnh](https://github.com/mastodon/mastodon/pull/14179)) +- Fix Thai being skipped from language detection ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/13989)) + - Since Thai has its own alphabet, it can be detected more reliably +- Fix broken hashtag column options styling in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14247)) +- Fix pointer cursor being shown on toots that are not clickable in web UI ([arielrodrigues](https://github.com/mastodon/mastodon/pull/14185)) +- Fix lock icon not being shown when locking account in profile settings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14190)) +- Fix domain blocks doing work the wrong way around ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13424)) + - Instead of suspending accounts one by one, mark all as suspended first (quick) + - Only then proceed to start removing their data (slow) + - Clear out media attachments in a separate worker (slow) + +## [3.1.5] - 2020-07-07 + +### Security + +- Fix media attachment enumeration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14254)) +- Change rate limits for various paths ([Gargron](https://github.com/mastodon/mastodon/pull/14253)) +- Fix other sessions not being logged out on password change ([Gargron](https://github.com/mastodon/mastodon/pull/14252)) + +## [3.1.4] - 2020-05-14 + +### Added + +- Add `vi` to available locales ([taicv](https://github.com/mastodon/mastodon/pull/13542)) +- Add ability to remove identity proofs from account ([Gargron](https://github.com/mastodon/mastodon/pull/13682)) +- Add ability to exclude local content from federated timeline ([noellabo](https://github.com/mastodon/mastodon/pull/13504), [noellabo](https://github.com/mastodon/mastodon/pull/13745)) + - Add `remote` param to `GET /api/v1/timelines/public` REST API + - Add `public/remote` / `public:remote` variants to streaming API + - "Remote only" option in federated timeline column settings in web UI +- Add ability to exclude remote content from hashtag timelines in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/13502)) + - No changes to REST API + - "Local only" option in hashtag column settings in web UI +- Add Capistrano tasks that reload the services after deploying ([berkes](https://github.com/mastodon/mastodon/pull/12642)) +- Add `invites_enabled` attribute to `GET /api/v1/instance` in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13501)) +- Add `tootctl emoji export` command ([lfuelling](https://github.com/mastodon/mastodon/pull/13534)) +- Add separate cache directory for non-local uploads ([Gargron](https://github.com/mastodon/mastodon/pull/12821), [Hanage999](https://github.com/mastodon/mastodon/pull/13593), [mayaeh](https://github.com/mastodon/mastodon/pull/13551)) + - Add `tootctl upgrade storage-schema` command to move old non-local uploads to the cache directory +- Add buttons to delete header and avatar from profile settings ([sternenseemann](https://github.com/mastodon/mastodon/pull/13234)) +- Add emoji graphics and shortcodes from Twemoji 12.1.5 ([DeeUnderscore](https://github.com/mastodon/mastodon/pull/13021)) + +### Changed + +- Change error message when trying to migrate to an account that does not have current account set as an alias to be more clear ([TheEvilSkeleton](https://github.com/mastodon/mastodon/pull/13746)) +- Change delivery failure tracking to work with hostnames instead of URLs ([Gargron](https://github.com/mastodon/mastodon/pull/13437), [noellabo](https://github.com/mastodon/mastodon/pull/13481), [noellabo](https://github.com/mastodon/mastodon/pull/13482), [noellabo](https://github.com/mastodon/mastodon/pull/13535)) +- Change Content-Security-Policy to not need unsafe-inline style-src ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13679), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13576), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13575), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13438)) +- Change how RSS items are titled and formatted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13592), [ykzts](https://github.com/mastodon/mastodon/pull/13591)) + +### Fixed + +- Fix dropdown of muted and followed accounts offering option to hide boosts in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13748)) +- Fix "You are already signed in" alert being shown at wrong times ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13547)) +- Fix retrying of failed-to-download media files not actually working ([noellabo](https://github.com/mastodon/mastodon/pull/13741)) +- Fix first poll option not being focused when adding a poll in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13740)) +- Fix `sr` locale being selected over `sr-Latn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13693)) +- Fix error within error when limiting backtrace to 3 lines ([Gargron](https://github.com/mastodon/mastodon/pull/13120)) +- Fix `tootctl media remove-orphans` crashing on "Import" files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13685)) +- Fix regression in `tootctl media remove-orphans` ([Gargron](https://github.com/mastodon/mastodon/pull/13405)) +- Fix old unique jobs digests not having been cleaned up ([Gargron](https://github.com/mastodon/mastodon/pull/13683)) +- Fix own following/followers not showing muted users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13614)) +- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/mastodon/mastodon/pull/13676)) +- Fix wrong pgHero Content-Security-Policy when `CDN_HOST` is set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13595)) +- Fix needlessly deduplicating usernames on collisions with remote accounts when signing-up through SAML/CAS ([kaiyou](https://github.com/mastodon/mastodon/pull/13581)) +- Fix page incorrectly scrolling when bringing up dropdown menus in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13574)) +- Fix messed up z-index when NoScript blocks media/previews in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13449)) +- Fix "See what's happening" page showing public instead of local timeline for logged-in users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13499)) +- Fix not being able to resolve public resources in development environment ([Gargron](https://github.com/mastodon/mastodon/pull/13505)) +- Fix uninformative error message when uploading unsupported image files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13540)) +- Fix expanded video player issues in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13541), [eai04191](https://github.com/mastodon/mastodon/pull/13533)) +- Fix and refactor keyboard navigation in dropdown menus in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13528)) +- Fix uploaded image orientation being messed up in some browsers in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13493)) +- Fix actions log crash when displaying updates of deleted announcements in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13489)) +- Fix search not working due to proxy settings when using hidden services ([Gargron](https://github.com/mastodon/mastodon/pull/13488)) +- Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/mastodon/mastodon/pull/13485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13490)) +- Fix confusing error when failing to add an alias to an unknown account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13480)) +- Fix "Email changed" notification sometimes having wrong e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13475)) +- Fix various issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452)) +- Fix API footer link in web UI ([bubblineyuri](https://github.com/mastodon/mastodon/pull/13441)) +- Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13445)) +- Fix styling of polls in JS-less fallback on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13436)) +- Fix trying to delete already deleted file when post-processing ([Gargron](https://github.com/mastodon/mastodon/pull/13406)) + +### Security + +- Fix Doorkeeper vulnerability that exposed app secret to users who authorized the app and reset secret of the web UI that could have been exposed ([dependabot-preview[bot]](https://github.com/mastodon/mastodon/pull/13613), [Gargron](https://github.com/mastodon/mastodon/pull/13688)) + - For apps that self-register on behalf of every individual user (such as most mobile apps), this is a non-issue + - The issue only affects developers of apps who are shared between multiple users, such as server-side apps like cross-posters + +## [3.1.3] - 2020-04-05 + +### Added + +- Add ability to filter audit log in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/13381)) +- Add titles to warning presets in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/13252)) +- Add option to include resolved DNS records when blacklisting e-mail domains in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/13254)) +- Add ability to delete files uploaded for settings in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13192)) +- Add sorting by username, creation and last activity in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13076)) +- Add explanation as to why unlocked accounts may have follow requests in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13385)) +- Add link to bookmarks to dropdown in web UI ([mayaeh](https://github.com/mastodon/mastodon/pull/13273)) +- Add support for links to statuses in announcements to be opened in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13212), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13250)) +- Add tooltips to audio/video player buttons in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13203)) +- Add submit button to the top of preferences pages ([guigeekz](https://github.com/mastodon/mastodon/pull/13068)) +- Add specific rate limits for posting, following and reporting ([Gargron](https://github.com/mastodon/mastodon/pull/13172), [Gargron](https://github.com/mastodon/mastodon/pull/13390)) + - 300 posts every 3 hours + - 400 follows or follow requests every 24 hours + - 400 reports every 24 hours +- Add federation support for the "hide network" preference ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11673)) +- Add `--skip-media-remove` option to `tootctl statuses remove` ([tateisu](https://github.com/mastodon/mastodon/pull/13080)) + +### Changed + +- **Change design of polls in web UI** ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/13257), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13313)) +- Change status click areas in web UI to be bigger ([ariasuni](https://github.com/mastodon/mastodon/pull/13327)) +- **Change `tootctl media remove-orphans` to work for all classes** ([Gargron](https://github.com/mastodon/mastodon/pull/13316)) +- **Change local media attachments to perform heavy processing asynchronously** ([Gargron](https://github.com/mastodon/mastodon/pull/13210)) +- Change video uploads to always be converted to H264/MP4 ([Gargron](https://github.com/mastodon/mastodon/pull/13220), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13239), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13242)) +- Change video uploads to enforce certain limits ([Gargron](https://github.com/mastodon/mastodon/pull/13218)) + - Dimensions smaller than 1920x1200px + - Frame rate at most 60fps +- Change the tooltip "Toggle visibility" to "Hide media" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13199)) +- Change description of privacy levels to be more intuitive in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13197)) +- Change GIF label to be displayed even when autoplay is enabled in web UI ([koyuawsmbrtn](https://github.com/mastodon/mastodon/pull/13209)) +- Change the string "Hide everything from …" to "Block domain …" in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13178), [mayaeh](https://github.com/mastodon/mastodon/pull/13221)) +- Change wording of media display preferences to be more intuitive ([ariasuni](https://github.com/mastodon/mastodon/pull/13198)) + +### Deprecated + +- `POST /api/v1/media` → `POST /api/v2/media` ([Gargron](https://github.com/mastodon/mastodon/pull/13210)) + +### Fixed + +- Fix `tootctl media remove-orphans` ignoring `PAPERCLIP_ROOT_PATH` ([Gargron](https://github.com/mastodon/mastodon/pull/13375)) +- Fix returning results when searching for URL with non-zero offset ([Gargron](https://github.com/mastodon/mastodon/pull/13377)) +- Fix pinning a column in web UI sometimes redirecting out of web UI ([Gargron](https://github.com/mastodon/mastodon/pull/13376)) +- Fix background jobs not using locks like they are supposed to ([Gargron](https://github.com/mastodon/mastodon/pull/13361)) +- Fix content warning being unnecessarily cleared when hiding content warning input in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13348)) +- Fix "Show more" not switching to "Show less" on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13174)) +- Fix import overwrite option not being selectable ([noellabo](https://github.com/mastodon/mastodon/pull/13347)) +- Fix wrong color for ellipsis in boost confirmation dialog in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13355)) +- Fix unnecessary unfollowing when importing follows with overwrite option ([noellabo](https://github.com/mastodon/mastodon/pull/13350)) +- Fix 404 and 410 API errors being silently discarded in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13279)) +- Fix OCR not working on Safari because of unsupported worker-src CSP ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13323)) +- Fix media not being marked sensitive when a content warning is set with no text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13277)) +- Fix crash after deleting announcements in web UI ([codesections](https://github.com/mastodon/mastodon/pull/13283), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13312)) +- Fix bookmarks not being searchable ([Kjwon15](https://github.com/mastodon/mastodon/pull/13271), [noellabo](https://github.com/mastodon/mastodon/pull/13293)) +- Fix reported accounts not being whitelisted from further spam checks when resolving a spam check report ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13289)) +- Fix web UI crash in single-column mode on prehistoric browsers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13267)) +- Fix some timeouts when searching for URLs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13253)) +- Fix detailed view of direct messages displaying a 0 boost count in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13244)) +- Fix regression in “Edit media†modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13243)) +- Fix public posts from silenced accounts not being changed to unlisted visibility ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13096)) +- Fix error when searching for URLs that contain the mention syntax ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13151)) +- Fix text area above/right of emoji picker being accidentally clickable in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13148)) +- Fix too large announcements not being scrollable in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13211)) +- Fix `tootctl media remove-orphans` crashing when encountering invalid media ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13170)) +- Fix installation failing when Redis password contains special characters ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13156)) +- Fix announcements with fully-qualified mentions to local users crashing web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13164)) + +### Security + +- Fix re-sending of e-mail confirmation not being rate limited ([Gargron](https://github.com/mastodon/mastodon/pull/13360)) + +## [v3.1.2] - 2020-02-27 + +### Added + +- Add `--reset-password` option to `tootctl accounts modify` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13126)) +- Add source-mapped stacktrace to error message in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13082)) + +### Fixed + +- Fix dismissing an announcement twice raising an obscure error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13124)) +- Fix misleading error when attempting to re-send a pending follow request ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13133)) +- Fix backups failing when files are missing from media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13146)) +- Fix duplicate accounts being created when fetching an account for its key only ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13147)) +- Fix `/web` redirecting to `/web/web` in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13128)) +- Fix previously OStatus-based accounts not being detected as ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13129)) +- Fix account JSON/RSS not being cacheable due to wrong mime type comparison ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13116)) +- Fix old browsers crashing because of missing `finally` polyfill in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13115)) +- Fix account's bio not being shown if there are no proofs/fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13075)) +- Fix sign-ups without checked user agreement being accepted through the web form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13088)) +- Fix non-x64 architectures not being able to build Docker image because of hardcoded Node.js architecture ([SaraSmiseth](https://github.com/mastodon/mastodon/pull/13081)) +- Fix invite request input not being shown on sign-up error if left empty ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13089)) +- Fix some migration hints mentioning GitLab instead of Mastodon ([saper](https://github.com/mastodon/mastodon/pull/13084)) + +### Security + +- Fix leak of arbitrary statuses through unfavourite action in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/13161)) + +## [3.1.1] - 2020-02-10 + +### Fixed + +- Fix yanked dependency preventing installation ([mayaeh](https://github.com/mastodon/mastodon/pull/13059)) + +## [3.1.0] - 2020-02-09 + +### Added + +- Add bookmarks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/7107), [Gargron](https://github.com/mastodon/mastodon/pull/12494), [Gomasy](https://github.com/mastodon/mastodon/pull/12381)) +- Add announcements ([Gargron](https://github.com/mastodon/mastodon/pull/12662), [Gargron](https://github.com/mastodon/mastodon/pull/12967), [Gargron](https://github.com/mastodon/mastodon/pull/12970), [Gargron](https://github.com/mastodon/mastodon/pull/12963), [Gargron](https://github.com/mastodon/mastodon/pull/12950), [Gargron](https://github.com/mastodon/mastodon/pull/12990), [Gargron](https://github.com/mastodon/mastodon/pull/12949), [Gargron](https://github.com/mastodon/mastodon/pull/12989), [Gargron](https://github.com/mastodon/mastodon/pull/12964), [Gargron](https://github.com/mastodon/mastodon/pull/12965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12957), [Gargron](https://github.com/mastodon/mastodon/pull/12955), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12946), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12954)) +- Add number animations in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12948), [Gargron](https://github.com/mastodon/mastodon/pull/12971)) +- Add `kab`, `is`, `kn`, `mr`, `ur` to available locales ([Gargron](https://github.com/mastodon/mastodon/pull/12882), [BoFFire](https://github.com/mastodon/mastodon/pull/12962), [Gargron](https://github.com/mastodon/mastodon/pull/12379)) +- Add profile filter category ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12918)) +- Add ability to add oneself to lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12271)) +- Add hint how to contribute translations to preferences page ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12736)) +- Add signatures to statuses in archive takeout ([noellabo](https://github.com/mastodon/mastodon/pull/12649)) +- Add support for `magnet:` and `xmpp` links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12905), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12709)) +- Add `follow_request` notification type ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12198)) +- Add ability to filter reports by account domain in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12154)) +- Add link to search for users connected from the same IP address to admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12157)) +- Add link to reports targeting a specific domain in admin view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12513)) +- Add support for EventSource streaming in web UI ([BenLubar](https://github.com/mastodon/mastodon/pull/12887)) +- Add hotkey for opening media attachments in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12498), [Kjwon15](https://github.com/mastodon/mastodon/pull/12546)) +- Add relationship-based options to status dropdowns in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12377), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12535), [Gargron](https://github.com/mastodon/mastodon/pull/12430)) +- Add support for submitting media description with `ctrl`+`enter` in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12272)) +- Add download button to audio and video players in web UI ([NimaBoscarino](https://github.com/mastodon/mastodon/pull/12179)) +- Add setting for whether to crop images in timelines in web UI ([duxovni](https://github.com/mastodon/mastodon/pull/12126)) +- Add support for `Event` activities ([tcitworld](https://github.com/mastodon/mastodon/pull/12637)) +- Add basic support for `Group` actors ([noellabo](https://github.com/mastodon/mastodon/pull/12071)) +- Add `S3_OVERRIDE_PATH_STYLE` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/12594)) +- Add `S3_OPEN_TIMEOUT` environment variable ([tateisu](https://github.com/mastodon/mastodon/pull/12459)) +- Add `LDAP_MAIL` environment variable ([madmath03](https://github.com/mastodon/mastodon/pull/12053)) +- Add `LDAP_UID_CONVERSION_ENABLED` environment variable ([madmath03](https://github.com/mastodon/mastodon/pull/12461)) +- Add `--remote-only` option to `tootctl emoji purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12810)) +- Add `tootctl media remove-orphans` ([Gargron](https://github.com/mastodon/mastodon/pull/12568), [Gargron](https://github.com/mastodon/mastodon/pull/12571)) +- Add `tootctl media lookup` command ([irlcatgirl](https://github.com/mastodon/mastodon/pull/12283)) +- Add cache for OEmbed endpoints to avoid extra HTTP requests ([Gargron](https://github.com/mastodon/mastodon/pull/12403)) +- Add support for KaiOS arrow navigation to public pages ([nolanlawson](https://github.com/mastodon/mastodon/pull/12251)) +- Add `discoverable` to accounts in REST API ([trwnh](https://github.com/mastodon/mastodon/pull/12508)) +- Add admin setting to disable default follows ([ArisuOngaku](https://github.com/mastodon/mastodon/pull/12566)) +- Add support for LDAP and PAM in the OAuth password grant strategy ([ntl-purism](https://github.com/mastodon/mastodon/pull/12390), [Gargron](https://github.com/mastodon/mastodon/pull/12743)) +- Allow support for `Accept`/`Reject` activities with a non-embedded object ([puckipedia](https://github.com/mastodon/mastodon/pull/12199)) +- Add "Show thread" button to public profiles ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/13000)) + +### Changed + +- Change `last_status_at` to be a date, not datetime in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12966)) +- Change followers page to relationships page in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/12927), [Gargron](https://github.com/mastodon/mastodon/pull/12934)) +- Change reported media attachments to always be hidden in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/12879), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12907)) +- Change string from "Disable" to "Disable login" in admin UI ([nileshkumar](https://github.com/mastodon/mastodon/pull/12201)) +- Change report page structure in admin UI ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12615)) +- Change swipe sensitivity to be lower on small screens in web UI ([umonaca](https://github.com/mastodon/mastodon/pull/12168)) +- Change audio/video playback to stop playback when out of view in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12486)) +- Change media description label based on upload type in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12270)) +- Change large numbers to render without decimal units in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/12706)) +- Change "Add a choice" button to be disabled rather than hidden when poll limit reached in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12319), [hinaloe](https://github.com/mastodon/mastodon/pull/12544)) +- Change `tootctl statuses remove` to keep statuses favourited or bookmarked by local users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11267), [Gomasy](https://github.com/mastodon/mastodon/pull/12818)) +- Change domain block behavior to update user records (fast) before deleting data (slower) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12247)) +- Change behaviour to strip audio metadata on uploads ([hugogameiro](https://github.com/mastodon/mastodon/pull/12171)) +- Change accepted length of remote media descriptions from 420 to 1,500 characters ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12262)) +- Change preferences pages structure ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12497), [mayaeh](https://github.com/mastodon/mastodon/pull/12517), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12801), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12797), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12799), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12793)) +- Change format of titles in RSS ([devkral](https://github.com/mastodon/mastodon/pull/8596)) +- Change favourite icon animation from spring-based motion to CSS animation in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12175)) +- Change minimum required Node.js version to 10, and default to 12 ([Shleeble](https://github.com/mastodon/mastodon/pull/12791), [mkody](https://github.com/mastodon/mastodon/pull/12906), [Shleeble](https://github.com/mastodon/mastodon/pull/12703)) +- Change spam check to exempt server staff ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12874)) +- Change to fallback to to `Create` audience when `object` has no defined audience ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12249)) +- Change Twemoji library to 12.1.3 in web UI ([koyuawsmbrtn](https://github.com/mastodon/mastodon/pull/12342)) +- Change blocked users to be hidden from following/followers lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12733)) +- Change signature verification to ignore signatures with invalid host ([Gargron](https://github.com/mastodon/mastodon/pull/13033)) + +### Removed + +- Remove unused dependencies ([ykzts](https://github.com/mastodon/mastodon/pull/12861), [mayaeh](https://github.com/mastodon/mastodon/pull/12826), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12822), [ykzts](https://github.com/mastodon/mastodon/pull/12533)) + +### Fixed + +- Fix some translatable strings being used wrongly ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12569), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12589), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12502), [mayaeh](https://github.com/mastodon/mastodon/pull/12231)) +- Fix headline of public timeline page when set to local-only ([ykzts](https://github.com/mastodon/mastodon/pull/12224)) +- Fix space between tabs not being spread evenly in web UI ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12944), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12961), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12446)) +- Fix interactive delays in database migrations with no TTY ([Gargron](https://github.com/mastodon/mastodon/pull/12969)) +- Fix status overflowing in report dialog in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12959)) +- Fix unlocalized dropdown button title in web UI ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/12947)) +- Fix media attachments without file being uploadable ([Gargron](https://github.com/mastodon/mastodon/pull/12562)) +- Fix unfollow confirmations in profile directory in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12922)) +- Fix duplicate `description` meta tag on accounts public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12923)) +- Fix slow query of federated timeline ([notozeki](https://github.com/mastodon/mastodon/pull/12886)) +- Fix not all of account's active IPs showing up in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/12909), [Gargron](https://github.com/mastodon/mastodon/pull/12943)) +- Fix search by IP not using alternative browser sessions in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/12904)) +- Fix “X new items†not showing up for slow mode on empty timelines in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12875)) +- Fix OEmbed endpoint being inaccessible in secure mode ([Gargron](https://github.com/mastodon/mastodon/pull/12864)) +- Fix proofs API being inaccessible in secure mode ([Gargron](https://github.com/mastodon/mastodon/pull/12495)) +- Fix Ruby 2.7 incompatibilities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12831), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12824), [Shleeble](https://github.com/mastodon/mastodon/pull/12759), [zunda](https://github.com/mastodon/mastodon/pull/12769)) +- Fix invalid poll votes being accepted in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12601)) +- Fix old migrations failing because of strong migrations update ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12787), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12692)) +- Fix reuse of detailed status components in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12792)) +- Fix base64-encoded file uploads not being possible in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/12748), [Gargron](https://github.com/mastodon/mastodon/pull/12857)) +- Fix error due to missing authentication call in filters controller ([Gargron](https://github.com/mastodon/mastodon/pull/12746)) +- Fix uncaught unknown format error in host meta controller ([Gargron](https://github.com/mastodon/mastodon/pull/12747)) +- Fix URL search not returning private toots user has access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12742), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/12336)) +- Fix cache digesting log noise on status embeds ([Gargron](https://github.com/mastodon/mastodon/pull/12750)) +- Fix slowness due to layout thrashing when reloading a large set of statuses in web UI ([panarom](https://github.com/mastodon/mastodon/pull/12661), [panarom](https://github.com/mastodon/mastodon/pull/12744), [Gargron](https://github.com/mastodon/mastodon/pull/12712)) +- Fix error when fetching followers/following from REST API when user has network hidden ([Gargron](https://github.com/mastodon/mastodon/pull/12716)) +- Fix IDN mentions not being processed, IDN domains not being rendered ([Gargron](https://github.com/mastodon/mastodon/pull/12715), [Gargron](https://github.com/mastodon/mastodon/pull/13035), [Gargron](https://github.com/mastodon/mastodon/pull/13030)) +- Fix error when searching for empty phrase ([Gargron](https://github.com/mastodon/mastodon/pull/12711)) +- Fix backups stopping due to read timeouts ([chr-1x](https://github.com/mastodon/mastodon/pull/12281)) +- Fix batch actions on non-pending tags in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12537)) +- Fix sample `SAML_ACS_URL`, `SAML_ISSUER` ([orlea](https://github.com/mastodon/mastodon/pull/12669)) +- Fix manual scrolling issue on Firefox/Windows in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12648)) +- Fix archive takeout failing if total dump size exceeds 2GB ([scd31](https://github.com/mastodon/mastodon/pull/12602), [Gargron](https://github.com/mastodon/mastodon/pull/12653)) +- Fix custom emoji category creation silently erroring out on duplicate category ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12647)) +- Fix link crawler not specifying preferred content type ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12646)) +- Fix featured hashtag setting page erroring out instead of rejecting invalid tags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12436)) +- Fix tooltip messages of single/multiple-choice polls switcher being reversed in web UI ([acid-chicken](https://github.com/mastodon/mastodon/pull/12616)) +- Fix typo in help text of `tootctl statuses remove` ([trwnh](https://github.com/mastodon/mastodon/pull/12603)) +- Fix generic HTTP 500 error on duplicate records ([Gargron](https://github.com/mastodon/mastodon/pull/12563)) +- Fix old migration failing with new status default scope ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12493)) +- Fix errors when using search API with no query ([Gargron](https://github.com/mastodon/mastodon/pull/12541), [trwnh](https://github.com/mastodon/mastodon/pull/12549)) +- Fix poll options not being selectable via keyboard in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12538)) +- Fix conversations not having an unread indicator in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12506)) +- Fix lost focus when modals open/close in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12437)) +- Fix pending upload count not being decremented on error in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12499)) +- Fix empty poll options not being removed on remote poll update ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12484)) +- Fix OCR with delete & redraft in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12465)) +- Fix blur behind closed registration message ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12442)) +- Fix OEmbed discovery not handling different URL variants in query ([Gargron](https://github.com/mastodon/mastodon/pull/12439)) +- Fix link crawler crashing on `` tags without `href` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12159)) +- Fix whitelisted subdomains being ignored in whitelist mode ([noiob](https://github.com/mastodon/mastodon/pull/12435)) +- Fix broken audit log in whitelist mode in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12303)) +- Fix unread indicator not honoring "Only media" option in local and federated timelines in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12330)) +- Fix error when rebuilding home feeds ([dariusk](https://github.com/mastodon/mastodon/pull/12324)) +- Fix relationship caches being broken as result of a follow request ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12299)) +- Fix more items than the limit being uploadable in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12300)) +- Fix various issues with account migration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12301)) +- Fix filtered out items being counted as pending items in slow mode in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12266)) +- Fix notification filters not applying to poll options ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12269)) +- Fix notification message for user's own poll saying it's a poll they voted on in web UI ([ykzts](https://github.com/mastodon/mastodon/pull/12219)) +- Fix polls with an expiration not showing up as expired in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/12222)) +- Fix volume slider having an offset between cursor and slider in Chromium in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12158)) +- Fix Vagrant image not accepting connections ([shrft](https://github.com/mastodon/mastodon/pull/12180)) +- Fix batch actions being hidden on small screens in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12183)) +- Fix incoming federation not working in whitelist mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12185)) +- Fix error when passing empty `source` param to `PUT /api/v1/accounts/update_credentials` ([jglauche](https://github.com/mastodon/mastodon/pull/12259)) +- Fix HTTP-based streaming API being cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/12945)) +- Fix users being able to register while `tootctl self-destruct` is in progress ([Kjwon15](https://github.com/mastodon/mastodon/pull/12877)) +- Fix microformats detection in link crawler not ignoring `h-card` links ([nightpool](https://github.com/mastodon/mastodon/pull/12189)) +- Fix outline on full-screen video in web UI ([hinaloe](https://github.com/mastodon/mastodon/pull/12176)) +- Fix TLD domain blocks not being editable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12805)) +- Fix Nanobox deploy hooks ([danhunsaker](https://github.com/mastodon/mastodon/pull/12663)) +- Fix needlessly complicated SQL query when performing account search amongst followings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12302)) +- Fix favourites count not updating when unfavouriting in web UI ([NimaBoscarino](https://github.com/mastodon/mastodon/pull/12140)) +- Fix occasional crash on scroll in Chromium in web UI ([hinaloe](https://github.com/mastodon/mastodon/pull/12274)) +- Fix intersection observer not working in single-column mode web UI ([panarom](https://github.com/mastodon/mastodon/pull/12735)) +- Fix voting issue with remote polls that contain trailing spaces ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12515)) +- Fix dynamic elements not working in pgHero due to CSP rules ([ykzts](https://github.com/mastodon/mastodon/pull/12489)) +- Fix overly verbose backtraces when delivering ActivityPub payloads ([zunda](https://github.com/mastodon/mastodon/pull/12798)) +- Fix rendering `` without `href` when scheme unsupported ([Gargron](https://github.com/mastodon/mastodon/pull/13040)) +- Fix unfiltered params error when generating ActivityPub tag pagination ([Gargron](https://github.com/mastodon/mastodon/pull/13049)) +- Fix malformed HTML causing uncaught error ([Gargron](https://github.com/mastodon/mastodon/pull/13042)) +- Fix native share button not being displayed for unlisted toots ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13045)) +- Fix remote convertible media attachments (e.g. GIFs) not being saved ([Gargron](https://github.com/mastodon/mastodon/pull/13032)) +- Fix account query not using faster index ([abcang](https://github.com/mastodon/mastodon/pull/13016)) +- Fix error when sending moderation notification ([renatolond](https://github.com/mastodon/mastodon/pull/13014)) + +### Security + +- Fix OEmbed leaking information about existence of non-public statuses ([Gargron](https://github.com/mastodon/mastodon/pull/12930)) +- Fix password change/reset not immediately invalidating other sessions ([Gargron](https://github.com/mastodon/mastodon/pull/12928)) +- Fix settings pages being cacheable by the browser ([Gargron](https://github.com/mastodon/mastodon/pull/12714)) + +## [3.0.1] - 2019-10-10 + +### Added + +- Add `tootctl media usage` command ([Gargron](https://github.com/mastodon/mastodon/pull/12115)) +- Add admin setting to auto-approve trending hashtags ([Gargron](https://github.com/mastodon/mastodon/pull/12122), [Gargron](https://github.com/mastodon/mastodon/pull/12130)) + +### Changed + +- Change `tootctl media refresh` to skip already downloaded attachments ([Gargron](https://github.com/mastodon/mastodon/pull/12118)) + +### Removed + +- Remove auto-silence behaviour from spam check ([Gargron](https://github.com/mastodon/mastodon/pull/12117)) +- Remove HTML `lang` attribute from individual statuses in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12124)) +- Remove fallback to long description on sidebar and meta description ([Gargron](https://github.com/mastodon/mastodon/pull/12119)) + +### Fixed + +- Fix preloaded JSON-LD context for identity not being used ([Gargron](https://github.com/mastodon/mastodon/pull/12138)) +- Fix media editing modal changing dimensions once the image loads ([Gargron](https://github.com/mastodon/mastodon/pull/12131)) +- Fix not showing whether a custom emoji has a local counterpart in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/12135)) +- Fix attachment not being re-downloaded even if file is not stored ([Gargron](https://github.com/mastodon/mastodon/pull/12125)) +- Fix old migration trying to use new column due to default status scope ([Gargron](https://github.com/mastodon/mastodon/pull/12095)) +- Fix column back button missing for not found accounts ([trwnh](https://github.com/mastodon/mastodon/pull/12094)) +- Fix issues with tootctl's parallelization and progress reporting ([Gargron](https://github.com/mastodon/mastodon/pull/12093), [Gargron](https://github.com/mastodon/mastodon/pull/12097)) +- Fix existing user records with now-renamed `pt` locale ([Gargron](https://github.com/mastodon/mastodon/pull/12092)) +- Fix hashtag timeline REST API accepting too many hashtags ([Gargron](https://github.com/mastodon/mastodon/pull/12091)) +- Fix `GET /api/v1/instance` REST APIs being unavailable in secure mode ([Gargron](https://github.com/mastodon/mastodon/pull/12089)) +- Fix performance of home feed regeneration and merging ([Gargron](https://github.com/mastodon/mastodon/pull/12084)) +- Fix ffmpeg performance issues due to stdout buffer overflow ([hugogameiro](https://github.com/mastodon/mastodon/pull/12088)) +- Fix S3 adapter retrying failing uploads with exponential backoff ([Gargron](https://github.com/mastodon/mastodon/pull/12085)) +- Fix `tootctl accounts cull` advertising unused option flag ([Kjwon15](https://github.com/mastodon/mastodon/pull/12074)) + +## [3.0.0] - 2019-10-03 + +### Added + +- Add "not available" label to unloaded media attachments in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11715), [Gargron](https://github.com/mastodon/mastodon/pull/11745)) +- **Add profile directory to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11688), [mayaeh](https://github.com/mastodon/mastodon/pull/11872)) + - Add profile directory opt-in federation + - Add profile directory REST API +- Add special alert for throttled requests in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11677)) +- Add confirmation modal when logging out from the web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11671)) +- **Add audio player in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11644), [Gargron](https://github.com/mastodon/mastodon/pull/11652), [Gargron](https://github.com/mastodon/mastodon/pull/11654), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11629), [Gargron](https://github.com/mastodon/mastodon/pull/12056)) +- **Add autosuggestions for hashtags in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11632), [Gargron](https://github.com/mastodon/mastodon/pull/11764), [Gargron](https://github.com/mastodon/mastodon/pull/11588), [Gargron](https://github.com/mastodon/mastodon/pull/11442)) +- **Add media editing modal with OCR tool in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11563), [Gargron](https://github.com/mastodon/mastodon/pull/11566), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11575), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11576), [Gargron](https://github.com/mastodon/mastodon/pull/11577), [Gargron](https://github.com/mastodon/mastodon/pull/11573), [Gargron](https://github.com/mastodon/mastodon/pull/11571)) +- Add indicator of unread notifications to window title when web UI is out of focus ([Gargron](https://github.com/mastodon/mastodon/pull/11560), [Gargron](https://github.com/mastodon/mastodon/pull/11572)) +- Add indicator for which options you voted for in a poll in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11195)) +- **Add search results pagination to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11447)) +- **Add option to disable real-time updates in web UI ("slow mode")** ([Gargron](https://github.com/mastodon/mastodon/pull/9984), [ykzts](https://github.com/mastodon/mastodon/pull/11880), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11883), [Gargron](https://github.com/mastodon/mastodon/pull/11898), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11859)) +- Add option to disable blurhash previews in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11188)) +- Add native smooth scrolling when supported in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11207)) +- Add scrolling to the search bar on focus in web UI ([Kjwon15](https://github.com/mastodon/mastodon/pull/12032)) +- Add refresh button to list of rebloggers/favouriters in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12031)) +- Add error description and button to copy stack trace to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/12033)) +- Add search and sort functions to hashtag admin UI ([mayaeh](https://github.com/mastodon/mastodon/pull/11829), [Gargron](https://github.com/mastodon/mastodon/pull/11897), [mayaeh](https://github.com/mastodon/mastodon/pull/11875)) +- Add setting for default search engine indexing in admin UI ([brortao](https://github.com/mastodon/mastodon/pull/11804)) +- Add account bio to account view in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11473)) +- **Add option to include reported statuses in warning e-mail from admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11639), [Gargron](https://github.com/mastodon/mastodon/pull/11812), [Gargron](https://github.com/mastodon/mastodon/pull/11741), [Gargron](https://github.com/mastodon/mastodon/pull/11698), [mayaeh](https://github.com/mastodon/mastodon/pull/11765)) +- Add number of pending accounts and pending hashtags to dashboard in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/11514)) +- **Add account migration UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11846), [noellabo](https://github.com/mastodon/mastodon/pull/11905), [noellabo](https://github.com/mastodon/mastodon/pull/11907), [noellabo](https://github.com/mastodon/mastodon/pull/11906), [noellabo](https://github.com/mastodon/mastodon/pull/11902)) +- **Add table of contents to about page** ([Gargron](https://github.com/mastodon/mastodon/pull/11885), [ykzts](https://github.com/mastodon/mastodon/pull/11941), [ykzts](https://github.com/mastodon/mastodon/pull/11895), [Kjwon15](https://github.com/mastodon/mastodon/pull/11916)) +- **Add password challenge to 2FA settings, e-mail notifications** ([Gargron](https://github.com/mastodon/mastodon/pull/11878)) +- **Add optional public list of domain blocks with comments** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11298), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11515), [Gargron](https://github.com/mastodon/mastodon/pull/11908)) +- Add an RSS feed for featured hashtags ([noellabo](https://github.com/mastodon/mastodon/pull/10502)) +- Add explanations to featured hashtags UI and profile ([Gargron](https://github.com/mastodon/mastodon/pull/11586)) +- **Add hashtag trends with admin and user settings** ([Gargron](https://github.com/mastodon/mastodon/pull/11490), [Gargron](https://github.com/mastodon/mastodon/pull/11502), [Gargron](https://github.com/mastodon/mastodon/pull/11641), [Gargron](https://github.com/mastodon/mastodon/pull/11594), [Gargron](https://github.com/mastodon/mastodon/pull/11517), [mayaeh](https://github.com/mastodon/mastodon/pull/11845), [Gargron](https://github.com/mastodon/mastodon/pull/11774), [Gargron](https://github.com/mastodon/mastodon/pull/11712), [Gargron](https://github.com/mastodon/mastodon/pull/11791), [Gargron](https://github.com/mastodon/mastodon/pull/11743), [Gargron](https://github.com/mastodon/mastodon/pull/11740), [Gargron](https://github.com/mastodon/mastodon/pull/11714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11631), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/11569), [Gargron](https://github.com/mastodon/mastodon/pull/11524), [Gargron](https://github.com/mastodon/mastodon/pull/11513)) + - Add hashtag usage breakdown to admin UI + - Add batch actions for hashtags to admin UI + - Add trends to web UI + - Add trends to public pages + - Add user preference to hide trends + - Add admin setting to disable trends +- **Add categories for custom emojis** ([Gargron](https://github.com/mastodon/mastodon/pull/11196), [Gargron](https://github.com/mastodon/mastodon/pull/11793), [Gargron](https://github.com/mastodon/mastodon/pull/11920), [highemerly](https://github.com/mastodon/mastodon/pull/11876)) + - Add custom emoji categories to emoji picker in web UI + - Add `category` to custom emojis in REST API + - Add batch actions for custom emojis in admin UI +- Add max image dimensions to error message ([raboof](https://github.com/mastodon/mastodon/pull/11552)) +- Add aac, m4a, 3gp, amr, wma to allowed audio formats ([Gargron](https://github.com/mastodon/mastodon/pull/11342), [umonaca](https://github.com/mastodon/mastodon/pull/11687)) +- **Add search syntax for operators and phrases** ([Gargron](https://github.com/mastodon/mastodon/pull/11411)) +- **Add REST API for managing featured hashtags** ([noellabo](https://github.com/mastodon/mastodon/pull/11778)) +- **Add REST API for managing timeline read markers** ([Gargron](https://github.com/mastodon/mastodon/pull/11762)) +- Add `exclude_unreviewed` param to `GET /api/v2/search` REST API ([Gargron](https://github.com/mastodon/mastodon/pull/11977)) +- Add `reason` param to `POST /api/v1/accounts` REST API ([Gargron](https://github.com/mastodon/mastodon/pull/12064)) +- **Add ActivityPub secure mode** ([Gargron](https://github.com/mastodon/mastodon/pull/11269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11332), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11295)) +- Add HTTP signatures to all outgoing ActivityPub GET requests ([Gargron](https://github.com/mastodon/mastodon/pull/11284), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11300)) +- Add support for ActivityPub Audio activities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11189)) +- Add ActivityPub actor representing the entire server ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11321), [rtucker](https://github.com/mastodon/mastodon/pull/11400), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11561), [Gargron](https://github.com/mastodon/mastodon/pull/11798)) +- **Add whitelist mode** ([Gargron](https://github.com/mastodon/mastodon/pull/11291), [mayaeh](https://github.com/mastodon/mastodon/pull/11634)) +- Add config of multipart threshold for S3 ([ykzts](https://github.com/mastodon/mastodon/pull/11924), [ykzts](https://github.com/mastodon/mastodon/pull/11944)) +- Add health check endpoint for web ([ykzts](https://github.com/mastodon/mastodon/pull/11770), [ykzts](https://github.com/mastodon/mastodon/pull/11947)) +- Add HTTP signature keyId to request log ([Gargron](https://github.com/mastodon/mastodon/pull/11591)) +- Add `SMTP_REPLY_TO` environment variable ([hugogameiro](https://github.com/mastodon/mastodon/pull/11718)) +- Add `tootctl preview_cards remove` command ([mayaeh](https://github.com/mastodon/mastodon/pull/11320)) +- Add `tootctl media refresh` command ([Gargron](https://github.com/mastodon/mastodon/pull/11775)) +- Add `tootctl cache recount` command ([Gargron](https://github.com/mastodon/mastodon/pull/11597)) +- Add option to exclude suspended domains from `tootctl domains crawl` ([dariusk](https://github.com/mastodon/mastodon/pull/11454)) +- Add parallelization to `tootctl search deploy` ([noellabo](https://github.com/mastodon/mastodon/pull/12051)) +- Add soft delete for statuses for instant deletes through API ([Gargron](https://github.com/mastodon/mastodon/pull/11623), [Gargron](https://github.com/mastodon/mastodon/pull/11648)) +- Add rails-level JSON caching ([Gargron](https://github.com/mastodon/mastodon/pull/11333), [Gargron](https://github.com/mastodon/mastodon/pull/11271)) +- **Add request pool to improve delivery performance** ([Gargron](https://github.com/mastodon/mastodon/pull/10353), [ykzts](https://github.com/mastodon/mastodon/pull/11756)) +- Add concurrent connection attempts to resolved IP addresses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11757)) +- Add index for remember_token to improve login performance ([abcang](https://github.com/mastodon/mastodon/pull/11881)) +- **Add more accurate hashtag search** ([Gargron](https://github.com/mastodon/mastodon/pull/11579), [Gargron](https://github.com/mastodon/mastodon/pull/11427), [Gargron](https://github.com/mastodon/mastodon/pull/11448)) +- **Add more accurate account search** ([Gargron](https://github.com/mastodon/mastodon/pull/11537), [Gargron](https://github.com/mastodon/mastodon/pull/11580)) +- **Add a spam check** ([Gargron](https://github.com/mastodon/mastodon/pull/11217), [Gargron](https://github.com/mastodon/mastodon/pull/11806), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11296)) +- Add new languages ([Gargron](https://github.com/mastodon/mastodon/pull/12062)) + - Breton + - Spanish (Argentina) + - Estonian + - Macedonian + - New Norwegian +- Add NodeInfo endpoint ([Gargron](https://github.com/mastodon/mastodon/pull/12002), [Gargron](https://github.com/mastodon/mastodon/pull/12058)) + +### Changed + +- **Change conversations UI** ([Gargron](https://github.com/mastodon/mastodon/pull/11896)) +- Change dashboard to short number notation ([noellabo](https://github.com/mastodon/mastodon/pull/11847), [noellabo](https://github.com/mastodon/mastodon/pull/11911)) +- Change REST API `GET /api/v1/timelines/public` to require authentication when public preview is off ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11802)) +- Change REST API `POST /api/v1/follow_requests/:id/(approve|reject)` to return relationship ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11800)) +- Change rate limit for media proxy ([ykzts](https://github.com/mastodon/mastodon/pull/11814)) +- Change unlisted custom emoji to not appear in autosuggestions ([Gargron](https://github.com/mastodon/mastodon/pull/11818)) +- Change max length of media descriptions from 420 to 1500 characters ([Gargron](https://github.com/mastodon/mastodon/pull/11819), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11836)) +- **Change deletes to preserve soft-deleted statuses in unresolved reports** ([Gargron](https://github.com/mastodon/mastodon/pull/11805)) +- **Change tootctl to use inline parallelization instead of Sidekiq** ([Gargron](https://github.com/mastodon/mastodon/pull/11776)) +- **Change account deletion page to have better explanations** ([Gargron](https://github.com/mastodon/mastodon/pull/11753), [Gargron](https://github.com/mastodon/mastodon/pull/11763)) +- Change hashtag component in web UI to show numbers for 2 last days ([Gargron](https://github.com/mastodon/mastodon/pull/11742), [Gargron](https://github.com/mastodon/mastodon/pull/11755), [Gargron](https://github.com/mastodon/mastodon/pull/11754)) +- Change OpenGraph description on sign-up page to reflect invite ([Gargron](https://github.com/mastodon/mastodon/pull/11744)) +- Change layout of public profile directory to be the same as in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11705)) +- Change detailed status child ordering to sort self-replies on top ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11686)) +- Change window resize handler to switch to/from mobile layout as soon as needed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11656)) +- Change icon button styles to make hover/focus states more obvious ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11474)) +- Change contrast of status links that are not mentions or hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11406)) +- **Change hashtags to preserve first-used casing** ([Gargron](https://github.com/mastodon/mastodon/pull/11416), [Gargron](https://github.com/mastodon/mastodon/pull/11508), [Gargron](https://github.com/mastodon/mastodon/pull/11504), [Gargron](https://github.com/mastodon/mastodon/pull/11507), [Gargron](https://github.com/mastodon/mastodon/pull/11441)) +- **Change unconfirmed user login behaviour** ([Gargron](https://github.com/mastodon/mastodon/pull/11375), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11394), [Gargron](https://github.com/mastodon/mastodon/pull/11860)) +- **Change single-column mode to scroll the whole page** ([Gargron](https://github.com/mastodon/mastodon/pull/11359), [Gargron](https://github.com/mastodon/mastodon/pull/11894), [Gargron](https://github.com/mastodon/mastodon/pull/11891), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11655), [Gargron](https://github.com/mastodon/mastodon/pull/11463), [Gargron](https://github.com/mastodon/mastodon/pull/11458), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11395), [Gargron](https://github.com/mastodon/mastodon/pull/11418)) +- Change `tootctl accounts follow` to only work with local accounts ([angristan](https://github.com/mastodon/mastodon/pull/11592)) +- Change Dockerfile ([Shleeble](https://github.com/mastodon/mastodon/pull/11710), [ykzts](https://github.com/mastodon/mastodon/pull/11768), [Shleeble](https://github.com/mastodon/mastodon/pull/11707)) +- Change supported Node versions to include v12 ([abcang](https://github.com/mastodon/mastodon/pull/11706)) +- Change Portuguese language from `pt` to `pt-PT` ([Gargron](https://github.com/mastodon/mastodon/pull/11820)) +- Change domain block silence to always require approval on follow ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11975)) +- Change link preview fetcher to not perform a HEAD request first ([Gargron](https://github.com/mastodon/mastodon/pull/12028)) +- Change `tootctl domains purge` to accept multiple domains at once ([Gargron](https://github.com/mastodon/mastodon/pull/12046)) + +### Removed + +- **Remove OStatus support** ([Gargron](https://github.com/mastodon/mastodon/pull/11205), [Gargron](https://github.com/mastodon/mastodon/pull/11303), [Gargron](https://github.com/mastodon/mastodon/pull/11460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11280), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11278)) +- Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` ([Gargron](https://github.com/mastodon/mastodon/pull/11247)) +- Remove WebP support ([angristan](https://github.com/mastodon/mastodon/pull/11589)) +- Remove deprecated config options from Heroku and Scalingo ([ykzts](https://github.com/mastodon/mastodon/pull/11925)) +- Remove deprecated REST API `GET /api/v1/search` API ([Gargron](https://github.com/mastodon/mastodon/pull/11823)) +- Remove deprecated REST API `GET /api/v1/statuses/:id/card` ([Gargron](https://github.com/mastodon/mastodon/pull/11213)) +- Remove deprecated REST API `POST /api/v1/notifications/dismiss?id=:id` ([Gargron](https://github.com/mastodon/mastodon/pull/11214)) +- Remove deprecated REST API `GET /api/v1/timelines/direct` ([Gargron](https://github.com/mastodon/mastodon/pull/11212)) + +### Fixed + +- Fix manifest warning ([ykzts](https://github.com/mastodon/mastodon/pull/11767)) +- Fix admin UI for custom emoji not respecting GIF autoplay preference ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11801)) +- Fix page body not being scrollable in admin/settings layout ([Gargron](https://github.com/mastodon/mastodon/pull/11893)) +- Fix placeholder colors for inputs not being explicitly defined ([Gargron](https://github.com/mastodon/mastodon/pull/11890)) +- Fix incorrect enclosure length in RSS ([tsia](https://github.com/mastodon/mastodon/pull/11889)) +- Fix TOTP codes not being filtered from logs during enabling/disabling ([Gargron](https://github.com/mastodon/mastodon/pull/11877)) +- Fix webfinger response not returning 410 when account is suspended ([Gargron](https://github.com/mastodon/mastodon/pull/11869)) +- Fix ActivityPub Move handler queuing jobs that will fail if account is suspended ([Gargron](https://github.com/mastodon/mastodon/pull/11864)) +- Fix SSO login not using existing account when e-mail is verified ([Gargron](https://github.com/mastodon/mastodon/pull/11862)) +- Fix web UI allowing uploads past status limit via drag & drop ([Gargron](https://github.com/mastodon/mastodon/pull/11863)) +- Fix expiring polls not being displayed as such in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11835)) +- Fix 2FA challenge and password challenge for non-database users ([Gargron](https://github.com/mastodon/mastodon/pull/11831), [Gargron](https://github.com/mastodon/mastodon/pull/11943)) +- Fix profile fields overflowing page width in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11828)) +- Fix web push subscriptions being deleted on rate limit or timeout ([Gargron](https://github.com/mastodon/mastodon/pull/11826)) +- Fix display of long poll options in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11717), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11833)) +- Fix search API not resolving URL when `type` is given ([Gargron](https://github.com/mastodon/mastodon/pull/11822)) +- Fix hashtags being split by ZWNJ character ([Gargron](https://github.com/mastodon/mastodon/pull/11821)) +- Fix scroll position resetting when opening media modals in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11815)) +- Fix duplicate HTML IDs on about page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11803)) +- Fix admin UI showing superfluous reject media/reports on suspended domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11749)) +- Fix ActivityPub context not being dynamically computed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11746)) +- Fix Mastodon logo style on hover on public pages' footer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11735)) +- Fix height of dashboard counters ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11736)) +- Fix custom emoji animation on hover in web UI directory bios ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11716)) +- Fix non-numbers being passed to Redis and causing an error ([Gargron](https://github.com/mastodon/mastodon/pull/11697)) +- Fix error in REST API for an account's statuses ([Gargron](https://github.com/mastodon/mastodon/pull/11700)) +- Fix uncaught error when resource param is missing in Webfinger request ([Gargron](https://github.com/mastodon/mastodon/pull/11701)) +- Fix uncaught domain normalization error in remote follow ([Gargron](https://github.com/mastodon/mastodon/pull/11703)) +- Fix uncaught 422 and 500 errors ([Gargron](https://github.com/mastodon/mastodon/pull/11590), [Gargron](https://github.com/mastodon/mastodon/pull/11811)) +- Fix uncaught parameter missing exceptions and missing error templates ([Gargron](https://github.com/mastodon/mastodon/pull/11702)) +- Fix encoding error when checking e-mail MX records ([Gargron](https://github.com/mastodon/mastodon/pull/11696)) +- Fix items in StatusContent render list not all having a key ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11645)) +- Fix remote and staff-removed statuses leaving media behind for a day ([Gargron](https://github.com/mastodon/mastodon/pull/11638)) +- Fix CSP needlessly allowing blob URLs in script-src ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11620)) +- Fix ignoring whole status because of one invalid hashtag ([Gargron](https://github.com/mastodon/mastodon/pull/11621)) +- Fix hidden statuses losing focus ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11208)) +- Fix loading bar being obscured by other elements in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11598)) +- Fix multiple issues with replies collection for pages further than self-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11582)) +- Fix blurhash and autoplay not working on public pages ([Gargron](https://github.com/mastodon/mastodon/pull/11585)) +- Fix 422 being returned instead of 404 when POSTing to unmatched routes ([Gargron](https://github.com/mastodon/mastodon/pull/11574), [Gargron](https://github.com/mastodon/mastodon/pull/11704)) +- Fix client-side resizing of image uploads ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11570)) +- Fix short number formatting for numbers above million in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11559)) +- Fix ActivityPub and REST API queries setting cookies and preventing caching ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11539), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11557), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11331)) +- Fix some emojis in profile metadata labels are not emojified. ([kedamaDQ](https://github.com/mastodon/mastodon/pull/11534)) +- Fix account search always returning exact match on paginated results ([Gargron](https://github.com/mastodon/mastodon/pull/11525)) +- Fix acct URIs with IDN domains not being resolved ([Gargron](https://github.com/mastodon/mastodon/pull/11520)) +- Fix admin dashboard missing latest features ([Gargron](https://github.com/mastodon/mastodon/pull/11505)) +- Fix jumping of toot date when clicking spoiler button ([ariasuni](https://github.com/mastodon/mastodon/pull/11449)) +- Fix boost to original audience not working on mobile in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11371)) +- Fix handling of webfinger redirects in ResolveAccountService ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11279)) +- Fix URLs appearing twice in errors of ActivityPub::DeliveryWorker ([Gargron](https://github.com/mastodon/mastodon/pull/11231)) +- Fix support for HTTP proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11245)) +- Fix HTTP requests to IPv6 hosts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11240)) +- Fix error in Elasticsearch index import ([mayaeh](https://github.com/mastodon/mastodon/pull/11192)) +- Fix duplicate account error when seeding development database ([ysksn](https://github.com/mastodon/mastodon/pull/11366)) +- Fix performance of session clean-up scheduler ([abcang](https://github.com/mastodon/mastodon/pull/11871)) +- Fix older migrations not running ([zunda](https://github.com/mastodon/mastodon/pull/11377)) +- Fix URLs counting towards RTL detection ([ahangarha](https://github.com/mastodon/mastodon/pull/11759)) +- Fix unnecessary status re-rendering in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11211)) +- Fix http_parser.rb gem not being compiled when no network available ([petabyteboy](https://github.com/mastodon/mastodon/pull/11444)) +- Fix muted text color not applying to all text ([trwnh](https://github.com/mastodon/mastodon/pull/11996)) +- Fix follower/following lists resetting on back-navigation in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11986)) +- Fix n+1 query when approving multiple follow requests ([abcang](https://github.com/mastodon/mastodon/pull/12004)) +- Fix records not being indexed into Elasticsearch sometimes ([Gargron](https://github.com/mastodon/mastodon/pull/12024)) +- Fix needlessly indexing unsearchable statuses into Elasticsearch ([Gargron](https://github.com/mastodon/mastodon/pull/12041)) +- Fix new user bootstrapping crashing when to-be-followed accounts are invalid ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12037)) +- Fix featured hashtag URL being interpreted as media or replies tab ([Gargron](https://github.com/mastodon/mastodon/pull/12048)) +- Fix account counters being overwritten by parallel writes ([Gargron](https://github.com/mastodon/mastodon/pull/12045)) + +### Security + +- Fix performance of GIF re-encoding and always strip EXIF data from videos ([Gargron](https://github.com/mastodon/mastodon/pull/12057)) + +## [2.9.3] - 2019-08-10 + +### Added + +- Add GIF and WebP support for custom emojis ([Gargron](https://github.com/mastodon/mastodon/pull/11519)) +- Add logout link to dropdown menu in web UI ([koyuawsmbrtn](https://github.com/mastodon/mastodon/pull/11353)) +- Add indication that text search is unavailable in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11112), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11202)) +- Add `suffix` to `Mastodon::Version` to help forks ([clarfon](https://github.com/mastodon/mastodon/pull/11407)) +- Add on-hover animation to animated custom emoji in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11348), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11404), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11522)) +- Add custom emoji support in profile metadata labels ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11350)) + +### Changed + +- Change default interface of web and streaming from 0.0.0.0 to 127.0.0.1 ([Gargron](https://github.com/mastodon/mastodon/pull/11302), [zunda](https://github.com/mastodon/mastodon/pull/11378), [Gargron](https://github.com/mastodon/mastodon/pull/11351), [zunda](https://github.com/mastodon/mastodon/pull/11326)) +- Change the retry limit of web push notifications ([highemerly](https://github.com/mastodon/mastodon/pull/11292)) +- Change ActivityPub deliveries to not retry HTTP 501 errors ([Gargron](https://github.com/mastodon/mastodon/pull/11233)) +- Change language detection to include hashtags as words ([Gargron](https://github.com/mastodon/mastodon/pull/11341)) +- Change terms and privacy policy pages to always be accessible ([Gargron](https://github.com/mastodon/mastodon/pull/11334)) +- Change robots tag to include `noarchive` when user opts out of indexing ([Kjwon15](https://github.com/mastodon/mastodon/pull/11421)) + +### Fixed + +- Fix account domain block not clearing out notifications ([Gargron](https://github.com/mastodon/mastodon/pull/11393)) +- Fix incorrect locale sometimes being detected for browser ([Gargron](https://github.com/mastodon/mastodon/pull/8657)) +- Fix crash when saving invalid domain name ([Gargron](https://github.com/mastodon/mastodon/pull/11528)) +- Fix pinned statuses REST API returning pagination headers ([Gargron](https://github.com/mastodon/mastodon/pull/11526)) +- Fix "cancel follow request" button having unreadable text in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11521)) +- Fix image uploads being blank when canvas read access is blocked ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11499)) +- Fix avatars not being animated on hover when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11349)) +- Fix overzealous sanitization of HTML lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11354)) +- Fix block crashing when a follow request exists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11288)) +- Fix backup service crashing when an attachment is missing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11241)) +- Fix account moderation action always sending e-mail notification ([Gargron](https://github.com/mastodon/mastodon/pull/11242)) +- Fix swiping columns on mobile sometimes failing in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11200)) +- Fix wrong actor URI being serialized into poll updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11194)) +- Fix statsd UDP sockets not being cleaned up in Sidekiq ([Gargron](https://github.com/mastodon/mastodon/pull/11230)) +- Fix expiration date of filters being set to "never" when editing them ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11204)) +- Fix support for MP4 files that are actually M4V files ([Gargron](https://github.com/mastodon/mastodon/pull/11210)) +- Fix `alerts` not being typecast correctly in push subscription in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/11343)) +- Fix some notices staying on unrelated pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11364)) +- Fix unboosting sometimes preventing a boost from reappearing on feed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11405), [Gargron](https://github.com/mastodon/mastodon/pull/11450)) +- Fix only one middle dot being recognized in hashtags ([Gargron](https://github.com/mastodon/mastodon/pull/11345), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11363)) +- Fix unnecessary SQL query performed on unauthenticated requests ([Gargron](https://github.com/mastodon/mastodon/pull/11179)) +- Fix incorrect timestamp displayed on featured tags ([Kjwon15](https://github.com/mastodon/mastodon/pull/11477)) +- Fix privacy dropdown active state when dropdown is placed on top of it ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11495)) +- Fix filters not being applied to poll options ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11174)) +- Fix keyboard navigation on various dropdowns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11511), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11492), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11491)) +- Fix keyboard navigation in modals ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11493)) +- Fix image conversation being non-deterministic due to timestamps ([Gargron](https://github.com/mastodon/mastodon/pull/11408)) +- Fix web UI performance ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11211), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11234)) +- Fix scrolling to compose form when not necessary in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11246), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/11182)) +- Fix save button being enabled when list title is empty in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11475)) +- Fix poll expiration not being pre-filled on delete & redraft in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11203)) +- Fix content warning sometimes being set when not requested in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11206)) + +### Security + +- Fix invites not being disabled upon account suspension ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11412)) +- Fix blocked domains still being able to fill database with account records ([Gargron](https://github.com/mastodon/mastodon/pull/11219)) + +## [2.9.2] - 2019-06-22 + +### Added + +- Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/11146)) + +### Changed + +- Change camera icon to paperclip icon in upload form ([koyuawsmbrtn](https://github.com/mastodon/mastodon/pull/11149)) + +### Fixed + +- Fix audio-only OGG and WebM files not being processed as such ([Gargron](https://github.com/mastodon/mastodon/pull/11151)) +- Fix audio not being downloaded from remote servers ([Gargron](https://github.com/mastodon/mastodon/pull/11145)) + +## [2.9.1] - 2019-06-22 + +### Added + +- Add moderation API ([Gargron](https://github.com/mastodon/mastodon/pull/9387)) +- Add audio uploads ([Gargron](https://github.com/mastodon/mastodon/pull/11123), [Gargron](https://github.com/mastodon/mastodon/pull/11141)) + +### Changed + +- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/mastodon/mastodon/pull/11138)) +- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/mastodon/mastodon/pull/11083)) + +### Removed + +- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/11139)) + +### Fixed + +- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/mastodon/mastodon/pull/11130)) +- Fix layout of identity proofs settings ([acid-chicken](https://github.com/mastodon/mastodon/pull/11126)) +- Fix active scope only returning suspended users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11111)) +- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/mastodon/mastodon/pull/10836)) +- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/mastodon/mastodon/pull/11121)) +- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11113)) +- Fix scrolling behaviour in compose form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11093)) + +## [2.9.0] - 2019-06-13 + +### Added + +- **Add single-column mode in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/10807), [Gargron](https://github.com/mastodon/mastodon/pull/10848), [Gargron](https://github.com/mastodon/mastodon/pull/11003), [Gargron](https://github.com/mastodon/mastodon/pull/10961), [Hanage999](https://github.com/mastodon/mastodon/pull/10915), [noellabo](https://github.com/mastodon/mastodon/pull/10917), [abcang](https://github.com/mastodon/mastodon/pull/10859), [Gargron](https://github.com/mastodon/mastodon/pull/10820), [Gargron](https://github.com/mastodon/mastodon/pull/10835), [Gargron](https://github.com/mastodon/mastodon/pull/10809), [Gargron](https://github.com/mastodon/mastodon/pull/10963), [noellabo](https://github.com/mastodon/mastodon/pull/10883), [Hanage999](https://github.com/mastodon/mastodon/pull/10839)) +- Add waiting time to the list of pending accounts in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/10985)) +- Add a keyboard shortcut to hide/show media in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10647), [Gargron](https://github.com/mastodon/mastodon/pull/10838), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10872)) +- Add `account_id` param to `GET /api/v1/notifications` ([pwoolcoc](https://github.com/mastodon/mastodon/pull/10796)) +- Add confirmation modal for unboosting toots in web UI ([aurelien-reeves](https://github.com/mastodon/mastodon/pull/10287)) +- Add emoji suggestions to content warning and poll option fields in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10555)) +- Add `source` attribute to response of `DELETE /api/v1/statuses/:id` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10669)) +- Add some caching for HTML versions of public status pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10701)) +- Add button to conveniently copy OAuth code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11065)) + +### Changed + +- **Change default layout to single column in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/10847)) +- **Change light theme** ([Gargron](https://github.com/mastodon/mastodon/pull/10992), [Gargron](https://github.com/mastodon/mastodon/pull/10996), [yuzulabo](https://github.com/mastodon/mastodon/pull/10754), [Gargron](https://github.com/mastodon/mastodon/pull/10845)) +- **Change preferences page into appearance, notifications, and other** ([Gargron](https://github.com/mastodon/mastodon/pull/10977), [Gargron](https://github.com/mastodon/mastodon/pull/10988)) +- Change priority of delete activity forwards for replies and reblogs ([Gargron](https://github.com/mastodon/mastodon/pull/11002)) +- Change Mastodon logo to use primary text color of the given theme ([Gargron](https://github.com/mastodon/mastodon/pull/10994)) +- Change reblogs counter to be updated when boosted privately ([Gargron](https://github.com/mastodon/mastodon/pull/10964)) +- Change bio limit from 160 to 500 characters ([trwnh](https://github.com/mastodon/mastodon/pull/10790)) +- Change API rate limiting to reduce allowed unauthenticated requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10860), [hinaloe](https://github.com/mastodon/mastodon/pull/10868), [mayaeh](https://github.com/mastodon/mastodon/pull/10867)) +- Change help text of `tootctl emoji import` command to specify a gzipped TAR archive is required ([dariusk](https://github.com/mastodon/mastodon/pull/11000)) +- Change web UI to hide poll options behind content warnings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10983)) +- Change silencing to ensure local effects and remote effects are the same for silenced local users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10575)) +- Change `tootctl domains purge` to remove custom emoji as well ([Kjwon15](https://github.com/mastodon/mastodon/pull/10721)) +- Change Docker image to keep `apt` working ([SuperSandro2000](https://github.com/mastodon/mastodon/pull/10830)) + +### Removed + +- Remove `dist-upgrade` from Docker image ([SuperSandro2000](https://github.com/mastodon/mastodon/pull/10822)) + +### Fixed + +- Fix RTL layout not being RTL within the columns area in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10990)) +- Fix display of alternative text when a media attachment is not available in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10981)) +- Fix not being able to directly switch between list timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10973)) +- Fix media sensitivity not being maintained in delete & redraft in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10980)) +- Fix emoji picker being always displayed in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/10979), [yuzulabo](https://github.com/mastodon/mastodon/pull/10801), [wcpaez](https://github.com/mastodon/mastodon/pull/10978)) +- Fix potential private status leak through caching ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10969)) +- Fix refreshing featured toots when the new collection is empty in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10971)) +- Fix undoing domain block also undoing individual moderation on users from before the domain block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10660)) +- Fix time not being local in the audit log ([yuzulabo](https://github.com/mastodon/mastodon/pull/10751)) +- Fix statuses removed by moderation re-appearing on subsequent fetches ([Kjwon15](https://github.com/mastodon/mastodon/pull/10732)) +- Fix misattribution of inlined announces if `attributedTo` isn't present in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10967)) +- Fix `GET /api/v1/polls/:id` not requiring authentication for non-public polls ([Gargron](https://github.com/mastodon/mastodon/pull/10960)) +- Fix handling of blank poll options in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10946)) +- Fix avatar preview aspect ratio on edit profile page ([Kjwon15](https://github.com/mastodon/mastodon/pull/10931)) +- Fix web push notifications not being sent for polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10864)) +- Fix cut off letters in last paragraph of statuses in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/10821)) +- Fix list not being automatically unpinned when it returns 404 in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11045)) +- Fix login sometimes redirecting to paths that are not pages ([Gargron](https://github.com/mastodon/mastodon/pull/11019)) + +## [2.8.4] - 2019-05-24 + +### Fixed + +- Fix delivery not retrying on some inbox errors that should be retriable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10812)) +- Fix unnecessary 5 minute cooldowns on signature verifications in some cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10813)) +- Fix possible race condition when processing statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10815)) + +### Security + +- Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10818)) + +## [2.8.3] - 2019-05-19 + +### Added + +- Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/mastodon/mastodon/pull/10779)) +- Add clickable area below avatar in statuses in web UI ([Dar13](https://github.com/mastodon/mastodon/pull/10766)) +- Add crossed-out eye icon on account gallery in web UI ([Kjwon15](https://github.com/mastodon/mastodon/pull/10715)) +- Add media description tooltip to thumbnails in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10713)) + +### Changed + +- Change "mark as sensitive" button into a checkbox for clarity ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10748)) + +### Fixed + +- Fix bug allowing users to publicly boost their private statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10775), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10783)) +- Fix performance in formatter by a little ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10765)) +- Fix some colors in the light theme ([yuzulabo](https://github.com/mastodon/mastodon/pull/10754)) +- Fix some colors of the high contrast theme ([yuzulabo](https://github.com/mastodon/mastodon/pull/10711)) +- Fix ambivalent active state of poll refresh button in web UI ([MaciekBaron](https://github.com/mastodon/mastodon/pull/10720)) +- Fix duplicate posting being possible from web UI ([hinaloe](https://github.com/mastodon/mastodon/pull/10785)) +- Fix "invited by" not showing up in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10791)) + +## [2.8.2] - 2019-05-05 + +### Added + +- Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/mastodon/mastodon/pull/10698)) + +### Fixed + +- Fix cropped hero image on frontpage ([BaptisteGelez](https://github.com/mastodon/mastodon/pull/10702)) +- Fix blurhash gem not compiling on some operating systems ([Gargron](https://github.com/mastodon/mastodon/pull/10700)) +- Fix unexpected CSS animations in some browsers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10699)) +- Fix closing video modal scrolling timelines to top ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10695)) + +## [2.8.1] - 2019-05-04 + +### Added + +- Add link to existing domain block when trying to block an already-blocked domain ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10663)) +- Add button to view context to media modal when opened from account gallery in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10676)) +- Add ability to create multiple-choice polls in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10603)) +- Add `GITHUB_REPOSITORY` and `SOURCE_BASE_URL` environment variables ([rosylilly](https://github.com/mastodon/mastodon/pull/10600)) +- Add `/interact/` paths to `robots.txt` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10666)) +- Add `blurhash` to the Attachment entity in the REST API ([Gargron](https://github.com/mastodon/mastodon/pull/10630)) + +### Changed + +- Change hidden media to be shown as a blurhash-based colorful gradient instead of a black box in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10630)) +- Change rejected media to be shown as a blurhash-based gradient instead of a list of filenames in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10630)) +- Change e-mail whitelist/blacklist to not be checked when invited ([Gargron](https://github.com/mastodon/mastodon/pull/10683)) +- Change cache header of REST API results to no-cache ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10655)) +- Change the "mark media as sensitive" button to be more obvious in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10673), [Gargron](https://github.com/mastodon/mastodon/pull/10682)) +- Change account gallery in web UI to display 3 columns, open media modal ([Gargron](https://github.com/mastodon/mastodon/pull/10667), [Gargron](https://github.com/mastodon/mastodon/pull/10674)) + +### Fixed + +- Fix LDAP/PAM/SAML/CAS users not being pre-approved ([Gargron](https://github.com/mastodon/mastodon/pull/10621)) +- Fix accounts created through tootctl not being always pre-approved ([Gargron](https://github.com/mastodon/mastodon/pull/10684)) +- Fix Sidekiq retrying ActivityPub processing jobs that fail validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10614)) +- Fix toots not being scrolled into view sometimes through keyboard selection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10593)) +- Fix expired invite links being usable to bypass approval mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10657)) +- Fix not being able to save e-mail preference for new pending accounts ([Gargron](https://github.com/mastodon/mastodon/pull/10622)) +- Fix upload progressbar when image resizing is involved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10632)) +- Fix block action not automatically cancelling pending follow request ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10633)) +- Fix stoplight logging to stderr separate from Rails logger ([Gargron](https://github.com/mastodon/mastodon/pull/10624)) +- Fix sign up button not saying sign up when invite is used ([Gargron](https://github.com/mastodon/mastodon/pull/10623)) +- Fix health checks in Docker Compose configuration ([fabianonline](https://github.com/mastodon/mastodon/pull/10553)) +- Fix modal items not being scrollable on touch devices ([kedamaDQ](https://github.com/mastodon/mastodon/pull/10605)) +- Fix Keybase configuration using wrong domain when a web domain is used ([BenLubar](https://github.com/mastodon/mastodon/pull/10565)) +- Fix avatar GIFs not being animated on-hover on public profiles ([hyenagirl64](https://github.com/mastodon/mastodon/pull/10549)) +- Fix OpenGraph parser not understanding some valid property meta tags ([da2x](https://github.com/mastodon/mastodon/pull/10604)) +- Fix wrong fonts being displayed when Roboto is installed on user's machine ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10594)) +- Fix confirmation modals being too narrow for a secondary action button ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10586)) + +## [2.8.0] - 2019-04-10 + +### Added + +- Add polls ([Gargron](https://github.com/mastodon/mastodon/pull/10111), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10155), [Gargron](https://github.com/mastodon/mastodon/pull/10184), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10196), [Gargron](https://github.com/mastodon/mastodon/pull/10248), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10255), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10322), [Gargron](https://github.com/mastodon/mastodon/pull/10138), [Gargron](https://github.com/mastodon/mastodon/pull/10139), [Gargron](https://github.com/mastodon/mastodon/pull/10144), [Gargron](https://github.com/mastodon/mastodon/pull/10145),[Gargron](https://github.com/mastodon/mastodon/pull/10146), [Gargron](https://github.com/mastodon/mastodon/pull/10148), [Gargron](https://github.com/mastodon/mastodon/pull/10151), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10150), [Gargron](https://github.com/mastodon/mastodon/pull/10168), [Gargron](https://github.com/mastodon/mastodon/pull/10165), [Gargron](https://github.com/mastodon/mastodon/pull/10172), [Gargron](https://github.com/mastodon/mastodon/pull/10170), [Gargron](https://github.com/mastodon/mastodon/pull/10171), [Gargron](https://github.com/mastodon/mastodon/pull/10186), [Gargron](https://github.com/mastodon/mastodon/pull/10189), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10200), [rinsuki](https://github.com/mastodon/mastodon/pull/10203), [Gargron](https://github.com/mastodon/mastodon/pull/10213), [Gargron](https://github.com/mastodon/mastodon/pull/10246), [Gargron](https://github.com/mastodon/mastodon/pull/10265), [Gargron](https://github.com/mastodon/mastodon/pull/10261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10333), [Gargron](https://github.com/mastodon/mastodon/pull/10352), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10140), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10141), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10161), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10158), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10156), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10160), [Gargron](https://github.com/mastodon/mastodon/pull/10185), [Gargron](https://github.com/mastodon/mastodon/pull/10188), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10195), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10208), [Gargron](https://github.com/mastodon/mastodon/pull/10187), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10214), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10209)) +- Add follows & followers managing UI ([Gargron](https://github.com/mastodon/mastodon/pull/10268), [Gargron](https://github.com/mastodon/mastodon/pull/10308), [Gargron](https://github.com/mastodon/mastodon/pull/10404), [Gargron](https://github.com/mastodon/mastodon/pull/10293)) +- Add identity proof integration with Keybase ([Gargron](https://github.com/mastodon/mastodon/pull/10297), [xgess](https://github.com/mastodon/mastodon/pull/10375), [Gargron](https://github.com/mastodon/mastodon/pull/10338), [Gargron](https://github.com/mastodon/mastodon/pull/10350), [Gargron](https://github.com/mastodon/mastodon/pull/10414)) +- Add option to overwrite imported data instead of merging ([Gargron](https://github.com/mastodon/mastodon/pull/9962)) +- Add featured hashtags to profiles ([Gargron](https://github.com/mastodon/mastodon/pull/9755), [Gargron](https://github.com/mastodon/mastodon/pull/10167), [Gargron](https://github.com/mastodon/mastodon/pull/10249), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10034)) +- Add admission-based registrations mode ([Gargron](https://github.com/mastodon/mastodon/pull/10250), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10269), [Gargron](https://github.com/mastodon/mastodon/pull/10264), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10321), [Gargron](https://github.com/mastodon/mastodon/pull/10349), [Gargron](https://github.com/mastodon/mastodon/pull/10469)) +- Add support for WebP uploads ([acid-chicken](https://github.com/mastodon/mastodon/pull/9879)) +- Add "copy link" item to status action bars in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/9983)) +- Add list title editing in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9748)) +- Add a "Block & Report" button to the block confirmation dialog in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10360)) +- Add disappointed elephant when the page crashes in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10275)) +- Add ability to upload multiple files at once in web UI ([tmm576](https://github.com/mastodon/mastodon/pull/9856)) +- Add indication when you are not allowed to follow an account in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10420), [Gargron](https://github.com/mastodon/mastodon/pull/10491)) +- Add validations to admin settings to catch common mistakes ([Gargron](https://github.com/mastodon/mastodon/pull/10348), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10354)) +- Add `type`, `limit`, `offset`, `min_id`, `max_id`, `account_id` to search API ([Gargron](https://github.com/mastodon/mastodon/pull/10091)) +- Add a preferences API so apps can share basic behaviours ([Gargron](https://github.com/mastodon/mastodon/pull/10109)) +- Add `visibility` param to reblog REST API ([Gargron](https://github.com/mastodon/mastodon/pull/9851), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10302)) +- Add `allowfullscreen` attribute to OEmbed iframe ([rinsuki](https://github.com/mastodon/mastodon/pull/10370)) +- Add `blocked_by` relationship to the REST API ([Gargron](https://github.com/mastodon/mastodon/pull/10373)) +- Add `tootctl statuses remove` to sweep unreferenced statuses ([Gargron](https://github.com/mastodon/mastodon/pull/10063)) +- Add `tootctl search deploy` to avoid ugly rake task syntax ([Gargron](https://github.com/mastodon/mastodon/pull/10403)) +- Add `tootctl self-destruct` to shut down server gracefully ([Gargron](https://github.com/mastodon/mastodon/pull/10367)) +- Add option to hide application used to toot ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9897), [rinsuki](https://github.com/mastodon/mastodon/pull/9994), [hinaloe](https://github.com/mastodon/mastodon/pull/10086)) +- Add `DB_SSLMODE` configuration variable ([sascha-sl](https://github.com/mastodon/mastodon/pull/10210)) +- Add click-to-copy UI to invites page ([Gargron](https://github.com/mastodon/mastodon/pull/10259)) +- Add self-replies fetching ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10106), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10128), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10175), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10201)) +- Add rate limit for media proxy requests ([Gargron](https://github.com/mastodon/mastodon/pull/10490)) +- Add `tootctl emoji purge` ([Gargron](https://github.com/mastodon/mastodon/pull/10481)) +- Add `tootctl accounts approve` ([Gargron](https://github.com/mastodon/mastodon/pull/10480)) +- Add `tootctl accounts reset-relationships` ([noellabo](https://github.com/mastodon/mastodon/pull/10483)) + +### Changed + +- Change design of landing page ([Gargron](https://github.com/mastodon/mastodon/pull/10232), [Gargron](https://github.com/mastodon/mastodon/pull/10260), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10284), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10291), [koyuawsmbrtn](https://github.com/mastodon/mastodon/pull/10356), [Gargron](https://github.com/mastodon/mastodon/pull/10245)) +- Change design of profile column in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10337), [Aditoo17](https://github.com/mastodon/mastodon/pull/10387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10390), [mayaeh](https://github.com/mastodon/mastodon/pull/10379), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10411)) +- Change language detector threshold from 140 characters to 4 words ([Gargron](https://github.com/mastodon/mastodon/pull/10376)) +- Change language detector to always kick in for non-latin alphabets ([Gargron](https://github.com/mastodon/mastodon/pull/10276)) +- Change icons of features on admin dashboard ([Gargron](https://github.com/mastodon/mastodon/pull/10366)) +- Change DNS timeouts from 1s to 5s ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10238)) +- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/mastodon/mastodon/pull/10100), [BenLubar](https://github.com/mastodon/mastodon/pull/10212)) +- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/9059)) +- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10339)) +- Change web UI to not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359)) +- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/mastodon/mastodon/pull/10378)) +- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/mastodon/mastodon/pull/9924)) +- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/mastodon/mastodon/pull/10289)) +- Change web UI to use new Web Share Target API ([gol-cha](https://github.com/mastodon/mastodon/pull/9963)) +- Change ActivityPub reports to have persistent URIs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10303)) +- Change `tootctl accounts cull --dry-run` to list accounts that would be deleted ([BenLubar](https://github.com/mastodon/mastodon/pull/10460)) +- Change format of CSV exports of follows and mutes to include extra settings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10495), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10335)) +- Change ActivityPub collections to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10467)) +- Change REST API and public profiles to not return follows/followers for users that have blocked you ([Gargron](https://github.com/mastodon/mastodon/pull/10491)) +- Change the groupings of menu items in settings navigation ([Gargron](https://github.com/mastodon/mastodon/pull/10533)) + +### Removed + +- Remove zopfli compression to speed up Webpack from 6min to 1min ([nolanlawson](https://github.com/mastodon/mastodon/pull/10288)) +- Remove stats.json generation to speed up Webpack ([nolanlawson](https://github.com/mastodon/mastodon/pull/10290)) + +### Fixed + +- Fix public timelines being broken by new toots when they are not mounted in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10131)) +- Fix quick filter settings not being saved when selecting a different filter in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10296)) +- Fix remote interaction dialogs being indexed by search engines ([Gargron](https://github.com/mastodon/mastodon/pull/10240)) +- Fix maxed-out invites not showing up as expired in UI ([Gargron](https://github.com/mastodon/mastodon/pull/10274)) +- Fix scrollbar styles on compose textarea ([Gargron](https://github.com/mastodon/mastodon/pull/10292)) +- Fix timeline merge workers being queued for remote users ([Gargron](https://github.com/mastodon/mastodon/pull/10355)) +- Fix alternative relay support regression ([Gargron](https://github.com/mastodon/mastodon/pull/10398)) +- Fix trying to fetch keys of unknown accounts on a self-delete from them ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10326)) +- Fix CAS `:service_validate_url` option ([enewhuis](https://github.com/mastodon/mastodon/pull/10328)) +- Fix race conditions when creating backups ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10234)) +- Fix whitespace not being stripped out of username before validation ([aurelien-reeves](https://github.com/mastodon/mastodon/pull/10239)) +- Fix n+1 query when deleting status ([Gargron](https://github.com/mastodon/mastodon/pull/10247)) +- Fix exiting follows not being rejected when suspending a remote account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10230)) +- Fix the underlying button element in a disabled icon button not being disabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10194)) +- Fix race condition when streaming out deleted statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10280)) +- Fix performance of admin federation UI by caching account counts ([Gargron](https://github.com/mastodon/mastodon/pull/10374)) +- Fix JS error on pages that don't define a CSRF token ([hinaloe](https://github.com/mastodon/mastodon/pull/10383)) +- Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/mastodon/mastodon/pull/10460)) + +## [2.7.4] - 2019-03-05 + +### Fixed + +- Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/mastodon/mastodon/pull/10108)) +- Fix redundant HTTP requests when resolving private statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10115)) +- Fix performance of account media query ([abcang](https://github.com/mastodon/mastodon/pull/10121)) +- Fix mention processing for unknown accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10125)) +- Fix getting started column not scrolling on short screens ([trwnh](https://github.com/mastodon/mastodon/pull/10075)) +- Fix direct messages pagination in the web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10126)) +- Fix serialization of Announce activities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10129)) +- Fix home timeline perpetually reloading when empty in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/10130)) +- Fix lists export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10136)) +- Fix edit profile page crash for suspended-then-unsuspended users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10178)) + +## [2.7.3] - 2019-02-23 + +### Added + +- Add domain filter to the admin federation page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10071)) +- Add quick link from admin account view to block/unblock instance ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10073)) + +### Fixed + +- Fix video player width not being updated to fit container width ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10069)) +- Fix domain filter being shown in admin page when local filter is active ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10074)) +- Fix crash when conversations have no valid participants ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10078)) +- Fix error when performing admin actions on no statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10094)) + +### Changed + +- Change custom emojis to randomize stored file name ([hinaloe](https://github.com/mastodon/mastodon/pull/10090)) + +## [2.7.2] - 2019-02-17 + +### Added + +- Add support for IPv6 in e-mail validation ([zoc](https://github.com/mastodon/mastodon/pull/10009)) +- Add record of IP address used for signing up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10026)) +- Add tight rate-limit for API deletions (30 per 30 minutes) ([Gargron](https://github.com/mastodon/mastodon/pull/10042)) +- Add support for embedded `Announce` objects attributed to the same actor ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9998), [Gargron](https://github.com/mastodon/mastodon/pull/10065)) +- Add spam filter for `Create` and `Announce` activities ([Gargron](https://github.com/mastodon/mastodon/pull/10005), [Gargron](https://github.com/mastodon/mastodon/pull/10041), [Gargron](https://github.com/mastodon/mastodon/pull/10062)) +- Add `registrations` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/10060)) +- Add `vapid_key` to `POST /api/v1/apps` and `GET /api/v1/apps/verify_credentials` ([Gargron](https://github.com/mastodon/mastodon/pull/10058)) + +### Fixed + +- Fix link color and add link underlines in high-contrast theme ([Gargron](https://github.com/mastodon/mastodon/pull/9949), [Gargron](https://github.com/mastodon/mastodon/pull/10028)) +- Fix unicode characters in URLs not being linkified ([JMendyk](https://github.com/mastodon/mastodon/pull/8447), [hinaloe](https://github.com/mastodon/mastodon/pull/9991)) +- Fix URLs linkifier grabbing ending quotation as part of the link ([Gargron](https://github.com/mastodon/mastodon/pull/9997)) +- Fix authorized applications page design ([rinsuki](https://github.com/mastodon/mastodon/pull/9969)) +- Fix custom emojis not showing up in share page emoji picker ([rinsuki](https://github.com/mastodon/mastodon/pull/9970)) +- Fix too liberal application of whitespace in toots ([trwnh](https://github.com/mastodon/mastodon/pull/9968)) +- Fix misleading e-mail hint being displayed in admin view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9973)) +- Fix tombstones not being cleared out ([abcang](https://github.com/mastodon/mastodon/pull/9978)) +- Fix some timeline jumps ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9982), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10001), [rinsuki](https://github.com/mastodon/mastodon/pull/10046)) +- Fix content warning input taking keyboard focus even when hidden ([hinaloe](https://github.com/mastodon/mastodon/pull/10017)) +- Fix hashtags select styling in default and high-contrast themes ([Gargron](https://github.com/mastodon/mastodon/pull/10029)) +- Fix style regressions on landing page ([Gargron](https://github.com/mastodon/mastodon/pull/10030)) +- Fix hashtag column not subscribing to stream on mount ([Gargron](https://github.com/mastodon/mastodon/pull/10040)) +- Fix relay enabling/disabling not resetting inbox availability status ([Gargron](https://github.com/mastodon/mastodon/pull/10048)) +- Fix mutes, blocks, domain blocks and follow requests not paginating ([Gargron](https://github.com/mastodon/mastodon/pull/10057)) +- Fix crash on public hashtag pages when streaming fails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10061)) + +### Changed + +- Change icon for unlisted visibility level ([clarcharr](https://github.com/mastodon/mastodon/pull/9952)) +- Change queue of actor deletes from push to pull for non-follower recipients ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10016)) +- Change robots.txt to exclude media proxy URLs ([nightpool](https://github.com/mastodon/mastodon/pull/10038)) +- Change upload description input to allow line breaks ([BenLubar](https://github.com/mastodon/mastodon/pull/10036)) +- Change `dist/mastodon-streaming.service` to recommend running node without intermediary npm command ([nolanlawson](https://github.com/mastodon/mastodon/pull/10032)) +- Change conversations to always show names of other participants ([Gargron](https://github.com/mastodon/mastodon/pull/10047)) +- Change buttons on timeline preview to open the interaction dialog ([Gargron](https://github.com/mastodon/mastodon/pull/10054)) +- Change error graphic to hover-to-play ([Gargron](https://github.com/mastodon/mastodon/pull/10055)) + +## [2.7.1] - 2019-01-28 + +### Fixed + +- Fix SSO authentication not working due to missing agreement boolean ([Gargron](https://github.com/mastodon/mastodon/pull/9915)) +- Fix slow fallback of CopyAccountStats migration setting stats to 0 ([Gargron](https://github.com/mastodon/mastodon/pull/9930)) +- Fix wrong command in migration error message ([angristan](https://github.com/mastodon/mastodon/pull/9877)) +- Fix initial value of volume slider in video player and handle volume changes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9929)) +- Fix missing hotkeys for notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9927)) +- Fix being able to attach unattached media created by other users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9921)) +- Fix unrescued SSL error during link verification ([renatolond](https://github.com/mastodon/mastodon/pull/9914)) +- Fix Firefox scrollbar color regression ([trwnh](https://github.com/mastodon/mastodon/pull/9908)) +- Fix scheduled status with media immediately creating a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9894)) +- Fix missing strong style for landing page description ([Kjwon15](https://github.com/mastodon/mastodon/pull/9892)) + +## [2.7.0] - 2019-01-20 + +### Added + +- Add link for adding a user to a list from their profile ([namelessGonbai](https://github.com/mastodon/mastodon/pull/9062)) +- Add joining several hashtags in a single column ([gdpelican](https://github.com/mastodon/mastodon/pull/8904)) +- Add volume sliders for videos ([sumdog](https://github.com/mastodon/mastodon/pull/9366)) +- Add a tooltip explaining what a locked account is ([pawelngei](https://github.com/mastodon/mastodon/pull/9403)) +- Add preloaded cache for common JSON-LD contexts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9412)) +- Add profile directory ([Gargron](https://github.com/mastodon/mastodon/pull/9427)) +- Add setting to not group reblogs in home feed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9248)) +- Add admin ability to remove a user's header image ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9495)) +- Add account hashtags to ActivityPub actor JSON ([Gargron](https://github.com/mastodon/mastodon/pull/9450)) +- Add error message for avatar image that's too large ([sumdog](https://github.com/mastodon/mastodon/pull/9518)) +- Add notification quick-filter bar ([pawelngei](https://github.com/mastodon/mastodon/pull/9399)) +- Add new first-time tutorial ([Gargron](https://github.com/mastodon/mastodon/pull/9531)) +- Add moderation warnings ([Gargron](https://github.com/mastodon/mastodon/pull/9519)) +- Add emoji codepoint mappings for v11.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9618)) +- Add REST API for creating an account ([Gargron](https://github.com/mastodon/mastodon/pull/9572)) +- Add support for Malayalam in language filter ([tachyons](https://github.com/mastodon/mastodon/pull/9624)) +- Add exclude_reblogs option to account statuses API ([Gargron](https://github.com/mastodon/mastodon/pull/9640)) +- Add local followers page to admin account UI ([chr-1x](https://github.com/mastodon/mastodon/pull/9610)) +- Add healthcheck commands to docker-compose.yml ([BenLubar](https://github.com/mastodon/mastodon/pull/9143)) +- Add handler for Move activity to migrate followers ([Gargron](https://github.com/mastodon/mastodon/pull/9629)) +- Add CSV export for lists and domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/9677)) +- Add `tootctl accounts follow ACCT` ([Gargron](https://github.com/mastodon/mastodon/pull/9414)) +- Add scheduled statuses ([Gargron](https://github.com/mastodon/mastodon/pull/9706)) +- Add immutable caching for S3 objects ([nolanlawson](https://github.com/mastodon/mastodon/pull/9722)) +- Add cache to custom emojis API ([Gargron](https://github.com/mastodon/mastodon/pull/9732)) +- Add preview cards to non-detailed statuses on public pages ([Gargron](https://github.com/mastodon/mastodon/pull/9714)) +- Add `mod` and `moderator` to list of default reserved usernames ([Gargron](https://github.com/mastodon/mastodon/pull/9713)) +- Add quick links to the admin interface in the web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8545)) +- Add `tootctl domains crawl` ([Gargron](https://github.com/mastodon/mastodon/pull/9809)) +- Add attachment list fallback to public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9780)) +- Add `tootctl --version` ([Gargron](https://github.com/mastodon/mastodon/pull/9835)) +- Add information about how to opt-in to the directory on the directory ([Gargron](https://github.com/mastodon/mastodon/pull/9834)) +- Add timeouts for S3 ([Gargron](https://github.com/mastodon/mastodon/pull/9842)) +- Add support for non-public reblogs from ActivityPub ([Gargron](https://github.com/mastodon/mastodon/pull/9841)) +- Add sending of `Reject` activity when sending a `Block` activity ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9811)) + +### Changed + +- Temporarily pause timeline if mouse moved recently ([lmorchard](https://github.com/mastodon/mastodon/pull/9200)) +- Change the password form order ([mayaeh](https://github.com/mastodon/mastodon/pull/9267)) +- Redesign admin UI for accounts ([Gargron](https://github.com/mastodon/mastodon/pull/9340), [Gargron](https://github.com/mastodon/mastodon/pull/9643)) +- Redesign admin UI for instances/domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/9645)) +- Swap avatar and header input fields in profile page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9271)) +- When posting in mobile mode, go back to previous history location ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9502)) +- Split out is_changing_upload from is_submitting ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9536)) +- Back to the getting-started when pins the timeline. ([kedamaDQ](https://github.com/mastodon/mastodon/pull/9561)) +- Allow unauthenticated REST API access to GET /api/v1/accounts/:id/statuses ([Gargron](https://github.com/mastodon/mastodon/pull/9573)) +- Limit maximum visibility of local silenced users to unlisted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9583)) +- Change API error message for unconfirmed accounts ([noellabo](https://github.com/mastodon/mastodon/pull/9625)) +- Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/mastodon/mastodon/pull/9378)) +- Do not ignore federated reports targeting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534)) +- Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9688)) +- Change e-mail digest frequency ([Gargron](https://github.com/mastodon/mastodon/pull/9689)) +- Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/mastodon/mastodon/pull/9438)) +- Display fallback link card thumbnail when none is given ([Gargron](https://github.com/mastodon/mastodon/pull/9715)) +- Change account bio length validation to ignore mention domains and URLs ([Gargron](https://github.com/mastodon/mastodon/pull/9717)) +- Use configured contact user for "anonymous" federation activities ([yukimochi](https://github.com/mastodon/mastodon/pull/9661)) +- Change remote interaction dialog to use specific actions instead of generic "interact" ([Gargron](https://github.com/mastodon/mastodon/pull/9743)) +- Always re-fetch public key when signature verification fails to support blind key rotation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9667)) +- Make replies to boosts impossible, connect reply to original status instead ([valerauko](https://github.com/mastodon/mastodon/pull/9129)) +- Change e-mail MX validation to check both A and MX records against blacklist ([Gargron](https://github.com/mastodon/mastodon/pull/9489)) +- Hide floating action button on search and getting started pages ([tmm576](https://github.com/mastodon/mastodon/pull/9826)) +- Redesign public hashtag page to use a masonry layout ([Gargron](https://github.com/mastodon/mastodon/pull/9822)) +- Use `summary` as summary instead of content warning for converted ActivityPub objects ([Gargron](https://github.com/mastodon/mastodon/pull/9823)) +- Display a double reply arrow on public pages for toots that are replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9808)) +- Change admin UI right panel size to be wider ([Kjwon15](https://github.com/mastodon/mastodon/pull/9768)) + +### Removed + +- Remove links to bridge.joinmastodon.org (non-functional) ([Gargron](https://github.com/mastodon/mastodon/pull/9608)) +- Remove LD-Signatures from activities that do not need them ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9659)) + +### Fixed + +- Remove unused computation of reblog references from updateTimeline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9244)) +- Fix loaded embeds resetting if a status arrives from API again ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9270)) +- Fix race condition causing shallow status with only a "favourited" attribute ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9272)) +- Remove intermediary arrays when creating hash maps from results ([Gargron](https://github.com/mastodon/mastodon/pull/9291)) +- Extract counters from accounts table to account_stats table to improve performance ([Gargron](https://github.com/mastodon/mastodon/pull/9295)) +- Change identities id column to a bigint ([Gargron](https://github.com/mastodon/mastodon/pull/9371)) +- Fix conversations API pagination ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9407)) +- Improve account suspension speed and completeness ([Gargron](https://github.com/mastodon/mastodon/pull/9290)) +- Fix thread depth computation in statuses_controller ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9426)) +- Fix database deadlocks by moving account stats update outside transaction ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9437)) +- Escape HTML in profile name preview in profile settings ([pawelngei](https://github.com/mastodon/mastodon/pull/9446)) +- Use same CORS policy for /@:username and /users/:username ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9485)) +- Make custom emoji domains case insensitive ([Esteth](https://github.com/mastodon/mastodon/pull/9474)) +- Various fixes to scrollable lists and media gallery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9501)) +- Fix bootsnap cache directory being declared relatively ([Gargron](https://github.com/mastodon/mastodon/pull/9511)) +- Fix timeline pagination in the web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9516)) +- Fix padding on dropdown elements in preferences ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9517)) +- Make avatar and headers respect GIF autoplay settings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9515)) +- Do no retry Web Push workers if the server returns a 4xx response ([Gargron](https://github.com/mastodon/mastodon/pull/9434)) +- Minor scrollable list fixes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9551)) +- Ignore low-confidence CharlockHolmes guesses when parsing link cards ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9510)) +- Fix `tootctl accounts rotate` not updating public keys ([Gargron](https://github.com/mastodon/mastodon/pull/9556)) +- Fix CSP / X-Frame-Options for media players ([jomo](https://github.com/mastodon/mastodon/pull/9558)) +- Fix unnecessary loadMore calls when the end of a timeline has been reached ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9581)) +- Skip mailer job retries when a record no longer exists ([Gargron](https://github.com/mastodon/mastodon/pull/9590)) +- Fix composer not getting focus after reply confirmation dialog ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9602)) +- Fix signature verification stoplight triggering on non-timeout errors ([Gargron](https://github.com/mastodon/mastodon/pull/9617)) +- Fix ThreadResolveWorker getting queued with invalid URLs ([Gargron](https://github.com/mastodon/mastodon/pull/9628)) +- Fix crash when clearing uninitialized timeline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9662)) +- Avoid duplicate work by merging ReplyDistributionWorker into DistributionWorker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9660)) +- Skip full text search if it fails, instead of erroring out completely ([Kjwon15](https://github.com/mastodon/mastodon/pull/9654)) +- Fix profile metadata links not verifying correctly sometimes ([shrft](https://github.com/mastodon/mastodon/pull/9673)) +- Ensure blocked user unfollows blocker if Block/Undo-Block activities are processed out of order ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9687)) +- Fix unreadable text color in report modal for some statuses ([Gargron](https://github.com/mastodon/mastodon/pull/9716)) +- Stop GIFV timeline preview explicitly when it's opened in modal ([kedamaDQ](https://github.com/mastodon/mastodon/pull/9749)) +- Fix scrollbar width compensation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9824)) +- Fix race conditions when processing deleted toots ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9815)) +- Fix SSO issues on WebKit browsers by disabling Same-Site cookie again ([moritzheiber](https://github.com/mastodon/mastodon/pull/9819)) +- Fix empty OEmbed error ([renatolond](https://github.com/mastodon/mastodon/pull/9807)) +- Fix drag & drop modal not disappearing sometimes ([hinaloe](https://github.com/mastodon/mastodon/pull/9797)) +- Fix statuses with content warnings being displayed in web push notifications sometimes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9778)) +- Fix scroll-to-detailed status not working on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9773)) +- Fix media modal loading indicator ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9771)) +- Fix hashtag search results not having a permalink fallback in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9810)) +- Fix slightly cropped font on settings page dropdowns when using system font ([ariasuni](https://github.com/mastodon/mastodon/pull/9839)) +- Fix not being able to drag & drop text into forms ([tmm576](https://github.com/mastodon/mastodon/pull/9840)) + +### Security + +- Sanitize and sandbox toot embeds in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9552)) +- Add tombstones for remote statuses to prevent replay attacks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9830)) + +## [2.6.5] - 2018-12-01 + +### Changed + +- Change lists to display replies to others on the list and list owner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9324)) + +### Fixed + +- Fix failures caused by commonly-used JSON-LD contexts being unavailable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9412)) + +## [2.6.4] - 2018-11-30 + +### Fixed + +- Fix yarn dependencies not installing due to yanked event-stream package ([Gargron](https://github.com/mastodon/mastodon/pull/9401)) + +## [2.6.3] - 2018-11-30 + +### Added + +- Add hyphen to characters allowed in remote usernames ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9345)) + +### Changed + +- Change server user count to exclude suspended accounts ([Gargron](https://github.com/mastodon/mastodon/pull/9380)) + +### Fixed + +- Fix ffmpeg processing sometimes stalling due to overfilled stdout buffer ([hugogameiro](https://github.com/mastodon/mastodon/pull/9368)) +- Fix missing DNS records raising the wrong kind of exception ([Gargron](https://github.com/mastodon/mastodon/pull/9379)) +- Fix already queued deliveries still trying to reach inboxes marked as unavailable ([Gargron](https://github.com/mastodon/mastodon/pull/9358)) + +### Security + +- Fix TLS handshake timeout not being enforced ([Gargron](https://github.com/mastodon/mastodon/pull/9381)) + +## [2.6.2] - 2018-11-23 + +### Added + +- Add Page to whitelisted ActivityPub types ([mbajur](https://github.com/mastodon/mastodon/pull/9188)) +- Add 20px to column width in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/9227)) +- Add amount of freed disk space in `tootctl media remove` ([Gargron](https://github.com/mastodon/mastodon/pull/9229), [Gargron](https://github.com/mastodon/mastodon/pull/9239), [mayaeh](https://github.com/mastodon/mastodon/pull/9288)) +- Add "Show thread" link to self-replies ([Gargron](https://github.com/mastodon/mastodon/pull/9228)) + +### Changed + +- Change order of Atom and RSS links so Atom is first ([Alkarex](https://github.com/mastodon/mastodon/pull/9302)) +- Change Nginx configuration for Nanobox apps ([danhunsaker](https://github.com/mastodon/mastodon/pull/9310)) +- Change the follow action to appear instant in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/9220)) +- Change how the ActiveRecord connection is instantiated in on_worker_boot ([Gargron](https://github.com/mastodon/mastodon/pull/9238)) +- Change `tootctl accounts cull` to always touch accounts so they can be skipped ([renatolond](https://github.com/mastodon/mastodon/pull/9293)) +- Change mime type comparison to ignore JSON-LD profile ([valerauko](https://github.com/mastodon/mastodon/pull/9179)) + +### Fixed + +- Fix web UI crash when conversation has no last status ([sammy8806](https://github.com/mastodon/mastodon/pull/9207)) +- Fix follow limit validator reporting lower number past threshold ([Gargron](https://github.com/mastodon/mastodon/pull/9230)) +- Fix form validation flash message color and input borders ([Gargron](https://github.com/mastodon/mastodon/pull/9235)) +- Fix invalid twitter:player cards being displayed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9254)) +- Fix emoji update date being processed incorrectly ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9255)) +- Fix playing embed resetting if status is reloaded in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9270), [Gargron](https://github.com/mastodon/mastodon/pull/9275)) +- Fix web UI crash when favouriting a deleted status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9272)) +- Fix intermediary arrays being created for hash maps ([Gargron](https://github.com/mastodon/mastodon/pull/9291)) +- Fix filter ID not being a string in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/9303)) + +### Security + +- Fix multiple remote account deletions being able to deadlock the database ([Gargron](https://github.com/mastodon/mastodon/pull/9292)) +- Fix HTTP connection timeout of 10s not being enforced ([Gargron](https://github.com/mastodon/mastodon/pull/9329)) + +## [2.6.1] - 2018-10-30 + +### Fixed + +- Fix resolving resources by URL not working due to a regression in [valerauko](https://github.com/mastodon/mastodon/pull/9132) ([Gargron](https://github.com/mastodon/mastodon/pull/9171)) +- Fix reducer error in web UI when a conversation has no last status ([Gargron](https://github.com/mastodon/mastodon/pull/9173)) + +## [2.6.0] - 2018-10-30 + +### Added + +- Add link ownership verification ([Gargron](https://github.com/mastodon/mastodon/pull/8703)) +- Add conversations API ([Gargron](https://github.com/mastodon/mastodon/pull/8832)) +- Add limit for the number of people that can be followed from one account ([Gargron](https://github.com/mastodon/mastodon/pull/8807)) +- Add admin setting to customize mascot ([ashleyhull-versent](https://github.com/mastodon/mastodon/pull/8766)) +- Add support for more granular ActivityPub audiences from other software, i.e. circles ([Gargron](https://github.com/mastodon/mastodon/pull/8950), [Gargron](https://github.com/mastodon/mastodon/pull/9093), [Gargron](https://github.com/mastodon/mastodon/pull/9150)) +- Add option to block all reports from a domain ([Gargron](https://github.com/mastodon/mastodon/pull/8830)) +- Add user preference to always expand toots marked with content warnings ([webroo](https://github.com/mastodon/mastodon/pull/8762)) +- Add user preference to always hide all media ([fvh-P](https://github.com/mastodon/mastodon/pull/8569)) +- Add `force_login` param to OAuth authorize page ([Gargron](https://github.com/mastodon/mastodon/pull/8655)) +- Add `tootctl accounts backup` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl accounts create` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl accounts cull` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl accounts delete` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl accounts modify` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl accounts refresh` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl feeds build` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl feeds clear` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl settings registrations open` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `tootctl settings registrations close` ([Gargron](https://github.com/mastodon/mastodon/pull/8642), [Gargron](https://github.com/mastodon/mastodon/pull/8811)) +- Add `min_id` param to REST API to support backwards pagination ([Gargron](https://github.com/mastodon/mastodon/pull/8736)) +- Add a confirmation dialog when hitting reply and the compose box isn't empty ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8893)) +- Add PostgreSQL disk space growth tracking in PGHero ([Gargron](https://github.com/mastodon/mastodon/pull/8906)) +- Add button for disabling local account to report quick actions bar ([Gargron](https://github.com/mastodon/mastodon/pull/9024)) +- Add Czech language ([Aditoo17](https://github.com/mastodon/mastodon/pull/8594)) +- Add `same-site` (`lax`) attribute to cookies ([sorin-davidoi](https://github.com/mastodon/mastodon/pull/8626)) +- Add support for styled scrollbars in Firefox Nightly ([sorin-davidoi](https://github.com/mastodon/mastodon/pull/8653)) +- Add highlight to the active tab in web UI profiles ([rhoio](https://github.com/mastodon/mastodon/pull/8673)) +- Add auto-focus for comment textarea in report modal ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8689)) +- Add auto-focus for emoji picker's search field ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8688)) +- Add nginx and systemd templates to `dist/` directory ([Gargron](https://github.com/mastodon/mastodon/pull/8770)) +- Add support for `/.well-known/change-password` ([Gargron](https://github.com/mastodon/mastodon/pull/8828)) +- Add option to override FFMPEG binary path ([sascha-sl](https://github.com/mastodon/mastodon/pull/8855)) +- Add `dns-prefetch` tag when using different host for assets or uploads ([Gargron](https://github.com/mastodon/mastodon/pull/8942)) +- Add `description` meta tag ([Gargron](https://github.com/mastodon/mastodon/pull/8941)) +- Add `Content-Security-Policy` header ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8957)) +- Add cache for the instance info API ([ykzts](https://github.com/mastodon/mastodon/pull/8765)) +- Add suggested follows to search screen in mobile layout ([Gargron](https://github.com/mastodon/mastodon/pull/9010)) +- Add CORS header to `/.well-known/*` routes ([BenLubar](https://github.com/mastodon/mastodon/pull/9083)) +- Add `card` attribute to statuses returned from REST API ([Gargron](https://github.com/mastodon/mastodon/pull/9120)) +- Add in-stream link preview ([Gargron](https://github.com/mastodon/mastodon/pull/9120)) +- Add support for ActivityPub `Page` objects ([mbajur](https://github.com/mastodon/mastodon/pull/9121)) + +### Changed + +- Change forms design ([Gargron](https://github.com/mastodon/mastodon/pull/8703)) +- Change reports overview to group by target account ([Gargron](https://github.com/mastodon/mastodon/pull/8674)) +- Change web UI to show "read more" link on overly long in-stream statuses ([lanodan](https://github.com/mastodon/mastodon/pull/8205)) +- Change design of direct messages column ([Gargron](https://github.com/mastodon/mastodon/pull/8832), [Gargron](https://github.com/mastodon/mastodon/pull/9022)) +- Change home timelines to exclude DMs ([Gargron](https://github.com/mastodon/mastodon/pull/8940)) +- Change list timelines to exclude all replies ([cbayerlein](https://github.com/mastodon/mastodon/pull/8683)) +- Change admin accounts UI default sort to most recent ([Gargron](https://github.com/mastodon/mastodon/pull/8813)) +- Change documentation URL in the UI ([Gargron](https://github.com/mastodon/mastodon/pull/8898)) +- Change style of success and failure messages ([Gargron](https://github.com/mastodon/mastodon/pull/8973)) +- Change DM filtering to always allow DMs from staff ([qguv](https://github.com/mastodon/mastodon/pull/8993)) +- Change recommended Ruby version to 2.5.3 ([zunda](https://github.com/mastodon/mastodon/pull/9003)) +- Change docker-compose default to persist volumes in current directory ([Gargron](https://github.com/mastodon/mastodon/pull/9055)) +- Change character counters on edit profile page to input length limit ([Gargron](https://github.com/mastodon/mastodon/pull/9100)) +- Change notification filtering to always let through messages from staff ([Gargron](https://github.com/mastodon/mastodon/pull/9152)) +- Change "hide boosts from user" function also hiding notifications about boosts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9147)) +- Change CSS `detailed-status__wrapper` class actually wrap the detailed status ([trwnh](https://github.com/mastodon/mastodon/pull/8547)) + +### Deprecated + +- `GET /api/v1/timelines/direct` → `GET /api/v1/conversations` ([Gargron](https://github.com/mastodon/mastodon/pull/8832)) +- `POST /api/v1/notifications/dismiss` → `POST /api/v1/notifications/:id/dismiss` ([Gargron](https://github.com/mastodon/mastodon/pull/8905)) +- `GET /api/v1/statuses/:id/card` → `card` attributed included in status ([Gargron](https://github.com/mastodon/mastodon/pull/9120)) + +### Removed + +- Remove "on this device" label in column push settings ([rhoio](https://github.com/mastodon/mastodon/pull/8704)) +- Remove rake tasks in favour of tootctl commands ([Gargron](https://github.com/mastodon/mastodon/pull/8675)) + +### Fixed + +- Fix remote statuses using instance's default locale if no language given ([Kjwon15](https://github.com/mastodon/mastodon/pull/8861)) +- Fix streaming API not exiting when port or socket is unavailable ([Gargron](https://github.com/mastodon/mastodon/pull/9023)) +- Fix network calls being performed in database transaction in ActivityPub handler ([Gargron](https://github.com/mastodon/mastodon/pull/8951)) +- Fix dropdown arrow position ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8637)) +- Fix first element of dropdowns being focused even if not using keyboard ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8679)) +- Fix tootctl requiring `bundle exec` invocation ([abcang](https://github.com/mastodon/mastodon/pull/8619)) +- Fix public pages not using animation preference for avatars ([renatolond](https://github.com/mastodon/mastodon/pull/8614)) +- Fix OEmbed/OpenGraph cards not understanding relative URLs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8669)) +- Fix some dark emojis not having a white outline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8597)) +- Fix media description not being displayed in various media modals ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8678)) +- Fix generated URLs of desktop notifications missing base URL ([GenbuHase](https://github.com/mastodon/mastodon/pull/8758)) +- Fix RTL styles ([mabkenar](https://github.com/mastodon/mastodon/pull/8764), [mabkenar](https://github.com/mastodon/mastodon/pull/8767), [mabkenar](https://github.com/mastodon/mastodon/pull/8823), [mabkenar](https://github.com/mastodon/mastodon/pull/8897), [mabkenar](https://github.com/mastodon/mastodon/pull/9005), [mabkenar](https://github.com/mastodon/mastodon/pull/9007), [mabkenar](https://github.com/mastodon/mastodon/pull/9018), [mabkenar](https://github.com/mastodon/mastodon/pull/9021), [mabkenar](https://github.com/mastodon/mastodon/pull/9145), [mabkenar](https://github.com/mastodon/mastodon/pull/9146)) +- Fix crash in streaming API when tag param missing ([Gargron](https://github.com/mastodon/mastodon/pull/8955)) +- Fix hotkeys not working when no element is focused ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8998)) +- Fix some hotkeys not working on detailed status view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9006)) +- Fix og:url on status pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9047)) +- Fix upload option buttons only being visible on hover ([Gargron](https://github.com/mastodon/mastodon/pull/9074)) +- Fix tootctl not returning exit code 1 on wrong arguments ([sascha-sl](https://github.com/mastodon/mastodon/pull/9094)) +- Fix preview cards for appearing for profiles mentioned in toot ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/6934), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/9158)) +- Fix local accounts sometimes being duplicated as faux-remote ([Gargron](https://github.com/mastodon/mastodon/pull/9109)) +- Fix emoji search when the shortcode has multiple separators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9124)) +- Fix dropdowns sometimes being partially obscured by other elements ([kedamaDQ](https://github.com/mastodon/mastodon/pull/9126)) +- Fix cache not updating when reply/boost/favourite counters or media sensitivity update ([Gargron](https://github.com/mastodon/mastodon/pull/9119)) +- Fix empty display name precedence over username in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/9163)) +- Fix td instead of th in sessions table header ([Gargron](https://github.com/mastodon/mastodon/pull/9162)) +- Fix handling of content types with profile ([valerauko](https://github.com/mastodon/mastodon/pull/9132)) + +## [2.5.2] - 2018-10-12 + +### Security + +- Fix XSS vulnerability ([Gargron](https://github.com/mastodon/mastodon/pull/8959)) + +## [2.5.1] - 2018-10-07 + +### Fixed + +- Fix database migrations for PostgreSQL below 9.5 ([Gargron](https://github.com/mastodon/mastodon/pull/8903)) +- Fix class autoloading issue in ActivityPub Create handler ([Gargron](https://github.com/mastodon/mastodon/pull/8820)) +- Fix cache statistics not being sent via statsd when statsd enabled ([ykzts](https://github.com/mastodon/mastodon/pull/8831)) +- Bump puma from 3.11.4 to 3.12.0 ([dependabot[bot]](https://github.com/mastodon/mastodon/pull/8883)) + +### Security + +- Fix some local images not having their EXIF metadata stripped on upload ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8714)) +- Fix being able to enable a disabled relay via ActivityPub Accept handler ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8864)) +- Bump nokogiri from 1.8.4 to 1.8.5 ([dependabot[bot]](https://github.com/mastodon/mastodon/pull/8881)) +- Fix being able to report statuses not belonging to the reported account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/8916)) diff --git a/CHANGELOG_KB.md b/CHANGELOG_KB.md new file mode 100644 index 0000000000..37c59ee93a --- /dev/null +++ b/CHANGELOG_KB.md @@ -0,0 +1,43 @@ +# kmyblue ã®å¤‰æ›´å±¥æ­´ + +## 4.2.0 kb-RC1 - 2023/5/23 + +### 追加 + +- カスタム絵文字ã®`isSensitive`値ã®ã‚µãƒãƒ¼ãƒˆ (from Misskey) +- ã‚«ã‚¹ã‚¿ãƒ çµµæ–‡å­—ã®æ¤œç´¢ç”¨ã®ã‚¨ã‚¤ãƒªã‚¢ã‚¹ã‚­ãƒ¼ãƒ¯ãƒ¼ãƒ‰ +- アンテナ㮠STL(ソーシャルタイムライン)モード +- ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æŠ•ç¨¿æ•°ã€ãƒ•ォロー数ã€ãƒ•ォロワー数ã®éš è”½ +- @WEB - ブーストã•ã‚ŒãŸæŠ•ç¨¿ã«ãŠã„ã¦ã€ãƒ–ースト自体ã®å…¬é–‹ç¯„囲ã®è¡¨ç¤º +- @WEB - 投稿をブーストã™ã‚‹ã¨ãã€ãƒ¦ãƒ¼ã‚¶ãƒ¼è¨­å®šã«é–¢ã‚らãšå¸¸ã«ãƒ€ã‚¤ã‚¢ãƒ­ã‚°ã‚’表示ã™ã‚‹ãƒ¡ãƒ‹ãƒ¥ãƒ¼é …ç›® + - ダイアログã«ã‚ˆã‚Šãƒ–ースト自体ã®å…¬é–‹ç¯„å›²ãŒæŒ‡å®šå¯èƒ½ +- @WEB - 設定画é¢ã«ãŠã„ã¦ã€kmyblue 独自ã®è¨­å®šé …ç›®ã«ãƒžãƒ¼ã‚¯ +- ブースト時ã«å…¬é–‹ç¯„囲を指定ã—ã¦ã„ãªã‹ã£ãŸå ´åˆã€ãƒ‡ãƒ•ォルトã§é©ç”¨ã•れる公開範囲ã®è¨­å®š +- 投稿文章や画åƒã® AI 利用ã«å¯¾ã—ã¦ä¸å¿«æ„Ÿã‚’表明ã™ã‚‹è¨­å®š +- スタンプã®ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã‚’åœæ­¢ã™ã‚‹è¨­å®š +- Glitch-soc 互æ›ã‚¹ã‚¿ãƒ³ãƒ— API(※当該 PR ã¯ã¾ã  Glitch ã§å¯©æŸ»ä¸­ï¼‰ +- アンテナã§ãƒ–ーストを無視ã™ã‚‹è¨­å®š +- 投稿ã«ã¤ã‘られãŸã‚¹ã‚¿ãƒ³ãƒ—ç·æ•°ã®è¡¨ç¤º +- @WEB - ユーザーメニューã«ã‚¢ãƒ³ãƒ†ãƒŠã®é …ç›® +- 投稿自動削除機能ã«ã‚¹ã‚¿ãƒ³ãƒ—æ¡ä»¶æŒ‡å®š + +### 変更 + +- @WEB - 横長絵文字ã®çµµæ–‡å­—ピッカー内ã«ãŠã‘る表示スタイル +- ç”»åƒã®ãªã„投稿ã®ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–フラグã«ã¤ã„ã¦ã€æŒ‡å®šãŒãªã„å ´åˆã«é™ã‚Šå¸¸ã«`false` +- 「絵文字リアクションã€ã‚’「スタンプã€ã«æ”¹ç§° +- アカウントã®ãƒ•ã‚£ãƒ¼ãƒ«ãƒ‰ã®æœ€å¤§æ•°ã‚’ï¼”ã‹ã‚‰ï¼–ã«æ‹¡å¼µ +- Searchability API ã‚’ Fedibird 互æ›ã« +- @WEB - 表示å¯èƒ½ãªç”»åƒã®æœ€å¤§æ•°ã‚’8ã‹ã‚‰ï¼‘ï¼–ã«å¼•ã上㒠+- å…¨æ–‡æ¤œç´¢ã®æ™‚系列順表示を廃止 +- ç”»åƒæ·»ä»˜ã¨æŠ•票を一ã¤ã®æŠ•稿ã§å…±å­˜å¯èƒ½ +- @WEB - å³å´ã‚µã‚¤ãƒ‰ãƒ¡ãƒ‹ãƒ¥ãƒ¼ã«è¡¨ç¤ºã•れるリスト数を4ã‹ã‚‰ï¼˜ã«å¼•ã上㒠+- ãƒ›ãƒ¼ãƒ ãƒ»ãƒªã‚¹ãƒˆã®æŠ•ç¨¿ä¿æŒæ•°ã‚’8ï¼ï¼ã‹ã‚‰ï¼‘ï¼ï¼ï¼ã«å¼•ã上㒠+- @ADMIN - アカウントã®è¡¨ç¤ºåã€ID ã®æ­£è¦è¡¨ç¾æ¤œç´¢ + +### 修正 + +- ã‚¹ã‚¿ãƒ³ãƒ—ãŒæŠ•ç¨¿ã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã«å映・更新ã•れãªã„å•題 +- デãƒãƒƒã‚°æ™‚ã«ç™ºç”Ÿã™ã‚‹`outbox`ã®ä¸€éƒ¨ã‚¨ãƒ©ãƒ¼ +- スタンプを削除ã™ã‚‹ API ã«ãŠã„ã¦ã€API ã®æˆ»ã‚Šå€¤ã«å‰Šé™¤ã•れãŸã‚¹ã‚¿ãƒ³ãƒ—ãŒæ®‹ã£ã¦ã„ã‚‹å•題 +- 休眠ユーザーã«ã‚¢ãƒ³ãƒ†ãƒŠã®æŠ•稿ãŒé…ä¿¡ã•れるå•題 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43f8a79249..a6ed08590a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,17 +4,17 @@ kmyblueã¯ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã®æ„見もèžãã«ã¯èžãã¾ã™ãŒå°Žå…¥ã™ ## ãƒã‚°å ±å‘Š -ãƒã‚°ã«ã¤ã„ã¦ã€L最新よりもéŽåŽ»ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¸ã®å¯¾å¿œã¯ã€LTSや特別ãªå ´åˆä»¥å¤–ã¯è¡Œã„ã¾ã›ã‚“。 +ãƒã‚°ã«ã¤ã„ã¦ã€æœ€æ–°ã‚ˆã‚Šã‚‚éŽåŽ»ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¸ã®å¯¾å¿œã¯ç‰¹åˆ¥ãªå ´åˆä»¥å¤–ã¯è¡Œã„ã¾ã›ã‚“。 以下ã®ã„ãšã‚Œã‹ã®æ–¹æ³•ã§å ±å‘Šã—ã¦ãã ã•ã„。 -- [GitHub Issues](https://github.com/kmycode/mastodon/issues) (セキュリティインシデントã¯ã“ã“ã®ä¸€ç•ªä¸‹ã‹ã‚‰ï¼‰ +- [GitHub Issues](https://github.com/kmycode/mastodon/issues) - [kmyblue開発者ã¸ã®é€£çµ¡](https://kmy.blue/@askyq) - [kmyblue開発者ã¸ã®ãƒ¡ãƒ¼ãƒ«](https://kmy.blue/about) ## 翻訳ã€ãƒ—ルリクエスト -æ–°ã—ã„æ©Ÿèƒ½ã‚„既存機能ã®ä¿®æ­£ã«ã¤ã„ã¦ã¯ã€ãƒ—ルリクエストã®ãŸã‚ã«ã‚³ãƒ¼ãƒ‰ã‚’作æˆã™ã‚‹å‰ã«ã€ã¾ãšGitHub Issuesã§æ©Ÿèƒ½ã®ææ¡ˆã‚’行ã„kmyblue開発者ã®è€ƒãˆã‚’èžãã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚ãƒã‚°ä¿®æ­£ã€ç¿»è¨³ã€ãƒ†ã‚¹ãƒˆã‚³ãƒ¼ãƒ‰ãªã©ã¯åŸºæœ¬å—ã‘入れã¾ã™ãŒã€ä¾å­˜ãƒ¢ã‚¸ãƒ¥ãƒ¼ãƒ«ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚¢ãƒƒãƒ—ã«ã¤ã„ã¦ã¯ç‰¹åˆ¥ãªäº‹æƒ…ãŒãªã‘ã‚Œã°æœ¬å®¶Mastodonよりも先ã«è¡Œã‹ãªã„よã†ã«ã—ã¦ãã ã•ã„。 +æ–°ã—ã„æ©Ÿèƒ½ã‚„既存機能ã®ä¿®æ­£ã«ã¤ã„ã¦ã¯ã€ãƒ—ルリクエストã®ãŸã‚ã«ã‚³ãƒ¼ãƒ‰ã‚’作æˆã™ã‚‹å‰ã«ã€ã¾ãšGitHub Issuesã§æ©Ÿèƒ½ã®ææ¡ˆã‚’行ã„kmyblue開発者ã®è€ƒãˆã‚’èžãã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚ãƒã‚°ä¿®æ­£ã€ç¿»è¨³ã€ãƒ†ã‚¹ãƒˆã‚³ãƒ¼ãƒ‰ãªã©ã¯åŸºæœ¬å—ã‘入れã¾ã™ãŒã€ä¾å­˜ãƒ¢ã‚¸ãƒ¥ãƒ¼ãƒ«ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚¢ãƒƒãƒ—ã«ã¤ã„ã¦ã¯æœ¬å®¶Mastodonよりも先ã«è¡Œã‹ãªã„よã†ã«ã—ã¦ãã ã•ã„。 プルリクエストã®ã‚¿ã‚¤ãƒˆãƒ«ã«ã¯ã€ãƒ—ルリクエストã®å†…å®¹ãŒæ˜Žç¢ºã«ãªã‚‹ã‚ˆã†ãªã‚‚ã®ã‚’設定ã—ã¦ãã ã•ã„。 @@ -30,6 +30,7 @@ kmyblueã¯ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã®æ„見もèžãã«ã¯èžãã¾ã™ãŒå°Žå…¥ã™ kmyblueãŒæ„図的ã«å®Ÿè£…ã—ã¦ã„ãªã„機能ã¯ã€ä¾‹ãˆã°ä»¥ä¸‹ã®ã‚‚ã®ãŒã‚りã¾ã™ã€‚詳ã—ã„ç†ç”±ãŒçŸ¥ã‚ŠãŸã„å ´åˆã¯[ã“ã®è¨˜äº‹ã‚’å‚ç…§ã™ã‚‹ã‹](https://note.com/kmycode/n/n463410b5e03c)ã€åˆ¥é€”ãŠå•ã„åˆã‚ã›ãã ã•ã„。もã¡ã‚ã‚“æ˜Žç¢ºãªæ ¹æ‹ ãŒã‚ã‚‹å ´åˆã€ã‚ãªãŸã¯ã“ã‚Œã«æŠ—è­°ã™ã‚‹æ¨©åˆ©ã‚’有ã—ã¾ã™ãŒã€ã‚ãªãŸãŒã“ã®kmyblueをフォークã—ã¦æ–°ã—ã„リãƒã‚¸ãƒˆãƒªã‚’作るã»ã†ãŒã‚ˆã‚Šè‡ªç”±ã§ã—ょã†ã€‚ +- 引用機能 - ãŠæ°—ã«å…¥ã‚Šä¸€è¦§ã®å…¬é–‹ - ブックマーク分類ã®å…¬é–‹ -- Fedibirdã€Misskeyã«ã‚るよã†ãªè©³ç´°ãªç”»é¢è¡¨ç¤ºã‚ªãƒ—ション +- ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã®æŠ•稿ã«å¯¾ã—ã¦ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒè¡Œã£ãŸçµµæ–‡å­—リアクションã®å—ã‘入れ diff --git a/Capfile b/Capfile new file mode 100644 index 0000000000..86efa5bacf --- /dev/null +++ b/Capfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'capistrano/setup' +require 'capistrano/deploy' +require 'capistrano/scm/git' + +install_plugin Capistrano::SCM::Git + +require 'capistrano/rbenv' +require 'capistrano/bundler' +require 'capistrano/yarn' +require 'capistrano/rails/assets' +require 'capistrano/rails/migrations' + +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Dockerfile b/Dockerfile index 6620f4c096..980d705092 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,404 +1,105 @@ -# syntax=docker/dockerfile:1.12 +# syntax=docker/dockerfile:1.4 +# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim +ARG NODE_VERSION="20.6-bookworm-slim" -# This file is designed for production server deployment, not local development work -# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker +FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.3-slim as ruby +FROM node:${NODE_VERSION} as build -# Please see https://docs.docker.com/engine/reference/builder for information about -# the extended buildx capabilities used in this file. -# Make sure multiarch TARGETPLATFORM is available for interpolation -# See: https://docs.docker.com/build/building/multi-platform/ -ARG TARGETPLATFORM=${TARGETPLATFORM} -ARG BUILDPLATFORM=${BUILDPLATFORM} -ARG BASE_REGISTRY="docker.io" +COPY --link --from=ruby /opt/ruby /opt/ruby -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] -# renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.4.2" -# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] -# renovate: datasource=node-version depName=node -ARG NODE_MAJOR_VERSION="22" -# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] -ARG DEBIAN_VERSION="bookworm" -# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim) -FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node -# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm) -FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby +ENV DEBIAN_FRONTEND="noninteractive" \ + PATH="${PATH}:/opt/ruby/bin" -# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA -# Example: v4.3.0-nightly.2023.11.09+pr-123456 -# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +WORKDIR /opt/mastodon +COPY Gemfile* package.json yarn.lock /opt/mastodon/ + +# hadolint ignore=DL3008 +RUN apt-get update && \ + apt-get -yq dist-upgrade && \ + apt-get install -y --no-install-recommends build-essential \ + git \ + libicu-dev \ + libidn-dev \ + libpq-dev \ + libjemalloc-dev \ + zlib1g-dev \ + libgdbm-dev \ + libgmp-dev \ + libssl-dev \ + libyaml-0-2 \ + ca-certificates \ + libreadline8 \ + python3 \ + shared-mime-info && \ + bundle config set --local deployment 'true' && \ + bundle config set --local without 'development test' && \ + bundle config set silence_root_warning true && \ + bundle install -j"$(nproc)" && \ + yarn install --pure-lockfile --production --network-timeout 600000 && \ + yarn cache clean + +FROM node:${NODE_VERSION} + +# Use those args to specify your own version flags & suffixes ARG MASTODON_VERSION_PRERELEASE="" -# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"] ARG MASTODON_VERSION_METADATA="" -# Will be available as Mastodon::Version.source_commit -ARG SOURCE_COMMIT="" -# Allow Ruby on Rails to serve static files -# 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/v3_2_4/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" -# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234] ARG UID="991" -# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234] ARG GID="991" -# Apply Mastodon build options based on options above -ENV \ - # Apply Mastodon version information - MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ - MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \ - SOURCE_COMMIT="${SOURCE_COMMIT}" \ - # Apply Mastodon static files and YJIT options - RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \ - RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \ - # Apply timezone - TZ=${TZ} +COPY --link --from=ruby /opt/ruby /opt/ruby -ENV \ - # Configure the IP to bind Mastodon to when serving traffic - BIND="0.0.0.0" \ - # Use production settings for Yarn, Node.js and related tools - NODE_ENV="production" \ - # Use production settings for Ruby on Rails - RAILS_ENV="production" \ - # Add Ruby and Mastodon installation to the PATH - DEBIAN_FRONTEND="noninteractive" \ - PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ - # Optimize jemalloc 5.x performance - MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ - # Enable libvips, should not be changed - MASTODON_USE_LIBVIPS=true \ - # Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes - MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs +SHELL ["/bin/bash", "-o", "pipefail", "-c"] -# Set default shell used for running commands -SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"] +ENV DEBIAN_FRONTEND="noninteractive" \ + PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -ARG TARGETPLATFORM +# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# hadolint ignore=DL3008,DL3009 +RUN apt-get update && \ + echo "Etc/UTC" > /etc/localtime && \ + groupadd -g "${GID}" mastodon && \ + useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \ + apt-get -y --no-install-recommends install whois \ + wget \ + procps \ + libssl3 \ + libpq5 \ + imagemagick \ + ffmpeg \ + libjemalloc2 \ + libicu72 \ + libidn12 \ + libyaml-0-2 \ + file \ + ca-certificates \ + tzdata \ + libreadline8 \ + tini && \ + ln -s /opt/mastodon /mastodon -RUN echo "Target platform is $TARGETPLATFORM" +# Note: no, cleaning here since Debian does this automatically +# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem -RUN \ - # Remove automatic apt cache Docker cleanup scripts - rm -f /etc/apt/apt.conf.d/docker-clean; \ - # Sets timezone - echo "${TZ}" > /etc/localtime; \ - # Creates mastodon user/group and sets home directory - groupadd -g "${GID}" mastodon; \ - useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \ - # Creates /mastodon symlink to /opt/mastodon - ln -s /opt/mastodon /mastodon; +COPY --chown=mastodon:mastodon . /opt/mastodon +COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon -# Set /opt/mastodon as working directory +ENV RAILS_ENV="production" \ + NODE_ENV="production" \ + RAILS_SERVE_STATIC_FILES="true" \ + BIND="0.0.0.0" \ + MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ + MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" + +# Set the run user +USER mastodon WORKDIR /opt/mastodon -# Add backport repository for some specific packages where we need the latest version -RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list +# Precompile assets +RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile -# hadolint ignore=DL3008,DL3005 -RUN \ - # Mount Apt cache and lib directories from Docker buildx caches - --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ - --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ - # Apt update & upgrade to check for security updates to Debian image - apt-get update; \ - apt-get dist-upgrade -yq; \ - # Install jemalloc, curl and other necessary components - apt-get install -y --no-install-recommends \ - curl \ - file \ - libjemalloc2 \ - patchelf \ - procps \ - tini \ - tzdata \ - wget \ - ; \ - # Patch Ruby to use jemalloc - patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \ - # Discard patchelf after use - apt-get purge -y \ - patchelf \ - ; - -# Create temporary build layer from base image -FROM ruby AS build - -ARG TARGETPLATFORM - -# hadolint ignore=DL3008 -RUN \ - # Mount Apt cache and lib directories from Docker buildx caches - --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ - --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ - # Install build tools and bundler dependencies from APT - apt-get install -y --no-install-recommends \ - autoconf \ - automake \ - build-essential \ - cmake \ - git \ - libgdbm-dev \ - libglib2.0-dev \ - libgmp-dev \ - libicu-dev \ - libidn-dev \ - libpq-dev \ - libssl-dev \ - libtool \ - libyaml-dev \ - meson \ - nasm \ - pkg-config \ - shared-mime-info \ - xz-utils \ - # libvips components - libcgif-dev \ - libexif-dev \ - libexpat1-dev \ - libgirepository1.0-dev \ - libheif-dev/bookworm-backports \ - libimagequant-dev \ - libjpeg62-turbo-dev \ - liblcms2-dev \ - liborc-dev \ - libspng-dev \ - libtiff-dev \ - libwebp-dev \ - # ffmpeg components - libdav1d-dev \ - liblzma-dev \ - libmp3lame-dev \ - libopus-dev \ - libsnappy-dev \ - libvorbis-dev \ - libvpx-dev \ - libx264-dev \ - libx265-dev \ - ; - -# Create temporary libvips specific build layer from build layer -FROM build AS libvips - -# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] -# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips -ARG VIPS_VERSION=8.16.1 -# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] -ARG VIPS_URL=https://github.com/libvips/libvips/releases/download - -WORKDIR /usr/local/libvips/src -# Download and extract libvips source code -ADD ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz /usr/local/libvips/src/ -RUN tar xf vips-${VIPS_VERSION}.tar.xz; - -WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION} - -# Configure and compile libvips -RUN \ - meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \ - cd build; \ - ninja; \ - ninja install; - -# Create temporary ffmpeg specific build layer from build layer -FROM build AS ffmpeg - -# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] -# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg -ARG FFMPEG_VERSION=7.1 -# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] -ARG FFMPEG_URL=https://ffmpeg.org/releases - -WORKDIR /usr/local/ffmpeg/src -# Download and extract ffmpeg source code -ADD ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz /usr/local/ffmpeg/src/ -RUN tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz; - -WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION} - -# Configure and compile ffmpeg -RUN \ - ./configure \ - --prefix=/usr/local/ffmpeg \ - --toolchain=hardened \ - --disable-debug \ - --disable-devices \ - --disable-doc \ - --disable-ffplay \ - --disable-network \ - --disable-static \ - --enable-ffmpeg \ - --enable-ffprobe \ - --enable-gpl \ - --enable-libdav1d \ - --enable-libmp3lame \ - --enable-libopus \ - --enable-libsnappy \ - --enable-libvorbis \ - --enable-libvpx \ - --enable-libwebp \ - --enable-libx264 \ - --enable-libx265 \ - --enable-shared \ - --enable-version3 \ - ; \ - make -j$(nproc); \ - make install; - -# Create temporary bundler specific build layer from build layer -FROM build AS bundler - -ARG TARGETPLATFORM - -# Copy Gemfile config into working directory -COPY Gemfile* /opt/mastodon/ - -RUN \ - # Mount Ruby Gem caches - --mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \ - # Configure bundle to prevent changes to Gemfile and Gemfile.lock - bundle config set --global frozen "true"; \ - # Configure bundle to not cache downloaded Gems - bundle config set --global cache_all "false"; \ - # Configure bundle to only process production Gems - bundle config set --local without "development test"; \ - # Configure bundle to not warn about root user - bundle config set silence_root_warning "true"; \ - # Download and install required Gems - bundle install -j"$(nproc)"; - -# Create temporary assets build layer from build layer -FROM build AS precompiler - -ARG TARGETPLATFORM - -# Copy Mastodon sources into layer -COPY . /opt/mastodon/ - -# Copy Node.js binaries/libraries into layer -COPY --from=node /usr/local/bin /usr/local/bin -COPY --from=node /usr/local/lib /usr/local/lib - -RUN \ - # Configure Corepack - rm /usr/local/bin/yarn*; \ - corepack enable; \ - corepack prepare --activate; - -# hadolint ignore=DL3008 -RUN \ - --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ - --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ - # Install Node.js packages - yarn workspaces focus --production @mastodon/mastodon; - -# Copy libvips components into layer for precompiler -COPY --from=libvips /usr/local/libvips/bin /usr/local/bin -COPY --from=libvips /usr/local/libvips/lib /usr/local/lib -# Copy bundler packages into layer for precompiler -COPY --from=bundler /opt/mastodon /opt/mastodon/ -COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ - -RUN \ - ldconfig; \ - # Use Ruby on Rails to create Mastodon assets - SECRET_KEY_BASE_DUMMY=1 \ - bundle exec rails assets:precompile; \ - # Cleanup temporary files - rm -fr /opt/mastodon/tmp; - -# Prep final Mastodon Ruby layer -FROM ruby AS mastodon - -ARG TARGETPLATFORM - -# hadolint ignore=DL3008 -RUN \ - # Mount Apt cache and lib directories from Docker buildx caches - --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ - --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ - # Mount Corepack and Yarn caches from Docker buildx caches - --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ - --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ - # Apt update install non-dev versions of necessary components - apt-get install -y --no-install-recommends \ - libexpat1 \ - libglib2.0-0 \ - libicu72 \ - libidn12 \ - libpq5 \ - libreadline8 \ - libssl3 \ - libyaml-0-2 \ - # libvips components - libcgif0 \ - libexif12 \ - libheif1/bookworm-backports \ - libimagequant0 \ - libjpeg62-turbo \ - liblcms2-2 \ - liborc-0.4-0 \ - libspng0 \ - libtiff6 \ - libwebp7 \ - libwebpdemux2 \ - libwebpmux3 \ - # ffmpeg components - libdav1d6 \ - libmp3lame0 \ - libopencore-amrnb0 \ - libopencore-amrwb0 \ - libopus0 \ - libsnappy1v5 \ - libtheora0 \ - libvorbis0a \ - libvorbisenc2 \ - libvorbisfile3 \ - libvpx7 \ - libx264-164 \ - libx265-199 \ - ; - -# Copy Mastodon sources into final layer -COPY . /opt/mastodon/ - -# Copy compiled assets to layer -COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs -COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets -# Copy bundler components to layer -COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ -# Copy libvips components to layer -COPY --from=libvips /usr/local/libvips/bin /usr/local/bin -COPY --from=libvips /usr/local/libvips/lib /usr/local/lib -# Copy ffpmeg components to layer -COPY --from=ffmpeg /usr/local/ffmpeg/bin /usr/local/bin -COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib - -RUN \ - ldconfig; \ - # Smoketest media processors - vips -v; \ - ffmpeg -version; \ - ffprobe -version; - -RUN \ - # Precompile bootsnap code for faster Rails startup - bundle exec bootsnap precompile --gemfile app/ lib/; - -RUN \ - # Pre-create and chown system volume to Mastodon user - mkdir -p /opt/mastodon/public/system; \ - chown mastodon:mastodon /opt/mastodon/public/system; \ - # Set Mastodon user as owner of tmp folder - chown -R mastodon:mastodon /opt/mastodon/tmp; - -# Set the running user for resulting container -USER mastodon -# Expose default Puma ports -EXPOSE 3000 -# Set container tini as default entry point +# Set the work dir and the container entry point ENTRYPOINT ["/usr/bin/tini", "--"] +EXPOSE 3000 4000 diff --git a/FEDERATION.md b/FEDERATION.md index 2819fa935a..e3721d7241 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -1,35 +1,19 @@ -# Federation - -## Supported federation protocols and standards - -- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server) -- [WebFinger](https://webfinger.net/) -- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) -- [NodeInfo](https://nodeinfo.diaspora.software/) - -## Supported FEPs - -- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) -- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) -- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md) -- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) - -## ActivityPub in Mastodon +## ActivityPub federation in Mastodon Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all. -- [Supported ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/) +Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/ ### Required extensions -#### WebFinger +#### Webfinger In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`). This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings. As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger. -- [WebFinger information and examples](https://docs.joinmastodon.org/spec/webfinger/) +More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/ #### HTTP Signatures @@ -37,13 +21,11 @@ In order to authenticate activities, Mastodon relies on HTTP Signatures, signing Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server. -- [HTTP Signatures information and examples](https://docs.joinmastodon.org/spec/security/#http) +More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http ### Optional extensions -- [Linked-Data Signatures](https://docs.joinmastodon.org/spec/security/#ld) -- [Bearcaps](https://docs.joinmastodon.org/spec/bearcaps/) - -### Additional documentation - -- [Mastodon documentation](https://docs.joinmastodon.org/) +- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld +- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/ +- Followers collection synchronization: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md +- Search indexing consent for actors: https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md diff --git a/Gemfile b/Gemfile index 9e5955e0b8..78f8716f78 100644 --- a/Gemfile +++ b/Gemfile @@ -1,36 +1,34 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 3.2.0', '< 3.5.0' +ruby '>= 3.0.0' -gem 'propshaft' gem 'puma', '~> 6.3' -gem 'rack', '~> 2.2.7' -gem 'rails', '~> 8.0' +gem 'rails', '~> 7.0' +gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' +gem 'rack', '~> 2.2.7' -gem 'dotenv' gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' gem 'pghero' +gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873 gem 'aws-sdk-s3', '~> 1.123', require: false -gem 'blurhash', '~> 0.1' -gem 'fog-core', '<= 2.6.0' -gem 'fog-openstack', '~> 1.0', require: false -gem 'jd-paperclip-azure', '~> 3.0', require: false +gem 'fog-core', '<= 2.4.0' +gem 'fog-openstack', '~> 0.3', require: false gem 'kt-paperclip', '~> 7.2' -gem 'ruby-vips', '~> 2.2', require: false +gem 'md-paperclip-azure', '~> 2.2', require: false +gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' -gem 'bootsnap', '~> 1.18.0', require: false +gem 'bootsnap', '~> 1.16.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' -gem 'devise-two-factor' +gem 'devise-two-factor', '~> 4.1' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' @@ -38,104 +36,80 @@ end gem 'net-ldap', '~> 0.18' -gem 'omniauth', '~> 2.0' -gem 'omniauth-cas', '~> 3.0.0.beta.1' -gem 'omniauth_openid_connect', '~> 0.8.0' -gem 'omniauth-rails_csrf_protection', '~> 1.0' +# TODO: Point back at released omniauth-cas gem when PR merged +# https://github.com/dlindahl/omniauth-cas/pull/68 +gem 'omniauth-cas', github: 'stanhu/omniauth-cas', ref: '4211e6d05941b4a981f9a36b49ec166cecd0e271' gem 'omniauth-saml', '~> 2.0' +gem 'omniauth_openid_connect', '~> 0.6.1' +gem 'omniauth', '~> 2.0' +gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'color_diff', '~> 0.1' -gem 'csv', '~> 3.2' gem 'discard', '~> 1.2' gem 'doorkeeper', '~> 5.6' -gem 'faraday-httpclient' +gem 'ed25519', '~> 1.3' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'hiredis', '~> 0.6' +gem 'redis-namespace', '~> 1.10' gem 'htmlentities', '~> 4.3' -gem 'http', '~> 5.2.0' +gem 'http', '~> 5.1' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.7.0', require: false -gem 'i18n' +gem 'httplog', '~> 1.6.2' gem 'idn-ruby', require: 'idn' -gem 'inline_svg' -gem 'irb', '~> 1.8' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' -gem 'linzer', '~> 0.6.1' -gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar' -gem 'mutex_m' +gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' +gem 'nsa' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' -gem 'premailer-rails' -gem 'public_suffix', '~> 6.0' +gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' +gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 8.0' +gem 'rails-i18n', '~> 7.0' +gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] -gem 'redis-namespace', '~> 1.10' +gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' -gem 'sanitize', '~> 7.0' +gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' -gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-scheduler', '~> 5.0' gem 'sidekiq-unique-jobs', '~> 7.1' -gem 'simple_form', '~> 5.2' +gem 'sidekiq-bulk', '~> 0.2.0' gem 'simple-navigation', '~> 4.4' -gem 'stoplight', '~> 4.1' -gem 'strong_migrations' +gem 'simple_form', '~> 5.2' +gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' +gem 'stoplight', '~> 3.0.1' +gem 'strong_migrations', '~> 0.8' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2023' -gem 'webauthn', '~> 3.0' gem 'webpacker', '~> 5.4' -gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913' +gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' +gem 'webauthn', '~> 3.0' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'prometheus_exporter', '~> 2.2', require: false - -gem 'opentelemetry-api', '~> 1.5.0' - -group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false - gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-http', '~> 0.24.0', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-rails', '~> 0.36.0', require: false - gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false - gem 'opentelemetry-sdk', '~> 1.4', require: false -end +gem 'private_address_check', '~> 0.5' group :test do - # Enable usage of all available CPUs/cores during spec runs - gem 'flatware-rspec' + # Used to split testing into chunks in CI + gem 'rspec_chunked', '~> 0.6' - # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab - gem 'rspec-github', '~> 3.0', require: false + # RSpec progress bar formatter + gem 'fuubar', '~> 2.5' - # RSpec helpers for email specs - gem 'email_spec' - - # Extra RSpec extension methods and helpers for sidekiq - gem 'rspec-sidekiq', '~> 5.0' + # Extra RSpec extenion methods and helpers for sidekiq + gem 'rspec-sidekiq', '~> 4.0' # Browser integration testing gem 'capybara', '~> 3.39' @@ -145,36 +119,42 @@ group :test do gem 'database_cleaner-active_record' # Used to mock environment variables - gem 'climate_control' + gem 'climate_control', '~> 0.2' + + # 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' # Validate schemas in specs - gem 'json-schema', '~> 5.0' + gem 'json-schema', '~> 4.0' # Test harness fo rack components gem 'rack-test', '~> 2.1' - gem 'shoulda-matchers' - - # Coverage formatter for RSpec + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false - gem 'simplecov-lcov', '~> 0.8', require: false # Stub web requests for specs gem 'webmock', '~> 3.18' + + gem 'rspec-retry', '>= 0.6.2' end group :development do # Code linting CLI and plugins gem 'rubocop', require: false gem 'rubocop-capybara', require: false - gem 'rubocop-i18n', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false - gem 'rubocop-rspec_rails', require: false # Annotates modules with schema - gem 'annotaterb', '~> 4.13', require: false + gem 'annotate', '~> 3.2' # Enhanced error message pages for development gem 'better_errors', '~> 2.9' @@ -182,49 +162,46 @@ group :development do # Preview mail in the browser gem 'letter_opener', '~> 1.8' - gem 'letter_opener_web', '~> 3.0' + gem 'letter_opener_web', '~> 2.0' # Security analysis CLI tools - gem 'brakeman', '~> 7.0', require: false + gem 'brakeman', '~> 6.0', require: false gem 'bundler-audit', '~> 0.9', require: false # Linter CLI for HAML files gem 'haml_lint', require: false + # Deployment automation + gem 'capistrano', '~> 3.17' + gem 'capistrano-rails', '~> 1.6' + gem 'capistrano-rbenv', '~> 2.2' + gem 'capistrano-yarn', '~> 2.0' + # Validate missing i18n keys gem 'i18n-tasks', '~> 1.0', require: false end group :development, :test do - # Interactive Debugging tools - gem 'debug', '~> 1.8', require: false - - # 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 gem 'stackprof', require: false - gem 'test-prof', require: false + gem 'test-prof' # RSpec runner for rails - gem 'rspec-rails', '~> 7.0' + gem 'rspec-rails', '~> 6.0' end group :production do gem 'lograge', '~> 0.12' end -gem 'cocoon', '~> 1.2' gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' +gem 'cocoon', '~> 1.2' -gem 'net-http', '~> 0.6.0' +gem 'net-http', '~> 0.3.2' gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index f13df0c43f..4ccccc52c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,146 +1,190 @@ GIT - remote: https://github.com/mastodon/webpush.git - revision: 9631ac63045cfabddacc69fc06e919b4c13eb913 - ref: 9631ac63045cfabddacc69fc06e919b4c13eb913 + remote: https://github.com/ClearlyClaire/webpush.git + revision: f14a4d52e201128b1b00245d11b6de80d6cfdcd9 + ref: f14a4d52e201128b1b00245d11b6de80d6cfdcd9 specs: - webpush (1.1.0) + webpush (0.3.8) hkdf (~> 0.2) jwt (~> 2.0) +GIT + remote: https://github.com/mastodon/rails-settings-cached.git + revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab + branch: v0.6.6-aliases-true + specs: + rails-settings-cached (0.6.6) + rails (>= 4.2.0) + +GIT + remote: https://github.com/stanhu/omniauth-cas.git + revision: 4211e6d05941b4a981f9a36b49ec166cecd0e271 + ref: 4211e6d05941b4a981f9a36b49ec166cecd0e271 + specs: + omniauth-cas (2.0.0) + addressable (~> 2.3) + nokogiri (~> 1.5) + omniauth (>= 1.2, < 3) + GEM remote: https://rubygems.org/ specs: - actioncable (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + actioncable (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) - mail (>= 2.8.0) - actionmailer (8.0.2) - actionpack (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activesupport (= 8.0.2) - mail (>= 2.8.0) - rails-dom-testing (~> 2.2) - actionpack (8.0.2) - actionview (= 8.0.2) - activesupport (= 8.0.2) - nokogiri (>= 1.8.5) - rack (>= 2.2.4) - rack-session (>= 1.0.1) + actionmailbox (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.8.4) + actionpack (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activesupport (= 7.0.8.4) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.8.4) + actionview (= 7.0.8.4) + activesupport (= 7.0.8.4) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - useragent (~> 0.16) - actiontext (8.0.2) - actionpack (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.8.4) + actionpack (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2) - activesupport (= 8.0.2) + actionview (7.0.8.4) + activesupport (= 7.0.8.4) builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - active_model_serializers (0.10.15) - actionpack (>= 4.1) - activemodel (>= 4.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_model_serializers (0.10.13) + actionpack (>= 4.1, < 7.1) + activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.0.2) - activesupport (= 8.0.2) + activejob (7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.3.6) - activemodel (8.0.2) - activesupport (= 8.0.2) - activerecord (8.0.2) - activemodel (= 8.0.2) - activesupport (= 8.0.2) - timeout (>= 0.4.0) - activestorage (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activesupport (= 8.0.2) + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) + activestorage (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activesupport (= 7.0.8.4) marcel (~> 1.0) - activesupport (8.0.2) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb + mini_mime (>= 1.1.0) + activesupport (7.0.8.4) + concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) - logger (>= 1.4.2) minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + tzinfo (~> 2.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) + airbrussh (1.4.1) + sshkit (>= 1.6.1, != 1.7.0) android_key_attestation (0.3.0) - annotaterb (4.14.0) - ast (2.4.3) - attr_required (1.0.2) - aws-eventstream (1.3.2) - aws-partitions (1.1087.0) - aws-sdk-core (3.215.1) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.96.0) - aws-sdk-core (~> 3, >= 3.210.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.177.0) - aws-sdk-core (~> 3, >= 3.210.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) + rake (>= 10.4, < 14.0) + ast (2.4.2) + attr_encrypted (4.0.0) + encryptor (~> 3.0.0) + attr_required (1.0.1) + awrence (1.2.1) + aws-eventstream (1.2.0) + aws-partitions (1.809.0) + aws-sdk-core (3.181.0) aws-eventstream (~> 1, >= 1.0.2) - azure-blob (0.5.7) - rexml - base64 (0.2.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.71.0) + aws-sdk-core (~> 3, >= 3.177.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.133.0) + aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) + aws-eventstream (~> 1, >= 1.0.2) + azure-storage-blob (2.0.3) + azure-storage-common (~> 2.0) + nokogiri (~> 1, >= 1.10.8) + azure-storage-common (2.0.4) + faraday (~> 1.0) + faraday_middleware (~> 1.0, >= 1.0.0.rc1) + net-http-persistent (~> 4.0) + nokogiri (~> 1, >= 1.10.8) + base64 (0.1.1) bcp47_spec (0.2.1) - bcrypt (3.1.20) - benchmark (0.4.0) + bcrypt (3.1.18) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.9) - bindata (2.5.1) - binding_of_caller (1.0.1) - debug_inspector (>= 1.2.0) - blurhash (0.1.8) - bootsnap (1.18.4) + better_html (2.0.1) + actionview (>= 6.0) + activesupport (>= 6.0) + ast (~> 2.0) + erubi (~> 1.4) + parser (>= 2.4) + smart_properties + bindata (2.4.15) + binding_of_caller (1.0.0) + debug_inspector (>= 0.0.1) + blurhash (0.1.7) + bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (7.0.2) - racc - browser (6.2.0) + brakeman (6.0.1) + browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) - builder (3.3.0) - bundler-audit (0.9.2) + builder (3.2.4) + bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - capybara (3.40.0) + capistrano (3.17.3) + airbrussh (>= 1.0.0) + i18n + rake (>= 10.0.0) + sshkit (>= 1.9.0) + capistrano-bundler (2.1.0) + capistrano (~> 3.1) + capistrano-rails (1.6.3) + capistrano (~> 3.1) + capistrano-bundler (>= 1.1, < 3) + capistrano-rbenv (2.2.0) + capistrano (~> 3.1) + sshkit (~> 1.3) + capistrano-yarn (2.0.2) + capistrano (~> 3.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.11) + nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) @@ -148,130 +192,129 @@ GEM case_transform (0.2) activesupport cbor (0.5.9.8) - charlock_holmes (0.7.9) - chewy (7.6.0) + charlock_holmes (0.7.8) + chewy (7.3.4) activesupport (>= 5.2) - elasticsearch (>= 7.14.0, < 8) + elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl - childprocess (5.1.0) - logger (~> 1.5) chunky_png (1.4.0) - climate_control (1.2.0) + climate_control (0.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.3.5) - connection_pool (2.5.0) - cose (1.3.1) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) - crack (1.0.0) - bigdecimal + crack (0.4.5) rexml crass (1.0.6) - css_parser (1.21.1) + css_parser (1.14.0) addressable - csv (3.3.4) - database_cleaner-active_record (2.2.0) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.1) - debug (1.10.0) - irb (~> 1.10) - reline (>= 0.3.8) - debug_inspector (1.2.0) - devise (4.9.4) + date (3.3.4) + debug_inspector (1.1.0) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (6.1.0) - activesupport (>= 7.0, < 8.1) + devise-two-factor (4.1.0) + activesupport (< 7.1) + attr_encrypted (>= 1.3, < 5, != 2) devise (~> 4.0) - railties (>= 7.0, < 8.1) + railties (< 7.1) rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.6.1) - discard (1.4.0) - activerecord (>= 4.2, < 9.0) - docile (1.4.1) - domain_name (0.6.20240107) - doorkeeper (5.8.2) + diff-lcs (1.5.0) + discard (1.2.1) + activerecord (>= 4.2, < 8) + docile (1.4.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + doorkeeper (5.6.6) railties (>= 5) - dotenv (3.1.8) - drb (2.2.1) - elasticsearch (7.17.11) - elasticsearch-api (= 7.17.11) - elasticsearch-transport (= 7.17.11) - elasticsearch-api (7.17.11) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) + ed25519 (1.3.0) + elasticsearch (7.13.3) + elasticsearch-api (= 7.13.3) + elasticsearch-transport (= 7.13.3) + elasticsearch-api (7.13.3) multi_json elasticsearch-dsl (0.1.10) - elasticsearch-transport (7.17.11) - base64 - faraday (>= 1, < 3) + elasticsearch-transport (7.13.3) + faraday (~> 1) multi_json - email_spec (2.3.0) - htmlentities (~> 4.3.3) - launchy (>= 2.1, < 4.0) - mail (~> 2.7) - email_validator (2.2.4) - activemodel - erubi (1.13.1) + encryptor (3.0.0) + erubi (1.12.0) et-orbi (1.2.11) tzinfo - excon (1.2.5) - logger - fabrication (2.31.0) - faker (3.5.1) + excon (0.100.0) + fabrication (2.30.0) + faker (3.2.1) i18n (>= 1.8.11, < 2) - faraday (2.13.0) - faraday-net_http (>= 2.0, < 3.5) - json - logger - faraday-follow_redirects (0.3.0) - faraday (>= 1, < 3) - faraday-httpclient (2.0.1) - httpclient (>= 2.2) - faraday-net_http (3.4.0) - net-http (>= 0.5.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) fast_blank (1.0.1) - fastimage (2.4.0) - ffi (1.17.2) - ffi-compiler (1.3.2) - ffi (>= 1.15.5) + fastimage (2.3.1) + ffi (1.15.5) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) rake - flatware (2.3.4) - drb - thor (< 2.0) - flatware-rspec (2.3.4) - flatware (= 2.3.4) - rspec (>= 3.6) - fog-core (2.6.0) + fog-core (2.1.0) builder - excon (~> 1.0) - formatador (>= 0.2, < 2.0) + excon (~> 0.58) + formatador (~> 0.2) mime-types fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (1.1.5) - fog-core (~> 2.1) + fog-openstack (0.3.10) + fog-core (>= 1.45, <= 2.1.0) fog-json (>= 1.0) - formatador (1.1.0) + ipaddress (>= 0.8) + formatador (0.3.0) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) - globalid (1.2.1) - activesupport (>= 6.1) - google-protobuf (4.30.2) - bigdecimal - rake (>= 13) - googleapis-common-protos-types (1.19.0) - google-protobuf (>= 3.18, < 5.a) - haml (6.3.0) + fuubar (2.5.1) + rspec-core (~> 3.0) + ruby-progressbar (~> 1.4) + globalid (1.1.0) + activesupport (>= 5.0) + haml (6.1.2) temple (>= 0.8.2) thor tilt @@ -280,89 +323,70 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.62.0) - haml (>= 5.0) + haml_lint (0.50.0) + haml (>= 4.0, < 6.2) parallel (~> 1.10) rainbow rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.1.2) + hashdiff (1.0.1) hashie (5.0.0) hcaptcha (7.1.0) json - highline (3.1.2) - reline + highline (2.1.0) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) - http (5.2.0) + http (5.1.1) addressable (~> 2.8) - base64 (~> 0.1) http-cookie (~> 1.0) http-form_data (~> 2.2) - llhttp-ffi (~> 0.5.0) - http-cookie (1.0.8) + llhttp-ffi (~> 0.4.0) + http-cookie (1.0.5) domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) - httpclient (2.9.0) - mutex_m - httplog (1.7.0) + httpclient (2.8.3) + httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.7) + i18n (1.14.5) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.15) + i18n-tasks (1.0.12) activesupport (>= 4.0.2) ast (>= 2.1.0) + better_html (>= 1.0, < 3.0) erubi highline (>= 2.0.0) i18n - parser (>= 3.2.2.1) + parser (>= 2.2.3.0) rails-i18n rainbow (>= 2.2.2, < 4.0) - ruby-progressbar (~> 1.8, >= 1.8.1) terminal-table (>= 1.5.1) idn-ruby (0.1.5) - inline_svg (1.10.0) - activesupport (>= 3.0) - nokogiri (>= 1.6) - io-console (0.8.0) - irb (1.15.2) - pp (>= 0.6.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - jd-paperclip-azure (3.0.0) - addressable (~> 2.5) - azure-blob (~> 0.5.2) - hashie (~> 5.0) + ipaddress (0.8.3) jmespath (1.6.2) - json (2.10.2) + json (2.6.3) json-canonicalization (1.0.0) - json-jwt (1.16.7) + json-jwt (1.15.3.1) activesupport (>= 4.2) aes_key_wrap - base64 bindata - faraday (~> 2.0) - faraday-follow_redirects - json-ld (3.3.2) + httpclient + json-ld (3.3.1) htmlentities (~> 4.3) json-canonicalization (~> 1.0) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) rack (>= 2.2, < 4) rdf (~> 3.3) - rexml (~> 3.2) - json-ld-preloaded (3.3.1) - json-ld (~> 3.3) - rdf (~> 3.3) - json-schema (5.1.1) - addressable (~> 2.8) - bigdecimal (~> 3.1) + json-ld-preloaded (3.2.2) + json-ld (~> 3.2) + rdf (~> 3.2) + json-schema (4.0.0) + addressable (>= 2.8) jsonapi-renderer (0.2.2) - jwt (2.10.1) - base64 + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -375,42 +399,32 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.2.2) + kt-paperclip (7.2.1) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) mime-types - terrapin (>= 0.6.0, < 2.0) - language_server-protocol (3.17.0.4) - launchy (3.1.1) + terrapin (~> 0.6.0) + language_server-protocol (3.17.0.3) + launchy (2.5.2) addressable (~> 2.8) - childprocess (~> 5.0) - logger (~> 1.6) - letter_opener (1.10.0) - launchy (>= 2.2, < 4) - letter_opener_web (3.0.0) - actionmailer (>= 6.1) - letter_opener (~> 1.9) - railties (>= 6.1) + letter_opener (1.8.1) + launchy (>= 2.2, < 3) + letter_opener_web (2.0.0) + actionmailer (>= 5.2) + letter_opener (~> 1.7) + railties (>= 5.2) rexml link_header (0.0.8) - lint_roller (1.1.0) - linzer (0.6.5) - openssl (~> 3.0, >= 3.0.0) - rack (>= 2.2, < 4.0) - starry (~> 0.2) - stringio (~> 3.1, >= 3.1.2) - uri (~> 1.0, >= 1.0.2) - llhttp-ffi (0.5.1) + llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) - logger (1.7.0) - lograge (0.14.0) + lograge (0.13.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.24.0) + loofah (2.21.4) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -422,400 +436,268 @@ GEM mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) - memory_profiler (1.1.0) - mime-types (3.6.2) - logger + md-paperclip-azure (2.2.0) + addressable (~> 2.5) + azure-storage-blob (~> 2.0.1) + hashie (~> 5.0) + memory_profiler (1.0.1) + method_source (1.0.0) + mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2025.0408) + mime-types-data (3.2023.0808) mini_mime (1.1.5) - mini_portile2 (2.8.8) - minitest (5.25.5) - msgpack (1.8.0) + mini_portile2 (2.8.7) + minitest (5.19.0) + msgpack (1.7.1) multi_json (1.15.0) - mutex_m (0.3.0) - net-http (0.6.0) + multipart-post (2.3.0) + net-http (0.3.2) uri - net-imap (0.5.6) + net-http-persistent (4.0.2) + connection_pool (~> 2.2) + net-imap (0.3.7) date net-protocol - net-ldap (0.19.0) + net-ldap (0.18.0) net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.1) + net-scp (4.0.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-smtp (0.3.4) net-protocol - nio4r (2.7.4) - nokogiri (1.18.7) + net-ssh (7.1.0) + nio4r (2.7.3) + nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.10) - bigdecimal (>= 3.0) - ostruct (>= 0.2) - omniauth (2.1.3) + nsa (0.3.0) + activesupport (>= 4.2, < 7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + sidekiq (>= 3.5) + statsd-ruby (~> 1.4, >= 1.4.0) + oj (3.16.1) + omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-cas (3.0.1) - addressable (~> 2.8) - nokogiri (~> 1.12) - omniauth (~> 2.1) - omniauth-rails_csrf_protection (1.0.2) + omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.2.3) + omniauth-saml (2.1.2) omniauth (~> 2.1) - ruby-saml (~> 1.18) - omniauth_openid_connect (0.8.0) + ruby-saml (~> 1.17) + omniauth_openid_connect (0.6.1) omniauth (>= 1.9, < 3) - openid_connect (~> 2.2) - openid_connect (2.3.1) + openid_connect (~> 1.1) + openid_connect (1.4.2) activemodel attr_required (>= 1.0.0) - email_validator - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.16) - mail - rack-oauth2 (~> 2.2) - swd (~> 2.0) + json-jwt (>= 1.15.0) + net-smtp + rack-oauth2 (~> 1.21) + swd (~> 1.3) tzinfo + validate_email validate_url - webfinger (~> 2.0) - openssl (3.3.0) + webfinger (~> 1.2) + openssl (3.1.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.5.0) - opentelemetry-common (0.22.0) - opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.30.0) - google-protobuf (>= 3.18) - googleapis-common-protos-types (~> 1.3) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-sdk (~> 1.2) - opentelemetry-semantic_conventions - opentelemetry-helpers-sql-obfuscation (0.3.0) - opentelemetry-common (~> 0.21) - opentelemetry-instrumentation-action_mailer (0.4.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.7) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-action_pack (0.12.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-action_view (0.9.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.7) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_job (0.8.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_model_serializers (0.22.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (>= 0.7.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_record (0.9.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_storage (0.1.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.7) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_support (0.8.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-base (0.23.0) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.21) - opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-concurrent_ruby (0.22.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-excon (0.23.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-faraday (0.26.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-http (0.24.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-http_client (0.23.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-net_http (0.23.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-pg (0.30.0) - opentelemetry-api (~> 1.0) - opentelemetry-helpers-sql-obfuscation - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-rack (0.26.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-rails (0.36.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-action_mailer (~> 0.4.0) - opentelemetry-instrumentation-action_pack (~> 0.12.0) - opentelemetry-instrumentation-action_view (~> 0.9.0) - opentelemetry-instrumentation-active_job (~> 0.8.0) - opentelemetry-instrumentation-active_record (~> 0.9.0) - opentelemetry-instrumentation-active_storage (~> 0.1.0) - opentelemetry-instrumentation-active_support (~> 0.8.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) - opentelemetry-instrumentation-redis (0.26.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-sidekiq (0.26.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-registry (0.4.0) - opentelemetry-api (~> 1.1) - opentelemetry-sdk (1.8.0) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-registry (~> 0.2) - opentelemetry-semantic_conventions - opentelemetry-semantic_conventions (1.11.0) - opentelemetry-api (~> 1.0) orm_adapter (0.5.0) - ostruct (0.6.1) - ox (2.14.22) - bigdecimal (>= 3.0) - parallel (1.27.0) - parser (3.3.8.0) + ox (2.14.17) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.9) - pghero (3.6.2) - activerecord (>= 6.1) - pp (0.6.2) - prettyprint - premailer (1.27.0) + pg (1.5.5) + pghero (3.3.4) + activerecord (>= 6) + premailer (1.21.0) addressable - css_parser (>= 1.19.0) + css_parser (>= 1.12.0) htmlentities (>= 4.0.0) premailer-rails (1.12.0) actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - prettyprint (0.2.0) - prism (1.4.0) - prometheus_exporter (2.2.0) - webrick - propshaft (1.1.0) - actionpack (>= 7.0.0) - activesupport (>= 7.0.0) - rack - railties (>= 7.0.0) - psych (5.2.3) - date - stringio - public_suffix (6.0.1) - puma (6.6.0) + private_address_check (0.5.0) + public_suffix (5.0.3) + puma (6.4.3) nio4r (~> 2.0) - pundit (2.5.0) + pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.13) + rack (2.2.9) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) rack (>= 2.0.0) - rack-oauth2 (2.2.1) + rack-oauth2 (1.21.3) activesupport attr_required - faraday (~> 2.0) - faraday-follow_redirects + httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.2.0) - base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) - rack-proxy (0.7.7) + rack-protection (3.0.6) rack - rack-session (1.0.2) - rack (< 3) - rack-test (2.2.0) + rack-proxy (0.7.6) + rack + rack-test (2.1.0) rack (>= 1.3) - rackup (1.0.1) - rack (< 3) - webrick - rails (8.0.2) - actioncable (= 8.0.2) - actionmailbox (= 8.0.2) - actionmailer (= 8.0.2) - actionpack (= 8.0.2) - actiontext (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activemodel (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + rails (7.0.8.4) + actioncable (= 7.0.8.4) + actionmailbox (= 7.0.8.4) + actionmailer (= 7.0.8.4) + actionpack (= 7.0.8.4) + actiontext (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activemodel (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) bundler (>= 1.15.0) - railties (= 8.0.2) - rails-dom-testing (2.2.0) + railties (= 7.0.8.4) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.1.1) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) + rails-html-sanitizer (1.6.0) loofah (~> 2.21) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (8.0.1) + nokogiri (~> 1.14) + rails-i18n (7.0.7) i18n (>= 0.7, < 2) - railties (>= 8.0.0, < 9) - railties (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) - irb (~> 1.13) - rackup (>= 1.0.0) + railties (>= 6.0.0, < 8) + railties (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) + method_source rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) + thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.2.1) - rdf (3.3.2) + rake (13.0.6) + rdf (3.3.1) bcp47_spec (~> 0.2) - bigdecimal (~> 3.1, >= 3.1.5) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.7.0) - rdf (~> 3.3) - rdoc (6.13.1) - psych (>= 4.0.0) - redcarpet (3.6.1) + rdf-normalize (0.6.1) + rdf (~> 3.2) + redcarpet (3.6.0) redis (4.8.1) redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.10.0) - reline (0.6.1) - io-console (~> 0.5) - request_store (1.7.0) + regexp_parser (2.8.1) + request_store (1.5.1) rack (>= 1.4) - responders (3.1.1) + responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.4.1) + rexml (3.3.7) rotp (6.3.0) - rouge (4.5.1) + rouge (4.1.2) rpam2 (4.0.2) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec (3.13.0) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) - rspec-core (3.13.3) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-github (3.0.0) - rspec-core (~> 3.0) - rspec-mocks (3.13.2) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-rails (7.1.1) - actionpack (>= 7.0) - activesupport (>= 7.0) - railties (>= 7.0) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) - rspec-sidekiq (5.1.0) + rspec-support (~> 3.12.0) + rspec-rails (6.0.3) + 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-retry (0.6.2) + rspec-core (> 3.3) + rspec-sidekiq (4.0.1) rspec-core (~> 3.0) rspec-expectations (~> 3.0) rspec-mocks (~> 3.0) - sidekiq (>= 5, < 9) - rspec-support (3.13.2) - rubocop (1.75.2) + sidekiq (>= 5, < 8) + rspec-support (3.12.1) + rspec_chunked (0.6) + rubocop (1.56.3) + base64 (~> 0.1.1) json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.3.0.2) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.44.0, < 2.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.44.1) - parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-capybara (2.22.1) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-i18n (3.2.3) - lint_roller (~> 1.1) - rubocop (>= 1.72.1) - rubocop-performance (1.25.0) - lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.31.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-performance (1.19.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.20.2) activesupport (>= 4.2.0) - lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rspec (3.5.0) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec_rails (2.31.0) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec (~> 3.5) - ruby-prof (1.7.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.23.2) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-prof (1.6.3) ruby-progressbar (1.13.0) - ruby-saml (1.18.0) + ruby-saml (1.17.0) nokogiri (>= 1.13.10) rexml - ruby-vips (2.2.3) - ffi (~> 1.12) - logger - rubyzip (2.4.1) - rufus-scheduler (3.9.2) - fugit (~> 1.1, >= 1.11.1) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + rufus-scheduler (3.9.1) + fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (7.0.0) + sanitize (6.0.2) crass (~> 1.0.2) - nokogiri (>= 1.16.8) - scenic (1.8.0) + nokogiri (>= 1.12.0) + scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - securerandom (0.4.1) - selenium-webdriver (4.31.0) - base64 (~> 0.2) - logger (~> 1.4) + selenium-webdriver (4.11.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.1.0) - shoulda-matchers (6.4.0) - activesupport (>= 5.2.0) + semantic_range (3.0.0) sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.6) + sidekiq-scheduler (5.0.3) rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) - tilt (>= 1.4.0, < 3) + tilt (>= 1.4.0) sidekiq-unique-jobs (7.1.33) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -824,40 +706,47 @@ GEM thor (>= 0.20, < 3.0) simple-navigation (4.4.0) activesupport (>= 2.3.2) - simple_form (5.3.1) + simple_form (5.2.0) actionpack (>= 5.2) activemodel (>= 5.2) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.1) - simplecov-lcov (0.8.0) + simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - stackprof (0.2.27) - starry (0.2.0) - base64 - stoplight (4.1.1) + smart_properties (1.17.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + sshkit (1.21.5) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) + stackprof (0.2.25) + statsd-ruby (1.5.0) + stoplight (3.0.2) redlock (~> 1.0) - stringio (3.1.6) - strong_migrations (2.3.0) - activerecord (>= 7) - swd (2.0.3) + strong_migrations (0.8.0) + activerecord (>= 5.2) + swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) - faraday (~> 2.0) - faraday-follow_redirects + httpclient (>= 2.4) sysexits (1.2.0) - temple (0.10.3) - terminal-table (4.0.0) - unicode-display_width (>= 1.1.1, < 4) - terrapin (1.1.0) - climate_control - test-prof (1.4.4) - thor (1.3.2) - tilt (2.6.0) - timeout (0.4.3) - tpm-key_attestation (0.14.0) + temple (0.10.2) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + terrapin (0.6.0) + climate_control (>= 0.0.3, < 1.0) + test-prof (1.2.3) + thor (1.3.1) + tilt (2.2.0) + timeout (0.4.1) + tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) @@ -870,40 +759,40 @@ GEM tty-cursor (~> 0.7) tty-screen (~> 0.8) wisper (~> 2.0) - tty-screen (0.8.2) + tty-screen (0.8.1) twitter-text (3.1.0) idn-ruby unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2025.2) + tzinfo-data (1.2023.3) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext - unf_ext (0.0.9.1) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) - uri (1.0.3) - useragent (0.16.11) + unf_ext (0.0.8.2) + unicode-display_width (2.4.2) + uri (0.12.2) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (3.4.0) + webauthn (3.0.0) android_key_attestation (~> 0.3.0) + awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) openssl (>= 2.2) safety_net_attestation (~> 0.4.0) - tpm-key_attestation (~> 0.14.0) - webfinger (2.1.3) + tpm-key_attestation (~> 0.12.0) + webfinger (1.2.0) activesupport - faraday (~> 2.0) - faraday-follow_redirects - webmock (3.25.1) + httpclient (>= 2.4) + webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -912,17 +801,15 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - webrick (1.9.1) - websocket (1.2.11) - websocket-driver (0.7.7) - base64 + websocket (1.2.9) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.6.16) PLATFORMS ruby @@ -930,135 +817,113 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) - annotaterb (~> 4.13) - aws-sdk-core (< 3.216.0) + annotate (~> 3.2) aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.18.0) - brakeman (~> 7.0) + bootsnap (~> 1.16.0) + brakeman (~> 6.0) browser bundler-audit (~> 0.9) + capistrano (~> 3.17) + capistrano-rails (~> 1.6) + capistrano-rbenv (~> 2.2) + capistrano-yarn (~> 2.0) capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control + climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool - csv (~> 3.2) database_cleaner-active_record - debug (~> 1.8) devise (~> 4.9) - devise-two-factor + devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) - dotenv - email_spec + dotenv-rails (~> 2.8) + ed25519 (~> 1.3) fabrication (~> 2.30) faker (~> 3.2) - faraday-httpclient fast_blank (~> 1.0) fastimage - flatware-rspec - fog-core (<= 2.6.0) - fog-openstack (~> 1.0) + fog-core (<= 2.4.0) + fog-openstack (~> 0.3) + fuubar (~> 2.5) haml-rails (~> 2.0) haml_lint hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) - http (~> 5.2.0) + http (~> 5.1) http_accept_language (~> 2.1) - httplog (~> 1.7.0) - i18n + httplog (~> 1.6.2) i18n-tasks (~> 1.0) idn-ruby - inline_svg - irb (~> 1.8) - jd-paperclip-azure (~> 3.0) json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 5.0) + json-schema (~> 4.0) kaminari (~> 1.2) kt-paperclip (~> 7.2) letter_opener (~> 1.8) - letter_opener_web (~> 3.0) + letter_opener_web (~> 2.0) link_header (~> 0.0) - linzer (~> 0.6.1) lograge (~> 0.12) mail (~> 2.8) mario-redis-lock (~> 1.2) + md-paperclip-azure (~> 2.2) memory_profiler - mime-types (~> 3.6.0) - mutex_m - net-http (~> 0.6.0) + mime-types (~> 3.5.0) + net-http (~> 0.3.2) net-ldap (~> 0.18) nokogiri (~> 1.15) + nsa oj (~> 3.14) omniauth (~> 2.0) - omniauth-cas (~> 3.0.0.beta.1) + omniauth-cas! omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) - omniauth_openid_connect (~> 0.8.0) - opentelemetry-api (~> 1.5.0) - opentelemetry-exporter-otlp (~> 0.30.0) - opentelemetry-instrumentation-active_job (~> 0.8.0) - opentelemetry-instrumentation-active_model_serializers (~> 0.22.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) - opentelemetry-instrumentation-excon (~> 0.23.0) - opentelemetry-instrumentation-faraday (~> 0.26.0) - opentelemetry-instrumentation-http (~> 0.24.0) - opentelemetry-instrumentation-http_client (~> 0.23.0) - opentelemetry-instrumentation-net_http (~> 0.23.0) - opentelemetry-instrumentation-pg (~> 0.30.0) - opentelemetry-instrumentation-rack (~> 0.26.0) - opentelemetry-instrumentation-rails (~> 0.36.0) - opentelemetry-instrumentation-redis (~> 0.26.0) - opentelemetry-instrumentation-sidekiq (~> 0.26.0) - opentelemetry-sdk (~> 1.4) + omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet pg (~> 1.5) pghero premailer-rails - prometheus_exporter (~> 2.2) - propshaft - public_suffix (~> 6.0) + private_address_check (~> 0.5) + public_suffix (~> 5.0) puma (~> 6.3) pundit (~> 2.3) rack (~> 2.2.7) rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 8.0) - rails-i18n (~> 8.0) + rails (~> 7.0) + rails-controller-testing (~> 1.0) + rails-i18n (~> 7.0) + rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) rqrcode (~> 2.2) - rspec-github (~> 3.0) - rspec-rails (~> 7.0) - rspec-sidekiq (~> 5.0) + rspec-rails (~> 6.0) + rspec-retry (>= 0.6.2) + rspec-sidekiq (~> 4.0) + rspec_chunked (~> 0.6) rubocop rubocop-capybara - rubocop-i18n rubocop-performance rubocop-rails rubocop-rspec - rubocop-rspec_rails ruby-prof ruby-progressbar (~> 1.13) - ruby-vips (~> 2.2) rubyzip (~> 2.3) - sanitize (~> 7.0) + sanitize (~> 6.0) scenic (~> 1.7) selenium-webdriver - shoulda-matchers sidekiq (~> 6.5) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 5.0) @@ -1066,10 +931,11 @@ DEPENDENCIES simple-navigation (~> 4.4) simple_form (~> 5.2) simplecov (~> 0.22) - simplecov-lcov (~> 0.8) + sprockets (~> 3.7.2) + sprockets-rails (~> 3.4) stackprof - stoplight (~> 4.1) - strong_migrations + stoplight (~> 3.0.1) + strong_migrations (~> 0.8) test-prof thor (~> 1.2) tty-prompt (~> 0.23) @@ -1082,7 +948,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.4.1p0 + ruby 3.2.2p53 BUNDLED WITH - 2.6.8 + 2.4.13 diff --git a/Procfile b/Procfile index f033fd36c6..d15c835b86 100644 --- a/Procfile +++ b/Procfile @@ -11,4 +11,4 @@ worker: bundle exec sidekiq # # and let the main app use the separate app: # -# heroku config:set STREAMING_API_BASE_URL=wss://.herokuapp.com -a +# heroku config:set STREAMING_API_BASE_URL=wss://.herokuapp.com -a diff --git a/Procfile.dev b/Procfile.dev index f81333b04c..fbb2c2de23 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,4 @@ web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq -stream: env PORT=4000 yarn workspace @mastodon/streaming start +stream: env PORT=4000 yarn run start webpack: bin/webpack-dev-server diff --git a/README.md b/README.md index 854e8ac3d9..5620e7069c 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,172 @@ -NAS is an KMY & Mastodon Fork +# kmyblue -The following are just a few of the most common features. There are many other minor changes to the specifications. +[![Ruby Testing](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml) -Emoji reactions +kmyblueã¯[Mastodon](https://github.com/mastodon/mastodon)ã®ãƒ•ォークã§ã™ã€‚創作作家ã®ãŸã‚ã®Mastodonを目指ã—ã¦é–‹ç™ºã—ã¾ã—ãŸã€‚ -Local Public (Does not appear on the federated timeline of remote servers, but does appear on followers' home timelines. This is different from local only) +kmyblueã¯ãƒ•ォークåã§ã‚りã€åŒæ™‚ã«[サーãƒãƒ¼å](https://kmy.blue)ã§ã‚‚ã‚りã¾ã™ã€‚以下ã¯ç‰¹ã«è¨˜è¿°ãŒãªã„é™ã‚Šã€ãƒ•ォークã¨ã—ã¦ã®kmyblueã‚’ã•ã—ã¾ã™ã€‚ -Bookmark classification +kmyblue㯠AGPL ライセンスã§å…¬é–‹ã•れã¦ã„ã‚‹ãŸã‚ã€ã©ãªãŸã§ã‚‚自由ã«ãƒ•ォークã—ã€ã“ã®ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã‚’å…ƒã«è‡ªåˆ†ã§ã‚µãƒ¼ãƒãƒ¼ã‚’ç«‹ã¦ã¦å…¬é–‹ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã¾ãŸ ActivityPub ã«å‚加ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚確ã‹ã«ã‚µãƒ¼ãƒãƒ¼kmyblueã¯å‰µä½œä½œå®¶å‘ã‘ã®ã‚‚ã®ã§ã™ãŒã€ãƒ•ォークã¨ã—ã¦ã®kmyblueã¯ã‚µãƒ¼ãƒãƒ¼ã¨ã¯åˆ¥ç‰©ã§ã‚りã€ä½œè€…ã¨æ”¿æ²»çš„ã«å¯¾ç«‹ã™ã‚‹ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã€å‰µä½œæ´»å‹•ã®ä¸€éƒ¨ï¼ˆã‚¨ãƒ­é–¢ä¿‚å«ã‚€ï¼‰ã¾ãŸã¯å…¨ä½“ã‚’å¦å®šã™ã‚‹ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ãªã©ã«ã‚‚平等ã«ãŠä½¿ã„ã„ãŸã ã‘ã¾ã™ã€‚サーãƒãƒ¼kmyblueã®ãƒ«ãƒ¼ãƒ«ã‚’é©ç”¨ã™ã‚‹å¿…è¦ã‚‚ãªãã€ã€ŒAnyone But Kmyblueã€ãªãƒ«ãƒ¼ãƒ«ã‚’設定ã™ã‚‹ã“ã¨ã™ã‚‰è¨±å®¹ã•れã¾ã™ã€‚ +kmyblueã¯ã€ç‰¹ã«æœªåŽè¼‰æŠ•ç¨¿ã®æ¤œç´¢ãŒå¼·åŒ–ã•れã¦ã„ã‚‹ãŸã‚ã€ãƒ­ãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«æŽ²è¼‰ã•れã¦ã„ãªã„投稿も検索・購読ã™ã‚‹ã“ã¨ãŒå¯èƒ½ãªå ´åˆãŒã‚りã¾ã™ã€‚閉鎖的ãªã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã€ã‚ã¾ã‚Šç›®ç«‹ã¡ãŸããªã„コミュニティã«ã¯ç‰¹ã«å¼·åŠ›ãªæ©Ÿèƒ½ã‚’æä¾›ã—ã¾ã™ã€‚ãれ以外ã®ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã«å¯¾ã—ã¦ã‚‚ã€kmyblueã¯ãƒ—ライãƒã‚·ãƒ¼ã‚’考慮ã—ãŸã†ãˆã§å¼·åŠ›ãªæ¤œç´¢ãƒ»è³¼èª­æ©Ÿèƒ½ã‚’æä¾›ã™ã‚‹ãŸã‚ã€æ±Žç”¨ã‚µãƒ¼ãƒãƒ¼ã¨ã—ã¦åˆ©ç”¨ã™ã‚‹ã«ã‚‚ååˆ†ãªæ©Ÿèƒ½ãŒæƒã£ã¦ã„ã¾ã™ã€‚ -Set who can search your posts for each post (Searchability) +ãŸã ã—kmyblueã«ãŠã„ã¦**テストコードã¯é£¾ã‚Š**ã§ã—ã‹ã‚りã¾ã›ã‚“。ã“れã¯kmyblueを利用ã™ã‚‹äººãŒæœ¬å®¶Mastodonより圧倒的ã«å°‘ãªãã€ãƒã‚°ã‚„セキュリティインシデントを発見ã™ã‚‹ã ã‘ã®äººæ•°ãŒè¶³ã‚Šãªã„ã“ã¨ã‚’æ„味ã—ã¾ã™ã€‚kmyblueã¯å¯¾ç­–ã¨ã—ã¦è‡ªå‹•テストを拡充ã—ã¦ã„ã¾ã™ã€‚独自機能ã®ãƒ†ã‚¹ãƒˆã‚’記述ã™ã‚‹ã ã‘ã§ãªãã€æœ¬å®¶ã®ãƒ†ã‚¹ãƒˆã‚³ãƒ¼ãƒ‰ã®è£œå¼·ã‚‚行ã£ã¦ãŠã‚Šã¾ã™ãŒã€ç¢ºèªæ¼ã‚Œã¯å¿…ãšç™ºç”Ÿã™ã‚‹ã‚‚ã®ã§ã™ã€‚ä¸å…·åˆãŒç™ºç”Ÿã—ã¦ã‚‚自己責任ã«ãªã‚Šã¾ã™ã€‚既知ã®ãƒã‚°ã‚‚ã„ãã¤ã‹ã‚りã¾ã™ã—ã€ç›´ã™äºˆå®šã®ãªã„ã‚‚ã®ã‚‚å«ã¾ã‚Œã¾ã™ã€‚ -Quote posts, modest quotes (references) +テストコードã€Lint ã©ã¡ã‚‰ã‚‚å‹•ã„ã¦ã„ã¾ã™ã€‚ -Record posts that meet certain conditions such as domains, accounts, and keywords (Subscriptions/Antennas) +## インストール方法 -Send posts to a designated set of followers (Circles) (different from direct messages) +[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 -Notification of new posts on lists +## 開発ã¸ã®å‚加方法 -Exclude posts from people you follow when filtering posts +CONTRIBUTING.mdã‚’å‚ç…§ã—ã¦ãã ã•ã„。 -Hide number of followers and followings +## テスト -Automatically delete posts after a specified time has passed +``` +# デãƒãƒƒã‚°å®Ÿè¡Œï¼ˆä»¥ä¸‹ã®ã„ãšã‚Œã‹ï¼‰ +foreman start +DB_USER=postgres DB_PASS=password foreman start -Expanding moderation functions +# 一部を除ãå…¨ã¦ã®ãƒ†ã‚¹ãƒˆã‚’行ㆠ+RAILS_ENV=test bundle exec rspec spec + +# ElasticSearch連æºãƒ†ã‚¹ãƒˆã‚’行ㆠ+RAILS_ENV=test ES_ENABLED=true RUN_SEARCH_SPECS=true bundle exec rspec spec/search +``` + +## kmyblueã®å¼·ã¿ + +### 本家Mastodonã¸ã®ç©æ¥µçš„追従 + +kmyblueã¯ã€ã„ãã¤ã‹ã®ãƒ•ォークã¨ç•°ãªã‚Šã€è¿½åŠ æ©Ÿèƒ½ã‚’æŽ§ãˆã‚ã«ã™ã‚‹ä»£ã‚ã‚Šã«æœ¬å®¶Mastodonã«ç©æ¥µçš„ã«è¿½å¾“を行ã„ã¾ã™ã€‚ãƒãƒ¼ã‚¸ãƒ§ãƒ³ 4 ã«ã¯ 4 ã®ã‚ˆã•ãŒã‚りã¾ã™ãŒã€æŠ€è¡“çš„ã«å¯èƒ½ã§ã‚ã‚‹é™ã‚Šã€ãƒãƒ¼ã‚¸ãƒ§ãƒ³ 5 ã¸ã®ã‚¢ãƒƒãƒ—グレードもやã¶ã•ã‹ã§ã¯ã‚りã¾ã›ã‚“。 +kmyblueã®è¿½åŠ æ©Ÿèƒ½ãã®ã¾ã¾ã«ã€Mastodonã®æ–°æ©Ÿèƒ½ã‚‚利用ã§ãるよã†èª¿æ•´ã‚’行ã„ã¾ã™ã€‚ + +### 絵文字リアクション対応 + +kmyblueã¯çµµæ–‡å­—リアクションã«å¯¾å¿œã—ã¦ã„るフォークã®ï¼‘ã¤ã§ã™ã€‚絵文字リアクション㯠Misskey 標準æ­è¼‰ã®æ©Ÿèƒ½ã§ã€éœ€è¦ãŒé«˜ã„機能ã§ã‚る割ã«ã¯ã€ã‚µãƒ¼ãƒãƒ¼ã«è² è·ãŒã‹ã‹ã‚‹ãŸã‚本家Mastodonã«ã¯æ­è¼‰ã•れã¦ã„ã¾ã›ã‚“。絵文字リアクションã«ã‚ˆã£ã¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã€ŒãŠæ°—ã«å…¥ã‚Šã€ä»¥ä¸Šã€Œè¿”ä¿¡ã€ä»¥ä¸‹ã®ã‚³ãƒŸãƒ¥ãƒ‹ã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã‚’気軽ã«è¡Œã†ã“ã¨ãŒã§ãã€Mastodonã®åˆ©ç”¨ä½“験ãŒå‘上ã—ã¾ã™ã€‚ +å„ユーザーãŒè‡ªåˆ†ã®æŠ•稿ã«çµµæ–‡å­—リアクションをã¤ã‘ã‚‹ã“ã¨ã‚’æ‹’å¦ã§ãã‚‹ã»ã‹ã€ã‚µãƒ¼ãƒãƒ¼å…¨ä½“ã¨ã—ã¦çµµæ–‡å­—リアクションを無効ã«ã™ã‚‹è¨­å®šã‚‚å¯èƒ½ã§ã™ï¼ˆã“ã®å ´åˆã€ä»–サーãƒãƒ¼ã‹ã‚‰æ¥ãŸçµµæ–‡å­—リアクションã¯ãŠæ°—ã«å…¥ã‚Šã¨ã—ã¦ä¿å­˜ã•れã¾ã™ï¼‰ + +## kmyblueã®ãƒ–ランム+ +- **main** - 管ç†è€…ãŒæœ¬å®¶Mastodonã«PRã™ã‚‹ã¨ãã«ä½¿ã†ã“ã¨ãŒã‚りã¾ã™ +- **kb_development** - ç¾åœ¨kmyblue本体ã§ä½¿ã‚れã¦ã„るソースコードã§ã™ +- **kb_lts** - LTSã®ç®¡ç†ã«ä½¿ã„ã¾ã™ã€‚LTSã¯ã“ã®ãƒ–ランãƒã‹ã‚‰å…¬é–‹ã•れã¾ã™ +- **kb_patch** - 修正パッãƒã®ç®¡ç†ã«ä½¿ã„ã¾ã™ã€‚マイナーãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚¢ãƒƒãƒ—デートã¯é€šå¸¸ã“ã®ãƒ–ランãƒã‹ã‚‰å…¬é–‹ã•れã¾ã™ +- **kb_migration** - 本家Mastodonã¸ã®è¿½å¾“を目的ã¨ã—ãŸãƒ–ランãƒã§ã™ã€‚`kb_development`上ã§é–‹ç™ºã‚’進ã‚ã¦ã„ã‚‹ã¨ãã«åˆ©ç”¨ã—ã¾ã™ +- **kb_migration_development** - 本家Mastodonã¸è¿½å¾“ã—ã€ã‹ã¤ãã®ä¸Šã§é–‹ç™ºã™ã‚‹ã¨ãã«ä½¿ã†ãƒ–ランãƒã§ã™ã€‚æœ€æ–°ã®æœ¬å®¶ã‚³ãƒ¼ãƒ‰ã§ãƒªãƒ•ァクタリングãŒè¡Œã‚れã€`kb_development`ã¨`kb_migration`ã®äº’æ›æ€§ã®ç¶­æŒãŒå›°é›£ã«ãªã£ãŸã¨ãã«åˆ©ç”¨ã—ã¾ã™ã€‚ã“ã“ã§è¿½åŠ ã•ã‚ŒãŸæ©Ÿèƒ½ã¯åŽŸå‰‡ã€æœ¬å®¶Mastodonã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚¢ãƒƒãƒ—ã¨åŒæ™‚ã«`kb_development`ã«å映ã•れã¾ã™ + +## 本家Mastodonã‹ã‚‰ã®è¿½åŠ æ©Ÿèƒ½ + +kmyblueã¯ã€æœ¬å®¶Mastodonã«ã„ãã¤ã‹ã®æ”¹é€ ã‚’加ãˆã¦ã„ã¾ã™ã€‚以下ã«ç¤ºã—ã¾ã™ã€‚ + +### ローカル公開 + +未åŽè¼‰ã‚’æ‹¡å¼µã—ãŸå…¬é–‹ç¯„囲ã§ã™ã€‚本æ¥ã®ã¿åŽè¼‰ã®å…¬é–‹ç¯„囲ã«åŠ ãˆã¦ã€è‡ªåˆ†ã®ã‚µãƒ¼ãƒãƒ¼ã®ãƒ­ãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«æŽ²è¼‰ã•れã¾ã™ã€‚ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã®é€£åˆã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«è¼‰ã›ãŸããªã„ã€è‡ªåˆ†ã¨å±žæ€§ã®è¿‘ã„人é”ãŒé›†ã¾ã£ãŸã‚µãƒ¼ãƒãƒ¼ã¨è‡ªåˆ†ã®ãƒ•ォロワーã«ã ã‘見ã›ãŸã„投稿ã«ç”¨ã„ã¾ã™ã€‚ + +### スタンプ(絵文字リアクション) + +kmyblue内ã§ã®å‘¼ç§°ã¯ã‚¹ã‚¿ãƒ³ãƒ—ã§ã™ãŒã€ä¸€èˆ¬ã«ã¯çµµæ–‡å­—リアクションã¨å‘¼ã°ã‚Œã‚‹æ©Ÿèƒ½ã§ã™ã€‚è‡ªåˆ†ã‚„ä»–äººã®æŠ•ç¨¿ã«çµµæ–‡å­—ã‚’ã¤ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚kmyblueã®ã‚¹ã‚¿ãƒ³ãƒ—㯠Fedibird ã®çµµæ–‡å­—リアクションã¨äº’æ›æ€§ã®ã‚ã‚‹ API ã‚’æŒã£ã¦ã„ã‚‹ãŸã‚ã€Fedibird 対応アプリã§kmyblueã®ã‚¹ã‚¿ãƒ³ãƒ—機能を利用ã§ãã‚‹å ´åˆãŒã‚りã¾ã™ã€‚ + +kmyblueã¯ã€ï¼‘ã¤ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒï¼‘ã¤ã®æŠ•稿ã«è¤‡æ•°ã®ã‚¹ã‚¿ãƒ³ãƒ—(絵文字リアクション)を最大3個ã¾ã§ã¤ã‘ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚ã¾ãŸã€ä¸‹è¨˜æ©Ÿèƒ½ã«ã‚‚対応ã—ã¦ã„ã¾ã™ã€‚ + +- ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒã¤ã‘ãŸã‚¹ã‚¿ãƒ³ãƒ—ã«ç›¸ä¹—りã™ã‚‹ +- 自分ãŒã‚¹ã‚¿ãƒ³ãƒ—を付ã‘ãŸæŠ•ç¨¿ä¸€è¦§ã‚’è¦‹ã‚‹ +- トレンド投稿ã®é¸å®šæ¡ä»¶ã«ã‚¹ã‚¿ãƒ³ãƒ—を付ã‘ãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æ•°ã‚’考慮ã™ã‚‹ +- 投稿ã®è‡ªå‹•削除ã§å‰Šé™¤æ¡ä»¶ã«ã‚¹ã‚¿ãƒ³ãƒ—ã®æ•°ã‚’指定ã™ã‚‹ + +kmyblueã¯ã€ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã®æŠ•稿ã«ã‚¹ã‚¿ãƒ³ãƒ—ã‚’ã¤ã‘ã‚‹ã“ã¨ã§ã€ç›¸æ‰‹ã‚µãƒ¼ãƒãƒ¼ã«æƒ…報をé€ä¿¡ã—ã¾ã™ã€‚ãŸã ã—スタンプã«å¯¾å¿œã—ã¦ã„ãªã„サーãƒãƒ¼ã«ãŠã„ã¦ã¯ã€ãŠæ°—ã«å…¥ã‚Šã¨ã—ã¦é€šçŸ¥ã•れã¾ã™ã€‚ + +### アンテナ + +「自分ã¯ãƒ•ォローã—ã¦ã„ãªã„ãŒé€£åˆã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«æµã‚Œã¦ã„るアカウントã€ã®æŠ•稿を購読ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚アンテナã¯ãƒ‰ãƒ¡ã‚¤ãƒ³ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€ã‚­ãƒ¼ãƒ¯ãƒ¼ãƒ‰ã®ï¼”種類ã®çµžã‚Šè¾¼ã¿æ¡ä»¶ã‚’æŒã¡ã€è¤‡åˆæŒ‡å®šã™ã‚‹ã“ã¨ã§ AND æ¡ä»¶ã¨ã—ã¦åƒãã¾ã™ã€‚アンテナã«ã‚ˆã£ã¦æ¤œå‡ºã•ã‚ŒãŸæŠ•ç¨¿ã¯ã€æŒ‡å®šã•れãŸãƒªã‚¹ãƒˆã€ã¾ãŸã¯ãƒ›ãƒ¼ãƒ ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«è¿½åŠ ã•れã¾ã™ã€‚ +アンテナ専用ã®ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã‚‚存在ã—ã€ã“ã“ã§ã¯ã‚¢ãƒ³ãƒ†ãƒŠã§æ‹¾ã£ãŸæŠ•ç¨¿ãŒæµã‚Œã¾ã™ã€‚ã“れã¯ãƒªã‚¹ãƒˆã‚„ホームã«é…ç½®ã—ãªãã¦ã‚‚容易ã«ç¢ºèªã§ãã¾ã™ã€‚ + +ドメイン購読ã«ãŠã„ã¦ã€è‡ªåˆ†è‡ªèº«ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’指定ã§ãã‚‹ã“ã¨ã‚‚特長ã®ï¼‘ã¤ã§ã™ã€‚ã¾ãŸã€STL(ソーシャルタイムライン)モードã«ã‚‚対応ã—ã¦ã„ã¾ã™ã€‚ + +絞り込ã¾ã‚ŒãŸæŠ•稿ã¯ã€ã•らã«ãƒ‰ãƒ¡ã‚¤ãƒ³ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€ã‚­ãƒ¼ãƒ¯ãƒ¼ãƒ‰ã®ï¼”ç¨®é¡žã®æ¡ä»¶ã‚’指定ã—ã¦é™¤å¤–ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã“れã¯ORæ¡ä»¶ã¨ã—ã¦åƒãã¾ã™ã€‚ + +ã‚¢ãƒ³ãƒ†ãƒŠã®æ¡ä»¶æŒ‡å®šã¯è¤‡é›‘ã§ã™ãŒã€Webã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã«æ­è¼‰ã•れãŸç·¨é›†ç”»é¢ã§ã¯ã€äº‹å‰ã®èª¬æ˜ŽãŒãªãã¦ã‚‚æ¡ä»¶æŒ‡å®šã®è½ã¨ã—ç©´ã«æ°—ã¥ãã‚„ã™ã„よã†ã«ã—ã¦ã„ã¾ã™ã€‚ + +è‡ªåˆ†ã®æŠ•ç¨¿ãŒã‚¢ãƒ³ãƒ†ãƒŠã«æ¤œå‡ºã•れるã®ã‚’æ‹’å¦ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ã“ã®æ‹’å¦è¨­å®šã¯ã€ActivityPubã§ä»–サーãƒãƒ¼ã«ã‚‚共有ã•れã¾ã™ãŒã€å¯¾å¿œã™ã‚‹ã‹ã¯ãれãžã‚Œã®åˆ¤æ–­ã«å§”ã­ã‚‰ã‚Œã¾ã™ã€‚ + +### サークル + +自分ã®ãƒ•ォロワーã®ä¸­ã§ã‚‚特ã«å¯¾è±¡ã‚’絞ã£ã¦ã‚µãƒ¼ã‚¯ãƒ«ã¨ã„ã†å˜ä½ã«ã¾ã¨ã‚ã€å¯¾è±¡ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã¿ãŒé–²è¦§å¯èƒ½ãªæŠ•稿をé€ä¿¡ã§ãã¾ã™ã€‚ãŸã ã—ã“れã¯Mastodonサーãƒãƒ¼ã¨ã—ã‹å…±æœ‰ã§ãã¾ã›ã‚“。(4.2.0-rc1ç¾åœ¨ã€æœ¬å®¶Mastodonã§ã¯ãƒã‚°ã®ãŸã‚正常ã«å—ä¿¡ã§ãã¾ã›ã‚“) + +相互フォローé™å®šæŠ•稿ã«ã‚‚対応ã—ã¦ã„ã¾ã™ã€‚ + +### 期間é™å®šæŠ•稿 + +例ãˆã°`#exp10m`ã‚¿ã‚°ã‚’ã¤ã‘ã‚‹ã¨ã€ãã®æŠ•ç¨¿ã¯ 10 分後ã«è‡ªå‹•削除ã•れã¾ã™ã€‚ç§’ã€åˆ†ã€æ™‚ã€æ—¥ã®ï¼”ç¨®é¡žã®æŒ‡å®šã«å¯¾å¿œã€æ•°å€¤ã¯ï¼”æ¡ã¾ã§ã€5 åˆ†æœªæº€ã®æ™‚間指定もå¯èƒ½ã§ã™ãŒç·¨é›†ãŒçµ¡ã‚€ã¨æ„図通り動作ã—ãªã„ã“ã¨ãŒã‚りã¾ã™ã€‚ + +ã“ã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ãŒã¤ã„ãŸæŠ•ç¨¿ã‚’ç·¨é›†ã—ã¦ã‚‚ã€å®Ÿéš›ã«å‰Šé™¤ã•れる時刻ã¯ç·¨é›†æ™‚刻ã§ã¯ãªãã€æŠ•ç¨¿æ™‚åˆ»ã‹ã‚‰èµ·ç®—ã•れã¾ã™ã€‚ + +### グループ + +kmyblueã¯ã‚°ãƒ«ãƒ¼ãƒ—機能ã«å¯¾å¿œã—ã¦ã„ã¾ã™ã€‚グループã®ãƒ•ォロワーã‹ã‚‰ã‚°ãƒ«ãƒ¼ãƒ—アカウントã¸ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã¯ã™ã¹ã¦ãƒ–ーストã•れã€ã‚°ãƒ«ãƒ¼ãƒ—ã®ãƒ•ォロワー全員ã«å±Šãã¾ã™ã€‚ãªãŠã“ã‚Œã¯æœ¬å®¶Mastodonã§ã‚‚ä»Šå¾Œå®Ÿè£…äºˆå®šã®æ©Ÿèƒ½ã§ã™ã€‚ + +### ãƒ‰ãƒ¡ã‚¤ãƒ³ãƒ–ãƒ­ãƒƒã‚¯ã®æ‹¡å¼µ + +ドメインブロック機能ãŒå¤§å¹…ã«æ‹¡å¼µã•れã€ã€Œåˆ¶é™ã€ã€Œåœæ­¢ã€ã¨ã—ãªãã¦ã‚‚ã€ç´°åˆ†åŒ–ã•ã‚ŒãŸæ“作を個別ã«ãƒ–ロックã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã“れã«ã‚ˆã£ã¦ã€ç›¸æ‰‹ã‚µãƒ¼ãƒãƒ¼ã¨ã®äº¤æµãŒå®Œå…¨ã«é®æ–­ã•れるリスクを減らã—ã¾ã™ã€‚é©åˆ‡ã«ç®¡ç†ã•れã¦ãŠã‚‰ãšå–„良ãªãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨ã‚¹ãƒ‘ãƒ ï¼æ”»æ’ƒè€…ãŒåŒå±…ã™ã‚‹ã‚ˆã†ãªã‚µãƒ¼ãƒãƒ¼ã¸ã®å¯¾ç­–ã¨ã—ã¦æœ‰åйã§ã™ã€‚ + +- ãŠæ°—ã«å…¥ã‚Šãƒ»çµµæ–‡å­—リアクションã®ã¿ã‚’ブロック +- メンションã®ã¿ã‚’ブロック +- 相手ã‹ã‚‰ã®ãƒ•ォローを強制的ã«å¯©æŸ»åˆ¶ã«ã™ã‚‹ +- 相手ã‹ã‚‰ã®ãƒ•ォローを全ã¦ãƒ–ロック + +ä»–ã«ã‚‚ã€ã€Œè‡ªåˆ†ã®ã‚µãƒ¼ãƒãƒ¼ã®ç‰¹å®šæŠ•稿を相手サーãƒãƒ¼ã«é€ä¿¡ã—ãªã„ã€è¨­å®šãŒå¯èƒ½ã§ã™ã€‚ã“れã¯ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªæŠ•ç¨¿ãªã©ã‚’政治的ãªç†ç”±ã§é€ä¿¡ã™ã‚‹ã“ã¨ãŒé›£ã—ã„サーãƒãƒ¼ã¸ã®å¯¾ç­–ã¨ã—ã¦å®Ÿè£…ã—ã¾ã—ãŸã€‚ + +#### Misskey 対策 + +Misskey ãŠã‚ˆã³ãã®ãƒ•ォーク(Calckey ãªã©ï¼‰ã¯ã€**フォローã—ã¦ã„ãªã„ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æœªåŽè¼‰æŠ•稿**ã‚’è‡ªç”±ã«æ¤œç´¢ãƒ»è³¼èª­ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã“れã¯Mastodonã®è¨­è¨ˆã¨ã¯æ ¹æœ¬çš„ã«ç•°ãªã‚‹ä»•様ã§ã™ã€‚kmyblueã§ã¯ã€ãã®ã‚µãƒ¼ãƒãƒ¼ã«æœªåŽè¼‰æŠ•稿をé€ä¿¡ã™ã‚‹ã¨ãã«ã€Œãƒ•ォロワーã®ã¿ã€ã«å¤‰æ›´ã—ã¾ã™ã€‚ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã«ã¯æœªåŽè¼‰ã¨ã—ã¦é€ä¿¡ã•れã¾ã™ã€‚ã“ã®å‹•ä½œã¯æ–°è¦ç™»éŒ²ã—ãŸã°ã‹ã‚Šã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ãŠã„ã¦ã¯ãƒ‡ãƒ•ォルトã§ã¯ã‚ªãƒ•ã¨ãªã£ã¦ãŠã‚Šã€ãƒ¦ãƒ¼ã‚¶ãƒ¼å„自ã®è¨­å®šãŒå¿…è¦ã«ãªã‚Šã¾ã™ã€‚ + +### ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã®æ‹¡å¼µ + +管ç†ã®åŠ¹çŽ‡åŒ–ã€è¦ç´„é•å・法律é•åコンテンツã¸ã®è¿…速ãªå¯¾å¿œï¼ˆç‰¹ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆåœæ­¢ã‚’ä¼´ã‚ãšã«æ¸ˆã‚€ã‚ˆã†ã«ã™ã‚‹ã“ã¨ï¼‰ã‚’目的ã¨ã—ã¦ã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³æ©Ÿèƒ½ã‚’æ‹¡å¼µã—ã¦ã„ã¾ã™ã€‚ + +#### å„æŠ•ç¨¿ã®æ“作 + +å„æŠ•ç¨¿ã«ã¤ã„ã¦ã€å¼·åˆ¶çš„㪠CW 付与ã€å¼·åˆ¶çš„ãªç”»åƒ NSFW フラグ付与ã€ç·¨é›†å±¥æ­´ã®å‰Šé™¤ã€ç”»åƒã®å‰Šé™¤ã€æŠ•稿ã®å‰Šé™¤ãŒè¡Œãªãˆã¾ã™ã€‚æ“作ã¯å³æ™‚åæ˜ ã•れã¾ã™ã€‚ + +#### ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æ­£è¦è¡¨ç¾æ¤œç´¢ + +アカウントåã€è¡¨ç¤ºåã«ã¤ã„ã¦æ­£è¦è¡¨ç¾ã§æ¤œç´¢ã§ãã¾ã™ã€‚ãŸã ã—動作ã¯é‡ããªã‚Šã¾ã™ã€‚ + +#### 全画åƒã®é–²è¦§ + +「未åŽè¼‰ã€ã€Œãƒ•ォロワーã®ã¿ã€ã€ŒæŒ‡å®šã•れãŸç›¸æ‰‹ã®ã¿ã€å…¬é–‹ç¯„囲ã®ã‚‚ã®ã‚‚å«ã‚ã€ã™ã¹ã¦ã®ç”»åƒã‚’閲覧ã§ãã‚‹ç”»é¢ãŒã‚りã¾ã™ã€‚法令ã€ã‚µãƒ¼ãƒãƒ¼è¦ç´„ã«é•åã—ãŸç”»åƒã‚’見ã¤ã‘ã‚‹ãŸã‚ã«å¿…è¦ã§ã™ã€‚ + +### AI å­¦ç¿’ç¦æ­¢ãƒ¡ã‚¿ã‚¿ã‚° + +ユーザー生æˆã‚³ãƒ³ãƒ†ãƒ³ãƒ„ãŒå«ã¾ã‚Œã‚‹å…¨ã¦ã®ãƒšãƒ¼ã‚¸ã«ã€AI å­¦ç¿’ã‚’ç¦æ­¢ã™ã‚‹ãƒ¡ã‚¿ã‚¿ã‚°ã‚’挿入ã—ã¦ã„ã¾ã™ã€‚ãŸã ã—å„ユーザーã®ãƒ—ロフィールページ・投稿ページã«é™ã‚Šã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯å„自設定㧠AI å­¦ç¿’ç¦æ­¢ãƒ¡ã‚¿ã‚¿ã‚°ã‚’除去ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚ + +### リンクã®ãƒ†ã‚­ã‚¹ãƒˆã¨å®Ÿéš›ã®ãƒªãƒ³ã‚¯å…ˆã®ç•°ãªã‚‹ã‚‚ã®ã®å¼·èª¿è¡¨ç¤º + +Misskey ãŠã‚ˆã³ãã®ãƒ•ォーク(Calckey ãªã©ï¼‰ã§ã¯ã€MFM を利用ã™ã‚‹ã“ã¨ã«ã‚ˆã‚Šã€ä¾‹ãˆã°ã€Œhttps://google.co.jp/ ã€ã«å‘ã‘ãŸãƒªãƒ³ã‚¯ã‚’「https://www.yahoo.co.jp/ ã€ã¨ã„ã†ãƒ†ã‚­ã‚¹ãƒˆã§æŽ²è¼‰ã™ã‚‹ã“ã¨ãŒå®¹æ˜“ã§ã™ã€‚ãれã«é–¢ã™ã‚‹åŸºæœ¬çš„ãªè©æ¬ºã‚’見分ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ãŸã ã—実際ã«ãƒ•ã‚£ãƒƒã‚·ãƒ³ã‚°è©æ¬ºã¸ã®åŠ¹æžœãŒã‚ã‚‹ã‹ã¯ç–‘å•ã§ã™ã€‚ + +### æŠ•ç¥¨é …ç›®æ•°ã®æ‹¡å¼µ + +投票ã«ã¤ã„ã¦ã€æœ¬å®¶Mastodonã§ã¯ï¼”é …ç›®ã¾ã§ã§ã™ãŒã€kmyblueã§ã¯ï¼˜å€‹ã¾ã§ã«æ‹¡å¼µã—ã¦ã„ã¾ã™ã€‚ + +### 連åˆã‹ã‚‰é€ã‚‰ã‚Œã¦ãã‚‹æŠ•ç¨¿ã®æ·»ä»˜ç”»åƒæœ€å¤§æ•°ã®æ‹¡å¼µ + +本家Mastodonã§ã¯ï¼”個ã¾ã§ã§ã™ãŒã€kmyblueã§ã¯ï¼˜å€‹ã¾ã§ã«æ‹¡å¼µã—ã¦ã„ã¾ã™ã€‚ãŸã ã— Web クライアントã§ã®è¡¨ç¤ºã«ã¯ã€å„自ユーザーã«ã‚ˆã‚‹è¨­å®šãŒå¿…è¦ã§ã™ã€‚kmyblueローカルã‹ã‚‰æŠ•稿ã§ãã‚‹ç”»åƒã®æžšæ•°ã«å¤‰æ›´ã¯ã‚りã¾ã›ã‚“。 + +### æ¤œç´¢è¨±å¯ + +ユーザーã¯å„投稿ã«ã€Œæ¤œç´¢è¨±å¯ã€ãƒ‘ラメータを付与ã§ãã¾ã™ã€‚ã“ã“ã§ã€Œå…¬é–‹ã€ãŒæŒ‡å®šã•ã‚ŒãŸæŠ•ç¨¿ã¯ã€èª°ã§ã‚‚è‡ªç”±ã«æ¤œç´¢æ©Ÿèƒ½ã‚’用ã„ã¦æ¤œç´¢ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ï¼ˆå…¨æ–‡æ¤œç´¢ã«é™ã‚‹ï¼‰ã€‚検索許å¯ã«å¯¾å¿œã—ã¦ã„ãªã„クライアントアプリã‹ã‚‰æŠ•稿ã—ãŸæ™‚ã®å€¤ã¯ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè¨­å®šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + +ã“れ㯠Fedibird ã®ã€Œæ¤œç´¢ç¯„å›²ã€æ©Ÿèƒ½ã«å¯¾å¿œã—ã¾ã™ã€‚API ã«äº’æ›æ€§ã¯ã‚りã¾ã›ã‚“ãŒã€ActivityPub 仕様ã¯å…±é€šã—ã¦ã„ã¾ã™ã€‚ + +ãªãŠMisskeyã‹ã‚‰ã®æŠ•稿ã¯ã€æ¤œç´¢è¨±å¯ãŒè‡ªå‹•çš„ã«ã€Œå…¨ã¦ã€ã«ãªã‚Šã¾ã™ã€‚ + +### ãƒˆãƒ¬ãƒ³ãƒ‰ã®æ‹¡å¼µ + +本家マストドンã§ã¯ã€ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–フラグã®ã¤ã„ãŸå…¨ã¦ã®æŠ•稿ãŒãƒˆãƒ¬ãƒ³ãƒ‰ã«æŽ²è¼‰ã•れã¾ã›ã‚“。kmyblueã¯ãã®ä¸­ã§ã‚‚ã€ã€Œã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–フラグã¯ã¤ã„ã¦ã„ã‚‹ãŒã€ç”»åƒãŒæ·»ä»˜ã•れã¦ãŠã‚‰ãš CW 付ãã§ã‚‚ãªã„ã€æŠ•ç¨¿ã‚’ãƒˆãƒ¬ãƒ³ãƒ‰ã«æŽ²è¼‰ã—ã¾ã™ã€‚ + +本æ¥ã“ã®ã‚ˆã†ãªæŠ•稿ã¯ãƒˆãƒ¬ãƒ³ãƒ‰ã«æŽ²è¼‰ã™ã¹ãã§ã‚りã¾ã›ã‚“ãŒã€æœ¬å®¶Mastodonã® Web クライアントã§ã¯æ–‡ç« ã ã‘ã®æŠ•ç¨¿ã®ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãƒ•ãƒ©ã‚°ã‚’è‡ªç”±ã«æ“作ã§ããªã„ã“ã¨ã‚’ç†ç”±ã¨ã—ãŸç‹¬è‡ªå¯¾å¿œã¨ãªã‚Šã¾ã™ã€‚ + +### Sidekiq ヘルスãƒã‚§ãƒƒã‚¯ + +Sidekiq ã®ãƒ˜ãƒ«ã‚¹ãƒã‚§ãƒƒã‚¯ã‚’目的ã¨ã—ã¦ã€ï¼“ï¼ç§’ã«ï¼‘回ãšã¤æŒ‡å®šã•れ㟠URL ã« HEAD リクエストをé€ä¿¡ã—ã¾ã™ã€‚é€ä¿¡å…ˆ URL ã¯ã€ç’°å¢ƒå¤‰æ•°ï¼ˆã¾ãŸã¯`.env.production`)ã«`SIDEKIQ_HEALTH_FETCH_URL`ã¨ã—ã¦æŒ‡å®šã—ã¾ã™ã€‚ diff --git a/Rakefile b/Rakefile index 488c551fee..e51cf0e17e 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require_relative 'config/application' +require File.expand_path('config/application', __dir__) Rails.application.load_tasks diff --git a/SECURITY.md b/SECURITY.md index 993dcfb546..81472b01b4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,20 +1,20 @@ -# セキュリティãƒãƒªã‚·ãƒ¼ +# Security Policy -kmyblueã®ãƒ—ログラムã«ãŠã„ã¦ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚¤ãƒ³ã‚·ãƒ‡ãƒ³ãƒˆã‚’発見ã—ãŸå ´åˆã€kmyblueã«å ±å‘Šã—ã¦ãã ã•ã„。 +If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either: -kmyblueã«ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚¤ãƒ³ã‚·ãƒ‡ãƒ³ãƒˆã‚’報告ã™ã‚‹å ´åˆã€ä»¥ä¸‹ã®æ‰‹é †ã‚’è¸ã‚“ã§ãã ã•ã„。 +- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) +- reach us at -- [ã“ã¡ã‚‰ã®ãƒªãƒ³ã‚¯ã‹ã‚‰æ–°è¦ã‚¤ãƒ³ã‚·ãƒ‡ãƒ³ãƒˆã‚’起票ã—ã¦ãã ã•ã„](https://github.com/kmycode/mastodon/security/advisories/new) -- メール ã€ã¾ãŸã¯[@askyq@kmy.blue](https://kmy.blue/@askyq)å®›ã«ã€**セキュリティインシデントを起票ã—ãŸã“ã¨ã ã‘**を連絡ã—ã¦ãã ã•ã„。セキュリティインシデントã®å†…容ã¯ã€çµ¶å¯¾ã«é€£çµ¡ã«å«ã‚ãªã„ã§ãã ã•ã„(リンクãらã„ãªã‚‰å«ã‚ã¦ã„ã„ã‹ãªï¼‰ +You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. -ä»–ã®kmyblueフォークã®åˆ©ç”¨è€…ã®å®‰å…¨ã®ãŸã‚ã«å°‘ã—ã§ã‚‚時間稼ãŽã‚’ã—ãªã‘れã°ã„ã‘ãªã„ã®ã§ã€ã“ã®å•題をIssueã‚’å«ã‚€å…¬é–‹ã•れãŸå ´æ‰€ã§è¨˜è¿°ã—ãªã„ã§ãã ã•ã„。 +## Scope -## 範囲 +A "vulnerability in Mastodon" is a vulnerability in the code distributed through our main source code repository on GitHub. Vulnerabilities that are specific to a given installation (e.g. misconfiguration) should be reported to the owner of that installation and not us. -ã“ã¡ã‚‰ãŒå¯¾å¿œã§ãる範囲ã¯ã€å½“リãƒã‚¸ãƒˆãƒªã§å…¬é–‹ã—ã¦ã„るソースコードã®ã¿ã¨ãªã‚Šã¾ã™ã€‚当リãƒã‚¸ãƒˆãƒªã®ä¾å­˜ãƒ‘ッケージ内ã«å•題ãŒã‚ã‚‹å ´åˆã¯ã€ãã¡ã‚‰ã«å ±å‘Šã—ã¦ãã ã•ã„。 +## Supported Versions -ã‚‚ã—ã‚ãªãŸã«å°‚門知識ãŒã‚りã€ãã‚ŒãŒæœ¬å®¶Mastodonç”±æ¥ã®å•題ã§ã‚ã‚‹ã¨ä¿¡ã˜ã‚‹ã«è¶³ã‚‹æ ¹æ‹ ãŒã‚ã‚‹å ´åˆã€kmyblueã§ã¯ãªãMastodonã®ã»ã†ã«å ±å‘Šã—ã¦ãã ã•ã„。kmyblueã«å ±å‘Šã•れã¦ã‚‚ã€Mastodonより先ã«ä¿®æ­£ã—ã¦ã—ã¾ã†ã“ã¨ã§Mastodonã«ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ãƒªã‚¹ã‚¯ã‚’発生ã•ã›ã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã—ã€æœ¬å®¶Mastodonã®å¯¾å¿œã‚’å¾…ã¤ã«ã—ã¦ã‚‚kmyblueã®ã»ã†ã«æ¥ã¦ã—ã¾ã£ãŸã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚¤ãƒ³ã‚·ãƒ‡ãƒ³ãƒˆã®å¯¾å¿œã«å›°ã‚Šã¾ã™ï¼ˆæœ¬å®¶ãŒãªã‹ãªã‹å¯¾å¿œã—ã¦ãれãªã„å¯èƒ½æ€§ã‚’考ãˆã‚‹ã¨å‰Šé™¤ã—ã¥ã‚‰ã„)。もã—é–“é•ã£ã¦kmyblueã«æ¥ãŸå ´åˆã€kmyblue開発者ã®è²¬ä»»ã§æŒ¯ã‚Šåˆ†ã‘を行ã„ã¾ã™ã€‚ - -## サãƒãƒ¼ãƒˆã™ã‚‹ãƒãƒ¼ã‚¸ãƒ§ãƒ³ - -(サãƒãƒ¼ãƒˆæœŸé–“)[https://github.com/kmycode/mastodon/wiki/%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E6%9C%9F%E9%96%93] ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 +| Version | Supported | +| ------- | --------- | +| 4.2.x | Yes | +| 4.1.x | Yes | +| < 4.1 | No | diff --git a/Vagrantfile b/Vagrantfile index ce456060cd..4303f8e067 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -10,11 +10,7 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' # Add repo for NodeJS -sudo mkdir -p /etc/apt/keyrings -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -NODE_MAJOR=20 -echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list -sudo apt-get update +curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - # Add firewall rule to redirect 80 to PORT and save sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} @@ -116,11 +112,11 @@ bundle install # Install node modules sudo corepack enable -corepack prepare +yarn set version classic yarn install # Build Mastodon -export RAILS_ENV=development +export RAILS_ENV=development export $(cat ".env.vagrant" | xargs) bundle exec rails db:setup @@ -151,12 +147,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| vb.customize ["modifyvm", :id, "--nictype2", "virtio"] end - config.vm.provider :libvirt do |libvirt| - libvirt.cpus = 3 - libvirt.memory = 8192 - end - - # This uses the vagrant-hostsupdater plugin, and lets you # access the development site at http://mastodon.local. # If you change it, also change it in .env.vagrant before provisioning @@ -174,12 +164,11 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| if config.vm.networks.any? { |type, options| type == :private_network } config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1'] else - config.vm.synced_folder ".", "/vagrant", type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"] + config.vm.synced_folder ".", "/vagrant" end # Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080 config.vm.network :forwarded_port, guest: 3000, host: 3000 - config.vm.network :forwarded_port, guest: 3035, host: 3035 config.vm.network :forwarded_port, guest: 4000, host: 4000 config.vm.network :forwarded_port, guest: 8080, host: 8080 config.vm.network :forwarded_port, guest: 9200, host: 9200 @@ -195,7 +184,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.post_up_message = <(account) { account.searchable_properties }) field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }) field(:domain, type: 'keyword', value: ->(account) { account.domain || '' }) - field(:display_name, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('display_name', 'analyzer')) do - field :edge_ngram, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('display_name', 'edge_ngram', 'analyzer'), search_analyzer: ChewyConfig.instance.accounts_analyzers.dig('display_name', 'edge_ngram', 'search_analyzer') - end - field(:username, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('username', 'analyzer'), value: lambda { |account| - [account.username, account.domain].compact.join('@') - }) do - field :edge_ngram, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('username', 'edge_ngram', 'analyzer'), - search_analyzer: ChewyConfig.instance.accounts_analyzers.dig('username', 'edge_ngram', 'search_analyzer') - end - field(:text, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('text', 'analyzer'), value: ->(account) { account.searchable_text }) { field(:stemmed, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('text', 'stemmed', 'analyzer')) } + field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(account) { account.searchable_text }) end end diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb index b71406d3e3..0d53eb2f41 100644 --- a/app/chewy/public_statuses_index.rb +++ b/app/chewy/public_statuses_index.rb @@ -3,20 +3,156 @@ class PublicStatusesIndex < Chewy::Index include DatetimeClampingConcern - # ElasticSearch config is moved to "/config/elasticsearch.default.yml". - # Edit it when original Mastodon changed ElasticSearch config. - settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: ChewyConfig.instance.public_statuses + DEVELOPMENT_SETTINGS = { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + + analyzer: { + verbatim: { + tokenizer: 'uax_url_email', + filter: %w(lowercase), + }, + + content: { + tokenizer: 'standard', + filter: %w( + lowercase + asciifolding + cjk_width + elision + english_possessive_stemmer + english_stop + english_stemmer + ), + }, + + sudachi_analyzer: { + tokenizer: 'standard', + filter: %w( + lowercase + asciifolding + cjk_width + elision + english_possessive_stemmer + english_stop + english_stemmer + ), + }, + + hashtag: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + }, + }.freeze + + PRODUCTION_SETTINGS = { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + + my_posfilter: { + type: 'sudachi_part_of_speech', + stoptags: [ + '助詞', + '助動詞', + '補助記å·,å¥ç‚¹', + '補助記å·,読点', + ], + }, + }, + + analyzer: { + content: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + hashtag: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + + sudachi_analyzer: { + tokenizer: 'sudachi_tokenizer', + type: 'custom', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + my_posfilter + sudachi_normalizedform + ), + }, + }, + tokenizer: { + sudachi_tokenizer: { + resources_path: '/etc/elasticsearch/sudachi', + split_mode: 'A', + type: 'sudachi_tokenizer', + discard_punctuation: 'true', + }, + }, + }.freeze + + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: Rails.env.test? ? DEVELOPMENT_SETTINGS : PRODUCTION_SETTINGS index_scope ::Status.unscoped .kept .indexable - .includes(:media_attachments, :preloadable_poll, :tags, :account, preview_cards_status: :preview_card) + .includes(:media_attachments, :preloadable_poll, :preview_cards, :tags, :account) root date_detection: false do field(:id, type: 'long') field(:account_id, type: 'long') - field(:text, type: 'text', analyzer: ChewyConfig.instance.public_statuses_analyzers.dig('text', 'analyzer'), value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: ChewyConfig.instance.public_statuses_analyzers.dig('text', 'stemmed', 'analyzer')) } - field(:tags, type: 'text', analyzer: ChewyConfig.instance.public_statuses_analyzers.dig('tags', 'analyzer'), value: ->(status) { status.tags.map(&:display_name) }) + field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(status) { status.searchable_text }) + field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) }) field(:language, type: 'keyword') field(:domain, type: 'keyword', value: ->(status) { status.account.domain || '' }) field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 44cb86d755..647b35c001 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -3,13 +3,153 @@ class StatusesIndex < Chewy::Index include DatetimeClampingConcern - # ElasticSearch config is moved to "/config/elasticsearch.default.yml". - # Edit it when original Mastodon changed ElasticSearch config. - settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: ChewyConfig.instance.statuses + DEVELOPMENT_SETTINGS = { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { + verbatim: { + tokenizer: 'uax_url_email', + filter: %w(lowercase), + }, + + content: { + tokenizer: 'standard', + filter: %w( + lowercase + asciifolding + cjk_width + elision + english_possessive_stemmer + english_stop + english_stemmer + ), + }, + + sudachi_analyzer: { + tokenizer: 'standard', + filter: %w( + lowercase + asciifolding + cjk_width + elision + english_possessive_stemmer + english_stop + english_stemmer + ), + }, + + hashtag: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + }, + }.freeze + + PRODUCTION_SETTINGS = { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + + my_posfilter: { + type: 'sudachi_part_of_speech', + stoptags: [ + '助詞', + '助動詞', + '補助記å·,å¥ç‚¹', + '補助記å·,読点', + ], + }, + }, + analyzer: { + verbatim: { + tokenizer: 'uax_url_email', + filter: %w(lowercase), + }, + + content: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + hashtag: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + + sudachi_analyzer: { + tokenizer: 'sudachi_tokenizer', + type: 'custom', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + my_posfilter + sudachi_normalizedform + ), + }, + }, + tokenizer: { + sudachi_tokenizer: { + resources_path: '/etc/elasticsearch/sudachi', + split_mode: 'A', + type: 'sudachi_tokenizer', + discard_punctuation: 'true', + }, + }, + }.freeze + + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: Rails.env.test? ? DEVELOPMENT_SETTINGS : PRODUCTION_SETTINGS index_scope ::Status.unscoped.kept.without_reblogs.includes( :account, :media_attachments, + :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, @@ -17,7 +157,6 @@ class StatusesIndex < Chewy::Index :local_emoji_reacted, :tags, :local_referenced, - preview_cards_status: :preview_card, preloadable_poll: :local_voters ), delete_if: lambda { |status| @@ -31,8 +170,8 @@ class StatusesIndex < Chewy::Index root date_detection: false do field(:id, type: 'long') field(:account_id, type: 'long') - field(:text, type: 'text', analyzer: ChewyConfig.instance.statuses_analyzers.dig('text', 'analyzer'), value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: ChewyConfig.instance.statuses_analyzers.dig('text', 'stemmed', 'analyzer')) } - field(:tags, type: 'text', analyzer: ChewyConfig.instance.statuses_analyzers.dig('tags', 'analyzer'), value: ->(status) { status.tags.map(&:display_name) }) + field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(status) { status.searchable_text }) + field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) }) field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by }) field(:mentioned_by, type: 'long', value: ->(status) { status.mentioned_by }) field(:favourited_by, type: 'long', value: ->(status) { status.favourited_by }) diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb index 965718e83e..c99218a47f 100644 --- a/app/chewy/tags_index.rb +++ b/app/chewy/tags_index.rb @@ -3,9 +3,36 @@ class TagsIndex < Chewy::Index include DatetimeClampingConcern - # ElasticSearch config is moved to "/config/elasticsearch.default.yml". - # Edit it when original Mastodon changed ElasticSearch config. - settings index: index_preset(refresh_interval: '30s'), analysis: ChewyConfig.instance.tags + settings index: index_preset(refresh_interval: '30s'), analysis: { + analyzer: { + content: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + + edge_ngram: { + tokenizer: 'edge_ngram', + filter: %w( + lowercase + asciifolding + cjk_width + ), + }, + }, + + tokenizer: { + edge_ngram: { + type: 'edge_ngram', + min_gram: 2, + max_gram: 15, + }, + }, + } index_scope ::Tag.listable @@ -14,9 +41,7 @@ class TagsIndex < Chewy::Index end root date_detection: false do - field(:name, type: 'text', analyzer: ChewyConfig.instance.tags_analyzers.dig('name', 'analyzer'), value: :display_name) do - field(:edge_ngram, type: 'text', analyzer: ChewyConfig.instance.tags_analyzers.dig('name', 'edge_ngram', 'analyzer'), search_analyzer: ChewyConfig.instance.tags_analyzers.dig('name', 'edge_ngram', 'search_analyzer')) - end + field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') } field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }) field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }) field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }) diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index ffccf7a28e..c4b7e9c9d2 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -5,7 +5,15 @@ class AboutController < ApplicationController skip_before_action :require_functional! + before_action :set_instance_presenter + def show expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 98e68bd873..8135dcf481 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -18,6 +18,8 @@ class AccountsController < ApplicationController respond_to do |format| format.html do expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? + + @rss_url = rss_url end format.rss do @@ -25,7 +27,7 @@ class AccountsController < ApplicationController limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE @statuses = filtered_statuses.without_reblogs.limit(limit) - @statuses = preload_collection(@statuses, Status) + @statuses = cache_collection(@statuses, Status) end format.json do @@ -46,15 +48,13 @@ class AccountsController < ApplicationController end def default_statuses - if current_account.present? - @account.statuses.distributable_visibility - else - @account.statuses.distributable_visibility_for_anonymous - end + visibilities = [:public, :unlisted, :public_unlisted] + visibilities << :login unless current_account.nil? + @account.statuses.where(visibility: visibilities) end def only_media_scope - Status.joins(:media_attachments).merge(@account.media_attachments).group(:id) + Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) end def no_replies_scope @@ -86,21 +86,29 @@ class AccountsController < ApplicationController short_account_url(@account, format: 'rss') end end - helper_method :rss_url def media_requested? - path_without_format.end_with?('/media') && !tag_requested? + request.path.split('.').first.end_with?('/media') && !tag_requested? end def replies_requested? - path_without_format.end_with?('/with_replies') && !tag_requested? + request.path.split('.').first.end_with?('/with_replies') && !tag_requested? end def tag_requested? - path_without_format.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end - def path_without_format - request.path.split('.').first + def cached_filtered_status_page + cache_collection_paginated_by_id( + filtered_statuses, + Status, + PAGE_SIZE, + params_slice(:max_id, :min_id, :since_id) + ) + end + + def params_slice(*keys) + params.slice(*keys).permit(*keys) end end diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index c2563c492e..388d4b9e1d 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -1,9 +1,6 @@ # 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 diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb new file mode 100644 index 0000000000..339333e462 --- /dev/null +++ b/app/controllers/activitypub/claims_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ActivityPub::ClaimsController < ActivityPub::BaseController + include SignatureVerification + include AccountOwnedConcern + + skip_before_action :authenticate_user! + + before_action :require_account_signature! + before_action :set_claim_result + + def create + render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer + end + + private + + def set_claim_result + @claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id]) + end +end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index c80db3500d..2188eb72a3 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -1,6 +1,9 @@ # 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? @@ -18,10 +21,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_items case params[:id] when 'featured' - @items = for_signed_account { preload_collection(@account.pinned_statuses, Status) } + @items = for_signed_account { cache_collection(@account.pinned_statuses, Status) } @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } when 'tags' @items = for_signed_account { @account.featured_tags } + when 'devices' + @items = @account.devices else not_found end @@ -29,7 +34,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_size case params[:id] - when 'featured', 'tags' + when 'featured', 'devices', 'tags' @size = @items.size else not_found @@ -40,7 +45,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController case params[:id] when 'featured' @type = :ordered - when 'tags' + when 'devices', 'tags' @type = :unordered else not_found @@ -49,7 +54,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def collection_presenter ActivityPub::CollectionPresenter.new( - id: ActivityPub::TagManager.instance.collection_uri_for(@account, params[:id]), + id: account_collection_url(@account, params[:id]), type: @type, size: @size, items: @items diff --git a/app/controllers/activitypub/contexts_controller.rb b/app/controllers/activitypub/contexts_controller.rb deleted file mode 100644 index a3263ed82e..0000000000 --- a/app/controllers/activitypub/contexts_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class ActivityPub::ContextsController < ActivityPub::BaseController - include SignatureVerification - - vary_by -> { 'Signature' if authorized_fetch_mode? } - - before_action :set_context - - def show - expires_in 3.minutes, public: true - render json: @context, - serializer: ActivityPub::ContextSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json' - end - - private - - def set_context - @context = Conversation.find(params[:id]) - end -end diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 392dd36bcd..976caa3445 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -1,6 +1,9 @@ # 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! @@ -21,7 +24,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro end def set_items - @items = @account.followers.matches_uri_prefix(uri_prefix).pluck(:uri) + @items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri) end def collection_presenter diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 49cfc8ad1c..5ee85474e7 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -1,7 +1,9 @@ # 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! @@ -22,7 +24,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController def unknown_affected_account? json = Oj.load(body, mode: :strict) - json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor']) + json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? rescue Oj::ParseError false end @@ -60,10 +62,11 @@ 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 - params = SignatureParser.parse(raw_params) + tree = SignatureParamsParser.new.parse(raw_params) + params = SignatureParamsTransformer.new.apply(tree) ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params) - rescue SignatureParser::ParsingError + rescue Parslet::ParseFailed Rails.logger.warn 'Error parsing Collection-Synchronization header' end diff --git a/app/controllers/activitypub/likes_controller.rb b/app/controllers/activitypub/likes_controller.rb deleted file mode 100644 index 4aa6a4a771..0000000000 --- a/app/controllers/activitypub/likes_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class ActivityPub::LikesController < ActivityPub::BaseController - include Authorization - - vary_by -> { 'Signature' if authorized_fetch_mode? } - - before_action :require_account_signature!, if: :authorized_fetch_mode? - before_action :set_status - - def index - expires_in 0, public: @status.distributable? && public_fetch_mode? - render json: likes_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' - end - - private - - def pundit_user - signed_request_account - end - - def set_status - @status = @account.statuses.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def likes_collection_presenter - ActivityPub::CollectionPresenter.new( - id: account_status_likes_url(@account, @status), - type: :unordered, - size: @status.favourites_count - ) - end -end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 171161d491..cce10cfccc 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -3,6 +3,9 @@ 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? @@ -41,8 +44,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController end end - def outbox_url(...) - ActivityPub::TagManager.instance.outbox_uri_for(@account, ...) + def outbox_url(**kwargs) + if params[:account_username].present? + account_outbox_url(@account, **kwargs) + else + instance_actor_outbox_url(**kwargs) + end end def next_page @@ -56,7 +63,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_statuses return unless page_requested? - @statuses = preload_collection_paginated_by_id( + @statuses = cache_collection_paginated_by_id( AccountStatusesFilter.new(@account, signed_request_account).results, Status, LIMIT, diff --git a/app/controllers/activitypub/references_controller.rb b/app/controllers/activitypub/references_controller.rb index 7412540fe4..58c70e2771 100644 --- a/app/controllers/activitypub/references_controller.rb +++ b/app/controllers/activitypub/references_controller.rb @@ -31,7 +31,7 @@ class ActivityPub::ReferencesController < ActivityPub::BaseController end def cached_references - preload_collection(Status.where(id: results).reorder(:id), Status) + cache_collection(Status.where(id: results).reorder(:id), Status) end def results @@ -69,7 +69,6 @@ class ActivityPub::ReferencesController < ActivityPub::BaseController ActivityPub::CollectionPresenter.new( type: :unordered, id: ActivityPub::TagManager.instance.references_uri_for(@status), - size: @status.status_referred_by_count, first: page ) end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 0a19275d38..fc93c02cfb 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class ActivityPub::RepliesController < ActivityPub::BaseController + include SignatureVerification include Authorization + include AccountOwnedConcern DESCENDANTS_LIMIT = 60 @@ -31,7 +33,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_replies @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses - @replies = @replies.distributable_visibility.where(in_reply_to_id: @status.id) + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted, :public_unlisted, :login]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end diff --git a/app/controllers/activitypub/shares_controller.rb b/app/controllers/activitypub/shares_controller.rb deleted file mode 100644 index 65b4a5b383..0000000000 --- a/app/controllers/activitypub/shares_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class ActivityPub::SharesController < ActivityPub::BaseController - include Authorization - - vary_by -> { 'Signature' if authorized_fetch_mode? } - - before_action :require_account_signature!, if: :authorized_fetch_mode? - before_action :set_status - - def index - expires_in 0, public: @status.distributable? && public_fetch_mode? - render json: shares_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' - end - - private - - def pundit_user - signed_request_account - end - - def set_status - @status = @account.statuses.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def shares_collection_presenter - ActivityPub::CollectionPresenter.new( - id: account_status_shares_url(@account, @status), - type: :unordered, - size: @status.reblogs_count - ) - end -end diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb index 91849811e3..e674bf55a0 100644 --- a/app/controllers/admin/account_actions_controller.rb +++ b/app/controllers/admin/account_actions_controller.rb @@ -34,8 +34,7 @@ module Admin end def resource_params - params - .expect(admin_account_action: [:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses]) + params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses) end end end diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 7f65ced517..4f36f33f47 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -13,10 +13,10 @@ module Admin redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg') else @account = @account_moderation_note.target_account - @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account) + @moderation_notes = @account.targeted_moderation_notes.latest @warnings = @account.strikes.custom.latest - render 'admin/accounts/show' + render template: 'admin/accounts/show' end end @@ -29,8 +29,10 @@ module Admin private def resource_params - params - .expect(account_moderation_note: [:content, :target_account_id]) + params.require(:account_moderation_note).permit( + :content, + :target_account_id + ) end def set_account_moderation_note diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index a779a0cf51..9beb8fde6b 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -3,13 +3,13 @@ module Admin class AccountsController < BaseController before_action :set_account, except: [:index, :batch] - before_action :require_remote_account!, only: [:redownload, :approve_remote, :reject_remote] + before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index authorize :account, :index? - @accounts = filtered_accounts.page(params[:page]).without_count + @accounts = filtered_accounts.page(params[:page]) @form = Form::AccountBatch.new end @@ -33,7 +33,7 @@ module Admin @deletion_request = @account.deletion_request @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) - @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account) + @moderation_notes = @account.targeted_moderation_notes.latest @warnings = @account.strikes.includes(:target_account, :account, :appeal).latest @domain_block = DomainBlock.rule_for(@account.domain) end @@ -66,20 +66,6 @@ module Admin redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) end - def approve_remote - authorize @account, :approve_remote? - @account.approve_remote! - log_action :approve_remote, @account - redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) - end - - def reject_remote - authorize @account, :reject_remote? - @account.reject_remote! - log_action :reject_remote, @account - redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) - end - def destroy authorize @account, :destroy? Admin::AccountDeletionWorker.perform_async(@account.id) @@ -172,8 +158,7 @@ module Admin end def form_account_batch_params - params - .expect(form_account_batch: [:action, account_ids: []]) + params.require(:form_account_batch).permit(:action, account_ids: []) end def action_from_button @@ -183,12 +168,6 @@ module Admin 'approve' elsif params[:reject] 'reject' - elsif params[:approve_remote] - 'approve_remote' - elsif params[:approve_remote_domain] - 'approve_remote_domain' - elsif params[:reject_remote] - 'reject_remote' end end end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 8b8e83fde7..42edec15a3 100644 --- a/app/controllers/admin/action_logs_controller.rb +++ b/app/controllers/admin/action_logs_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :audit_log, :index? - @auditable_accounts = Account.auditable.select(:id, :username) + @auditable_accounts = Account.where(id: Admin::ActionLog.reorder(nil).select('distinct account_id')).select(:id, :username) end private diff --git a/app/controllers/admin/announcements/distributions_controller.rb b/app/controllers/admin/announcements/distributions_controller.rb deleted file mode 100644 index 4bd8769834..0000000000 --- a/app/controllers/admin/announcements/distributions_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Admin::Announcements::DistributionsController < Admin::BaseController - before_action :set_announcement - - def create - authorize @announcement, :distribute? - @announcement.touch(:notification_sent_at) - Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id) - redirect_to admin_announcements_path - end - - private - - def set_announcement - @announcement = Announcement.find(params[:announcement_id]) - end -end diff --git a/app/controllers/admin/announcements/previews_controller.rb b/app/controllers/admin/announcements/previews_controller.rb deleted file mode 100644 index d77f931a7f..0000000000 --- a/app/controllers/admin/announcements/previews_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class Admin::Announcements::PreviewsController < Admin::BaseController - before_action :set_announcement - - def show - authorize @announcement, :distribute? - @user_count = @announcement.scope_for_notification.count - end - - private - - def set_announcement - @announcement = Announcement.find(params[:announcement_id]) - end -end diff --git a/app/controllers/admin/announcements/tests_controller.rb b/app/controllers/admin/announcements/tests_controller.rb deleted file mode 100644 index f2457eb23a..0000000000 --- a/app/controllers/admin/announcements/tests_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class Admin::Announcements::TestsController < Admin::BaseController - before_action :set_announcement - - def create - authorize @announcement, :distribute? - UserMailer.announcement_published(current_user, @announcement).deliver_later! - redirect_to admin_announcements_path - end - - private - - def set_announcement - @announcement = Announcement.find(params[:announcement_id]) - end -end diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index eaf84aab25..8f9708183a 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -6,7 +6,6 @@ class Admin::AnnouncementsController < Admin::BaseController def index authorize :announcement, :index? - @published_announcements_count = Announcement.published.async_count end def new @@ -84,7 +83,6 @@ class Admin::AnnouncementsController < Admin::BaseController end def resource_params - params - .expect(announcement: [:text, :scheduled_at, :starts_at, :ends_at, :all_day]) + params.require(:announcement).permit(:text, :scheduled_at, :starts_at, :ends_at, :all_day) end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 14338dd293..4b5afbe157 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -7,14 +7,19 @@ module Admin layout 'admin' - before_action :set_referrer_policy_header + before_action :set_body_classes + before_action :set_cache_headers after_action :verify_authorized private - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'same-origin' + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end def set_user diff --git a/app/controllers/admin/change_emails_controller.rb b/app/controllers/admin/change_emails_controller.rb index c923b94b1a..a689d3a530 100644 --- a/app/controllers/admin/change_emails_controller.rb +++ b/app/controllers/admin/change_emails_controller.rb @@ -41,8 +41,9 @@ module Admin end def resource_params - params - .expect(user: [:unconfirmed_email]) + params.require(:user).permit( + :unconfirmed_email + ) end end end diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 702550eecc..6f4e426797 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -3,11 +3,11 @@ module Admin class ConfirmationsController < BaseController before_action :set_user - before_action :redirect_confirmed_user, only: [:resend], if: :user_confirmed? + before_action :check_confirmation, only: [:resend] def create authorize @user, :confirm? - @user.mark_email_as_confirmed! + @user.confirm! log_action :confirm, @user redirect_to admin_accounts_path end @@ -25,13 +25,11 @@ module Admin private - def redirect_confirmed_user - flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') - redirect_to admin_accounts_path - end - - def user_confirmed? - @user.confirmed? + def check_confirmation + if @user.confirmed? + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 596b167249..507acde98a 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -2,12 +2,10 @@ module Admin class CustomEmojisController < BaseController - before_action :set_custom_emoji, only: [:edit, :update] - def index authorize :custom_emoji, :index? - @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]).without_count + @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @form = Form::CustomEmojiBatch.new end @@ -17,10 +15,6 @@ module Admin @custom_emoji = CustomEmoji.new end - def edit - authorize :custom_emoji, :create? - end - def create authorize :custom_emoji, :create? @@ -34,19 +28,6 @@ module Admin end end - def update - authorize :custom_emoji, :create? - - @custom_emoji.assign_attributes(update_params) - - if @custom_emoji.save - log_action :update, @custom_emoji - redirect_to admin_custom_emojis_path(filter_params), notice: I18n.t('admin.custom_emojis.updated_msg') - else - render :new - end - end - def batch authorize :custom_emoji, :index? @@ -62,18 +43,8 @@ module Admin private - def set_custom_emoji - @custom_emoji = CustomEmoji.find(params[:id]) - end - def resource_params - params - .expect(custom_emoji: [:shortcode, :image, :category_id, :visible_in_picker, :aliases_raw, :license]) - end - - def update_params - params - .expect(custom_emoji: [:category_id, :visible_in_picker, :aliases_raw, :license]) + params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) end def filtered_custom_emojis @@ -103,8 +74,7 @@ module Admin end def form_custom_emoji_batch_params - params - .expect(form_custom_emoji_batch: [:action, :category_id, :category_name, custom_emoji_ids: []]) + params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, :aliases_raw, custom_emoji_ids: []) end end end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 5b0867dcfb..3a6df662ea 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -7,12 +7,12 @@ module Admin def index authorize :dashboard, :index? - @pending_appeals_count = Appeal.pending.async_count - @pending_reports_count = Report.unresolved.async_count - @pending_tags_count = Tag.pending_review.async_count - @pending_users_count = User.pending.async_count @system_checks = Admin::SystemCheck.perform(current_user) @time_period = (29.days.ago.to_date...Time.now.utc.to_date) + @pending_users_count = User.pending.count + @pending_reports_count = Report.unresolved.count + @pending_tags_count = Tag.pending_review.count + @pending_appeals_count = Appeal.pending.count end end end diff --git a/app/controllers/admin/disputes/appeals_controller.rb b/app/controllers/admin/disputes/appeals_controller.rb index 0c41553676..32e5e2f6fd 100644 --- a/app/controllers/admin/disputes/appeals_controller.rb +++ b/app/controllers/admin/disputes/appeals_controller.rb @@ -6,7 +6,6 @@ class Admin::Disputes::AppealsController < Admin::BaseController def index authorize :appeal, :index? - @pending_appeals_count = Appeal.pending.async_count @appeals = filtered_appeals.page(params[:page]) end @@ -21,7 +20,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController authorize @appeal, :approve? log_action :reject, @appeal @appeal.reject!(current_account) - UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later + UserMailer.appeal_rejected(@appeal.account.user, @appeal) redirect_to disputes_strike_path(@appeal.strike) end diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb index 913c1a8246..b0f139e3a8 100644 --- a/app/controllers/admin/domain_allows_controller.rb +++ b/app/controllers/admin/domain_allows_controller.rb @@ -37,7 +37,6 @@ class Admin::DomainAllowsController < Admin::BaseController end def resource_params - params - .expect(domain_allow: [:domain]) + params.require(:domain_allow).permit(:domain) end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 520db814f2..11ea8fb566 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -4,28 +4,6 @@ module Admin class DomainBlocksController < BaseController before_action :set_domain_block, only: [:destroy, :edit, :update] - PERMITTED_PARAMS = %i( - block_trends - detect_invalid_subscription - domain - hidden - obfuscate - private_comment - public_comment - reject_favourite - reject_friend - reject_hashtag - reject_media - reject_new_follow - reject_reply_exclude_followers - reject_reports - reject_send_sensitive - reject_straight_follow - severity - ).freeze - - PERMITTED_UPDATE_PARAMS = PERMITTED_PARAMS.without(:domain).freeze - def batch authorize :domain_block, :create? @form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) @@ -35,9 +13,7 @@ module Admin rescue Mastodon::NotPermittedError flash[:alert] = I18n.t('admin.domain_blocks.not_permitted') else - flash[:notice] = I18n.t('admin.domain_blocks.created_msg') - ensure - redirect_to admin_instances_path(limited: '1') + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') end def new @@ -57,7 +33,7 @@ module Admin # Disallow accidentally downgrading a domain block if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) - @domain_block.validate + @domain_block.save flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) return render :new @@ -112,28 +88,18 @@ module Admin end def update_params - params - .require(:domain_block) - .slice(*PERMITTED_UPDATE_PARAMS) - .permit(*PERMITTED_UPDATE_PARAMS) + params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, + :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) end def resource_params - params - .require(:domain_block) - .slice(*PERMITTED_PARAMS) - .permit(*PERMITTED_PARAMS) + params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, + :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) end def form_domain_block_batch_params - params - .expect( - form_domain_block_batch: [ - domain_blocks_attributes: [[:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate, - :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, - :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :hidden]], - ] - ) + params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, + :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden]) end def action_from_button diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 12f221164f..4a3228ec30 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -5,7 +5,7 @@ module Admin def index authorize :email_domain_block, :index? - @email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page]) + @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) @form = Form::EmailDomainBlockBatch.new end @@ -38,9 +38,9 @@ module Admin log_action :create, @email_domain_block (@email_domain_block.other_domains || []).uniq.each do |domain| - next if EmailDomainBlock.exists?(domain: domain) + next if EmailDomainBlock.where(domain: domain).exists? - other_email_domain_block = EmailDomainBlock.create!(domain: domain, allow_with_approval: @email_domain_block.allow_with_approval, parent: @email_domain_block) + other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block) log_action :create, other_email_domain_block end end @@ -58,17 +58,18 @@ module Admin private def set_resolved_records - @resolved_records = DomainResource.new(@email_domain_block.domain).mx + Resolv::DNS.open do |dns| + dns.timeouts = 5 + @resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a + end end def resource_params - params - .expect(email_domain_block: [:domain, :allow_with_approval, other_domains: []]) + params.require(:email_domain_block).permit(:domain, other_domains: []) end def form_email_domain_block_batch_params - params - .expect(form_email_domain_block_batch: [email_domain_block_ids: []]) + params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: []) end def action_from_button diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb index ca88c6525e..adfc39da21 100644 --- a/app/controllers/admin/export_domain_allows_controller.rb +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -4,7 +4,7 @@ require 'csv' module Admin class ExportDomainAllowsController < BaseController - include Admin::ExportControllerConcern + include AdminExportControllerConcern before_action :set_dummy_import!, only: [:new] diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index 8d7350c765..816422d4ff 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -4,7 +4,7 @@ require 'csv' module Admin class ExportDomainBlocksController < BaseController - include Admin::ExportControllerConcern + include AdminExportControllerConcern before_action :set_dummy_import!, only: [:new] @@ -36,17 +36,7 @@ module Admin reject_reports: row.fetch('#reject_reports', false), private_comment: @global_private_comment, public_comment: row['#public_comment'], - obfuscate: row.fetch('#obfuscate', false), - reject_favourite: row.fetch('#reject_favourite', false), - reject_send_sensitive: row.fetch('#reject_send_sensitive', false), - reject_hashtag: row.fetch('#reject_hashtag', false), - reject_straight_follow: row.fetch('#reject_straight_follow', false), - reject_new_follow: row.fetch('#reject_new_follow', false), - hidden: row.fetch('#hidden', false), - detect_invalid_subscription: row.fetch('#detect_invalid_subscription', false), - reject_reply_exclude_followers: row.fetch('#reject_reply_exclude_followers', false), - reject_friend: row.fetch('#reject_friend', false), - block_trends: row.fetch('#block_trends', false)) + obfuscate: row.fetch('#obfuscate', false)) if domain_block.invalid? flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: domain_block.errors.full_messages.join(', ')) @@ -59,7 +49,7 @@ module Admin next end - @warning_domains = instances_from_imported_blocks.pluck(:domain) + @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) rescue ActionController::ParameterMissing flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') set_dummy_import! @@ -68,56 +58,18 @@ module Admin private - def instances_from_imported_blocks - Instance.with_domain_follows(@domain_blocks.map(&:domain)) - end - def export_filename 'domain_blocks.csv' end def export_headers - %w( - #domain - #severity - #reject_media - #reject_reports - #public_comment - #obfuscate - #reject_favourite - #reject_send_sensitive - #reject_hashtag - #reject_straight_follow - #reject_new_follow - #hidden - #detect_invalid_subscription - #reject_reply_exclude_followers - #reject_friend - #block_trends - ) + %w(#domain #severity #reject_media #reject_reports #public_comment #obfuscate) end def export_data CSV.generate(headers: export_headers, write_headers: true) do |content| - DomainBlock.with_limitations.order(id: :asc).each do |instance| - content << [ - instance.domain, - instance.severity, - instance.reject_media, - instance.reject_reports, - instance.public_comment, - instance.obfuscate, - instance.reject_favourite, - instance.reject_send_sensitive, - instance.reject_hashtag, - instance.reject_straight_follow, - instance.reject_new_follow, - instance.hidden, - instance.detect_invalid_subscription, - instance.reject_reply_exclude_followers, - instance.reject_friend, - instance.block_trends, - ] + DomainBlock.with_limitations.each do |instance| + content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] end end end diff --git a/app/controllers/admin/fasp/debug/callbacks_controller.rb b/app/controllers/admin/fasp/debug/callbacks_controller.rb deleted file mode 100644 index 28aba5e489..0000000000 --- a/app/controllers/admin/fasp/debug/callbacks_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::Debug::CallbacksController < Admin::BaseController - def index - authorize [:admin, :fasp, :provider], :update? - - @callbacks = Fasp::DebugCallback - .includes(:fasp_provider) - .order(created_at: :desc) - end - - def destroy - authorize [:admin, :fasp, :provider], :update? - - callback = Fasp::DebugCallback.find(params[:id]) - callback.destroy - - redirect_to admin_fasp_debug_callbacks_path - end -end diff --git a/app/controllers/admin/fasp/debug_calls_controller.rb b/app/controllers/admin/fasp/debug_calls_controller.rb deleted file mode 100644 index 1e1b6dbf3c..0000000000 --- a/app/controllers/admin/fasp/debug_calls_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::DebugCallsController < Admin::BaseController - before_action :set_provider - - def create - authorize [:admin, @provider], :update? - - @provider.perform_debug_call - - redirect_to admin_fasp_providers_path - end - - private - - def set_provider - @provider = Fasp::Provider.find(params[:provider_id]) - end -end diff --git a/app/controllers/admin/fasp/providers_controller.rb b/app/controllers/admin/fasp/providers_controller.rb deleted file mode 100644 index 4f1f1271bf..0000000000 --- a/app/controllers/admin/fasp/providers_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::ProvidersController < Admin::BaseController - before_action :set_provider, only: [:show, :edit, :update, :destroy] - - def index - authorize [:admin, :fasp, :provider], :index? - - @providers = Fasp::Provider.order(confirmed: :asc, created_at: :desc) - end - - def show - authorize [:admin, @provider], :show? - end - - def edit - authorize [:admin, @provider], :update? - end - - def update - authorize [:admin, @provider], :update? - - if @provider.update(provider_params) - redirect_to admin_fasp_providers_path - else - render :edit - end - end - - def destroy - authorize [:admin, @provider], :destroy? - - @provider.destroy - - redirect_to admin_fasp_providers_path - end - - private - - def provider_params - params.expect(fasp_provider: [capabilities_attributes: {}]) - end - - def set_provider - @provider = Fasp::Provider.find(params[:id]) - end -end diff --git a/app/controllers/admin/fasp/registrations_controller.rb b/app/controllers/admin/fasp/registrations_controller.rb deleted file mode 100644 index 52c46c2eb6..0000000000 --- a/app/controllers/admin/fasp/registrations_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::RegistrationsController < Admin::BaseController - before_action :set_provider - - def new - authorize [:admin, @provider], :create? - end - - def create - authorize [:admin, @provider], :create? - - @provider.update_info!(confirm: true) - - redirect_to edit_admin_fasp_provider_path(@provider) - end - - private - - def set_provider - @provider = Fasp::Provider.find(params[:provider_id]) - end -end diff --git a/app/controllers/admin/follow_recommendations_controller.rb b/app/controllers/admin/follow_recommendations_controller.rb index b060cfbe94..841e3cc7fb 100644 --- a/app/controllers/admin/follow_recommendations_controller.rb +++ b/app/controllers/admin/follow_recommendations_controller.rb @@ -8,7 +8,7 @@ module Admin authorize :follow_recommendation, :show? @form = Form::AccountBatch.new - @accounts = filtered_follow_recommendations.page(params[:page]) + @accounts = filtered_follow_recommendations end def update @@ -37,8 +37,7 @@ module Admin end def form_account_batch_params - params - .expect(form_account_batch: [:action, account_ids: []]) + params.require(:form_account_batch).permit(:action, account_ids: []) end def filter_params diff --git a/app/controllers/admin/friend_servers_controller.rb b/app/controllers/admin/friend_servers_controller.rb deleted file mode 100644 index ec41ba672c..0000000000 --- a/app/controllers/admin/friend_servers_controller.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -module Admin - class FriendServersController < BaseController - before_action :set_friend, except: [:index, :new, :create] - before_action :warn_signatures_not_enabled!, only: [:new, :edit, :create, :follow, :unfollow, :accept, :reject] - - def index - authorize :friend_server, :update? - @friends = FriendDomain.all - end - - def new - authorize :friend_server, :update? - @friend = FriendDomain.new - end - - def edit - authorize :friend_server, :update? - end - - def create - authorize :friend_server, :update? - - @friend = FriendDomain.new(resource_params) - - if @friend.save - @friend.follow! - redirect_to admin_friend_servers_path - else - render action: :new - end - end - - def update - authorize :friend_server, :update? - - if @friend.update(update_resource_params) - redirect_to admin_friend_servers_path - else - render action: :edit - end - end - - def destroy - authorize :friend_server, :update? - @friend.destroy - redirect_to admin_friend_servers_path - end - - def follow - authorize :friend_server, :update? - @friend.follow! - render action: :edit - end - - def unfollow - authorize :friend_server, :update? - @friend.unfollow! - render action: :edit - end - - def accept - authorize :friend_server, :update? - @friend.accept! - render action: :edit - end - - def reject - authorize :friend_server, :update? - @friend.reject! - render action: :edit - end - - private - - def set_friend - @friend = FriendDomain.find(params[:id]) - end - - def resource_params - params.expect(friend_domain: [:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts]) - end - - def update_resource_params - params.expect(friend_domain: [:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts]) - end - - def warn_signatures_not_enabled! - flash.now[:error] = I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode? - end - end -end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index a48c4773ed..e5a55de06d 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -5,8 +5,6 @@ module Admin before_action :set_instances, only: :index before_action :set_instance, except: :index - LOGS_LIMIT = 5 - def index authorize :instance, :index? preload_delivery_failures! @@ -15,7 +13,6 @@ module Admin def show authorize :instance, :show? @time_period = (6.days.ago.to_date...Time.now.utc.to_date) - @action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT) end def destroy @@ -52,7 +49,7 @@ module Admin private def set_instance - @instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip)) + @instance = Instance.find(TagManager.instance.normalize_domain(params[:id]&.strip)) end def set_instances diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb index ac4ee35271..dabfe97655 100644 --- a/app/controllers/admin/invites_controller.rb +++ b/app/controllers/admin/invites_controller.rb @@ -32,15 +32,14 @@ module Admin def deactivate_all authorize :invite, :deactivate_all? - Invite.available.in_batches.touch_all(:expires_at) + Invite.available.in_batches.update_all(expires_at: Time.now.utc) redirect_to admin_invites_path end private def resource_params - params - .expect(invite: [:max_uses, :expires_in]) + params.require(:invite).permit(:max_uses, :expires_in) end def filtered_invites diff --git a/app/controllers/admin/ip_blocks_controller.rb b/app/controllers/admin/ip_blocks_controller.rb index afabda1b88..1bd7ec8059 100644 --- a/app/controllers/admin/ip_blocks_controller.rb +++ b/app/controllers/admin/ip_blocks_controller.rb @@ -44,8 +44,7 @@ module Admin private def resource_params - params - .expect(ip_block: [:ip, :severity, :comment, :expires_in]) + params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in) end def action_from_button @@ -53,8 +52,7 @@ module Admin end def form_ip_block_batch_params - params - .expect(form_ip_block_batch: [ip_block_ids: []]) + params.require(:form_ip_block_batch).permit(ip_block_ids: []) end end end diff --git a/app/controllers/admin/ng_rule_histories_controller.rb b/app/controllers/admin/ng_rule_histories_controller.rb deleted file mode 100644 index 9dccefaf49..0000000000 --- a/app/controllers/admin/ng_rule_histories_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgRuleHistoriesController < BaseController - before_action :set_ng_rule - before_action :set_histories - - PER_PAGE = 20 - - def show - authorize :ng_words, :show? - end - - private - - def set_ng_rule - @ng_rule = ::NgRule.find(params[:id]) - end - - def set_histories - @histories = NgRuleHistory.where(ng_rule_id: params[:id]).order(id: :desc).page(params[:page]).per(PER_PAGE) - end - end -end diff --git a/app/controllers/admin/ng_rules_controller.rb b/app/controllers/admin/ng_rules_controller.rb deleted file mode 100644 index 0bdda41c0c..0000000000 --- a/app/controllers/admin/ng_rules_controller.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgRulesController < BaseController - before_action :set_ng_rule, only: [:edit, :update, :destroy, :duplicate] - - def index - authorize :ng_words, :show? - - @ng_rules = ::NgRule.order(id: :asc) - end - - def new - authorize :ng_words, :show? - - @ng_rule = ::NgRule.build - end - - def edit - authorize :ng_words, :show? - end - - def create - authorize :ng_words, :create? - - begin - test_words! - rescue - flash[:alert] = I18n.t('admin.ng_rules.test_error') - redirect_to new_admin_ng_rule_path - return - end - - @ng_rule = ::NgRule.build(resource_params) - - if @ng_rule.save - redirect_to admin_ng_rules_path - else - render :new - end - end - - def update - authorize :ng_words, :create? - - begin - test_words! - rescue - flash[:alert] = I18n.t('admin.ng_rules.test_error') - redirect_to edit_admin_ng_rule_path(id: @ng_rule.id) - return - end - - if @ng_rule.update(resource_params) - redirect_to admin_ng_rules_path - else - render :edit - end - end - - def duplicate - authorize :ng_words, :create? - - @ng_rule = @ng_rule.copy! - - flash[:alert] = I18n.t('admin.ng_rules.copy_error') unless @ng_rule.save - - redirect_to admin_ng_rules_path - end - - def destroy - authorize :ng_words, :create? - - @ng_rule.destroy - redirect_to admin_ng_rules_path - end - - private - - def set_ng_rule - @ng_rule = ::NgRule.find(params[:id]) - end - - def resource_params - params.expect(ng_rule: [:title, :expires_in, :available, :account_domain, :account_username, :account_display_name, - :account_note, :account_field_name, :account_field_value, :account_avatar_state, - :account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag, - :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, - :status_mention_state, :status_reference_state, - :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, - :status_mention_threshold, :status_allow_follower_mention, - :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, - :status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local, - status_visibility: [], status_searchability: [], reaction_type: []]) - end - - def test_words! - arr = [ - resource_params[:account_domain], - resource_params[:account_username], - resource_params[:account_display_name], - resource_params[:account_note], - resource_params[:account_field_name], - resource_params[:account_field_value], - resource_params[:status_spoiler_text], - resource_params[:status_text], - resource_params[:status_tag], - resource_params[:emoji_reaction_name], - resource_params[:emoji_reaction_origin_domain], - ].compact_blank.join("\n") - - Admin::NgRule.extract_test!(arr) if arr.present? - end - end -end diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb deleted file mode 100644 index 10969204e8..0000000000 --- a/app/controllers/admin/ng_words/keywords_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWords::KeywordsController < NgWordsController - def show - super - @ng_words = ::NgWord.caches.presence || [::NgWord.new] - end - - protected - - def validate - begin - ::NgWord.save_from_raws(settings_params_test) - return true - rescue - flash[:alert] = I18n.t('admin.ng_words.test_error') - redirect_to after_update_redirect_path - end - - false - end - - def avoid_save? - true - end - - private - - def after_update_redirect_path - admin_ng_words_keywords_path - end - end -end diff --git a/app/controllers/admin/ng_words/settings_controller.rb b/app/controllers/admin/ng_words/settings_controller.rb deleted file mode 100644 index 63edadfce5..0000000000 --- a/app/controllers/admin/ng_words/settings_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWords::SettingsController < NgWordsController - protected - - def after_update_redirect_path - admin_ng_words_settings_path - end - end -end diff --git a/app/controllers/admin/ng_words/white_list_controller.rb b/app/controllers/admin/ng_words/white_list_controller.rb deleted file mode 100644 index 8fdb7df327..0000000000 --- a/app/controllers/admin/ng_words/white_list_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWords::WhiteListController < NgWordsController - def show - super - @white_list_domains = SpecifiedDomain.white_list_domain_caches.presence || [SpecifiedDomain.new] - end - - protected - - def validate - begin - SpecifiedDomain.save_from_raws_as_white_list(settings_params_list) - return true - rescue - flash[:alert] = I18n.t('admin.ng_words.save_error') - redirect_to after_update_redirect_path - end - - false - end - - def after_update_redirect_path - admin_ng_words_white_list_path - end - - private - - def settings_params_list - params.require(:form_admin_settings)[:specified_domains] - end - end -end diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb index 9e437f8c8b..e26ca96b8c 100644 --- a/app/controllers/admin/ng_words_controller.rb +++ b/app/controllers/admin/ng_words_controller.rb @@ -11,10 +11,10 @@ module Admin def create authorize :ng_words, :create? - return unless validate - - if avoid_save? - flash[:notice] = I18n.t('generic.changes_saved_msg') + begin + test_words + rescue + flash[:alert] = I18n.t('admin.ng_words.test_error') redirect_to after_update_redirect_path return end @@ -29,28 +29,19 @@ module Admin end end - protected + private - def validate - true + def test_words + ng_words = settings_params['ng_words'].split(/\r\n|\r|\n/) + Admin::NgWord.reject_with_custom_words?('Sample text', ng_words) end def after_update_redirect_path admin_ng_words_path end - def avoid_save? - false - end - - private - def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) - end - - def settings_params_test - params.expect(form_admin_settings: [ng_words_test: [keywords: [], regexps: [], strangers: [], temporary_ids: []]])['ng_words_test'] + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/admin/ngword_histories_controller.rb b/app/controllers/admin/ngword_histories_controller.rb deleted file mode 100644 index 90f13db2fe..0000000000 --- a/app/controllers/admin/ngword_histories_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgwordHistoriesController < BaseController - before_action :set_histories - - PER_PAGE = 20 - - def index - authorize :ng_words, :show? - end - - private - - def set_histories - @histories = NgwordHistory.order(id: :desc).page(params[:page]).per(PER_PAGE) - end - end -end diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb index 9a796949de..c1297c8b99 100644 --- a/app/controllers/admin/relays_controller.rb +++ b/app/controllers/admin/relays_controller.rb @@ -21,32 +21,28 @@ module Admin @relay = Relay.new(resource_params) if @relay.save - log_action :create, @relay @relay.enable! redirect_to admin_relays_path else - render :new + render action: :new end end def destroy authorize :relay, :update? @relay.destroy - log_action :destroy, @relay redirect_to admin_relays_path end def enable authorize :relay, :update? @relay.enable! - log_action :enable, @relay redirect_to admin_relays_path end def disable authorize :relay, :update? @relay.disable! - log_action :disable, @relay redirect_to admin_relays_path end @@ -57,8 +53,7 @@ module Admin end def resource_params - params - .expect(relay: [:inbox_url]) + params.require(:relay).permit(:inbox_url) end def warn_signatures_not_enabled! diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 10dbe846e4..3fd815b60a 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -21,12 +21,12 @@ module Admin redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg') else - @report_notes = @report.notes.chronological.includes(:account) + @report_notes = @report.notes.includes(:account).order(id: :desc) @action_logs = @report.history.includes(:target) @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes - render 'admin/reports/show' + render template: 'admin/reports/show' end end @@ -47,8 +47,10 @@ module Admin end def resource_params - params - .expect(report_note: [:content, :report_id]) + params.require(:report_note).permit( + :content, + :report_id + ) end def set_report_note diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index aa877f1448..00d200d7c8 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -13,7 +13,7 @@ module Admin authorize @report, :show? @report_note = @report.notes.new - @report_notes = @report.notes.chronological.includes(:account) + @report_notes = @report.notes.includes(:account).order(id: :desc) @action_logs = @report.history.includes(:target) @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 2f9af8a6fc..bcfc11159c 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -61,8 +61,7 @@ module Admin end def resource_params - params - .expect(user_role: [:name, :color, :highlighted, :position, permissions_as_keys: []]) + params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: []) end end end diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index 289b6a98c3..d31aec6ea8 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -53,8 +53,7 @@ module Admin end def resource_params - params - .expect(rule: [:text, :hint, :priority]) + params.require(:rule).permit(:text, :priority) end end end diff --git a/app/controllers/admin/sensitive_words_controller.rb b/app/controllers/admin/sensitive_words_controller.rb index 716dcc708a..eaa6ae802e 100644 --- a/app/controllers/admin/sensitive_words_controller.rb +++ b/app/controllers/admin/sensitive_words_controller.rb @@ -6,14 +6,13 @@ module Admin authorize :sensitive_words, :show? @admin_settings = Form::AdminSettings.new - @sensitive_words = ::SensitiveWord.caches.presence || [::SensitiveWord.new] end def create authorize :sensitive_words, :create? begin - ::SensitiveWord.save_from_raws(settings_params_test) + test_words rescue flash[:alert] = I18n.t('admin.ng_words.test_error') redirect_to after_update_redirect_path @@ -32,16 +31,18 @@ module Admin private + def test_words + sensitive_words = settings_params['sensitive_words'].split(/\r\n|\r|\n/) + sensitive_words_for_full = settings_params['sensitive_words_for_full'].split(/\r\n|\r|\n/) + Admin::NgWord.reject_with_custom_words?('Sample text', sensitive_words + sensitive_words_for_full) + end + def after_update_redirect_path admin_sensitive_words_path end def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) - end - - def settings_params_test - params.require(:form_admin_settings)[:sensitive_words_test] + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb index 6dbc86df9a..b4a74349c0 100644 --- a/app/controllers/admin/settings/registrations_controller.rb +++ b/app/controllers/admin/settings/registrations_controller.rb @@ -1,18 +1,9 @@ # frozen_string_literal: true class Admin::Settings::RegistrationsController < Admin::SettingsController - include RegistrationLimitationHelper - - before_action :set_limitation_counts, only: :show # rubocop:disable Rails/LexicallyScopedActionFilter - private def after_update_redirect_path admin_settings_registrations_path end - - def set_limitation_counts - @current_users_count = user_count_for_registration - @current_users_count_today = today_increase_user_count - end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 2ae5ec8255..338a3638c4 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -28,8 +28,7 @@ module Admin end def settings_params - params - .expect(form_admin_settings: [*Form::AdminSettings::KEYS]) + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb index 96e61cf6bb..a5d2cf41cf 100644 --- a/app/controllers/admin/site_uploads_controller.rb +++ b/app/controllers/admin/site_uploads_controller.rb @@ -9,7 +9,7 @@ module Admin @site_upload.destroy! - redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') + redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') end private diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb index c9be97eb71..52d8cb41e6 100644 --- a/app/controllers/admin/software_updates_controller.rb +++ b/app/controllers/admin/software_updates_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :software_update, :index? - @software_updates = SoftwareUpdate.by_version.filter(&:pending?) + @software_updates = SoftwareUpdate.all.sort_by(&:gem_version) end private diff --git a/app/controllers/admin/special_domains_controller.rb b/app/controllers/admin/special_domains_controller.rb deleted file mode 100644 index b36fe28d6e..0000000000 --- a/app/controllers/admin/special_domains_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SpecialDomainsController < BaseController - def show - authorize :instance, :show? - - @admin_settings = Form::AdminSettings.new - end - - def create - authorize :instance, :destroy? - - @admin_settings = Form::AdminSettings.new(settings_params) - - if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - else - render :show - end - end - - private - - def after_update_redirect_path - admin_special_domains_path - end - - def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) - end - end -end diff --git a/app/controllers/admin/special_instances_controller.rb b/app/controllers/admin/special_instances_controller.rb deleted file mode 100644 index a16bae13ef..0000000000 --- a/app/controllers/admin/special_instances_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SpecialInstancesController < BaseController - def show - authorize :instance, :show? - - @admin_settings = Form::AdminSettings.new - end - - def create - authorize :instance, :destroy? - - @admin_settings = Form::AdminSettings.new(settings_params) - - if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - else - render :show - end - end - - private - - def after_update_redirect_path - admin_special_instances_path - end - - def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) - end - end -end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 956950fe0d..9f57c091bf 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -16,8 +16,6 @@ module Admin def show authorize [:admin, @status], :show? - - @status_batch_action = Admin::StatusBatchAction.new end def batch @@ -36,8 +34,7 @@ module Admin UpdateStatusService.new.call( @status, edit_status_account_id, - no_history: true, - bypass_validation: true + no_history: true ) log_action(:remove_history, @status) redirect_to admin_account_status_path @@ -49,8 +46,7 @@ module Admin @status, edit_status_account_id, media_ids: [], - media_attributes: [], - bypass_validation: true + media_attributes: [] ) log_action(:remove_media, @status) redirect_to admin_account_status_path @@ -61,8 +57,7 @@ module Admin UpdateStatusService.new.call( @status, edit_status_account_id, - sensitive: true, - bypass_validation: true + sensitive: true ) log_action(:force_sensitive, @status) redirect_to admin_account_status_path @@ -73,8 +68,7 @@ module Admin UpdateStatusService.new.call( @status, edit_status_account_id, - spoiler_text: 'CW', - bypass_validation: true + spoiler_text: 'CW' ) log_action(:force_cw, @status) redirect_to admin_account_status_path @@ -92,14 +86,8 @@ module Admin private - def batched_ordered_status_edits - @status.edits.includes(:account, status: [:account]).find_each(order: :asc) - end - helper_method :batched_ordered_status_edits - def admin_status_batch_action_params - params - .expect(admin_status_batch_action: [status_ids: []]) + params.require(:admin_status_batch_action).permit(status_ids: []) end def after_create_redirect_path @@ -128,7 +116,7 @@ module Admin return @edit_account_id || @account.id if @edit_account_checked @edit_account_checked = true - @edit_account_id = Account.representative.id + @edit_account_id = Account.local.find_by(username: 'official')&.id || @account.id end def filter_params diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index a7bfd64794..4f727c398a 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -2,15 +2,7 @@ module Admin class TagsController < BaseController - before_action :set_tag, except: [:index] - - PER_PAGE = 20 - - def index - authorize :tag, :index? - - @tags = filtered_tags.page(params[:page]).per(PER_PAGE) - end + before_action :set_tag def show authorize @tag, :show? @@ -37,16 +29,7 @@ module Admin end def tag_params - params - .expect(tag: [:name, :display_name, :trendable, :usable, :listable]) - end - - def filtered_tags - TagFilter.new(filter_params.with_defaults(order: 'newest')).results - end - - def filter_params - params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS) + params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable) end end end diff --git a/app/controllers/admin/terms_of_service/distributions_controller.rb b/app/controllers/admin/terms_of_service/distributions_controller.rb deleted file mode 100644 index c639b083dd..0000000000 --- a/app/controllers/admin/terms_of_service/distributions_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::DistributionsController < Admin::BaseController - before_action :set_terms_of_service - - def create - authorize @terms_of_service, :distribute? - @terms_of_service.touch(:notification_sent_at) - Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id) - redirect_to admin_terms_of_service_index_path - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) - end -end diff --git a/app/controllers/admin/terms_of_service/drafts_controller.rb b/app/controllers/admin/terms_of_service/drafts_controller.rb deleted file mode 100644 index 0c67eb9df8..0000000000 --- a/app/controllers/admin/terms_of_service/drafts_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::DraftsController < Admin::BaseController - before_action :set_terms_of_service - - def show - authorize :terms_of_service, :create? - end - - def update - authorize @terms_of_service, :update? - - @terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish' - - if @terms_of_service.update(resource_params) - log_action(:publish, @terms_of_service) if @terms_of_service.published? - redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path - else - render :show - end - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now) - end - - def current_terms_of_service - TermsOfService.live.first - end - - def resource_params - params - .expect(terms_of_service: [:text, :changelog, :effective_date]) - end -end diff --git a/app/controllers/admin/terms_of_service/generates_controller.rb b/app/controllers/admin/terms_of_service/generates_controller.rb deleted file mode 100644 index 0edc87893e..0000000000 --- a/app/controllers/admin/terms_of_service/generates_controller.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::GeneratesController < Admin::BaseController - before_action :set_instance_presenter - - def show - authorize :terms_of_service, :create? - - @generator = TermsOfService::Generator.new( - domain: @instance_presenter.domain, - admin_email: @instance_presenter.contact.email - ) - end - - def create - authorize :terms_of_service, :create? - - @generator = TermsOfService::Generator.new(resource_params) - - if @generator.valid? - TermsOfService.create!(text: @generator.render) - redirect_to admin_terms_of_service_draft_path - else - render :show - end - end - - private - - def set_instance_presenter - @instance_presenter = InstancePresenter.new - end - - def resource_params - params - .expect(terms_of_service_generator: [*TermsOfService::Generator::VARIABLES]) - end -end diff --git a/app/controllers/admin/terms_of_service/histories_controller.rb b/app/controllers/admin/terms_of_service/histories_controller.rb deleted file mode 100644 index 8f12341aea..0000000000 --- a/app/controllers/admin/terms_of_service/histories_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::HistoriesController < Admin::BaseController - def show - authorize :terms_of_service, :index? - @terms_of_service = TermsOfService.published.all - end -end diff --git a/app/controllers/admin/terms_of_service/previews_controller.rb b/app/controllers/admin/terms_of_service/previews_controller.rb deleted file mode 100644 index 0a1a966751..0000000000 --- a/app/controllers/admin/terms_of_service/previews_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::PreviewsController < Admin::BaseController - before_action :set_terms_of_service - - def show - authorize @terms_of_service, :distribute? - @user_count = @terms_of_service.scope_for_notification.count - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) - end -end diff --git a/app/controllers/admin/terms_of_service/tests_controller.rb b/app/controllers/admin/terms_of_service/tests_controller.rb deleted file mode 100644 index e2483c1005..0000000000 --- a/app/controllers/admin/terms_of_service/tests_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::TestsController < Admin::BaseController - before_action :set_terms_of_service - - def create - authorize @terms_of_service, :distribute? - UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later! - redirect_to admin_terms_of_service_preview_path(@terms_of_service) - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) - end -end diff --git a/app/controllers/admin/terms_of_service_controller.rb b/app/controllers/admin/terms_of_service_controller.rb deleted file mode 100644 index 10aa5c66ca..0000000000 --- a/app/controllers/admin/terms_of_service_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfServiceController < Admin::BaseController - def index - authorize :terms_of_service, :index? - @terms_of_service = TermsOfService.published.first - end -end diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb index 5a650d5d8c..768b79f8db 100644 --- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb @@ -4,7 +4,6 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll def index authorize :preview_card_provider, :review? - @pending_preview_card_providers_count = PreviewCardProvider.unreviewed.async_count @preview_card_providers = filtered_preview_card_providers.page(params[:page]) @form = Trends::PreviewCardProviderBatch.new end @@ -31,8 +30,7 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll end def trends_preview_card_provider_batch_params - params - .expect(trends_preview_card_provider_batch: [:action, preview_card_provider_ids: []]) + params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index 68aa73c992..83d68eba63 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -4,7 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController def index authorize :preview_card, :review? - @locales = PreviewCardTrend.locales + @locales = PreviewCardTrend.pluck('distinct language') @preview_cards = filtered_preview_cards.page(params[:page]) @form = Trends::PreviewCardBatch.new end @@ -31,8 +31,7 @@ class Admin::Trends::LinksController < Admin::BaseController end def trends_preview_card_batch_params - params - .expect(trends_preview_card_batch: [:action, preview_card_ids: []]) + params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb index 873d777fe3..3d8b53ea8a 100644 --- a/app/controllers/admin/trends/statuses_controller.rb +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -4,7 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController def index authorize [:admin, :status], :review? - @locales = StatusTrend.locales + @locales = StatusTrend.pluck('distinct language') @statuses = filtered_statuses.page(params[:page]) @form = Trends::StatusBatch.new end @@ -31,8 +31,7 @@ class Admin::Trends::StatusesController < Admin::BaseController end def trends_status_batch_params - params - .expect(trends_status_batch: [:action, status_ids: []]) + params.require(:trends_status_batch).permit(:action, status_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb index 1ccd740686..f5946448ae 100644 --- a/app/controllers/admin/trends/tags_controller.rb +++ b/app/controllers/admin/trends/tags_controller.rb @@ -4,7 +4,6 @@ class Admin::Trends::TagsController < Admin::BaseController def index authorize :tag, :review? - @pending_tags_count = Tag.pending_review.async_count @tags = filtered_tags.page(params[:page]) @form = Trends::TagBatch.new end @@ -31,8 +30,7 @@ class Admin::Trends::TagsController < Admin::BaseController end def trends_tag_batch_params - params - .expect(trends_tag_batch: [:action, tag_ids: []]) + params.require(:trends_tag_batch).permit(:action, tag_ids: []) end def action_from_button diff --git a/app/controllers/admin/users/roles_controller.rb b/app/controllers/admin/users/roles_controller.rb index e8b58de504..f5dfc643d4 100644 --- a/app/controllers/admin/users/roles_controller.rb +++ b/app/controllers/admin/users/roles_controller.rb @@ -28,8 +28,7 @@ module Admin end def resource_params - params - .expect(user: [:role_id]) + params.require(:user).permit(:role_id) end end end diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb index dcf88294ee..efbf65b119 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -52,8 +52,7 @@ module Admin end def warning_preset_params - params - .expect(account_warning_preset: [:title, :text]) + params.require(:account_warning_preset).permit(:title, :text) end end end diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index 31db369637..f1aad7c4b5 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -74,8 +74,7 @@ module Admin end def resource_params - params - .expect(webhook: [:url, :template, events: []]) + params.require(:webhook).permit(:url, :template, events: []) end end end diff --git a/app/controllers/antennas_controller.rb b/app/controllers/antennas_controller.rb new file mode 100644 index 0000000000..ca7ee5d2a2 --- /dev/null +++ b/app/controllers/antennas_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class AntennasController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + before_action :set_antenna, only: [:edit, :update, :destroy] + before_action :set_body_classes + before_action :set_cache_headers + + def index + @antennas = current_account.antennas.includes(:antenna_domains).includes(:antenna_tags).includes(:antenna_accounts) + end + + def edit; end + + def update + if @antenna.update(resource_params) + redirect_to antennas_path + else + render action: :edit + end + end + + def destroy + @antenna.destroy + redirect_to antennas_path + end + + private + + def set_antenna + @antenna = current_account.antennas.find(params[:id]) + end + + def resource_params + params.require(:antenna).permit(:title, :available, :expires_in) + end + + def thin_resource_params + params.require(:antenna).permit(:title) + end + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end +end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 06a113511c..7f913a8862 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -4,12 +4,9 @@ class Api::BaseController < ApplicationController DEFAULT_STATUSES_LIMIT = 20 DEFAULT_ACCOUNTS_LIMIT = 40 - include Api::RateLimitHeaders - include Api::AccessTokenTrackingConcern - include Api::CachingConcern - include Api::ContentSecurityPolicy - include Api::ErrorHandling - include Api::Pagination + include RateLimitHeaders + include AccessTokenTrackingConcern + include ApiCachingConcern skip_before_action :require_functional!, unless: :limited_federation_mode? @@ -20,8 +17,73 @@ class Api::BaseController < ApplicationController protect_from_forgery with: :null_session + content_security_policy do |p| + # Set every directive that does not have a fallback + p.default_src :none + p.frame_ancestors :none + p.form_action :none + + # Disable every directive with a fallback to cut on response size + p.base_uri false + p.font_src false + p.img_src false + p.style_src false + p.media_src false + p.frame_src false + p.manifest_src false + p.connect_src false + p.script_src false + p.child_src false + p.worker_src false + end + + 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' } } + { json: { error: (error.try(:description) || 'Not authorized') } } end def doorkeeper_forbidden_render_options(*) @@ -30,10 +92,17 @@ class Api::BaseController < ApplicationController protected - def limit_param(default_limit, max_limit = nil) + def set_pagination_headers(next_path = nil, prev_path = nil) + links = [] + links << [next_path, [%w(rel next)]] if next_path + links << [prev_path, [%w(rel prev)]] if prev_path + response.headers['Link'] = LinkHeader.new(links) unless links.empty? + end + + def limit_param(default_limit) return default_limit unless params[:limit] - [params[:limit].to_i.abs, max_limit || (default_limit * 2)].min + [params[:limit].to_i.abs, default_limit * 2].min end def params_slice(*keys) @@ -55,7 +124,7 @@ class Api::BaseController < ApplicationController end def require_not_suspended! - render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable? + render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.suspended? end def require_user! diff --git a/app/controllers/api/fasp/base_controller.rb b/app/controllers/api/fasp/base_controller.rb deleted file mode 100644 index 690f7e419a..0000000000 --- a/app/controllers/api/fasp/base_controller.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -class Api::Fasp::BaseController < ApplicationController - class Error < ::StandardError; end - - DIGEST_PATTERN = /sha-256=:(.*?):/ - KEYID_PATTERN = /keyid="(.*?)"/ - - attr_reader :current_provider - - skip_forgery_protection - - before_action :check_fasp_enabled - before_action :require_authentication - after_action :sign_response - - private - - def require_authentication - validate_content_digest! - validate_signature! - rescue Error, Linzer::Error, ActiveRecord::RecordNotFound => e - logger.debug("FASP Authentication error: #{e}") - authentication_error - end - - def authentication_error - respond_to do |format| - format.json { head 401 } - end - end - - def validate_content_digest! - content_digest_header = request.headers['content-digest'] - raise Error, 'content-digest missing' if content_digest_header.blank? - - digest_received = content_digest_header.match(DIGEST_PATTERN)[1] - - digest_computed = OpenSSL::Digest.base64digest('sha256', request.body&.string || '') - - raise Error, 'content-digest does not match' if digest_received != digest_computed - end - - def validate_signature! - signature_input = request.headers['signature-input']&.encode('UTF-8') - raise Error, 'signature-input is missing' if signature_input.blank? - - keyid = signature_input.match(KEYID_PATTERN)[1] - provider = Fasp::Provider.find(keyid) - linzer_request = Linzer.new_request( - request.method, - request.original_url, - {}, - { - 'content-digest' => request.headers['content-digest'], - 'signature-input' => signature_input, - 'signature' => request.headers['signature'], - } - ) - message = Linzer::Message.new(linzer_request) - key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) - signature = Linzer::Signature.build(message.headers) - Linzer.verify(key, message, signature) - @current_provider = provider - end - - def sign_response - response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:" - - linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] }) - message = Linzer::Message.new(linzer_response) - key = Linzer.new_ed25519_key(current_provider.server_private_key_pem) - signature = Linzer.sign(key, message, %w(@status content-digest)) - - response.headers.merge!(signature.to_h) - end - - def check_fasp_enabled - raise ActionController::RoutingError unless Mastodon::Feature.fasp_enabled? - end -end diff --git a/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb b/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb deleted file mode 100644 index 794e53f095..0000000000 --- a/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class Api::Fasp::Debug::V0::Callback::ResponsesController < Api::Fasp::BaseController - def create - Fasp::DebugCallback.create( - fasp_provider: current_provider, - ip: request.remote_ip, - request_body: request.raw_post - ) - - respond_to do |format| - format.json { head 201 } - end - end -end diff --git a/app/controllers/api/fasp/registrations_controller.rb b/app/controllers/api/fasp/registrations_controller.rb deleted file mode 100644 index fecc992fec..0000000000 --- a/app/controllers/api/fasp/registrations_controller.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class Api::Fasp::RegistrationsController < Api::Fasp::BaseController - skip_before_action :require_authentication - - def create - @current_provider = Fasp::Provider.create!( - name: params[:name], - base_url: params[:baseUrl], - remote_identifier: params[:serverId], - provider_public_key_base64: params[:publicKey] - ) - - render json: registration_confirmation - end - - private - - def registration_confirmation - { - faspId: current_provider.id.to_s, - publicKey: current_provider.server_public_key_base64, - registrationCompletionUri: new_admin_fasp_provider_registration_url(current_provider), - } - end -end diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index b7f22824a7..66da65beda 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -7,7 +7,7 @@ class Api::OEmbedController < Api::BaseController before_action :require_public_status! def show - render json: @status, serializer: OEmbedSerializer, width: params[:maxwidth], height: params[:maxheight] + render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default end private @@ -23,4 +23,12 @@ class Api::OEmbedController < Api::BaseController def status_finder StatusFinder.new(params[:url]) end + + def maxwidth_or_default + (params[:maxwidth].presence || 400).to_i + end + + def maxheight_or_default + params[:maxheight].present? ? params[:maxheight].to_i : nil + end end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index bdd7732b87..011693c7c7 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::CredentialsController < Api::BaseController - before_action -> { doorkeeper_authorize! :profile, :read, :'read:accounts' }, except: [:update] + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update] before_action :require_user! @@ -14,10 +14,8 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) current_user.update(user_params) if user_params - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer - rescue ActiveRecord::RecordInvalid => e - render json: ValidationErrorFormatter.new(e).as_json, status: 422 end private @@ -32,9 +30,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController :bot, :discoverable, :searchability, + :dissubscribable, :hide_collections, :indexable, - attribution_domains: [], fields_attributes: [:name, :value] ) end diff --git a/app/controllers/api/v1/accounts/endorsements_controller.rb b/app/controllers/api/v1/accounts/endorsements_controller.rb deleted file mode 100644 index 1e21994a90..0000000000 --- a/app/controllers/api/v1/accounts/endorsements_controller.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::EndorsementsController < Api::BaseController - include Authorization - - before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index - before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index - before_action :require_user!, except: :index - before_action :set_account - before_action :set_endorsed_accounts, only: :index - after_action :insert_pagination_headers, only: :index - - def index - cache_if_unauthenticated! - render json: @endorsed_accounts, each_serializer: REST::AccountSerializer - end - - def create - AccountPin.find_or_create_by!(account: current_account, target_account: @account) - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - def destroy - pin = AccountPin.find_by(account: current_account, target_account: @account) - pin&.destroy! - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end - - def set_endorsed_accounts - @endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts - end - - def paginated_endorsed_accounts - @account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params[:max_id], - params[:since_id] - ) - end - - def relationships_presenter - AccountRelationshipsPresenter.new([@account], current_user.account_id) - end - - def next_path - api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty? - end - - def pagination_collection - @endorsed_accounts - end - - def records_continue? - @endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end -end diff --git a/app/controllers/api/v1/accounts/exclude_antennas_controller.rb b/app/controllers/api/v1/accounts/exclude_antennas_controller.rb index 65d75dbc6e..c1f5c5981c 100644 --- a/app/controllers/api/v1/accounts/exclude_antennas_controller.rb +++ b/app/controllers/api/v1/accounts/exclude_antennas_controller.rb @@ -6,7 +6,7 @@ class Api::V1::Accounts::ExcludeAntennasController < Api::BaseController before_action :set_account def index - @antennas = @account.suspended? ? [] : current_account.antennas.where("exclude_accounts @> '#{@account.id}'") + @antennas = @account.suspended? ? [] : current_account.antennas.where('exclude_accounts @> \'[?]\'', @account.id) render json: @antennas, each_serializer: REST::AntennaSerializer end diff --git a/app/controllers/api/v1/accounts/familiar_followers_controller.rb b/app/controllers/api/v1/accounts/familiar_followers_controller.rb index 81f0a9ed0f..b0bd8018a2 100644 --- a/app/controllers/api/v1/accounts/familiar_followers_controller.rb +++ b/app/controllers/api/v1/accounts/familiar_followers_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController private def set_accounts - @accounts = Account.without_suspended.where(id: account_ids).select(:id, :hide_collections) + @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact end def familiar_followers diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb index f95846366c..0101fb469b 100644 --- a/app/controllers/api/v1/accounts/featured_tags_controller.rb +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController end def set_featured_tags - @featured_tags = @account.unavailable? ? [] : @account.featured_tags + @featured_tags = @account.suspended? ? [] : @account.featured_tags end end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 3f2ecb892d..1a996d362a 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -21,16 +21,16 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end def hide_results? - @account.unavailable? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts - Account.includes(:active_relationships, :account_stat, :user).references(:active_relationships) + Account.includes(:active_relationships, :account_stat).references(:active_relationships) end def paginated_follows @@ -41,6 +41,10 @@ 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 @@ -60,4 +64,8 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 7c16a3487e..6e6ebae43b 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -21,16 +21,16 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end def hide_results? - @account.unavailable? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts - Account.includes(:passive_relationships, :account_stat, :user).references(:passive_relationships) + Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) end def paginated_follows @@ -41,6 +41,10 @@ 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 @@ -60,4 +64,8 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 02a45e8758..48f293f47a 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Api::V1::Accounts::IdentityProofsController < Api::BaseController - include DeprecationConcern - - deprecate_api '2022-03-30' - before_action :require_user! before_action :set_account diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb new file mode 100644 index 0000000000..0eb13c048c --- /dev/null +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::PinsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:accounts' } + before_action :require_user! + before_action :set_account + + def create + AccountPin.find_or_create_by!(account: current_account, target_account: @account) + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + def destroy + pin = AccountPin.find_by(account: current_account, target_account: @account) + pin&.destroy! + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def relationships_presenter + AccountRelationshipsPresenter.new([@account], current_user.account_id) + end +end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index d43832177a..038d6700f5 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,9 +5,10 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - @accounts = Account.where(id: account_ids).select(:id, :domain) - @accounts.merge!(Account.without_suspended) unless truthy_param?(:with_suspended) - render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships + @accounts = Account.without_suspended.where(id: account_ids).select(:id, :domain).to_a + # .where doesn't guarantee that our results are in the same order + # we requested them, so return the "right" order to the requestor. + render json: @accounts.index_by(&:id).values_at(*account_ids).compact, each_serializer: REST::RelationshipSerializer, relationships: relationships end private diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6213f6f9e1..7004dbb530 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -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 + after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index cache_if_unauthenticated! @@ -21,11 +21,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - @account.unavailable? ? [] : preloaded_account_statuses + @account.suspended? ? [] : cached_account_statuses end - def preloaded_account_statuses - preload_collection_paginated_by_id( + def cached_account_statuses + cache_collection_paginated_by_id( AccountStatusesFilter.new(@account, current_account, params).results, Status, limit_param(DEFAULT_STATUSES_LIMIT), @@ -37,6 +37,10 @@ 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 @@ -49,7 +53,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) end - def pagination_collection - @statuses + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 46838aeb66..5601d54094 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,31 +1,22 @@ # frozen_string_literal: true class Api::V1::AccountsController < Api::BaseController - include RegistrationHelper - before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers] before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, only: [:block, :unblock] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create] - before_action :require_user!, except: [:index, :show, :create] - before_action :set_account, except: [:index, :create] - before_action :set_accounts, only: [:index] - before_action :check_account_approval, except: [:index, :create] - before_action :check_account_confirmation, except: [:index, :create] + before_action :require_user!, except: [:show, :create] + before_action :set_account, except: [:create] + before_action :check_account_approval, except: [:create] + before_action :check_account_confirmation, except: [:create] before_action :check_enabled_registrations, only: [:create] - before_action :check_accounts_limit, only: [:index] - before_action :check_following_self, only: [:follow] skip_before_action :require_authenticated_user!, only: :create override_rate_limit_headers :follow, family: :follows - def index - render json: @accounts, each_serializer: REST::AccountSerializer - end - def show cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer @@ -45,12 +36,7 @@ class Api::V1::AccountsController < Api::BaseController def follow follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) - options = if @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) - {} - else - { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, - requested_map: { @account.id => false } } - end + options = @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end @@ -61,7 +47,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: params[:duration].to_i) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0)) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end @@ -91,10 +77,6 @@ class Api::V1::AccountsController < Api::BaseController @account = Account.find(params[:id]) end - def set_accounts - @accounts = Account.where(id: account_ids).without_unapproved - end - def check_account_approval raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending? end @@ -103,35 +85,23 @@ class Api::V1::AccountsController < Api::BaseController raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed? end - def check_accounts_limit - raise(Mastodon::ValidationError) if account_ids.size > DEFAULT_ACCOUNTS_LIMIT - end - - def check_following_self - render json: { error: I18n.t('accounts.self_follow_error') }, status: 403 if current_user.account.id == @account.id - end - - def relationships(**) - AccountRelationshipsPresenter.new([@account], current_user.account_id, **) - end - - def account_ids - Array(accounts_params[:id]).uniq.map(&:to_i) - end - - def accounts_params - params.permit(id: []) + def relationships(**options) + AccountRelationshipsPresenter.new([@account], current_user.account_id, **options) end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth) - end - - def invite - Invite.find_by(code: params[:invite_code]) if params[:invite_code].present? + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) end def check_enabled_registrations - forbidden unless allowed_registration?(request.remote_ip, invite) + forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? + end + + def allowed_registrations? + Setting.registrations_mode != 'none' + end + + def omniauth_only? + ENV['OMNIAUTH_ONLY'] == 'true' end end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index ff6f41e01d..ff9cae6398 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -125,6 +125,10 @@ 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 @@ -133,8 +137,12 @@ 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_collection - @accounts + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index c144a9e0f9..7b192b979f 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -16,6 +16,8 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController after_action :verify_authorized after_action :insert_pagination_headers, only: :index + PAGINATION_PARAMS = %i(limit).freeze + def index authorize :canonical_email_block, :index? render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer @@ -63,6 +65,10 @@ 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 @@ -71,11 +77,19 @@ 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_collection - @canonical_email_blocks + def pagination_max_id + @canonical_email_blocks.last.id + end + + def pagination_since_id + @canonical_email_blocks.first.id end def records_continue? @canonical_email_blocks.size == limit_param(LIMIT) end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end end diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 24f68aa1bd..dd54d67106 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -5,7 +5,6 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController include AccountableConcern LIMIT = 100 - MAX_LIMIT = 500 before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show] @@ -15,6 +14,8 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController after_action :verify_authorized after_action :insert_pagination_headers, only: :index + PAGINATION_PARAMS = %i(limit).freeze + def index authorize :domain_allow, :index? render json: @domain_allows, each_serializer: REST::Admin::DomainAllowSerializer @@ -48,13 +49,22 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController private def set_domain_allows - @domain_allows = DomainAllow.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id)) + @domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def set_domain_allow @domain_allow = DomainAllow.find(params[:id]) end + def filtered_domain_allows + # TODO: no filtering yet + 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 @@ -63,12 +73,20 @@ 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_collection - @domain_allows + def pagination_max_id + @domain_allows.last.id + end + + def pagination_since_id + @domain_allows.first.id end def records_continue? - @domain_allows.size == limit_param(LIMIT, MAX_LIMIT) + @domain_allows.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) end def resource_params diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index d84c4a8695..765ea9572b 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -5,7 +5,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController include AccountableConcern LIMIT = 100 - MAX_LIMIT = 500 before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show] @@ -15,6 +14,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController after_action :verify_authorized after_action :insert_pagination_headers, only: :index + PAGINATION_PARAMS = %i(limit).freeze + def index authorize :domain_block, :index? render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer @@ -60,16 +61,25 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController end def set_domain_blocks - @domain_blocks = DomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id)) + @domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def set_domain_block @domain_block = DomainBlock.find(params[:id]) end + def filtered_domain_blocks + # TODO: no filtering yet + DomainBlock.all + end + def domain_block_params - params.permit(:severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_reports, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, - :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden) + params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, + :reject_new_follow, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) end def next_path @@ -80,16 +90,24 @@ 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_collection - @domain_blocks + def pagination_max_id + @domain_blocks.last.id + end + + def pagination_since_id + @domain_blocks.first.id end def records_continue? - @domain_blocks.size == limit_param(LIMIT, MAX_LIMIT) + @domain_blocks.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) end def resource_params - params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, - :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) + params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, + :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) end end diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb index e7bd804e36..850eda6224 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -14,6 +14,10 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController after_action :verify_authorized after_action :insert_pagination_headers, only: :index + PAGINATION_PARAMS = %i( + limit + ).freeze + def index authorize :email_domain_block, :index? render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer @@ -51,7 +55,11 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController end def resource_params - params.permit(:domain, :allow_with_approval) + params.permit(:domain) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) end def next_path @@ -62,11 +70,19 @@ 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_collection - @email_domain_blocks + def pagination_max_id + @email_domain_blocks.last.id + end + + def pagination_since_id + @email_domain_blocks.first.id end def records_continue? @email_domain_blocks.size == limit_param(LIMIT) end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end end diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb index e132a3a87d..61c1912344 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -14,6 +14,10 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController after_action :verify_authorized after_action :insert_pagination_headers, only: :index + PAGINATION_PARAMS = %i( + limit + ).freeze + def index authorize :ip_block, :index? render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer @@ -59,6 +63,10 @@ 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 @@ -67,11 +75,19 @@ 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_collection - @ip_blocks + def pagination_max_id + @ip_blocks.last.id + end + + def pagination_since_id + @ip_blocks.first.id end def records_continue? @ip_blocks.size == limit_param(LIMIT) end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index 9b5beeab67..9dfb181a28 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -35,7 +35,6 @@ 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 @@ -89,6 +88,10 @@ 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 @@ -97,8 +100,12 @@ 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_collection - @reports + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb index 283383acb4..6a7c9f5bf3 100644 --- a/app/controllers/api/v1/admin/tags_controller.rb +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -12,13 +12,7 @@ class Api::V1::Admin::TagsController < Api::BaseController after_action :verify_authorized LIMIT = 100 - - PERMITTED_PARAMS = %i( - display_name - listable - trendable - usable - ).freeze + PAGINATION_PARAMS = %i(limit).freeze def index authorize :tag, :index? @@ -47,9 +41,11 @@ class Api::V1::Admin::TagsController < Api::BaseController end def tag_params - params - .slice(*PERMITTED_PARAMS) - .permit(*PERMITTED_PARAMS) + params.permit(:display_name, :trendable, :usable, :listable) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) end def next_path @@ -60,11 +56,19 @@ 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_collection - @tags + def pagination_max_id + @tags.last.id + end + + def pagination_since_id + @tags.first.id end def records_continue? @tags.size == limit_param(LIMIT) end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end end diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb index 2b0f39b98f..5d9fcc82c0 100644 --- a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb @@ -12,6 +12,8 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC after_action :verify_authorized after_action :insert_pagination_headers, only: :index + PAGINATION_PARAMS = %i(limit).freeze + def index authorize :preview_card_provider, :index? @@ -40,6 +42,10 @@ 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 @@ -48,11 +54,19 @@ 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_collection - @providers + def pagination_max_id + @providers.last.id + end + + def pagination_since_id + @providers.first.id end def records_continue? @providers.size == limit_param(LIMIT) end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end end diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb deleted file mode 100644 index b1aee288dd..0000000000 --- a/app/controllers/api/v1/annual_reports_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::AnnualReportsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index - before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index - before_action :require_user! - before_action :set_annual_report, except: :index - - def index - with_read_replica do - @presenter = AnnualReportsPresenter.new(GeneratedAnnualReport.where(account_id: current_account.id).pending) - @relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id) - end - - render json: @presenter, - serializer: REST::AnnualReportsSerializer, - relationships: @relationships - end - - def show - with_read_replica do - @presenter = AnnualReportsPresenter.new([@annual_report]) - @relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id) - end - - render json: @presenter, - serializer: REST::AnnualReportsSerializer, - relationships: @relationships - end - - def read - @annual_report.view! - render_empty - end - - private - - def set_annual_report - @annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id]) - end -end diff --git a/app/controllers/api/v1/antennas_controller.rb b/app/controllers/api/v1/antennas_controller.rb index 4040263c00..37bfb7f552 100644 --- a/app/controllers/api/v1/antennas_controller.rb +++ b/app/controllers/api/v1/antennas_controller.rb @@ -21,7 +21,7 @@ class Api::V1::AntennasController < Api::BaseController end def create - @antenna = Antenna.create!(antenna_params.merge(account: current_account)) + @antenna = Antenna.create!(antenna_params.merge(account: current_account, list_id: 0)) render json: @antenna, serializer: REST::AntennaSerializer end @@ -42,6 +42,6 @@ class Api::V1::AntennasController < Api::BaseController end def antenna_params - params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog, :favourite) + params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog) end end diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb index 29ab920383..0475b2d4a2 100644 --- a/app/controllers/api/v1/apps/credentials_controller.rb +++ b/app/controllers/api/v1/apps/credentials_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class Api::V1::Apps::CredentialsController < Api::BaseController - def show - return doorkeeper_render_error unless valid_doorkeeper_token? + before_action -> { doorkeeper_authorize! :read } - render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer + def show + render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) end end diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index 50feaf1854..97177547a2 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -5,7 +5,7 @@ class Api::V1::AppsController < Api::BaseController def create @app = Doorkeeper::Application.create!(application_options) - render json: @app, serializer: REST::CredentialApplicationSerializer + render json: @app, serializer: REST::ApplicationSerializer end private @@ -24,6 +24,6 @@ class Api::V1::AppsController < Api::BaseController end def app_params - params.permit(:client_name, :scopes, :website, :redirect_uris, redirect_uris: []) + params.permit(:client_name, :redirect_uris, :scopes, :website) end end diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index d7516c927b..06a8bfa891 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -17,7 +17,7 @@ class Api::V1::BlocksController < Api::BaseController end def paginated_blocks - @paginated_blocks ||= Block.eager_load(target_account: [:account_stat, :user]) + @paginated_blocks ||= Block.eager_load(target_account: :account_stat) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) @@ -28,6 +28,10 @@ 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 @@ -36,11 +40,19 @@ class Api::V1::BlocksController < Api::BaseController api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty? end - def pagination_collection - paginated_blocks + def pagination_max_id + paginated_blocks.last.id + end + + def pagination_since_id + paginated_blocks.first.id end def records_continue? paginated_blocks.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index 009367d078..0985366964 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -15,11 +15,11 @@ class Api::V1::BookmarksController < Api::BaseController private def load_statuses - preloaded_bookmarks + cached_bookmarks end - def preloaded_bookmarks - preload_collection(results.map(&:status), Status) + def cached_bookmarks + cache_collection(results.map(&:status), Status) end def results @@ -33,6 +33,10 @@ 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 @@ -41,11 +45,19 @@ class Api::V1::BookmarksController < Api::BaseController api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty? end - def pagination_collection - results + def pagination_max_id + results.last.id + end + + def pagination_since_id + results.first.id end def records_continue? results.size == limit_param(DEFAULT_STATUSES_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/circles/statuses_controller.rb b/app/controllers/api/v1/circles/statuses_controller.rb deleted file mode 100644 index 705731936b..0000000000 --- a/app/controllers/api/v1/circles/statuses_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Circles::StatusesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - - before_action :require_user! - before_action :set_circle - - after_action :insert_pagination_headers, only: :show - - def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer - end - - private - - def set_circle - @circle = current_account.circles.find(params[:circle_id]) - end - - def load_statuses - if unlimited? - @circle.statuses.includes(:status_stat).all - else - @circle.statuses.includes(:status_stat).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - end - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - return if unlimited? - - api_v1_circle_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - return if unlimited? - - api_v1_circle_statuses_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id - end - - def records_continue? - @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - - def unlimited? - params[:limit] == '0' - end -end diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 60db082a8e..b3ca2f7903 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -38,21 +38,25 @@ class Api::V1::ConversationsController < Api::BaseController def paginated_conversations AccountConversation.where(account: current_account) .includes( - account: [:account_stat, user: :role], + account: :account_stat, last_status: [ :media_attachments, + :preview_cards, :status_stat, :tags, { - preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, - active_mentions: :account, - account: [:account_stat, user: :role], + active_mentions: [account: :account_stat], + account: :account_stat, }, ] ) .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 @@ -72,4 +76,8 @@ class Api::V1::ConversationsController < Api::BaseController def records_continue? @conversations.size == limit_param(LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/crypto/deliveries_controller.rb b/app/controllers/api/v1/crypto/deliveries_controller.rb new file mode 100644 index 0000000000..aa9df6e03b --- /dev/null +++ b/app/controllers/api/v1/crypto/deliveries_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::DeliveriesController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_current_device + + def create + devices.each do |device_params| + DeliverToDeviceService.new.call(current_account, @current_device, device_params) + end + + render_empty + end + + private + + def set_current_device + @current_device = Device.find_by!(access_token: doorkeeper_token) + end + + def resource_params + params.require(:device) + params.permit(device: [:account_id, :device_id, :type, :body, :hmac]) + end + + def devices + Array(resource_params[:device]) + end +end diff --git a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb new file mode 100644 index 0000000000..68cf4384f7 --- /dev/null +++ b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController + LIMIT = 80 + + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_current_device + + before_action :set_encrypted_messages, only: :index + after_action :insert_pagination_headers, only: :index + + def index + render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer + end + + def clear + @current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all + render_empty + end + + private + + def set_current_device + @current_device = Device.find_by!(access_token: doorkeeper_token) + end + + def set_encrypted_messages + @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 + + def prev_path + 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 + end + + def records_continue? + @encrypted_messages.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/crypto/keys/claims_controller.rb b/app/controllers/api/v1/crypto/keys/claims_controller.rb new file mode 100644 index 0000000000..f9d202d67b --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/claims_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_claim_results + + def create + render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer + end + + private + + def set_claim_results + @claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) } + end + + def resource_params + params.permit(device: [:account_id, :device_id]) + end + + def devices + Array(resource_params[:device]) + end +end diff --git a/app/controllers/api/v1/crypto/keys/counts_controller.rb b/app/controllers/api/v1/crypto/keys/counts_controller.rb new file mode 100644 index 0000000000..ffd7151b78 --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/counts_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::CountsController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_current_device + + def show + render json: { one_time_keys: @current_device.one_time_keys.count } + end + + private + + def set_current_device + @current_device = Device.find_by!(access_token: doorkeeper_token) + end +end diff --git a/app/controllers/api/v1/crypto/keys/queries_controller.rb b/app/controllers/api/v1/crypto/keys/queries_controller.rb new file mode 100644 index 0000000000..e6ce9f9192 --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/queries_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::QueriesController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_accounts + before_action :set_query_results + + def create + render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer + end + + private + + def set_accounts + @accounts = Account.where(id: account_ids).includes(:devices) + end + + def set_query_results + @query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) } + end + + def account_ids + Array(params[:id]).map(&:to_i) + end +end diff --git a/app/controllers/api/v1/crypto/keys/uploads_controller.rb b/app/controllers/api/v1/crypto/keys/uploads_controller.rb new file mode 100644 index 0000000000..fc4abf63b3 --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/uploads_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::UploadsController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + + def create + device = Device.find_or_initialize_by(access_token: doorkeeper_token) + + device.transaction do + device.account = current_account + device.update!(resource_params[:device]) + + if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable) + resource_params[:one_time_keys].each do |one_time_key_params| + device.one_time_keys.create!(one_time_key_params) + end + end + end + + render json: device, serializer: REST::Keys::DeviceSerializer + end + + private + + def resource_params + params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature]) + end +end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index 6c540404ea..35c504a7ff 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -12,7 +12,7 @@ class Api::V1::DirectoriesController < Api::BaseController private def require_enabled! - not_found unless Setting.profile_directory + return not_found unless Setting.profile_directory end def set_accounts @@ -27,7 +27,7 @@ class Api::V1::DirectoriesController < Api::BaseController scope.merge!(local_account_scope) if local_accounts? scope.merge!(account_exclusion_scope) if current_account scope.merge!(account_domain_block_scope) if current_account && !local_accounts? - end.includes(:account_stat, user: :role) + end end def local_accounts? diff --git a/app/controllers/api/v1/domain_blocks/previews_controller.rb b/app/controllers/api/v1/domain_blocks/previews_controller.rb deleted file mode 100644 index a917bddd98..0000000000 --- a/app/controllers/api/v1/domain_blocks/previews_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::DomainBlocks::PreviewsController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' } - before_action :require_user! - before_action :set_domain - before_action :set_domain_block_preview - - def show - render json: @domain_block_preview, serializer: REST::DomainBlockPreviewSerializer - end - - private - - def set_domain - @domain = TagManager.instance.normalize_domain(params[:domain]) - end - - def set_domain_block_preview - @domain_block_preview = with_read_replica do - DomainBlockPreviewPresenter.new( - following_count: current_account.following.where(domain: @domain).count, - followers_count: current_account.followers.where(domain: @domain).count - ) - end - end -end diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index 780ecbf189..34def3c44a 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -38,6 +38,10 @@ 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 @@ -46,14 +50,22 @@ class Api::V1::DomainBlocksController < Api::BaseController api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty? end - def pagination_collection - @blocks + def pagination_max_id + @blocks.last.id + end + + def pagination_since_id + @blocks.first.id end def records_continue? @blocks.size == limit_param(BLOCK_LIMIT) end + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + def domain_block_params params.permit(:domain) end diff --git a/app/controllers/api/v1/emoji_reactions_controller.rb b/app/controllers/api/v1/emoji_reactions_controller.rb index 5e913eef2a..43b39921db 100644 --- a/app/controllers/api/v1/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/emoji_reactions_controller.rb @@ -19,7 +19,7 @@ class Api::V1::EmojiReactionsController < Api::BaseController end def cached_emoji_reactions - preload_collection(results.map(&:status), EmojiReaction) + cache_collection(results.map(&:status), EmojiReaction) end def results diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 09bafe0231..46e3fcd647 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,11 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended + current_account.endorsed_accounts.includes(:account_stat).without_suspended + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) end def next_path @@ -40,14 +44,22 @@ class Api::V1::EndorsementsController < Api::BaseController api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end - def pagination_collection - @accounts + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id end def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + def unlimited? params[:limit] == '0' end diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 9b7d663bc1..984f3d3051 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -15,11 +15,11 @@ class Api::V1::FavouritesController < Api::BaseController private def load_statuses - preloaded_favourites + cached_favourites end - def preloaded_favourites - preload_collection(results.map(&:status), Status) + def cached_favourites + cache_collection(results.map(&:status), Status) end def results @@ -33,6 +33,10 @@ 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 @@ -41,11 +45,19 @@ class Api::V1::FavouritesController < Api::BaseController api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty? end - def pagination_collection - results + def pagination_max_id + results.last.id + end + + def pagination_since_id + results.first.id end def records_continue? results.size == limit_param(DEFAULT_STATUSES_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb index d533b1af7b..4f732ed2d5 100644 --- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -5,8 +5,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController before_action :require_user! before_action :set_recently_used_tags, only: :index - RECENT_TAGS_LIMIT = 10 - def index render json: @recently_used_tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@recently_used_tags, current_user&.account_id) end @@ -14,6 +12,10 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController private def set_recently_used_tags - @recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT) + @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 diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index f8d91c5f7f..3b097a3478 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Api::V1::FiltersController < Api::BaseController - include DeprecationConcern - - deprecate_api '2022-11-14' - before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! @@ -56,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController end def resource_params - params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :whole_word, context: []) + params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :whole_word, context: []) end def filter_params - resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :context) + resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :context) end def keyword_params diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 4b44cfe531..ee717ebbcc 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -28,8 +28,8 @@ class Api::V1::FollowRequestsController < Api::BaseController @account ||= Account.find(params[:id]) end - def relationships(**) - AccountRelationshipsPresenter.new([account], current_user.account_id, **) + def relationships(**options) + AccountRelationshipsPresenter.new([account], current_user.account_id, **options) end def load_accounts @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.without_suspended.includes(:follow_requests, :account_stat, :user).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) end def paginated_follow_requests @@ -48,6 +48,10 @@ 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 @@ -67,4 +71,8 @@ class Api::V1::FollowRequestsController < Api::BaseController def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/followed_tags_controller.rb b/app/controllers/api/v1/followed_tags_controller.rb index 7d8f0eda1e..eae2bdc010 100644 --- a/app/controllers/api/v1/followed_tags_controller.rb +++ b/app/controllers/api/v1/followed_tags_controller.rb @@ -22,6 +22,10 @@ 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 @@ -30,11 +34,19 @@ class Api::V1::FollowedTagsController < Api::BaseController api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty? end - def pagination_collection - @results + def pagination_max_id + @results.last.id + end + + def pagination_since_id + @results.first.id end def records_continue? @results.size == limit_param(TAGS_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index 06e4fd8b8f..9da77f8dab 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -class Api::V1::Instances::ActivityController < Api::V1::Instances::BaseController +class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! - WEEKS_OF_ACTIVITY = 12 + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + + vary_by '' def show cache_even_if_authenticated! @@ -13,40 +15,23 @@ class Api::V1::Instances::ActivityController < Api::V1::Instances::BaseControlle private def activity - activity_weeks.map do |weeks_ago| - activity_json(*week_edge_days(weeks_ago)) + statuses_tracker = ActivityTracker.new('activity:statuses:local', :basic) + logins_tracker = ActivityTracker.new('activity:logins', :unique) + registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic) + + (0...12).map do |i| + start_of_week = i.weeks.ago + end_of_week = start_of_week + 6.days + + { + week: start_of_week.to_i.to_s, + statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s, + logins: logins_tracker.sum(start_of_week, end_of_week).to_s, + registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s, + } end end - def activity_json(start_of_week, end_of_week) - { - week: start_of_week.to_i.to_s, - statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s, - logins: logins_tracker.sum(start_of_week, end_of_week).to_s, - registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s, - } - end - - def activity_weeks - 0...WEEKS_OF_ACTIVITY - end - - def week_edge_days(num) - [num.weeks.ago, num.weeks.ago + 6.days] - end - - def statuses_tracker - ActivityTracker.new('activity:statuses:local', :basic) - end - - def logins_tracker - ActivityTracker.new('activity:logins', :unique) - end - - def registrations_tracker - ActivityTracker.new('activity:accounts:local', :basic) - end - def require_enabled_api! head 404 unless Setting.activity_api_enabled && !limited_federation_mode? end diff --git a/app/controllers/api/v1/instances/base_controller.rb b/app/controllers/api/v1/instances/base_controller.rb deleted file mode 100644 index ed0bebf0ff..0000000000 --- a/app/controllers/api/v1/instances/base_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Instances::BaseController < Api::BaseController - skip_before_action :require_authenticated_user!, - unless: :limited_federation_mode? - - vary_by '' -end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index bf96fbaaa8..c91234e088 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseController +class Api::V1::Instances::DomainBlocksController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + before_action :require_enabled_api! before_action :set_domain_blocks @@ -13,40 +15,16 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr cache_if_unauthenticated! end - render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: show_rationale_in_response? + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) end private def require_enabled_api! - head 404 unless api_enabled? - end - - def api_enabled? - show_domain_blocks_for_all? || show_domain_blocks_to_user? - end - - def show_domain_blocks_for_all? - Setting.show_domain_blocks == 'all' - end - - def show_domain_blocks_to_user? - Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved? + head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) end def set_domain_blocks @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity end - - def show_rationale_in_response? - always_show_rationale? || show_rationale_for_user? - end - - def always_show_rationale? - Setting.show_domain_blocks_rationale == 'all' - end - - def show_rationale_for_user? - Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved? - end end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index db3d082f61..376fec9066 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true -class Api::V1::Instances::ExtendedDescriptionsController < Api::V1::Instances::BaseController +class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale before_action :set_extended_description - # Override `current_user` to avoid reading session cookies unless in limited federation mode + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user super if limited_federation_mode? end diff --git a/app/controllers/api/v1/instances/languages_controller.rb b/app/controllers/api/v1/instances/languages_controller.rb index ea184d90da..17509e748c 100644 --- a/app/controllers/api/v1/instances/languages_controller.rb +++ b/app/controllers/api/v1/instances/languages_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -class Api::V1::Instances::LanguagesController < Api::V1::Instances::BaseController +class Api::V1::Instances::LanguagesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale before_action :set_languages + vary_by '' + def show cache_even_if_authenticated! render json: @languages, each_serializer: REST::LanguageSerializer diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index fac763b405..08a982f227 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true -class Api::V1::Instances::PeersController < Api::V1::Instances::BaseController +class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale - # Override `current_user` to avoid reading session cookies unless in limited federation mode + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user super if limited_federation_mode? end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index 9f87317d53..f5b1b4ec5f 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true -class Api::V1::Instances::PrivacyPoliciesController < Api::V1::Instances::BaseController +class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + before_action :set_privacy_policy + vary_by '' + def show cache_even_if_authenticated! render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index 3930eec0dd..2f71984b05 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true -class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController +class Api::V1::Instances::RulesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale before_action :set_rules - # Override `current_user` to avoid reading session cookies unless in limited federation mode + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user super if limited_federation_mode? end diff --git a/app/controllers/api/v1/instances/terms_of_services_controller.rb b/app/controllers/api/v1/instances/terms_of_services_controller.rb deleted file mode 100644 index 0a861dd7bb..0000000000 --- a/app/controllers/api/v1/instances/terms_of_services_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController - before_action :set_terms_of_service - - def show - cache_even_if_authenticated! - render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer - end - - private - - def set_terms_of_service - @terms_of_service = begin - if params[:date].present? - TermsOfService.published.find_by!(effective_date: params[:date]) - else - TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet - end - end - end -end diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb index b8f7a16383..78423e40e4 100644 --- a/app/controllers/api/v1/instances/translation_languages_controller.rb +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true -class Api::V1::Instances::TranslationLanguagesController < Api::V1::Instances::BaseController +class Api::V1::Instances::TranslationLanguagesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + before_action :set_languages + vary_by '' + def show cache_even_if_authenticated! render json: @languages diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index e01267c000..df4a14af15 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,9 +1,15 @@ # frozen_string_literal: true -class Api::V1::InstancesController < Api::V2::InstancesController - include DeprecationConcern +class Api::V1::InstancesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale - deprecate_api '2022-11-14' + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if limited_federation_mode? + end def show cache_even_if_authenticated! diff --git a/app/controllers/api/v1/invites_controller.rb b/app/controllers/api/v1/invites_controller.rb deleted file mode 100644 index ea17ba7403..0000000000 --- a/app/controllers/api/v1/invites_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::InvitesController < Api::BaseController - include RegistrationHelper - - skip_before_action :require_authenticated_user! - skip_around_action :set_locale - - before_action :set_invite - before_action :check_enabled_registrations! - - # Override `current_user` to avoid reading session cookies - def current_user; end - - def show - render json: { invite_code: params[:invite_code], instance_api_url: api_v2_instance_url }, status: 200 - end - - private - - def set_invite - @invite = Invite.find_by!(code: params[:invite_code]) - end - - def check_enabled_registrations! - return render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use? - - raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite) - end -end diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 616159f05f..8e12cb7b65 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -15,12 +15,17 @@ class Api::V1::Lists::AccountsController < Api::BaseController end def create - AddAccountsToListService.new.call(@list, Account.find(account_ids)) + ApplicationRecord.transaction do + list_accounts.each do |account| + @list.accounts << account + end + end + render_empty end def destroy - RemoveAccountsFromListService.new.call(@list, Account.where(id: account_ids)) + ListAccount.where(list: @list, account_id: account_ids).destroy_all render_empty end @@ -32,12 +37,16 @@ class Api::V1::Lists::AccountsController < Api::BaseController def load_accounts if unlimited? - @list.accounts.without_suspended.includes(:account_stat, :user).all + @list.accounts.without_suspended.includes(:account_stat).all else - @list.accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end + def list_accounts + Account.find(account_ids) + end + def account_ids Array(resource_params[:account_ids]) end @@ -46,6 +55,10 @@ 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? @@ -58,14 +71,22 @@ 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_collection - @accounts + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id end def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + def unlimited? params[:limit] == '0' end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index b019ab6018..4071c34799 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -7,6 +7,10 @@ class Api::V1::ListsController < Api::BaseController before_action :require_user! before_action :set_list, except: [:index, :create] + rescue_from ArgumentError do |e| + render json: { error: e.to_s }, status: 422 + end + def index @lists = List.where(account: current_account).all render json: @lists, each_serializer: REST::ListSerializer @@ -34,16 +38,6 @@ class Api::V1::ListsController < Api::BaseController render_empty end - def favourite - @list.favourite! - render json: @list, serializer: REST::ListSerializer - end - - def unfavourite - @list.unfavourite! - render json: @list, serializer: REST::ListSerializer - end - private def set_list @@ -51,6 +45,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy, :exclusive, :notify, :favourite) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb index 8eaf7767df..f8dfba8a94 100644 --- a/app/controllers/api/v1/markers_controller.rb +++ b/app/controllers/api/v1/markers_controller.rb @@ -19,7 +19,7 @@ class Api::V1::MarkersController < Api::BaseController @markers = {} resource_params.each_pair do |timeline, timeline_params| - @markers[timeline] = current_user.markers.find_or_create_by(timeline: timeline) + @markers[timeline] = current_user.markers.find_or_initialize_by(timeline: timeline) @markers[timeline].update!(timeline_params) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index c427e055ea..5ea26d55bd 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -3,8 +3,8 @@ class Api::V1::MediaController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action :require_user! - before_action :set_media_attachment, except: [:create, :destroy] - before_action :check_processing, except: [:create, :destroy] + before_action :set_media_attachment, except: [:create] + before_action :check_processing, except: [:create] def show render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment @@ -25,15 +25,6 @@ class Api::V1::MediaController < Api::BaseController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment end - def destroy - @media_attachment = current_account.media_attachments.find(params[:id]) - - return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil? - - @media_attachment.destroy - render_empty - end - private def status_code_for_media_attachment @@ -63,8 +54,4 @@ class Api::V1::MediaController < Api::BaseController def processing_error { error: 'Error processing thumbnail for uploaded media' } end - - def in_usage_error - { error: 'Media attachment is currently used by a status' } - end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index d2b50e3336..555485823c 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -17,7 +17,7 @@ class Api::V1::MutesController < Api::BaseController end def paginated_mutes - @paginated_mutes ||= Mute.eager_load(target_account: [:account_stat, :user]) + @paginated_mutes ||= Mute.eager_load(:target_account) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) @@ -28,6 +28,10 @@ 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 @@ -36,11 +40,19 @@ class Api::V1::MutesController < Api::BaseController api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty? end - def pagination_collection - paginated_mutes + def pagination_max_id + paginated_mutes.last.id + end + + def pagination_since_id + paginated_mutes.first.id end def records_continue? paginated_mutes.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/notifications/policies_controller.rb b/app/controllers/api/v1/notifications/policies_controller.rb deleted file mode 100644 index 9d70c283be..0000000000 --- a/app/controllers/api/v1/notifications/policies_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# 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::V1::NotificationPolicySerializer - end - - def update - @policy.update!(resource_params) - render json: @policy, serializer: REST::V1::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 diff --git a/app/controllers/api/v1/notifications/requests_controller.rb b/app/controllers/api/v1/notifications/requests_controller.rb deleted file mode 100644 index 3c90f13ce2..0000000000 --- a/app/controllers/api/v1/notifications/requests_controller.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Notifications::RequestsController < Api::BaseController - include Redisable - - before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: [:index, :show, :merged?] - before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: [:index, :show, :merged?] - - before_action :require_user! - before_action :set_request, only: [:show, :accept, :dismiss] - before_action :set_requests, only: [:accept_bulk, :dismiss_bulk] - - 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 merged? - render json: { merged: redis.get("notification_unfilter_jobs:#{current_account.id}").to_i <= 0 } - end - - def show - render json: @request, serializer: REST::NotificationRequestSerializer - end - - def accept - AcceptNotificationRequestService.new.call(@request) - render_empty - end - - def dismiss - DismissNotificationRequestService.new.call(@request) - render_empty - end - - def accept_bulk - @requests.each { |request| AcceptNotificationRequestService.new.call(request) } - render_empty - end - - def dismiss_bulk - @requests.each(&:destroy!) - render_empty - end - - private - - def load_requests - requests = NotificationRequest.where(account: current_account).without_suspended.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| - preload_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 set_requests - @requests = NotificationRequest.where(account: current_account, id: Array(params[:id]).uniq.map(&:to_i)) - end - - def next_path - api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty? - end - - def records_continue? - @requests.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def pagination_max_id - @requests.last.id - end - - def pagination_since_id - @requests.first.id - end -end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 13919b400d..406ab97538 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -7,8 +7,6 @@ class Api::V1::NotificationsController < Api::BaseController after_action :insert_pagination_headers, only: :index DEFAULT_NOTIFICATIONS_LIMIT = 40 - DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100 - MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000 def index with_read_replica do @@ -19,14 +17,6 @@ class Api::V1::NotificationsController < Api::BaseController render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships end - def unread_count - limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT) - - with_read_replica do - render json: { count: browserable_account_notifications.paginate_by_min_id(limit, notification_marker&.last_read_id).count } - end - end - def show @notification = current_account.notifications.without_suspended.find(params[:id]) render json: @notification, serializer: REST::NotificationSerializer @@ -51,7 +41,7 @@ class Api::V1::NotificationsController < Api::BaseController ) Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| - preload_collection(target_statuses, Status) + cache_collection(target_statuses, Status) end end @@ -59,19 +49,18 @@ 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], - include_filtered: truthy_param?(:include_filtered) + from_account_id: browserable_params[:account_id] ) end - def notification_marker - current_user.markers.find_by(timeline: 'notifications') - end - def target_statuses_from_notifications @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 @@ -80,15 +69,19 @@ class Api::V1::NotificationsController < Api::BaseController api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? end - def pagination_collection - @notifications + def pagination_max_id + @notifications.last.id + end + + def pagination_since_id + @notifications.first.id end def browserable_params - params.permit(:account_id, :include_filtered, types: [], exclude_types: []) + params.permit(:account_id, types: [], exclude_types: []) end def pagination_params(core_params) - params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params) + params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params) end end diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb index d9c8232702..0c503d9bc5 100644 --- a/app/controllers/api/v1/peers/search_controller.rb +++ b/app/controllers/api/v1/peers/search_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Peers::SearchController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale - LIMIT = 10 - vary_by '' def index @@ -29,7 +27,7 @@ class Api::V1::Peers::SearchController < Api::BaseController @domains = InstancesIndex.query(function_score: { query: { prefix: { - domain: normalized_domain, + domain: TagManager.instance.normalize_domain(params[:q].strip), }, }, @@ -37,20 +35,13 @@ class Api::V1::Peers::SearchController < Api::BaseController field: 'accounts_count', modifier: 'log2p', }, - }).limit(LIMIT).pluck(:domain) + }).limit(10).pluck(:domain) else - domain = normalized_domain - @domains = Instance.searchable.domain_starts_with(domain).limit(LIMIT).pluck(:domain) + domain = params[:q].strip + domain = TagManager.instance.normalize_domain(domain) + @domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain) end rescue Addressable::URI::InvalidURIError @domains = [] end - - def normalized_domain - TagManager.instance.normalize_domain(query_value) - end - - def query_value - params[:q].strip - end end diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index 2833687a38..513b937ef2 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -8,20 +8,20 @@ class Api::V1::Polls::VotesController < Api::BaseController before_action :set_poll def create - VoteService.new.call(current_account, @poll, vote_params) + VoteService.new.call(current_account, @poll, vote_params[:choices]) render json: @poll, serializer: REST::PollSerializer end private def set_poll - @poll = Poll.find(params[:poll_id]) + @poll = Poll.attached.find(params[:poll_id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError not_found end def vote_params - params.require(:choices) + params.permit(choices: []) end end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index b4c25476e8..ffc70a8496 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -15,7 +15,7 @@ class Api::V1::PollsController < Api::BaseController private def set_poll - @poll = Poll.find(params[:id]) + @poll = Poll.attached.find(params[:id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/profile/avatars_controller.rb b/app/controllers/api/v1/profile/avatars_controller.rb index e6c954ed63..bc4d01a597 100644 --- a/app/controllers/api/v1/profile/avatars_controller.rb +++ b/app/controllers/api/v1/profile/avatars_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/profile/headers_controller.rb b/app/controllers/api/v1/profile/headers_controller.rb index 4472a01b05..9f4daa2f77 100644 --- a/app/controllers/api/v1/profile/headers_controller.rb +++ b/app/controllers/api/v1/profile/headers_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { header: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index f2c52f2846..3634acf956 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true class Api::V1::Push::SubscriptionsController < Api::BaseController - include Redisable - include Lockable - before_action -> { doorkeeper_authorize! :push } before_action :require_user! - before_action :set_push_subscription, only: [:show, :update] + before_action :set_push_subscription before_action :check_push_subscription, only: [:show, :update] def show @@ -14,19 +11,16 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def create - with_redis_lock("push_subscription:#{current_user.id}") do - destroy_web_push_subscriptions! + @push_subscription&.destroy! - @push_subscription = Web::PushSubscription.create!( - endpoint: subscription_params[:endpoint], - key_p256dh: subscription_params[:keys][:p256dh], - key_auth: subscription_params[:keys][:auth], - standard: subscription_params[:standard] || false, - data: data_params, - user_id: current_user.id, - access_token_id: doorkeeper_token.id - ) - end + @push_subscription = Web::PushSubscription.create!( + endpoint: subscription_params[:endpoint], + key_p256dh: subscription_params[:keys][:p256dh], + key_auth: subscription_params[:keys][:auth], + data: data_params, + user_id: current_user.id, + access_token_id: doorkeeper_token.id + ) render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end @@ -37,18 +31,14 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def destroy - destroy_web_push_subscriptions! + @push_subscription&.destroy! render_empty end private - def destroy_web_push_subscriptions! - doorkeeper_token.web_push_subscriptions.destroy_all - end - def set_push_subscription - @push_subscription = doorkeeper_token.web_push_subscriptions.first + @push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) end def check_push_subscription @@ -56,12 +46,12 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def subscription_params - params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) + params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end def data_params return {} if params[:data].blank? - params.expect(data: [:policy, alerts: Notification::TYPES]) + params.require(:data).permit(:policy, alerts: Notification::TYPES) end end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 72f358bb5b..300c9faa3f 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -10,7 +10,7 @@ class Api::V1::ReportsController < Api::BaseController @report = ReportService.new.call( current_account, reported_account, - report_params.merge(application: doorkeeper_token.application) + report_params ) render json: @report, serializer: REST::ReportSerializer diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index c62305d711..b33b534ebb 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -44,6 +44,14 @@ class Api::V1::ScheduledStatusesController < Api::BaseController params.permit(:scheduled_at) end + def pagination_params(core_params) + 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 @@ -56,7 +64,11 @@ class Api::V1::ScheduledStatusesController < Api::BaseController @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) end - def pagination_collection - @statuses + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/statuses/base_controller.rb b/app/controllers/api/v1/statuses/base_controller.rb deleted file mode 100644 index 3f56b68bcf..0000000000 --- a/app/controllers/api/v1/statuses/base_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::BaseController < Api::BaseController - include Authorization - - before_action :set_status - - private - - def set_status - @status = Status.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end -end diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb index 109b12f467..19963c002a 100644 --- a/app/controllers/api/v1/statuses/bookmarks_controller.rb +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::BookmarksController < Api::BaseController + include Authorization + before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' } before_action :require_user! - skip_before_action :set_status, only: [:destroy] + before_action :set_status, only: [:create] def create current_account.bookmarks.find_or_create_by!(account: current_account, status: @status) @@ -26,4 +28,13 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController rescue Mastodon::NotPermittedError not_found end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end end diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index 1f103beb71..4dc4bd92c8 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -3,9 +3,9 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :write, :'write:favourites' } + before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' } before_action :require_user! - before_action :set_status, only: %i(create update) + before_action :set_status, only: %i(create update destroy) before_action :set_status_without_authorize, only: [:destroy] def create @@ -27,13 +27,11 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController authorize @status, :show? if emoji_reaction.nil? - UnEmojiReactService.new.call(current_account, @status, emoji_reaction) if emoji_reaction.present? - else - authorize @status, :show? + UnEmojiReactService.new.call(current_account.id, @status.id, emoji_reaction) if emoji_reaction.present? end render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new( - [@status], current_account.id + [@status], current_account.id, emoji_reactions_map: { @status.id => false } ) rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 5a5c2fdc97..73eb11e711 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true -class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController + include Authorization + before_action -> { authorize_if_got_token! :read, :'read:accounts' } + before_action :set_status after_action :insert_pagination_headers def index @@ -14,14 +17,14 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas def load_accounts scope = default_accounts - scope = scope.not_excluded_by_account(current_account) unless current_account.nil? + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope.merge(paginated_favourites).to_a end def default_accounts Account .without_suspended - .includes(:favourites, :account_stat, :user) + .includes(:favourites, :account_stat) .references(:favourites) .where(favourites: { status_id: @status.id }) end @@ -34,6 +37,10 @@ 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 @@ -53,4 +60,15 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index dbc75a0364..f3428e3df4 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::FavouritesController < Api::BaseController + include Authorization + before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action :require_user! - skip_before_action :set_status, only: [:destroy] + before_action :set_status, only: [:create] def create FavouriteService.new.call(current_account, @status) @@ -28,4 +30,13 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle rescue Mastodon::NotPermittedError not_found end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end end diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index e381ea2c67..2913472b04 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true -class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::HistoriesController < Api::BaseController + include Authorization + before_action -> { authorize_if_got_token! :read, :'read:statuses' } + before_action :set_status def show cache_if_unauthenticated! @@ -11,6 +14,13 @@ class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController private def status_edits - @status.edits.ordered.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + end + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found end end diff --git a/app/controllers/api/v1/statuses/mentioned_accounts_controller.rb b/app/controllers/api/v1/statuses/mentioned_accounts_controller.rb deleted file mode 100644 index 4d905ef1a6..0000000000 --- a/app/controllers/api/v1/statuses/mentioned_accounts_controller.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::MentionedAccountsController < Api::BaseController - include Authorization - - before_action -> { authorize_if_got_token! :read, :'read:accounts' } - before_action :set_status - after_action :insert_pagination_headers - - def index - cache_if_unauthenticated! - @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer - end - - private - - def load_accounts - scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? - scope.merge(paginated_mentioned_users).to_a - end - - def default_accounts - Account - .without_suspended - .includes(:mentions, :account_stat) - .references(:mentions) - .where(mentions: { status_id: @status.id }) - end - - def paginated_mentioned_users - Mention.paginate_by_max_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params[:max_id], - params[:since_id] - ) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - api_v1_status_mentioned_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_status_mentioned_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? - end - - def pagination_max_id - @accounts.last.mentions.last.id - end - - def pagination_since_id - @accounts.first.mentions.first.id - end - - def records_continue? - @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def set_status - @status = Status.find(params[:status_id]) - authorize @status, :show_mentioned_users? - rescue Mastodon::NotPermittedError - not_found - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end -end diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index 26b92bb8af..87071a2b9a 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -class Api::V1::Statuses::MutesController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::MutesController < Api::BaseController + include Authorization + before_action -> { doorkeeper_authorize! :write, :'write:mutes' } before_action :require_user! + before_action :set_status before_action :set_conversation def create @@ -21,6 +24,13 @@ class Api::V1::Statuses::MutesController < Api::V1::Statuses::BaseController private + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + def set_conversation @conversation = @status.conversation raise Mastodon::ValidationError if @conversation.nil? diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 7107890af1..51b1621b6f 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::PinsController < Api::BaseController + include Authorization + before_action -> { doorkeeper_authorize! :write, :'write:accounts' } before_action :require_user! + before_action :set_status def create StatusPin.create!(account: current_account, status: @status) @@ -23,6 +26,10 @@ class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController private + def set_status + @status = Status.find(params[:status_id]) + end + def distribute_add_activity! json = ActiveModelSerializers::SerializableResource.new( @status, diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 0eba4fae32..5cc71a6eab 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true -class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController + include Authorization + before_action -> { authorize_if_got_token! :read, :'read:accounts' } + before_action :set_status after_action :insert_pagination_headers def index @@ -14,22 +17,26 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base def load_accounts scope = default_accounts - scope = scope.not_excluded_by_account(current_account) unless current_account.nil? + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope.merge(paginated_statuses).to_a end def default_accounts - Account.without_suspended.includes(:statuses, :account_stat, :user).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) end def paginated_statuses - Status.where(reblog_of_id: @status.id).distributable_visibility.paginate_by_max_id( + Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted, :public_unlisted, :login]).paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id] ) 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 @@ -49,4 +56,15 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base def records_continue? @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) end + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 971b054c54..3ca6231178 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::ReblogsController < Api::BaseController + include Authorization include Redisable include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! before_action :set_reblog, only: [:create] - skip_before_action :set_status override_rate_limit_headers :create, family: :statuses diff --git a/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb b/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb index c13c5ff0e8..b0ad6754ea 100644 --- a/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb +++ b/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb @@ -33,7 +33,7 @@ class Api::V1::Statuses::ReferredByStatusesController < Api::BaseController domains = statuses.filter_map(&:account_domain).uniq relations = account&.relations_map(account_ids, domains) || {} - statuses = preload_collection_paginated_by_id( + statuses = cache_collection_paginated_by_id( statuses, Status, limit_param(DEFAULT_STATUSES_LIMIT), diff --git a/app/controllers/api/v1/statuses/sources_controller.rb b/app/controllers/api/v1/statuses/sources_controller.rb index 5ceda4c7ef..4340864513 100644 --- a/app/controllers/api/v1/statuses/sources_controller.rb +++ b/app/controllers/api/v1/statuses/sources_controller.rb @@ -1,9 +1,21 @@ # frozen_string_literal: true -class Api::V1::Statuses::SourcesController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::SourcesController < Api::BaseController + include Authorization + before_action -> { doorkeeper_authorize! :read, :'read:statuses' } + before_action :set_status def show render json: @status, serializer: REST::StatusSourceSerializer end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end end diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb index bd5cd9bb07..5e5ee7d38e 100644 --- a/app/controllers/api/v1/statuses/translations_controller.rb +++ b/app/controllers/api/v1/statuses/translations_controller.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController +class Api::V1::Statuses::TranslationsController < Api::BaseController + include Authorization + before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action :require_user! + before_action :set_status before_action :set_translation rescue_from TranslationService::NotConfiguredError, with: :not_found @@ -22,7 +25,14 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl private + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + def set_translation - @translation = TranslateStatusService.new.call(@status, I18n.locale.to_s) + @translation = TranslateStatusService.new.call(@status, content_locale) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 1217b70752..065ec07613 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -5,11 +5,9 @@ class Api::V1::StatusesController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy] - before_action :require_user!, except: [:index, :show, :context] - before_action :set_statuses, only: [:index] - before_action :set_status, only: [:show, :context] - before_action :set_thread, only: [:create] - before_action :check_statuses_limit, only: [:index] + before_action :require_user!, except: [:show, :context] + before_action :set_status, only: [:show, :context] + before_action :set_thread, only: [:create] override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :update, family: :statuses @@ -25,14 +23,9 @@ class Api::V1::StatusesController < Api::BaseController DESCENDANTS_LIMIT = 60 DESCENDANTS_DEPTH_LIMIT = 20 - def index - @statuses = preload_collection(@statuses, Status) - render json: @statuses, each_serializer: REST::StatusSerializer - end - def show cache_if_unauthenticated! - @status = preload_collection([@status], Status).first + @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end @@ -52,23 +45,14 @@ class Api::V1::StatusesController < Api::BaseController ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account) descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit) references_results = @status.readable_references(current_account) - loaded_ancestors = preload_collection(ancestors_results, Status) - loaded_descendants = preload_collection(descendants_results, Status) - loaded_references = preload_collection(references_results, Status) - - if params[:with_reference] - loaded_references.reject! { |status| loaded_ancestors.any? { |ancestor| ancestor.id == status.id } } - else - loaded_ancestors = (loaded_ancestors + loaded_references).uniq(&:id) - loaded_references = [] - end + loaded_ancestors = cache_collection(ancestors_results, Status) + loaded_descendants = cache_collection(descendants_results, Status) + loaded_references = cache_collection(references_results, Status) @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants, references: loaded_references) statuses = [@status] + @context.ancestors + @context.descendants + @context.references render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) - - ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies? end def create @@ -93,9 +77,13 @@ class Api::V1::StatusesController < Api::BaseController with_rate_limit: true ) - render json: @status, serializer: serializer_for_status + render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer rescue PostStatusService::UnexpectedMentionsError => e - render json: unexpected_accounts_error_json(e), status: 422 + unexpected_accounts = ActiveModel::Serializer::CollectionSerializer.new( + e.accounts, + serializer: REST::AccountSerializer + ) + render json: { error: e.message, unexpected_accounts: unexpected_accounts }, status: 422 end def update @@ -127,17 +115,13 @@ class Api::V1::StatusesController < Api::BaseController @status.account.statuses_count = @status.account.statuses_count - 1 json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true - RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) }) + RemovalWorker.perform_async(@status.id, { 'redraft' => true }) render json: json end private - def set_statuses - @statuses = Status.permitted_statuses_from_ids(status_ids, current_account) - end - def set_status @status = Status.find(params[:id]) authorize @status, :show? @@ -152,18 +136,6 @@ class Api::V1::StatusesController < Api::BaseController render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404 end - def check_statuses_limit - raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT - end - - def status_ids - Array(statuses_params[:id]).uniq.map(&:to_i) - end - - def statuses_params - params.permit(id: []) - end - def status_params params.permit( :status, @@ -195,18 +167,7 @@ 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) + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) end end diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index df9346832f..9737ae5cb6 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -2,27 +2,23 @@ class Api::V1::SuggestionsController < Api::BaseController include Authorization - include DeprecationConcern - deprecate_api '2021-05-16', only: [:index] - - before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index - before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + before_action -> { doorkeeper_authorize! :read } before_action :require_user! - before_action :set_suggestions def index - render json: @suggestions.get(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:offset].to_i).map(&:account), each_serializer: REST::AccountSerializer + suggestions = suggestions_source.get(current_account, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT)) + render json: suggestions.map(&:account), each_serializer: REST::AccountSerializer end def destroy - @suggestions.remove(params[:id]) + suggestions_source.remove(current_account, params[:id]) render_empty end private - def set_suggestions - @suggestions = AccountSuggestions.new(current_account) + def suggestions_source + AccountSuggestions::PastInteractionsSource.new end end diff --git a/app/controllers/api/v1/timelines/antenna_controller.rb b/app/controllers/api/v1/timelines/antenna_controller.rb index 69554361be..29322e44ae 100644 --- a/app/controllers/api/v1/timelines/antenna_controller.rb +++ b/app/controllers/api/v1/timelines/antenna_controller.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -class Api::V1::Timelines::AntennaController < Api::V1::Timelines::BaseController +class Api::V1::Timelines::AntennaController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:lists' } before_action :require_user! before_action :set_antenna before_action :set_statuses - PERMITTED_PARAMS = %i(limit).freeze + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show render json: @statuses, @@ -25,7 +25,7 @@ class Api::V1::Timelines::AntennaController < Api::V1::Timelines::BaseController end def cached_list_statuses - preload_collection list_statuses, Status + cache_collection list_statuses, Status end def list_statuses @@ -41,11 +41,27 @@ class Api::V1::Timelines::AntennaController < Api::V1::Timelines::BaseController AntennaFeed.new(@antenna) 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 + def next_path - api_v1_timelines_antenna_url params[:id], next_path_params + api_v1_timelines_antenna_url params[:id], pagination_params(max_id: pagination_max_id) end def prev_path - api_v1_timelines_antenna_url params[:id], prev_path_params + api_v1_timelines_antenna_url params[:id], pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb deleted file mode 100644 index 1dba4a5bb2..0000000000 --- a/app/controllers/api/v1/timelines/base_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Timelines::BaseController < Api::BaseController - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } - - before_action :require_user!, if: :require_auth? - - private - - def require_auth? - !Setting.timeline_preview - end - - def pagination_collection - @statuses - end - - def next_path_params - permitted_params.merge(max_id: pagination_max_id) - end - - def prev_path_params - permitted_params.merge(min_id: pagination_since_id) - end - - def permitted_params - params - .slice(*self.class::PERMITTED_PARAMS) - .permit(*self.class::PERMITTED_PARAMS) - end -end diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index e48c5ae251..b7c78e8734 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController +class Api::V1::Timelines::HomeController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] before_action :require_user!, only: [:show] - - PERMITTED_PARAMS = %i(local limit).freeze + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show with_read_replica do @@ -23,11 +22,11 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController private def load_statuses - preloaded_home_statuses + cached_home_statuses end - def preloaded_home_statuses - preload_collection home_statuses, Status + def cached_home_statuses + cache_collection home_statuses, Status end def home_statuses @@ -43,11 +42,27 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController HomeFeed.new(current_account) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_params(core_params) + params.slice(:local, :limit).permit(:local, :limit).merge(core_params) + end + def next_path - api_v1_timelines_home_url next_path_params + api_v1_timelines_home_url pagination_params(max_id: pagination_max_id) end def prev_path - api_v1_timelines_home_url prev_path_params + api_v1_timelines_home_url pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/timelines/link_controller.rb b/app/controllers/api/v1/timelines/link_controller.rb deleted file mode 100644 index 37ed084f06..0000000000 --- a/app/controllers/api/v1/timelines/link_controller.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController - before_action -> { authorize_if_got_token! :read, :'read:statuses' } - before_action :set_preview_card - before_action :set_statuses - - PERMITTED_PARAMS = %i( - url - limit - ).freeze - - def show - cache_if_unauthenticated! - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) - end - - private - - def set_preview_card - @preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url]) - end - - def set_statuses - @statuses = @preview_card.nil? ? [] : preload_collection(link_timeline_statuses, Status) - end - - def link_timeline_statuses - link_feed.get( - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id], - params[:min_id] - ) - end - - def link_feed - LinkFeed.new(@preview_card, current_account) - end - - def next_path - api_v1_timelines_link_url next_path_params - end - - def prev_path - api_v1_timelines_link_url prev_path_params - end -end diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb index d8cdbdb74c..a15eae468d 100644 --- a/app/controllers/api/v1/timelines/list_controller.rb +++ b/app/controllers/api/v1/timelines/list_controller.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController +class Api::V1::Timelines::ListController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:lists' } before_action :require_user! before_action :set_list before_action :set_statuses - PERMITTED_PARAMS = %i(limit).freeze + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show render json: @statuses, @@ -21,11 +21,11 @@ class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController end def set_statuses - @statuses = preloaded_list_statuses + @statuses = cached_list_statuses end - def preloaded_list_statuses - preload_collection list_statuses, Status + def cached_list_statuses + cache_collection list_statuses, Status end def list_statuses @@ -41,11 +41,27 @@ class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController ListFeed.new(@list) 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 + def next_path - api_v1_timelines_list_url params[:id], next_path_params + api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id) end def prev_path - api_v1_timelines_list_url params[:id], prev_path_params + api_v1_timelines_list_url params[:id], pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 73dcc5aee4..b71a26719c 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController +class Api::V1::Timelines::PublicController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' } - - PERMITTED_PARAMS = %i(local remote limit only_media).freeze + before_action :require_user!, only: [:show], if: :require_auth? + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show cache_if_unauthenticated! @@ -15,12 +15,16 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController private - def load_statuses - preloaded_public_statuses_page + def require_auth? + !Setting.timeline_preview end - def preloaded_public_statuses_page - preload_collection(public_statuses, Status) + def load_statuses + cached_public_statuses_page + end + + def cached_public_statuses_page + cache_collection(public_statuses, Status) end def public_statuses @@ -41,11 +45,27 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_params(core_params) + params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params) + end + def next_path - api_v1_timelines_public_url next_path_params + api_v1_timelines_public_url pagination_params(max_id: pagination_max_id) end def prev_path - api_v1_timelines_public_url prev_path_params + api_v1_timelines_public_url pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index b9855fb67a..1084a3a7d9 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController +class Api::V1::Timelines::TagController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' } + before_action :require_user!, if: :require_auth? before_action :load_tag - - PERMITTED_PARAMS = %i(local limit only_media).freeze + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show cache_if_unauthenticated! @@ -25,11 +25,11 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController end def load_statuses - preloaded_tagged_statuses + cached_tagged_statuses end - def preloaded_tagged_statuses - @tag.nil? ? [] : preload_collection(tag_timeline_statuses, Status) + def cached_tagged_statuses + @tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status) end def tag_timeline_statuses @@ -54,11 +54,27 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_params(core_params) + params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) + end + def next_path - api_v1_timelines_tag_url params[:id], next_path_params + api_v1_timelines_tag_url params[:id], pagination_params(max_id: pagination_max_id) end def prev_path - api_v1_timelines_tag_url params[:id], prev_path_params + api_v1_timelines_tag_url params[:id], pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 3c5aecff43..57cfa0b7e4 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -34,6 +34,14 @@ 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 + def next_path api_v1_trends_links_url pagination_params(offset: offset_param + limit_param(DEFAULT_LINKS_LIMIT)) if records_continue? end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index cdbfce0685..c186864c3b 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Trends::StatusesController < Api::BaseController def set_statuses @statuses = if enabled? - preload_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) + cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) else [] end @@ -32,6 +32,14 @@ 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 + def next_path api_v1_trends_statuses_url pagination_params(offset: offset_param + limit_param(DEFAULT_STATUSES_LIMIT)) if records_continue? end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index ecac3579fc..aca3dd7089 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true class Api::V1::Trends::TagsController < Api::BaseController - include DeprecationConcern - before_action :set_tags after_action :insert_pagination_headers - DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i - - deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' } + DEFAULT_TAGS_LIMIT = 10 def index cache_if_unauthenticated! @@ -31,9 +27,15 @@ class Api::V1::Trends::TagsController < Api::BaseController end def tags_from_trends - scope = Trends.tags.query.allowed.in_locale(content_locale) - scope = scope.filtered_for(current_account) if user_signed_in? - scope + 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 def next_path diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 51e778e1d6..f3e9938d8c 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -35,7 +35,7 @@ class Api::V2::FiltersController < Api::BaseController private def set_filters - @filters = current_account.custom_filters.includes(:keywords, :statuses) + @filters = current_account.custom_filters.includes(:keywords) end def set_filter @@ -43,6 +43,6 @@ class Api::V2::FiltersController < Api::BaseController end def resource_params - params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) end end diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index 62adf95260..8346e28830 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -1,16 +1,6 @@ # frozen_string_literal: true -class Api::V2::InstancesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? - skip_around_action :set_locale - - vary_by '' - - # Override `current_user` to avoid reading session cookies unless in limited federation mode - def current_user - super if limited_federation_mode? - end - +class Api::V2::InstancesController < Api::V1::InstancesController def show cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 36c15165da..72bc694421 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -2,22 +2,12 @@ class Api::V2::MediaController < Api::V1::MediaController def create - @media_attachment = current_account.media_attachments.create!(media_and_delay_params) - render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_from_media_processing + @media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params)) + render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 rescue Paperclip::Error => e Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end - - private - - def media_and_delay_params - { delay_processing: true }.merge(media_attachment_params) - end - - def status_from_media_processing - @media_attachment.not_processed? ? 202 : 200 - end end diff --git a/app/controllers/api/v2/notifications/accounts_controller.rb b/app/controllers/api/v2/notifications/accounts_controller.rb deleted file mode 100644 index 771e966388..0000000000 --- a/app/controllers/api/v2/notifications/accounts_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class Api::V2::Notifications::AccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:notifications' } - before_action :require_user! - before_action :set_notifications! - after_action :insert_pagination_headers, only: :index - - def index - @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer - end - - private - - def load_accounts - @paginated_notifications.map(&:from_account) - end - - def set_notifications! - @paginated_notifications = begin - current_account - .notifications - .without_suspended - .where(group_key: params[:notification_group_key]) - .includes(from_account: [:account_stat, :user]) - .paginate_by_max_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params[:max_id], - params[:since_id] - ) - end - end - - def next_path - api_v2_notification_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v2_notification_accounts_url pagination_params(min_id: pagination_since_id) unless @paginated_notifications.empty? - end - - def pagination_collection - @paginated_notifications - end - - def records_continue? - @paginated_notifications.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end -end diff --git a/app/controllers/api/v2/notifications/policies_controller.rb b/app/controllers/api/v2/notifications/policies_controller.rb deleted file mode 100644 index 637587967f..0000000000 --- a/app/controllers/api/v2/notifications/policies_controller.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class Api::V2::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( - :for_not_following, - :for_not_followers, - :for_new_accounts, - :for_private_mentions, - :for_limited_accounts - ) - end -end diff --git a/app/controllers/api/v2/notifications_controller.rb b/app/controllers/api/v2/notifications_controller.rb deleted file mode 100644 index 848c361cfc..0000000000 --- a/app/controllers/api/v2/notifications_controller.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -class Api::V2::NotificationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] - before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] - before_action :require_user! - after_action :insert_pagination_headers, only: :index - - DEFAULT_NOTIFICATIONS_LIMIT = 40 - DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100 - MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000 - - def index - with_read_replica do - @notifications = load_notifications - @grouped_notifications = load_grouped_notifications - @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) - @presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param) - - # Preload associations to avoid N+1s - ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call - end - - MastodonOTELTracer.in_span('Api::V2::NotificationsController#index rendering') do |span| - statuses = @grouped_notifications.filter_map { |group| group.target_status&.id } - - span.add_attributes( - 'app.notification_grouping.count' => @grouped_notifications.size, - 'app.notification_grouping.account.count' => @presenter.accounts.size, - 'app.notification_grouping.partial_account.count' => @presenter.partial_accounts.size, - 'app.notification_grouping.status.count' => statuses.size, - 'app.notification_grouping.status.unique_count' => statuses.uniq.size, - 'app.notification_grouping.expand_accounts_param' => expand_accounts_param - ) - - render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param - end - end - - def unread_count - limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT) - - with_read_replica do - render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id, grouped_types: params[:grouped_types]).count } - end - end - - def show - @notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take! - presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification])) - render json: presenter, serializer: REST::DedupNotificationGroupSerializer - end - - def clear - current_account.notifications.delete_all - render_empty - end - - def dismiss - current_account.notifications.by_group_key(params[:group_key]).destroy_all - render_empty - end - - private - - def load_notifications - MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_notifications') do - notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id( - limit_param(DEFAULT_NOTIFICATIONS_LIMIT), - params.slice(:max_id, :since_id, :min_id, :grouped_types).permit(:max_id, :since_id, :min_id, grouped_types: []) - ) - - Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| - preload_collection(target_statuses, Status) - end - end - end - - def load_grouped_notifications - return [] if @notifications.empty? - - MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do - pagination_range = (@notifications.last.id)..@notifications.first.id - - # If the page is incomplete, we know we are on the last page - if incomplete_page? - if paginating_up? - pagination_range = @notifications.last.id...(params[:max_id]&.to_i) - else - range_start = params[:since_id]&.to_i - range_start += 1 unless range_start.nil? - pagination_range = range_start..(@notifications.first.id) - end - end - - NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types]) - end - end - - def incomplete_page? - @notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT) - end - - def paginating_up? - params[:min_id].present? - end - - def browserable_account_notifications - current_account.notifications.without_suspended.browserable( - types: Array(browserable_params[:types]), - exclude_types: Array(browserable_params[:exclude_types]), - include_filtered: truthy_param?(:include_filtered) - ) - end - - def notification_marker - current_user.markers.find_by(timeline: 'notifications') - end - - def target_statuses_from_notifications - @notifications.filter_map(&:target_status) - end - - def next_path - api_v2_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty? - end - - def prev_path - api_v2_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? - end - - def pagination_collection - @notifications - end - - def browserable_params - params.slice(:include_filtered, :types, :exclude_types, :grouped_types).permit(:include_filtered, types: [], exclude_types: [], grouped_types: []) - end - - def pagination_params(core_params) - params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types).permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: []).merge(core_params) - end - - def expand_accounts_param - case params[:expand_accounts] - when nil, 'full' - 'full' - when 'partial_avatars' - 'partial_avatars' - else - raise Mastodon::InvalidParameterError, "Invalid value for 'expand_accounts': '#{params[:expand_accounts]}', allowed values are 'full' and 'partial_avatars'" - end - end -end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index a701cbe582..3b450b2420 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -8,12 +8,6 @@ class Api::V2::SearchController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:search' } before_action :validate_search_params! - with_options unless: :user_signed_in? do - before_action :query_pagination_error, if: :pagination_requested? - before_action :remote_resolve_error, if: :remote_resolve_requested? - end - before_action :require_valid_pagination_options! - def index @search = Search.new(search_results) render json: @search, serializer: REST::SearchSerializer @@ -27,22 +21,12 @@ class Api::V2::SearchController < Api::BaseController def validate_search_params! params.require(:q) - end - def query_pagination_error - render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 - end + return if user_signed_in? - def remote_resolve_error - render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 - end + return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present? - def remote_resolve_requested? - truthy_param?(:resolve) - end - - def pagination_requested? - params[:offset].present? + render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve) end def search_results @@ -50,15 +34,7 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - combined_search_params - ) - end - - def combined_search_params - search_params.merge( - resolve: truthy_param?(:resolve), - exclude_unreviewed: truthy_param?(:exclude_unreviewed), - following: truthy_param?(:following) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) ) end diff --git a/app/controllers/api/v2/suggestions_controller.rb b/app/controllers/api/v2/suggestions_controller.rb index 8516796e86..35eb276c01 100644 --- a/app/controllers/api/v2/suggestions_controller.rb +++ b/app/controllers/api/v2/suggestions_controller.rb @@ -3,23 +3,17 @@ class Api::V2::SuggestionsController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index - before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + before_action -> { doorkeeper_authorize! :read } before_action :require_user! before_action :set_suggestions def index - render json: @suggestions.get(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:offset].to_i), each_serializer: REST::SuggestionSerializer - end - - def destroy - @suggestions.remove(params[:id]) - render_empty + render json: @suggestions, each_serializer: REST::SuggestionSerializer end private def set_suggestions - @suggestions = AccountSuggestions.new(current_account) + @suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT)) end end diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb index f82c1c50d7..63c3f2d90a 100644 --- a/app/controllers/api/web/embeds_controller.rb +++ b/app/controllers/api/web/embeds_controller.rb @@ -9,7 +9,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController return not_found if @status.hidden? if @status.local? - render json: @status, serializer: OEmbedSerializer + render json: @status, serializer: OEmbedSerializer, width: 400 else return not_found unless user_signed_in? diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 2711071b4a..5167928e93 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -1,15 +1,39 @@ # frozen_string_literal: true class Api::Web::PushSubscriptionsController < Api::Web::BaseController - before_action :require_user!, except: :destroy + before_action :require_user! before_action :set_push_subscription, only: :update - before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions? - after_action :update_session_with_subscription, only: :create def create - @push_subscription = ::Web::PushSubscription.create!(web_push_subscription_params) + active_session = current_session - render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer + unless active_session.web_push_subscription.nil? + active_session.web_push_subscription.destroy! + active_session.update!(web_push_subscription: nil) + end + + # Mobile devices do not support regular notifications, so we enable push notifications by default + alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet? + + data = { + policy: 'all', + alerts: Notification::TYPES.index_with { alerts_enabled }, + } + + data.deep_merge!(data_params) if params[:data] + + push_subscription = ::Web::PushSubscription.create!( + endpoint: subscription_params[:endpoint], + key_p256dh: subscription_params[:keys][:p256dh], + key_auth: subscription_params[:keys][:auth], + data: data, + user_id: active_session.user_id, + access_token_id: active_session.access_token_id + ) + + active_session.update!(web_push_subscription: push_subscription) + + render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer end def update @@ -17,71 +41,17 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end - def destroy - push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id]) - push_subscription&.destroy - - head 200 - end - private - def active_session - @active_session ||= current_session - end - - def destroy_previous_subscriptions - active_session.web_push_subscription.destroy! - active_session.update!(web_push_subscription: nil) - end - - def prior_subscriptions? - active_session.web_push_subscription.present? - end - - def subscription_data - default_subscription_data.tap do |data| - data.deep_merge!(data_params) if params[:data] - end - end - - def default_subscription_data - { - policy: 'all', - alerts: Notification::TYPES.index_with { alerts_enabled }, - } - end - - def alerts_enabled - # Mobile devices do not support regular notifications, so we enable push notifications by default - active_session.detection.device.mobile? || active_session.detection.device.tablet? - end - - def update_session_with_subscription - active_session.update!(web_push_subscription: @push_subscription) - end - def set_push_subscription @push_subscription = ::Web::PushSubscription.find(params[:id]) end def subscription_params - @subscription_params ||= params.expect(subscription: [:standard, :endpoint, keys: [:auth, :p256dh]]) - end - - def web_push_subscription_params - { - access_token_id: active_session.access_token_id, - data: subscription_data, - endpoint: subscription_params[:endpoint], - key_auth: subscription_params[:keys][:auth], - key_p256dh: subscription_params[:keys][:p256dh], - standard: subscription_params[:standard] || false, - user_id: active_session.user_id, - } + @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end def data_params - @data_params ||= params.expect(data: [:policy, alerts: Notification::TYPES]) + @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1b071e8655..6ec93f824e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,19 +9,19 @@ class ApplicationController < ActionController::Base include UserTrackingConcern include SessionTrackingConcern include CacheConcern - include PreloadingConcern include DomainControlHelper include DatabaseHelper include AuthorizedFetchHelper - include SelfDestructHelper helper_method :current_account helper_method :current_session helper_method :current_theme helper_method :single_user_mode? helper_method :use_seamless_external_login? + helper_method :omniauth_only? helper_method :sso_account_settings helper_method :limited_federation_mode? + helper_method :body_class_string helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request @@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests - rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) + rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable rescue_from Seahorse::Client::NetworkingError do |e| @@ -39,8 +39,6 @@ class ApplicationController < ActionController::Base service_unavailable end - before_action :check_self_destruct! - before_action :store_referrer, except: :raise_not_found, if: :devise_controller? before_action :require_functional!, if: :user_signed_in? @@ -70,13 +68,7 @@ class ApplicationController < ActionController::Base end def require_functional! - return if current_user.functional? - - if current_user.confirmed? - redirect_to edit_user_registration_path - else - redirect_to auth_setup_path - end + redirect_to edit_user_registration_path unless current_user.functional? end def skip_csrf_meta_tags? @@ -134,13 +126,17 @@ class ApplicationController < ActionController::Base end def single_user_mode? - @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists? + @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? end def use_seamless_external_login? Devise.pam_authentication || Devise.ldap_authentication end + def omniauth_only? + ENV['OMNIAUTH_ONLY'] == 'true' + end + def sso_account_settings ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) end @@ -163,6 +159,10 @@ class ApplicationController < ActionController::Base current_user.setting_theme end + def body_class_string + @body_classes || '' + end + def respond_with_error(code) respond_to do |format| format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] } @@ -170,15 +170,6 @@ class ApplicationController < ActionController::Base end end - def check_self_destruct! - return unless self_destruct? - - respond_to do |format| - format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] } - format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 } - end - end - def set_cache_control_defaults response.cache_control.replace(private: true, no_store: true) end diff --git a/app/controllers/auth/challenges_controller.rb b/app/controllers/auth/challenges_controller.rb index 7ede420b51..060944240a 100644 --- a/app/controllers/auth/challenges_controller.rb +++ b/app/controllers/auth/challenges_controller.rb @@ -7,7 +7,6 @@ class Auth::ChallengesController < ApplicationController before_action :authenticate_user! - skip_before_action :check_self_destruct! skip_before_action :require_functional! def create diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 13c2468eb8..632b624a37 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -1,26 +1,20 @@ # frozen_string_literal: true class Auth::ConfirmationsController < Devise::ConfirmationsController - include Auth::CaptchaConcern - include RegistrationLimitationHelper + include CaptchaConcern layout 'auth' + before_action :set_body_classes before_action :set_confirmation_user!, only: [:show, :confirm_captcha] - before_action :redirect_confirmed_user, if: :signed_in_confirmed_user? + before_action :require_unconfirmed! before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] before_action :require_captcha_if_needed!, only: [:show] - skip_before_action :check_self_destruct! skip_before_action :require_functional! def show - if reach_registrations_limit? && !current_user&.valid_invitation? - render :limitation_error - return - end - old_session_values = session.to_hash reset_session session.update old_session_values.except('session_id') @@ -44,12 +38,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController show end - def redirect_to_app? - truthy_param?(:redirect_to_app) - end - - helper_method :redirect_to_app? - private def require_captcha_if_needed! @@ -67,15 +55,17 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController end def captcha_user_bypass? - @confirmation_user.nil? || @confirmation_user.confirmed? + return true if @confirmation_user.nil? || @confirmation_user.confirmed? end - def redirect_confirmed_user - redirect_to(current_user.approved? ? root_path : edit_user_registration_path) + def require_unconfirmed! + if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? + redirect_to(current_user.approved? ? root_path : edit_user_registration_path) + end end - def signed_in_confirmed_user? - user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? + def set_body_classes + @body_classes = 'lighter' end def after_resending_confirmation_instructions_path_for(_resource_name) @@ -91,7 +81,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController end def after_confirmation_path_for(_resource_name, user) - if user.created_by_application && redirect_to_app? + if user.created_by_application && truthy_param?(:redirect_to_app) user.created_by_application.confirmation_redirect_uri elsif user_signed_in? web_url('start') diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 9d496220a3..b8570d0bfa 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController - skip_before_action :check_self_destruct! skip_before_action :verify_authenticity_token def self.provides_callback_for(provider) diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 7c1ff59671..a8ad669297 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Auth::PasswordsController < Devise::PasswordsController - skip_before_action :check_self_destruct! - before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid? + before_action :check_validity_of_reset_password_token, only: :edit + before_action :set_body_classes layout 'auth' @@ -18,9 +18,15 @@ class Auth::PasswordsController < Devise::PasswordsController private - def redirect_invalid_reset_token - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) + def check_validity_of_reset_password_token + unless reset_password_token_is_valid? + flash[:error] = I18n.t('auth.invalid_reset_password_token') + redirect_to new_password_path(resource_name) + end + end + + def set_body_classes + @body_classes = 'lighter' end def reset_password_token_is_valid? diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 0b6f5b3af4..e70ae5b1b8 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class Auth::RegistrationsController < Devise::RegistrationsController - include RegistrationHelper - include Auth::RegistrationSpamConcern + include RegistrationSpamConcern layout :determine_layout @@ -11,26 +10,20 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :set_sessions, only: [:edit, :update] before_action :set_strikes, only: [:edit, :update] + before_action :set_instance_presenter, only: [:new, :create, :update] + before_action :set_body_classes, only: [:new, :create, :edit, :update] before_action :require_not_suspended!, only: [:update] + before_action :set_cache_headers, only: [:edit, :update] before_action :set_rules, only: :new before_action :require_rules_acceptance!, only: :new before_action :set_registration_form_time, only: :new - skip_before_action :check_self_destruct!, only: [:edit, :update] skip_before_action :require_functional!, only: [:edit, :update] def new super(&:build_invite_request) end - def edit # rubocop:disable Lint/UselessMethodDefinition - super - end - - def create # rubocop:disable Lint/UselessMethodDefinition - super - end - def update super do |resource| resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? @@ -50,7 +43,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def build_resource(hash = nil) - super + super(hash) resource.locale = I18n.locale resource.invite_code = @invite&.code if resource.invite_code.blank? @@ -62,7 +55,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |user_params| - user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth) + user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) end end @@ -89,7 +82,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def check_enabled_registrations - redirect_to root_path unless allowed_registration?(request.remote_ip, @invite) + redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations? || ip_blocked? + end + + def allowed_registrations? + Setting.registrations_mode != 'none' || @invite&.valid_for_use? + end + + def omniauth_only? + ENV['OMNIAUTH_ONLY'] == 'true' + end + + def ip_blocked? + IpBlock.where(severity: :sign_up_block).where('ip >>= ?', request.remote_ip.to_s).exists? end def invite_code @@ -102,6 +107,14 @@ class Auth::RegistrationsController < Devise::RegistrationsController private + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def set_body_classes + @body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter' + end + def set_invite @invite = begin invite = Invite.find_by(code: invite_code) if invite_code.present? @@ -122,7 +135,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def require_not_suspended! - forbidden if current_account.unavailable? + forbidden if current_account.suspended? end def set_rules @@ -138,11 +151,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController set_locale { render :rules } end - def is_flashing_format? # rubocop:disable Naming/PredicateName - if params[:action] == 'create' - false # Disable flash messages for sign-up - else - super - end + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 5f9f133659..8212e2e630 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -7,19 +7,26 @@ class Auth::SessionsController < Devise::SessionsController layout 'auth' - skip_before_action :check_self_destruct! skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_functional! skip_before_action :update_user_sign_in prepend_before_action :check_suspicious!, only: [:create] - include Auth::TwoFactorAuthenticationConcern + include TwoFactorAuthenticationConcern + + before_action :set_instance_presenter, only: [:new] + before_action :set_body_classes content_security_policy only: :new do |p| p.form_action(false) end + def check_suspicious! + user = find_user + @login_is_suspicious = suspicious_sign_in?(user) unless user.nil? + end + def create super do |resource| # We only need to call this if this hasn't already been @@ -73,11 +80,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.expect(user: [:email, :password, :otp_attempt, :disable_css, credential: {}]) - end - - def login_page_params - params.permit(:with_options) + params.require(:user).permit(:email, :password, :otp_attempt, credential: {}) end def after_sign_in_path_for(resource) @@ -100,9 +103,12 @@ class Auth::SessionsController < Devise::SessionsController private - def check_suspicious! - user = find_user - @login_is_suspicious = suspicious_sign_in?(user) unless user.nil? + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def set_body_classes + @body_classes = 'lighter' end def home_paths(resource) @@ -117,11 +123,6 @@ class Auth::SessionsController < Devise::SessionsController truthy_param?(:continue) end - def with_login_options? - login_page_params[:with_options] == '1' - end - helper_method :with_login_options? - def restart_session clear_attempt_from_session redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout') @@ -160,8 +161,6 @@ class Auth::SessionsController < Devise::SessionsController sign_in(user) flash.delete(:notice) - disable_custom_css!(user) if disable_custom_css? - LoginActivity.create( user: user, success: true, @@ -173,15 +172,6 @@ class Auth::SessionsController < Devise::SessionsController UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious end - def disable_custom_css? - user_params[:disable_css].present? && user_params[:disable_css] == '1' - end - - def disable_custom_css!(user) - user.settings['web.use_custom_css'] = false - user.save! - end - def suspicious_sign_in?(user) SuspiciousSignInDetector.new(user).suspicious?(request) end @@ -195,27 +185,9 @@ class Auth::SessionsController < Devise::SessionsController ip: request.remote_ip, user_agent: request.user_agent ) - - # Only send a notification email every hour at most - 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 def second_factor_attempts_key(user) "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" end - - def respond_to_on_destroy - respond_to do |format| - format.json do - render json: { - redirect_to: after_sign_out_path_for(resource_name), - }, status: 200 - end - format.all { super } - end - end end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 5e7b14646a..40916d2887 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -5,6 +5,7 @@ class Auth::SetupController < ApplicationController before_action :authenticate_user! before_action :require_unconfirmed_or_pending! + before_action :set_body_classes before_action :set_user skip_before_action :require_functional! @@ -18,7 +19,7 @@ class Auth::SetupController < ApplicationController if @user.update(user_params) @user.resend_confirmation_instructions unless @user.confirmed? - redirect_to auth_setup_path, notice: t('auth.setup.new_confirmation_instructions_sent') + redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') else render :show end @@ -34,7 +35,11 @@ class Auth::SetupController < ApplicationController @user = current_user end + def set_body_classes + @body_classes = 'lighter' + end + def user_params - params.expect(user: [:email]) + params.require(:user).permit(:email) end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 076d19874b..db23fefbbc 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -3,21 +3,18 @@ class BackupsController < ApplicationController include RoutingHelper - skip_before_action :check_self_destruct! skip_before_action :require_functional! before_action :authenticate_user! before_action :set_backup - BACKUP_LINK_TIMEOUT = 1.hour.freeze - def download case Paperclip::Attachment.default_options[:storage] when :s3, :azure - redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true + redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true else redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end diff --git a/app/controllers/concerns/api/access_token_tracking_concern.rb b/app/controllers/concerns/access_token_tracking_concern.rb similarity index 92% rename from app/controllers/concerns/api/access_token_tracking_concern.rb rename to app/controllers/concerns/access_token_tracking_concern.rb index bc6ae51c77..cf60cfb995 100644 --- a/app/controllers/concerns/api/access_token_tracking_concern.rb +++ b/app/controllers/concerns/access_token_tracking_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Api::AccessTokenTrackingConcern +module AccessTokenTrackingConcern extend ActiveSupport::Concern ACCESS_TOKEN_UPDATE_FREQUENCY = 24.hours.freeze diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index b75f3e3581..e9cff22ca8 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -9,18 +9,24 @@ module AccountControllerConcern FOLLOW_PER_PAGE = 12 included do + before_action :set_instance_presenter + after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end private + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + def set_link_headers response.headers['Link'] = LinkHeader.new( [ webfinger_account_link, actor_url_link, ] - ).to_s + ) end def webfinger_account_link diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 2b132417f7..3fc0938bfc 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -34,8 +34,8 @@ module AccountOwnedConcern end def check_account_suspension - if @account.permanently_unavailable? - permanent_unavailability_response + if @account.suspended_permanently? + permanent_suspension_response elsif @account.suspended? && !skip_temporary_suspension_response? temporary_suspension_response end @@ -45,7 +45,7 @@ module AccountOwnedConcern false end - def permanent_unavailability_response + def permanent_suspension_response expires_in(3.minutes, public: true) gone end diff --git a/app/controllers/concerns/admin/export_controller_concern.rb b/app/controllers/concerns/admin/export_controller_concern.rb deleted file mode 100644 index ce03b2a24a..0000000000 --- a/app/controllers/concerns/admin/export_controller_concern.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Admin::ExportControllerConcern - extend ActiveSupport::Concern - - private - - def send_export_file - respond_to do |format| - format.csv { send_data export_data, filename: export_filename } - end - end - - def export_data - raise 'Override in controller' - end - - def export_filename - raise 'Override in controller' - end - - def set_dummy_import! - @import = Admin::Import.new - end - - def import_params - params.expect(admin_import: [:data]) - end -end diff --git a/app/controllers/concerns/admin_export_controller_concern.rb b/app/controllers/concerns/admin_export_controller_concern.rb new file mode 100644 index 0000000000..4ac48a04b7 --- /dev/null +++ b/app/controllers/concerns/admin_export_controller_concern.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module AdminExportControllerConcern + extend ActiveSupport::Concern + + private + + def send_export_file + respond_to do |format| + format.csv { send_data export_data, filename: export_filename } + end + end + + def export_data + raise 'Override in controller' + end + + def export_filename + raise 'Override in controller' + end + + def set_dummy_import! + @import = Admin::Import.new + end + + def import_params + params.require(:admin_import).permit(:data) + end +end diff --git a/app/controllers/concerns/api/caching_concern.rb b/app/controllers/concerns/api/caching_concern.rb deleted file mode 100644 index 55d7fe56d7..0000000000 --- a/app/controllers/concerns/api/caching_concern.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Api::CachingConcern - extend ActiveSupport::Concern - - def cache_if_unauthenticated! - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - end - - def cache_even_if_authenticated! - expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless limited_federation_mode? - end -end diff --git a/app/controllers/concerns/api/content_security_policy.rb b/app/controllers/concerns/api/content_security_policy.rb deleted file mode 100644 index 8116dca57b..0000000000 --- a/app/controllers/concerns/api/content_security_policy.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Api::ContentSecurityPolicy - extend ActiveSupport::Concern - - included do - content_security_policy do |policy| - # Set every directive that does not have a fallback - policy.default_src :none - policy.frame_ancestors :none - policy.form_action :none - - # Disable every directive with a fallback to cut on response size - policy.base_uri false - policy.font_src false - policy.img_src false - policy.style_src false - policy.media_src false - policy.frame_src false - policy.manifest_src false - policy.connect_src false - policy.script_src false - policy.child_src false - policy.worker_src false - end - end -end diff --git a/app/controllers/concerns/api/error_handling.rb b/app/controllers/concerns/api/error_handling.rb deleted file mode 100644 index 9ce4795b02..0000000000 --- a/app/controllers/concerns/api/error_handling.rb +++ /dev/null @@ -1,52 +0,0 @@ -# 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(*Mastodon::HTTP_CONNECTION_ERRORS, 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 diff --git a/app/controllers/concerns/api/pagination.rb b/app/controllers/concerns/api/pagination.rb deleted file mode 100644 index b0b4ae4603..0000000000 --- a/app/controllers/concerns/api/pagination.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Api::Pagination - extend ActiveSupport::Concern - - PAGINATION_PARAMS = %i(limit).freeze - - 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 - links << [prev_path, [%w(rel prev)]] if prev_path - response.headers['Link'] = LinkHeader.new(links).to_s unless links.empty? - end - - def require_valid_pagination_options! - render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid? - end - - def pagination_params(core_params) - params - .slice(*PAGINATION_PARAMS) - .permit(*PAGINATION_PARAMS) - .merge(core_params) - end - - 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 -end diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb new file mode 100644 index 0000000000..12264d514e --- /dev/null +++ b/app/controllers/concerns/api_caching_concern.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ApiCachingConcern + extend ActiveSupport::Concern + + def cache_if_unauthenticated! + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end + + def cache_even_if_authenticated! + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless limited_federation_mode? + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 1cdd4d7cf0..e4e32cdad9 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -3,6 +3,158 @@ module CacheConcern extend ActiveSupport::Concern + module ActiveRecordCoder + EMPTY_HASH = {}.freeze + + class << self + def dump(record) + instances = InstanceTracker.new + serialized_associations = serialize_associations(record, instances) + serialized_records = instances.map { |r| serialize_record(r) } + [serialized_associations, *serialized_records] + end + + def load(payload) + instances = InstanceTracker.new + serialized_associations, *serialized_records = payload + serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } + deserialize_associations(serialized_associations, instances) + end + + private + + # Records without associations, or which have already been visited before, + # are serialized by their id alone. + # + # Records with associations are serialized as a two-element array including + # their id and the record's association cache. + # + def serialize_associations(record, instances) + return unless record + + if (id = instances.lookup(record)) + payload = id + else + payload = instances.push(record) + + cached_associations = record.class.reflect_on_all_associations.select do |reflection| + record.association_cached?(reflection.name) + end + + unless cached_associations.empty? + serialized_associations = cached_associations.map do |reflection| + association = record.association(reflection.name) + + serialized_target = if reflection.collection? + association.target.map { |target_record| serialize_associations(target_record, instances) } + else + serialize_associations(association.target, instances) + end + + [reflection.name, serialized_target] + end + + payload = [payload, serialized_associations] + end + end + + payload + end + + def deserialize_associations(payload, instances) + return unless payload + + id, associations = payload + record = instances.fetch(id) + + associations&.each do |name, serialized_target| + begin + association = record.association(name) + rescue ActiveRecord::AssociationNotFoundError + raise AssociationMissingError, "undefined association: #{name}" + end + + target = if association.reflection.collection? + serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } + else + deserialize_associations(serialized_target, instances) + end + + association.target = target + end + + record + end + + def serialize_record(record) + arguments = [record.class.name, attributes_for_database(record)] + arguments << true if record.new_record? + arguments + end + + if Rails.gem_version >= Gem::Version.new('7.0') + def attributes_for_database(record) + attributes = record.attributes_for_database + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + else + def attributes_for_database(record) + attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database) + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + end + + def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter + begin + klass = Object.const_get(class_name) + rescue NameError + raise ClassMissingError, "undefined class: #{class_name}" + end + + # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass + # wether the record was persisted or not. + attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) + klass.allocate.init_with_attributes(attributes, new_record) + end + end + + class Error < StandardError + end + + class ClassMissingError < Error + end + + class AssociationMissingError < Error + end + + class InstanceTracker + def initialize + @instances = [] + @ids = {}.compare_by_identity + end + + def map(&block) + @instances.map(&block) + end + + def fetch(...) + @instances.fetch(...) + end + + def push(instance) + id = @ids[instance] = @instances.size + @instances << instance + id + end + + def lookup(instance) + @ids[instance] + end + end + end + class_methods do def vary_by(value, **kwargs) before_action(**kwargs) do |controller| @@ -38,7 +190,7 @@ module CacheConcern return render(options) end - key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields]&.join(',')].compact.join(':') + key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes body = Rails.cache.read(key, raw: true) @@ -55,4 +207,20 @@ module CacheConcern Rails.cache.write(key, response.body, expires_in: expires_in, raw: true) end end + + # TODO: Rename this method, as it does not perform any caching anymore. + def cache_collection(raw, klass) + return raw unless klass.respond_to?(:preload_cacheable_associations) + + records = raw.to_a + + klass.preload_cacheable_associations(records) + + records + end + + # TODO: Rename this method, as it does not perform any caching anymore. + def cache_collection_paginated_by_id(raw, klass, limit, options) + cache_collection raw.to_a_paginated_by_id(limit, options), klass + end end diff --git a/app/controllers/concerns/auth/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb similarity index 90% rename from app/controllers/concerns/auth/captcha_concern.rb rename to app/controllers/concerns/captcha_concern.rb index c01da21249..170c8f5e03 100644 --- a/app/controllers/concerns/auth/captcha_concern.rb +++ b/app/controllers/concerns/captcha_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Auth::CaptchaConcern +module CaptchaConcern extend ActiveSupport::Concern include Hcaptcha::Adapters::ViewMethods @@ -10,7 +10,7 @@ module Auth::CaptchaConcern end def captcha_available? - Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present? + ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? end def captcha_enabled? diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb index 7fbc469bdf..2995a25e09 100644 --- a/app/controllers/concerns/challengable_concern.rb +++ b/app/controllers/concerns/challengable_concern.rb @@ -42,7 +42,8 @@ module ChallengableConcern end def render_challenge - render 'auth/challenges/new', layout: 'auth' + @body_classes = 'lighter' + render template: 'auth/challenges/new', layout: 'auth' end def challenge_passed? @@ -58,6 +59,6 @@ module ChallengableConcern end def challenge_params - params.expect(form_challenge: [:current_password, :return_to]) + params.require(:form_challenge).permit(:current_password, :return_to) end end diff --git a/app/controllers/concerns/deprecation_concern.rb b/app/controllers/concerns/deprecation_concern.rb deleted file mode 100644 index ad8de724a1..0000000000 --- a/app/controllers/concerns/deprecation_concern.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module DeprecationConcern - extend ActiveSupport::Concern - - class_methods do - def deprecate_api(date, sunset: nil, **kwargs) - deprecation_timestamp = "@#{date.to_datetime.to_i}" - sunset = sunset&.to_date&.httpdate - - before_action(**kwargs) do - response.headers['Deprecation'] = deprecation_timestamp - response.headers['Sunset'] = sunset if sunset - end - end - end -end diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb new file mode 100644 index 0000000000..24cfc7a012 --- /dev/null +++ b/app/controllers/concerns/export_controller_concern.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module ExportControllerConcern + extend ActiveSupport::Concern + + included do + before_action :authenticate_user! + before_action :load_export + + skip_before_action :require_functional! + end + + private + + def load_export + @export = Export.new(current_account) + end + + def send_export_file + respond_to do |format| + format.csv { send_data export_data, filename: export_filename } + end + end + + def export_data + raise 'Override in controller' + end + + def export_filename + "#{controller_name}.csv" + end +end diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index 14742e3b5c..ede299d5a4 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -25,7 +25,7 @@ module Localized end def available_locale_or_nil(locale_name) - locale_name.to_sym if locale_name.respond_to?(:to_sym) && I18n.available_locales.include?(locale_name.to_sym) + locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) end def content_locale diff --git a/app/controllers/concerns/preloading_concern.rb b/app/controllers/concerns/preloading_concern.rb deleted file mode 100644 index 61e2213649..0000000000 --- a/app/controllers/concerns/preloading_concern.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module PreloadingConcern - extend ActiveSupport::Concern - - def preload_collection(scope, klass) - return scope unless klass.respond_to?(:preload_cacheable_associations) - - scope.to_a.tap do |records| - klass.preload_cacheable_associations(records) - end - end - - def preload_collection_paginated_by_id(scope, klass, limit, options) - preload_collection scope.to_a_paginated_by_id(limit, options), klass - end -end diff --git a/app/controllers/concerns/api/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb similarity index 98% rename from app/controllers/concerns/api/rate_limit_headers.rb rename to app/controllers/concerns/rate_limit_headers.rb index fe57b6f6bd..5b83d8575b 100644 --- a/app/controllers/concerns/api/rate_limit_headers.rb +++ b/app/controllers/concerns/rate_limit_headers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Api::RateLimitHeaders +module RateLimitHeaders extend ActiveSupport::Concern class_methods do diff --git a/app/controllers/concerns/auth/registration_spam_concern.rb b/app/controllers/concerns/registration_spam_concern.rb similarity index 81% rename from app/controllers/concerns/auth/registration_spam_concern.rb rename to app/controllers/concerns/registration_spam_concern.rb index 9f4798b537..af434c985a 100644 --- a/app/controllers/concerns/auth/registration_spam_concern.rb +++ b/app/controllers/concerns/registration_spam_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Auth::RegistrationSpamConcern +module RegistrationSpamConcern extend ActiveSupport::Concern def set_registration_form_time diff --git a/app/controllers/concerns/settings/export_controller_concern.rb b/app/controllers/concerns/settings/export_controller_concern.rb deleted file mode 100644 index 2cf28cced8..0000000000 --- a/app/controllers/concerns/settings/export_controller_concern.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Settings::ExportControllerConcern - extend ActiveSupport::Concern - - included do - before_action :authenticate_user! - before_action :load_export - - skip_before_action :check_self_destruct! - skip_before_action :require_functional! - end - - private - - def load_export - @export = Export.new(current_account) - end - - def send_export_file - respond_to do |format| - format.csv { send_data export_data, filename: export_filename } - end - end - - def export_data - raise 'Override in controller' - end - - def export_filename - "#{controller_name}.csv" - end -end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index ffe612f468..92f1eb5a16 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -10,6 +10,41 @@ module SignatureVerification EXPIRATION_WINDOW_LIMIT = 12.hours CLOCK_SKEW_MARGIN = 1.hour + 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 @@ -32,7 +67,7 @@ module SignatureVerification def signature_key_id signature_params['keyId'] - rescue Mastodon::SignatureVerificationError + rescue SignatureVerificationError nil end @@ -43,17 +78,17 @@ module SignatureVerification def signed_request_actor return @signed_request_actor if defined?(@signed_request_actor) - raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request? - raise Mastodon::SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? - raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm) - raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? + raise SignatureVerificationError, 'Request not signed' unless signed_request? + raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? + raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm) + raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? verify_signature_strength! verify_body_digest! actor = actor_from_key_id(signature_params['keyId']) - raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? signature = Base64.decode64(signature_params['signature']) compare_signed_string = build_signed_string(include_query_string: true) @@ -64,9 +99,9 @@ module SignatureVerification compare_signed_string = build_signed_string(include_query_string: false) return actor unless verify_signature(actor, signature, compare_signed_string).nil? - actor = stoplight_wrapper.run { actor_refresh_key!(actor) } + actor = stoplight_wrap_request { actor_refresh_key!(actor) } - raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? + raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? compare_signed_string = build_signed_string(include_query_string: true) return actor unless verify_signature(actor, signature, compare_signed_string).nil? @@ -76,9 +111,9 @@ module SignatureVerification return actor unless verify_signature(actor, signature, compare_signed_string).nil? fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] - rescue Mastodon::SignatureVerificationError => e + rescue SignatureVerificationError => e fail_with! e.message - rescue *Mastodon::HTTP_CONNECTION_ERRORS => e + rescue HTTP::Error, OpenSSL::SSL::SSLError => e fail_with! "Failed to fetch remote data: #{e.message}" rescue Mastodon::UnexpectedResponseError fail_with! 'Failed to fetch remote data (got unexpected reply from server)' @@ -100,9 +135,13 @@ module SignatureVerification end def signature_params - @signature_params ||= SignatureParser.parse(request.headers['Signature']) - rescue SignatureParser::ParsingError - raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters' + @signature_params ||= begin + raw_signature = request.headers['Signature'] + tree = SignatureParamsParser.new.parse(raw_signature) + SignatureParamsTransformer.new.apply(tree) + end + rescue Parslet::ParseFailed + raise SignatureVerificationError, 'Error parsing signature parameters' end def signature_algorithm @@ -114,31 +153,31 @@ module SignatureVerification end def verify_signature_strength! - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest') - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') + raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') + raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest') + raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') + raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') end def verify_body_digest! return unless signed_headers.include?('digest') - raise Mastodon::SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') + raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } sha256 = digests.assoc('sha-256') - raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? + raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? return if body_digest == sha256[1] digest_size = begin Base64.strict_decode64(sha256[1].strip).length rescue ArgumentError - raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" end - raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 - raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end def verify_signature(actor, signature, compare_signed_string) @@ -153,23 +192,23 @@ module SignatureVerification def build_signed_string(include_query_string: true) signed_headers.map do |signed_header| case signed_header - when HttpSignatureDraft::REQUEST_TARGET + when Request::REQUEST_TARGET if include_query_string - "#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" else # Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header. # Therefore, temporarily support such incorrect signatures for compatibility. # TODO: remove eventually some time after release of the fixed version - "#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" end when '(created)' - raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' - raise Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? + raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' + raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? "(created): #{signature_params['created']}" when '(expires)' - raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' - raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? + raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' + raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? "(expires): #{signature_params['expires']}" else @@ -191,7 +230,7 @@ module SignatureVerification expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present? rescue ArgumentError => e - raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}" + raise SignatureVerificationError, "Invalid Date header: #{e.message}" end expires_time ||= created_time + 5.minutes unless created_time.nil? @@ -224,23 +263,24 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) - account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } + account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } account end rescue Mastodon::PrivateNetworkAddressError => e - raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e - raise Mastodon::SignatureVerificationError, e.message + raise SignatureVerificationError, e.message end - def stoplight_wrapper - Stoplight("source:#{request.remote_ip}") + def stoplight_wrap_request(&block) + Stoplight("source:#{request.remote_ip}", &block) .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } + .run end def actor_refresh_key!(actor) @@ -249,8 +289,8 @@ module SignatureVerification ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false) rescue Mastodon::PrivateNetworkAddressError => e - raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e - raise Mastodon::SignatureVerificationError, e.message + raise SignatureVerificationError, e.message end end diff --git a/app/controllers/concerns/auth/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb similarity index 95% rename from app/controllers/concerns/auth/two_factor_authentication_concern.rb rename to app/controllers/concerns/two_factor_authentication_concern.rb index 0fb11428dc..90fa392a13 100644 --- a/app/controllers/concerns/auth/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -module Auth::TwoFactorAuthenticationConcern +module TwoFactorAuthenticationConcern extend ActiveSupport::Concern included do prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] - helper_method :webauthn_enabled? end def two_factor_enabled? @@ -83,6 +82,7 @@ module Auth::TwoFactorAuthenticationConcern def prompt_for_two_factor(user) register_attempt_in_session(user) + @body_classes = 'lighter' @webauthn_enabled = user.webauthn_enabled? @scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank? 'webauthn' @@ -92,10 +92,4 @@ module Auth::TwoFactorAuthenticationConcern set_locale { render :two_factor } end - - protected - - def webauthn_enabled? - @webauthn_enabled - end end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index ec2256aa9c..5687d6e5b6 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,45 +7,24 @@ module WebAppControllerConcern vary_by 'Accept, Accept-Language, Cookie' before_action :redirect_unauthenticated_to_permalinks! - before_action :set_referer_header - - content_security_policy do |p| - policy = ContentSecurityPolicy.new - - if policy.sso_host.present? - p.form_action policy.sso_host, -> { "https://#{request.host}/auth/auth/" } - else - p.form_action :none - end - end + before_action :set_app_body_class end def skip_csrf_meta_tags? !(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil? end + def set_app_body_class + @body_classes = 'app-body' + end + def redirect_unauthenticated_to_permalinks! return if user_signed_in? && current_account.moved_to_account_id.nil? - permalink_redirector = PermalinkRedirector.new(request.original_fullpath) - return if permalink_redirector.redirect_path.blank? + redirect_path = PermalinkRedirector.new(request.path).redirect_path + return if redirect_path.blank? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - - respond_to do |format| - format.html do - redirect_to(permalink_redirector.redirect_confirmation_path, allow_other_host: false) - end - - format.json do - redirect_to(permalink_redirector.redirect_uri, allow_other_host: true) - end - end - end - - protected - - def set_referer_header - response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin') + redirect_to(redirect_path) end end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 5b98914114..e7a02ea89c 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -2,14 +2,7 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController def show - expires_in 1.month, public: true + expires_in 3.minutes, public: true render content_type: 'text/css' end - - private - - def custom_css_styles - Setting.custom_css - end - helper_method :custom_css_styles end diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb index 797f31cf78..eefd92b5a8 100644 --- a/app/controllers/disputes/appeals_controller.rb +++ b/app/controllers/disputes/appeals_controller.rb @@ -11,7 +11,7 @@ class Disputes::AppealsController < Disputes::BaseController redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') rescue ActiveRecord::RecordInvalid => e @appeal = e.record - render 'disputes/strikes/show' + render template: 'disputes/strikes/show' end private @@ -21,6 +21,6 @@ class Disputes::AppealsController < Disputes::BaseController end def appeal_params - params.expect(appeal: [:text]) + params.require(:appeal).permit(:text) end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 07677fd3f3..1054f3db80 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -7,5 +7,17 @@ class Disputes::BaseController < ApplicationController skip_before_action :require_functional! + before_action :set_body_classes before_action :authenticate_user! + before_action :set_cache_headers + + private + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index d85b017aaa..94993f938b 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -6,6 +6,8 @@ class Filters::StatusesController < ApplicationController before_action :authenticate_user! before_action :set_filter before_action :set_status_filters + before_action :set_body_classes + before_action :set_cache_headers PER_PAGE = 20 @@ -33,10 +35,18 @@ class Filters::StatusesController < ApplicationController end def status_filter_batch_action_params - params.expect(form_status_filter_batch_action: [status_filter_ids: []]) + params.require(:form_status_filter_batch_action).permit(status_filter_ids: []) end def action_from_button 'remove' if params[:remove] end + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 20b8135908..b0b2168884 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -5,6 +5,8 @@ class FiltersController < ApplicationController before_action :authenticate_user! before_action :set_filter, only: [:edit, :update, :destroy] + before_action :set_body_classes + before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -23,7 +25,7 @@ class FiltersController < ApplicationController if @filter.save redirect_to filters_path else - render :new + render action: :new end end @@ -31,7 +33,7 @@ class FiltersController < ApplicationController if @filter.update(resource_params) redirect_to filters_path else - render :edit + render action: :edit end end @@ -47,6 +49,14 @@ class FiltersController < ApplicationController end def resource_params - params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [[:id, :keyword, :whole_word, :_destroy]]]) + params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + end + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 85f6ccc5e4..44d90ec671 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -46,7 +46,7 @@ class FollowerAccountsController < ApplicationController end def page_url(page) - ActivityPub::TagManager.instance.followers_uri_for(@account, page: page) unless page.nil? + account_followers_url(@account, page: page) unless page.nil? end def next_page_url diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 7bc424d0a4..2a22a05570 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class HealthController < ActionController::Base # rubocop:disable Rails/ApplicationController +class HealthController < ActionController::Base def show render plain: 'OK' end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 03aa3eb52a..ee940e6707 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,7 +3,15 @@ class HomeController < ApplicationController include WebAppControllerConcern + before_action :set_instance_presenter + def index expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index f2b1eaa3e7..8422d74bc3 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -6,8 +6,6 @@ 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 @@ -18,11 +16,6 @@ class InstanceActorsController < ActivityPub::BaseController private - # Skips various `before_action` from `AccountOwnedConcern` - def account_required? - false - end - def set_account @account = Account.representative end diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index 65c315208d..ea024e30e6 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -1,26 +1,27 @@ # frozen_string_literal: true class IntentsController < ApplicationController - EXPECTED_SCHEME = 'web+mastodon' + before_action :check_uri - before_action :handle_invalid_uri, unless: :valid_uri? rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri def show - 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 + 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 end + + not_found end private - def valid_uri? - uri.present? && uri.scheme == EXPECTED_SCHEME + def check_uri + not_found if uri.blank? end def handle_invalid_uri diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index fc65333ac4..9bc5164d59 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,6 +6,8 @@ class InvitesController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes + before_action :set_cache_headers def index authorize :invite, :create? @@ -42,6 +44,14 @@ class InvitesController < ApplicationController end def resource_params - params.expect(invite: [:max_uses, :expires_in, :autofollow, :comment]) + params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) + end + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb index 34df75f63a..1caeaaacf4 100644 --- a/app/controllers/mail_subscriptions_controller.rb +++ b/app/controllers/mail_subscriptions_controller.rb @@ -5,6 +5,7 @@ class MailSubscriptionsController < ApplicationController skip_before_action :require_functional! + before_action :set_body_classes before_action :set_user before_action :set_type @@ -24,6 +25,10 @@ class MailSubscriptionsController < ApplicationController not_found unless @user end + def set_body_classes + @body_classes = 'lighter' + end + def set_type @type = email_type_from_param end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 9d10468e69..53eee40012 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -19,7 +19,9 @@ class MediaController < ApplicationController redirect_to @media_attachment.file.url(:original) end - def player; end + def player + @body_classes = 'player' + end private diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index f68d85e44e..c4230d62c3 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -13,7 +13,7 @@ class MediaProxyController < ApplicationController rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found rescue_from Mastodon::NotPermittedError, with: :not_found - rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) + rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show with_redis_lock("media_download:#{params[:id]}") do diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index deafedeaef..66e774425d 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -5,6 +5,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController before_action :store_current_location before_action :authenticate_resource_owner! + before_action :set_cache_headers content_security_policy do |p| p.form_action(false) @@ -31,4 +32,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController def truthy_param?(key) ActiveModel::Type::Boolean.new.cast(params[key]) end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 8b11a519ea..17f1be23de 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,6 +6,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :require_not_suspended!, only: :destroy + before_action :set_body_classes + before_action :set_cache_headers before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } @@ -21,15 +23,29 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio private + def set_body_classes + @body_classes = 'admin' + end + def store_current_location store_location_for(:user, request.url) end def require_not_suspended! - forbidden if current_account.unavailable? + forbidden if current_account.suspended? + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end def set_last_used_at_by_app - @last_used_at_by_app = current_resource_owner.applications_last_used + @last_used_at_by_app = Doorkeeper::AccessToken + .select('DISTINCT ON (application_id) application_id, last_used_at') + .where(resource_owner_id: current_resource_owner.id) + .where.not(last_used_at: nil) + .order(application_id: :desc, last_used_at: :desc) + .pluck(:application_id, :last_used_at) + .to_h end end diff --git a/app/controllers/oauth/userinfo_controller.rb b/app/controllers/oauth/userinfo_controller.rb deleted file mode 100644 index e268b70dcc..0000000000 --- a/app/controllers/oauth/userinfo_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class Oauth::UserinfoController < Api::BaseController - before_action -> { doorkeeper_authorize! :profile }, only: [:show] - before_action :require_user! - - def show - @account = current_account - render json: @account, serializer: OauthUserinfoSerializer - end -end diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb index 860e7c77a0..070ee8a06a 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -5,7 +5,15 @@ class PrivacyController < ApplicationController skip_before_action :require_functional! + before_action :set_instance_presenter + def show expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end end diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb deleted file mode 100644 index 713ccf2ca1..0000000000 --- a/app/controllers/redirect/accounts_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class Redirect::AccountsController < Redirect::BaseController - private - - def set_resource - @resource = Account.find(params[:id]) - not_found if @resource.local? - end -end diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb deleted file mode 100644 index 34558a4126..0000000000 --- a/app/controllers/redirect/base_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class Redirect::BaseController < ApplicationController - vary_by 'Accept-Language' - - before_action :set_resource - - def show - @redirect_path = ActivityPub::TagManager.instance.url_for(@resource) - - render 'redirects/show', layout: 'application' - end - - private - - def set_resource - raise NotImplementedError - end -end diff --git a/app/controllers/redirect/statuses_controller.rb b/app/controllers/redirect/statuses_controller.rb deleted file mode 100644 index 37a938c651..0000000000 --- a/app/controllers/redirect/statuses_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class Redirect::StatusesController < Redirect::BaseController - private - - def set_resource - @resource = Status.find(params[:id]) - not_found if @resource.local? || !@resource.distributable? - end -end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 7e793fc734..dd794f3199 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -6,6 +6,8 @@ class RelationshipsController < ApplicationController before_action :authenticate_user! before_action :set_accounts, only: :show before_action :set_relationships, only: :show + before_action :set_body_classes + before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -35,7 +37,7 @@ class RelationshipsController < ApplicationController end def form_account_batch_params - params.expect(form_account_batch: [:action, account_ids: []]) + params.require(:form_account_batch).permit(:action, account_ids: []) end def following_relationship? @@ -65,4 +67,12 @@ class RelationshipsController < ApplicationController 'remove_domains_from_followers' end end + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index c21d43eeb3..a421b8ede3 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -30,7 +30,7 @@ class Settings::AliasesController < Settings::BaseController private def resource_params - params.expect(account_alias: [:acct]) + params.require(:account_alias).permit(:acct) end def set_alias diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 8e39741f89..d4b7205681 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -2,6 +2,7 @@ class Settings::ApplicationsController < Settings::BaseController before_action :set_application, only: [:show, :update, :destroy, :regenerate] + before_action :prepare_scopes, only: [:create, :update] def index @applications = current_user.applications.order(id: :desc).page(params[:page]) @@ -12,7 +13,7 @@ class Settings::ApplicationsController < Settings::BaseController def new @application = Doorkeeper::Application.new( redirect_uri: Doorkeeper.configuration.native_redirect_uri, - scopes: 'profile' + scopes: 'read write follow' ) end @@ -59,6 +60,16 @@ class Settings::ApplicationsController < Settings::BaseController end def application_params - params.expect(doorkeeper_application: [:name, :redirect_uri, :website, scopes: []]) + params.require(:doorkeeper_application).permit( + :name, + :redirect_uri, + :scopes, + :website + ) + end + + def prepare_scopes + scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) + params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array end end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 7f2279aa8f..64dcd47d12 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -4,10 +4,20 @@ class Settings::BaseController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes + before_action :set_cache_headers private + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + def require_not_suspended! - forbidden if current_account.unavailable? + forbidden if current_account.suspended? end end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 815d95ad83..bb096567a9 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -21,11 +21,11 @@ class Settings::DeletesController < Settings::BaseController private def resource_params - params.expect(form_delete_confirmation: [:password, :username]) + params.require(:form_delete_confirmation).permit(:password, :username) end def require_not_suspended! - forbidden if current_account.unavailable? + forbidden if current_account.suspended? end def challenge_passed? diff --git a/app/controllers/settings/exports/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb index 906564a3dc..2190caa361 100644 --- a/app/controllers/settings/exports/blocked_accounts_controller.rb +++ b/app/controllers/settings/exports/blocked_accounts_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class BlockedAccountsController < BaseController - include Settings::ExportControllerConcern + include ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/blocked_domains_controller.rb b/app/controllers/settings/exports/blocked_domains_controller.rb index 09dc52392f..bee4b2431e 100644 --- a/app/controllers/settings/exports/blocked_domains_controller.rb +++ b/app/controllers/settings/exports/blocked_domains_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class BlockedDomainsController < BaseController - include Settings::ExportControllerConcern + include ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/bookmarks_controller.rb b/app/controllers/settings/exports/bookmarks_controller.rb index 0321565b97..c12e2f147a 100644 --- a/app/controllers/settings/exports/bookmarks_controller.rb +++ b/app/controllers/settings/exports/bookmarks_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class BookmarksController < BaseController - include Settings::ExportControllerConcern + include ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb index 0ac9031fb9..acefcb15da 100644 --- a/app/controllers/settings/exports/following_accounts_controller.rb +++ b/app/controllers/settings/exports/following_accounts_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class FollowingAccountsController < BaseController - include Settings::ExportControllerConcern + include ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/lists_controller.rb b/app/controllers/settings/exports/lists_controller.rb index d90c71e248..bc65f56a0e 100644 --- a/app/controllers/settings/exports/lists_controller.rb +++ b/app/controllers/settings/exports/lists_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class ListsController < BaseController - include Settings::ExportControllerConcern + include ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/muted_accounts_controller.rb b/app/controllers/settings/exports/muted_accounts_controller.rb index e4b1158902..50b7bf1f79 100644 --- a/app/controllers/settings/exports/muted_accounts_controller.rb +++ b/app/controllers/settings/exports/muted_accounts_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class MutedAccountsController < BaseController - include Settings::ExportControllerConcern + include ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 263d20eaea..46a340aeb3 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -5,11 +5,10 @@ class Settings::ExportsController < Settings::BaseController include Redisable include Lockable - skip_before_action :check_self_destruct! skip_before_action :require_functional! def show - @export_summary = ExportSummary.new(preloaded_account) + @export = Export.new(current_account) @backups = current_user.backups end @@ -25,15 +24,4 @@ class Settings::ExportsController < Settings::BaseController redirect_to settings_export_path end - - private - - def preloaded_account - current_account.tap do |account| - ActiveRecord::Associations::Preloader.new( - records: [account], - associations: :account_stat - ).call - end - end end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index 0f352e1913..c384402650 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -5,8 +5,6 @@ class Settings::FeaturedTagsController < Settings::BaseController before_action :set_featured_tag, except: [:index, :create] before_action :set_recently_used_tags, only: :index - RECENT_TAGS_LIMIT = 10 - def index @featured_tag = FeaturedTag.new end @@ -40,10 +38,10 @@ class Settings::FeaturedTagsController < Settings::BaseController end def set_recently_used_tags - @recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT) + @recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) end def featured_tag_params - params.expect(featured_tag: [:name]) + params.require(:featured_tag).permit(:name) end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index be1699315f..983caf22fa 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -24,8 +24,6 @@ class Settings::ImportsController < Settings::BaseController lists: false, }.freeze - RECENT_IMPORTS_LIMIT = 10 - def index @import = Form::Import.new(current_account: current_account) end @@ -33,7 +31,7 @@ class Settings::ImportsController < Settings::BaseController def show; end def failures - @bulk_import = current_account.bulk_imports.state_finished.find(params[:id]) + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) respond_to do |format| format.csv do @@ -90,14 +88,14 @@ class Settings::ImportsController < Settings::BaseController private def import_params - params.expect(form_import: [:data, :type, :mode]) + params.require(:form_import).permit(:data, :type, :mode) end def set_bulk_import - @bulk_import = current_account.bulk_imports.state_unconfirmed.find(params[:id]) + @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) end def set_recent_imports - @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(RECENT_IMPORTS_LIMIT) + @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10) end end diff --git a/app/controllers/settings/login_activities_controller.rb b/app/controllers/settings/login_activities_controller.rb index 50e2d70cb9..57fa6aef0c 100644 --- a/app/controllers/settings/login_activities_controller.rb +++ b/app/controllers/settings/login_activities_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::LoginActivitiesController < Settings::BaseController - skip_before_action :check_self_destruct! - skip_before_action :require_functional! - def index @login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page]) end diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index d850e05e94..6d469f3842 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -33,6 +33,6 @@ class Settings::Migration::RedirectsController < Settings::BaseController private def resource_params - params.expect(form_redirect: [:acct, :current_password, :current_username]) + params.require(:form_redirect).permit(:acct, :current_password, :current_username) end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 92e3611fd9..62603aba81 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -27,7 +27,7 @@ class Settings::MigrationsController < Settings::BaseController private def resource_params - params.expect(account_migration: [:acct, :current_password, :current_username]) + params.require(:account_migration).permit(:acct, :current_password, :current_username) end def set_migrations diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index 7e61e6d580..58a4325307 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -8,7 +8,7 @@ module Settings def destroy if valid_picture? if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303 else redirect_to settings_profile_path diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index c2e705da3c..c1f8b49898 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -19,16 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController end def user_params - original_user_params.tap do |params| - params[:settings_attributes]&.merge!(disabled_visibilities_params[:settings_attributes] || {}) - end - end - - def original_user_params - params.expect(user: [:locale, :time_zone, :custom_css_text, chosen_languages: [], settings_attributes: UserSettings.keys]) - end - - def disabled_visibilities_params - params.expect(user: [settings_attributes: { enabled_visibilities: [] }]) + params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) end end diff --git a/app/controllers/settings/preferences/custom_css_controller.rb b/app/controllers/settings/preferences/custom_css_controller.rb deleted file mode 100644 index 6a7369ec49..0000000000 --- a/app/controllers/settings/preferences/custom_css_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class Settings::Preferences::CustomCssController < Settings::Preferences::BaseController - private - - def after_update_redirect_path - settings_preferences_custom_css_path - end -end diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb index 02925fa6e5..77b4fe10f8 100644 --- a/app/controllers/settings/preferences/other_controller.rb +++ b/app/controllers/settings/preferences/other_controller.rb @@ -4,8 +4,8 @@ class Settings::Preferences::OtherController < Settings::Preferences::BaseContro include DtlHelper def show - @dtl_enabled = dtl_enabled? - @dtl_tag = dtl_tag_name + @dtl_enabled = DTL_ENABLED + @dtl_tag = DTL_TAG end private diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index 96efa03ccf..1102c89fad 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params.except(:settings)) current_user.update!(settings_attributes: account_params[:settings]) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg') else render :show @@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController private def account_params - params.expect(account: [:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys]) + params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys) end def set_account diff --git a/app/controllers/settings/privacy_extra_controller.rb b/app/controllers/settings/privacy_extra_controller.rb index f1292e644c..85364ec35d 100644 --- a/app/controllers/settings/privacy_extra_controller.rb +++ b/app/controllers/settings/privacy_extra_controller.rb @@ -18,7 +18,7 @@ class Settings::PrivacyExtraController < Settings::BaseController private def account_params - params.expect(account: [settings: UserSettings.keys]) + params.require(:account).permit(:dissubscribable, settings: UserSettings.keys) end def set_account diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 04a10fbfb9..c33567956c 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else @account.build_fields @@ -20,7 +20,8 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.expect(account: [:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [[:name, :value]]]) + # params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :locked, :my_actor_type, :searchability, :dissubscribable, :group_allow_private_message, :discoverable, :discoverable_local, :hide_collections, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, :group_allow_private_message, :dissubscribable, fields_attributes: [:name, :value]) end def set_account diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index eae990e79b..1a0afe58b0 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -38,7 +38,7 @@ module Settings private def confirmation_params - params.expect(form_two_factor_confirmation: [:otp_attempt]) + params.require(:form_two_factor_confirmation).permit(:otp_attempt) end def prepare_two_factor_form diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index ca8d46afe4..0bff01ec27 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -15,7 +15,7 @@ module Settings end def create - session[:new_otp_secret] = User.generate_otp_secret + session[:new_otp_secret] = User.generate_otp_secret(32) redirect_to new_settings_two_factor_authentication_confirmation_path end diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index 9714d54f95..4007f13470 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -3,11 +3,10 @@ module Settings module TwoFactorAuthentication class WebauthnCredentialsController < BaseController - skip_before_action :check_self_destruct! skip_before_action :require_functional! - before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? } - before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? } + before_action :require_otp_enabled + before_action :require_webauthn_enabled, only: [:index, :destroy] def index; end def new; end @@ -85,14 +84,18 @@ module Settings private - def redirect_invalid_otp - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path + def require_otp_enabled + unless current_user.otp_enabled? + flash[:error] = t('webauthn_credentials.otp_required') + redirect_to settings_two_factor_authentication_methods_path + end end - def redirect_invalid_webauthn - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path + def require_webauthn_enabled + unless current_user.webauthn_enabled? + flash[:error] = t('webauthn_credentials.not_enabled') + redirect_to settings_two_factor_authentication_methods_path + end end end end diff --git a/app/controllers/settings/two_factor_authentication_methods_controller.rb b/app/controllers/settings/two_factor_authentication_methods_controller.rb index a6d5c1fe2d..205933ea81 100644 --- a/app/controllers/settings/two_factor_authentication_methods_controller.rb +++ b/app/controllers/settings/two_factor_authentication_methods_controller.rb @@ -4,7 +4,6 @@ module Settings class TwoFactorAuthenticationMethodsController < BaseController include ChallengableConcern - skip_before_action :check_self_destruct! skip_before_action :require_functional! before_action :require_challenge!, only: :disable diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb index 4b949ca72d..fc4f23bb18 100644 --- a/app/controllers/settings/verifications_controller.rb +++ b/app/controllers/settings/verifications_controller.rb @@ -2,32 +2,14 @@ class Settings::VerificationsController < Settings::BaseController before_action :set_account - before_action :set_verified_links - def show; end - - def update - if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) - redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg') - else - render :show - end + def show + @verified_links = @account.fields.select(&:verified?) end private - def account_params - params.expect(account: [:attribution_domains]).tap do |params| - params[:attribution_domains] = params[:attribution_domains].split if params[:attribution_domains] - end - end - def set_account @account = current_account end - - def set_verified_links - @verified_links = @account.fields.select(&:verified?) - end end diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb deleted file mode 100644 index 817abebf62..0000000000 --- a/app/controllers/severed_relationships_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class SeveredRelationshipsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - - 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 -end diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 1aa0ce5a0d..6546b84978 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -4,6 +4,13 @@ class SharesController < ApplicationController layout 'modal' before_action :authenticate_user! + before_action :set_body_classes def show; end + + private + + def set_body_classes + @body_classes = 'modal-layout compose-standalone' + end end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index a25e544392..a738fee8d5 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -5,6 +5,8 @@ class StatusesCleanupController < ApplicationController before_action :authenticate_user! before_action :set_policy + before_action :set_body_classes + before_action :set_cache_headers def show; end @@ -12,8 +14,10 @@ class StatusesCleanupController < ApplicationController if @policy.update(resource_params) redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg') else - render :show + render action: :show end + rescue ActionController::ParameterMissing + # Do nothing end def require_functional! @@ -27,6 +31,14 @@ class StatusesCleanupController < ApplicationController end def resource_params - params.expect(account_statuses_cleanup_policy: [:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis]) + params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis) + end + + def set_body_classes + @body_classes = 'admin' + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 4779f79c5a..9ae15a6ed0 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -10,7 +10,9 @@ class StatusesController < ApplicationController before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status + before_action :set_instance_presenter before_action :redirect_to_original, only: :show + before_action :set_body_classes, only: :embed after_action :set_link_headers @@ -28,7 +30,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? && !misskey_software? && !@status.expires? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? && !misskey_software? render_with_cache json: @status, content_type: 'application/activity+json', serializer: status_activity_serializer, adapter: ActivityPub::Adapter, cancel_cache: misskey_software? end end @@ -50,10 +52,12 @@ class StatusesController < ApplicationController private + def set_body_classes + @body_classes = 'with-modals' + end + def set_link_headers - response.headers['Link'] = LinkHeader.new( - [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]] - ).to_s + response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]) end def set_status @@ -61,8 +65,6 @@ class StatusesController < ApplicationController if request.authorization.present? && request.authorization.match(/^Bearer /i) raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, '')) - elsif request.format == :json && @status.expires? - raise Mastodon::NotPermittedError unless StatusPolicy.new(signed_request_account, @status).show_activity? else authorize @status, :show? end @@ -70,15 +72,23 @@ class StatusesController < ApplicationController not_found end + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + def misskey_software? return @misskey_software if defined?(@misskey_software) @misskey_software = false - return false if !@status.local? || signed_request_account&.domain.blank? || !@status.sending_maybe_compromised_privacy? - return @misskey_software = true if DomainBlock.detect_invalid_subscription?(signed_request_account.domain) + return false if !@status.local? || signed_request_account&.domain.blank? - @misskey_software = InstanceInfo.invalid_subscription_software?(signed_request_account.domain) + info = InstanceInfo.find_by(domain: signed_request_account.domain) + return false if info.nil? + + @misskey_software = %w(misskey calckey cherrypick sharkey).include?(info.software) && + ((@status.public_unlisted_visibility? && @status.account.user&.setting_reject_public_unlisted_subscription) || + (@status.unlisted_visibility? && @status.account.user&.setting_reject_unlisted_subscription)) end def status_activity_serializer diff --git a/app/controllers/system_css_controller.rb b/app/controllers/system_css_controller.rb deleted file mode 100644 index dd90491894..0000000000 --- a/app/controllers/system_css_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class SystemCssController < ActionController::Base # rubocop:disable Rails/ApplicationController - def show - expires_in 3.minutes, public: true - render content_type: 'text/css' - end -end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index d6c0d872c8..2007fe8462 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -14,6 +14,7 @@ class TagsController < ApplicationController before_action :set_local before_action :set_tag before_action :set_statuses, if: -> { request.format == :rss } + before_action :set_instance_presenter skip_before_action :require_functional!, unless: :limited_federation_mode? @@ -45,7 +46,11 @@ class TagsController < ApplicationController end def set_statuses - @statuses = preload_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) + @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) + end + + def set_instance_presenter + @instance_presenter = InstancePresenter.new end def limit_param diff --git a/app/controllers/terms_of_service_controller.rb b/app/controllers/terms_of_service_controller.rb deleted file mode 100644 index 672fb07915..0000000000 --- a/app/controllers/terms_of_service_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class TermsOfServiceController < ApplicationController - include WebAppControllerConcern - - skip_before_action :require_functional! - - def show - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - end -end diff --git a/app/controllers/user_custom_css_controller.rb b/app/controllers/user_custom_css_controller.rb deleted file mode 100644 index 2535e07c03..0000000000 --- a/app/controllers/user_custom_css_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class UserCustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController - before_action :authenticate_user! - - def show - render content_type: 'text/css' - end - - private - - def user_custom_css - current_user.custom_css_text - end - helper_method :user_custom_css -end diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 6dee587baf..201da9fbc3 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -7,23 +7,7 @@ module WellKnown def show @webfinger_template = "#{webfinger_url}?resource={uri}" expires_in 3.days, public: true - - respond_to do |format| - format.any do - render content_type: 'application/xrd+xml', formats: [:xml] - end - - format.json do - render json: { - links: [ - { - rel: 'lrdd', - template: @webfinger_template, - }, - ], - } - end - end + render content_type: 'application/xrd+xml', formats: [:xml] end end end diff --git a/app/controllers/well_known/node_info_controller.rb b/app/controllers/well_known/nodeinfo_controller.rb similarity index 100% rename from app/controllers/well_known/node_info_controller.rb rename to app/controllers/well_known/nodeinfo_controller.rb diff --git a/app/controllers/well_known/oauth_metadata_controller.rb b/app/controllers/well_known/oauth_metadata_controller.rb deleted file mode 100644 index c80be2d652..0000000000 --- a/app/controllers/well_known/oauth_metadata_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module WellKnown - class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController - include CacheConcern - - # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` - # and thus re-issuing session cookies - serialization_scope nil - - def show - # Due to this document potentially changing between Mastodon versions (as - # new OAuth scopes are added), we don't use expires_in to cache upstream, - # instead just caching in the rails cache: - render_with_cache( - json: ::OauthMetadataPresenter.new, - serializer: ::OauthMetadataSerializer, - content_type: 'application/json', - expires_in: 15.minutes - ) - end - end -end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 72f0ea890f..6cf37c2ff0 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -42,7 +42,7 @@ module WellKnown end def check_account_suspension - gone if @account.permanently_unavailable? + gone if @account.suspended_permanently? end def gone diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index bdb6ca979c..ce7e5455fd 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -19,24 +19,28 @@ module AccountsHelper end end - def account_formatted_stat(value) - number_to_human(value, precision: 3, strip_insignificant_zeros: true) + def account_action_button(account) + return if account.memorial? || account.moved? + + link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do + safe_join([logo_as_symbol, t('accounts.follow')]) + end end def account_description(account) prepend_str = [ [ - account_formatted_stat(account.public_statuses_count), + number_to_human(account.public_statuses_count, precision: 3, strip_insignificant_zeros: true), I18n.t('accounts.posts', count: account.public_statuses_count), ].join(' '), [ - account_formatted_stat(account.public_following_count), + number_to_human(account.public_following_count, precision: 3, strip_insignificant_zeros: true), I18n.t('accounts.following', count: account.public_following_count), ].join(' '), [ - account_formatted_stat(account.public_followers_count), + number_to_human(account.public_followers_count, precision: 3, strip_insignificant_zeros: true), I18n.t('accounts.followers', count: account.public_followers_count), ].join(' '), ].join(', ') diff --git a/app/helpers/admin/account_actions_helper.rb b/app/helpers/admin/account_actions_helper.rb deleted file mode 100644 index e132680a68..0000000000 --- a/app/helpers/admin/account_actions_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Admin::AccountActionsHelper - def account_action_type_label(type) - safe_join( - [ - I18n.t("simple_form.labels.admin_account_action.types.#{type}"), - content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint'), - ] - ) - end -end diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index 7c931c1157..2f08538ca6 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -4,42 +4,27 @@ module Admin::AccountModerationNotesHelper def admin_account_link_to(account, path: nil) return if account.nil? - link_to( - labeled_account_avatar(account), - path || admin_account_path(account.id), - class: class_names('name-tag', suspended: suspended_account?(account)), - title: account.acct - ) + link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do + safe_join([ + image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), + content_tag(:span, account.acct, class: 'username'), + ], ' ') + end end - def admin_account_inline_link_to(account, path: nil) + def admin_account_inline_link_to(account) return if account.nil? - link_to( - account_inline_text(account), - path || admin_account_path(account.id), - class: class_names('inline-name-tag', suspended: suspended_account?(account)), - title: account.acct - ) + link_to admin_account_path(account.id), class: name_tag_classes(account, true), title: account.acct do + content_tag(:span, account.acct, class: 'username') + end end private - def labeled_account_avatar(account) - safe_join( - [ - image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), - account_inline_text(account), - ], - ' ' - ) - end - - def account_inline_text(account) - content_tag(:span, account.acct, class: 'username') - end - - def suspended_account?(account) - account.suspended? || (account.local? && account.user.nil?) + def name_tag_classes(account, inline = false) + classes = [inline ? 'inline-name-tag' : 'name-tag'] + classes << 'suspended' if account.suspended? || (account.local? && account.user.nil?) + classes.join(' ') end end diff --git a/app/helpers/admin/accounts_helper.rb b/app/helpers/admin/accounts_helper.rb deleted file mode 100644 index a2d2f75308..0000000000 --- a/app/helpers/admin/accounts_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Admin::AccountsHelper - def admin_accounts_moderation_options - [ - [t('admin.accounts.moderation.active'), 'active'], - [t('admin.accounts.moderation.silenced'), 'silenced'], - [t('admin.accounts.moderation.disabled'), 'disabled'], - [t('admin.accounts.moderation.suspended'), 'suspended'], - [t('admin.accounts.moderation.remote_pending'), 'remote_pending'], - [safe_join([t('admin.accounts.moderation.pending'), "(#{pending_user_count_label})"], ' '), 'pending'], - ] - end - - private - - def pending_user_count_label - number_with_delimiter User.pending.count - end -end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 07381cc6ad..4018ef6b1c 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -15,17 +15,15 @@ module Admin::ActionLogsHelper link_to log.human_identifier, admin_roles_path(log.target_id) when 'Report' link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) - when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain' - log.human_identifier.present? ? link_to(log.human_identifier, admin_instance_path(log.human_identifier)) : I18n.t('admin.action_logs.unavailable_instance') + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' + link_to log.human_identifier, "https://#{log.human_identifier.presence}" when 'Status' link_to log.human_identifier, log.permalink when 'AccountWarning' link_to log.human_identifier, disputes_strike_path(log.target_id) when 'Announcement' link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) - when 'CustomEmoji' - link_to log.human_identifier, edit_admin_custom_emoji_path(log.target_id) - when 'IpBlock', 'EmailDomainBlock' + when 'IpBlock', 'Instance', 'CustomEmoji' log.human_identifier when 'CanonicalEmailBlock' content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier) @@ -35,15 +33,6 @@ module Admin::ActionLogsHelper else I18n.t('admin.action_logs.deleted_account') end - when 'Relay' - link_to log.human_identifier, admin_relays_path end end - - def sorted_action_log_types - Admin::ActionLogFilter::ACTION_TYPE_MAP - .keys - .map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] } - .sort_by(&:first) - end end diff --git a/app/helpers/admin/announcements_helper.rb b/app/helpers/admin/announcements_helper.rb deleted file mode 100644 index 97abe8e011..0000000000 --- a/app/helpers/admin/announcements_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Admin::AnnouncementsHelper - def datetime_pattern - '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}' - end - - def datetime_placeholder - Time.zone.now.strftime('%FT%R') - end -end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb index f87fdad708..6096ff1381 100644 --- a/app/helpers/admin/dashboard_helper.rb +++ b/app/helpers/admin/dashboard_helper.rb @@ -18,11 +18,6 @@ module Admin::DashboardHelper end end - def date_range(range) - [l(range.first), l(range.last)] - .join(' - ') - end - def relevant_account_timestamp(account) timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago [account.user_current_sign_in_at, true] @@ -30,8 +25,6 @@ module Admin::DashboardHelper [account.user_current_sign_in_at, false] elsif account.user_pending? [account.user_created_at, true] - elsif account.suspended_at.present? && account.local? && account.user.nil? - [account.suspended_at, true] elsif account.last_status_at.present? [account.last_status_at, true] else diff --git a/app/helpers/admin/disputes_helper.rb b/app/helpers/admin/disputes_helper.rb deleted file mode 100644 index 366a470ed2..0000000000 --- a/app/helpers/admin/disputes_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Admin - module DisputesHelper - def strike_action_label(appeal) - t(key_for_action(appeal), - scope: 'admin.strikes.actions', - name: content_tag(:span, appeal.strike.account.username, class: 'username'), - target: content_tag(:span, appeal.account.username, class: 'target')) - .html_safe - end - - private - - def key_for_action(appeal) - AccountWarning.actions.slice(appeal.strike.action).keys.first - end - end -end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 40806a4515..140fc73ede 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -25,7 +25,7 @@ module Admin::FilterHelper end def table_link_to(icon, text, path, **options) - link_to safe_join([material_symbol(icon), text]), path, options.merge(class: 'table-action-link') + link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link') end def selected?(more_params) diff --git a/app/helpers/admin/ip_blocks_helper.rb b/app/helpers/admin/ip_blocks_helper.rb deleted file mode 100644 index 4aae3aae7a..0000000000 --- a/app/helpers/admin/ip_blocks_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Admin::IpBlocksHelper - def ip_blocks_severity_label(severity) - safe_join( - [ - I18n.t("simple_form.labels.ip_block.severities.#{severity}"), - content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint'), - ] - ) - end -end diff --git a/app/helpers/admin/roles_helper.rb b/app/helpers/admin/roles_helper.rb deleted file mode 100644 index 7b4702e268..0000000000 --- a/app/helpers/admin/roles_helper.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Admin - module RolesHelper - def privilege_label(privilege) - safe_join( - [ - t("admin.roles.privileges.#{privilege}"), - content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint'), - ] - ) - end - - def disable_permissions?(permissions) - permissions.filter { |privilege| role_flag_value(privilege).zero? } - end - - private - - def role_flag_value(privilege) - UserRole::FLAGS[privilege] & current_user.role.computed_permissions - end - end -end diff --git a/app/helpers/admin/settings/discovery_helper.rb b/app/helpers/admin/settings/discovery_helper.rb deleted file mode 100644 index 0aa4d4368f..0000000000 --- a/app/helpers/admin/settings/discovery_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Admin::Settings::DiscoveryHelper - def discovery_warning_hint_text - authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil - end - - def discovery_hint_text - t('admin.settings.security.authorized_fetch_hint') - end - - def discovery_recommended_value - authorized_fetch_overridden? ? :overridden : nil - end -end diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb index 9b950d5a63..552a3ee5a8 100644 --- a/app/helpers/admin/settings_helper.rb +++ b/app/helpers/admin/settings_helper.rb @@ -2,62 +2,6 @@ module Admin::SettingsHelper def captcha_available? - Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present? - end - - def login_activity_title(activity) - t( - "login_activities.#{login_activity_key(activity)}", - method: login_activity_method(activity), - ip: login_activity_ip(activity), - browser: login_activity_browser(activity) - ) - end - - private - - def login_activity_key(activity) - activity.success? ? 'successful_sign_in_html' : 'failed_sign_in_html' - end - - def login_activity_method(activity) - content_tag( - :span, - login_activity_method_string(activity), - class: 'target' - ) - end - - def login_activity_ip(activity) - content_tag( - :span, - activity.ip, - class: 'target' - ) - end - - def login_activity_browser(activity) - content_tag( - :span, - login_activity_browser_description(activity), - class: 'target', - title: activity.user_agent - ) - end - - def login_activity_method_string(activity) - if activity.omniauth? - t("auth.providers.#{activity.provider}") - else - t("login_activities.authentication_methods.#{activity.authentication_method}") - end - end - - def login_activity_browser_description(activity) - t( - 'sessions.description', - browser: t(activity.browser, scope: 'sessions.browsers', default: activity.browser.to_s), - platform: t(activity.platform, scope: 'sessions.platforms', default: activity.platform.to_s) - ) + ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? end end diff --git a/app/helpers/admin/tags_helper.rb b/app/helpers/admin/tags_helper.rb deleted file mode 100644 index eb928a6db2..0000000000 --- a/app/helpers/admin/tags_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Admin::TagsHelper - def admin_tags_moderation_options - [ - [t('admin.tags.moderation.reviewed'), 'reviewed'], - [t('admin.tags.moderation.review_requested'), 'review_requested'], - [t('admin.tags.moderation.unreviewed'), 'unreviewed'], - [t('admin.tags.moderation.trendable'), 'trendable'], - [t('admin.tags.moderation.not_trendable'), 'not_trendable'], - [t('admin.tags.moderation.usable'), 'usable'], - [t('admin.tags.moderation.not_usable'), 'not_usable'], - ] - end -end diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index 33da1f7216..79fee44dc4 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,18 +2,11 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = begin - if status.local? - status.text.split("\n").first - else - Nokogiri::HTML5(status.text).css('html > body > *').first&.text - end - rescue ArgumentError - # This can happen if one of the Nokogumbo limits is encountered - # Unfortunately, it does not use a more precise error class - # nor allows more graceful handling - '' - end + text = if status.local? + status.text.split("\n").first + else + Nokogiri::HTML(status.text).css('html > body > *').first&.text + end return '' if text.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0d7d0e8117..9fd3fe1601 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true module ApplicationHelper - include RegistrationLimitationHelper + DANGEROUS_SCOPES = %w( + read + write + follow + ).freeze RTL_LOCALES = %i( ar @@ -24,12 +28,20 @@ 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' && registrations_in_time? + Setting.registrations_mode == 'open' end def approved_registrations? - Setting.registrations_mode == 'approved' || (Setting.registrations_mode == 'open' && !registrations_in_time?) + Setting.registrations_mode == 'approved' end def closed_registrations? @@ -79,56 +91,55 @@ module ApplicationHelper end end - def html_title - safe_join( - [content_for(:page_title), title] - .compact_blank, - ' - ' - ) - end - def title Rails.env.production? ? site_title : "#{site_title} (Dev)" end - def label_for_scope(scope) - safe_join [ - tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }), - tag.span(t("doorkeeper.scopes.#{scope}"), class: :hint), - ] + def class_for_scope(scope) + 'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s) end def can?(action, record) return false if record.nil? - policy(record).public_send(:"#{action}?") + policy(record).public_send("#{action}?") end - def material_symbol(icon, attributes = {}) - safe_join( - [ - inline_svg_tag( - "400-24px/#{icon}.svg", - class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split), - role: :img, - data: attributes[:data] - ), - ' ', - ] - ) + def fa_icon(icon, attributes = {}) + class_names = attributes[:class]&.split(' ') || [] + class_names << 'fa' + class_names += icon.split.map { |cl| "fa-#{cl}" } + + content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end def check_icon - inline_svg_tag 'check.svg' + 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') + end + + def visibility_icon(status) + if status.public_visibility? + fa_icon('globe', title: I18n.t('statuses.visibilities.public')) + elsif status.unlisted_visibility? + fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) + elsif status.public_unlisted_visibility? + fa_icon('cloud', title: I18n.t('statuses.visibilities.public_unlisted')) + elsif status.login_visibility? + fa_icon('key', title: I18n.t('statuses.visibilities.login')) + elsif status.private_visibility? || status.limited_visibility? + fa_icon('lock', title: I18n.t('statuses.visibilities.private')) + elsif status.direct_visibility? + fa_icon('at', title: I18n.t('statuses.visibilities.direct')) + end end def interrelationships_icon(relationships, account_id) if relationships.following[account_id] && relationships.followed_by[account_id] - material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive') + fa_icon('exchange', title: I18n.t('relationships.mutual'), class: 'fa-fw active passive') elsif relationships.following[account_id] - material_symbol(locale_direction == 'ltr' ? 'arrow_right_alt' : 'arrow_left_alt', title: I18n.t('relationships.following'), class: 'active') + fa_icon(locale_direction == 'ltr' ? 'arrow-right' : 'arrow-left', title: I18n.t('relationships.following'), class: 'fa-fw active') elsif relationships.followed_by[account_id] - material_symbol(locale_direction == 'ltr' ? 'arrow_left_alt' : 'arrow_right_alt', title: I18n.t('relationships.followers'), class: 'passive') + fa_icon(locale_direction == 'ltr' ? 'arrow-left' : 'arrow-right', title: I18n.t('relationships.followers'), class: 'fa-fw passive') end end @@ -145,14 +156,11 @@ module ApplicationHelper end def body_classes - output = [] - output << content_for(:body_classes) + output = body_class_string.split output << "theme-#{current_theme.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui - output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') output << 'rtl' if locale_direction == 'rtl' - output << "content-font-size__#{current_account&.user&.setting_content_font_size}" output.compact_blank.join(' ') end @@ -205,7 +213,7 @@ module ApplicationHelper state_params[:moved_to_account] = current_account.moved_to_account end - state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode? + state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode? json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json # rubocop:disable Rails/OutputSafety @@ -232,43 +240,10 @@ module ApplicationHelper EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s end - def mascot_url - full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg')) - end - - def server_css? - return true if current_account&.user.nil? - - current_account.user.setting_use_server_css - end - - def user_custom_css? - return false if current_account&.user.nil? - - current_account.user.setting_use_custom_css && current_account.user.custom_css_text.present? - end - - def user_custom_css_version - return '0' if current_account&.user&.custom_css.nil? - - current_account&.user&.custom_css&.updated_at.to_s - end - - def copyable_input(options = {}) - tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options) - end - - def recent_tag_usage(tag) - people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts - I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people - end - - def app_store_url_ios - 'https://apps.apple.com/app/mastodon-for-iphone-and-ipad/id1571998974' - end - - def app_store_url_android - 'https://play.google.com/store/apps/details?id=org.joinmastodon.android' + def prerender_custom_emojis_from_hash(html, custom_emojis_hash) + # rubocop:disable Style/OpenStructUse + prerender_custom_emojis(html, JSON.parse([custom_emojis_hash].to_json, object_class: OpenStruct)) + # rubocop:enable Style/OpenStructUse end private diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index 8201f36e3c..2b9c233c23 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -19,6 +19,17 @@ module BrandingHelper end def render_logo - image_tag(frontend_asset_path('images/logo.svg'), alt: 'Mastodon', class: 'logo logo--icon') + 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 diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 077c5272a5..69a8767f0f 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -30,11 +30,15 @@ module ContextHelper other_setting: { 'fedibird' => 'http://fedibird.com/ns#', 'otherSetting' => 'fedibird:otherSetting' }, references: { 'fedibird' => 'http://fedibird.com/ns#', 'references' => { '@id' => 'fedibird:references', '@type' => '@id' } }, quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, - keywords: { 'schema' => 'http://schema.org#', 'keywords' => 'schema:keywords' }, - license: { 'schema' => 'http://schema.org#', 'license' => 'schema:license' }, + 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', + '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' + }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, - attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, - misskey_license: { 'misskey' => 'https://misskey-hub.net/ns#', '_misskey_license' => 'misskey:_misskey_license' }, }.freeze def full_context @@ -42,11 +46,13 @@ module ContextHelper end def serialized_context(named_contexts_map, context_extensions_map) + context_array = [] + named_contexts = named_contexts_map.keys context_extensions = context_extensions_map.keys - context_array = named_contexts.map do |key| - NAMED_CONTEXT_MAP[key] + named_contexts.each do |key| + context_array << NAMED_CONTEXT_MAP[key] end extensions = context_extensions.each_with_object({}) do |key, h| diff --git a/app/helpers/dtl_helper.rb b/app/helpers/dtl_helper.rb index aa2c414c5f..d3c3a8c662 100644 --- a/app/helpers/dtl_helper.rb +++ b/app/helpers/dtl_helper.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true module DtlHelper - def dtl_enabled? - ENV.fetch('DTL_ENABLED', 'false') == 'true' - end - - def dtl_tag_name - ENV.fetch('DTL_TAG', 'kmyblue') - end + DTL_ENABLED = ENV.fetch('DTL_ENABLED', 'false') == 'true' + DTL_TAG = ENV.fetch('DTL_TAG', 'kmyblue') end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb deleted file mode 100644 index 22a1c172de..0000000000 --- a/app/helpers/filters_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module FiltersHelper - def filter_action_label(action) - safe_join( - [ - t("simple_form.labels.filters.actions.#{action}"), - content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint'), - ] - ) - end -end diff --git a/app/helpers/follow_helper.rb b/app/helpers/follow_helper.rb deleted file mode 100644 index a02e2a8832..0000000000 --- a/app/helpers/follow_helper.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module FollowHelper - def request_pending_follow?(source_account, target_account) - target_account.locked? || source_account.silenced? || block_straight_follow?(source_account) || - ((source_account.bot? || proxy_account?(source_account)) && target_account.user&.setting_lock_follow_from_bot) - end - - def block_straight_follow?(account) - return false if account.local? - - DomainBlock.reject_straight_follow?(account.domain) - end - - def proxy_account?(account) - (account.username.downcase.include?('_proxy') || - account.username.downcase.end_with?('proxy') || - account.username.downcase.include?('_bot_') || - account.username.downcase.end_with?('bot') || - account.display_name&.downcase&.include?('proxy') || - account.display_name&.include?('プロキシ') || - account.note&.include?('プロキシ')) && - (account.following_count.zero? || account.following_count > account.followers_count) && - proxyable_software?(account) - end - - def proxyable_software?(account) - return false if account.local? - - InstanceInfo.proxy_account_software?(account.domain) - end -end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index dc7442ac33..112c4256f5 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -1,14 +1,6 @@ # frozen_string_literal: true module FormattingHelper - SYNDICATED_EMOJI_STYLES = <<~CSS.squish - height: 1.1em; - margin: -.2ex .15em .2ex; - object-fit: contain; - vertical-align: middle; - width: 1.1em; - CSS - def html_aware_format(text, local, options = {}) HtmlAwareFormatter.new(text, local, options).to_s end @@ -17,10 +9,6 @@ module FormattingHelper TextFormatter.new(text, options).to_s end - def url_for_preview_card(preview_card) - preview_card.url - end - def extract_status_plain_text(status) PlainTextFormatter.new(status.text, status.local?).to_s end @@ -31,33 +19,46 @@ module FormattingHelper end def status_content_format(status) - MastodonOTELTracer.in_span('HtmlAwareFormatter rendering') do |span| - span.add_attributes( - 'app.formatter.content.type' => 'status', - 'app.formatter.content.origin' => status.local? ? 'local' : 'remote' - ) + html_aware_format(status.text, status.local?, markdown: status.markdown, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) + end - html_aware_format(status.text, status.local?, markdown: status.markdown, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) - end + def emoji_name_format(emoji_reaction, status) + html_aware_format(emoji_reaction['url'].present? ? ":#{emoji_reaction['name']}:" : emoji_reaction['name'], status.local?, markdown: status.markdown) end def rss_status_content_format(status) + html = status_content_format(status) + + before_html = if status.spoiler_text? + tag.p do + tag.strong do + I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) + end + + status.spoiler_text + end + tag.hr + end + + after_html = if status.preloadable_poll + tag.p do + safe_join( + status.preloadable_poll.options.map do |o| + tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) + end, + tag.br + ) + end + end + prerender_custom_emojis( - wrapped_status_content_format(status), + safe_join([before_html, html, after_html]), status.emojis, - style: SYNDICATED_EMOJI_STYLES + style: 'min-width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex' ).to_str end def account_bio_format(account) - MastodonOTELTracer.in_span('HtmlAwareFormatter rendering') do |span| - span.add_attributes( - 'app.formatter.content.type' => 'account_bio', - 'app.formatter.content.origin' => account.local? ? 'local' : 'remote' - ) - - html_aware_format(account.note, account.local?, markdown: account.user&.setting_bio_markdown) - end + html_aware_format(account.note, account.local?, markdown: account.user&.setting_bio_markdown) end def account_field_value_format(field, with_rel_me: true) @@ -67,51 +68,4 @@ module FormattingHelper html_aware_format(field.value, field.account.local?, markdown: false, with_rel_me: with_rel_me, with_domains: true, multiline: false) end end - - def markdown(text) - Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety - end - - private - - def wrapped_status_content_format(status) - safe_join [ - rss_content_preroll(status), - status_content_format(status), - rss_content_postroll(status), - ] - end - - def rss_content_preroll(status) - if status.spoiler_text? - safe_join [ - tag.p { spoiler_with_warning(status) }, - tag.hr, - ] - end - end - - def spoiler_with_warning(status) - safe_join [ - tag.strong { I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) }, - status.spoiler_text, - ] - end - - def rss_content_postroll(status) - if status.preloadable_poll - tag.p do - poll_option_tags(status) - end - end - end - - def poll_option_tags(status) - safe_join( - status.preloadable_poll.options.map do |option| - tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', option, disabled: true) - end, - tag.br - ) - end end diff --git a/app/helpers/high_load_helper.rb b/app/helpers/high_load_helper.rb deleted file mode 100644 index b4606c039f..0000000000 --- a/app/helpers/high_load_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module HighLoadHelper - def allow_high_load? - ENV.fetch('ALLOW_HIGH_LOAD', 'true') == 'true' - end - module_function :allow_high_load? -end diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index 018c69e620..893afdd51f 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -13,22 +13,6 @@ module InstanceHelper safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') end - def instance_presenter - @instance_presenter ||= InstancePresenter.new - end - - def favicon_path(size = '48') - instance_presenter.favicon&.file&.url(size) - end - - def app_icon_path(size = '48') - instance_presenter.app_icon&.file&.url(size) - end - - def use_mask_icon? - instance_presenter.app_icon.blank? - end - private def description_prefix(invite) diff --git a/app/helpers/invites_helper.rb b/app/helpers/invites_helper.rb deleted file mode 100644 index c189061db0..0000000000 --- a/app/helpers/invites_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module InvitesHelper - def invites_max_uses_options - [1, 5, 10, 25, 50, 100] - end - - def invites_expires_options - [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week] - end -end diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb deleted file mode 100644 index 693cdf730f..0000000000 --- a/app/helpers/json_ld_helper.rb +++ /dev/null @@ -1,266 +0,0 @@ -# frozen_string_literal: true - -module JsonLdHelper - include ContextHelper - - def equals_or_includes?(haystack, needle) - haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle - end - - def equals_or_includes_any?(haystack, needles) - needles.any? { |needle| equals_or_includes?(haystack, needle) } - end - - def first_of_value(value) - value.is_a?(Array) ? value.first : value - end - - def uri_from_bearcap(str) - if str&.start_with?('bear:') - Addressable::URI.parse(str).query_values['u'] - else - str - end - end - - # The url attribute can be a string, an array of strings, or an array of objects. - # The objects could include a mimeType. Not-included mimeType means it's text/html. - def url_to_href(value, preferred_type = nil) - single_value = if value.is_a?(Array) && !value.first.is_a?(String) - value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } - elsif value.is_a?(Array) - value.first - else - value - end - - if single_value.nil? || single_value.is_a?(String) - single_value - else - single_value['href'] - end - end - - def as_array(value) - if value.nil? - [] - elsif value.is_a?(Array) - value - else - [value] - end - end - - def as_array_ex(value) - if value.is_a?(Hash) - [] - else - as_array(value) - end - end - - def value_or_id(value) - value.is_a?(String) || value.nil? ? value : value['id'] - end - - def supported_context?(json) - !json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) - end - - def unsupported_uri_scheme?(uri) - uri.nil? || !uri.start_with?('http://', 'https://') - end - - def non_matching_uri_hosts?(base_url, comparison_url) - return true if unsupported_uri_scheme?(comparison_url) - - needle = Addressable::URI.parse(comparison_url).host - haystack = Addressable::URI.parse(base_url).host - - !haystack.casecmp(needle).zero? - end - - def canonicalize(json) - graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context)) - graph.dump(:normalize) - end - - def compact(json) - compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context)) - compacted['signature'] = json['signature'] - compacted - end - - # Patches a JSON-LD document to avoid compatibility issues on redistribution - # - # Since compacting a JSON-LD document against Mastodon's built-in vocabulary - # means other extension namespaces will be expanded, malformed JSON-LD - # attributes lost, and some values “unexpectedly†compacted this method - # patches the following likely sources of incompatibility: - # - 'https://www.w3.org/ns/activitystreams#Public' being compacted to - # 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand - # 'as:Public') - # - single-item arrays being compacted to the item itself (`[foo]` being - # compacted to `foo`) - # - # It is not always possible for `patch_for_forwarding!` to produce a document - # deemed safe for forwarding. Use `safe_for_forwarding?` to check the status - # of the output document. - # - # @param original [Hash] The original JSON-LD document used as reference - # @param compacted [Hash] The compacted JSON-LD document to be patched - # @return [void] - def patch_for_forwarding!(original, compacted) - original.without('@context', 'signature').each do |key, value| - next if value.nil? || !compacted.key?(key) - - compacted_value = compacted[key] - if value.is_a?(Hash) && compacted_value.is_a?(Hash) - patch_for_forwarding!(value, compacted_value) - elsif value.is_a?(Array) - compacted_value = [compacted_value] unless compacted_value.is_a?(Array) - next if value.size != compacted_value.size - - compacted[key] = value.zip(compacted_value).map do |v, vc| - if v.is_a?(Hash) && vc.is_a?(Hash) - patch_for_forwarding!(v, vc) - vc - elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public' - v - else - vc - end - end - elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public' - compacted[key] = value - end - end - end - - # Tests whether a JSON-LD compaction is deemed safe for redistribution, - # that is, if it doesn't change its meaning to consumers that do not actually - # handle JSON-LD, but rely on values being serialized in a certain way. - # - # See `patch_for_forwarding!` for details. - # - # @param original [Hash] The original JSON-LD document used as reference - # @param compacted [Hash] The compacted JSON-LD document to be patched - # @return [Boolean] Whether the patched document is deemed safe - def safe_for_forwarding?(original, compacted) - original.without('@context', 'signature').all? do |key, value| - compacted_value = compacted[key] - return false unless value.instance_of?(compacted_value.class) - - if value.is_a?(Hash) - safe_for_forwarding?(value, compacted_value) - elsif value.is_a?(Array) - value.zip(compacted_value).all? do |v, vc| - v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc - end - else - value == compacted_value - end - end - end - - # Fetch the resource given by uri. - # @param uri [String] - # @param id_is_known [Boolean] - # @param on_behalf_of [nil, Account] - # @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values - def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {}) - unless id_is_known - json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error) - - return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) - - uri = json['id'] - end - - json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options) - json.present? && json['id'] == uri ? json : nil - end - - # Fetch the resource given by uri - # - # If an error is raised, it contains the response and can be captured for handling like - # - # begin - # fetch_resource_without_id_validation(uri, nil, true) - # rescue Mastodon::UnexpectedResponseError => e - # e.response - # end - # - # @param uri [String] - # @param on_behalf_of [nil, Account] - # @param raise_on_error [Symbol<:all, :temporary, :none>] - # - +:all+ - raise if response code is not in the 2xx range - # - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404 - # (see {#response_error_unsalvageable} ) - # - +:none+ - do not raise, return +nil+ - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {}) - on_behalf_of ||= Account.representative - - build_request(uri, on_behalf_of, options: request_options).perform do |response| - raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && ( - raise_on_error == :all || - (!response_error_unsalvageable?(response) && raise_on_error == :temporary) - ) - - body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response) - end - end - - def valid_activitypub_content_type?(response) - return true if response.mime_type == 'application/activity+json' - - # When the mime type is `application/ld+json`, we need to check the profile, - # but `http.rb` does not parse it for us. - return false unless response.mime_type == 'application/ld+json' - - response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str| - str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams') - end - end - - def body_to_json(body, compare_id: nil) - json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body - - return if compare_id.present? && json['id'] != compare_id - - json - rescue Oj::ParseError - nil - end - - def response_successful?(response) - (200...300).cover?(response.code) - end - - def response_error_unsalvageable?(response) - response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) - end - - def build_request(uri, on_behalf_of = nil, options: {}) - Request.new(:get, uri, **options).tap do |request| - request.on_behalf_of(on_behalf_of) if on_behalf_of - request.add_headers('Accept' => 'application/activity+json, application/ld+json') - end - end - - def load_jsonld_context(url, _options = {}, &block) - json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do - request = Request.new(:get, url) - request.add_headers('Accept' => 'application/ld+json') - request.perform do |res| - raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json' - - res.body_with_limit - end - end - - doc = JSON::LD::API::RemoteDocument.new(json, documentUrl: url) - - block ? yield(doc) : doc - end -end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb new file mode 100644 index 0000000000..2db421d10e --- /dev/null +++ b/app/helpers/jsonld_helper.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +module JsonLdHelper + include ContextHelper + + def equals_or_includes?(haystack, needle) + haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle + end + + def equals_or_includes_any?(haystack, needles) + needles.any? { |needle| equals_or_includes?(haystack, needle) } + end + + def first_of_value(value) + value.is_a?(Array) ? value.first : value + end + + def uri_from_bearcap(str) + if str&.start_with?('bear:') + Addressable::URI.parse(str).query_values['u'] + else + str + end + end + + # The url attribute can be a string, an array of strings, or an array of objects. + # The objects could include a mimeType. Not-included mimeType means it's text/html. + def url_to_href(value, preferred_type = nil) + single_value = if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end + + if single_value.nil? || single_value.is_a?(String) + single_value + else + single_value['href'] + end + end + + def as_array(value) + if value.nil? + [] + elsif value.is_a?(Array) + value + else + [value] + end + end + + def value_or_id(value) + value.is_a?(String) || value.nil? ? value : value['id'] + end + + def supported_context?(json) + !json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) + end + + def unsupported_uri_scheme?(uri) + uri.nil? || !uri.start_with?('http://', 'https://') + end + + def non_matching_uri_hosts?(base_url, comparison_url) + return true if unsupported_uri_scheme?(comparison_url) + + needle = Addressable::URI.parse(comparison_url).host + haystack = Addressable::URI.parse(base_url).host + + !haystack.casecmp(needle).zero? + end + + def canonicalize(json) + graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context)) + graph.dump(:normalize) + end + + def compact(json) + compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context)) + compacted['signature'] = json['signature'] + compacted + end + + # Patches a JSON-LD document to avoid compatibility issues on redistribution + # + # Since compacting a JSON-LD document against Mastodon's built-in vocabulary + # means other extension namespaces will be expanded, malformed JSON-LD + # attributes lost, and some values “unexpectedly†compacted this method + # patches the following likely sources of incompatibility: + # - 'https://www.w3.org/ns/activitystreams#Public' being compacted to + # 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand + # 'as:Public') + # - single-item arrays being compacted to the item itself (`[foo]` being + # compacted to `foo`) + # + # It is not always possible for `patch_for_forwarding!` to produce a document + # deemed safe for forwarding. Use `safe_for_forwarding?` to check the status + # of the output document. + # + # @param original [Hash] The original JSON-LD document used as reference + # @param compacted [Hash] The compacted JSON-LD document to be patched + # @return [void] + def patch_for_forwarding!(original, compacted) + original.without('@context', 'signature').each do |key, value| + next if value.nil? || !compacted.key?(key) + + compacted_value = compacted[key] + if value.is_a?(Hash) && compacted_value.is_a?(Hash) + patch_for_forwarding!(value, compacted_value) + elsif value.is_a?(Array) + compacted_value = [compacted_value] unless compacted_value.is_a?(Array) + next if value.size != compacted_value.size + + compacted[key] = value.zip(compacted_value).map do |v, vc| + if v.is_a?(Hash) && vc.is_a?(Hash) + patch_for_forwarding!(v, vc) + vc + elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public' + v + else + vc + end + end + elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public' + compacted[key] = value + end + end + end + + # Tests whether a JSON-LD compaction is deemed safe for redistribution, + # that is, if it doesn't change its meaning to consumers that do not actually + # handle JSON-LD, but rely on values being serialized in a certain way. + # + # See `patch_for_forwarding!` for details. + # + # @param original [Hash] The original JSON-LD document used as reference + # @param compacted [Hash] The compacted JSON-LD document to be patched + # @return [Boolean] Whether the patched document is deemed safe + def safe_for_forwarding?(original, compacted) + original.without('@context', 'signature').all? do |key, value| + compacted_value = compacted[key] + return false unless value.class == compacted_value.class + + if value.is_a?(Hash) + safe_for_forwarding?(value, compacted_value) + elsif value.is_a?(Array) + value.zip(compacted_value).all? do |v, vc| + v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc + end + else + value == compacted_value + end + end + end + + def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {}) + unless id_is_known + json = fetch_resource_without_id_validation(uri, on_behalf_of) + + return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) + + uri = json['id'] + end + + json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) + json.present? && json['id'] == uri ? json : nil + end + + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) + on_behalf_of ||= Account.representative + + build_request(uri, on_behalf_of, options: request_options).perform do |response| + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + + body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response) + end + end + + def valid_activitypub_content_type?(response) + return true if response.mime_type == 'application/activity+json' + + # When the mime type is `application/ld+json`, we need to check the profile, + # but `http.rb` does not parse it for us. + return false unless response.mime_type == 'application/ld+json' + + response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str| + str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams') + end + end + + def body_to_json(body, compare_id: nil) + json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body + + return if compare_id.present? && json['id'] != compare_id + + json + rescue Oj::ParseError + nil + end + + def merge_context(context, new_context) + if context.is_a?(Array) + context << new_context + else + [context, new_context] + end + end + + def response_successful?(response) + (200...300).cover?(response.code) + end + + def response_error_unsalvageable?(response) + response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) + end + + def build_request(uri, on_behalf_of = nil, options: {}) + Request.new(:get, uri, **options).tap do |request| + request.on_behalf_of(on_behalf_of) if on_behalf_of + request.add_headers('Accept' => 'application/activity+json, application/ld+json') + end + end + + def load_jsonld_context(url, _options = {}, &block) + json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do + request = Request.new(:get, url) + request.add_headers('Accept' => 'application/ld+json') + request.perform do |res| + raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json' + + res.body_with_limit + end + end + + doc = JSON::LD::API::RemoteDocument.new(json, documentUrl: url) + + block ? yield(doc) : doc + end +end diff --git a/app/helpers/kmyblue_capabilities_helper.rb b/app/helpers/kmyblue_capabilities_helper.rb index 279505bec8..c1a67b2627 100644 --- a/app/helpers/kmyblue_capabilities_helper.rb +++ b/app/helpers/kmyblue_capabilities_helper.rb @@ -2,50 +2,25 @@ module KmyblueCapabilitiesHelper def fedibird_capabilities - capabilities = %i( - enable_wide_emoji - kmyblue_searchability - searchability - kmyblue_markdown - kmyblue_reaction_deck - kmyblue_visibility_login - status_reference - visibility_mutual - visibility_limited - kmyblue_limited_scope - kmyblue_antenna - kmyblue_bookmark_category - kmyblue_quote - kmyblue_searchability_limited - kmyblue_circle_history - kmyblue_list_notification - kmyblue_server_features - favourite_list - kmyblue_favourite_antenna - ) - - capabilities << :full_text_search if Chewy.enabled? - if Setting.enable_emoji_reaction - capabilities << :emoji_reaction - capabilities << :enable_wide_emoji_reaction - end - capabilities << :kmyblue_visibility_public_unlisted if Setting.enable_public_unlisted_visibility - capabilities << :kmyblue_searchability_public_unlisted if Setting.enable_public_unlisted_visibility - capabilities << :kmyblue_no_public_visibility unless Setting.enable_public_visibility - capabilities << :timeline_no_local unless Setting.enable_local_timeline - - capabilities - end - - def capabilities_for_nodeinfo - capabilities = %i( - enable_wide_emoji - status_reference - quote - emoji_keywords - circle - ) + capabilities = [ + :enable_wide_emoji, + :kmyblue_searchability, + :searchability, + :kmyblue_markdown, + :kmyblue_reaction_deck, + :kmyblue_visibility_login, + :status_reference, + :visibility_mutual, + :visibility_limited, + :kmyblue_limited_scope, + :kmyblue_antenna, + :kmyblue_bookmark_category, + :kmyblue_quote, + :kmyblue_searchability_limited, + :kmyblue_visibility_public_unlisted, + ] + capabilities << :profile_search unless Chewy.enabled? if Setting.enable_emoji_reaction capabilities << :emoji_reaction capabilities << :enable_wide_emoji_reaction diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 0a8ebcde54..c42c4c23ef 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -109,7 +109,6 @@ 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, @@ -128,7 +127,7 @@ module LanguagesHelper om: ['Oromo', 'Afaan Oromoo'].freeze, or: ['Oriya', 'ଓଡ଼ିଆ'].freeze, os: ['Ossetian', 'ирон æвзаг'].freeze, - pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze, + pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze, pi: ['PÄli', 'पाऴि'].freeze, pl: ['Polish', 'Polski'].freeze, ps: ['Pashto', 'پښتو'].freeze, @@ -162,7 +161,7 @@ module LanguagesHelper th: ['Thai', 'ไทย'].freeze, ti: ['Tigrinya', 'ትáŒáˆ­áŠ›'].freeze, tk: ['Turkmen', 'Türkmen'].freeze, - tl: ['Tagalog', 'Tagalog'].freeze, + tl: ['Tagalog', 'Wikang Tagalog'].freeze, tn: ['Tswana', 'Setswana'].freeze, to: ['Tonga', 'faka Tonga'].freeze, tr: ['Turkish', 'Türkçe'].freeze, @@ -192,21 +191,15 @@ module LanguagesHelper chr: ['Cherokee', 'á£áŽ³áŽ© Ꭶá¬á‚Ꭿáá—'].freeze, ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, cnr: ['Montenegrin', 'crnogorski'].freeze, - csb: ['Kashubian', 'Kaszëbsczi'].freeze, - gsw: ['Swiss German', 'Schwiizertütsch'].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, @@ -231,28 +224,12 @@ module LanguagesHelper 'en-GB': 'English (British)', 'es-AR': 'Español (Argentina)', 'es-MX': 'Español (México)', - 'fr-CA': 'Français (Canadien)', + 'fr-QC': 'Français (Canadien)', 'pt-BR': 'Português (Brasil)', 'pt-PT': 'Português (Portugal)', 'sr-Latn': 'Srpski (latinica)', }.freeze - # Helper for self.sorted_locale_keys - private_class_method def self.locale_name_for_sorting(locale) - if (supported_locale = SUPPORTED_LOCALES[locale.to_sym]) - ASCIIFolding.new.fold(supported_locale[1]).downcase - elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym]) - ASCIIFolding.new.fold(regional_locale).downcase - else - locale - end - end - - # Sort locales by native name for dropdown menus - def self.sorted_locale_keys(locale_keys) - locale_keys.sort_by { |key, _| locale_name_for_sorting(key) } - end - def native_locale_name(locale) if locale.blank? || locale == 'und' I18n.t('generic.none') @@ -303,3 +280,5 @@ module LanguagesHelper locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) end end + +# rubocop:enable Metrics/ModuleLength diff --git a/app/helpers/mascot_helper.rb b/app/helpers/mascot_helper.rb new file mode 100644 index 0000000000..0124c74f19 --- /dev/null +++ b/app/helpers/mascot_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module MascotHelper + def mascot_url + full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg')) + end + + private + + def instance_presenter + @instance_presenter ||= InstancePresenter.new + end +end diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb index 269566528a..fa8f34fb4d 100644 --- a/app/helpers/media_component_helper.rb +++ b/app/helpers/media_component_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module MediaComponentHelper - def render_video_component(status, **) + def render_video_component(status, **options) video = status.ordered_media_attachments.first meta = video.file.meta || {} @@ -18,14 +18,14 @@ module MediaComponentHelper media: [ serialize_media_attachment(video), ].as_json, - }.merge(**) + }.merge(**options) react_component :video, component_params do render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } end end - def render_audio_component(status, **) + def render_audio_component(status, **options) audio = status.ordered_media_attachments.first meta = audio.file.meta || {} @@ -38,25 +38,45 @@ module MediaComponentHelper foregroundColor: meta.dig('colors', 'foreground'), accentColor: meta.dig('colors', 'accent'), duration: meta.dig('original', 'duration'), - }.merge(**) + }.merge(**options) react_component :audio, component_params do render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } end end - def render_media_gallery_component(status, **) + def render_media_gallery_component(status, **options) component_params = { sensitive: sensitive_viewer?(status, current_account), autoplay: prefers_autoplay?, media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json }, - }.merge(**) + }.merge(**options) react_component :media_gallery, component_params do render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } end end + def render_card_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + card: serialize_status_card(status).as_json, + }.merge(**options) + + react_component :card, component_params + end + + def render_poll_component(status, **options) + component_params = { + disabled: true, + poll: serialize_status_poll(status).as_json, + }.merge(**options) + + react_component :poll, component_params do + render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } + end + end + private def serialize_media_attachment(attachment) @@ -66,6 +86,22 @@ module MediaComponentHelper ) end + def serialize_status_card(status) + ActiveModelSerializers::SerializableResource.new( + status.preview_card, + serializer: REST::PreviewCardSerializer + ) + end + + def serialize_status_poll(status) + ActiveModelSerializers::SerializableResource.new( + status.preloadable_poll, + serializer: REST::PollSerializer, + scope: current_user, + scope_name: :current_user + ) + end + def sensitive_viewer?(status, account) if !account.nil? && account.id == status.account_id status.sensitive diff --git a/app/helpers/ng_rule_helper.rb b/app/helpers/ng_rule_helper.rb deleted file mode 100644 index 104442b117..0000000000 --- a/app/helpers/ng_rule_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module NgRuleHelper - def check_invalid_status_for_ng_rule!(account, **options) - (check_for_ng_rule!(account, **options) { |rule| !rule.check_status_or_record! }).none? - end - - def check_invalid_reaction_for_ng_rule!(account, **options) - (check_for_ng_rule!(account, **options) { |rule| !rule.check_reaction_or_record! }).none? - end - - private - - def check_for_ng_rule!(account, **options, &block) - NgRule.cached_rules - .map { |raw_rule| Admin::NgRule.new(raw_rule, account, **options) } - .filter(&block) - end - - def do_account_action_for_rule!(account, action) - case action - when :silence - account.silence! - when :suspend - account.suspend! - end - end -end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index 821a6f1e2d..ce616e8306 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -15,20 +15,9 @@ 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 diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb deleted file mode 100644 index f387a48309..0000000000 --- a/app/helpers/registration_helper.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module RegistrationHelper - extend ActiveSupport::Concern - - include RegistrationLimitationHelper - - def allowed_registration?(remote_ip, invite) - !Rails.configuration.x.single_user_mode && !omniauth_only? && ((registrations_open? && !reach_registrations_limit?) || invite&.valid_for_use?) && !ip_blocked?(remote_ip) - end - - def registrations_open? - Setting.registrations_mode != 'none' - end - - def omniauth_only? - ENV['OMNIAUTH_ONLY'] == 'true' - end - - def ip_blocked?(remote_ip) - IpBlock.severity_sign_up_block.containing(remote_ip.to_s).exists? - end -end diff --git a/app/helpers/registration_limitation_helper.rb b/app/helpers/registration_limitation_helper.rb deleted file mode 100644 index c295dc0746..0000000000 --- a/app/helpers/registration_limitation_helper.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module RegistrationLimitationHelper - def reach_registrations_limit? - ((Setting.registrations_limit.presence || 0).positive? && Setting.registrations_limit <= user_count_for_registration) || - ((Setting.registrations_limit_per_day.presence || 0).positive? && Setting.registrations_limit_per_day <= today_increase_user_count) - end - - def user_count_for_registration - Rails.cache.fetch('registrations:user_count') { User.confirmed.enabled.joins(:account).merge(Account.without_suspended).count } - end - - def today_increase_user_count - today_date = Time.now.utc.beginning_of_day.to_i - count = 0 - - if Rails.cache.fetch('registrations:today_date') { today_date } == today_date - count = Rails.cache.fetch('registrations:today_increase_user_count') { today_increase_user_count_value } - else - count = today_increase_user_count_value - Rails.cache.write('registrations:today_date', today_date) - Rails.cache.write('registrations:today_increase_user_count', count) - end - - count - end - - def today_increase_user_count_value - User.confirmed.enabled.where(users: { created_at: Time.now.utc.beginning_of_day.. }).joins(:account).merge(Account.without_suspended).count - end - - def registrations_in_time? - start_hour = Setting.registrations_start_hour - end_hour = Setting.registrations_end_hour - secondary_start_hour = Setting.registrations_secondary_start_hour - secondary_end_hour = Setting.registrations_secondary_end_hour - - start_hour = 0 unless start_hour.is_a?(Integer) - end_hour = 0 unless end_hour.is_a?(Integer) - secondary_start_hour = 0 unless secondary_start_hour.is_a?(Integer) - secondary_end_hour = 0 unless secondary_end_hour.is_a?(Integer) - - return true if start_hour >= end_hour && secondary_start_hour >= secondary_end_hour - - current_hour = Time.now.utc.hour - - (start_hour < end_hour && end_hour.positive? && current_hour.between?(start_hour, end_hour - 1)) || - (secondary_start_hour < secondary_end_hour && secondary_end_hour.positive? && current_hour.between?(secondary_start_hour, secondary_end_hour - 1)) - end - - def reset_registration_limit_caches! - Rails.cache.delete('registrations:user_count') - Rails.cache.delete('registrations:today_increase_user_count') - end -end diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb index 22efc5f092..0d5a8505a2 100644 --- a/app/helpers/routing_helper.rb +++ b/app/helpers/routing_helper.rb @@ -3,19 +3,18 @@ module RoutingHelper extend ActiveSupport::Concern + include Rails.application.routes.url_helpers include ActionView::Helpers::AssetTagHelper include Webpacker::Helper included do - include Rails.application.routes.url_helpers - def default_url_options ActionMailer::Base.default_url_options end end - def full_asset_url(source, **) - source = ActionController::Base.helpers.asset_url(source, **) unless use_storage? + def full_asset_url(source, **options) + source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage? URI.join(asset_host, source).to_s end @@ -24,12 +23,8 @@ module RoutingHelper Rails.configuration.action_controller.asset_host || root_url end - def frontend_asset_path(source, **) - asset_pack_path("media/#{source}", **) - end - - def frontend_asset_url(source, **) - full_asset_url(frontend_asset_path(source, **)) + def full_pack_url(source, **options) + full_asset_url(asset_pack_path(source, **options)) end def use_storage? diff --git a/app/helpers/self_destruct_helper.rb b/app/helpers/self_destruct_helper.rb deleted file mode 100644 index f1927b1e04..0000000000 --- a/app/helpers/self_destruct_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module SelfDestructHelper - VERIFY_PURPOSE = 'self-destruct' - - def self.self_destruct? - value = Rails.configuration.x.mastodon.self_destruct_value - value.present? && Rails.application.message_verifier(VERIFY_PURPOSE).verify(value) == ENV['LOCAL_DOMAIN'] - rescue ActiveSupport::MessageVerifier::InvalidSignature - false - end - - def self_destruct? - SelfDestructHelper.self_destruct? - end -end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index fd631ce92e..889ca7f402 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -2,36 +2,18 @@ module SettingsHelper def filterable_languages - LanguagesHelper.sorted_locale_keys(LanguagesHelper::SUPPORTED_LOCALES.keys) - end - - def ui_languages - LanguagesHelper.sorted_locale_keys(I18n.available_locales) - end - - def featured_tags_hint(recently_used_tags) - recently_used_tags.present? && - safe_join( - [ - t('simple_form.hints.featured_tag.name'), - safe_join( - links_for_featured_tags(recently_used_tags), - ', ' - ), - ], - ' ' - ) + LanguagesHelper::SUPPORTED_LOCALES.keys end def session_device_icon(session) device = session.detection.device if device.mobile? - 'smartphone' + 'mobile' elsif device.tablet? 'tablet' else - 'desktop_mac' + 'desktop' end end @@ -39,21 +21,7 @@ module SettingsHelper return if account.nil? link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do - safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') + safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') end end - - private - - def links_for_featured_tags(tags) - tags.map { |tag| post_link_to_featured_tag(tag) } - end - - def post_link_to_featured_tag(tag) - link_to( - "##{tag.display_name}", - settings_featured_tags_path(featured_tag: { name: tag.name }), - method: :post - ) - end end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 7e42dd4623..01102645de 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true module StatusesHelper - VISIBLITY_ICONS = { - public: 'globe', - unlisted: 'lock_open', - private: 'lock', - direct: 'alternate_email', - public_unlisted: 'cloud', - login: 'key', - limited: 'shield', - }.freeze + 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 = '') - tag.div(class: ['nothing-here', extra_classes]) do + content_tag(:div, class: "nothing-here #{extra_classes}") do t('accounts.nothing_here') end end @@ -60,8 +61,31 @@ module StatusesHelper components.compact_blank.join("\n\n") end - def visibility_icon(status) - VISIBLITY_ICONS[status.visibility.to_sym] + def stream_link_target + embedded_view? ? '_blank' : nil + end + + def fa_visibility_icon(status) + case status.visibility + when 'public' + fa_icon 'globe fw' + when 'unlisted' + fa_icon 'unlock fw' + when 'public_unlisted' + fa_icon 'cloud fw' + when 'login' + fa_icon 'key fw' + when 'private' + fa_icon 'lock fw' + when 'limited' + fa_icon 'get-pocket fw' + when 'direct' + fa_icon 'at fw' + end + end + + def embedded_view? + params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end def prefers_autoplay? diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb deleted file mode 100644 index f4d88a1ef0..0000000000 --- a/app/helpers/theme_helper.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module ThemeHelper - def theme_style_tags(theme) - if theme == 'system' - ''.html_safe.tap do |tags| - tags << stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') - tags << stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') - end - else - stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous' - end - end - - def theme_color_tags(theme) - if theme == 'system' - ''.html_safe.tap do |tags| - tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') - tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)') - end - else - tag.meta name: 'theme-color', content: theme_color_for(theme) - end - end - - def custom_stylesheet - if active_custom_stylesheet.present? - stylesheet_link_tag( - custom_css_path(active_custom_stylesheet), - host: root_url, - media: :all, - skip_pipeline: true - ) - end - end - - def system_stylesheet - stylesheet_link_tag( - system_css_path, - host: root_url, - media: :all, - skip_pipeline: true - ) - end - - def user_custom_stylesheet - stylesheet_link_tag( - user_custom_css_path({ version: user_custom_css_version }), - host: root_url, - media: :all, - skip_pipeline: true - ) - end - - private - - def active_custom_stylesheet - if cached_custom_css_digest.present? - [:custom, cached_custom_css_digest.to_s.first(8)] - .compact_blank - .join('-') - end - end - - def cached_custom_css_digest - Rails.cache.fetch(:setting_digest_custom_css) do - Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) } - end - end - - def theme_color_for(theme) - theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] - end -end diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb new file mode 100644 index 0000000000..482f4e19ea --- /dev/null +++ b/app/helpers/webfinger_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module WebfingerHelper + def webfinger!(uri) + Webfinger.new(uri).perform + end +end diff --git a/app/inputs/date_of_birth_input.rb b/app/inputs/date_of_birth_input.rb deleted file mode 100644 index 131234b02e..0000000000 --- a/app/inputs/date_of_birth_input.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class DateOfBirthInput < SimpleForm::Inputs::Base - OPTIONS = [ - { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze, - { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze, - { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze, - ].freeze - - def input(wrapper_options = nil) - merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) - merged_input_options[:inputmode] = 'numeric' - - values = (object.public_send(attribute_name) || '').split('.') - - safe_join(Array.new(3) do |index| - options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index] - @builder.text_field("#{attribute_name}(#{index + 1}i)", options) - end) - end - - def label_target - "#{attribute_name}_1i" - end - - private - - def generate_id(index) - "#{object_name}_#{attribute_name}_#{index + 1}i" - end -end diff --git a/app/javascript/__mocks__/svg.js b/app/javascript/__mocks__/svg.js deleted file mode 100644 index 762bc165d0..0000000000 --- a/app/javascript/__mocks__/svg.js +++ /dev/null @@ -1,3 +0,0 @@ -const ReactComponent = 'div'; - -export default ReactComponent; diff --git a/app/javascript/entrypoints/admin.tsx b/app/javascript/entrypoints/admin.tsx deleted file mode 100644 index 64192f54ad..0000000000 --- a/app/javascript/entrypoints/admin.tsx +++ /dev/null @@ -1,440 +0,0 @@ -import './public-path'; -import { createRoot } from 'react-dom/client'; - -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( - '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( - '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(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( - '#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( - 'input#batch_checkbox_all', - ); - const selectAllMatchingElement = document.querySelector( - '.batch-table__select-all', - ); - - if (checkAllElement) { - const allCheckboxes = Array.from( - document.querySelectorAll(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( - '#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( - '.form_admin_settings_registrations_mode .warning-hint', - ) - .forEach((warning_hint) => { - warning_hint.style.display = target.value === 'open' ? 'inline' : 'none'; - }); - - const toggleEnabled = (input: HTMLInputElement, value: boolean) => { - input.disabled = !value; - if (value) { - 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')); - } - }; - - document - .querySelectorAll( - 'input#form_admin_settings_require_invite_text', - ) - .forEach((input) => { - toggleEnabled(input, enabled); - }); - - document - .querySelectorAll( - '#form_admin_settings_registrations_start_hour, #form_admin_settings_registrations_end_hour, #form_admin_settings_registrations_secondary_start_hour, #form_admin_settings_registrations_secondary_end_hour', - ) - .forEach((input) => { - toggleEnabled(input, target.value === 'open'); - }); -}; - -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); - }, -); - -const addTableRow = (tableId: string) => { - const templateElement = document.querySelector(`#${tableId} .template-row`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - if ( - typeof templateElement === 'undefined' || - typeof tableElement === 'undefined' - ) - return; - - let temporaryId = 0; - tableElement - .querySelectorAll('.temporary_id') - .forEach((input) => { - if (parseInt(input.value) + 1 > temporaryId) { - temporaryId = parseInt(input.value) + 1; - } - }); - - const cloned = templateElement.cloneNode(true) as HTMLTableRowElement; - cloned.className = ''; - cloned.querySelector('.temporary_id')!.value = // eslint-disable-line @typescript-eslint/no-non-null-assertion - temporaryId.toString(); - cloned - .querySelectorAll('input[type=checkbox]') - .forEach((input) => { - input.value = temporaryId.toString(); - }); - tableElement.appendChild(cloned); -}; - -const removeTableRow = (target: EventTarget | null, tableId: string) => { - const tableRowElement = (target as HTMLElement).closest('tr') as Node; - const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - if ( - typeof tableRowElement === 'undefined' || - typeof tableElement === 'undefined' - ) - return; - - tableElement.removeChild(tableRowElement); -}; - -const setupTableList = (id: string) => { - Rails.delegate(document, `#${id} .add-row-button`, 'click', (ev) => { - ev.preventDefault(); - addTableRow(id); - }); - - Rails.delegate(document, `#${id} .delete-row-button`, 'click', (ev) => { - ev.preventDefault(); - removeTableRow(ev.target, id); - }); -}; - -setupTableList('sensitive-words-table'); -setupTableList('ng-words-table'); -setupTableList('white-list-table'); - -async function mountReactComponent(element: Element) { - const componentName = element.getAttribute('data-admin-component'); - const stringProps = element.getAttribute('data-props'); - - if (!stringProps) return; - - const componentProps = JSON.parse(stringProps) as object; - - const { default: AdminComponent } = await import( - '@/mastodon/containers/admin_component' - ); - - const { default: Component } = (await import( - `@/mastodon/components/admin/${componentName}` - )) as { default: React.ComponentType }; - - const root = createRoot(element); - - root.render( - - - , - ); -} - -ready(() => { - const domainBlockSeveritySelect = document.querySelector( - 'select#domain_block_severity', - ); - if (domainBlockSeveritySelect) - onDomainBlockSeverityChange(domainBlockSeveritySelect); - - const enableBootstrapTimelineAccounts = - document.querySelector( - 'input#form_admin_settings_enable_bootstrap_timeline_accounts', - ); - if (enableBootstrapTimelineAccounts) - onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts); - - const registrationMode = document.querySelector( - 'select#form_admin_settings_registrations_mode', - ); - if (registrationMode) onChangeRegistrationMode(registrationMode); - - const checkAllElement = document.querySelector( - 'input#batch_checkbox_all', - ); - if (checkAllElement) { - const allCheckboxes = Array.from( - document.querySelectorAll(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( - '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('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('input[type="datetime-local"]') - .forEach((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); - } - - document.querySelectorAll('[data-admin-component]').forEach((element) => { - void mountReactComponent(element); - }); -}).catch((reason: unknown) => { - throw reason; -}); diff --git a/app/javascript/entrypoints/application.ts b/app/javascript/entrypoints/application.ts deleted file mode 100644 index 1087b1c4cb..0000000000 --- a/app/javascript/entrypoints/application.ts +++ /dev/null @@ -1,15 +0,0 @@ -import './public-path'; -import main from 'mastodon/main'; - -import { start } from '../mastodon/common'; -import { loadLocale } from '../mastodon/locales'; -import { loadPolyfills } from '../mastodon/polyfills'; - -start(); - -loadPolyfills() - .then(loadLocale) - .then(main) - .catch((e: unknown) => { - console.error(e); - }); diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx deleted file mode 100644 index 6c091e4d07..0000000000 --- a/app/javascript/entrypoints/embed.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import './public-path'; -import { createRoot } from 'react-dom/client'; - -import { afterInitialRender } from 'mastodon/hooks/useRenderSignal'; - -import { start } from '../mastodon/common'; -import { Status } from '../mastodon/features/standalone/status'; -import { loadPolyfills } from '../mastodon/polyfills'; -import ready from '../mastodon/ready'; - -start(); - -function loaded() { - const mountNode = document.getElementById('mastodon-status'); - - if (mountNode) { - const attr = mountNode.getAttribute('data-props'); - - if (!attr) return; - - const props = JSON.parse(attr) as { id: string; locale: string }; - const root = createRoot(mountNode); - - root.render(); - } -} - -function main() { - ready(loaded).catch((error: unknown) => { - console.error(error); - }); -} - -loadPolyfills() - .then(main) - .catch((error: unknown) => { - console.error(error); - }); - -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; - - // Only set overflow to `hidden` once we got the expected `message` so the post can still be scrolled if - // embedded without parent Javascript support - document.body.style.overflow = 'hidden'; - - // We use a timeout to allow for the React page to render before calculating the height - afterInitialRender(() => { - window.parent.postMessage( - { - type: 'setHeight', - id: data.id, - height: document.getElementsByTagName('html')[0]?.scrollHeight, - }, - '*', - ); - }); -}); diff --git a/app/javascript/entrypoints/error.ts b/app/javascript/entrypoints/error.ts deleted file mode 100644 index db68484f3a..0000000000 --- a/app/javascript/entrypoints/error.ts +++ /dev/null @@ -1,18 +0,0 @@ -import './public-path'; -import ready from '../mastodon/ready'; - -ready(() => { - const image = document.querySelector('img'); - - if (!image) return; - - image.addEventListener('mouseenter', () => { - image.src = '/oops.gif'; - }); - - image.addEventListener('mouseleave', () => { - image.src = '/oops.png'; - }); -}).catch((e: unknown) => { - console.error(e); -}); diff --git a/app/javascript/entrypoints/inert.ts b/app/javascript/entrypoints/inert.ts deleted file mode 100644 index 7c04a97faf..0000000000 --- a/app/javascript/entrypoints/inert.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* Placeholder file to have `inert.scss` compiled by Webpack - This is used by the `wicg-inert` polyfill */ - -import '../styles/inert.scss'; diff --git a/app/javascript/entrypoints/public-path.ts b/app/javascript/entrypoints/public-path.ts deleted file mode 100644 index ac4b9355b9..0000000000 --- a/app/javascript/entrypoints/public-path.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Dynamically set webpack's loading path depending on a meta header, in order -// to share the same assets regardless of instance configuration. -// See https://webpack.js.org/guides/public-path/#on-the-fly - -function removeOuterSlashes(string: string) { - return string.replace(/^\/*/, '').replace(/\/*$/, ''); -} - -function formatPublicPath(host = '', path = '') { - let formattedHost = removeOuterSlashes(host); - if (formattedHost && !/^http/i.test(formattedHost)) { - formattedHost = `//${formattedHost}`; - } - const formattedPath = removeOuterSlashes(path); - return `${formattedHost}/${formattedPath}/`; -} - -const cdnHost = document.querySelector('meta[name=cdn-host]'); - -__webpack_public_path__ = formatPublicPath( - cdnHost ? cdnHost.content : '', - process.env.PUBLIC_OUTPUT_PATH, -); diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx deleted file mode 100644 index 9374d6b2d1..0000000000 --- a/app/javascript/entrypoints/public.tsx +++ /dev/null @@ -1,420 +0,0 @@ -import { createRoot } from 'react-dom/client'; - -import './public-path'; - -import { IntlMessageFormat } from 'intl-messageformat'; -import type { MessageDescriptor, PrimitiveType } from 'react-intl'; -import { defineMessages } from 'react-intl'; - -import Rails from '@rails/ujs'; -import axios from 'axios'; -import { throttle } from 'lodash'; - -import { start } from '../mastodon/common'; -import { timeAgoString } from '../mastodon/components/relative_timestamp'; -import emojify from '../mastodon/features/emoji/emoji'; -import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; -import { loadLocale, getLocale } from '../mastodon/locales'; -import { loadPolyfills } from '../mastodon/polyfills'; -import ready from '../mastodon/ready'; - -import 'cocoon-js-vanilla'; - -start(); - -const messages = defineMessages({ - usernameTaken: { - id: 'username.taken', - defaultMessage: 'That username is taken. Try another', - }, - passwordExceedsLength: { - id: 'password_confirmation.exceeds_maxlength', - defaultMessage: 'Password confirmation exceeds the maximum password length', - }, - passwordDoesNotMatch: { - id: 'password_confirmation.mismatching', - defaultMessage: 'Password confirmation does not match', - }, -}); - -function loaded() { - const { messages: localeData } = getLocale(); - - const locale = document.documentElement.lang; - - const dateTimeFormat = new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - }); - - const dateFormat = new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - - const timeFormat = new Intl.DateTimeFormat(locale, { - timeStyle: 'short', - }); - - const formatMessage = ( - { id, defaultMessage }: MessageDescriptor, - values?: Record, - ) => { - let message: string | undefined = undefined; - - if (id) message = localeData[id]; - - message ??= defaultMessage as string; - - const messageFormat = new IntlMessageFormat(message, locale); - return messageFormat.format(values) as string; - }; - - document.querySelectorAll('.emojify').forEach((content) => { - content.innerHTML = emojify(content.innerHTML); - }); - - document - .querySelectorAll('time.formatted') - .forEach((content) => { - const datetime = new Date(content.dateTime); - const formattedDate = dateTimeFormat.format(datetime); - - content.title = formattedDate; - content.textContent = formattedDate; - }); - - const isToday = (date: Date) => { - const today = new Date(); - - return ( - date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear() - ); - }; - const todayFormat = new IntlMessageFormat( - localeData['relative_format.today'] ?? 'Today at {time}', - locale, - ); - - document - .querySelectorAll('time.relative-formatted') - .forEach((content) => { - const datetime = new Date(content.dateTime); - - let formattedContent: string; - - if (isToday(datetime)) { - const formattedTime = timeFormat.format(datetime); - - formattedContent = todayFormat.format({ - time: formattedTime, - }) as string; - } else { - formattedContent = dateFormat.format(datetime); - } - - const timeGiven = content.dateTime.includes('T'); - content.title = timeGiven - ? dateTimeFormat.format(datetime) - : dateFormat.format(datetime); - - content.textContent = formattedContent; - }); - - document - .querySelectorAll('time.time-ago') - .forEach((content) => { - const datetime = new Date(content.dateTime); - const now = new Date(); - - const timeGiven = content.dateTime.includes('T'); - content.title = timeGiven - ? dateTimeFormat.format(datetime) - : dateFormat.format(datetime); - content.textContent = timeAgoString( - { - formatMessage, - formatDate: (date: Date, options) => - new Intl.DateTimeFormat(locale, options).format(date), - }, - datetime, - now.getTime(), - now.getFullYear(), - timeGiven, - ); - }); - - const reactComponents = document.querySelectorAll('[data-component]'); - - if (reactComponents.length > 0) { - import( - /* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container' - ) - .then(({ default: MediaContainer }) => { - reactComponents.forEach((component) => { - Array.from(component.children).forEach((child) => { - component.removeChild(child); - }); - }); - - const content = document.createElement('div'); - - const root = createRoot(content); - root.render( - , - ); - document.body.appendChild(content); - - return true; - }) - .catch((error: unknown) => { - console.error(error); - }); - } - - Rails.delegate( - document, - 'input#user_account_attributes_username', - 'input', - throttle( - ({ target }) => { - if (!(target instanceof HTMLInputElement)) return; - - if (target.value && target.value.length > 0) { - axios - .get('/api/v1/accounts/lookup', { params: { acct: target.value } }) - .then(() => { - target.setCustomValidity(formatMessage(messages.usernameTaken)); - return true; - }) - .catch(() => { - target.setCustomValidity(''); - }); - } else { - target.setCustomValidity(''); - } - }, - 500, - { leading: false, trailing: true }, - ), - ); - - Rails.delegate( - document, - '#user_password,#user_password_confirmation', - 'input', - () => { - const password = document.querySelector( - 'input#user_password', - ); - const confirmation = document.querySelector( - 'input#user_password_confirmation', - ); - if (!confirmation || !password) return; - - if ( - confirmation.value && - confirmation.value.length > password.maxLength - ) { - confirmation.setCustomValidity( - formatMessage(messages.passwordExceedsLength), - ); - } else if (password.value && password.value !== confirmation.value) { - confirmation.setCustomValidity( - formatMessage(messages.passwordDoesNotMatch), - ); - } else { - confirmation.setCustomValidity(''); - } - }, - ); -} - -Rails.delegate( - document, - '#edit_profile input[type=file]', - 'change', - ({ target }) => { - if (!(target instanceof HTMLInputElement)) return; - - const avatar = document.querySelector( - `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( - '.input-copy__wrapper input', - ); - - if (!input) return; - - navigator.clipboard - .writeText(input.value) - .then(() => { - const parent = target.parentElement; - - if (parent) { - parent.classList.add('copied'); - - setTimeout(() => { - parent.classList.remove('copied'); - }, 700); - } - - return true; - }) - .catch((error: unknown) => { - console.error(error); - }); -}); - -const toggleSidebar = () => { - const sidebar = document.querySelector('.sidebar ul'); - const toggleButton = document.querySelector( - 'a.sidebar__toggle__icon', - ); - - if (!sidebar || !toggleButton) return; - - if (sidebar.classList.contains('visible')) { - document.body.style.overflow = ''; - toggleButton.setAttribute('aria-expanded', 'false'); - } else { - document.body.style.overflow = 'hidden'; - toggleButton.setAttribute('aria-expanded', 'true'); - } - - toggleButton.classList.toggle('active'); - sidebar.classList.toggle('visible'); -}; - -Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => { - toggleSidebar(); -}); - -Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault(); - toggleSidebar(); - } -}); - -Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => { - if (target instanceof HTMLImageElement && target.dataset.original) - target.src = target.dataset.original; -}); -Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => { - if (target instanceof HTMLImageElement && target.dataset.static) - target.src = target.dataset.static; -}); - -const setInputDisabled = ( - input: HTMLInputElement | HTMLSelectElement, - disabled: boolean, -) => { - input.disabled = disabled; - - const wrapper = input.closest('.with_label'); - if (wrapper) { - wrapper.classList.toggle('disabled', input.disabled); - - const hidden = - input.type === 'checkbox' && - wrapper.querySelector('input[type=hidden][value="0"]'); - if (hidden) { - hidden.disabled = input.disabled; - } - } -}; - -Rails.delegate( - document, - '#account_statuses_cleanup_policy_enabled', - 'change', - ({ target }) => { - if (!(target instanceof HTMLInputElement) || !target.form) return; - - target.form - .querySelectorAll< - HTMLInputElement | HTMLSelectElement - >('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select') - .forEach((input) => { - setInputDisabled(input, !target.checked); - }); - }, -); - -// Empty the honeypot fields in JS in case something like an extension -// automatically filled them. -Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { - [ - 'user_website', - 'user_confirm_password', - 'registration_user_website', - 'registration_user_confirm_password', - ].forEach((id) => { - const field = document.querySelector(`input#${id}`); - if (field) { - field.value = ''; - } - }); -}); - -Rails.delegate(document, '.rules-list button', 'click', ({ target }) => { - if (!(target instanceof HTMLElement)) { - return; - } - - const button = target.closest('button'); - - if (!button) { - return; - } - - if (button.ariaExpanded === 'true') { - button.ariaExpanded = 'false'; - } else { - button.ariaExpanded = 'true'; - } -}); - -function main() { - ready(loaded).catch((error: unknown) => { - console.error(error); - }); -} - -loadPolyfills() - .then(loadLocale) - .then(main) - .then(loadKeyboardExtensions) - .catch((error: unknown) => { - console.error(error); - }); diff --git a/app/javascript/entrypoints/share.tsx b/app/javascript/entrypoints/share.tsx deleted file mode 100644 index 7926250851..0000000000 --- a/app/javascript/entrypoints/share.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import './public-path'; -import { createRoot } from 'react-dom/client'; - -import { start } from '../mastodon/common'; -import ComposeContainer from '../mastodon/containers/compose_container'; -import { loadPolyfills } from '../mastodon/polyfills'; -import ready from '../mastodon/ready'; - -start(); - -function loaded() { - const mountNode = document.getElementById('mastodon-compose'); - - if (mountNode) { - const attr = mountNode.getAttribute('data-props'); - - if (!attr) return; - - const props = JSON.parse(attr) as object; - const root = createRoot(mountNode); - - root.render(); - } -} - -function main() { - ready(loaded).catch((error: unknown) => { - console.error(error); - }); -} - -loadPolyfills() - .then(main) - .catch((error: unknown) => { - console.error(error); - }); diff --git a/app/javascript/entrypoints/sign_up.ts b/app/javascript/entrypoints/sign_up.ts deleted file mode 100644 index 880738fcb7..0000000000 --- a/app/javascript/entrypoints/sign_up.ts +++ /dev/null @@ -1,48 +0,0 @@ -import './public-path'; -import axios from 'axios'; - -import ready from '../mastodon/ready'; - -async function checkConfirmation() { - const response = await axios.get('/api/v1/emails/check_confirmation'); - - if (response.data) { - window.location.href = '/start'; - } -} - -ready(() => { - setInterval(() => { - void checkConfirmation(); - }, 5000); - - document - .querySelectorAll('button.timer-button') - .forEach((button) => { - let counter = 30; - - const container = document.createElement('span'); - - const updateCounter = () => { - container.innerText = ` (${counter})`; - }; - - updateCounter(); - - const countdown = setInterval(() => { - counter--; - - if (counter === 0) { - button.disabled = false; - button.removeChild(container); - clearInterval(countdown); - } else { - updateCounter(); - } - }, 1000); - - button.appendChild(container); - }); -}).catch((e: unknown) => { - throw e; -}); diff --git a/app/javascript/entrypoints/two_factor_authentication.ts b/app/javascript/entrypoints/two_factor_authentication.ts deleted file mode 100644 index 981481694b..0000000000 --- a/app/javascript/entrypoints/two_factor_authentication.ts +++ /dev/null @@ -1,197 +0,0 @@ -import * as WebAuthnJSON from '@github/webauthn-json'; -import axios, { AxiosError } from 'axios'; - -import ready from '../mastodon/ready'; - -import 'regenerator-runtime/runtime'; - -type PublicKeyCredentialCreationOptionsJSON = - WebAuthnJSON.CredentialCreationOptionsJSON['publicKey']; - -function exceptionHasAxiosError( - error: unknown, -): error is AxiosError<{ error: unknown }> { - return ( - error instanceof AxiosError && - typeof error.response?.data === 'object' && - 'error' in error.response.data - ); -} - -function logAxiosResponseError(error: unknown) { - if (exceptionHasAxiosError(error)) console.error(error); -} - -function getCSRFToken() { - return document - .querySelector('meta[name="csrf-token"]') - ?.getAttribute('content'); -} - -function hideFlashMessages() { - document.querySelectorAll('.flash-message').forEach((flashMessage) => { - flashMessage.classList.add('hidden'); - }); -} - -async function callback( - url: string, - body: - | { - credential: WebAuthnJSON.PublicKeyCredentialWithAttestationJSON; - nickname: string; - } - | { - user: { credential: WebAuthnJSON.PublicKeyCredentialWithAssertionJSON }; - }, -) { - try { - const response = await axios.post<{ redirect_path: string }>( - url, - JSON.stringify(body), - { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-CSRF-Token': getCSRFToken(), - }, - }, - ); - - window.location.replace(response.data.redirect_path); - } catch (error) { - if (error instanceof AxiosError && error.response?.status === 422) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage?.classList.remove('hidden'); - - logAxiosResponseError(error); - } else { - console.error(error); - } - } -} - -async function handleWebauthnCredentialRegistration(nickname: string) { - try { - const response = await axios.get( - '/settings/security_keys/options', - ); - - const credentialOptions = response.data; - - try { - const credential = await WebAuthnJSON.create({ - publicKey: credentialOptions, - }); - - const params = { - credential: credential, - nickname: nickname, - }; - - await callback('/settings/security_keys', params); - } catch (error) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage?.classList.remove('hidden'); - console.error(error); - } - } catch (error) { - logAxiosResponseError(error); - } -} - -async function handleWebauthnCredentialAuthentication() { - try { - const response = await axios.get( - 'sessions/security_key_options', - ); - - const credentialOptions = response.data; - - try { - const credential = await WebAuthnJSON.get({ - publicKey: credentialOptions, - }); - - const params = { user: { credential: credential } }; - void callback('sign_in', params); - } catch (error) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage?.classList.remove('hidden'); - console.error(error); - } - } catch (error) { - logAxiosResponseError(error); - } -} - -ready(() => { - if (!WebAuthnJSON.supported()) { - const unsupported_browser_message = document.getElementById( - 'unsupported-browser-message', - ); - if (unsupported_browser_message) { - unsupported_browser_message.classList.remove('hidden'); - const button = document.querySelector( - 'button.btn.js-webauthn', - ); - if (button) button.disabled = true; - } - } - - const webAuthnCredentialRegistrationForm = - document.querySelector('form#new_webauthn_credential'); - if (webAuthnCredentialRegistrationForm) { - webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => { - event.preventDefault(); - - if (!(event.target instanceof HTMLFormElement)) return; - - const nickname = event.target.querySelector( - 'input[name="new_webauthn_credential[nickname]"]', - ); - - if (nickname?.value) { - void handleWebauthnCredentialRegistration(nickname.value); - } else { - nickname?.focus(); - } - }); - } - - const webAuthnCredentialAuthenticationForm = - document.getElementById('webauthn-form'); - if (webAuthnCredentialAuthenticationForm) { - webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => { - event.preventDefault(); - void handleWebauthnCredentialAuthentication(); - }); - - const otpAuthenticationForm = document.getElementById( - 'otp-authentication-form', - ); - - const linkToOtp = document.getElementById('link-to-otp'); - - linkToOtp?.addEventListener('click', () => { - webAuthnCredentialAuthenticationForm.classList.add('hidden'); - otpAuthenticationForm?.classList.remove('hidden'); - hideFlashMessages(); - }); - - const linkToWebAuthn = document.getElementById('link-to-webauthn'); - linkToWebAuthn?.addEventListener('click', () => { - otpAuthenticationForm?.classList.add('hidden'); - webAuthnCredentialAuthenticationForm.classList.remove('hidden'); - hideFlashMessages(); - }); - } -}).catch((e: unknown) => { - throw e; -}); diff --git a/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 b/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 deleted file mode 100644 index e6345f2e3d..0000000000 Binary files a/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 and /dev/null differ diff --git a/app/javascript/mastodon/hooks/useHovering.ts b/app/javascript/hooks/useHovering.ts similarity index 100% rename from app/javascript/mastodon/hooks/useHovering.ts rename to app/javascript/hooks/useHovering.ts diff --git a/app/javascript/icons/android-chrome-144x144.png b/app/javascript/icons/android-chrome-144x144.png old mode 100644 new mode 100755 index 698fb4a260..d636e94c43 Binary files a/app/javascript/icons/android-chrome-144x144.png and b/app/javascript/icons/android-chrome-144x144.png differ diff --git a/app/javascript/icons/android-chrome-192x192.png b/app/javascript/icons/android-chrome-192x192.png old mode 100644 new mode 100755 index 2b6b632648..4a2681ffb9 Binary files a/app/javascript/icons/android-chrome-192x192.png and b/app/javascript/icons/android-chrome-192x192.png differ diff --git a/app/javascript/icons/android-chrome-256x256.png b/app/javascript/icons/android-chrome-256x256.png old mode 100644 new mode 100755 index 51e3849a26..8fab493ede Binary files a/app/javascript/icons/android-chrome-256x256.png and b/app/javascript/icons/android-chrome-256x256.png differ diff --git a/app/javascript/icons/android-chrome-36x36.png b/app/javascript/icons/android-chrome-36x36.png old mode 100644 new mode 100755 index 925f69c4fc..335d012db1 Binary files a/app/javascript/icons/android-chrome-36x36.png and b/app/javascript/icons/android-chrome-36x36.png differ diff --git a/app/javascript/icons/android-chrome-384x384.png b/app/javascript/icons/android-chrome-384x384.png old mode 100644 new mode 100755 index 9d256a83cb..02b1e6fced Binary files a/app/javascript/icons/android-chrome-384x384.png and b/app/javascript/icons/android-chrome-384x384.png differ diff --git a/app/javascript/icons/android-chrome-48x48.png b/app/javascript/icons/android-chrome-48x48.png old mode 100644 new mode 100755 index bcfe7475d0..43cf411b8c Binary files a/app/javascript/icons/android-chrome-48x48.png and b/app/javascript/icons/android-chrome-48x48.png differ diff --git a/app/javascript/icons/android-chrome-512x512.png b/app/javascript/icons/android-chrome-512x512.png old mode 100644 new mode 100755 index bffacfb699..1856b80c7c Binary files a/app/javascript/icons/android-chrome-512x512.png and b/app/javascript/icons/android-chrome-512x512.png differ diff --git a/app/javascript/icons/android-chrome-72x72.png b/app/javascript/icons/android-chrome-72x72.png old mode 100644 new mode 100755 index 16679d5731..335008bf85 Binary files a/app/javascript/icons/android-chrome-72x72.png and b/app/javascript/icons/android-chrome-72x72.png differ diff --git a/app/javascript/icons/android-chrome-96x96.png b/app/javascript/icons/android-chrome-96x96.png old mode 100644 new mode 100755 index 9ade87cf32..d1cb095822 Binary files a/app/javascript/icons/android-chrome-96x96.png and b/app/javascript/icons/android-chrome-96x96.png differ diff --git a/app/javascript/icons/apple-touch-icon-1024x1024.png b/app/javascript/icons/apple-touch-icon-1024x1024.png old mode 100644 new mode 100755 index 8ec371eb27..c2a2d516ef Binary files a/app/javascript/icons/apple-touch-icon-1024x1024.png and b/app/javascript/icons/apple-touch-icon-1024x1024.png differ diff --git a/app/javascript/icons/apple-touch-icon-114x114.png b/app/javascript/icons/apple-touch-icon-114x114.png old mode 100644 new mode 100755 index e1563f51e5..218b415439 Binary files a/app/javascript/icons/apple-touch-icon-114x114.png and b/app/javascript/icons/apple-touch-icon-114x114.png differ diff --git a/app/javascript/icons/apple-touch-icon-120x120.png b/app/javascript/icons/apple-touch-icon-120x120.png old mode 100644 new mode 100755 index e9a5f5b0e5..be53bc7c10 Binary files a/app/javascript/icons/apple-touch-icon-120x120.png and b/app/javascript/icons/apple-touch-icon-120x120.png differ diff --git a/app/javascript/icons/apple-touch-icon-144x144.png b/app/javascript/icons/apple-touch-icon-144x144.png old mode 100644 new mode 100755 index 698fb4a260..cbb055732f Binary files a/app/javascript/icons/apple-touch-icon-144x144.png and b/app/javascript/icons/apple-touch-icon-144x144.png differ diff --git a/app/javascript/icons/apple-touch-icon-152x152.png b/app/javascript/icons/apple-touch-icon-152x152.png old mode 100644 new mode 100755 index 0cc93cc288..3a7975c054 Binary files a/app/javascript/icons/apple-touch-icon-152x152.png and b/app/javascript/icons/apple-touch-icon-152x152.png differ diff --git a/app/javascript/icons/apple-touch-icon-167x167.png b/app/javascript/icons/apple-touch-icon-167x167.png old mode 100644 new mode 100755 index 9bbbf53120..25be4eb5f5 Binary files a/app/javascript/icons/apple-touch-icon-167x167.png and b/app/javascript/icons/apple-touch-icon-167x167.png differ diff --git a/app/javascript/icons/apple-touch-icon-180x180.png b/app/javascript/icons/apple-touch-icon-180x180.png old mode 100644 new mode 100755 index 329b803b91..dc0e9bc20b Binary files a/app/javascript/icons/apple-touch-icon-180x180.png and b/app/javascript/icons/apple-touch-icon-180x180.png differ diff --git a/app/javascript/icons/apple-touch-icon-192x192.png b/app/javascript/icons/apple-touch-icon-192x192.png deleted file mode 100644 index 2b6b632648..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-192x192.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-256x256.png b/app/javascript/icons/apple-touch-icon-256x256.png deleted file mode 100644 index 51e3849a26..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-256x256.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-36x36.png b/app/javascript/icons/apple-touch-icon-36x36.png deleted file mode 100644 index 925f69c4fc..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-36x36.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-384x384.png b/app/javascript/icons/apple-touch-icon-384x384.png deleted file mode 100644 index 9d256a83cb..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-384x384.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-48x48.png b/app/javascript/icons/apple-touch-icon-48x48.png deleted file mode 100644 index bcfe7475d0..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-48x48.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-512x512.png b/app/javascript/icons/apple-touch-icon-512x512.png deleted file mode 100644 index bffacfb699..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-512x512.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-57x57.png b/app/javascript/icons/apple-touch-icon-57x57.png old mode 100644 new mode 100755 index e00e142c64..bb0dc957cd Binary files a/app/javascript/icons/apple-touch-icon-57x57.png and b/app/javascript/icons/apple-touch-icon-57x57.png differ diff --git a/app/javascript/icons/apple-touch-icon-60x60.png b/app/javascript/icons/apple-touch-icon-60x60.png old mode 100644 new mode 100755 index 011285b564..9143a0bf07 Binary files a/app/javascript/icons/apple-touch-icon-60x60.png and b/app/javascript/icons/apple-touch-icon-60x60.png differ diff --git a/app/javascript/icons/apple-touch-icon-72x72.png b/app/javascript/icons/apple-touch-icon-72x72.png old mode 100644 new mode 100755 index 16679d5731..2b7d19484c Binary files a/app/javascript/icons/apple-touch-icon-72x72.png and b/app/javascript/icons/apple-touch-icon-72x72.png differ diff --git a/app/javascript/icons/apple-touch-icon-76x76.png b/app/javascript/icons/apple-touch-icon-76x76.png old mode 100644 new mode 100755 index 83c8748876..0985e33bcb Binary files a/app/javascript/icons/apple-touch-icon-76x76.png and b/app/javascript/icons/apple-touch-icon-76x76.png differ diff --git a/app/javascript/icons/apple-touch-icon-96x96.png b/app/javascript/icons/apple-touch-icon-96x96.png deleted file mode 100644 index 9ade87cf32..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-96x96.png and /dev/null differ diff --git a/app/javascript/icons/favicon-16x16.png b/app/javascript/icons/favicon-16x16.png old mode 100644 new mode 100755 index 7f865cfe96..1326ba0462 Binary files a/app/javascript/icons/favicon-16x16.png and b/app/javascript/icons/favicon-16x16.png differ diff --git a/app/javascript/icons/favicon-32x32.png b/app/javascript/icons/favicon-32x32.png old mode 100644 new mode 100755 index 7f865cfe96..f5058cb0a5 Binary files a/app/javascript/icons/favicon-32x32.png and b/app/javascript/icons/favicon-32x32.png differ diff --git a/app/javascript/icons/favicon-48x48.png b/app/javascript/icons/favicon-48x48.png old mode 100644 new mode 100755 index 7f865cfe96..6253d054c7 Binary files a/app/javascript/icons/favicon-48x48.png and b/app/javascript/icons/favicon-48x48.png differ diff --git a/app/javascript/images/archetypes/booster.png b/app/javascript/images/archetypes/booster.png deleted file mode 100755 index df2a0226f8..0000000000 Binary files a/app/javascript/images/archetypes/booster.png and /dev/null differ diff --git a/app/javascript/images/archetypes/lurker.png b/app/javascript/images/archetypes/lurker.png deleted file mode 100755 index e37f98aab2..0000000000 Binary files a/app/javascript/images/archetypes/lurker.png and /dev/null differ diff --git a/app/javascript/images/archetypes/oracle.png b/app/javascript/images/archetypes/oracle.png deleted file mode 100755 index 9d4e2177c5..0000000000 Binary files a/app/javascript/images/archetypes/oracle.png and /dev/null differ diff --git a/app/javascript/images/archetypes/pollster.png b/app/javascript/images/archetypes/pollster.png deleted file mode 100755 index 9fe6281af0..0000000000 Binary files a/app/javascript/images/archetypes/pollster.png and /dev/null differ diff --git a/app/javascript/images/archetypes/replier.png b/app/javascript/images/archetypes/replier.png deleted file mode 100755 index 6c6325b9f1..0000000000 Binary files a/app/javascript/images/archetypes/replier.png and /dev/null differ diff --git a/app/javascript/images/check.svg b/app/javascript/images/check.svg deleted file mode 100644 index 8a0ebe878d..0000000000 --- a/app/javascript/images/check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/app/javascript/images/filter-stripes.svg b/app/javascript/images/filter-stripes.svg deleted file mode 100755 index 4c1b58cb74..0000000000 --- a/app/javascript/images/filter-stripes.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/javascript/images/logo_full.svg b/app/javascript/images/logo_full.svg new file mode 100644 index 0000000000..03bcf93e39 --- /dev/null +++ b/app/javascript/images/logo_full.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/logo_transparent.svg b/app/javascript/images/logo_transparent.svg new file mode 100644 index 0000000000..a1e7b403e0 --- /dev/null +++ b/app/javascript/images/logo_transparent.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/mailer-new/common/header-bg-end.png b/app/javascript/images/mailer-new/common/header-bg-end.png deleted file mode 100644 index 900196678a..0000000000 Binary files a/app/javascript/images/mailer-new/common/header-bg-end.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/common/header-bg-start.png b/app/javascript/images/mailer-new/common/header-bg-start.png deleted file mode 100644 index 0037c1ad93..0000000000 Binary files a/app/javascript/images/mailer-new/common/header-bg-start.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/common/logo-footer.png b/app/javascript/images/mailer-new/common/logo-footer.png deleted file mode 100644 index 2baafd8d7f..0000000000 Binary files a/app/javascript/images/mailer-new/common/logo-footer.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/common/logo-header.png b/app/javascript/images/mailer-new/common/logo-header.png deleted file mode 100644 index 46a6bddaa1..0000000000 Binary files a/app/javascript/images/mailer-new/common/logo-header.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/2fa-disabled.png b/app/javascript/images/mailer-new/heading/2fa-disabled.png deleted file mode 100644 index b1e342a87c..0000000000 Binary files a/app/javascript/images/mailer-new/heading/2fa-disabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/2fa-enabled.png b/app/javascript/images/mailer-new/heading/2fa-enabled.png deleted file mode 100644 index 3ce3e04f84..0000000000 Binary files a/app/javascript/images/mailer-new/heading/2fa-enabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/2fa-recovery.png b/app/javascript/images/mailer-new/heading/2fa-recovery.png deleted file mode 100644 index cefb21e1eb..0000000000 Binary files a/app/javascript/images/mailer-new/heading/2fa-recovery.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/LICENSE b/app/javascript/images/mailer-new/heading/LICENSE deleted file mode 100644 index 974db1ac4b..0000000000 --- a/app/javascript/images/mailer-new/heading/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020-2024 PaweÅ‚ Kuna - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/app/javascript/images/mailer-new/heading/README.md b/app/javascript/images/mailer-new/heading/README.md deleted file mode 100644 index ecd4b949e7..0000000000 --- a/app/javascript/images/mailer-new/heading/README.md +++ /dev/null @@ -1 +0,0 @@ -Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). diff --git a/app/javascript/images/mailer-new/heading/appeal-approved.png b/app/javascript/images/mailer-new/heading/appeal-approved.png deleted file mode 100755 index b2476ec346..0000000000 Binary files a/app/javascript/images/mailer-new/heading/appeal-approved.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/appeal-rejected.png b/app/javascript/images/mailer-new/heading/appeal-rejected.png deleted file mode 100644 index 7ae38ad0c1..0000000000 Binary files a/app/javascript/images/mailer-new/heading/appeal-rejected.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/archive.png b/app/javascript/images/mailer-new/heading/archive.png deleted file mode 100644 index b0c7fad84d..0000000000 Binary files a/app/javascript/images/mailer-new/heading/archive.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/boost.png b/app/javascript/images/mailer-new/heading/boost.png deleted file mode 100644 index e33b759976..0000000000 Binary files a/app/javascript/images/mailer-new/heading/boost.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/email.png b/app/javascript/images/mailer-new/heading/email.png deleted file mode 100644 index c922c5239e..0000000000 Binary files a/app/javascript/images/mailer-new/heading/email.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/favorite.png b/app/javascript/images/mailer-new/heading/favorite.png deleted file mode 100644 index 0e483ee9b2..0000000000 Binary files a/app/javascript/images/mailer-new/heading/favorite.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/follow.png b/app/javascript/images/mailer-new/heading/follow.png deleted file mode 100644 index ff5b7e0042..0000000000 Binary files a/app/javascript/images/mailer-new/heading/follow.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-added.png b/app/javascript/images/mailer-new/heading/key-added.png deleted file mode 100755 index 82dcd464bf..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-added.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-deleted.png b/app/javascript/images/mailer-new/heading/key-deleted.png deleted file mode 100755 index 2930f591a0..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-deleted.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-disabled.png b/app/javascript/images/mailer-new/heading/key-disabled.png deleted file mode 100755 index e0f259359a..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-disabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-enabled.png b/app/javascript/images/mailer-new/heading/key-enabled.png deleted file mode 100644 index b2476ec346..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-enabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/login.png b/app/javascript/images/mailer-new/heading/login.png deleted file mode 100644 index 89a6e9ee33..0000000000 Binary files a/app/javascript/images/mailer-new/heading/login.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/mention.png b/app/javascript/images/mailer-new/heading/mention.png deleted file mode 100644 index c4dccff8ef..0000000000 Binary files a/app/javascript/images/mailer-new/heading/mention.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/password.png b/app/javascript/images/mailer-new/heading/password.png deleted file mode 100755 index 552c7c0687..0000000000 Binary files a/app/javascript/images/mailer-new/heading/password.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/user.png b/app/javascript/images/mailer-new/heading/user.png deleted file mode 100644 index f1dd58a18d..0000000000 Binary files a/app/javascript/images/mailer-new/heading/user.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/warning.png b/app/javascript/images/mailer-new/heading/warning.png deleted file mode 100755 index 7764837abe..0000000000 Binary files a/app/javascript/images/mailer-new/heading/warning.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/store-icons/btn-app-store.png b/app/javascript/images/mailer-new/store-icons/btn-app-store.png deleted file mode 100644 index ee3bd9385c..0000000000 Binary files a/app/javascript/images/mailer-new/store-icons/btn-app-store.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/store-icons/btn-google-play.png b/app/javascript/images/mailer-new/store-icons/btn-google-play.png deleted file mode 100644 index ed43ff29aa..0000000000 Binary files a/app/javascript/images/mailer-new/store-icons/btn-google-play.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/LICENSE b/app/javascript/images/mailer-new/welcome-icons/LICENSE deleted file mode 100644 index 974db1ac4b..0000000000 --- a/app/javascript/images/mailer-new/welcome-icons/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020-2024 PaweÅ‚ Kuna - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/app/javascript/images/mailer-new/welcome-icons/README.md b/app/javascript/images/mailer-new/welcome-icons/README.md deleted file mode 100644 index ecd4b949e7..0000000000 --- a/app/javascript/images/mailer-new/welcome-icons/README.md +++ /dev/null @@ -1 +0,0 @@ -Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). diff --git a/app/javascript/images/mailer-new/welcome-icons/apps_step-off.png b/app/javascript/images/mailer-new/welcome-icons/apps_step-off.png deleted file mode 100755 index ca270f5478..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/apps_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/apps_step-on.png b/app/javascript/images/mailer-new/welcome-icons/apps_step-on.png deleted file mode 100644 index fd631bf97e..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/apps_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-off.png b/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-off.png deleted file mode 100644 index dfcdd04e16..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-on.png b/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-on.png deleted file mode 100644 index c3776d17df..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/follow_step-off.png b/app/javascript/images/mailer-new/welcome-icons/follow_step-off.png deleted file mode 100755 index a262454d2d..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/follow_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/follow_step-on.png b/app/javascript/images/mailer-new/welcome-icons/follow_step-on.png deleted file mode 100644 index 3ac011539b..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/follow_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/post_step-off.png b/app/javascript/images/mailer-new/welcome-icons/post_step-off.png deleted file mode 100755 index 972de65a56..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/post_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/post_step-on.png b/app/javascript/images/mailer-new/welcome-icons/post_step-on.png deleted file mode 100644 index aa318e66c8..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/post_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/share_step-off.png b/app/javascript/images/mailer-new/welcome-icons/share_step-off.png deleted file mode 100755 index f45e9a2c9a..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/share_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/share_step-on.png b/app/javascript/images/mailer-new/welcome-icons/share_step-on.png deleted file mode 100644 index 98782d9317..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/share_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/checkbox-off.png b/app/javascript/images/mailer-new/welcome/checkbox-off.png deleted file mode 100644 index 51c190efe6..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/checkbox-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/checkbox-on.png b/app/javascript/images/mailer-new/welcome/checkbox-on.png deleted file mode 100644 index 162095e7df..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/checkbox-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_audience.png b/app/javascript/images/mailer-new/welcome/feature_audience.png deleted file mode 100644 index 902de133b4..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_audience.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_control.png b/app/javascript/images/mailer-new/welcome/feature_control.png deleted file mode 100644 index 1afb6c238c..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_control.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_creativity.png b/app/javascript/images/mailer-new/welcome/feature_creativity.png deleted file mode 100644 index 3365856699..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_creativity.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_moderation.png b/app/javascript/images/mailer-new/welcome/feature_moderation.png deleted file mode 100644 index 7cee9b29b8..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_moderation.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/purple-extra-soft-spacer.png b/app/javascript/images/mailer-new/welcome/purple-extra-soft-spacer.png deleted file mode 100644 index ec1ad5c957..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/purple-extra-soft-spacer.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/purple-extra-soft-wave.png b/app/javascript/images/mailer-new/welcome/purple-extra-soft-wave.png deleted file mode 100644 index ba8f6dd3d9..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/purple-extra-soft-wave.png and /dev/null differ diff --git a/app/javascript/images/quote-stripes.svg b/app/javascript/images/quote-stripes.svg deleted file mode 100755 index 1234d4d0a6..0000000000 --- a/app/javascript/images/quote-stripes.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/app/javascript/images/quote.svg b/app/javascript/images/quote.svg deleted file mode 100644 index ae6fbbe04a..0000000000 --- a/app/javascript/images/quote.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png new file mode 100644 index 0000000000..a724ac0bcd Binary files /dev/null and b/app/javascript/images/reticle.png differ diff --git a/app/javascript/images/warning-stripes.svg b/app/javascript/images/warning-stripes.svg deleted file mode 100755 index 9d68acdada..0000000000 --- a/app/javascript/images/warning-stripes.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/javascript/mastodon/actions/account_notes.ts b/app/javascript/mastodon/actions/account_notes.ts index 9e7d199dc9..eeef23e366 100644 --- a/app/javascript/mastodon/actions/account_notes.ts +++ b/app/javascript/mastodon/actions/account_notes.ts @@ -1,9 +1,18 @@ -import { apiSubmitAccountNote } from 'mastodon/api/accounts'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; +import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; -export const submitAccountNote = createDataLoadingThunk( +import api from '../api'; + +export const submitAccountNote = createAppAsyncThunk( 'account_note/submit', - ({ accountId, note }: { accountId: string; note: string }) => - apiSubmitAccountNote(accountId, note), - (relationship) => ({ relationship }), + async (args: { id: string; value: string }, { getState }) => { + // TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged + const response = await api(getState).post( + `/api/v1/accounts/${args.id}/note`, + { + comment: args.value, + }, + ); + + return { relationship: response.data }; + }, ); diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index d821381ce0..3a85393d6c 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -1,18 +1,5 @@ -import { browserHistory } from 'mastodon/components/router'; -import { debounceWithDispatchAndArguments } from 'mastodon/utils/debounce'; - import api, { getLinks } from '../api'; -import { - followAccountSuccess, unfollowAccountSuccess, - authorizeFollowRequestSuccess, rejectFollowRequestSuccess, - followAccountRequest, followAccountFail, - unfollowAccountRequest, unfollowAccountFail, - muteAccountSuccess, unmuteAccountSuccess, - blockAccountSuccess, unblockAccountSuccess, - pinAccountSuccess, unpinAccountSuccess, - fetchRelationshipsSuccess, -} from './accounts_typed'; import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; @@ -23,22 +10,36 @@ export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST'; export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS'; export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; +export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; +export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; +export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; + +export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; +export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; +export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL'; + export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; +export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; +export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; +export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; +export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST'; +export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS'; export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL'; export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; +export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; @@ -58,6 +59,7 @@ export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL'; export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; +export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS'; export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL'; export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; @@ -69,21 +71,21 @@ 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 ACCOUNT_REVEAL = 'ACCOUNT_REVEAL'; -export * from './accounts_typed'; - export function fetchAccount(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchRelationships([id])); dispatch(fetchAccountRequest(id)); - api().get(`/api/v1/accounts/${id}`).then(response => { + api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(importFetchedAccount(response.data)); dispatch(fetchAccountSuccess()); }).catch(error => { @@ -92,10 +94,10 @@ export function fetchAccount(id) { }; } -export const lookupAccount = acct => (dispatch) => { +export const lookupAccount = acct => (dispatch, getState) => { dispatch(lookupAccountRequest(acct)); - api().get('/api/v1/accounts/lookup', { params: { acct } }).then(response => { + api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => { dispatch(fetchRelationships([response.data.id])); dispatch(importFetchedAccount(response.data)); dispatch(lookupAccountSuccess()); @@ -142,24 +144,17 @@ export function fetchAccountFail(id, error) { }; } -/** - * @param {string} id - * @param {Object} options - * @param {boolean} [options.reblogs] - * @param {boolean} [options.notify] - * @returns {function(): void} - */ export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); - dispatch(followAccountRequest({ id, locked })); + dispatch(followAccountRequest(id, locked)); - api().post(`/api/v1/accounts/${id}/follow`, options).then(response => { - dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing})); + api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { + dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { - dispatch(followAccountFail({ id, error, locked })); + dispatch(followAccountFail(error, locked)); }); }; } @@ -168,35 +163,87 @@ export function unfollowAccount(id) { return (dispatch, getState) => { dispatch(unfollowAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/unfollow`).then(response => { - dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')})); + api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { + dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { - dispatch(unfollowAccountFail({ id, error })); + dispatch(unfollowAccountFail(error)); }); }; } +export function followAccountRequest(id, locked) { + return { + type: ACCOUNT_FOLLOW_REQUEST, + id, + locked, + skipLoading: true, + }; +} + +export function followAccountSuccess(relationship, alreadyFollowing) { + return { + type: ACCOUNT_FOLLOW_SUCCESS, + relationship, + alreadyFollowing, + skipLoading: true, + }; +} + +export function followAccountFail(error, locked) { + return { + type: ACCOUNT_FOLLOW_FAIL, + error, + locked, + skipLoading: true, + }; +} + +export function unfollowAccountRequest(id) { + return { + type: ACCOUNT_UNFOLLOW_REQUEST, + id, + skipLoading: true, + }; +} + +export function unfollowAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_UNFOLLOW_SUCCESS, + relationship, + statuses, + skipLoading: true, + }; +} + +export function unfollowAccountFail(error) { + return { + type: ACCOUNT_UNFOLLOW_FAIL, + error, + skipLoading: true, + }; +} + export function blockAccount(id) { return (dispatch, getState) => { dispatch(blockAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/block`).then(response => { + api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') })); + dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { - dispatch(blockAccountFail({ id, error })); + dispatch(blockAccountFail(id, error)); }); }; } export function unblockAccount(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unblockAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/unblock`).then(response => { - dispatch(unblockAccountSuccess({ relationship: response.data })); + api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { + dispatch(unblockAccountSuccess(response.data)); }).catch(error => { - dispatch(unblockAccountFail({ id, error })); + dispatch(unblockAccountFail(id, error)); }); }; } @@ -207,6 +254,15 @@ export function blockAccountRequest(id) { id, }; } + +export function blockAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_BLOCK_SUCCESS, + relationship, + statuses, + }; +} + export function blockAccountFail(error) { return { type: ACCOUNT_BLOCK_FAIL, @@ -221,6 +277,13 @@ export function unblockAccountRequest(id) { }; } +export function unblockAccountSuccess(relationship) { + return { + type: ACCOUNT_UNBLOCK_SUCCESS, + relationship, + }; +} + export function unblockAccountFail(error) { return { type: ACCOUNT_UNBLOCK_FAIL, @@ -233,23 +296,23 @@ export function muteAccount(id, notifications, duration=0) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') })); + dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { - dispatch(muteAccountFail({ id, error })); + dispatch(muteAccountFail(id, error)); }); }; } export function unmuteAccount(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unmuteAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/unmute`).then(response => { - dispatch(unmuteAccountSuccess({ relationship: response.data })); + api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { + dispatch(unmuteAccountSuccess(response.data)); }).catch(error => { - dispatch(unmuteAccountFail({ id, error })); + dispatch(unmuteAccountFail(id, error)); }); }; } @@ -261,6 +324,14 @@ export function muteAccountRequest(id) { }; } +export function muteAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_MUTE_SUCCESS, + relationship, + statuses, + }; +} + export function muteAccountFail(error) { return { type: ACCOUNT_MUTE_FAIL, @@ -275,6 +346,13 @@ export function unmuteAccountRequest(id) { }; } +export function unmuteAccountSuccess(relationship) { + return { + type: ACCOUNT_UNMUTE_SUCCESS, + relationship, + }; +} + export function unmuteAccountFail(error) { return { type: ACCOUNT_UNMUTE_FAIL, @@ -284,10 +362,10 @@ export function unmuteAccountFail(error) { export function fetchFollowers(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchFollowersRequest(id)); - api().get(`/api/v1/accounts/${id}/followers`).then(response => { + api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -334,7 +412,7 @@ export function expandFollowers(id) { dispatch(expandFollowersRequest(id)); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -371,10 +449,10 @@ export function expandFollowersFail(id, error) { } export function fetchFollowing(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchFollowingRequest(id)); - api().get(`/api/v1/accounts/${id}/following`).then(response => { + api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -421,7 +499,7 @@ export function expandFollowing(id) { dispatch(expandFollowingRequest(id)); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -457,20 +535,6 @@ export function expandFollowingFail(id, error) { }; } -const debouncedFetchRelationships = debounceWithDispatchAndArguments((dispatch, ...newAccountIds) => { - if (newAccountIds.length === 0) { - return; - } - - dispatch(fetchRelationshipsRequest(newAccountIds)); - - api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { - dispatch(fetchRelationshipsSuccess({ relationships: response.data })); - }).catch(error => { - dispatch(fetchRelationshipsFail(error)); - }); -}, { delay: 500 }); - export function fetchRelationships(accountIds) { return (dispatch, getState) => { const state = getState(); @@ -482,7 +546,13 @@ export function fetchRelationships(accountIds) { return; } - debouncedFetchRelationships(dispatch, ...newAccountIds); + dispatch(fetchRelationshipsRequest(newAccountIds)); + + api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { + dispatch(fetchRelationshipsSuccess(response.data)); + }).catch(error => { + dispatch(fetchRelationshipsFail(error)); + }); }; } @@ -494,6 +564,14 @@ export function fetchRelationshipsRequest(ids) { }; } +export function fetchRelationshipsSuccess(relationships) { + return { + type: RELATIONSHIPS_FETCH_SUCCESS, + relationships, + skipLoading: true, + }; +} + export function fetchRelationshipsFail(error) { return { type: RELATIONSHIPS_FETCH_FAIL, @@ -504,10 +582,10 @@ export function fetchRelationshipsFail(error) { } export function fetchFollowRequests() { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchFollowRequestsRequest()); - api().get('/api/v1/follow_requests').then(response => { + api(getState).get('/api/v1/follow_requests').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null)); @@ -546,7 +624,7 @@ export function expandFollowRequests() { dispatch(expandFollowRequestsRequest()); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null)); @@ -576,12 +654,12 @@ export function expandFollowRequestsFail(error) { } export function authorizeFollowRequest(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(authorizeFollowRequestRequest(id)); - api() + api(getState) .post(`/api/v1/follow_requests/${id}/authorize`) - .then(() => dispatch(authorizeFollowRequestSuccess({ id }))) + .then(() => dispatch(authorizeFollowRequestSuccess(id))) .catch(error => dispatch(authorizeFollowRequestFail(id, error))); }; } @@ -593,6 +671,13 @@ export function authorizeFollowRequestRequest(id) { }; } +export function authorizeFollowRequestSuccess(id) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + id, + }; +} + export function authorizeFollowRequestFail(id, error) { return { type: FOLLOW_REQUEST_AUTHORIZE_FAIL, @@ -603,12 +688,12 @@ export function authorizeFollowRequestFail(id, error) { export function rejectFollowRequest(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(rejectFollowRequestRequest(id)); - api() + api(getState) .post(`/api/v1/follow_requests/${id}/reject`) - .then(() => dispatch(rejectFollowRequestSuccess({ id }))) + .then(() => dispatch(rejectFollowRequestSuccess(id))) .catch(error => dispatch(rejectFollowRequestFail(id, error))); }; } @@ -620,6 +705,13 @@ export function rejectFollowRequestRequest(id) { }; } +export function rejectFollowRequestSuccess(id) { + return { + type: FOLLOW_REQUEST_REJECT_SUCCESS, + id, + }; +} + export function rejectFollowRequestFail(id, error) { return { type: FOLLOW_REQUEST_REJECT_FAIL, @@ -629,11 +721,11 @@ export function rejectFollowRequestFail(id, error) { } export function pinAccount(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(pinAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/pin`).then(response => { - dispatch(pinAccountSuccess({ relationship: response.data })); + api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => { + dispatch(pinAccountSuccess(response.data)); }).catch(error => { dispatch(pinAccountFail(error)); }); @@ -641,11 +733,11 @@ export function pinAccount(id) { } export function unpinAccount(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unpinAccountRequest(id)); - api().post(`/api/v1/accounts/${id}/unpin`).then(response => { - dispatch(unpinAccountSuccess({ relationship: response.data })); + api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => { + dispatch(unpinAccountSuccess(response.data)); }).catch(error => { dispatch(unpinAccountFail(error)); }); @@ -659,6 +751,13 @@ export function pinAccountRequest(id) { }; } +export function pinAccountSuccess(relationship) { + return { + type: ACCOUNT_PIN_SUCCESS, + relationship, + }; +} + export function pinAccountFail(error) { return { type: ACCOUNT_PIN_FAIL, @@ -673,6 +772,13 @@ export function unpinAccountRequest(id) { }; } +export function unpinAccountSuccess(relationship) { + return { + type: ACCOUNT_UNPIN_SUCCESS, + relationship, + }; +} + export function unpinAccountFail(error) { return { type: ACCOUNT_UNPIN_FAIL, @@ -680,27 +786,7 @@ export function unpinAccountFail(error) { }; } -export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch) => { - const data = new FormData(); - - data.append('display_name', displayName); - data.append('note', note); - if (avatar) data.append('avatar', avatar); - if (header) data.append('header', header); - data.append('discoverable', discoverable); - data.append('indexable', indexable); - - return api().patch('/api/v1/accounts/update_credentials', data).then(response => { - dispatch(importFetchedAccount(response.data)); - }); -}; - -export const navigateToProfile = (accountId) => { - return (_dispatch, getState) => { - const acct = getState().accounts.getIn([accountId, 'acct']); - - if (acct) { - browserHistory.push(`/@${acct}`); - } - }; -}; +export const revealAccount = id => ({ + type: ACCOUNT_REVEAL, + id, +}); diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts deleted file mode 100644 index 058a68a099..0000000000 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; - -export const revealAccount = createAction<{ - id: string; -}>('accounts/revealAccount'); - -export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>( - 'accounts/importAccounts', -); - -function actionWithSkipLoadingTrue(args: Args) { - return { - payload: { - ...args, - skipLoading: true, - }, - }; -} - -export const followAccountSuccess = createAction( - 'accounts/followAccount/SUCCESS', - actionWithSkipLoadingTrue<{ - relationship: ApiRelationshipJSON; - alreadyFollowing: boolean; - }>, -); - -export const unfollowAccountSuccess = createAction( - 'accounts/unfollowAccount/SUCCESS', - actionWithSkipLoadingTrue<{ - relationship: ApiRelationshipJSON; - statuses: unknown; - alreadyFollowing?: boolean; - }>, -); - -export const authorizeFollowRequestSuccess = createAction<{ id: string }>( - 'accounts/followRequestAuthorize/SUCCESS', -); - -export const rejectFollowRequestSuccess = createAction<{ id: string }>( - 'accounts/followRequestReject/SUCCESS', -); - -export const followAccountRequest = createAction( - 'accounts/follow/REQUEST', - actionWithSkipLoadingTrue<{ id: string; locked: boolean }>, -); - -export const followAccountFail = createAction( - 'accounts/follow/FAIL', - actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>, -); - -export const unfollowAccountRequest = createAction( - 'accounts/unfollow/REQUEST', - actionWithSkipLoadingTrue<{ id: string }>, -); - -export const unfollowAccountFail = createAction( - 'accounts/unfollow/FAIL', - actionWithSkipLoadingTrue<{ id: string; error: string }>, -); - -export const blockAccountSuccess = createAction<{ - relationship: ApiRelationshipJSON; - statuses: unknown; -}>('accounts/block/SUCCESS'); - -export const unblockAccountSuccess = createAction<{ - relationship: ApiRelationshipJSON; -}>('accounts/unblock/SUCCESS'); - -export const muteAccountSuccess = createAction<{ - relationship: ApiRelationshipJSON; - statuses: unknown; -}>('accounts/mute/SUCCESS'); - -export const unmuteAccountSuccess = createAction<{ - relationship: ApiRelationshipJSON; -}>('accounts/unmute/SUCCESS'); - -export const pinAccountSuccess = createAction<{ - relationship: ApiRelationshipJSON; -}>('accounts/pin/SUCCESS'); - -export const unpinAccountSuccess = createAction<{ - relationship: ApiRelationshipJSON; -}>('accounts/unpin/SUCCESS'); - -export const fetchRelationshipsSuccess = createAction( - 'relationships/fetch/SUCCESS', - actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>, -); diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js new file mode 100644 index 0000000000..051a9675b3 --- /dev/null +++ b/app/javascript/mastodon/actions/alerts.js @@ -0,0 +1,59 @@ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, + rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' }, + rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' }, +}); + +export const ALERT_SHOW = 'ALERT_SHOW'; +export const ALERT_DISMISS = 'ALERT_DISMISS'; +export const ALERT_CLEAR = 'ALERT_CLEAR'; +export const ALERT_NOOP = 'ALERT_NOOP'; + +export const dismissAlert = alert => ({ + type: ALERT_DISMISS, + alert, +}); + +export const clearAlert = () => ({ + type: ALERT_CLEAR, +}); + +export const showAlert = alert => ({ + type: ALERT_SHOW, + alert, +}); + +export const showAlertForError = (error, skipNotFound = false) => { + if (error.response) { + const { data, status, statusText, headers } = error.response; + + // Skip these errors as they are reflected in the UI + if (skipNotFound && (status === 404 || status === 410)) { + return { type: ALERT_NOOP }; + } + + // Rate limit errors + if (status === 429 && headers['x-ratelimit-reset']) { + return showAlert({ + title: messages.rateLimitedTitle, + message: messages.rateLimitedMessage, + values: { 'retry_time': new Date(headers['x-ratelimit-reset']) }, + }); + } + + return showAlert({ + title: `${status}`, + message: data.error || statusText, + }); + } + + console.error(error); + + return showAlert({ + title: messages.unexpectedTitle, + message: messages.unexpectedMessage, + }); +} diff --git a/app/javascript/mastodon/actions/alerts.ts b/app/javascript/mastodon/actions/alerts.ts deleted file mode 100644 index 4fd293e252..0000000000 --- a/app/javascript/mastodon/actions/alerts.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { defineMessages } from 'react-intl'; - -import { createAction } from '@reduxjs/toolkit'; - -import { AxiosError } from 'axios'; -import type { AxiosResponse } from 'axios'; - -import type { Alert } from 'mastodon/models/alert'; - -interface ApiErrorResponse { - error?: string; -} - -const messages = defineMessages({ - unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, - unexpectedMessage: { - id: 'alert.unexpected.message', - defaultMessage: 'An unexpected error occurred.', - }, - rateLimitedTitle: { - id: 'alert.rate_limited.title', - defaultMessage: 'Rate limited', - }, - rateLimitedMessage: { - id: 'alert.rate_limited.message', - defaultMessage: 'Please retry after {retry_time, time, medium}.', - }, -}); - -export const dismissAlert = createAction<{ key: number }>('alerts/dismiss'); - -export const clearAlerts = createAction('alerts/clear'); - -export const showAlert = createAction>('alerts/show'); - -const ignoreAlert = createAction('alerts/ignore'); - -export const showAlertForError = (error: unknown, skipNotFound = false) => { - if (error instanceof AxiosError && error.response) { - const { status, statusText, headers } = error.response; - const { data } = error.response as AxiosResponse; - - // Skip these errors as they are reflected in the UI - if (skipNotFound && (status === 404 || status === 410)) { - return ignoreAlert(); - } - - // Rate limit errors - if (status === 429 && headers['x-ratelimit-reset']) { - return showAlert({ - title: messages.rateLimitedTitle, - message: messages.rateLimitedMessage, - values: { - retry_time: new Date(headers['x-ratelimit-reset'] as string), - }, - }); - } - - return showAlert({ - title: `${status}`, - message: data.error ?? statusText, - }); - } - - // An aborted request, e.g. due to reloading the browser window, is not really an error - if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) { - return ignoreAlert(); - } - - console.error(error); - - return showAlert({ - title: messages.unexpectedTitle, - message: messages.unexpectedMessage, - }); -}; diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js index 7657b05dc4..339c5f3adc 100644 --- a/app/javascript/mastodon/actions/announcements.js +++ b/app/javascript/mastodon/actions/announcements.js @@ -26,10 +26,10 @@ export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW'; const noOp = () => {}; -export const fetchAnnouncements = (done = noOp) => (dispatch) => { +export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => { dispatch(fetchAnnouncementsRequest()); - api().get('/api/v1/announcements').then(response => { + api(getState).get('/api/v1/announcements').then(response => { dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x)))); }).catch(error => { dispatch(fetchAnnouncementsFail(error)); @@ -61,10 +61,10 @@ export const updateAnnouncements = announcement => ({ announcement: normalizeAnnouncement(announcement), }); -export const dismissAnnouncement = announcementId => (dispatch) => { +export const dismissAnnouncement = announcementId => (dispatch, getState) => { dispatch(dismissAnnouncementRequest(announcementId)); - api().post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => { + api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => { dispatch(dismissAnnouncementSuccess(announcementId)); }).catch(error => { dispatch(dismissAnnouncementFail(announcementId, error)); @@ -103,7 +103,7 @@ export const addReaction = (announcementId, name) => (dispatch, getState) => { dispatch(addReactionRequest(announcementId, name, alreadyAdded)); } - api().put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => { + api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => { dispatch(addReactionSuccess(announcementId, name, alreadyAdded)); }).catch(err => { if (!alreadyAdded) { @@ -134,10 +134,10 @@ export const addReactionFail = (announcementId, name, error) => ({ skipLoading: true, }); -export const removeReaction = (announcementId, name) => (dispatch) => { +export const removeReaction = (announcementId, name) => (dispatch, getState) => { dispatch(removeReactionRequest(announcementId, name)); - api().delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => { + api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => { dispatch(removeReactionSuccess(announcementId, name)); }).catch(err => { dispatch(removeReactionFail(announcementId, name, err)); diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js index 30dc2d42c6..4716897586 100644 --- a/app/javascript/mastodon/actions/antennas.js +++ b/app/javascript/mastodon/actions/antennas.js @@ -1,5 +1,8 @@ import api from '../api'; +import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; + export const ANTENNA_FETCH_REQUEST = 'ANTENNA_FETCH_REQUEST'; export const ANTENNA_FETCH_SUCCESS = 'ANTENNA_FETCH_SUCCESS'; export const ANTENNA_FETCH_FAIL = 'ANTENNA_FETCH_FAIL'; @@ -8,10 +11,121 @@ export const ANTENNAS_FETCH_REQUEST = 'ANTENNAS_FETCH_REQUEST'; export const ANTENNAS_FETCH_SUCCESS = 'ANTENNAS_FETCH_SUCCESS'; export const ANTENNAS_FETCH_FAIL = 'ANTENNAS_FETCH_FAIL'; +export const ANTENNA_EDITOR_TITLE_CHANGE = 'ANTENNA_EDITOR_TITLE_CHANGE'; +export const ANTENNA_EDITOR_RESET = 'ANTENNA_EDITOR_RESET'; +export const ANTENNA_EDITOR_SETUP = 'ANTENNA_EDITOR_SETUP'; + +export const ANTENNA_CREATE_REQUEST = 'ANTENNA_CREATE_REQUEST'; +export const ANTENNA_CREATE_SUCCESS = 'ANTENNA_CREATE_SUCCESS'; +export const ANTENNA_CREATE_FAIL = 'ANTENNA_CREATE_FAIL'; + +export const ANTENNA_UPDATE_REQUEST = 'ANTENNA_UPDATE_REQUEST'; +export const ANTENNA_UPDATE_SUCCESS = 'ANTENNA_UPDATE_SUCCESS'; +export const ANTENNA_UPDATE_FAIL = 'ANTENNA_UPDATE_FAIL'; + export const ANTENNA_DELETE_REQUEST = 'ANTENNA_DELETE_REQUEST'; export const ANTENNA_DELETE_SUCCESS = 'ANTENNA_DELETE_SUCCESS'; export const ANTENNA_DELETE_FAIL = 'ANTENNA_DELETE_FAIL'; +export const ANTENNA_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_ACCOUNTS_FETCH_REQUEST'; +export const ANTENNA_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_ACCOUNTS_FETCH_SUCCESS'; +export const ANTENNA_ACCOUNTS_FETCH_FAIL = 'ANTENNA_ACCOUNTS_FETCH_FAIL'; + +export const ANTENNA_EDITOR_SUGGESTIONS_CHANGE = 'ANTENNA_EDITOR_SUGGESTIONS_CHANGE'; +export const ANTENNA_EDITOR_SUGGESTIONS_READY = 'ANTENNA_EDITOR_SUGGESTIONS_READY'; +export const ANTENNA_EDITOR_SUGGESTIONS_CLEAR = 'ANTENNA_EDITOR_SUGGESTIONS_CLEAR'; + +export const ANTENNA_EDITOR_ADD_REQUEST = 'ANTENNA_EDITOR_ADD_REQUEST'; +export const ANTENNA_EDITOR_ADD_SUCCESS = 'ANTENNA_EDITOR_ADD_SUCCESS'; +export const ANTENNA_EDITOR_ADD_FAIL = 'ANTENNA_EDITOR_ADD_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_REQUEST = 'ANTENNA_EDITOR_REMOVE_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_FAIL = 'ANTENNA_EDITOR_REMOVE_FAIL'; + +export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST'; +export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS'; +export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL'; + +export const ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL'; + +export const ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST = 'ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST'; +export const ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS = 'ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS'; +export const ANTENNA_EDITOR_FETCH_DOMAINS_FAIL = 'ANTENNA_EDITOR_FETCH_DOMAINS_FAIL'; + +export const ANTENNA_EDITOR_ADD_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_DOMAIN_REQUEST'; +export const ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS'; +export const ANTENNA_EDITOR_ADD_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_DOMAIN_FAIL'; + +export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDEDOMAIN_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL'; + +export const ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST = 'ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST'; +export const ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS = 'ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS'; +export const ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL = 'ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL'; + +export const ANTENNA_EDITOR_ADD_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_KEYWORD_REQUEST'; +export const ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS'; +export const ANTENNA_EDITOR_ADD_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_KEYWORD_FAIL'; + +export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL'; + +export const ANTENNA_EDITOR_FETCH_TAGS_REQUEST = 'ANTENNA_EDITOR_FETCH_TAGS_REQUEST'; +export const ANTENNA_EDITOR_FETCH_TAGS_SUCCESS = 'ANTENNA_EDITOR_FETCH_TAGS_SUCCESS'; +export const ANTENNA_EDITOR_FETCH_TAGS_FAIL = 'ANTENNA_EDITOR_FETCH_TAGS_FAIL'; + +export const ANTENNA_EDITOR_ADD_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_TAG_REQUEST'; +export const ANTENNA_EDITOR_ADD_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_TAG_SUCCESS'; +export const ANTENNA_EDITOR_ADD_TAG_FAIL = 'ANTENNA_EDITOR_ADD_TAG_FAIL'; + +export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_TAG_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_TAG_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_TAG_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL'; + +export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET'; +export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP'; + +export const ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST'; +export const ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS'; +export const ANTENNA_ADDER_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_ANTENNAS_FETCH_FAIL'; + +export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST'; +export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS'; +export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL'; + export const fetchAntenna = id => (dispatch, getState) => { if (getState().getIn(['antennas', id])) { return; @@ -62,6 +176,98 @@ export const fetchAntennasFail = error => ({ error, }); +export const submitAntennaEditor = shouldReset => (dispatch, getState) => { + const antennaId = getState().getIn(['antennaEditor', 'antennaId']); + const title = getState().getIn(['antennaEditor', 'title']); + + if (antennaId === null) { + dispatch(createAntenna(title, shouldReset)); + } else { + dispatch(updateAntenna(antennaId, title, shouldReset)); + } +}; + +export const setupAntennaEditor = antennaId => (dispatch, getState) => { + dispatch({ + type: ANTENNA_EDITOR_SETUP, + antenna: getState().getIn(['antennas', antennaId]), + }); + + dispatch(fetchAntennaAccounts(antennaId)); +}; + +export const setupExcludeAntennaEditor = antennaId => (dispatch, getState) => { + dispatch({ + type: ANTENNA_EDITOR_SETUP, + antenna: getState().getIn(['antennas', antennaId]), + }); + + dispatch(fetchAntennaExcludeAccounts(antennaId)); +}; + +export const changeAntennaEditorTitle = value => ({ + type: ANTENNA_EDITOR_TITLE_CHANGE, + value, +}); + +export const createAntenna = (title, shouldReset) => (dispatch, getState) => { + dispatch(createAntennaRequest()); + + api(getState).post('/api/v1/antennas', { title }).then(({ data }) => { + dispatch(createAntennaSuccess(data)); + + if (shouldReset) { + dispatch(resetAntennaEditor()); + } + }).catch(err => dispatch(createAntennaFail(err))); +}; + +export const createAntennaRequest = () => ({ + type: ANTENNA_CREATE_REQUEST, +}); + +export const createAntennaSuccess = antenna => ({ + type: ANTENNA_CREATE_SUCCESS, + antenna, +}); + +export const createAntennaFail = error => ({ + type: ANTENNA_CREATE_FAIL, + error, +}); + +export const updateAntenna = (id, title, shouldReset, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => { + dispatch(updateAntennaRequest(id)); + + api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => { + dispatch(updateAntennaSuccess(data)); + + if (shouldReset) { + dispatch(resetAntennaEditor()); + } + }).catch(err => dispatch(updateAntennaFail(id, err))); +}; + +export const updateAntennaRequest = id => ({ + type: ANTENNA_UPDATE_REQUEST, + id, +}); + +export const updateAntennaSuccess = antenna => ({ + type: ANTENNA_UPDATE_SUCCESS, + antenna, +}); + +export const updateAntennaFail = (id, error) => ({ + type: ANTENNA_UPDATE_FAIL, + id, + error, +}); + +export const resetAntennaEditor = () => ({ + type: ANTENNA_EDITOR_RESET, +}); + export const deleteAntenna = id => (dispatch, getState) => { dispatch(deleteAntennaRequest(id)); @@ -85,3 +291,696 @@ export const deleteAntennaFail = (id, error) => ({ id, error, }); + +export const fetchAntennaAccounts = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaAccountsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchAntennaAccountsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaAccountsFail(antennaId, err))); +}; + +export const fetchAntennaAccountsRequest = id => ({ + type: ANTENNA_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchAntennaAccountsSuccess = (id, accounts, next) => ({ + type: ANTENNA_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchAntennaAccountsFail = (id, error) => ({ + type: ANTENNA_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +export const fetchAntennaExcludeAccounts = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaExcludeAccountsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchAntennaExcludeAccountsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaExcludeAccountsFail(antennaId, err))); +}; + +export const fetchAntennaExcludeAccountsRequest = id => ({ + type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchAntennaExcludeAccountsSuccess = (id, accounts, next) => ({ + type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchAntennaExcludeAccountsFail = (id, error) => ({ + type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +export const fetchAntennaSuggestions = q => (dispatch, getState) => { + const params = { + q, + resolve: false, + }; + + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchAntennaSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); +}; + +export const fetchAntennaSuggestionsReady = (query, accounts) => ({ + type: ANTENNA_EDITOR_SUGGESTIONS_READY, + query, + accounts, +}); + +export const clearAntennaSuggestions = () => ({ + type: ANTENNA_EDITOR_SUGGESTIONS_CLEAR, +}); + +export const changeAntennaSuggestions = value => ({ + type: ANTENNA_EDITOR_SUGGESTIONS_CHANGE, + value, +}); + +export const addToAntennaEditor = accountId => (dispatch, getState) => { + dispatch(addToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const addToAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(addToAntennaRequest(antennaId, accountId)); + + api(getState).post(`/api/v1/antennas/${antennaId}/accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addToAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(addToAntennaFail(antennaId, accountId, err))); +}; + +export const addToAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_ADD_REQUEST, + antennaId, + accountId, +}); + +export const addToAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_ADD_SUCCESS, + antennaId, + accountId, +}); + +export const addToAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_ADD_FAIL, + antennaId, + accountId, + error, +}); + +export const addExcludeToAntennaEditor = accountId => (dispatch, getState) => { + dispatch(addExcludeToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const addExcludeToAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(addExcludeToAntennaRequest(antennaId, accountId)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addExcludeToAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(addExcludeToAntennaFail(antennaId, accountId, err))); +}; + +export const addExcludeToAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST, + antennaId, + accountId, +}); + +export const addExcludeToAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS, + antennaId, + accountId, +}); + +export const addExcludeToAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_FAIL, + antennaId, + accountId, + error, +}); + +export const removeFromAntennaEditor = accountId => (dispatch, getState) => { + dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const removeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(removeFromAntennaRequest(antennaId, accountId)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err))); +}; + +export const removeFromAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_REQUEST, + antennaId, + accountId, +}); + +export const removeFromAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_SUCCESS, + antennaId, + accountId, +}); + +export const removeFromAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_REMOVE_FAIL, + antennaId, + accountId, + error, +}); + +export const removeExcludeFromAntennaEditor = accountId => (dispatch, getState) => { + dispatch(removeExcludeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const removeExcludeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(removeExcludeFromAntennaRequest(antennaId, accountId)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeExcludeFromAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(removeExcludeFromAntennaFail(antennaId, accountId, err))); +}; + +export const removeExcludeFromAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST, + antennaId, + accountId, +}); + +export const removeExcludeFromAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS, + antennaId, + accountId, +}); + +export const removeExcludeFromAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL, + antennaId, + accountId, + error, +}); + +export const fetchAntennaDomains = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaDomainsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/domains`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(fetchAntennaDomainsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaDomainsFail(antennaId, err))); +}; + +export const fetchAntennaDomainsRequest = id => ({ + type: ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST, + id, +}); + +export const fetchAntennaDomainsSuccess = (id, domains) => ({ + type: ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS, + id, + domains, +}); + +export const fetchAntennaDomainsFail = (id, error) => ({ + type: ANTENNA_EDITOR_FETCH_DOMAINS_FAIL, + id, + error, +}); + +export const addDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(addDomainToAntennaRequest(antennaId, domain)); + + api(getState).post(`/api/v1/antennas/${antennaId}/domains`, { domains: [domain] }) + .then(() => dispatch(addDomainToAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(addDomainToAntennaFail(antennaId, domain, err))); +}; + +export const addDomainToAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_ADD_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const addDomainToAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const addDomainToAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_ADD_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + +export const removeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(removeDomainFromAntennaRequest(antennaId, domain)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/domains`, { params: { domains: [domain] } }) + .then(() => dispatch(removeDomainFromAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(removeDomainFromAntennaFail(antennaId, domain, err))); +}; + +export const removeDomainFromAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + +export const addExcludeDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(addExcludeDomainToAntennaRequest(antennaId, domain)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_domains`, { domains: [domain] }) + .then(() => dispatch(addExcludeDomainToAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(addExcludeDomainToAntennaFail(antennaId, domain, err))); +}; + +export const addExcludeDomainToAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const addExcludeDomainToAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const addExcludeDomainToAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + +export const removeExcludeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(removeExcludeDomainFromAntennaRequest(antennaId, domain)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_domains`, { params: { domains: [domain] } }) + .then(() => dispatch(removeExcludeDomainFromAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(removeExcludeDomainFromAntennaFail(antennaId, domain, err))); +}; + +export const removeExcludeDomainFromAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const removeExcludeDomainFromAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const removeExcludeDomainFromAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + +export const fetchAntennaKeywords = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaKeywordsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/keywords`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(fetchAntennaKeywordsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaKeywordsFail(antennaId, err))); +}; + +export const fetchAntennaKeywordsRequest = id => ({ + type: ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST, + id, +}); + +export const fetchAntennaKeywordsSuccess = (id, keywords) => ({ + type: ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS, + id, + keywords, +}); + +export const fetchAntennaKeywordsFail = (id, error) => ({ + type: ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL, + id, + error, +}); + +export const addKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { + dispatch(addKeywordToAntennaRequest(antennaId, keyword)); + + api(getState).post(`/api/v1/antennas/${antennaId}/keywords`, { keywords: [keyword] }) + .then(() => dispatch(addKeywordToAntennaSuccess(antennaId, keyword))) + .catch(err => dispatch(addKeywordToAntennaFail(antennaId, keyword, err))); +}; + +export const addKeywordToAntennaRequest = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_ADD_KEYWORD_REQUEST, + antennaId, + keyword, +}); + +export const addKeywordToAntennaSuccess = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS, + antennaId, + keyword, +}); + +export const addKeywordToAntennaFail = (antennaId, keyword, error) => ({ + type: ANTENNA_EDITOR_ADD_KEYWORD_FAIL, + antennaId, + keyword, + error, +}); + +export const removeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { + dispatch(removeKeywordFromAntennaRequest(antennaId, keyword)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/keywords`, { params: { keywords: [keyword] } }) + .then(() => dispatch(removeKeywordFromAntennaSuccess(antennaId, keyword))) + .catch(err => dispatch(removeKeywordFromAntennaFail(antennaId, keyword, err))); +}; + +export const removeKeywordFromAntennaRequest = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST, + antennaId, + keyword, +}); + +export const removeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS, + antennaId, + keyword, +}); + +export const removeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ + type: ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL, + antennaId, + keyword, + error, +}); + +export const addExcludeKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { + dispatch(addExcludeKeywordToAntennaRequest(antennaId, keyword)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_keywords`, { keywords: [keyword] }) + .then(() => dispatch(addExcludeKeywordToAntennaSuccess(antennaId, keyword))) + .catch(err => dispatch(addExcludeKeywordToAntennaFail(antennaId, keyword, err))); +}; + +export const addExcludeKeywordToAntennaRequest = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST, + antennaId, + keyword, +}); + +export const addExcludeKeywordToAntennaSuccess = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS, + antennaId, + keyword, +}); + +export const addExcludeKeywordToAntennaFail = (antennaId, keyword, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL, + antennaId, + keyword, + error, +}); + +export const removeExcludeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { + dispatch(removeExcludeKeywordFromAntennaRequest(antennaId, keyword)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_keywords`, { params: { keywords: [keyword] } }) + .then(() => dispatch(removeExcludeKeywordFromAntennaSuccess(antennaId, keyword))) + .catch(err => dispatch(removeExcludeKeywordFromAntennaFail(antennaId, keyword, err))); +}; + +export const removeExcludeKeywordFromAntennaRequest = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST, + antennaId, + keyword, +}); + +export const removeExcludeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS, + antennaId, + keyword, +}); + +export const removeExcludeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL, + antennaId, + keyword, + error, +}); + +export const fetchAntennaTags = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaTagsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/tags`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(fetchAntennaTagsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaTagsFail(antennaId, err))); +}; + +export const fetchAntennaTagsRequest = id => ({ + type: ANTENNA_EDITOR_FETCH_TAGS_REQUEST, + id, +}); + +export const fetchAntennaTagsSuccess = (id, tags) => ({ + type: ANTENNA_EDITOR_FETCH_TAGS_SUCCESS, + id, + tags, +}); + +export const fetchAntennaTagsFail = (id, error) => ({ + type: ANTENNA_EDITOR_FETCH_TAGS_FAIL, + id, + error, +}); + +export const addTagToAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(addTagToAntennaRequest(antennaId, tag)); + + api(getState).post(`/api/v1/antennas/${antennaId}/tags`, { tags: [tag] }) + .then(() => dispatch(addTagToAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(addTagToAntennaFail(antennaId, tag, err))); +}; + +export const addTagToAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_TAG_REQUEST, + antennaId, + tag, +}); + +export const addTagToAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_TAG_SUCCESS, + antennaId, + tag, +}); + +export const addTagToAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_ADD_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const removeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(removeTagFromAntennaRequest(antennaId, tag)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/tags`, { params: { tags: [tag] } }) + .then(() => dispatch(removeTagFromAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(removeTagFromAntennaFail(antennaId, tag, err))); +}; + +export const removeTagFromAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_TAG_REQUEST, + antennaId, + tag, +}); + +export const removeTagFromAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_TAG_SUCCESS, + antennaId, + tag, +}); + +export const removeTagFromAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_REMOVE_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const addExcludeTagToAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(addExcludeTagToAntennaRequest(antennaId, tag)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_tags`, { tags: [tag] }) + .then(() => dispatch(addExcludeTagToAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(addExcludeTagToAntennaFail(antennaId, tag, err))); +}; + +export const addExcludeTagToAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST, + antennaId, + tag, +}); + +export const addExcludeTagToAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS, + antennaId, + tag, +}); + +export const addExcludeTagToAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const removeExcludeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(removeExcludeTagFromAntennaRequest(antennaId, tag)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_tags`, { params: { tags: [tag] } }) + .then(() => dispatch(removeExcludeTagFromAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(removeExcludeTagFromAntennaFail(antennaId, tag, err))); +}; + +export const removeExcludeTagFromAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST, + antennaId, + tag, +}); + +export const removeExcludeTagFromAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS, + antennaId, + tag, +}); + +export const removeExcludeTagFromAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const resetAntennaAdder = () => ({ + type: ANTENNA_ADDER_RESET, +}); + +export const setupAntennaAdder = accountId => (dispatch, getState) => { + dispatch({ + type: ANTENNA_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchAntennas()); + dispatch(fetchAccountAntennas(accountId)); +}; + +export const setupExcludeAntennaAdder = accountId => (dispatch, getState) => { + dispatch({ + type: ANTENNA_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchAntennas()); + dispatch(fetchExcludeAccountAntennas(accountId)); +}; + +export const fetchAccountAntennas = accountId => (dispatch, getState) => { + dispatch(fetchAccountAntennasRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/antennas`) + .then(({ data }) => dispatch(fetchAccountAntennasSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountAntennasFail(accountId, err))); +}; + +export const fetchAccountAntennasRequest = id => ({ + type:ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST, + id, +}); + +export const fetchAccountAntennasSuccess = (id, antennas) => ({ + type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS, + id, + antennas, +}); + +export const fetchAccountAntennasFail = (id, err) => ({ + type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL, + id, + err, +}); + +export const fetchExcludeAccountAntennas = accountId => (dispatch, getState) => { + dispatch(fetchExcludeAccountAntennasRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/exclude_antennas`) + .then(({ data }) => dispatch(fetchExcludeAccountAntennasSuccess(accountId, data))) + .catch(err => dispatch(fetchExcludeAccountAntennasFail(accountId, err))); +}; + +export const fetchExcludeAccountAntennasRequest = id => ({ + type:ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST, + id, +}); + +export const fetchExcludeAccountAntennasSuccess = (id, antennas) => ({ + type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS, + id, + antennas, +}); + +export const fetchExcludeAccountAntennasFail = (id, err) => ({ + type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL, + id, + err, +}); + +export const addToAntennaAdder = antennaId => (dispatch, getState) => { + dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); +}; + +export const removeFromAntennaAdder = antennaId => (dispatch, getState) => { + dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); +}; + +export const addExcludeToAntennaAdder = antennaId => (dispatch, getState) => { + dispatch(addExcludeToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); +}; + +export const removeExcludeFromAntennaAdder = antennaId => (dispatch, getState) => { + dispatch(removeExcludeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); +}; + diff --git a/app/javascript/mastodon/actions/antennas_typed.ts b/app/javascript/mastodon/actions/antennas_typed.ts deleted file mode 100644 index f385b37d6e..0000000000 --- a/app/javascript/mastodon/actions/antennas_typed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { apiCreate, apiUpdate } from 'mastodon/api/antennas'; -import type { Antenna } from 'mastodon/models/antenna'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const createAntenna = createDataLoadingThunk( - 'antenna/create', - (antenna: Partial) => apiCreate(antenna), -); - -export const updateAntenna = createDataLoadingThunk( - 'antenna/update', - (antenna: Partial) => apiUpdate(antenna), -); diff --git a/app/javascript/mastodon/actions/blocks.js b/app/javascript/mastodon/actions/blocks.js index 5c66e27bec..e293657ad3 100644 --- a/app/javascript/mastodon/actions/blocks.js +++ b/app/javascript/mastodon/actions/blocks.js @@ -12,11 +12,13 @@ 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) => { + return (dispatch, getState) => { dispatch(fetchBlocksRequest()); - api().get('/api/v1/blocks').then(response => { + api(getState).get('/api/v1/blocks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); @@ -56,7 +58,7 @@ export function expandBlocks() { dispatch(expandBlocksRequest()); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); @@ -88,12 +90,11 @@ export function expandBlocksFail(error) { export function initBlockModal(account) { return dispatch => { - dispatch(openModal({ - modalType: 'BLOCK', - modalProps: { - accountId: account.get('id'), - acct: account.get('acct'), - }, - })); + dispatch({ + type: BLOCKS_INIT_MODAL, + account, + }); + + dispatch(openModal({ modalType: 'BLOCK' })); }; } diff --git a/app/javascript/mastodon/actions/bookmark_categories.js b/app/javascript/mastodon/actions/bookmark_categories.js index 313d5de8f2..b6f82b49c8 100644 --- a/app/javascript/mastodon/actions/bookmark_categories.js +++ b/app/javascript/mastodon/actions/bookmark_categories.js @@ -1,6 +1,10 @@ +import { bookmarkCategoryNeeded } from 'mastodon/initial_state'; +import { makeGetStatus } from 'mastodon/selectors'; + import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +import { unbookmark } from './interactions'; export const BOOKMARK_CATEGORY_FETCH_REQUEST = 'BOOKMARK_CATEGORY_FETCH_REQUEST'; export const BOOKMARK_CATEGORY_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_FETCH_SUCCESS'; @@ -10,6 +14,18 @@ export const BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORIES_FETCH_REQU export const BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORIES_FETCH_SUCCESS'; export const BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORIES_FETCH_FAIL'; +export const BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE = 'BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE'; +export const BOOKMARK_CATEGORY_EDITOR_RESET = 'BOOKMARK_CATEGORY_EDITOR_RESET'; +export const BOOKMARK_CATEGORY_EDITOR_SETUP = 'BOOKMARK_CATEGORY_EDITOR_SETUP'; + +export const BOOKMARK_CATEGORY_CREATE_REQUEST = 'BOOKMARK_CATEGORY_CREATE_REQUEST'; +export const BOOKMARK_CATEGORY_CREATE_SUCCESS = 'BOOKMARK_CATEGORY_CREATE_SUCCESS'; +export const BOOKMARK_CATEGORY_CREATE_FAIL = 'BOOKMARK_CATEGORY_CREATE_FAIL'; + +export const BOOKMARK_CATEGORY_UPDATE_REQUEST = 'BOOKMARK_CATEGORY_UPDATE_REQUEST'; +export const BOOKMARK_CATEGORY_UPDATE_SUCCESS = 'BOOKMARK_CATEGORY_UPDATE_SUCCESS'; +export const BOOKMARK_CATEGORY_UPDATE_FAIL = 'BOOKMARK_CATEGORY_UPDATE_FAIL'; + export const BOOKMARK_CATEGORY_DELETE_REQUEST = 'BOOKMARK_CATEGORY_DELETE_REQUEST'; export const BOOKMARK_CATEGORY_DELETE_SUCCESS = 'BOOKMARK_CATEGORY_DELETE_SUCCESS'; export const BOOKMARK_CATEGORY_DELETE_FAIL = 'BOOKMARK_CATEGORY_DELETE_FAIL'; @@ -18,13 +34,25 @@ export const BOOKMARK_CATEGORY_STATUSES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_STATU export const BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS'; export const BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL = 'BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL'; +export const BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST'; +export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS'; +export const BOOKMARK_CATEGORY_EDITOR_ADD_FAIL = 'BOOKMARK_CATEGORY_EDITOR_ADD_FAIL'; + +export const BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST'; +export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS'; +export const BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL'; + +export const BOOKMARK_CATEGORY_ADDER_RESET = 'BOOKMARK_CATEGORY_ADDER_RESET'; +export const BOOKMARK_CATEGORY_ADDER_SETUP = 'BOOKMARK_CATEGORY_ADDER_SETUP'; + +export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST'; +export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS'; +export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL'; + export const BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST'; export const BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS'; export const BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL'; -export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS'; -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS'; - export const fetchBookmarkCategory = id => (dispatch, getState) => { if (getState().getIn(['bookmark_categories', id])) { return; @@ -75,6 +103,89 @@ export const fetchBookmarkCategoriesFail = error => ({ error, }); +export const submitBookmarkCategoryEditor = shouldReset => (dispatch, getState) => { + const bookmarkCategoryId = getState().getIn(['bookmarkCategoryEditor', 'bookmarkCategoryId']); + const title = getState().getIn(['bookmarkCategoryEditor', 'title']); + + if (bookmarkCategoryId === null) { + dispatch(createBookmarkCategory(title, shouldReset)); + } else { + dispatch(updateBookmarkCategory(bookmarkCategoryId, title, shouldReset)); + } +}; + +export const setupBookmarkCategoryEditor = bookmarkCategoryId => (dispatch, getState) => { + dispatch({ + type: BOOKMARK_CATEGORY_EDITOR_SETUP, + bookmarkCategory: getState().getIn(['bookmark_categories', bookmarkCategoryId]), + }); + + dispatch(fetchBookmarkCategoryStatuses(bookmarkCategoryId)); +}; + +export const changeBookmarkCategoryEditorTitle = value => ({ + type: BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE, + value, +}); + +export const createBookmarkCategory = (title, shouldReset) => (dispatch, getState) => { + dispatch(createBookmarkCategoryRequest()); + + api(getState).post('/api/v1/bookmark_categories', { title }).then(({ data }) => { + dispatch(createBookmarkCategorySuccess(data)); + + if (shouldReset) { + dispatch(resetBookmarkCategoryEditor()); + } + }).catch(err => dispatch(createBookmarkCategoryFail(err))); +}; + +export const createBookmarkCategoryRequest = () => ({ + type: BOOKMARK_CATEGORY_CREATE_REQUEST, +}); + +export const createBookmarkCategorySuccess = bookmarkCategory => ({ + type: BOOKMARK_CATEGORY_CREATE_SUCCESS, + bookmarkCategory, +}); + +export const createBookmarkCategoryFail = error => ({ + type: BOOKMARK_CATEGORY_CREATE_FAIL, + error, +}); + +export const updateBookmarkCategory = (id, title, shouldReset) => (dispatch, getState) => { + dispatch(updateBookmarkCategoryRequest(id)); + + api(getState).put(`/api/v1/bookmark_categories/${id}`, { title }).then(({ data }) => { + dispatch(updateBookmarkCategorySuccess(data)); + + if (shouldReset) { + dispatch(resetBookmarkCategoryEditor()); + } + }).catch(err => dispatch(updateBookmarkCategoryFail(id, err))); +}; + +export const updateBookmarkCategoryRequest = id => ({ + type: BOOKMARK_CATEGORY_UPDATE_REQUEST, + id, +}); + +export const updateBookmarkCategorySuccess = bookmarkCategory => ({ + type: BOOKMARK_CATEGORY_UPDATE_SUCCESS, + bookmarkCategory, +}); + +export const updateBookmarkCategoryFail = (id, error) => ({ + type: BOOKMARK_CATEGORY_UPDATE_FAIL, + id, + error, +}); + +export const resetBookmarkCategoryEditor = () => ({ + type: BOOKMARK_CATEGORY_EDITOR_RESET, +}); + export const deleteBookmarkCategory = id => (dispatch, getState) => { dispatch(deleteBookmarkCategoryRequest(id)); @@ -102,10 +213,9 @@ export const deleteBookmarkCategoryFail = (id, error) => ({ export const fetchBookmarkCategoryStatuses = bookmarkCategoryId => (dispatch, getState) => { dispatch(fetchBookmarkCategoryStatusesRequest(bookmarkCategoryId)); - api(getState).get(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`).then((response) => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchBookmarkCategoryStatusesSuccess(bookmarkCategoryId, response.data, next ? next.uri : null)); + api(getState).get(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedStatuses(data)); + dispatch(fetchBookmarkCategoryStatusesSuccess(bookmarkCategoryId, data)); }).catch(err => dispatch(fetchBookmarkCategoryStatusesFail(bookmarkCategoryId, err))); }; @@ -127,11 +237,121 @@ export const fetchBookmarkCategoryStatusesFail = (id, error) => ({ error, }); +export const addToBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => { + dispatch(addToBookmarkCategoryRequest(bookmarkCategoryId, statusId)); + + api(getState).post(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { status_ids: [statusId] }) + .then(() => dispatch(addToBookmarkCategorySuccess(bookmarkCategoryId, statusId))) + .catch(err => dispatch(addToBookmarkCategoryFail(bookmarkCategoryId, statusId, err))); +}; + +export const addToBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({ + type: BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST, + bookmarkCategoryId, + statusId, +}); + +export const addToBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({ + type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS, + bookmarkCategoryId, + statusId, +}); + +export const addToBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({ + type: BOOKMARK_CATEGORY_EDITOR_ADD_FAIL, + bookmarkCategoryId, + statusId, + error, +}); + +export const removeFromBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => { + dispatch(removeFromBookmarkCategoryRequest(bookmarkCategoryId, statusId)); + + api(getState).delete(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { params: { status_ids: [statusId] } }) + .then(() => dispatch(removeFromBookmarkCategorySuccess(bookmarkCategoryId, statusId))) + .catch(err => dispatch(removeFromBookmarkCategoryFail(bookmarkCategoryId, statusId, err))); +}; + +export const removeFromBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({ + type: BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST, + bookmarkCategoryId, + statusId, +}); + +export const removeFromBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({ + type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS, + bookmarkCategoryId, + statusId, +}); + +export const removeFromBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({ + type: BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL, + bookmarkCategoryId, + statusId, + error, +}); + +export const resetBookmarkCategoryAdder = () => ({ + type: BOOKMARK_CATEGORY_ADDER_RESET, +}); + +export const setupBookmarkCategoryAdder = statusId => (dispatch, getState) => { + dispatch({ + type: BOOKMARK_CATEGORY_ADDER_SETUP, + status: getState().getIn(['statuses', statusId]), + }); + dispatch(fetchBookmarkCategories()); + dispatch(fetchStatusBookmarkCategories(statusId)); +}; + +export const fetchStatusBookmarkCategories = statusId => (dispatch, getState) => { + dispatch(fetchStatusBookmarkCategoriesRequest(statusId)); + + api(getState).get(`/api/v1/statuses/${statusId}/bookmark_categories`) + .then(({ data }) => dispatch(fetchStatusBookmarkCategoriesSuccess(statusId, data))) + .catch(err => dispatch(fetchStatusBookmarkCategoriesFail(statusId, err))); +}; + +export const fetchStatusBookmarkCategoriesRequest = id => ({ + type:BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST, + id, +}); + +export const fetchStatusBookmarkCategoriesSuccess = (id, bookmarkCategories) => ({ + type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS, + id, + bookmarkCategories, +}); + +export const fetchStatusBookmarkCategoriesFail = (id, err) => ({ + type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL, + id, + err, +}); + +export const addToBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { + dispatch(addToBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); +}; + +export const removeFromBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { + if (bookmarkCategoryNeeded) { + const categories = getState().getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']); + if (categories && categories.count() <= 1) { + const status = makeGetStatus()(getState(), { id: getState().getIn(['bookmarkCategoryAdder', 'statusId']) }); + dispatch(unbookmark(status)); + } else { + dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); + } + } else { + dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); + } +}; + export function expandBookmarkCategoryStatuses(bookmarkCategoryId) { return (dispatch, getState) => { - const url = getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'next'], null); + const url = getState().getIn(['bookmark_categories', bookmarkCategoryId, 'next'], null); - if (url === null || getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'isLoading'])) { + if (url === null || getState().getIn(['bookmark_categories', bookmarkCategoryId, 'isLoading'])) { return; } @@ -171,19 +391,3 @@ export function expandBookmarkCategoryStatusesFail(id, error) { }; } -export function bookmarkCategoryEditorAddSuccess(id, statusId) { - return { - type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS, - id, - statusId, - }; -} - -export function bookmarkCategoryEditorRemoveSuccess(id, statusId) { - return { - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS, - id, - statusId, - }; -} - diff --git a/app/javascript/mastodon/actions/bookmark_categories_typed.ts b/app/javascript/mastodon/actions/bookmark_categories_typed.ts deleted file mode 100644 index 3570ce5991..0000000000 --- a/app/javascript/mastodon/actions/bookmark_categories_typed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { apiCreate, apiUpdate } from 'mastodon/api/bookmark_categories'; -import type { BookmarkCategory } from 'mastodon/models/bookmark_category'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const createBookmarkCategory = createDataLoadingThunk( - 'bookmark_category/create', - (bookmarkCategory: Partial) => apiCreate(bookmarkCategory), -); - -export const updateBookmarkCategory = createDataLoadingThunk( - 'bookmark_category/update', - (bookmarkCategory: Partial) => apiUpdate(bookmarkCategory), -); diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js index 133e49e839..0b16f61e63 100644 --- a/app/javascript/mastodon/actions/bookmarks.js +++ b/app/javascript/mastodon/actions/bookmarks.js @@ -1,5 +1,3 @@ -// Kmyblue tracking marker: copied bookmark_categories.js - import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; @@ -20,7 +18,7 @@ export function fetchBookmarkedStatuses() { dispatch(fetchBookmarkedStatusesRequest()); - api().get('/api/v1/bookmarks').then(response => { + api(getState).get('/api/v1/bookmarks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); @@ -61,7 +59,7 @@ export function expandBookmarkedStatuses() { dispatch(expandBookmarkedStatusesRequest()); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js new file mode 100644 index 0000000000..f219ca3ef3 --- /dev/null +++ b/app/javascript/mastodon/actions/boosts.js @@ -0,0 +1,32 @@ +import { openModal } from './modal'; + +export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL'; +export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY'; + +export function initBoostModal(props) { + return (dispatch, getState) => { + const default_privacy = getState().getIn(['compose', 'default_privacy']); + + const privacy = props.status.get('visibility_ex') === 'private' ? 'private' : default_privacy; + + dispatch({ + type: BOOSTS_INIT_MODAL, + privacy, + }); + + dispatch(openModal({ + modalType: 'BOOST', + modalProps: props, + })); + }; +} + + +export function changeBoostPrivacy(privacy) { + return dispatch => { + dispatch({ + type: BOOSTS_CHANGE_PRIVACY, + privacy, + }); + }; +} diff --git a/app/javascript/mastodon/actions/circles.js b/app/javascript/mastodon/actions/circles.js index 221c0c683a..6a52e541c9 100644 --- a/app/javascript/mastodon/actions/circles.js +++ b/app/javascript/mastodon/actions/circles.js @@ -1,6 +1,7 @@ -import api, { getLinks } from '../api'; +import api from '../api'; -import { importFetchedStatuses } from './importer'; +import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST'; export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS'; @@ -10,6 +11,10 @@ export const CIRCLES_FETCH_REQUEST = 'CIRCLES_FETCH_REQUEST'; export const CIRCLES_FETCH_SUCCESS = 'CIRCLES_FETCH_SUCCESS'; export const CIRCLES_FETCH_FAIL = 'CIRCLES_FETCH_FAIL'; +export const CIRCLE_EDITOR_TITLE_CHANGE = 'CIRCLE_EDITOR_TITLE_CHANGE'; +export const CIRCLE_EDITOR_RESET = 'CIRCLE_EDITOR_RESET'; +export const CIRCLE_EDITOR_SETUP = 'CIRCLE_EDITOR_SETUP'; + export const CIRCLE_CREATE_REQUEST = 'CIRCLE_CREATE_REQUEST'; export const CIRCLE_CREATE_SUCCESS = 'CIRCLE_CREATE_SUCCESS'; export const CIRCLE_CREATE_FAIL = 'CIRCLE_CREATE_FAIL'; @@ -22,13 +27,28 @@ export const CIRCLE_DELETE_REQUEST = 'CIRCLE_DELETE_REQUEST'; export const CIRCLE_DELETE_SUCCESS = 'CIRCLE_DELETE_SUCCESS'; export const CIRCLE_DELETE_FAIL = 'CIRCLE_DELETE_FAIL'; -export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST'; -export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS'; -export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL'; +export const CIRCLE_ACCOUNTS_FETCH_REQUEST = 'CIRCLE_ACCOUNTS_FETCH_REQUEST'; +export const CIRCLE_ACCOUNTS_FETCH_SUCCESS = 'CIRCLE_ACCOUNTS_FETCH_SUCCESS'; +export const CIRCLE_ACCOUNTS_FETCH_FAIL = 'CIRCLE_ACCOUNTS_FETCH_FAIL'; -export const CIRCLE_STATUSES_EXPAND_REQUEST = 'CIRCLE_STATUSES_EXPAND_REQUEST'; -export const CIRCLE_STATUSES_EXPAND_SUCCESS = 'CIRCLE_STATUSES_EXPAND_SUCCESS'; -export const CIRCLE_STATUSES_EXPAND_FAIL = 'CIRCLE_STATUSES_EXPAND_FAIL'; +export const CIRCLE_EDITOR_SUGGESTIONS_CHANGE = 'CIRCLE_EDITOR_SUGGESTIONS_CHANGE'; +export const CIRCLE_EDITOR_SUGGESTIONS_READY = 'CIRCLE_EDITOR_SUGGESTIONS_READY'; +export const CIRCLE_EDITOR_SUGGESTIONS_CLEAR = 'CIRCLE_EDITOR_SUGGESTIONS_CLEAR'; + +export const CIRCLE_EDITOR_ADD_REQUEST = 'CIRCLE_EDITOR_ADD_REQUEST'; +export const CIRCLE_EDITOR_ADD_SUCCESS = 'CIRCLE_EDITOR_ADD_SUCCESS'; +export const CIRCLE_EDITOR_ADD_FAIL = 'CIRCLE_EDITOR_ADD_FAIL'; + +export const CIRCLE_EDITOR_REMOVE_REQUEST = 'CIRCLE_EDITOR_REMOVE_REQUEST'; +export const CIRCLE_EDITOR_REMOVE_SUCCESS = 'CIRCLE_EDITOR_REMOVE_SUCCESS'; +export const CIRCLE_EDITOR_REMOVE_FAIL = 'CIRCLE_EDITOR_REMOVE_FAIL'; + +export const CIRCLE_ADDER_RESET = 'CIRCLE_ADDER_RESET'; +export const CIRCLE_ADDER_SETUP = 'CIRCLE_ADDER_SETUP'; + +export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_REQUEST'; +export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS'; +export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL'; export const fetchCircle = id => (dispatch, getState) => { if (getState().getIn(['circles', id])) { @@ -80,6 +100,89 @@ export const fetchCirclesFail = error => ({ error, }); +export const submitCircleEditor = shouldReset => (dispatch, getState) => { + const circleId = getState().getIn(['circleEditor', 'circleId']); + const title = getState().getIn(['circleEditor', 'title']); + + if (circleId === null) { + dispatch(createCircle(title, shouldReset)); + } else { + dispatch(updateCircle(circleId, title, shouldReset)); + } +}; + +export const setupCircleEditor = circleId => (dispatch, getState) => { + dispatch({ + type: CIRCLE_EDITOR_SETUP, + circle: getState().getIn(['circles', circleId]), + }); + + dispatch(fetchCircleAccounts(circleId)); +}; + +export const changeCircleEditorTitle = value => ({ + type: CIRCLE_EDITOR_TITLE_CHANGE, + value, +}); + +export const createCircle = (title, shouldReset) => (dispatch, getState) => { + dispatch(createCircleRequest()); + + api(getState).post('/api/v1/circles', { title }).then(({ data }) => { + dispatch(createCircleSuccess(data)); + + if (shouldReset) { + dispatch(resetCircleEditor()); + } + }).catch(err => dispatch(createCircleFail(err))); +}; + +export const createCircleRequest = () => ({ + type: CIRCLE_CREATE_REQUEST, +}); + +export const createCircleSuccess = circle => ({ + type: CIRCLE_CREATE_SUCCESS, + circle, +}); + +export const createCircleFail = error => ({ + type: CIRCLE_CREATE_FAIL, + error, +}); + +export const updateCircle = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { + dispatch(updateCircleRequest(id)); + + api(getState).put(`/api/v1/circles/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { + dispatch(updateCircleSuccess(data)); + + if (shouldReset) { + dispatch(resetCircleEditor()); + } + }).catch(err => dispatch(updateCircleFail(id, err))); +}; + +export const updateCircleRequest = id => ({ + type: CIRCLE_UPDATE_REQUEST, + id, +}); + +export const updateCircleSuccess = circle => ({ + type: CIRCLE_UPDATE_SUCCESS, + circle, +}); + +export const updateCircleFail = (id, error) => ({ + type: CIRCLE_UPDATE_FAIL, + id, + error, +}); + +export const resetCircleEditor = () => ({ + type: CIRCLE_EDITOR_RESET, +}); + export const deleteCircle = id => (dispatch, getState) => { dispatch(deleteCircleRequest(id)); @@ -104,93 +207,166 @@ export const deleteCircleFail = (id, error) => ({ error, }); -export function fetchCircleStatuses(circleId) { - return (dispatch, getState) => { - if (getState().getIn(['circles', circleId, 'isLoading'])) { - return; - } - const items = getState().getIn(['circles', circleId, 'items']); - if (items && items.size > 0) { - return; - } +export const fetchCircleAccounts = circleId => (dispatch, getState) => { + dispatch(fetchCircleAccountsRequest(circleId)); - dispatch(fetchCircleStatusesRequest(circleId)); + api(getState).get(`/api/v1/circles/${circleId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchCircleAccountsSuccess(circleId, data)); + }).catch(err => dispatch(fetchCircleAccountsFail(circleId, err))); +}; - api(getState).get(`/api/v1/circles/${circleId}/statuses`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchCircleStatusesSuccess(circleId, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(fetchCircleStatusesFail(circleId, error)); - }); +export const fetchCircleAccountsRequest = id => ({ + type: CIRCLE_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchCircleAccountsSuccess = (id, accounts, next) => ({ + type: CIRCLE_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchCircleAccountsFail = (id, error) => ({ + type: CIRCLE_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +export const fetchCircleSuggestions = q => (dispatch, getState) => { + const params = { + q, + resolve: false, + follower: true, }; -} -export function fetchCircleStatusesRequest(id) { - return { - type: CIRCLE_STATUSES_FETCH_REQUEST, - id, - }; -} + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchCircleSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); +}; -export function fetchCircleStatusesSuccess(id, statuses, next) { - return { - type: CIRCLE_STATUSES_FETCH_SUCCESS, - id, - statuses, - next, - }; -} +export const fetchCircleSuggestionsReady = (query, accounts) => ({ + type: CIRCLE_EDITOR_SUGGESTIONS_READY, + query, + accounts, +}); -export function fetchCircleStatusesFail(id, error) { - return { - type: CIRCLE_STATUSES_FETCH_FAIL, - id, - error, - }; -} +export const clearCircleSuggestions = () => ({ + type: CIRCLE_EDITOR_SUGGESTIONS_CLEAR, +}); -export function expandCircleStatuses(circleId) { - return (dispatch, getState) => { - const url = getState().getIn(['status_lists', 'circle_statuses', circleId, 'next'], null); +export const changeCircleSuggestions = value => ({ + type: CIRCLE_EDITOR_SUGGESTIONS_CHANGE, + value, +}); - if (url === null || getState().getIn(['status_lists', 'circle_statuses', circleId, 'isLoading'])) { - return; - } +export const addToCircleEditor = accountId => (dispatch, getState) => { + dispatch(addToCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); +}; - dispatch(expandCircleStatusesRequest(circleId)); +export const addToCircle = (circleId, accountId) => (dispatch, getState) => { + dispatch(addToCircleRequest(circleId, accountId)); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandCircleStatusesSuccess(circleId, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandCircleStatusesFail(circleId, error)); - }); - }; -} + api(getState).post(`/api/v1/circles/${circleId}/accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addToCircleSuccess(circleId, accountId))) + .catch(err => dispatch(addToCircleFail(circleId, accountId, err))); +}; -export function expandCircleStatusesRequest(id) { - return { - type: CIRCLE_STATUSES_EXPAND_REQUEST, - id, - }; -} +export const addToCircleRequest = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_ADD_REQUEST, + circleId, + accountId, +}); -export function expandCircleStatusesSuccess(id, statuses, next) { - return { - type: CIRCLE_STATUSES_EXPAND_SUCCESS, - id, - statuses, - next, - }; -} +export const addToCircleSuccess = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_ADD_SUCCESS, + circleId, + accountId, +}); -export function expandCircleStatusesFail(id, error) { - return { - type: CIRCLE_STATUSES_EXPAND_FAIL, - id, - error, - }; -} +export const addToCircleFail = (circleId, accountId, error) => ({ + type: CIRCLE_EDITOR_ADD_FAIL, + circleId, + accountId, + error, +}); + +export const removeFromCircleEditor = accountId => (dispatch, getState) => { + dispatch(removeFromCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); +}; + +export const removeFromCircle = (circleId, accountId) => (dispatch, getState) => { + dispatch(removeFromCircleRequest(circleId, accountId)); + + api(getState).delete(`/api/v1/circles/${circleId}/accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeFromCircleSuccess(circleId, accountId))) + .catch(err => dispatch(removeFromCircleFail(circleId, accountId, err))); +}; + +export const removeFromCircleRequest = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_REMOVE_REQUEST, + circleId, + accountId, +}); + +export const removeFromCircleSuccess = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_REMOVE_SUCCESS, + circleId, + accountId, +}); + +export const removeFromCircleFail = (circleId, accountId, error) => ({ + type: CIRCLE_EDITOR_REMOVE_FAIL, + circleId, + accountId, + error, +}); + +export const resetCircleAdder = () => ({ + type: CIRCLE_ADDER_RESET, +}); + +export const setupCircleAdder = accountId => (dispatch, getState) => { + dispatch({ + type: CIRCLE_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchCircles()); + dispatch(fetchAccountCircles(accountId)); +}; + +export const fetchAccountCircles = accountId => (dispatch, getState) => { + dispatch(fetchAccountCirclesRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/circles`) + .then(({ data }) => dispatch(fetchAccountCirclesSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountCirclesFail(accountId, err))); +}; + +export const fetchAccountCirclesRequest = id => ({ + type:CIRCLE_ADDER_CIRCLES_FETCH_REQUEST, + id, +}); + +export const fetchAccountCirclesSuccess = (id, circles) => ({ + type: CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS, + id, + circles, +}); + +export const fetchAccountCirclesFail = (id, err) => ({ + type: CIRCLE_ADDER_CIRCLES_FETCH_FAIL, + id, + err, +}); + +export const addToCircleAdder = circleId => (dispatch, getState) => { + dispatch(addToCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); +}; + +export const removeFromCircleAdder = circleId => (dispatch, getState) => { + dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); +}; diff --git a/app/javascript/mastodon/actions/circles_typed.ts b/app/javascript/mastodon/actions/circles_typed.ts deleted file mode 100644 index 6f4117ff81..0000000000 --- a/app/javascript/mastodon/actions/circles_typed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { apiCreate, apiUpdate } from 'mastodon/api/circles'; -import type { Circle } from 'mastodon/models/circle'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const createCircle = createDataLoadingThunk( - 'circle/create', - (circle: Partial) => apiCreate(circle), -); - -export const updateCircle = createDataLoadingThunk( - 'circle/update', - (circle: Partial) => apiUpdate(circle), -); diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 9a92528f3a..1f682d1321 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -4,7 +4,6 @@ import axios from 'axios'; import { throttle } from 'lodash'; import api from 'mastodon/api'; -import { browserHistory } from 'mastodon/components/router'; import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light'; import { tagHistory } from 'mastodon/settings'; @@ -29,8 +28,6 @@ export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; -export const COMPOSE_WITH_CIRCLE_SUCCESS = 'COMPOSE_WITH_CIRCLE_SUCCESS'; - export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; @@ -65,7 +62,6 @@ export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; export const COMPOSE_EXPIRATION_INSERT = 'COMPOSE_EXPIRATION_INSERT'; -export const COMPOSE_FEATURED_TAG_INSERT = 'COMPOSE_FEATURED_TAG_INSERT'; export const COMPOSE_REFERENCE_INSERT = 'COMPOSE_REFERENCE_INSERT'; export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; @@ -85,7 +81,6 @@ 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'; @@ -98,9 +93,9 @@ const messages = defineMessages({ saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' }, }); -export const ensureComposeIsVisible = (getState) => { +export const ensureComposeIsVisible = (getState, routerHistory) => { if (!getState().getIn(['compose', 'mounted'])) { - browserHistory.push('/publish'); + routerHistory.push('/publish'); } }; @@ -120,26 +115,14 @@ export function changeCompose(text) { }; } -export function replyCompose(status) { +export function replyCompose(status, routerHistory) { return (dispatch, getState) => { dispatch({ type: COMPOSE_REPLY, status: status, }); - ensureComposeIsVisible(getState); - }; -} - -export function replyComposeById(statusId) { - return (dispatch, getState) => { - const state = getState(); - const status = state.statuses.get(statusId); - - if (status) { - const account = state.accounts.get(status.get('account')); - dispatch(replyCompose(status.set('account', account))); - } + ensureComposeIsVisible(getState, routerHistory); }; } @@ -155,50 +138,42 @@ export function resetCompose() { }; } -export const focusCompose = (defaultText) => (dispatch, getState) => { +export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => { dispatch({ type: COMPOSE_FOCUS, defaultText, }); - ensureComposeIsVisible(getState); + ensureComposeIsVisible(getState, routerHistory); }; -export function mentionCompose(account) { +export function mentionCompose(account, routerHistory) { return (dispatch, getState) => { dispatch({ type: COMPOSE_MENTION, account: account, }); - ensureComposeIsVisible(getState); + ensureComposeIsVisible(getState, routerHistory); }; } -export function mentionComposeById(accountId) { - return (dispatch, getState) => { - dispatch(mentionCompose(getState().accounts.get(accountId))); - }; -} - -export function directCompose(account) { +export function directCompose(account, routerHistory) { return (dispatch, getState) => { dispatch({ type: COMPOSE_DIRECT, account: account, }); - ensureComposeIsVisible(getState); + ensureComposeIsVisible(getState, routerHistory); }; } -export function submitCompose() { +export function submitCompose(routerHistory) { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); const statusId = getState().getIn(['compose', 'id'], null); - const circleId = getState().getIn(['compose', 'circle_id'], null); - const privacy = getState().getIn(['compose', 'privacy']); if ((!status || !status.length) && media.size === 0) { return; @@ -226,7 +201,7 @@ export function submitCompose() { }); } - api().request({ + api(getState).request({ url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, method: statusId === null ? 'post' : 'put', data: { @@ -234,7 +209,7 @@ export function submitCompose() { in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), media_attributes, - sensitive: media.size > 0 ? getState().getIn(['compose', 'spoiler']) : false, + sensitive: media.size > 0 ? getState().getIn(['compose', 'sensitive']) : false, spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', markdown: getState().getIn(['compose', 'markdown']), visibility: getState().getIn(['compose', 'privacy']), @@ -247,8 +222,8 @@ export function submitCompose() { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { - if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new') && window.history.state) { - browserHistory.goBack(); + if (routerHistory && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new') && window.history.state) { + routerHistory.goBack(); } dispatch(insertIntoTagHistory(response.data.tags, status)); @@ -272,21 +247,17 @@ export function submitCompose() { insertIfOnline('home'); } - if (statusId === null && response.data.in_reply_to_id === null && ['public', 'public_unlisted', 'login'].includes(response.data.visibility_ex)) { + if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility_ex === 'public') { insertIfOnline('community'); insertIfOnline('public'); insertIfOnline(`account:${response.data.account.id}`); } - if (statusId === null && privacy === 'circle' && circleId !== null && circleId !== 0) { - dispatch(submitComposeWithCircleSuccess({ ...response.data }, `${circleId}`)); - } - dispatch(showAlert({ message: statusId === null ? messages.published : messages.saved, action: messages.open, dismissAfter: 10000, - onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`), + onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`), })); }).catch(function (error) { dispatch(submitComposeFail(error)); @@ -307,14 +278,6 @@ export function submitComposeSuccess(status) { }; } -export function submitComposeWithCircleSuccess(status, circleId) { - return { - type: COMPOSE_WITH_CIRCLE_SUCCESS, - statusId: status.id, - circleId, - }; -} - export function submitComposeFail(error) { return { type: COMPOSE_SUBMIT_FAIL, @@ -324,11 +287,9 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function (dispatch, getState) { - const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']); + const uploadLimit = 4; const media = getState().getIn(['compose', 'media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']); - const defaultSensitive = getState().getIn(['compose', 'default_sensitive']); - const spoiler = getState().getIn(['compose', 'spoiler']); const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); @@ -341,12 +302,12 @@ export function uploadCompose(files) { dispatch(uploadComposeRequest()); for (const [i, file] of Array.from(files).entries()) { - if (media.size + i > (uploadLimit - 1)) break; + if (media.size + i >= 4) break; const data = new FormData(); data.append('file', file); - api().post('/api/v2/media', data, { + api(getState).post('/api/v2/media', data, { onUploadProgress: function({ loaded }){ progress[i] = loaded; dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); @@ -357,17 +318,13 @@ export function uploadCompose(files) { if (status === 200) { dispatch(uploadComposeSuccess(data, file)); - - if (defaultSensitive && !spoiler && (media.size + i) === 0) { - dispatch(changeComposeSpoilerness()); - } } else if (status === 202) { dispatch(uploadComposeProcessing()); let tryCount = 1; const poll = () => { - api().get(`/api/v1/media/${data.id}`).then(response => { + api(getState).get(`/api/v1/media/${data.id}`).then(response => { if (response.status === 200) { dispatch(uploadComposeSuccess(response.data, file)); } else if (response.status === 206) { @@ -389,7 +346,7 @@ export const uploadComposeProcessing = () => ({ type: COMPOSE_UPLOAD_PROCESSING, }); -export const uploadThumbnail = (id, file) => (dispatch) => { +export const uploadThumbnail = (id, file) => (dispatch, getState) => { dispatch(uploadThumbnailRequest()); const total = file.size; @@ -397,7 +354,7 @@ export const uploadThumbnail = (id, file) => (dispatch) => { data.append('thumbnail', file); - api().put(`/api/v1/media/${id}`, data, { + api(getState).put(`/api/v1/media/${id}`, data, { onUploadProgress: ({ loaded }) => { dispatch(uploadThumbnailProgress(loaded, total)); }, @@ -441,7 +398,7 @@ export function initMediaEditModal(id) { dispatch(openModal({ modalType: 'FOCAL_POINT', - modalProps: { mediaId: id }, + modalProps: { id }, })); }; } @@ -480,7 +437,7 @@ export function changeUploadCompose(id, params) { dispatch(changeUploadComposeSuccess(data, true)); } else { - api().put(`/api/v1/media/${id}`, params).then(response => { + api(getState).put(`/api/v1/media/${id}`, params).then(response => { dispatch(changeUploadComposeSuccess(response.data, false)); }).catch(error => { dispatch(changeUploadComposeFail(id, error)); @@ -568,7 +525,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => fetchComposeSuggestionsAccountsController = new AbortController(); - api().get('/api/v1/accounts/search', { + api(getState).get('/api/v1/accounts/search', { signal: fetchComposeSuggestionsAccountsController.signal, params: { @@ -602,7 +559,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { fetchComposeSuggestionsTagsController = new AbortController(); - api().get('/api/v2/search', { + api(getState).get('/api/v2/search', { signal: fetchComposeSuggestionsTagsController.signal, params: { @@ -828,24 +785,12 @@ export function insertExpirationCompose(position, data) { }; } -export function insertFeaturedTagCompose(position, data) { +export function insertReferenceCompose(position, url, attributeType) { return { - type: COMPOSE_FEATURED_TAG_INSERT, + type: COMPOSE_REFERENCE_INSERT, position, - data, - }; -} - -export function insertReferenceCompose(position, url, attributeType, routerHistory) { - return (dispatch, getState) => { - dispatch({ - type: COMPOSE_REFERENCE_INSERT, - position, - url, - attributeType, - }); - - ensureComposeIsVisible(getState, routerHistory); + url, + attributeType, }; } @@ -875,12 +820,11 @@ export function addPollOption(title) { }; } -export function changePollOption(index, title, maxOptions) { +export function changePollOption(index, title) { return { type: COMPOSE_POLL_OPTION_CHANGE, index, title, - maxOptions, }; } @@ -899,12 +843,6 @@ export function changePollSettings(expiresIn, isMultiple) { }; } -export const changeMediaOrder = (a, b) => ({ - type: COMPOSE_CHANGE_MEDIA_ORDER, - a, - b, -}); - export function changeCircle(circleId) { return { type: COMPOSE_CIRCLE_CHANGE, diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts deleted file mode 100644 index 97f0d68c51..0000000000 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; - -import { apiUpdateMedia } from 'mastodon/api/compose'; -import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; -import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { - unattached?: boolean; -}; - -const simulateModifiedApiResponse = ( - media: MediaAttachment, - params: { description?: string; focus?: string }, -): SimulatedMediaAttachmentJSON => { - const [x, y] = (params.focus ?? '').split(','); - - const data = { - ...media.toJS(), - ...params, - meta: { - focus: { - x: parseFloat(x ?? '0'), - y: parseFloat(y ?? '0'), - }, - }, - } as unknown as SimulatedMediaAttachmentJSON; - - return data; -}; - -export const changeUploadCompose = createDataLoadingThunk( - 'compose/changeUpload', - async ( - { - id, - ...params - }: { - id: string; - description: string; - focus: string; - }, - { getState }, - ) => { - const media = ( - (getState().compose as ImmutableMap).get( - 'media_attachments', - ) as ImmutableList - ).find((item) => item.get('id') === id); - - // Editing already-attached media is deferred to editing the post itself. - // For simplicity's sake, fake an API reply. - if (media && !media.get('unattached')) { - return new Promise((resolve) => { - resolve(simulateModifiedApiResponse(media, params)); - }); - } - - return apiUpdateMedia(id, params); - }, - (media: SimulatedMediaAttachmentJSON) => { - return { - media, - attached: typeof media.unattached !== 'undefined' && !media.unattached, - }; - }, - { - useLoadingBar: false, - }, -); diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js index 03174c485d..8c4c4529fb 100644 --- a/app/javascript/mastodon/actions/conversations.js +++ b/app/javascript/mastodon/actions/conversations.js @@ -28,13 +28,13 @@ export const unmountConversations = () => ({ type: CONVERSATIONS_UNMOUNT, }); -export const markConversationRead = conversationId => (dispatch) => { +export const markConversationRead = conversationId => (dispatch, getState) => { dispatch({ type: CONVERSATIONS_READ, id: conversationId, }); - api().post(`/api/v1/conversations/${conversationId}/read`); + api(getState).post(`/api/v1/conversations/${conversationId}/read`); }; export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { @@ -48,7 +48,7 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { const isLoadingRecent = !!params.since_id; - api().get('/api/v1/conversations', { params }) + api(getState).get('/api/v1/conversations', { params }) .then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); @@ -88,10 +88,10 @@ export const updateConversations = conversation => dispatch => { }); }; -export const deleteConversation = conversationId => (dispatch) => { +export const deleteConversation = conversationId => (dispatch, getState) => { dispatch(deleteConversationRequest(conversationId)); - api().delete(`/api/v1/conversations/${conversationId}`) + api(getState).delete(`/api/v1/conversations/${conversationId}`) .then(() => dispatch(deleteConversationSuccess(conversationId))) .catch(error => dispatch(deleteConversationFail(conversationId, error))); }; diff --git a/app/javascript/mastodon/actions/custom_emojis.js b/app/javascript/mastodon/actions/custom_emojis.js index fb65f072dc..9ec8156b17 100644 --- a/app/javascript/mastodon/actions/custom_emojis.js +++ b/app/javascript/mastodon/actions/custom_emojis.js @@ -5,10 +5,10 @@ export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS'; export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL'; export function fetchCustomEmojis() { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchCustomEmojisRequest()); - api().get('/api/v1/custom_emojis').then(response => { + api(getState).get('/api/v1/custom_emojis').then(response => { dispatch(fetchCustomEmojisSuccess(response.data)); }).catch(error => { dispatch(fetchCustomEmojisFail(error)); diff --git a/app/javascript/mastodon/actions/directory.js b/app/javascript/mastodon/actions/directory.js new file mode 100644 index 0000000000..cda63f2b5a --- /dev/null +++ b/app/javascript/mastodon/actions/directory.js @@ -0,0 +1,62 @@ +import api from '../api'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; +export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; +export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL'; + +export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST'; +export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS'; +export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL'; + +export const fetchDirectory = params => (dispatch, getState) => { + dispatch(fetchDirectoryRequest()); + + api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchDirectorySuccess(data)); + dispatch(fetchRelationships(data.map(x => x.id))); + }).catch(error => dispatch(fetchDirectoryFail(error))); +}; + +export const fetchDirectoryRequest = () => ({ + type: DIRECTORY_FETCH_REQUEST, +}); + +export const fetchDirectorySuccess = accounts => ({ + type: DIRECTORY_FETCH_SUCCESS, + accounts, +}); + +export const fetchDirectoryFail = error => ({ + type: DIRECTORY_FETCH_FAIL, + error, +}); + +export const expandDirectory = params => (dispatch, getState) => { + dispatch(expandDirectoryRequest()); + + const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size; + + api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(expandDirectorySuccess(data)); + dispatch(fetchRelationships(data.map(x => x.id))); + }).catch(error => dispatch(expandDirectoryFail(error))); +}; + +export const expandDirectoryRequest = () => ({ + type: DIRECTORY_EXPAND_REQUEST, +}); + +export const expandDirectorySuccess = accounts => ({ + type: DIRECTORY_EXPAND_SUCCESS, + accounts, +}); + +export const expandDirectoryFail = error => ({ + type: DIRECTORY_EXPAND_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/directory.ts b/app/javascript/mastodon/actions/directory.ts deleted file mode 100644 index 34ac309c66..0000000000 --- a/app/javascript/mastodon/actions/directory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { List as ImmutableList } from 'immutable'; - -import { apiGetDirectory } from 'mastodon/api/directory'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const fetchDirectory = createDataLoadingThunk( - 'directory/fetch', - async (params: Parameters[0]) => - apiGetDirectory(params), - (data, { dispatch }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchRelationships(data.map((x) => x.id))); - - return { accounts: data }; - }, -); - -export const expandDirectory = createDataLoadingThunk( - 'directory/expand', - async (params: Parameters[0], { getState }) => { - const loadedItems = getState().user_lists.getIn([ - 'directory', - 'items', - ]) as ImmutableList; - - return apiGetDirectory({ ...params, offset: loadedItems.size }, 20); - }, - (data, { dispatch }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchRelationships(data.map((x) => x.id))); - - return { accounts: data }; - }, -); diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js index 279ec1bef7..d06de20a2d 100644 --- a/app/javascript/mastodon/actions/domain_blocks.js +++ b/app/javascript/mastodon/actions/domain_blocks.js @@ -1,26 +1,30 @@ import api, { getLinks } from '../api'; -import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed"; -import { openModal } from './modal'; - - -export * from "./domain_blocks_typed"; - export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; +export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; +export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; +export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; +export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; + +export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST'; +export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS'; +export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; + export function blockDomain(domain) { return (dispatch, getState) => { dispatch(blockDomainRequest(domain)); - api().post('/api/v1/domain_blocks', { domain }).then(() => { + api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); - dispatch(blockDomainSuccess({ domain, accounts })); + dispatch(blockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(blockDomainFail(domain, err)); }); @@ -34,6 +38,14 @@ export function blockDomainRequest(domain) { }; } +export function blockDomainSuccess(domain, accounts) { + return { + type: DOMAIN_BLOCK_SUCCESS, + domain, + accounts, + }; +} + export function blockDomainFail(domain, error) { return { type: DOMAIN_BLOCK_FAIL, @@ -46,10 +58,10 @@ export function unblockDomain(domain) { return (dispatch, getState) => { dispatch(unblockDomainRequest(domain)); - api().delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { + api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); - dispatch(unblockDomainSuccess({ domain, accounts })); + dispatch(unblockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(unblockDomainFail(domain, err)); }); @@ -63,6 +75,14 @@ export function unblockDomainRequest(domain) { }; } +export function unblockDomainSuccess(domain, accounts) { + return { + type: DOMAIN_UNBLOCK_SUCCESS, + domain, + accounts, + }; +} + export function unblockDomainFail(domain, error) { return { type: DOMAIN_UNBLOCK_FAIL, @@ -71,11 +91,76 @@ export function unblockDomainFail(domain, 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'), - }, -})); +export function fetchDomainBlocks() { + return (dispatch, getState) => { + dispatch(fetchDomainBlocksRequest()); + + api(getState).get('/api/v1/domain_blocks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchDomainBlocksFail(err)); + }); + }; +} + +export function fetchDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_FETCH_REQUEST, + }; +} + +export function fetchDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_FETCH_SUCCESS, + domains, + next, + }; +} + +export function fetchDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_FETCH_FAIL, + error, + }; +} + +export function expandDomainBlocks() { + return (dispatch, getState) => { + const url = getState().getIn(['domain_lists', 'blocks', 'next']); + + if (!url) { + return; + } + + dispatch(expandDomainBlocksRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(expandDomainBlocksFail(err)); + }); + }; +} + +export function expandDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_EXPAND_REQUEST, + }; +} + +export function expandDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_EXPAND_SUCCESS, + domains, + next, + }; +} + +export function expandDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_EXPAND_FAIL, + error, + }; +} diff --git a/app/javascript/mastodon/actions/domain_blocks_typed.ts b/app/javascript/mastodon/actions/domain_blocks_typed.ts deleted file mode 100644 index 6a4cace0de..0000000000 --- a/app/javascript/mastodon/actions/domain_blocks_typed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { Account } from 'mastodon/models/account'; - -export const blockDomainSuccess = createAction<{ - domain: string; - accounts: Account[]; -}>('domain_blocks/block/SUCCESS'); - -export const unblockDomainSuccess = createAction<{ - domain: string; - accounts: Account[]; -}>('domain_blocks/unblock/SUCCESS'); diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js new file mode 100644 index 0000000000..023151d4bf --- /dev/null +++ b/app/javascript/mastodon/actions/dropdown_menu.js @@ -0,0 +1,10 @@ +export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; +export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; + +export function openDropdownMenu(id, keyboard, scroll_key) { + return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key }; +} + +export function closeDropdownMenu(id) { + return { type: DROPDOWN_MENU_CLOSE, id }; +} diff --git a/app/javascript/mastodon/actions/dropdown_menu.ts b/app/javascript/mastodon/actions/dropdown_menu.ts deleted file mode 100644 index d9d395ba33..0000000000 --- a/app/javascript/mastodon/actions/dropdown_menu.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -export const openDropdownMenu = createAction<{ - id: number; - keyboard: boolean; - scrollKey?: string; -}>('dropdownMenu/open'); - -export const closeDropdownMenu = createAction<{ id: number }>( - 'dropdownMenu/close', -); diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js index 6270f3473a..2d4d4e6206 100644 --- a/app/javascript/mastodon/actions/favourites.js +++ b/app/javascript/mastodon/actions/favourites.js @@ -1,5 +1,3 @@ -// Kmyblue tracking marker: copied emoji_reactions.js - import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; @@ -20,7 +18,7 @@ export function fetchFavouritedStatuses() { dispatch(fetchFavouritedStatusesRequest()); - api().get('/api/v1/favourites').then(response => { + api(getState).get('/api/v1/favourites').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); @@ -64,7 +62,7 @@ export function expandFavouritedStatuses() { dispatch(expandFavouritedStatusesRequest()); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); diff --git a/app/javascript/mastodon/actions/featured_tags.js b/app/javascript/mastodon/actions/featured_tags.js index 6ee4dee2bc..18bb615394 100644 --- a/app/javascript/mastodon/actions/featured_tags.js +++ b/app/javascript/mastodon/actions/featured_tags.js @@ -11,7 +11,7 @@ export const fetchFeaturedTags = (id) => (dispatch, getState) => { dispatch(fetchFeaturedTagsRequest(id)); - api().get(`/api/v1/accounts/${id}/featured_tags`) + api(getState).get(`/api/v1/accounts/${id}/featured_tags`) .then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data))) .catch(err => dispatch(fetchFeaturedTagsFail(id, err))); }; diff --git a/app/javascript/mastodon/actions/filters.js b/app/javascript/mastodon/actions/filters.js index 588e390f0a..a11956ac56 100644 --- a/app/javascript/mastodon/actions/filters.js +++ b/app/javascript/mastodon/actions/filters.js @@ -23,13 +23,13 @@ export const initAddFilter = (status, { contextType }) => dispatch => }, })); -export const fetchFilters = () => (dispatch) => { +export const fetchFilters = () => (dispatch, getState) => { dispatch({ type: FILTERS_FETCH_REQUEST, skipLoading: true, }); - api() + api(getState) .get('/api/v2/filters') .then(({ data }) => dispatch({ type: FILTERS_FETCH_SUCCESS, @@ -44,10 +44,10 @@ export const fetchFilters = () => (dispatch) => { })); }; -export const createFilterStatus = (params, onSuccess, onFail) => (dispatch) => { +export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => { dispatch(createFilterStatusRequest()); - api().post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => { + api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => { dispatch(createFilterStatusSuccess(response.data)); if (onSuccess) onSuccess(); }).catch(error => { @@ -70,10 +70,10 @@ export const createFilterStatusFail = error => ({ error, }); -export const createFilter = (params, onSuccess, onFail) => (dispatch) => { +export const createFilter = (params, onSuccess, onFail) => (dispatch, getState) => { dispatch(createFilterRequest()); - api().post('/api/v2/filters', params).then(response => { + api(getState).post('/api/v2/filters', params).then(response => { dispatch(createFilterSuccess(response.data)); if (onSuccess) onSuccess(response.data); }).catch(error => { diff --git a/app/javascript/mastodon/actions/history.js b/app/javascript/mastodon/actions/history.js index 07732ea187..52401b7dce 100644 --- a/app/javascript/mastodon/actions/history.js +++ b/app/javascript/mastodon/actions/history.js @@ -15,7 +15,7 @@ export const fetchHistory = statusId => (dispatch, getState) => { dispatch(fetchHistoryRequest(statusId)); - api().get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => { + api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => { dispatch(importFetchedAccounts(data.map(x => x.account))); dispatch(fetchHistorySuccess(statusId, data)); }).catch(error => dispatch(fetchHistoryFail(error))); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index fc165b1a1f..369be6b8fb 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,12 +1,10 @@ -import { createPollFromServerJSON } from 'mastodon/models/poll'; - -import { importAccounts } from '../accounts_typed'; - -import { normalizeStatus } from './normalizer'; -import { importPolls } from './polls'; +import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; +export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; +export const POLLS_IMPORT = 'POLLS_IMPORT'; export const FILTERS_IMPORT = 'FILTERS_IMPORT'; function pushUnique(array, object) { @@ -15,6 +13,14 @@ function pushUnique(array, object) { } } +export function importAccount(account) { + return { type: ACCOUNT_IMPORT, account }; +} + +export function importAccounts(accounts) { + return { type: ACCOUNTS_IMPORT, accounts }; +} + export function importStatus(status) { return { type: STATUS_IMPORT, status }; } @@ -27,6 +33,10 @@ export function importFilters(filters) { return { type: FILTERS_IMPORT, filters }; } +export function importPolls(polls) { + return { type: POLLS_IMPORT, polls }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -35,7 +45,7 @@ export function importFetchedAccounts(accounts) { const normalAccounts = []; function processAccount(account) { - pushUnique(normalAccounts, account); + pushUnique(normalAccounts, normalizeAccount(account)); if (account.moved) { processAccount(account.moved); @@ -44,7 +54,7 @@ export function importFetchedAccounts(accounts) { accounts.forEach(processAccount); - return importAccounts({ accounts: normalAccounts }); + return importAccounts(normalAccounts); } export function importFetchedStatus(status) { @@ -66,28 +76,26 @@ export function importFetchedStatuses(statuses) { status.filtered.forEach(result => pushUnique(filters, result.filter)); } - if (status.reblog?.id) { + if (status.reblog && status.reblog.id) { processStatus(status.reblog); } - if (status.quote?.id && !getState().getIn(['statuses', status.id])) { - processStatus(status.quote); - } - - if (status.poll?.id) { - pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); - } - - if (status.card) { - status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account)); + if (status.poll && status.poll.id) { + pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); } } statuses.forEach(processStatus); - dispatch(importPolls({ polls })); + dispatch(importPolls(polls)); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); dispatch(importFilters(filters)); }; } + +export function importFetchedPoll(poll) { + return (dispatch, getState) => { + dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))])); + }; +} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index b643cf5613..3220118f3d 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,18 +1,48 @@ import escapeTextContentForBrowser from 'escape-html'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; - import emojify from '../../features/emoji/emoji'; import { expandSpoilers, me } from '../../initial_state'; +import { unescapeHTML } from '../../utils/html'; const domParser = new DOMParser(); +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + export function searchTextFromRawStatus (status) { const spoilerText = status.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } +export function normalizeAccount(account) { + account = { ...account }; + + const emojiMap = makeEmojiMap(account.emojis); + const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; + + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); + account.note_plain = unescapeHTML(account.note); + + if (account.fields) { + account.fields = account.fields.map(pair => ({ + ...pair, + name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap), + value_emojified: emojify(pair.value, emojiMap), + value_plain: unescapeHTML(pair.value), + })); + } + + if (account.moved) { + account.moved = account.moved.id; + } + + return account; +} + export function normalizeFilterResult(result) { const normalResult = { ...result }; @@ -33,17 +63,6 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } - if (status.card) { - normalStatus.card = { - ...status.card, - authors: status.card.authors.map(author => ({ - ...author, - accountId: author.account?.id, - account: undefined, - })), - }; - } - if (status.filtered) { normalStatus.filtered = status.filtered.map(normalizeFilterResult); } @@ -66,11 +85,6 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.spoiler_text = normalOldStatus.get('spoiler_text'); normalStatus.hidden = normalOldStatus.get('hidden'); - // for quoted post - if (!normalStatus.filtered && normalOldStatus.get('filtered')) { - normalStatus.filtered = normalOldStatus.get('filtered'); - } - if (normalOldStatus.get('translation')) { normalStatus.translation = normalOldStatus.get('translation'); } @@ -102,7 +116,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.media_attachments.forEach(item => { const oldItem = list.find(i => i.get('id') === item.id); if (oldItem && oldItem.get('description') === item.description) { - item.translation = oldItem.get('translation'); + item.translation = oldItem.get('translation') } }); } @@ -137,6 +151,38 @@ export function normalizeStatusTranslation(translation, status) { return normalTranslation; } +export function normalizePoll(poll, normalOldPoll) { + const normalPoll = { ...poll }; + const emojiMap = makeEmojiMap(poll.emojis); + + normalPoll.options = poll.options.map((option, index) => { + const normalOption = { + ...option, + voted: poll.own_votes && poll.own_votes.includes(index), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), + } + + if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) { + normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']); + } + + return normalOption + }); + + return normalPoll; +} + +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; const emojiMap = makeEmojiMap(normalAnnouncement.emojis); diff --git a/app/javascript/mastodon/actions/importer/polls.ts b/app/javascript/mastodon/actions/importer/polls.ts deleted file mode 100644 index 5bbe7d57d6..0000000000 --- a/app/javascript/mastodon/actions/importer/polls.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { Poll } from 'mastodon/models/poll'; - -export const importPolls = createAction<{ polls: Poll[] }>( - 'poll/importMultiple', -); diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 1392d3a667..b361809309 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -1,11 +1,11 @@ -import { boostModal } from 'mastodon/initial_state'; - import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer'; -import { unreblog, reblog } from './interactions_typed'; -import { openModal } from './modal'; + +export const REBLOG_REQUEST = 'REBLOG_REQUEST'; +export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; +export const REBLOG_FAIL = 'REBLOG_FAIL'; export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST'; export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS'; @@ -19,6 +19,10 @@ export const EMOJIREACT_REQUEST = 'EMOJIREACT_REQUEST'; export const EMOJIREACT_SUCCESS = 'EMOJIREACT_SUCCESS'; export const EMOJIREACT_FAIL = 'EMOJIREACT_FAIL'; +export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; +export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; +export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; + export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; @@ -67,21 +71,89 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; -export const MENTIONED_USERS_FETCH_REQUEST = 'MENTIONED_USERS_FETCH_REQUEST'; -export const MENTIONED_USERS_FETCH_SUCCESS = 'MENTIONED_USERS_FETCH_SUCCESS'; -export const MENTIONED_USERS_FETCH_FAIL = 'MENTIONED_USERS_FETCH_FAIL'; +export function reblog(status, visibility) { + return function (dispatch, getState) { + dispatch(reblogRequest(status)); -export const MENTIONED_USERS_EXPAND_REQUEST = 'MENTIONED_USERS_EXPAND_REQUEST'; -export const MENTIONED_USERS_EXPAND_SUCCESS = 'MENTIONED_USERS_EXPAND_SUCCESS'; -export const MENTIONED_USERS_EXPAND_FAIL = 'MENTIONED_USERS_EXPAND_FAIL'; + api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) { + // The reblog API method returns a new status wrapped around the original. In this case we are only + // interested in how the original is modified, hence passing it skipping the wrapper + dispatch(importFetchedStatus(response.data.reblog)); + dispatch(reblogSuccess(status)); + }).catch(function (error) { + dispatch(reblogFail(status, error)); + }); + }; +} -export * from "./interactions_typed"; +export function unreblog(status) { + return (dispatch, getState) => { + dispatch(unreblogRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unreblogSuccess(status)); + }).catch(error => { + dispatch(unreblogFail(status, error)); + }); + }; +} + +export function reblogRequest(status) { + return { + type: REBLOG_REQUEST, + status: status, + skipLoading: true, + }; +} + +export function reblogSuccess(status) { + return { + type: REBLOG_SUCCESS, + status: status, + skipLoading: true, + }; +} + +export function reblogFail(status, error) { + return { + type: REBLOG_FAIL, + status: status, + error: error, + skipLoading: true, + }; +} + +export function unreblogRequest(status) { + return { + type: UNREBLOG_REQUEST, + status: status, + skipLoading: true, + }; +} + +export function unreblogSuccess(status) { + return { + type: UNREBLOG_SUCCESS, + status: status, + skipLoading: true, + }; +} + +export function unreblogFail(status, error) { + return { + type: UNREBLOG_FAIL, + status: status, + error: error, + skipLoading: true, + }; +} export function favourite(status) { - return function (dispatch) { + return function (dispatch, getState) { dispatch(favouriteRequest(status)); - api().post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { + api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { dispatch(importFetchedStatus(response.data)); dispatch(favouriteSuccess(status)); }).catch(function (error) { @@ -91,10 +163,10 @@ export function favourite(status) { } export function unfavourite(status) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unfavouriteRequest(status)); - api().post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { + api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unfavouriteSuccess(status)); }).catch(error => { @@ -239,10 +311,10 @@ export function unEmojiReactFail(status, emoji, error) { } export function bookmark(status) { - return function (dispatch) { + return function (dispatch, getState) { dispatch(bookmarkRequest(status)); - api().post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) { + api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) { dispatch(importFetchedStatus(response.data)); dispatch(bookmarkSuccess(status, response.data)); }).catch(function (error) { @@ -252,10 +324,10 @@ export function bookmark(status) { } export function unbookmark(status) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unbookmarkRequest(status)); - api().post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { + api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unbookmarkSuccess(status, response.data)); }).catch(error => { @@ -311,10 +383,10 @@ export function unbookmarkFail(status, error) { } export function fetchReblogs(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchReblogsRequest(id)); - api().get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { + api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null)); @@ -358,7 +430,7 @@ export function expandReblogs(id) { dispatch(expandReblogsRequest(id)); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -393,10 +465,10 @@ export function expandReblogsFail(id, error) { } export function fetchFavourites(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchFavouritesRequest(id)); - api().get(`/api/v1/statuses/${id}/favourited_by`).then(response => { + api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null)); @@ -440,7 +512,7 @@ export function expandFavourites(id) { dispatch(expandFavouritesRequest(id)); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); @@ -589,10 +661,10 @@ export function fetchStatusReferencesFail(id, error) { } export function pin(status) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(pinRequest(status)); - api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { + api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(pinSuccess(status)); }).catch(error => { @@ -627,10 +699,10 @@ export function pinFail(status, error) { } export function unpin (status) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unpinRequest(status)); - api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { + api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unpinSuccess(status)); }).catch(error => { @@ -663,131 +735,3 @@ export function unpinFail(status, error) { skipLoading: true, }; } - -export function fetchMentionedUsers(id) { - return (dispatch, getState) => { - dispatch(fetchMentionedUsersRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/mentioned_by`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchMentionedUsersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => { - dispatch(fetchMentionedUsersFail(id, error)); - }); - }; -} - -export function fetchMentionedUsersRequest(id) { - return { - type: MENTIONED_USERS_FETCH_REQUEST, - id, - }; -} - -export function fetchMentionedUsersSuccess(id, accounts, next) { - return { - type: MENTIONED_USERS_FETCH_SUCCESS, - id, - accounts, - next, - }; -} - -export function fetchMentionedUsersFail(id, error) { - return { - type: MENTIONED_USERS_FETCH_FAIL, - id, - error, - }; -} - -export function expandMentionedUsers(id) { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'mentioned_users', id, 'next']); - if (url === null) { - return; - } - - dispatch(expandMentionedUsersRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(expandMentionedUsersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(expandMentionedUsersFail(id, error))); - }; -} - -export function expandMentionedUsersRequest(id) { - return { - type: MENTIONED_USERS_EXPAND_REQUEST, - id, - }; -} - -export function expandMentionedUsersSuccess(id, accounts, next) { - return { - type: MENTIONED_USERS_EXPAND_SUCCESS, - id, - accounts, - next, - }; -} - -export function expandMentionedUsersFail(id, error) { - return { - type: MENTIONED_USERS_EXPAND_FAIL, - id, - error, - }; -} - -function toggleReblogWithoutConfirmation(status, visibility) { - return (dispatch) => { - if (status.get('reblogged')) { - dispatch(unreblog({ statusId: status.get('id') })); - } else { - dispatch(reblog({ statusId: status.get('id'), visibility })); - } - }; -} - -export function toggleReblog(statusId, skipModal = false, forceModal = false) { - return (dispatch, getState) => { - const state = getState(); - let status = state.statuses.get(statusId); - - if (!status) - return; - - // The reblog modal expects a pre-filled account in status - // TODO: fix this by having the reblog modal get a statusId and do the work itself - status = status.set('account', state.accounts.get(status.get('account'))); - - if ((boostModal && !skipModal) || (forceModal && !status.get('reblogged'))) { - dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } })); - } else { - dispatch(toggleReblogWithoutConfirmation(status)); - } - }; -} - -export function toggleFavourite(statusId) { - return (dispatch, getState) => { - const state = getState(); - const status = state.statuses.get(statusId); - - if (!status) - return; - - if (status.get('favourited')) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }; -} diff --git a/app/javascript/mastodon/actions/interactions_typed.ts b/app/javascript/mastodon/actions/interactions_typed.ts deleted file mode 100644 index f58faffa86..0000000000 --- a/app/javascript/mastodon/actions/interactions_typed.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { apiReblog, apiUnreblog } from 'mastodon/api/interactions'; -import type { StatusVisibility } from 'mastodon/models/status'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -import { importFetchedStatus } from './importer'; - -export const reblog = createDataLoadingThunk( - 'status/reblog', - ({ - statusId, - visibility, - }: { - statusId: string; - visibility: StatusVisibility; - }) => apiReblog(statusId, visibility), - (data, { dispatch, discardLoadData }) => { - // The reblog API method returns a new status wrapped around the original. In this case we are only - // interested in how the original is modified, hence passing it skipping the wrapper - dispatch(importFetchedStatus(data.reblog)); - - // The payload is not used in any actions - return discardLoadData; - }, -); - -export const unreblog = createDataLoadingThunk( - 'status/unreblog', - ({ statusId }: { statusId: string }) => apiUnreblog(statusId), - (data, { dispatch, discardLoadData }) => { - dispatch(importFetchedStatus(data)); - - // The payload is not used in any actions - return discardLoadData; - }, -); diff --git a/app/javascript/mastodon/actions/languages.js b/app/javascript/mastodon/actions/languages.js new file mode 100644 index 0000000000..ad186ba0cc --- /dev/null +++ b/app/javascript/mastodon/actions/languages.js @@ -0,0 +1,12 @@ +import { saveSettings } from './settings'; + +export const LANGUAGE_USE = 'LANGUAGE_USE'; + +export const useLanguage = language => dispatch => { + dispatch({ + type: LANGUAGE_USE, + language, + }); + + dispatch(saveSettings()); +}; diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index 7b6dd22041..e5c606f4ec 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -1,7 +1,8 @@ -// Kmyblue tracking marker: copied circles.js, antennas.js - import api from '../api'; +import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; + export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; @@ -10,10 +11,45 @@ export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST'; export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS'; export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL'; +export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE'; +export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET'; +export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP'; + +export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST'; +export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS'; +export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL'; + +export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST'; +export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS'; +export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL'; + export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST'; export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS'; export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL'; +export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST'; +export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS'; +export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL'; + +export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE'; +export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY'; +export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR'; + +export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST'; +export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS'; +export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL'; + +export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST'; +export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS'; +export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL'; + +export const LIST_ADDER_RESET = 'LIST_ADDER_RESET'; +export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP'; + +export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST'; +export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS'; +export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL'; + export const fetchList = id => (dispatch, getState) => { if (getState().getIn(['lists', id])) { return; @@ -21,7 +57,7 @@ export const fetchList = id => (dispatch, getState) => { dispatch(fetchListRequest(id)); - api().get(`/api/v1/lists/${id}`) + api(getState).get(`/api/v1/lists/${id}`) .then(({ data }) => dispatch(fetchListSuccess(data))) .catch(err => dispatch(fetchListFail(id, err))); }; @@ -42,10 +78,10 @@ export const fetchListFail = (id, error) => ({ error, }); -export const fetchLists = () => (dispatch) => { +export const fetchLists = () => (dispatch, getState) => { dispatch(fetchListsRequest()); - api().get('/api/v1/lists') + api(getState).get('/api/v1/lists') .then(({ data }) => dispatch(fetchListsSuccess(data))) .catch(err => dispatch(fetchListsFail(err))); }; @@ -64,10 +100,93 @@ export const fetchListsFail = error => ({ error, }); -export const deleteList = id => (dispatch) => { +export const submitListEditor = shouldReset => (dispatch, getState) => { + const listId = getState().getIn(['listEditor', 'listId']); + const title = getState().getIn(['listEditor', 'title']); + + if (listId === null) { + dispatch(createList(title, shouldReset)); + } else { + dispatch(updateList(listId, title, shouldReset)); + } +}; + +export const setupListEditor = listId => (dispatch, getState) => { + dispatch({ + type: LIST_EDITOR_SETUP, + list: getState().getIn(['lists', listId]), + }); + + dispatch(fetchListAccounts(listId)); +}; + +export const changeListEditorTitle = value => ({ + type: LIST_EDITOR_TITLE_CHANGE, + value, +}); + +export const createList = (title, shouldReset) => (dispatch, getState) => { + dispatch(createListRequest()); + + api(getState).post('/api/v1/lists', { title }).then(({ data }) => { + dispatch(createListSuccess(data)); + + if (shouldReset) { + dispatch(resetListEditor()); + } + }).catch(err => dispatch(createListFail(err))); +}; + +export const createListRequest = () => ({ + type: LIST_CREATE_REQUEST, +}); + +export const createListSuccess = list => ({ + type: LIST_CREATE_SUCCESS, + list, +}); + +export const createListFail = error => ({ + type: LIST_CREATE_FAIL, + error, +}); + +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { + dispatch(updateListRequest(id)); + + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { + dispatch(updateListSuccess(data)); + + if (shouldReset) { + dispatch(resetListEditor()); + } + }).catch(err => dispatch(updateListFail(id, err))); +}; + +export const updateListRequest = id => ({ + type: LIST_UPDATE_REQUEST, + id, +}); + +export const updateListSuccess = list => ({ + type: LIST_UPDATE_SUCCESS, + list, +}); + +export const updateListFail = (id, error) => ({ + type: LIST_UPDATE_FAIL, + id, + error, +}); + +export const resetListEditor = () => ({ + type: LIST_EDITOR_RESET, +}); + +export const deleteList = id => (dispatch, getState) => { dispatch(deleteListRequest(id)); - api().delete(`/api/v1/lists/${id}`) + api(getState).delete(`/api/v1/lists/${id}`) .then(() => dispatch(deleteListSuccess(id))) .catch(err => dispatch(deleteListFail(id, err))); }; @@ -87,3 +206,167 @@ export const deleteListFail = (id, error) => ({ id, error, }); + +export const fetchListAccounts = listId => (dispatch, getState) => { + dispatch(fetchListAccountsRequest(listId)); + + api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListAccountsSuccess(listId, data)); + }).catch(err => dispatch(fetchListAccountsFail(listId, err))); +}; + +export const fetchListAccountsRequest = id => ({ + type: LIST_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchListAccountsSuccess = (id, accounts, next) => ({ + type: LIST_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchListAccountsFail = (id, error) => ({ + type: LIST_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +export const fetchListSuggestions = q => (dispatch, getState) => { + const params = { + q, + resolve: false, + following: true, + }; + + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); +}; + +export const fetchListSuggestionsReady = (query, accounts) => ({ + type: LIST_EDITOR_SUGGESTIONS_READY, + query, + accounts, +}); + +export const clearListSuggestions = () => ({ + type: LIST_EDITOR_SUGGESTIONS_CLEAR, +}); + +export const changeListSuggestions = value => ({ + type: LIST_EDITOR_SUGGESTIONS_CHANGE, + value, +}); + +export const addToListEditor = accountId => (dispatch, getState) => { + dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId)); +}; + +export const addToList = (listId, accountId) => (dispatch, getState) => { + dispatch(addToListRequest(listId, accountId)); + + api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addToListSuccess(listId, accountId))) + .catch(err => dispatch(addToListFail(listId, accountId, err))); +}; + +export const addToListRequest = (listId, accountId) => ({ + type: LIST_EDITOR_ADD_REQUEST, + listId, + accountId, +}); + +export const addToListSuccess = (listId, accountId) => ({ + type: LIST_EDITOR_ADD_SUCCESS, + listId, + accountId, +}); + +export const addToListFail = (listId, accountId, error) => ({ + type: LIST_EDITOR_ADD_FAIL, + listId, + accountId, + error, +}); + +export const removeFromListEditor = accountId => (dispatch, getState) => { + dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId)); +}; + +export const removeFromList = (listId, accountId) => (dispatch, getState) => { + dispatch(removeFromListRequest(listId, accountId)); + + api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeFromListSuccess(listId, accountId))) + .catch(err => dispatch(removeFromListFail(listId, accountId, err))); +}; + +export const removeFromListRequest = (listId, accountId) => ({ + type: LIST_EDITOR_REMOVE_REQUEST, + listId, + accountId, +}); + +export const removeFromListSuccess = (listId, accountId) => ({ + type: LIST_EDITOR_REMOVE_SUCCESS, + listId, + accountId, +}); + +export const removeFromListFail = (listId, accountId, error) => ({ + type: LIST_EDITOR_REMOVE_FAIL, + listId, + accountId, + error, +}); + +export const resetListAdder = () => ({ + type: LIST_ADDER_RESET, +}); + +export const setupListAdder = accountId => (dispatch, getState) => { + dispatch({ + type: LIST_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchLists()); + dispatch(fetchAccountLists(accountId)); +}; + +export const fetchAccountLists = accountId => (dispatch, getState) => { + dispatch(fetchAccountListsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/lists`) + .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountListsFail(accountId, err))); +}; + +export const fetchAccountListsRequest = id => ({ + type:LIST_ADDER_LISTS_FETCH_REQUEST, + id, +}); + +export const fetchAccountListsSuccess = (id, lists) => ({ + type: LIST_ADDER_LISTS_FETCH_SUCCESS, + id, + lists, +}); + +export const fetchAccountListsFail = (id, err) => ({ + type: LIST_ADDER_LISTS_FETCH_FAIL, + id, + err, +}); + +export const addToListAdder = listId => (dispatch, getState) => { + dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId']))); +}; + +export const removeFromListAdder = listId => (dispatch, getState) => { + dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId']))); +}; + diff --git a/app/javascript/mastodon/actions/lists_typed.ts b/app/javascript/mastodon/actions/lists_typed.ts deleted file mode 100644 index eca051f52c..0000000000 --- a/app/javascript/mastodon/actions/lists_typed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { apiCreate, apiUpdate } from 'mastodon/api/lists'; -import type { List } from 'mastodon/models/list'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const createList = createDataLoadingThunk( - 'list/create', - (list: Partial) => apiCreate(list), -); - -// Kmyblue tracking marker: copied antenna, circle, bookmark_category - -export const updateList = createDataLoadingThunk( - 'list/update', - (list: Partial) => apiUpdate(list), -); - -// Kmyblue tracking marker: copied antenna, circle, bookmark_category diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js new file mode 100644 index 0000000000..cfc329a8b7 --- /dev/null +++ b/app/javascript/mastodon/actions/markers.js @@ -0,0 +1,152 @@ +import { List as ImmutableList } from 'immutable'; + +import { debounce } from 'lodash'; + +import api from '../api'; +import { compareId } from '../compare_id'; + +export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; +export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; +export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; +export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; + +export const synchronouslySubmitMarkers = () => (dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = _buildParams(getState()); + + if (Object.keys(params).length === 0 || accessToken === '') { + return; + } + + // The Fetch API allows us to perform requests that will be carried out + // after the page closes. But that only works if the `keepalive` attribute + // is supported. + if (window.fetch && 'keepalive' in new Request('')) { + fetch('/api/v1/markers', { + keepalive: true, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + body: JSON.stringify(params), + }); + + return; + } else if (navigator && navigator.sendBeacon) { + // Failing that, we can use sendBeacon, but we have to encode the data as + // FormData for DoorKeeper to recognize the token. + const formData = new FormData(); + + formData.append('bearer_token', accessToken); + + for (const [id, value] of Object.entries(params)) { + formData.append(`${id}[last_read_id]`, value.last_read_id); + } + + if (navigator.sendBeacon('/api/v1/markers', formData)) { + return; + } + } + + // If neither Fetch nor sendBeacon worked, try to perform a synchronous + // request. + try { + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.send(JSON.stringify(params)); + } catch (e) { + // Do not make the BeforeUnload handler error out + } +}; + +const _buildParams = (state) => { + const params = {}; + + const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null); + const lastNotificationId = state.getIn(['notifications', 'lastReadId']); + + if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) { + params.notifications = { + last_read_id: lastNotificationId, + }; + } + + return params; +}; + +const debouncedSubmitMarkers = debounce((dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = _buildParams(getState()); + + if (Object.keys(params).length === 0 || accessToken === '') { + return; + } + + api(getState).post('/api/v1/markers', params).then(() => { + dispatch(submitMarkersSuccess(params)); + }).catch(() => {}); +}, 300000, { leading: true, trailing: true }); + +export function submitMarkersSuccess({ home, notifications }) { + return { + type: MARKERS_SUBMIT_SUCCESS, + home: (home || {}).last_read_id, + notifications: (notifications || {}).last_read_id, + }; +} + +export function submitMarkers(params = {}) { + const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); + + if (params.immediate === true) { + debouncedSubmitMarkers.flush(); + } + + return result; +} + +export const fetchMarkers = () => (dispatch, getState) => { + const params = { timeline: ['notifications'] }; + + dispatch(fetchMarkersRequest()); + + api(getState).get('/api/v1/markers', { params }).then(response => { + dispatch(fetchMarkersSuccess(response.data)); + }).catch(error => { + dispatch(fetchMarkersFail(error)); + }); +}; + +export function fetchMarkersRequest() { + return { + type: MARKERS_FETCH_REQUEST, + skipLoading: true, + }; +} + +export function fetchMarkersSuccess(markers) { + return { + type: MARKERS_FETCH_SUCCESS, + markers, + skipLoading: true, + }; +} + +export function fetchMarkersFail(error) { + return { + type: MARKERS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +} diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts deleted file mode 100644 index 251546cb9a..0000000000 --- a/app/javascript/mastodon/actions/markers.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { debounce } from 'lodash'; - -import type { MarkerJSON } from 'mastodon/api_types/markers'; -import { getAccessToken } from 'mastodon/initial_state'; -import type { AppDispatch, RootState } from 'mastodon/store'; -import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; - -import api from '../api'; -import { compareId } from '../compare_id'; - -export const synchronouslySubmitMarkers = createAppAsyncThunk( - 'markers/submit', - async (_args, { getState }) => { - const accessToken = getAccessToken(); - const params = buildPostMarkersParams(getState()); - - if ( - Object.keys(params).length === 0 || - !accessToken || - accessToken === '' - ) { - return; - } - - // The Fetch API allows us to perform requests that will be carried out - // after the page closes. But that only works if the `keepalive` attribute - // is supported. - if ('fetch' in window && 'keepalive' in new Request('')) { - await fetch('/api/v1/markers', { - keepalive: true, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify(params), - }); - - return; - } else if ('sendBeacon' in navigator) { - // Failing that, we can use sendBeacon, but we have to encode the data as - // FormData for DoorKeeper to recognize the token. - const formData = new FormData(); - - formData.append('bearer_token', accessToken); - - for (const [id, value] of Object.entries(params)) { - if (value.last_read_id) - formData.append(`${id}[last_read_id]`, value.last_read_id); - } - - if (navigator.sendBeacon('/api/v1/markers', formData)) { - return; - } - } - - // If neither Fetch nor sendBeacon worked, try to perform a synchronous - // request. - try { - const client = new XMLHttpRequest(); - - client.open('POST', '/api/v1/markers', false); - client.setRequestHeader('Content-Type', 'application/json'); - client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.send(JSON.stringify(params)); - } catch { - // Do not make the BeforeUnload handler error out - } - }, -); - -interface MarkerParam { - last_read_id?: string; -} - -function getLastNotificationId(state: RootState): string | undefined { - return state.notificationGroups.lastReadId; -} - -const buildPostMarkersParams = (state: RootState) => { - const params = {} as { home?: MarkerParam; notifications?: MarkerParam }; - - const lastNotificationId = getLastNotificationId(state); - - if ( - lastNotificationId && - compareId(lastNotificationId, state.markers.notifications) > 0 - ) { - params.notifications = { - last_read_id: lastNotificationId, - }; - } - - return params; -}; - -export const submitMarkersAction = createAppAsyncThunk<{ - home: string | undefined; - notifications: string | undefined; -}>('markers/submitAction', async (_args, { getState }) => { - const accessToken = getAccessToken(); - const params = buildPostMarkersParams(getState()); - - if (Object.keys(params).length === 0 || !accessToken || accessToken === '') { - return { home: undefined, notifications: undefined }; - } - - await api().post('/api/v1/markers', params); - - return { - home: params.home?.last_read_id, - notifications: params.notifications?.last_read_id, - }; -}); - -const debouncedSubmitMarkers = debounce( - (dispatch: AppDispatch) => { - void dispatch(submitMarkersAction()); - }, - 300000, - { - leading: true, - trailing: true, - }, -); - -export const submitMarkers = createAppAsyncThunk( - 'markers/submit', - (params: { immediate?: boolean }, { dispatch }) => { - debouncedSubmitMarkers(dispatch); - - if (params.immediate) { - debouncedSubmitMarkers.flush(); - } - }, -); - -export const fetchMarkers = createAppAsyncThunk('markers/fetch', async () => { - const response = await api().get>( - `/api/v1/markers`, - { params: { timeline: ['notifications'] } }, - ); - - return { markers: response.data }; -}); diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts index 49af176a11..af34f5d6af 100644 --- a/app/javascript/mastodon/actions/modal.ts +++ b/app/javascript/mastodon/actions/modal.ts @@ -1,15 +1,12 @@ import { createAction } from '@reduxjs/toolkit'; -import type { ModalProps } from 'mastodon/reducers/modal'; - import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root'; export type ModalType = keyof typeof MODAL_COMPONENTS; interface OpenModalPayload { modalType: ModalType; - modalProps: ModalProps; - previousModalProps?: ModalProps; + modalProps: unknown; } export const openModal = createAction('MODAL_OPEN'); diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index 3676748cf3..fb041078b8 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -12,11 +12,15 @@ 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) => { + return (dispatch, getState) => { dispatch(fetchMutesRequest()); - api().get('/api/v1/mutes').then(response => { + api(getState).get('/api/v1/mutes').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); @@ -56,7 +60,7 @@ export function expandMutes() { dispatch(expandMutesRequest()); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); @@ -88,12 +92,26 @@ export function expandMutesFail(error) { export function initMuteModal(account) { return dispatch => { - dispatch(openModal({ - modalType: 'MUTE', - modalProps: { - accountId: account.get('id'), - acct: account.get('acct'), - }, - })); + 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, + }); }; } diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts deleted file mode 100644 index c7b192accc..0000000000 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import { - apiClearNotifications, - apiFetchNotificationGroups, -} from 'mastodon/api/notifications'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { - ApiNotificationGroupJSON, - ApiNotificationJSON, - NotificationType, -} from 'mastodon/api_types/notifications'; -import { allNotificationTypes } from 'mastodon/api_types/notifications'; -import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; -import { enableEmojiReaction, usePendingItems } from 'mastodon/initial_state'; -import type { NotificationGap } from 'mastodon/reducers/notification_groups'; -import { - selectSettingsNotificationsExcludedTypes, - selectSettingsNotificationsGroupFollows, - selectSettingsNotificationsQuickFilterActive, - selectSettingsNotificationsShows, -} from 'mastodon/selectors/settings'; -import type { AppDispatch, RootState } from 'mastodon/store'; -import { - createAppAsyncThunk, - createDataLoadingThunk, -} from 'mastodon/store/typed_functions'; - -import { importFetchedAccounts, importFetchedStatuses } from './importer'; -import { NOTIFICATIONS_FILTER_SET } from './notifications'; -import { saveSettings } from './settings'; - -function excludeAllTypesExcept(filter: string) { - return allNotificationTypes.filter((item) => item !== filter); -} - -function getExcludedTypes(state: RootState) { - const activeFilter = selectSettingsNotificationsQuickFilterActive(state); - - const types = - activeFilter === 'all' - ? selectSettingsNotificationsExcludedTypes(state) - : excludeAllTypesExcept(activeFilter); - if (!enableEmojiReaction && !types.includes('emoji_reaction')) { - types.push('emoji_reaction'); - } - - return types; -} - -function dispatchAssociatedRecords( - dispatch: AppDispatch, - notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[], -) { - const fetchedAccounts: ApiAccountJSON[] = []; - const fetchedStatuses: ApiStatusJSON[] = []; - - notifications.forEach((notification) => { - if (notification.type === 'admin.report') { - fetchedAccounts.push(notification.report.target_account); - } - - if (notification.type === 'moderation_warning') { - fetchedAccounts.push(notification.moderation_warning.target_account); - } - - if ('status' in notification && notification.status) { - fetchedStatuses.push(notification.status); - } - }); - - if (fetchedAccounts.length > 0) - dispatch(importFetchedAccounts(fetchedAccounts)); - - if (fetchedStatuses.length > 0) - dispatch(importFetchedStatuses(fetchedStatuses)); -} - -function selectNotificationGroupedTypes(state: RootState) { - const types: NotificationType[] = ['favourite', 'reblog', 'emoji_reaction']; - - if (selectSettingsNotificationsGroupFollows(state)) types.push('follow'); - - return types; -} - -export const fetchNotifications = createDataLoadingThunk( - 'notificationGroups/fetch', - async (_params, { getState }) => - apiFetchNotificationGroups({ - grouped_types: selectNotificationGroupedTypes(getState()), - exclude_types: getExcludedTypes(getState()), - }), - ({ notifications, accounts, statuses }, { dispatch }) => { - dispatch(importFetchedAccounts(accounts)); - dispatch(importFetchedStatuses(statuses)); - dispatchAssociatedRecords(dispatch, notifications); - const payload: (ApiNotificationGroupJSON | NotificationGap)[] = - notifications; - - // TODO: might be worth not using gaps for that… - // if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri }); - if (notifications.length > 1) - payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id }); - - return payload; - // dispatch(submitMarkers()); - }, -); - -export const fetchNotificationsGap = createDataLoadingThunk( - 'notificationGroups/fetchGap', - async (params: { gap: NotificationGap }, { getState }) => - apiFetchNotificationGroups({ - grouped_types: selectNotificationGroupedTypes(getState()), - max_id: params.gap.maxId, - exclude_types: getExcludedTypes(getState()), - }), - ({ notifications, accounts, statuses }, { dispatch }) => { - dispatch(importFetchedAccounts(accounts)); - dispatch(importFetchedStatuses(statuses)); - dispatchAssociatedRecords(dispatch, notifications); - - return { notifications }; - }, -); - -export const pollRecentNotifications = createDataLoadingThunk( - 'notificationGroups/pollRecentNotifications', - async (_params, { getState }) => { - return apiFetchNotificationGroups({ - grouped_types: selectNotificationGroupedTypes(getState()), - max_id: undefined, - exclude_types: getExcludedTypes(getState()), - // In slow mode, we don't want to include notifications that duplicate the already-displayed ones - since_id: usePendingItems - ? getState().notificationGroups.groups.find( - (group) => group.type !== 'gap', - )?.page_max_id - : undefined, - }); - }, - ({ notifications, accounts, statuses }, { dispatch }) => { - dispatch(importFetchedAccounts(accounts)); - dispatch(importFetchedStatuses(statuses)); - dispatchAssociatedRecords(dispatch, notifications); - - return { notifications }; - }, - { - useLoadingBar: false, - }, -); - -export const processNewNotificationForGroups = createAppAsyncThunk( - 'notificationGroups/processNew', - (notification: ApiNotificationJSON, { dispatch, getState }) => { - const state = getState(); - const activeFilter = selectSettingsNotificationsQuickFilterActive(state); - const notificationShows = selectSettingsNotificationsShows(state); - - const showInColumn = - activeFilter === 'all' - ? notificationShows[notification.type] !== false - : activeFilter === notification.type; - - if (!showInColumn) return; - - if ( - (notification.type === 'mention' || notification.type === 'update') && - notification.status?.filtered - ) { - const filters = notification.status.filtered.filter((result) => - result.filter.context.includes('notifications'), - ); - - if (filters.some((result) => result.filter.filter_action === 'hide')) { - return; - } - } - - dispatchAssociatedRecords(dispatch, [notification]); - - return { - notification, - groupedTypes: selectNotificationGroupedTypes(state), - }; - }, -); - -export const loadPending = createAction('notificationGroups/loadPending'); - -export const updateScrollPosition = createAppAsyncThunk( - 'notificationGroups/updateScrollPosition', - ({ top }: { top: boolean }, { dispatch, getState }) => { - if ( - top && - getState().notificationGroups.mergedNotifications === 'needs-reload' - ) { - void dispatch(fetchNotifications()); - } - - return { top }; - }, -); - -export const setNotificationsFilter = createAppAsyncThunk( - 'notifications/filter/set', - ({ filterType }: { filterType: string }, { dispatch }) => { - dispatch({ - type: NOTIFICATIONS_FILTER_SET, - path: ['notifications', 'quickFilter', 'active'], - value: filterType, - }); - void dispatch(fetchNotifications()); - dispatch(saveSettings()); - }, -); - -export const clearNotifications = createDataLoadingThunk( - 'notifications/clear', - () => apiClearNotifications(), -); - -export const markNotificationsAsRead = createAction( - 'notificationGroups/markAsRead', -); - -export const mountNotifications = createAppAsyncThunk( - 'notificationGroups/mount', - (_, { dispatch, getState }) => { - const state = getState(); - - if ( - state.notificationGroups.mounted === 0 && - state.notificationGroups.mergedNotifications === 'needs-reload' - ) { - void dispatch(fetchNotifications()); - } - }, -); - -export const unmountNotifications = createAction('notificationGroups/unmount'); - -export const refreshStaleNotificationGroups = createAppAsyncThunk<{ - deferredRefresh: boolean; -}>('notificationGroups/refreshStale', (_, { dispatch, getState }) => { - const state = getState(); - - if ( - state.notificationGroups.scrolledToTop || - !state.notificationGroups.mounted - ) { - void dispatch(fetchNotifications()); - return { deferredRefresh: false }; - } - - return { deferredRefresh: true }; -}); diff --git a/app/javascript/mastodon/actions/notification_policies.ts b/app/javascript/mastodon/actions/notification_policies.ts deleted file mode 100644 index fd798eaad7..0000000000 --- a/app/javascript/mastodon/actions/notification_policies.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import { - apiGetNotificationPolicy, - apiUpdateNotificationsPolicy, -} from 'mastodon/api/notification_policies'; -import type { NotificationPolicy } from 'mastodon/models/notification_policy'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const fetchNotificationPolicy = createDataLoadingThunk( - 'notificationPolicy/fetch', - () => apiGetNotificationPolicy(), -); - -export const updateNotificationsPolicy = createDataLoadingThunk( - 'notificationPolicy/update', - (policy: Partial) => apiUpdateNotificationsPolicy(policy), -); - -export const decreasePendingRequestsCount = createAction( - 'notificationPolicy/decreasePendingRequestsCount', -); diff --git a/app/javascript/mastodon/actions/notification_requests.ts b/app/javascript/mastodon/actions/notification_requests.ts deleted file mode 100644 index 8352ff2aad..0000000000 --- a/app/javascript/mastodon/actions/notification_requests.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { - apiFetchNotificationRequest, - apiFetchNotificationRequests, - apiFetchNotifications, - apiAcceptNotificationRequest, - apiDismissNotificationRequest, - apiAcceptNotificationRequests, - apiDismissNotificationRequests, -} from 'mastodon/api/notifications'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { - ApiNotificationGroupJSON, - ApiNotificationJSON, -} from 'mastodon/api_types/notifications'; -import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; -import type { AppDispatch } from 'mastodon/store'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -import { importFetchedAccounts, importFetchedStatuses } from './importer'; -import { decreasePendingRequestsCount } from './notification_policies'; - -// TODO: refactor with notification_groups -function dispatchAssociatedRecords( - dispatch: AppDispatch, - notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[], -) { - const fetchedAccounts: ApiAccountJSON[] = []; - const fetchedStatuses: ApiStatusJSON[] = []; - - notifications.forEach((notification) => { - if (notification.type === 'admin.report') { - fetchedAccounts.push(notification.report.target_account); - } - - if (notification.type === 'moderation_warning') { - fetchedAccounts.push(notification.moderation_warning.target_account); - } - - if ('status' in notification && notification.status) { - fetchedStatuses.push(notification.status); - } - }); - - if (fetchedAccounts.length > 0) - dispatch(importFetchedAccounts(fetchedAccounts)); - - if (fetchedStatuses.length > 0) - dispatch(importFetchedStatuses(fetchedStatuses)); -} - -export const fetchNotificationRequests = createDataLoadingThunk( - 'notificationRequests/fetch', - async (_params, { getState }) => { - let sinceId = undefined; - - if (getState().notificationRequests.items.length > 0) { - sinceId = getState().notificationRequests.items[0]?.id; - } - - return apiFetchNotificationRequests({ - since_id: sinceId, - }); - }, - ({ requests, links }, { dispatch }) => { - const next = links.refs.find((link) => link.rel === 'next'); - - dispatch(importFetchedAccounts(requests.map((request) => request.account))); - - return { requests, next: next?.uri }; - }, - { - condition: (_params, { getState }) => - !getState().notificationRequests.isLoading, - }, -); - -export const fetchNotificationRequest = createDataLoadingThunk( - 'notificationRequest/fetch', - async ({ id }: { id: string }) => apiFetchNotificationRequest(id), - { - condition: ({ id }, { getState }) => - !( - getState().notificationRequests.current.item?.id === id || - getState().notificationRequests.current.isLoading - ), - }, -); - -export const expandNotificationRequests = createDataLoadingThunk( - 'notificationRequests/expand', - async (_, { getState }) => { - const nextUrl = getState().notificationRequests.next; - if (!nextUrl) throw new Error('missing URL'); - - return apiFetchNotificationRequests(undefined, nextUrl); - }, - ({ requests, links }, { dispatch }) => { - const next = links.refs.find((link) => link.rel === 'next'); - - dispatch(importFetchedAccounts(requests.map((request) => request.account))); - - return { requests, next: next?.uri }; - }, - { - condition: (_, { getState }) => - !!getState().notificationRequests.next && - !getState().notificationRequests.isLoading, - }, -); - -export const fetchNotificationsForRequest = createDataLoadingThunk( - 'notificationRequest/fetchNotifications', - async ({ accountId }: { accountId: string }, { getState }) => { - const sinceId = - // @ts-expect-error current.notifications.items is not yet typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - getState().notificationRequests.current.notifications.items[0]?.get( - 'id', - ) as string | undefined; - - return apiFetchNotifications({ - since_id: sinceId, - account_id: accountId, - }); - }, - ({ notifications, links }, { dispatch }) => { - const next = links.refs.find((link) => link.rel === 'next'); - - dispatchAssociatedRecords(dispatch, notifications); - - return { notifications, next: next?.uri }; - }, - { - condition: ({ accountId }, { getState }) => { - const current = getState().notificationRequests.current; - return !( - current.item?.account_id === accountId && - current.notifications.isLoading - ); - }, - }, -); - -export const expandNotificationsForRequest = createDataLoadingThunk( - 'notificationRequest/expandNotifications', - async (_, { getState }) => { - const nextUrl = getState().notificationRequests.current.notifications.next; - if (!nextUrl) throw new Error('missing URL'); - - return apiFetchNotifications(undefined, nextUrl); - }, - ({ notifications, links }, { dispatch }) => { - const next = links.refs.find((link) => link.rel === 'next'); - - dispatchAssociatedRecords(dispatch, notifications); - - return { notifications, next: next?.uri }; - }, - { - condition: ({ accountId }: { accountId: string }, { getState }) => { - const url = getState().notificationRequests.current.notifications.next; - - return ( - !!url && - !getState().notificationRequests.current.notifications.isLoading && - getState().notificationRequests.current.item?.account_id === accountId - ); - }, - }, -); - -export const acceptNotificationRequest = createDataLoadingThunk( - 'notificationRequest/accept', - ({ id }: { id: string }) => apiAcceptNotificationRequest(id), - (_data, { dispatch, discardLoadData }) => { - dispatch(decreasePendingRequestsCount(1)); - - // The payload is not used in any functions - return discardLoadData; - }, -); - -export const dismissNotificationRequest = createDataLoadingThunk( - 'notificationRequest/dismiss', - ({ id }: { id: string }) => apiDismissNotificationRequest(id), - (_data, { dispatch, discardLoadData }) => { - dispatch(decreasePendingRequestsCount(1)); - - // The payload is not used in any functions - return discardLoadData; - }, -); - -export const acceptNotificationRequests = createDataLoadingThunk( - 'notificationRequests/acceptBulk', - ({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids), - (_data, { dispatch, discardLoadData, actionArg: { ids } }) => { - dispatch(decreasePendingRequestsCount(ids.length)); - - // The payload is not used in any functions - return discardLoadData; - }, -); - -export const dismissNotificationRequests = createDataLoadingThunk( - 'notificationRequests/dismissBulk', - ({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids), - (_data, { dispatch, discardLoadData, actionArg: { ids } }) => { - dispatch(decreasePendingRequestsCount(ids.length)); - - // The payload is not used in any functions - return discardLoadData; - }, -); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 87b842e51f..e80131f979 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,52 +1,78 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; +import { List as ImmutableList } from 'immutable'; + +import { compareId } from 'mastodon/compare_id'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; + +import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; import { requestNotificationPermission } from '../utils/notifications'; -import { fetchFollowRequests } from './accounts'; +import { fetchFollowRequests, fetchRelationships } from './accounts'; import { importFetchedAccount, + importFetchedAccounts, + importFetchedStatus, + importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; -import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; +import { saveSettings } from './settings'; import { STATUS_EMOJI_REACTION_UPDATE } from './statuses'; -export * from "./notifications_typed"; +export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; + +export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; +export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; +export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; + +export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; +export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; + +export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; + export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -const messages = defineMessages({ - // mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, +defineMessages({ + mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, - 'message_admin.report': { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, - 'message_admin.sign_up': { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' }, - message_emoji_reaction: { id: 'notification.emoji_reaction', defaultMessage: '{name} reacted your post with emoji' }, - message_favourite: { id: 'notification.favourite', defaultMessage: '{name} favorited your post' }, - message_follow: { id: 'notification.follow', defaultMessage: '{name} followed you' }, - message_list_status: { id: 'notification.list_status', defaultMessage: '{name} post is added to {listName}' }, - message_mention: { id: 'notification.mention', defaultMessage: 'Mention' }, - message_poll: { id: 'notification.poll', defaultMessage: 'A poll you voted in has ended' }, - message_reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your post' }, - message_status: { id: 'notification.status', defaultMessage: '{name} just posted' }, - message_status_reference: { id: 'notification.status_reference', defaultMessage: '{name} quoted your post' }, - message_update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, }); -export function updateEmojiReactions(emoji_reaction) { +const fetchRelatedRelationships = (dispatch, notifications) => { + const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id); + + if (accountIds.length > 0) { + dispatch(fetchRelationships(accountIds)); + } +}; + +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + +export function updateEmojiReactions(emoji_reaction, accountId) { return (dispatch) => dispatch({ type: STATUS_EMOJI_REACTION_UPDATE, emoji_reaction, + accountId, }); } export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type; const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); @@ -68,17 +94,35 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(submitMarkers()); - // `notificationsUpdate` is still used in `user_lists` and `relationships` reducers - dispatch(importFetchedAccount(notification.account)); - dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered})); + if (showInColumn) { + dispatch(importFetchedAccount(notification.account)); + + if (notification.status) { + dispatch(importFetchedStatus(notification.status)); + } + + if (notification.report) { + dispatch(importFetchedAccount(notification.report.target_account)); + } + + dispatch({ + type: NOTIFICATIONS_UPDATE, + notification, + usePendingItems: preferPendingItems, + meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, + }); + + fetchRelatedRelationships(dispatch, [notification]); + } else if (playSound && !filtered) { + dispatch({ + type: NOTIFICATIONS_UPDATE_NOOP, + meta: { sound: 'boop' }, + }); + } // Desktop notifications if (typeof window.Notification !== 'undefined' && showAlert && !filtered) { - const messageTemplate = intlMessages[`notification.${notification.type}`] || messages[`message_${notification.type}`] || '[NO MESSAGE DEFINITION]'; - const title = new IntlMessageFormat(messageTemplate, intlLocale).format({ - name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username, - listName: notification.list && notification.list.title, - }); + const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : ''); const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id }); @@ -91,8 +135,156 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; } +const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + +const excludeTypesFromFilter = filter => { + const allTypes = ImmutableList([ + 'follow', + 'follow_request', + 'favourite', + 'emoji_reaction', + 'reblog', + 'status_reference', + 'mention', + 'poll', + 'status', + 'update', + 'admin.sign_up', + 'admin.report', + ]); + + return allTypes.filterNot(item => item === filter).toJS(); +}; + const noOp = () => {}; +let expandNotificationsController = new AbortController(); + +export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) { + return (dispatch, getState) => { + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const notifications = getState().get('notifications'); + const isLoadingMore = !!maxId; + + if (notifications.get('isLoading')) { + if (forceLoad) { + expandNotificationsController.abort(); + expandNotificationsController = new AbortController(); + } else { + done(); + return; + } + } + + const params = { + max_id: maxId, + exclude_types: activeFilter === 'all' + ? excludeTypesFromSettings(getState()) + : excludeTypesFromFilter(activeFilter), + }; + + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } + } + + const isLoadingRecent = !!params.since_id; + + dispatch(expandNotificationsRequest(isLoadingMore)); + + api(getState).get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map(item => item.account))); + dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); + + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); + fetchRelatedRelationships(dispatch, response.data); + dispatch(submitMarkers()); + }).catch(error => { + dispatch(expandNotificationsFail(error, isLoadingMore)); + }).finally(() => { + done(); + }); + }; +} + +export function expandNotificationsRequest(isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_REQUEST, + skipLoading: !isLoadingMore, + }; +} + +export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { + return { + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications, + next, + isLoadingRecent: isLoadingRecent, + usePendingItems, + skipLoading: !isLoadingMore, + }; +} + +export function expandNotificationsFail(error, isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_FAIL, + error, + skipLoading: !isLoadingMore, + skipAlert: !isLoadingMore || error.name === 'AbortError', + }; +} + +export function clearNotifications() { + return (dispatch, getState) => { + dispatch({ + type: NOTIFICATIONS_CLEAR, + }); + + api(getState).post('/api/v1/notifications/clear'); + }; +} + +export function scrollTopNotifications(top) { + return { + type: NOTIFICATIONS_SCROLL_TOP, + top, + }; +} + +export function setFilter (filterType) { + return dispatch => { + dispatch({ + type: NOTIFICATIONS_FILTER_SET, + path: ['notifications', 'quickFilter', 'active'], + value: filterType, + }); + dispatch(expandNotifications({ forceLoad: true })); + dispatch(saveSettings()); + }; +} + +export const mountNotifications = () => ({ + type: NOTIFICATIONS_MOUNT, +}); + +export const unmountNotifications = () => ({ + type: NOTIFICATIONS_UNMOUNT, +}); + + +export const markNotificationsAsRead = () => ({ + type: NOTIFICATIONS_MARK_AS_READ, +}); + // Browser support export function setupBrowserNotifications() { return dispatch => { diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts deleted file mode 100644 index 3eb1230666..0000000000 --- a/app/javascript/mastodon/actions/notifications_typed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { ApiNotificationJSON } from 'mastodon/api_types/notifications'; - -export const notificationsUpdate = createAction( - 'notifications/update', - ({ - playSound, - ...args - }: { - notification: ApiNotificationJSON; - playSound: boolean; - }) => ({ - payload: args, - meta: { sound: playSound ? 'boop' : undefined }, - }), -); diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js new file mode 100644 index 0000000000..898375abeb --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.js @@ -0,0 +1,46 @@ +// @ts-check + +export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY'; +export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; + +/** + * @typedef MediaProps + * @property {string} src + * @property {boolean} muted + * @property {number} volume + * @property {number} currentTime + * @property {string} poster + * @property {string} backgroundColor + * @property {string} foregroundColor + * @property {string} accentColor + */ + +/** + * @param {string} statusId + * @param {string} accountId + * @param {string} playerType + * @param {MediaProps} props + * @returns {object} + */ +export const deployPictureInPicture = (statusId, accountId, playerType, props) => { + // @ts-expect-error + return (dispatch, getState) => { + // Do not open a player for a toot that does not exist + if (getState().hasIn(['statuses', statusId])) { + dispatch({ + type: PICTURE_IN_PICTURE_DEPLOY, + statusId, + accountId, + playerType, + props, + }); + } + }; +}; + +/* + * @return {object} + */ +export const removePictureInPicture = () => ({ + type: PICTURE_IN_PICTURE_REMOVE, +}); diff --git a/app/javascript/mastodon/actions/picture_in_picture.ts b/app/javascript/mastodon/actions/picture_in_picture.ts deleted file mode 100644 index d34b508a33..0000000000 --- a/app/javascript/mastodon/actions/picture_in_picture.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture'; -import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; - -interface DeployParams { - statusId: string; - accountId: string; - playerType: 'audio' | 'video'; - props: PIPMediaProps; -} - -export const removePictureInPicture = createAction('pip/remove'); - -export const deployPictureInPictureAction = - createAction('pip/deploy'); - -export const deployPictureInPicture = createAppAsyncThunk( - 'pip/deploy', - (args: DeployParams, { dispatch, getState }) => { - const { statusId } = args; - - // Do not open a player for a toot that does not exist - - // @ts-expect-error state.statuses is not yet typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (getState().hasIn(['statuses', statusId])) { - dispatch(deployPictureInPictureAction(args)); - } - }, -); diff --git a/app/javascript/mastodon/actions/pin_statuses.js b/app/javascript/mastodon/actions/pin_statuses.js index d583eab573..baa10d1562 100644 --- a/app/javascript/mastodon/actions/pin_statuses.js +++ b/app/javascript/mastodon/actions/pin_statuses.js @@ -8,10 +8,10 @@ export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; export function fetchPinnedStatuses() { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); - api().get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { + api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { dispatch(importFetchedStatuses(response.data)); dispatch(fetchPinnedStatusesSuccess(response.data, null)); }).catch(error => { diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js new file mode 100644 index 0000000000..a37410dc90 --- /dev/null +++ b/app/javascript/mastodon/actions/polls.js @@ -0,0 +1,61 @@ +import api from '../api'; + +import { importFetchedPoll } from './importer'; + +export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +export const vote = (pollId, choices) => (dispatch, getState) => { + dispatch(voteRequest()); + + api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) + .catch(err => dispatch(voteFail(err))); +}; + +export const fetchPoll = pollId => (dispatch, getState) => { + dispatch(fetchPollRequest()); + + api(getState).get(`/api/v1/polls/${pollId}`) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) + .catch(err => dispatch(fetchPollFail(err))); +}; + +export const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +export const voteSuccess = poll => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +export const voteFail = error => ({ + type: POLL_VOTE_FAIL, + error, +}); + +export const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +export const fetchPollSuccess = poll => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +export const fetchPollFail = error => ({ + type: POLL_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/polls.ts b/app/javascript/mastodon/actions/polls.ts deleted file mode 100644 index 65a96e8f62..0000000000 --- a/app/javascript/mastodon/actions/polls.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { apiGetPoll, apiPollVote } from 'mastodon/api/polls'; -import type { ApiPollJSON } from 'mastodon/api_types/polls'; -import { createPollFromServerJSON } from 'mastodon/models/poll'; -import { - createAppAsyncThunk, - createDataLoadingThunk, -} from 'mastodon/store/typed_functions'; - -import { importPolls } from './importer/polls'; - -export const importFetchedPoll = createAppAsyncThunk( - 'poll/importFetched', - (args: { poll: ApiPollJSON }, { dispatch, getState }) => { - const { poll } = args; - - dispatch( - importPolls({ - polls: [createPollFromServerJSON(poll, getState().polls[poll.id])], - }), - ); - }, -); - -export const vote = createDataLoadingThunk( - 'poll/vote', - ({ pollId, choices }: { pollId: string; choices: string[] }) => - apiPollVote(pollId, choices), - async (poll, { dispatch, discardLoadData }) => { - await dispatch(importFetchedPoll({ poll })); - return discardLoadData; - }, -); - -export const fetchPoll = createDataLoadingThunk( - 'poll/fetch', - ({ pollId }: { pollId: string }) => apiGetPoll(pollId), - async (poll, { dispatch }) => { - await dispatch(importFetchedPoll({ poll })); - }, -); diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index 647a6bd9fb..b3d3850e31 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/app/javascript/mastodon/actions/push_notifications/registerer.js @@ -33,7 +33,7 @@ const unsubscribe = ({ registration, subscription }) => subscription ? subscription.unsubscribe().then(() => registration) : registration; const sendSubscriptionToBackend = (subscription) => { - const params = { subscription: { ...subscription.toJSON(), standard: true } }; + const params = { subscription }; if (me) { const data = pushNotificationsSetting.get(me); diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js index 49b89b0d13..756b8cd05e 100644 --- a/app/javascript/mastodon/actions/reports.js +++ b/app/javascript/mastodon/actions/reports.js @@ -15,10 +15,10 @@ export const initReport = (account, status) => dispatch => }, })); -export const submitReport = (params, onSuccess, onFail) => (dispatch) => { +export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { dispatch(submitReportRequest()); - api().post('/api/v1/reports', params).then(response => { + api(getState).post('/api/v1/reports', params).then(response => { dispatch(submitReportSuccess(response.data)); if (onSuccess) onSuccess(); }).catch(error => { diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js new file mode 100644 index 0000000000..38a089b486 --- /dev/null +++ b/app/javascript/mastodon/actions/search.js @@ -0,0 +1,210 @@ +import { fromJS } from 'immutable'; + +import { searchHistory } from 'mastodon/settings'; + +import api from '../api'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; + +export const SEARCH_CHANGE = 'SEARCH_CHANGE'; +export const SEARCH_CLEAR = 'SEARCH_CLEAR'; +export const SEARCH_SHOW = 'SEARCH_SHOW'; + +export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; +export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; +export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; + +export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; +export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; +export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; + +export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE'; + +export function changeSearch(value) { + return { + type: SEARCH_CHANGE, + value, + }; +} + +export function clearSearch() { + return { + type: SEARCH_CLEAR, + }; +} + +export function submitSearch(type) { + return (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const signedIn = !!getState().getIn(['meta', 'me']); + + if (value.length === 0) { + dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type)); + return; + } + + dispatch(fetchSearchRequest(type)); + + api(getState).get('/api/v2/search', { + params: { + q: value, + resolve: signedIn, + limit: 11, + type, + }, + }).then(response => { + if (response.data.accounts) { + dispatch(importFetchedAccounts(response.data.accounts)); + } + + if (response.data.statuses) { + dispatch(importFetchedStatuses(response.data.statuses)); + } + + dispatch(fetchSearchSuccess(response.data, value, type)); + dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(fetchSearchFail(error)); + }); + }; +} + +export function fetchSearchRequest(searchType) { + return { + type: SEARCH_FETCH_REQUEST, + searchType, + }; +} + +export function fetchSearchSuccess(results, searchTerm, searchType) { + return { + type: SEARCH_FETCH_SUCCESS, + results, + searchType, + searchTerm, + }; +} + +export function fetchSearchFail(error) { + return { + type: SEARCH_FETCH_FAIL, + error, + }; +} + +export const expandSearch = type => (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const offset = getState().getIn(['search', 'results', type]).size - 1; + + dispatch(expandSearchRequest(type)); + + api(getState).get('/api/v2/search', { + params: { + q: value, + type, + offset, + limit: 11, + }, + }).then(({ data }) => { + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(expandSearchSuccess(data, value, type)); + dispatch(fetchRelationships(data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(expandSearchFail(error)); + }); +}; + +export const expandSearchRequest = (searchType) => ({ + type: SEARCH_EXPAND_REQUEST, + searchType, +}); + +export const expandSearchSuccess = (results, searchTerm, searchType) => ({ + type: SEARCH_EXPAND_SUCCESS, + results, + searchTerm, + searchType, +}); + +export const expandSearchFail = error => ({ + type: SEARCH_EXPAND_FAIL, + error, +}); + +export const showSearch = () => ({ + type: SEARCH_SHOW, +}); + +export const openURL = (value, history, onFailure) => (dispatch, getState) => { + const signedIn = !!getState().getIn(['meta', 'me']); + + if (!signedIn) { + if (onFailure) { + onFailure(); + } + + return; + } + + dispatch(fetchSearchRequest()); + + api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => { + if (response.data.accounts?.length > 0) { + dispatch(importFetchedAccounts(response.data.accounts)); + history.push(`/@${response.data.accounts[0].acct}`); + } else if (response.data.statuses?.length > 0) { + dispatch(importFetchedStatuses(response.data.statuses)); + history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`); + } else if (onFailure) { + onFailure(); + } + + dispatch(fetchSearchSuccess(response.data, value)); + }).catch(err => { + dispatch(fetchSearchFail(err)); + + if (onFailure) { + onFailure(); + } + }); +}; + +export const clickSearchResult = (q, type) => (dispatch, getState) => { + const previous = getState().getIn(['search', 'recent']); + const me = getState().getIn(['meta', 'me']); + const current = previous.add(fromJS({ type, q })).takeLast(4); + + searchHistory.set(me, current.toJS()); + dispatch(updateSearchHistory(current)); +}; + +export const forgetSearchResult = q => (dispatch, getState) => { + const previous = getState().getIn(['search', 'recent']); + const me = getState().getIn(['meta', 'me']); + const current = previous.filterNot(result => result.get('q') === q); + + searchHistory.set(me, current.toJS()); + dispatch(updateSearchHistory(current)); +}; + +export const updateSearchHistory = recent => ({ + type: SEARCH_HISTORY_UPDATE, + recent, +}); + +export const hydrateSearch = () => (dispatch, getState) => { + const me = getState().getIn(['meta', 'me']); + const history = searchHistory.get(me); + + if (history !== null) { + dispatch(updateSearchHistory(history)); + } +}; \ No newline at end of file diff --git a/app/javascript/mastodon/actions/search.ts b/app/javascript/mastodon/actions/search.ts deleted file mode 100644 index 13a4ee4432..0000000000 --- a/app/javascript/mastodon/actions/search.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import { apiGetSearch } from 'mastodon/api/search'; -import type { ApiSearchType } from 'mastodon/api_types/search'; -import type { - RecentSearch, - SearchType as RecentSearchType, -} from 'mastodon/models/search'; -import { searchHistory } from 'mastodon/settings'; -import { - createDataLoadingThunk, - createAppAsyncThunk, -} from 'mastodon/store/typed_functions'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts, importFetchedStatuses } from './importer'; - -export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE'; - -export const submitSearch = createDataLoadingThunk( - 'search/submit', - async ({ q, type }: { q: string; type?: ApiSearchType }, { getState }) => { - const signedIn = !!getState().meta.get('me'); - - return apiGetSearch({ - q, - type, - resolve: signedIn, - limit: 11, - }); - }, - (data, { dispatch }) => { - if (data.accounts.length > 0) { - dispatch(importFetchedAccounts(data.accounts)); - dispatch(fetchRelationships(data.accounts.map((account) => account.id))); - } - - if (data.statuses.length > 0) { - dispatch(importFetchedStatuses(data.statuses)); - } - - return data; - }, - { - useLoadingBar: false, - }, -); - -export const expandSearch = createDataLoadingThunk( - 'search/expand', - async ({ type }: { type: ApiSearchType }, { getState }) => { - const q = getState().search.q; - const results = getState().search.results; - const offset = results?.[type].length; - - return apiGetSearch({ - q, - type, - limit: 10, - offset, - }); - }, - (data, { dispatch }) => { - if (data.accounts.length > 0) { - dispatch(importFetchedAccounts(data.accounts)); - dispatch(fetchRelationships(data.accounts.map((account) => account.id))); - } - - if (data.statuses.length > 0) { - dispatch(importFetchedStatuses(data.statuses)); - } - - return data; - }, - { - useLoadingBar: true, - }, -); - -export const openURL = createDataLoadingThunk( - 'search/openURL', - ({ url }: { url: string }) => - apiGetSearch({ - q: url, - resolve: true, - limit: 1, - }), - (data, { dispatch }) => { - if (data.accounts.length > 0) { - dispatch(importFetchedAccounts(data.accounts)); - } else if (data.statuses.length > 0) { - dispatch(importFetchedStatuses(data.statuses)); - } - - return data; - }, - { - useLoadingBar: true, - }, -); - -export const clickSearchResult = createAppAsyncThunk( - 'search/clickResult', - ( - { q, type }: { q: string; type?: RecentSearchType }, - { dispatch, getState }, - ) => { - const previous = getState().search.recent; - - if (previous.some((x) => x.q === q && x.type === type)) { - return; - } - - const me = getState().meta.get('me') as string; - const current = [{ type, q }, ...previous].slice(0, 4); - - searchHistory.set(me, current); - dispatch(updateSearchHistory(current)); - }, -); - -export const forgetSearchResult = createAppAsyncThunk( - 'search/forgetResult', - (q: string, { dispatch, getState }) => { - const previous = getState().search.recent; - const me = getState().meta.get('me') as string; - const current = previous.filter((result) => result.q !== q); - - searchHistory.set(me, current); - dispatch(updateSearchHistory(current)); - }, -); - -export const updateSearchHistory = createAction( - 'search/updateHistory', -); - -export const hydrateSearch = createAppAsyncThunk( - 'search/hydrate', - (_args, { dispatch, getState }) => { - const me = getState().meta.get('me') as string; - const history = searchHistory.get(me) as RecentSearch[] | null; - - if (history !== null) { - dispatch(updateSearchHistory(history)); - } - }, -); diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js index 32ee093afa..65f3efc3a7 100644 --- a/app/javascript/mastodon/actions/server.js +++ b/app/javascript/mastodon/actions/server.js @@ -25,7 +25,7 @@ export const fetchServer = () => (dispatch, getState) => { dispatch(fetchServerRequest()); - api() + api(getState) .get('/api/v2/instance').then(({ data }) => { if (data.contact.account) dispatch(importFetchedAccount(data.contact.account)); dispatch(fetchServerSuccess(data)); @@ -46,10 +46,10 @@ const fetchServerFail = error => ({ error, }); -export const fetchServerTranslationLanguages = () => (dispatch) => { +export const fetchServerTranslationLanguages = () => (dispatch, getState) => { dispatch(fetchServerTranslationLanguagesRequest()); - api() + api(getState) .get('/api/v1/instance/translation_languages').then(({ data }) => { dispatch(fetchServerTranslationLanguagesSuccess(data)); }).catch(err => dispatch(fetchServerTranslationLanguagesFail(err))); @@ -76,7 +76,7 @@ export const fetchExtendedDescription = () => (dispatch, getState) => { dispatch(fetchExtendedDescriptionRequest()); - api() + api(getState) .get('/api/v1/instance/extended_description') .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data))) .catch(err => dispatch(fetchExtendedDescriptionFail(err))); @@ -103,7 +103,7 @@ export const fetchDomainBlocks = () => (dispatch, getState) => { dispatch(fetchDomainBlocksRequest()); - api() + api(getState) .get('/api/v1/instance/domain_blocks') .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data))) .catch(err => { diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js index 7659fb5f98..fbd89f9d4b 100644 --- a/app/javascript/mastodon/actions/settings.js +++ b/app/javascript/mastodon/actions/settings.js @@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => { api().put('/api/web/settings', { data }) .then(() => dispatch({ type: SETTING_SAVE })) .catch(error => dispatch(showAlertForError(error))); -}, 2000, { leading: true, trailing: true }); +}, 5000, { trailing: true }); export function saveSettings() { return (dispatch, getState) => debouncedSave(dispatch, getState); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 5064e65e7b..b9779cdcf4 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -1,9 +1,5 @@ -import { browserHistory } from 'mastodon/components/router'; -import { me } from 'mastodon/initial_state'; - import api from '../api'; -import { fetchRelationships } from './accounts'; import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; import { deleteFromTimelines } from './timelines'; @@ -53,13 +49,11 @@ export function fetchStatusRequest(id, skipLoading) { }; } -export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) { +export function fetchStatus(id, forceFetch = false) { return (dispatch, getState) => { const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; - if (alsoFetchContext) { - dispatch(fetchContext(id)); - } + dispatch(fetchContext(id)); if (skipLoading) { return; @@ -67,14 +61,8 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) { dispatch(fetchStatusRequest(id, skipLoading)); - api().get(`/api/v1/statuses/${id}`).then(response => { + api(getState).get(`/api/v1/statuses/${id}`).then(response => { dispatch(importFetchedStatus(response.data)); - - const accountId = response.data.account.id; - if (me && !getState().getIn(['relationships', accountId])) { - dispatch(fetchRelationships([accountId])); - } - dispatch(fetchStatusSuccess(skipLoading)); }).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); @@ -107,7 +95,7 @@ export function redraft(status, raw_text) { }; } -export const editStatus = (id) => (dispatch, getState) => { +export const editStatus = (id, routerHistory) => (dispatch, getState) => { let status = getState().getIn(['statuses', id]); if (status.get('poll')) { @@ -116,9 +104,9 @@ export const editStatus = (id) => (dispatch, getState) => { dispatch(fetchStatusSourceRequest()); - api().get(`/api/v1/statuses/${id}/source`).then(response => { + api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { dispatch(fetchStatusSourceSuccess()); - ensureComposeIsVisible(getState); + ensureComposeIsVisible(getState, routerHistory); dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text)); }).catch(error => { dispatch(fetchStatusSourceFail(error)); @@ -138,7 +126,7 @@ export const fetchStatusSourceFail = error => ({ error, }); -export function deleteStatus(id, withRedraft = false) { +export function deleteStatus(id, routerHistory, withRedraft = false) { return (dispatch, getState) => { let status = getState().getIn(['statuses', id]); @@ -148,14 +136,14 @@ export function deleteStatus(id, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => { + api(getState).delete(`/api/v1/statuses/${id}`).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); if (withRedraft) { dispatch(redraft(status, response.data.text)); - ensureComposeIsVisible(getState); + ensureComposeIsVisible(getState, routerHistory); } }).catch(error => { dispatch(deleteStatusFail(id, error)); @@ -189,10 +177,10 @@ export const updateStatus = status => dispatch => dispatch(importFetchedStatus(status)); export function fetchContext(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(fetchContextRequest(id)); - api().get(`/api/v1/statuses/${id}/context?with_reference=1`).then(response => { + api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants).concat(response.data.references))); dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants, response.data.references)); @@ -234,10 +222,10 @@ export function fetchContextFail(id, error) { } export function muteStatus(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(muteStatusRequest(id)); - api().post(`/api/v1/statuses/${id}/mute`).then(() => { + api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { dispatch(muteStatusSuccess(id)); }).catch(error => { dispatch(muteStatusFail(id, error)); @@ -268,10 +256,10 @@ export function muteStatusFail(id, error) { } export function unmuteStatus(id) { - return (dispatch) => { + return (dispatch, getState) => { dispatch(unmuteStatusRequest(id)); - api().post(`/api/v1/statuses/${id}/unmute`).then(() => { + api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { dispatch(unmuteStatusSuccess(id)); }).catch(error => { dispatch(unmuteStatusFail(id, error)); @@ -323,21 +311,6 @@ export function revealStatus(ids) { }; } -export function toggleStatusSpoilers(statusId) { - return (dispatch, getState) => { - const status = getState().statuses.get(statusId); - - if (!status) - return; - - if (status.get('hidden')) { - dispatch(revealStatus(statusId)); - } else { - dispatch(hideStatus(statusId)); - } - }; -} - export function toggleStatusCollapse(id, isCollapsed) { return { type: STATUS_COLLAPSE, @@ -346,10 +319,10 @@ export function toggleStatusCollapse(id, isCollapsed) { }; } -export const translateStatus = id => (dispatch) => { +export const translateStatus = id => (dispatch, getState) => { dispatch(translateStatusRequest(id)); - api().post(`/api/v1/statuses/${id}/translate`).then(response => { + api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => { dispatch(translateStatusSuccess(id, response.data)); }).catch(error => { dispatch(translateStatusFail(id, error)); @@ -383,15 +356,3 @@ export const updateEmojiReaction = (emoji_reaction) => ({ type: STATUS_EMOJI_REACTION_UPDATE, emoji_reaction, }); - -export const navigateToStatus = (statusId) => { - return (_dispatch, getState) => { - const state = getState(); - const accountId = state.statuses.getIn([statusId, 'account']); - const acct = state.accounts.getIn([accountId, 'acct']); - - if (acct) { - browserHistory.push(`/@${acct}/${statusId}`); - } - }; -}; diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index e8fec13453..682b0f5db7 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,4 +1,4 @@ -import { fromJS, isIndexed } from 'immutable'; +import { Iterable, fromJS } from 'immutable'; import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; @@ -9,7 +9,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => fromJS(rawState, (k, v) => - isIndexed(v) ? v.toList() : v.toMap()); + Iterable.isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { return dispatch => { diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index f9d784c2b4..262d055448 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -10,8 +10,7 @@ import { deleteAnnouncement, } from './announcements'; import { updateConversations } from './conversations'; -import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; -import { updateNotifications, updateEmojiReactions } from './notifications'; +import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -38,7 +37,7 @@ const randomUpTo = max => * @param {string} channelName * @param {Object.} params * @param {Object} options - * @param {function(Function, Function): Promise} [options.fallback] + * @param {function(Function, Function): void} [options.fallback] * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] * @returns {function(): void} @@ -53,13 +52,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti let pollingId; /** - * @param {function(Function, Function): Promise} fallback + * @param {function(Function, Function): void} fallback */ - const useFallback = async fallback => { - await fallback(dispatch, getState); - // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook - pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); + const useFallback = fallback => { + fallback(dispatch, () => { + // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook + pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); + }); }; return { @@ -78,7 +78,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, onDisconnect() { - dispatch(disconnectTimeline({ timeline: timelineId })); + dispatch(disconnectTimeline(timelineId)); if (options.fallback) { // @ts-expect-error @@ -99,22 +99,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'delete': dispatch(deleteFromTimelines(data.payload)); break; - case 'notification': { + case 'notification': // @ts-expect-error - const notificationJSON = JSON.parse(data.payload); - dispatch(updateNotifications(notificationJSON, messages, locale)); - // TODO: remove this once the groups feature replaces the previous one - dispatch(processNewNotificationForGroups(notificationJSON)); + dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; - } case 'emoji_reaction': // @ts-expect-error - dispatch(updateEmojiReactions(JSON.parse(data.payload))); + dispatch(updateEmojiReactions(JSON.parse(data.payload), getState().getIn(['meta', 'me']))); break; - case 'notifications_merged': { - dispatch(refreshStaleNotificationGroups()); - break; - } case 'conversation': // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); @@ -138,24 +130,21 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti /** * @param {Function} dispatch + * @param {function(): void} done */ -async function refreshHomeTimelineAndNotification(dispatch) { - await dispatch(expandHomeTimeline({ maxId: undefined })); - - // TODO: polling for merged notifications - try { - await dispatch(pollRecentGroupNotifications()); - } catch { - // TODO - } - - await dispatch(fetchAnnouncements()); -} +const refreshHomeTimelineAndNotification = (dispatch, done) => { + // @ts-expect-error + dispatch(expandHomeTimeline({}, () => + // @ts-expect-error + dispatch(expandNotifications({}, () => + dispatch(fetchAnnouncements(done)))))); +}; /** * @returns {function(): void} */ export const connectUserStream = () => + // @ts-expect-error connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); /** @@ -203,4 +192,4 @@ export const connectListStream = listId => * @returns {function(): void} */ export const connectAntennaStream = antennaId => - connectTimelineStream(`antenna:${antennaId}`, 'antenna', { antenna: antennaId }, { fillGaps: () => fillAntennaTimelineGaps(antennaId) }); +connectTimelineStream(`antenna:${antennaId}`, 'antenna', { antenna: antennaId }, { fillGaps: () => fillAntennaTimelineGaps(antennaId) }); diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js new file mode 100644 index 0000000000..870a311024 --- /dev/null +++ b/app/javascript/mastodon/actions/suggestions.js @@ -0,0 +1,65 @@ +import api from '../api'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; +export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; +export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; + +export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; + +export function fetchSuggestions(withRelationships = false) { + return (dispatch, getState) => { + dispatch(fetchSuggestionsRequest()); + + api(getState).get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => { + dispatch(importFetchedAccounts(response.data.map(x => x.account))); + dispatch(fetchSuggestionsSuccess(response.data)); + + if (withRelationships) { + dispatch(fetchRelationships(response.data.map(item => item.account.id))); + } + }).catch(error => dispatch(fetchSuggestionsFail(error))); + }; +} + +export function fetchSuggestionsRequest() { + return { + type: SUGGESTIONS_FETCH_REQUEST, + skipLoading: true, + }; +} + +export function fetchSuggestionsSuccess(suggestions) { + return { + type: SUGGESTIONS_FETCH_SUCCESS, + suggestions, + skipLoading: true, + }; +} + +export function fetchSuggestionsFail(error) { + return { + type: SUGGESTIONS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +} + +export const dismissSuggestion = accountId => (dispatch, getState) => { + dispatch({ + type: SUGGESTIONS_DISMISS, + id: accountId, + }); + + api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => { + dispatch(fetchSuggestionsRequest()); + + api(getState).get('/api/v2/suggestions').then(response => { + dispatch(importFetchedAccounts(response.data.map(x => x.account))); + dispatch(fetchSuggestionsSuccess(response.data)); + }).catch(error => dispatch(fetchSuggestionsFail(error))); + }).catch(() => {}); +}; diff --git a/app/javascript/mastodon/actions/suggestions.ts b/app/javascript/mastodon/actions/suggestions.ts deleted file mode 100644 index 0eadfa6b47..0000000000 --- a/app/javascript/mastodon/actions/suggestions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - apiGetSuggestions, - apiDeleteSuggestion, -} from 'mastodon/api/suggestions'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const fetchSuggestions = createDataLoadingThunk( - 'suggestions/fetch', - () => apiGetSuggestions(20), - (data, { dispatch }) => { - dispatch(importFetchedAccounts(data.map((x) => x.account))); - dispatch(fetchRelationships(data.map((x) => x.account.id))); - - return data; - }, -); - -export const dismissSuggestion = createDataLoadingThunk( - 'suggestions/dismiss', - ({ accountId }: { accountId: string }) => apiDeleteSuggestion(accountId), -); diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js new file mode 100644 index 0000000000..dda8c924bb --- /dev/null +++ b/app/javascript/mastodon/actions/tags.js @@ -0,0 +1,172 @@ +import api, { getLinks } from '../api'; + +export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; +export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; +export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; + +export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; +export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; +export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; + +export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; +export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; +export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; + +export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; +export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; +export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; + +export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST'; +export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS'; +export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL'; + +export const fetchHashtag = name => (dispatch, getState) => { + dispatch(fetchHashtagRequest()); + + api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => { + dispatch(fetchHashtagSuccess(name, data)); + }).catch(err => { + dispatch(fetchHashtagFail(err)); + }); +}; + +export const fetchHashtagRequest = () => ({ + type: HASHTAG_FETCH_REQUEST, +}); + +export const fetchHashtagSuccess = (name, tag) => ({ + type: HASHTAG_FETCH_SUCCESS, + name, + tag, +}); + +export const fetchHashtagFail = error => ({ + type: HASHTAG_FETCH_FAIL, + error, +}); + +export const fetchFollowedHashtags = () => (dispatch, getState) => { + dispatch(fetchFollowedHashtagsRequest()); + + api(getState).get('/api/v1/followed_tags').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchFollowedHashtagsFail(err)); + }); +}; + +export function fetchFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_FETCH_REQUEST, + }; +} + +export function fetchFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, + followed_tags, + next, + }; +} + +export function fetchFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_FETCH_FAIL, + error, + }; +} + +export function expandFollowedHashtags() { + return (dispatch, getState) => { + const url = getState().getIn(['followed_tags', 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowedHashtagsRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFollowedHashtagsFail(error)); + }); + }; +} + +export function expandFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, + }; +} + +export function expandFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + followed_tags, + next, + }; +} + +export function expandFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_FAIL, + error, + }; +} + +export const followHashtag = name => (dispatch, getState) => { + dispatch(followHashtagRequest(name)); + + api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => { + dispatch(followHashtagSuccess(name, data)); + }).catch(err => { + dispatch(followHashtagFail(name, err)); + }); +}; + +export const followHashtagRequest = name => ({ + type: HASHTAG_FOLLOW_REQUEST, + name, +}); + +export const followHashtagSuccess = (name, tag) => ({ + type: HASHTAG_FOLLOW_SUCCESS, + name, + tag, +}); + +export const followHashtagFail = (name, error) => ({ + type: HASHTAG_FOLLOW_FAIL, + name, + error, +}); + +export const unfollowHashtag = name => (dispatch, getState) => { + dispatch(unfollowHashtagRequest(name)); + + api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => { + dispatch(unfollowHashtagSuccess(name, data)); + }).catch(err => { + dispatch(unfollowHashtagFail(name, err)); + }); +}; + +export const unfollowHashtagRequest = name => ({ + type: HASHTAG_UNFOLLOW_REQUEST, + name, +}); + +export const unfollowHashtagSuccess = (name, tag) => ({ + type: HASHTAG_UNFOLLOW_SUCCESS, + name, + tag, +}); + +export const unfollowHashtagFail = (name, error) => ({ + type: HASHTAG_UNFOLLOW_FAIL, + name, + error, +}); diff --git a/app/javascript/mastodon/actions/tags_typed.ts b/app/javascript/mastodon/actions/tags_typed.ts deleted file mode 100644 index 6dca32fd84..0000000000 --- a/app/javascript/mastodon/actions/tags_typed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const fetchHashtag = createDataLoadingThunk( - 'tags/fetch', - ({ tagId }: { tagId: string }) => apiGetTag(tagId), -); - -export const followHashtag = createDataLoadingThunk( - 'tags/follow', - ({ tagId }: { tagId: string }) => apiFollowTag(tagId), -); - -export const unfollowHashtag = createDataLoadingThunk( - 'tags/unfollow', - ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId), -); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 740e893a57..4e246a39dc 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -6,11 +6,9 @@ import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; -import {timelineDelete} from './timelines_typed'; - -export { disconnectTimeline } from './timelines_typed'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; +export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; @@ -19,13 +17,10 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; -export const TIMELINE_INSERT = 'TIMELINE_INSERT'; - -export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions'; -export const TIMELINE_GAP = null; export const loadPending = timeline => ({ type: TIMELINE_LOAD_PENDING, @@ -63,10 +58,16 @@ export function updateTimeline(timeline, status, accept) { export function deleteFromTimelines(id) { return (dispatch, getState) => { const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON(); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); - dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf })); + dispatch({ + type: TIMELINE_DELETE, + id, + accountId, + references, + reblogOf, + }); }; } @@ -76,18 +77,21 @@ export function clearTimeline(timeline) { }; } +const noOp = () => {}; + const parseTags = (tags = {}, mode) => { return (tags[mode] || []).map((tag) => { return tag.value; }); }; -export function expandTimeline(timelineId, path, params = {}) { - return async (dispatch, getState) => { +export function expandTimeline(timelineId, path, params = {}, done = noOp) { + return (dispatch, getState) => { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); const isLoadingMore = !!params.max_id; if (timeline.get('isLoading')) { + done(); return; } @@ -106,69 +110,61 @@ export function expandTimeline(timelineId, path, params = {}) { dispatch(expandTimelineRequest(timelineId, isLoadingMore)); - try { - const response = await api().get(path, { params }); + api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); - if (timelineId === 'home' && !isLoadingMore && !isLoadingRecent) { - const now = new Date(); - const fittingIndex = response.data.findIndex(status => now - (new Date(status.created_at)) > 4 * 3600 * 1000); - - if (fittingIndex !== -1) { - dispatch(insertIntoTimeline(timelineId, TIMELINE_SUGGESTIONS, Math.max(1, fittingIndex))); - } - } - if (timelineId === 'home') { dispatch(submitMarkers()); } - } catch(error) { + }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); - } + }).finally(() => { + done(); + }); }; } -export function fillTimelineGaps(timelineId, path, params = {}) { - return async (dispatch, getState) => { +export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) { + return (dispatch, getState) => { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); const items = timeline.get('items'); const nullIndexes = items.map((statusId, index) => statusId === null ? index : null); const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null); // Only expand at most two gaps to avoid doing too many requests - for (const maxId of gaps.take(2)) { - await dispatch(expandTimeline(timelineId, path, { ...params, maxId })); - } + done = gaps.take(2).reduce((done, maxId) => { + return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done))); + }, done); + + done(); }; } -export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }); -export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }); -export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }); +export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); -export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); -export const expandAntennaTimeline = (id, { maxId } = {}) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }); -export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }); -export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => { +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); +export const expandAntennaTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }, done); +export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, any: parseTags(tags, 'any'), all: parseTags(tags, 'all'), none: parseTags(tags, 'none'), local: local, - }); + }, done); }; -export const fillHomeTimelineGaps = () => fillTimelineGaps('home', '/api/v1/timelines/home', {}); -export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }); -export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }); -export const fillListTimelineGaps = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}); -export const fillAntennaTimelineGaps = (id) => fillTimelineGaps(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, {}); +export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done); +export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done); +export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done); +export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done); +export const fillAntennaTimelineGaps = (id, done = noOp) => fillTimelineGaps(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, {}, done); export function expandTimelineRequest(timeline, isLoadingMore) { return { @@ -217,14 +213,13 @@ export function connectTimeline(timeline) { }; } +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); + export const markAsPartial = timeline => ({ type: TIMELINE_MARK_AS_PARTIAL, timeline, }); - -export const insertIntoTimeline = (timeline, key, index) => ({ - type: TIMELINE_INSERT, - timeline, - index, - key, -}); diff --git a/app/javascript/mastodon/actions/timelines_typed.ts b/app/javascript/mastodon/actions/timelines_typed.ts deleted file mode 100644 index 07d82b2f01..0000000000 --- a/app/javascript/mastodon/actions/timelines_typed.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; - -export const disconnectTimeline = createAction( - 'timeline/disconnect', - ({ timeline }: { timeline: string }) => ({ - payload: { - timeline, - usePendingItems: preferPendingItems, - }, - }), -); - -export const timelineDelete = createAction<{ - statusId: string; - accountId: string; - references: string[]; - reblogOf: string | null; -}>('timelines/delete'); diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 0bdf17a5d2..d314423884 100644 --- a/app/javascript/mastodon/actions/trends.js +++ b/app/javascript/mastodon/actions/trends.js @@ -1,6 +1,6 @@ import api, { getLinks } from '../api'; -import { importFetchedStatuses, importFetchedAccounts } from './importer'; +import { importFetchedStatuses } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS'; @@ -18,10 +18,10 @@ export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST'; export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS'; export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_FAIL'; -export const fetchTrendingHashtags = () => (dispatch) => { +export const fetchTrendingHashtags = () => (dispatch, getState) => { dispatch(fetchTrendingHashtagsRequest()); - api() + api(getState) .get('/api/v1/trends/tags') .then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data))) .catch(err => dispatch(fetchTrendingHashtagsFail(err))); @@ -45,15 +45,12 @@ export const fetchTrendingHashtagsFail = error => ({ skipAlert: true, }); -export const fetchTrendingLinks = () => (dispatch) => { +export const fetchTrendingLinks = () => (dispatch, getState) => { dispatch(fetchTrendingLinksRequest()); - api() - .get('/api/v1/trends/links', { params: { limit: 20 } }) - .then(({ data }) => { - dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account))); - dispatch(fetchTrendingLinksSuccess(data)); - }) + api(getState) + .get('/api/v1/trends/links') + .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data))) .catch(err => dispatch(fetchTrendingLinksFail(err))); }; @@ -82,7 +79,7 @@ export const fetchTrendingStatuses = () => (dispatch, getState) => { dispatch(fetchTrendingStatusesRequest()); - api().get('/api/v1/trends/statuses').then(response => { + api(getState).get('/api/v1/trends/statuses').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null)); @@ -118,7 +115,7 @@ export const expandTrendingStatuses = () => (dispatch, getState) => { dispatch(expandTrendingStatusesRequest()); - api().get(url).then(response => { + api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null)); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index a41b058d2c..f262fd8570 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -1,14 +1,9 @@ -import type { - AxiosError, - AxiosResponse, - Method, - RawAxiosRequestHeaders, -} from 'axios'; +import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios'; import axios from 'axios'; import LinkHeader from 'http-link-header'; -import { getAccessToken } from './initial_state'; import ready from './ready'; +import type { GetState } from './store'; export const getLinks = (response: AxiosResponse) => { const value = response.headers.link as string | undefined; @@ -34,25 +29,25 @@ const setCSRFHeader = () => { void ready(setCSRFHeader); -const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => { - const accessToken = getAccessToken(); +const authorizationHeaderFromState = (getState?: GetState) => { + const accessToken = + getState && (getState().meta.get('access_token', '') as string); - if (!accessToken) return {}; + if (!accessToken) { + return {}; + } return { Authorization: `Bearer ${accessToken}`, - }; + } as RawAxiosRequestHeaders; }; // eslint-disable-next-line import/no-default-export -export default function api(withAuthorization = true) { - const instance = axios.create({ - transitional: { - clarifyTimeoutError: true, - }, +export default function api(getState: GetState) { + return axios.create({ headers: { ...csrfHeader, - ...(withAuthorization ? authorizationTokenFromInitialState() : {}), + ...authorizationHeaderFromState(getState), }, transformResponse: [ @@ -65,69 +60,4 @@ export default function api(withAuthorization = true) { }, ], }); - - instance.interceptors.response.use( - (response: AxiosResponse) => { - if (response.headers.deprecation) { - console.warn( - `Deprecated request: ${response.config.method} ${response.config.url}`, - ); - } - return response; - }, - (error: AxiosError) => { - return Promise.reject(error); - }, - ); - - return instance; -} - -type RequestParamsOrData = Record; - -export async function apiRequest( - method: Method, - url: string, - args: { - signal?: AbortSignal; - params?: RequestParamsOrData; - data?: RequestParamsOrData; - timeout?: number; - } = {}, -) { - const { data } = await api().request({ - method, - url: '/api/' + url, - ...args, - }); - - return data; -} - -export async function apiRequestGet( - url: string, - params?: RequestParamsOrData, -) { - return apiRequest('GET', url, { params }); -} - -export async function apiRequestPost( - url: string, - data?: RequestParamsOrData, -) { - return apiRequest('POST', url, { data }); -} - -export async function apiRequestPut( - url: string, - data?: RequestParamsOrData, -) { - return apiRequest('PUT', url, { data }); -} - -export async function apiRequestDelete( - url: string, - params?: RequestParamsOrData, -) { - return apiRequest('DELETE', url, { params }); } diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts deleted file mode 100644 index 717010ba74..0000000000 --- a/app/javascript/mastodon/api/accounts.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { apiRequestPost } from 'mastodon/api'; -import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; - -export const apiSubmitAccountNote = (id: string, value: string) => - apiRequestPost(`v1/accounts/${id}/note`, { - comment: value, - }); - -export const apiFollowAccount = ( - id: string, - params?: { - reblogs: boolean; - }, -) => - apiRequestPost(`v1/accounts/${id}/follow`, { - ...params, - }); - -export const apiUnfollowAccount = (id: string) => - apiRequestPost(`v1/accounts/${id}/unfollow`); diff --git a/app/javascript/mastodon/api/antennas.ts b/app/javascript/mastodon/api/antennas.ts deleted file mode 100644 index 61fd84185d..0000000000 --- a/app/javascript/mastodon/api/antennas.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { - apiRequestPost, - apiRequestPut, - apiRequestGet, - apiRequestDelete, -} from 'mastodon/api'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { ApiAntennaJSON } from 'mastodon/api_types/antennas'; - -export const apiCreate = (antenna: Partial) => - apiRequestPost('v1/antennas', antenna); - -export const apiUpdate = (antenna: Partial) => - apiRequestPut(`v1/antennas/${antenna.id}`, antenna); - -export const apiGetAccounts = (antennaId: string) => - apiRequestGet(`v1/antennas/${antennaId}/accounts`, { - limit: 0, - }); - -export const apiGetExcludeAccounts = (antennaId: string) => - apiRequestGet(`v1/antennas/${antennaId}/exclude_accounts`, { - limit: 0, - }); - -export const apiGetDomains = (antennaId: string) => - apiRequestGet<{ domains: string[]; exclude_domains: string[] }>( - `v1/antennas/${antennaId}/domains`, - { - limit: 0, - }, - ); - -export const apiAddDomain = (antennaId: string, domain: string) => - apiRequestPost(`v1/antennas/${antennaId}/domains`, { - domains: [domain], - }); - -export const apiRemoveDomain = (antennaId: string, domain: string) => - apiRequestDelete(`v1/antennas/${antennaId}/domains`, { - domains: [domain], - }); - -export const apiAddExcludeDomain = (antennaId: string, domain: string) => - apiRequestPost(`v1/antennas/${antennaId}/exclude_domains`, { - domains: [domain], - }); - -export const apiRemoveExcludeDomain = (antennaId: string, domain: string) => - apiRequestDelete(`v1/antennas/${antennaId}/exclude_domains`, { - domains: [domain], - }); - -export const apiGetTags = (antennaId: string) => - apiRequestGet<{ tags: string[]; exclude_tags: string[] }>( - `v1/antennas/${antennaId}/tags`, - { - limit: 0, - }, - ); - -export const apiAddTag = (antennaId: string, tag: string) => - apiRequestPost(`v1/antennas/${antennaId}/tags`, { - tags: [tag], - }); - -export const apiRemoveTag = (antennaId: string, tag: string) => - apiRequestDelete(`v1/antennas/${antennaId}/tags`, { - tags: [tag], - }); - -export const apiAddExcludeTag = (antennaId: string, tag: string) => - apiRequestPost(`v1/antennas/${antennaId}/exclude_tags`, { - tags: [tag], - }); - -export const apiRemoveExcludeTag = (antennaId: string, tag: string) => - apiRequestDelete(`v1/antennas/${antennaId}/exclude_tags`, { - tags: [tag], - }); - -export const apiGetKeywords = (antennaId: string) => - apiRequestGet<{ keywords: string[]; exclude_keywords: string[] }>( - `v1/antennas/${antennaId}/keywords`, - { - limit: 0, - }, - ); - -export const apiAddKeyword = (antennaId: string, keyword: string) => - apiRequestPost(`v1/antennas/${antennaId}/keywords`, { - keywords: [keyword], - }); - -export const apiRemoveKeyword = (antennaId: string, keyword: string) => - apiRequestDelete(`v1/antennas/${antennaId}/keywords`, { - keywords: [keyword], - }); - -export const apiAddExcludeKeyword = (antennaId: string, keyword: string) => - apiRequestPost(`v1/antennas/${antennaId}/exclude_keywords`, { - keywords: [keyword], - }); - -export const apiRemoveExcludeKeyword = (antennaId: string, keyword: string) => - apiRequestDelete(`v1/antennas/${antennaId}/exclude_keywords`, { - keywords: [keyword], - }); - -export const apiGetAccountAntennas = (accountId: string) => - apiRequestGet(`v1/accounts/${accountId}/antennas`); - -export const apiAddAccountToAntenna = (antennaId: string, accountId: string) => - apiRequestPost(`v1/antennas/${antennaId}/accounts`, { - account_ids: [accountId], - }); - -export const apiRemoveAccountFromAntenna = ( - antennaId: string, - accountId: string, -) => - apiRequestDelete(`v1/antennas/${antennaId}/accounts`, { - account_ids: [accountId], - }); - -export const apiGetExcludeAccountAntennas = (accountId: string) => - apiRequestGet(`v1/accounts/${accountId}/exclude_antennas`); - -export const apiAddExcludeAccountToAntenna = ( - antennaId: string, - accountId: string, -) => - apiRequestPost(`v1/antennas/${antennaId}/exclude_accounts`, { - account_ids: [accountId], - }); - -export const apiRemoveExcludeAccountFromAntenna = ( - antennaId: string, - accountId: string, -) => - apiRequestDelete(`v1/antennas/${antennaId}/exclude_accounts`, { - account_ids: [accountId], - }); diff --git a/app/javascript/mastodon/api/bookmark_categories.ts b/app/javascript/mastodon/api/bookmark_categories.ts deleted file mode 100644 index d6d3394b3a..0000000000 --- a/app/javascript/mastodon/api/bookmark_categories.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - apiRequestPost, - apiRequestPut, - apiRequestGet, - apiRequestDelete, -} from 'mastodon/api'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { ApiBookmarkCategoryJSON } from 'mastodon/api_types/bookmark_categories'; - -export const apiCreate = (bookmarkCategory: Partial) => - apiRequestPost( - 'v1/bookmark_categories', - bookmarkCategory, - ); - -export const apiUpdate = (bookmarkCategory: Partial) => - apiRequestPut( - `v1/bookmark_categories/${bookmarkCategory.id}`, - bookmarkCategory, - ); - -export const apiGetStatuses = (bookmarkCategoryId: string) => - apiRequestGet( - `v1/bookmark_categories/${bookmarkCategoryId}/statuses`, - { - limit: 0, - }, - ); - -export const apiGetStatusBookmarkCategories = (accountId: string) => - apiRequestGet( - `v1/statuses/${accountId}/bookmark_categories`, - ); - -export const apiAddStatusToBookmarkCategory = ( - bookmarkCategoryId: string, - statusId: string, -) => - apiRequestPost(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { - status_ids: [statusId], - }); - -export const apiRemoveStatusFromBookmarkCategory = ( - bookmarkCategoryId: string, - statusId: string, -) => - apiRequestDelete(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { - status_ids: [statusId], - }); diff --git a/app/javascript/mastodon/api/circles.ts b/app/javascript/mastodon/api/circles.ts deleted file mode 100644 index 04971e1e6b..0000000000 --- a/app/javascript/mastodon/api/circles.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - apiRequestPost, - apiRequestPut, - apiRequestGet, - apiRequestDelete, -} from 'mastodon/api'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { ApiCircleJSON } from 'mastodon/api_types/circles'; - -export const apiCreate = (circle: Partial) => - apiRequestPost('v1/circles', circle); - -export const apiUpdate = (circle: Partial) => - apiRequestPut(`v1/circles/${circle.id}`, circle); - -export const apiGetAccounts = (circleId: string) => - apiRequestGet(`v1/circles/${circleId}/accounts`, { - limit: 0, - }); - -export const apiGetAccountCircles = (accountId: string) => - apiRequestGet(`v1/accounts/${accountId}/circles`); - -export const apiAddAccountToCircle = (circleId: string, accountId: string) => - apiRequestPost(`v1/circles/${circleId}/accounts`, { - account_ids: [accountId], - }); - -export const apiRemoveAccountFromCircle = ( - circleId: string, - accountId: string, -) => - apiRequestDelete(`v1/circles/${circleId}/accounts`, { - account_ids: [accountId], - }); diff --git a/app/javascript/mastodon/api/compose.ts b/app/javascript/mastodon/api/compose.ts deleted file mode 100644 index 757e9961c9..0000000000 --- a/app/javascript/mastodon/api/compose.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { apiRequestPut } from 'mastodon/api'; -import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; - -export const apiUpdateMedia = ( - id: string, - params?: { description?: string; focus?: string }, -) => apiRequestPut(`v1/media/${id}`, params); diff --git a/app/javascript/mastodon/api/directory.ts b/app/javascript/mastodon/api/directory.ts deleted file mode 100644 index cd39f8f269..0000000000 --- a/app/javascript/mastodon/api/directory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { apiRequestGet } from 'mastodon/api'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; - -export const apiGetDirectory = ( - params: { - order: string; - local: boolean; - offset?: number; - }, - limit = 20, -) => - apiRequestGet('v1/directory', { - ...params, - limit, - }); diff --git a/app/javascript/mastodon/api/domain_blocks.ts b/app/javascript/mastodon/api/domain_blocks.ts deleted file mode 100644 index 4e153b0ee9..0000000000 --- a/app/javascript/mastodon/api/domain_blocks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import api, { getLinks } from 'mastodon/api'; - -export const apiGetDomainBlocks = async (url?: string) => { - const response = await api().request({ - method: 'GET', - url: url ?? '/api/v1/domain_blocks', - }); - - return { - domains: response.data, - links: getLinks(response), - }; -}; diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts deleted file mode 100644 index 764e8daab2..0000000000 --- a/app/javascript/mastodon/api/instance.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { apiRequestGet } from 'mastodon/api'; -import type { - ApiTermsOfServiceJSON, - ApiPrivacyPolicyJSON, -} from 'mastodon/api_types/instance'; - -export const apiGetTermsOfService = (version?: string) => - apiRequestGet( - version - ? `v1/instance/terms_of_service/${version}` - : 'v1/instance/terms_of_service', - ); - -export const apiGetPrivacyPolicy = () => - apiRequestGet('v1/instance/privacy_policy'); diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts deleted file mode 100644 index 118b5f06d2..0000000000 --- a/app/javascript/mastodon/api/interactions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { apiRequestPost } from 'mastodon/api'; -import type { Status, StatusVisibility } from 'mastodon/models/status'; - -export const apiReblog = (statusId: string, visibility: StatusVisibility) => - apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, { - visibility, - }); - -export const apiUnreblog = (statusId: string) => - apiRequestPost(`v1/statuses/${statusId}/unreblog`); diff --git a/app/javascript/mastodon/api/lists.ts b/app/javascript/mastodon/api/lists.ts deleted file mode 100644 index a5586eb6d4..0000000000 --- a/app/javascript/mastodon/api/lists.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - apiRequestPost, - apiRequestPut, - apiRequestGet, - apiRequestDelete, -} from 'mastodon/api'; -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; -import type { ApiListJSON } from 'mastodon/api_types/lists'; - -export const apiCreate = (list: Partial) => - apiRequestPost('v1/lists', list); - -export const apiUpdate = (list: Partial) => - apiRequestPut(`v1/lists/${list.id}`, list); - -export const apiGetAccounts = (listId: string) => - apiRequestGet(`v1/lists/${listId}/accounts`, { - limit: 0, - }); - -export const apiGetAccountLists = (accountId: string) => - apiRequestGet(`v1/accounts/${accountId}/lists`); - -export const apiAddAccountToList = (listId: string, accountId: string) => - apiRequestPost(`v1/lists/${listId}/accounts`, { - account_ids: [accountId], - }); - -export const apiRemoveAccountFromList = (listId: string, accountId: string) => - apiRequestDelete(`v1/lists/${listId}/accounts`, { - account_ids: [accountId], - }); diff --git a/app/javascript/mastodon/api/notification_policies.ts b/app/javascript/mastodon/api/notification_policies.ts deleted file mode 100644 index 3bc8174139..0000000000 --- a/app/javascript/mastodon/api/notification_policies.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { apiRequestGet, apiRequestPut } from 'mastodon/api'; -import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies'; - -export const apiGetNotificationPolicy = () => - apiRequestGet('v2/notifications/policy'); - -export const apiUpdateNotificationsPolicy = ( - policy: Partial, -) => apiRequestPut('v2/notifications/policy', policy); diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts deleted file mode 100644 index 5c31baab8e..0000000000 --- a/app/javascript/mastodon/api/notifications.ts +++ /dev/null @@ -1,109 +0,0 @@ -import api, { - apiRequest, - getLinks, - apiRequestGet, - apiRequestPost, -} from 'mastodon/api'; -import type { - ApiNotificationGroupsResultJSON, - ApiNotificationGroupJSON, - ApiNotificationRequestJSON, - ApiNotificationJSON, -} from 'mastodon/api_types/notifications'; -import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; - -const exceptInvalidNotifications = ( - notifications: ApiNotificationGroupJSON[], -) => { - return notifications.filter((n) => { - if ('status' in n) { - return (n.status as ApiStatusJSON | null) !== null; - } - return true; - }); -}; - -export const apiFetchNotifications = async ( - params?: { - account_id?: string; - since_id?: string; - }, - url?: string, -) => { - const response = await api().request({ - method: 'GET', - url: url ?? '/api/v1/notifications', - params, - }); - - return { - notifications: response.data, - links: getLinks(response), - }; -}; - -export const apiFetchNotificationGroups = async (params?: { - url?: string; - grouped_types?: string[]; - exclude_types?: string[]; - max_id?: string; - since_id?: string; -}) => { - const response = await api().request({ - method: 'GET', - url: '/api/v2/notifications', - params, - }); - - const { statuses, accounts, notification_groups } = response.data; - - return { - statuses, - accounts, - notifications: exceptInvalidNotifications(notification_groups), - links: getLinks(response), - }; -}; - -export const apiClearNotifications = () => - apiRequest('POST', 'v1/notifications/clear'); - -export const apiFetchNotificationRequests = async ( - params?: { - since_id?: string; - }, - url?: string, -) => { - const response = await api().request({ - method: 'GET', - url: url ?? '/api/v1/notifications/requests', - params, - }); - - return { - requests: response.data, - links: getLinks(response), - }; -}; - -export const apiFetchNotificationRequest = async (id: string) => { - return apiRequestGet( - `v1/notifications/requests/${id}`, - ); -}; - -export const apiAcceptNotificationRequest = async (id: string) => { - return apiRequestPost(`v1/notifications/requests/${id}/accept`); -}; - -export const apiDismissNotificationRequest = async (id: string) => { - return apiRequestPost(`v1/notifications/requests/${id}/dismiss`); -}; - -export const apiAcceptNotificationRequests = async (id: string[]) => { - return apiRequestPost('v1/notifications/requests/accept', { id }); -}; - -export const apiDismissNotificationRequests = async (id: string[]) => { - return apiRequestPost('v1/notifications/requests/dismiss', { id }); -}; diff --git a/app/javascript/mastodon/api/polls.ts b/app/javascript/mastodon/api/polls.ts deleted file mode 100644 index cb659986f5..0000000000 --- a/app/javascript/mastodon/api/polls.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { apiRequestGet, apiRequestPost } from 'mastodon/api'; -import type { ApiPollJSON } from 'mastodon/api_types/polls'; - -export const apiGetPoll = (pollId: string) => - apiRequestGet(`/v1/polls/${pollId}`); - -export const apiPollVote = (pollId: string, choices: string[]) => - apiRequestPost(`/v1/polls/${pollId}/votes`, { - choices, - }); diff --git a/app/javascript/mastodon/api/search.ts b/app/javascript/mastodon/api/search.ts deleted file mode 100644 index 79b0385fe8..0000000000 --- a/app/javascript/mastodon/api/search.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { apiRequestGet } from 'mastodon/api'; -import type { - ApiSearchType, - ApiSearchResultsJSON, -} from 'mastodon/api_types/search'; - -export const apiGetSearch = (params: { - q: string; - resolve?: boolean; - type?: ApiSearchType; - limit?: number; - offset?: number; -}) => - apiRequestGet('v2/search', { - ...params, - }); diff --git a/app/javascript/mastodon/api/suggestions.ts b/app/javascript/mastodon/api/suggestions.ts deleted file mode 100644 index d4817698cc..0000000000 --- a/app/javascript/mastodon/api/suggestions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { apiRequestGet, apiRequestDelete } from 'mastodon/api'; -import type { ApiSuggestionJSON } from 'mastodon/api_types/suggestions'; - -export const apiGetSuggestions = (limit: number) => - apiRequestGet('v2/suggestions', { limit }); - -export const apiDeleteSuggestion = (accountId: string) => - apiRequestDelete(`v1/suggestions/${accountId}`); diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts deleted file mode 100644 index 4b111def81..0000000000 --- a/app/javascript/mastodon/api/tags.ts +++ /dev/null @@ -1,23 +0,0 @@ -import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api'; -import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; - -export const apiGetTag = (tagId: string) => - apiRequestGet(`v1/tags/${tagId}`); - -export const apiFollowTag = (tagId: string) => - apiRequestPost(`v1/tags/${tagId}/follow`); - -export const apiUnfollowTag = (tagId: string) => - apiRequestPost(`v1/tags/${tagId}/unfollow`); - -export const apiGetFollowedTags = async (url?: string) => { - const response = await api().request({ - method: 'GET', - url: url ?? '/api/v1/followed_tags', - }); - - return { - tags: response.data, - links: getLinks(response), - }; -}; diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts deleted file mode 100644 index 9d7974eda0..0000000000 --- a/app/javascript/mastodon/api_types/accounts.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { ApiCustomEmojiJSON } from './custom_emoji'; - -export interface ApiAccountFieldJSON { - name: string; - value: string; - verified_at: string | null; -} - -export interface ApiAccountRoleJSON { - color: string; - id: string; - name: string; -} - -export interface ApiAccountOtherSettingsJSON { - noindex: boolean; - hide_network: boolean; - hide_statuses_count: boolean; - hide_following_count: boolean; - hide_followers_count: boolean; - translatable_private: boolean; - link_preview: boolean; - allow_quote: boolean; - emoji_reaction_policy: - | 'allow' - | 'outside_only' - | 'following_only' - | 'followers_only' - | 'mutuals_only' - | 'block'; - subscription_policy: 'allow' | 'followers_only' | 'block'; -} - -export interface ApiServerFeaturesJSON { - circle: boolean; - emoji_reaction: boolean; - quote: boolean; - status_reference: boolean; -} - -// See app/serializers/rest/account_serializer.rb -export interface BaseApiAccountJSON { - acct: string; - avatar: string; - avatar_static: string; - bot: boolean; - created_at: string; - discoverable?: boolean; - indexable: boolean; - display_name: string; - emojis: ApiCustomEmojiJSON[]; - fields: ApiAccountFieldJSON[]; - followers_count: number; - following_count: number; - group: boolean; - header: string; - header_static: string; - id: string; - last_status_at: string; - locked: boolean; - noindex?: boolean; - note: string; - other_settings: ApiAccountOtherSettingsJSON; - roles?: ApiAccountJSON[]; - server_features: ApiServerFeaturesJSON; - subscribable: boolean; - statuses_count: number; - uri: string; - url: string; - username: string; - moved?: ApiAccountJSON; - suspended?: boolean; - limited?: boolean; - memorial?: boolean; - hide_collections: boolean; -} - -// See app/serializers/rest/muted_account_serializer.rb -export interface ApiMutedAccountJSON extends BaseApiAccountJSON { - mute_expires_at?: string | null; -} - -// For now, we have the same type representing both `Account` and `MutedAccount` -// objects, but we should refactor this in the future. -export type ApiAccountJSON = ApiMutedAccountJSON; diff --git a/app/javascript/mastodon/api_types/antennas.ts b/app/javascript/mastodon/api_types/antennas.ts deleted file mode 100644 index a2a8a997ba..0000000000 --- a/app/javascript/mastodon/api_types/antennas.ts +++ /dev/null @@ -1,17 +0,0 @@ -// See app/serializers/rest/antenna_serializer.rb - -import type { ApiListJSON } from './lists'; - -export interface ApiAntennaJSON { - id: string; - title: string; - stl: boolean; - ltl: boolean; - insert_feeds: boolean; - with_media_only: boolean; - ignore_reblog: boolean; - favourite: boolean; - list: ApiListJSON | null; - - list_id: string | undefined; -} diff --git a/app/javascript/mastodon/api_types/bookmark_categories.ts b/app/javascript/mastodon/api_types/bookmark_categories.ts deleted file mode 100644 index 5407b6b125..0000000000 --- a/app/javascript/mastodon/api_types/bookmark_categories.ts +++ /dev/null @@ -1,6 +0,0 @@ -// See app/serializers/rest/bookmark_category_serializer.rb - -export interface ApiBookmarkCategoryJSON { - id: string; - title: string; -} diff --git a/app/javascript/mastodon/api_types/circles.ts b/app/javascript/mastodon/api_types/circles.ts deleted file mode 100644 index 9905d480b8..0000000000 --- a/app/javascript/mastodon/api_types/circles.ts +++ /dev/null @@ -1,6 +0,0 @@ -// See app/serializers/rest/circle_serializer.rb - -export interface ApiCircleJSON { - id: string; - title: string; -} diff --git a/app/javascript/mastodon/api_types/custom_emoji.ts b/app/javascript/mastodon/api_types/custom_emoji.ts deleted file mode 100644 index 9f25e6e410..0000000000 --- a/app/javascript/mastodon/api_types/custom_emoji.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See app/serializers/rest/account_serializer.rb -export interface ApiCustomEmojiJSON { - shortcode: string; - static_url: string; - url: string; - category?: string; - visible_in_picker: boolean; - width?: number; - height?: number; - sensitive?: boolean; - aliases?: string[]; - license?: string; -} diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts deleted file mode 100644 index 3a29684b70..0000000000 --- a/app/javascript/mastodon/api_types/instance.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface ApiTermsOfServiceJSON { - effective_date: string; - effective: boolean; - succeeded_by: string | null; - content: string; -} - -export interface ApiPrivacyPolicyJSON { - updated_at: string; - content: string; -} diff --git a/app/javascript/mastodon/api_types/lists.ts b/app/javascript/mastodon/api_types/lists.ts deleted file mode 100644 index bc32b33883..0000000000 --- a/app/javascript/mastodon/api_types/lists.ts +++ /dev/null @@ -1,15 +0,0 @@ -// See app/serializers/rest/list_serializer.rb - -import type { ApiAntennaJSON } from './antennas'; - -export type RepliesPolicyType = 'list' | 'followed' | 'none'; - -export interface ApiListJSON { - id: string; - title: string; - exclusive: boolean; - replies_policy: RepliesPolicyType; - notify: boolean; - favourite: boolean; - antennas?: ApiAntennaJSON[]; -} diff --git a/app/javascript/mastodon/api_types/markers.ts b/app/javascript/mastodon/api_types/markers.ts deleted file mode 100644 index f7664fd7c1..0000000000 --- a/app/javascript/mastodon/api_types/markers.ts +++ /dev/null @@ -1,7 +0,0 @@ -// See app/serializers/rest/account_serializer.rb - -export interface MarkerJSON { - last_read_id: string; - version: string; - updated_at: string; -} diff --git a/app/javascript/mastodon/api_types/media_attachments.ts b/app/javascript/mastodon/api_types/media_attachments.ts deleted file mode 100644 index fc027ccd2a..0000000000 --- a/app/javascript/mastodon/api_types/media_attachments.ts +++ /dev/null @@ -1,22 +0,0 @@ -// See app/serializers/rest/media_attachment_serializer.rb - -export type MediaAttachmentType = - | 'image' - | 'gifv' - | 'video' - | 'unknown' - | 'audio'; - -export interface ApiMediaAttachmentJSON { - id: string; - type: MediaAttachmentType; - url: string; - preview_url: string; - remoteUrl: string; - preview_remote_url: string; - text_url: string; - // TODO: how to define this? - meta: unknown; - description?: string; - blurhash: string; -} diff --git a/app/javascript/mastodon/api_types/notification_policies.ts b/app/javascript/mastodon/api_types/notification_policies.ts deleted file mode 100644 index 1c3970782c..0000000000 --- a/app/javascript/mastodon/api_types/notification_policies.ts +++ /dev/null @@ -1,15 +0,0 @@ -// See app/serializers/rest/notification_policy_serializer.rb - -export type NotificationPolicyValue = 'accept' | 'filter' | 'drop'; - -export interface NotificationPolicyJSON { - for_not_following: NotificationPolicyValue; - for_not_followers: NotificationPolicyValue; - for_new_accounts: NotificationPolicyValue; - for_private_mentions: NotificationPolicyValue; - for_limited_accounts: NotificationPolicyValue; - summary: { - pending_requests_count: number; - pending_notifications_count: number; - }; -} diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts deleted file mode 100644 index 41daed25ad..0000000000 --- a/app/javascript/mastodon/api_types/notifications.ts +++ /dev/null @@ -1,200 +0,0 @@ -// See app/serializers/rest/notification_group_serializer.rb - -import type { AccountWarningAction } from 'mastodon/models/notification_group'; - -import type { ApiAccountJSON } from './accounts'; -import type { ApiListJSON } from './lists'; -import type { ApiReportJSON } from './reports'; -import type { ApiStatusJSON } from './statuses'; - -// See app/model/notification.rb -export const allNotificationTypes = [ - 'follow', - 'follow_request', - 'favourite', - 'emoji_reaction', - 'reblog', - 'mention', - 'status_reference', - 'poll', - 'status', - 'list_status', - 'update', - 'admin.sign_up', - 'admin.report', - 'moderation_warning', - 'severed_relationships', - 'annual_report', -]; - -export type NotificationWithStatusType = - | 'favourite' - | 'emoji_reaction' - | 'reblog' - | 'status' - | 'list_status' - | 'mention' - | 'status_reference' - | 'poll' - | 'update'; - -export type NotificationType = - | NotificationWithStatusType - | 'follow' - | 'follow_request' - | 'moderation_warning' - | 'severed_relationships' - | 'admin.sign_up' - | 'admin.report' - | 'annual_report'; - -export interface NotifyEmojiReactionJSON { - name: string; - count: number; - me: boolean; - url?: string; - static_url?: string; - domain?: string; - width?: number; - height?: number; -} - -export interface NotificationEmojiReactionGroupJSON { - emoji_reaction: NotifyEmojiReactionJSON; - sample_account_ids: string[]; -} - -export interface BaseNotificationJSON { - id: string; - type: NotificationType; - created_at: string; - group_key: string; - account: ApiAccountJSON; - emoji_reaction?: NotifyEmojiReactionJSON; - list?: ApiListJSON; -} - -export interface BaseNotificationGroupJSON { - group_key: string; - notifications_count: number; - type: NotificationType; - sample_account_ids: string[]; - latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly - most_recent_notification_id: string; - page_min_id?: string; - page_max_id?: string; - emoji_reaction_groups?: NotificationEmojiReactionGroupJSON[]; - list?: ApiListJSON; -} - -interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON { - type: NotificationWithStatusType; - status_id: string | null; -} - -interface NotificationWithStatusJSON extends BaseNotificationJSON { - type: NotificationWithStatusType; - status: ApiStatusJSON | null; - emoji_reaction?: NotifyEmojiReactionJSON; -} - -interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON { - type: 'admin.report'; - report: ApiReportJSON; -} - -interface ReportNotificationJSON extends BaseNotificationJSON { - type: 'admin.report'; - report: ApiReportJSON; -} - -type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up'; -interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON { - type: SimpleNotificationTypes; -} - -interface SimpleNotificationJSON extends BaseNotificationJSON { - type: SimpleNotificationTypes; -} - -export interface ApiAccountWarningJSON { - id: string; - action: AccountWarningAction; - text: string; - status_ids: string[]; - created_at: string; - target_account: ApiAccountJSON; - appeal: unknown; -} - -interface ModerationWarningNotificationGroupJSON - extends BaseNotificationGroupJSON { - type: 'moderation_warning'; - moderation_warning: ApiAccountWarningJSON; -} - -interface ModerationWarningNotificationJSON extends BaseNotificationJSON { - type: 'moderation_warning'; - moderation_warning: ApiAccountWarningJSON; -} - -export interface ApiAccountRelationshipSeveranceEventJSON { - id: string; - type: 'account_suspension' | 'domain_block' | 'user_domain_block'; - purged: boolean; - target_name: string; - followers_count: number; - following_count: number; - created_at: string; -} - -interface AccountRelationshipSeveranceNotificationGroupJSON - extends BaseNotificationGroupJSON { - type: 'severed_relationships'; - event: ApiAccountRelationshipSeveranceEventJSON; -} - -interface AccountRelationshipSeveranceNotificationJSON - extends BaseNotificationJSON { - type: 'severed_relationships'; - event: ApiAccountRelationshipSeveranceEventJSON; -} - -export interface ApiAnnualReportEventJSON { - year: string; -} - -interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON { - type: 'annual_report'; - annual_report: ApiAnnualReportEventJSON; -} - -export type ApiNotificationJSON = - | SimpleNotificationJSON - | ReportNotificationJSON - | AccountRelationshipSeveranceNotificationJSON - | NotificationWithStatusJSON - | ModerationWarningNotificationJSON; - -export type ApiNotificationGroupJSON = - | SimpleNotificationGroupJSON - | ReportNotificationGroupJSON - | AccountRelationshipSeveranceNotificationGroupJSON - | NotificationGroupWithStatusJSON - | ModerationWarningNotificationGroupJSON - | AnnualReportNotificationGroupJSON; - -export interface ApiNotificationGroupsResultJSON { - accounts: ApiAccountJSON[]; - statuses: ApiStatusJSON[]; - notification_groups: ApiNotificationGroupJSON[]; -} - -export interface ApiNotificationRequestJSON { - id: string; - created_at: string; - updated_at: string; - notifications_count: string; - account: ApiAccountJSON; - last_status?: ApiStatusJSON; -} diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts deleted file mode 100644 index 891a2faba7..0000000000 --- a/app/javascript/mastodon/api_types/polls.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ApiCustomEmojiJSON } from './custom_emoji'; - -// See app/serializers/rest/poll_serializer.rb - -export interface ApiPollOptionJSON { - title: string; - votes_count: number; -} - -export interface ApiPollJSON { - id: string; - expires_at: string; - expired: boolean; - multiple: boolean; - votes_count: number; - voters_count: number | null; - - options: ApiPollOptionJSON[]; - emojis: ApiCustomEmojiJSON[]; - - voted?: boolean; - own_votes?: number[]; -} diff --git a/app/javascript/mastodon/api_types/relationships.ts b/app/javascript/mastodon/api_types/relationships.ts deleted file mode 100644 index 9f26a0ce9b..0000000000 --- a/app/javascript/mastodon/api_types/relationships.ts +++ /dev/null @@ -1,18 +0,0 @@ -// See app/serializers/rest/relationship_serializer.rb -export interface ApiRelationshipJSON { - blocked_by: boolean; - blocking: boolean; - domain_blocking: boolean; - endorsed: boolean; - followed_by: boolean; - following: boolean; - id: string; - languages: string[] | null; - muting_notifications: boolean; - muting: boolean; - note: string; - notifying: boolean; - requested_by: boolean; - requested: boolean; - showing_reblogs: boolean; -} diff --git a/app/javascript/mastodon/api_types/reports.ts b/app/javascript/mastodon/api_types/reports.ts deleted file mode 100644 index b11cfdd2eb..0000000000 --- a/app/javascript/mastodon/api_types/reports.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ApiAccountJSON } from './accounts'; - -export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation'; - -export interface ApiReportJSON { - id: string; - action_taken: unknown; - action_taken_at: unknown; - category: ReportCategory; - comment: string; - forwarded: boolean; - created_at: string; - status_ids: string[]; - rule_ids: string[]; - target_account: ApiAccountJSON; -} diff --git a/app/javascript/mastodon/api_types/search.ts b/app/javascript/mastodon/api_types/search.ts deleted file mode 100644 index 795cbb2b41..0000000000 --- a/app/javascript/mastodon/api_types/search.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ApiAccountJSON } from './accounts'; -import type { ApiStatusJSON } from './statuses'; -import type { ApiHashtagJSON } from './tags'; - -export type ApiSearchType = 'accounts' | 'statuses' | 'hashtags'; - -export interface ApiSearchResultsJSON { - accounts: ApiAccountJSON[]; - statuses: ApiStatusJSON[]; - hashtags: ApiHashtagJSON[]; -} diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts deleted file mode 100644 index a83c99351e..0000000000 --- a/app/javascript/mastodon/api_types/statuses.ts +++ /dev/null @@ -1,127 +0,0 @@ -// See app/serializers/rest/status_serializer.rb - -import type { ApiAccountJSON } from './accounts'; -import type { ApiCustomEmojiJSON } from './custom_emoji'; -import type { ApiMediaAttachmentJSON } from './media_attachments'; -import type { ApiPollJSON } from './polls'; - -// See app/modals/status.rb visibility+limited_scope -export type StatusVisibility = - | 'public' - | 'unlisted' - | 'private' - | 'direct' - | 'public_unlisted' - | 'login' - | 'mutual' - | 'circle' - | 'personal' - | 'reply' - | 'limited'; - -export interface ApiStatusApplicationJSON { - name: string; - website: string; -} - -export interface ApiTagJSON { - name: string; - url: string; -} - -export interface ApiMentionJSON { - id: string; - username: string; - url: string; - acct: string; -} - -export interface ApiPreviewCardAuthorJSON { - name: string; - url: string; - account?: ApiAccountJSON; -} - -export interface ApiPreviewCardJSON { - url: string; - title: string; - description: string; - language: string; - type: string; - author_name: string; - author_url: string; - author_account?: ApiAccountJSON; - provider_name: string; - provider_url: string; - html: string; - width: number; - height: number; - image: string; - image_description: string; - embed_url: string; - blurhash: string; - published_at: string; - authors: ApiPreviewCardAuthorJSON[]; -} - -export type FilterContext = - | 'home' - | 'notifications' - | 'public' - | 'thread' - | 'account'; - -export interface ApiFilterJSON { - id: string; - title: string; - context: FilterContext; - expires_at: string; - filter_action: 'warn' | 'hide'; - keywords?: unknown[]; // TODO: FilterKeywordSerializer - statuses?: unknown[]; // TODO: FilterStatusSerializer -} - -export interface ApiFilterResultJSON { - filter: ApiFilterJSON; - keyword_matches: string[]; - status_matches: string[]; -} - -export interface ApiStatusJSON { - id: string; - created_at: string; - in_reply_to_id?: string; - in_reply_to_account_id?: string; - sensitive: boolean; - spoiler_text?: string; - visibility: StatusVisibility; - language: string; - uri: string; - url: string; - replies_count: number; - reblogs_count: number; - favorites_count: number; - edited_at?: string; - - favorited?: boolean; - reblogged?: boolean; - muted?: boolean; - bookmarked?: boolean; - pinned?: boolean; - - filtered?: ApiFilterResultJSON[]; - content?: string; - text?: string; - - reblog?: ApiStatusJSON; - application?: ApiStatusApplicationJSON; - account: ApiAccountJSON; - media_attachments: ApiMediaAttachmentJSON[]; - mentions: ApiMentionJSON[]; - - tags: ApiTagJSON[]; - emojis: ApiCustomEmojiJSON[]; - - card?: ApiPreviewCardJSON; - poll?: ApiPollJSON; -} diff --git a/app/javascript/mastodon/api_types/suggestions.ts b/app/javascript/mastodon/api_types/suggestions.ts deleted file mode 100644 index 7d91daf901..0000000000 --- a/app/javascript/mastodon/api_types/suggestions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; - -export type ApiSuggestionSourceJSON = - | 'featured' - | 'most_followed' - | 'most_interactions' - | 'similar_to_recently_followed' - | 'friends_of_friends'; - -export interface ApiSuggestionJSON { - sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]]; - account: ApiAccountJSON; -} diff --git a/app/javascript/mastodon/api_types/tags.ts b/app/javascript/mastodon/api_types/tags.ts deleted file mode 100644 index 0c16c8bd28..0000000000 --- a/app/javascript/mastodon/api_types/tags.ts +++ /dev/null @@ -1,13 +0,0 @@ -interface ApiHistoryJSON { - day: string; - accounts: string; - uses: string; -} - -export interface ApiHashtagJSON { - id: string; - name: string; - url: string; - history: [ApiHistoryJSON, ...ApiHistoryJSON[]]; - following?: boolean; -} diff --git a/app/javascript/mastodon/common.js b/app/javascript/mastodon/common.js index c61e02250c..0ec8449343 100644 --- a/app/javascript/mastodon/common.js +++ b/app/javascript/mastodon/common.js @@ -1,11 +1,12 @@ import Rails from '@rails/ujs'; +import 'font-awesome/css/font-awesome.css'; export function start() { - require.context('../images/', true, /\.(jpg|png|svg)$/); + require.context('../images/', true); try { Rails.start(); - } catch { + } catch (e) { // If called twice } } diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap index dc955b7abe..1c37278483 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap @@ -9,11 +9,7 @@ exports[` renders emoji with custom url 1`] = ` className="emojione" src="http://example.com/emoji.png" /> -

- :foobar: -
+ :foobar: `; @@ -26,10 +22,6 @@ exports[` renders native emoji 1`] = ` className="emojione" src="/emoji/1f499.svg" /> -
- :foobar: -
+ :foobar: `; diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap index 124b50d8c7..7fbdedeb23 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap @@ -2,7 +2,7 @@ exports[` Autoplay renders a animated avatar 1`] = `
Autoplay renders a animated avatar 1`] = ` } >
@@ -23,7 +21,7 @@ exports[` Autoplay renders a animated avatar 1`] = ` exports[` Still renders a still avatar 1`] = `
Still renders a still avatar 1`] = ` } >
diff --git a/app/javascript/mastodon/components/__tests__/button-test.jsx b/app/javascript/mastodon/components/__tests__/button-test.jsx index f38ff6a7dd..6de961f784 100644 --- a/app/javascript/mastodon/components/__tests__/button-test.jsx +++ b/app/javascript/mastodon/components/__tests__/button-test.jsx @@ -1,8 +1,7 @@ +import { render, fireEvent, screen } from '@testing-library/react'; import renderer from 'react-test-renderer'; -import { render, fireEvent, screen } from 'mastodon/test_helpers'; - -import { Button } from '../button'; +import Button from '../button'; describe('