Compare commits

..

4 Commits

Author SHA1 Message Date
Eugen Rochko
661f3f26b0 Bump version to 3.1.5 2020-07-07 15:22:47 +02:00
Thibaut Girka
2d2e3651ee Fix media attachment enumeration
Signed-off-by: Eugen Rochko <eugen@zeonfederated.com>
2020-07-07 15:13:23 +02:00
Eugen Rochko
951e997b26 Change rate limits for various paths
- Rate limit login attempts by target account
- Rate limit password resets and e-mail re-confirmations by target account
- Rate limit sign-up/login attempts, password resets, and e-mail re-confirmations by IP like before
2020-07-07 15:13:19 +02:00
Eugen Rochko
fa3f78e4bf Fix other sessions not being logged out on password change
While OAuth tokens were immediately revoked, accessing the home
controller immediately generated new OAuth tokens and "revived"
the session due to a combination of using remember_me tokens and
overwriting the `authenticate_user!` method
2020-07-07 15:13:14 +02:00
2121 changed files with 27575 additions and 79806 deletions

View File

@@ -1,3 +1,4 @@
https://github.com/heroku/heroku-buildpack-apt https://github.com/heroku/heroku-buildpack-apt
https://github.com/Scalingo/ffmpeg-buildpack https://github.com/Scalingo/ffmpeg-buildpack
https://github.com/Scalingo/nodejs-buildpack
https://github.com/Scalingo/ruby-buildpack https://github.com/Scalingo/ruby-buildpack

View File

@@ -72,12 +72,11 @@ aliases:
- run: - run:
name: Set bundler settings name: Set bundler settings
command: | command: |
bundle config --local clean 'true' bundle config clean 'true'
bundle config --local deployment 'true' bundle config deployment 'true'
bundle config --local with 'pam_authentication' bundle config with 'pam_authentication'
bundle config --local without 'development production' bundle config without 'development production'
bundle config --local frozen 'true' bundle config frozen 'true'
bundle config --local path $BUNDLE_PATH
- run: - run:
name: Install bundler dependencies name: Install bundler dependencies
command: bundle check || (bundle install && bundle clean) command: bundle check || (bundle install && bundle clean)
@@ -129,13 +128,6 @@ jobs:
environment: *ruby_environment environment: *ruby_environment
<<: *install_ruby_dependencies <<: *install_ruby_dependencies
install-ruby3.0:
<<: *defaults
docker:
- image: circleci/ruby:3.0-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies
build: build:
<<: *defaults <<: *defaults
steps: steps:
@@ -167,45 +159,8 @@ jobs:
name: Create database name: Create database
command: ./bin/rails db:create command: ./bin/rails db:create
- run: - run:
command: ./bin/rails db:migrate VERSION=20171010025614 name: Run migrations
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate command: ./bin/rails db:migrate
name: Run all remaining migrations
test-two-step-migrations:
<<: *defaults
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
steps:
- *attach_workspace
- *install_system_dependencies
- run:
command: ./bin/rails db:create
name: Create database
- run:
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all pre-deployment migrations
evironment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
test-ruby2.7: test-ruby2.7:
<<: *defaults <<: *defaults
@@ -231,18 +186,6 @@ jobs:
- image: circleci/redis:5-alpine - image: circleci/redis:5-alpine
<<: *test_steps <<: *test_steps
test-ruby3.0:
<<: *defaults
docker:
- image: circleci/ruby:3.0-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps
test-webui: test-webui:
<<: *defaults <<: *defaults
docker: docker:
@@ -253,6 +196,24 @@ jobs:
name: Run jest name: Run jest
command: yarn test:jest command: yarn test:jest
check-i18n:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Check locale file normalization
command: bundle exec i18n-tasks check-normalized
- run:
name: Check for unused strings
command: bundle exec i18n-tasks unused -l en
- run:
name: Check for wrong string interpolations
command: bundle exec i18n-tasks check-consistent-interpolations
- run:
name: Check that all required locale files exist
command: bundle exec rake repo:check_locales_files
workflows: workflows:
version: 2 version: 2
build-and-test: build-and-test:
@@ -265,19 +226,12 @@ workflows:
requires: requires:
- install - install
- install-ruby2.7 - install-ruby2.7
- install-ruby3.0:
requires:
- install
- install-ruby2.7
- build: - build:
requires: requires:
- install-ruby2.7 - install-ruby2.7
- test-migrations: - test-migrations:
requires: requires:
- install-ruby2.7 - install-ruby2.7
- test-two-step-migrations:
requires:
- install-ruby2.7
- test-ruby2.7: - test-ruby2.7:
requires: requires:
- install-ruby2.7 - install-ruby2.7
@@ -286,10 +240,9 @@ workflows:
requires: requires:
- install-ruby2.6 - install-ruby2.6
- build - build
- test-ruby3.0:
requires:
- install-ruby3.0
- build
- test-webui: - test-webui:
requires: requires:
- install - install
- check-i18n:
requires:
- install-ruby2.7

View File

@@ -27,10 +27,10 @@ plugins:
enabled: true enabled: true
eslint: eslint:
enabled: true enabled: true
channel: eslint-7 channel: eslint-6
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-1-9-1 channel: rubocop-0-82
sass-lint: sass-lint:
enabled: true enabled: true
exclude_patterns: exclude_patterns:

View File

@@ -1,23 +0,0 @@
version = 1
test_patterns = ["app/javascript/mastodon/**/__tests__/**"]
exclude_patterns = [
"db/migrate/**",
"db/post_migrate/**"
]
[[analyzers]]
name = "ruby"
enabled = true
[[analyzers]]
name = "javascript"
enabled = true
[analyzers.meta]
environment = [
"browser",
"jest",
"nodejs"
]

28
.dependabot/config.yml Normal file
View File

@@ -0,0 +1,28 @@
version: 1
update_configs:
- package_manager: "ruby:bundler"
directory: "/"
update_schedule: "weekly"
# Supported update schedule: live daily weekly monthly
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security
- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"
# Supported update schedule: live daily weekly monthly
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security

View File

@@ -1,10 +1,6 @@
.bundle .bundle
.env .env
.env.* .env.*
.git
.gitattributes
.gitignore
.github
public/system public/system
public/assets public/assets
public/packs public/packs
@@ -17,4 +13,3 @@ vendor/bundle
postgres postgres
redis redis
elasticsearch elasticsearch
chart

View File

@@ -1,66 +1,262 @@
# This is a sample configuration file. You can generate your configuration # Service dependencies
# with the `rake mastodon:setup` interactive setup wizard, but to customize # You may set REDIS_URL instead for more advanced options
# your setup even further, you'll need to edit it manually. This sample does # You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers
# not demonstrate all available configuration options. Please look at REDIS_HOST=redis
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895
# Federation
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=example.com
# Redis
# -----
REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
# You may set DATABASE_URL instead for more advanced options
# PostgreSQL DB_HOST=db
# ---------- DB_USER=postgres
DB_HOST=/var/run/postgresql DB_NAME=postgres
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS= DB_PASS=
DB_PORT=5432 DB_PORT=5432
# Optional ElasticSearch configuration
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
# ES_ENABLED=true
# ES_HOST=es
# ES_PORT=9200
# ElasticSearch (optional) # Federation
# ------------------------ # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
ES_ENABLED=true # LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
ES_HOST=localhost LOCAL_DOMAIN=example.com
ES_PORT=9200
# Secrets # Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
# -------
# Make sure to use `rake secret` to generate secrets # Use this only if you need to run mastodon on a different domain than the one used for federation.
# ------- # You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
# WEB_DOMAIN=mastodon.example.com
# Use this if you want to have several aliases handler@example1.com
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose)
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
# Web Push # VAPID keys (used for push notifications
# -------- # You can generate the keys using the following command (first is the private key, second is the public one)
# Generate with `rake mastodon:webpush:generate_vapid_key` # You should only generate this once per instance. If you later decide to change it, all push subscription will
# -------- # be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key` if you use docker compose)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY= VAPID_PUBLIC_KEY=
# Sending mail # Registrations
# ------------ # Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
# If you want to use an SMTP server without authentication (e.g local Postfix relay)
# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notificatons@example.com SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# File storage (optional) # Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# ----------------------- # PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
S3_ENABLED=true # PAPERCLIP_ROOT_URL=/system
S3_BUCKET=files.example.com
AWS_ACCESS_KEY_ID= # Optional asset host for multi-server setups
AWS_SECRET_ACCESS_KEY= # The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
S3_ALIAS_HOST=files.example.com # if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com
# S3 (optional)
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Some OpenStack V3 providers require PROJECT_ID (optional)
# SWIFT_PROJECT_ID=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)
# S3_ALIAS_HOST=
# Streaming API integration
# STREAMING_API_BASE_URL=
# Advanced settings
# If you need to use pgBouncer, you need to disable prepared statements:
# PREPARED_STATEMENTS=false
# Cluster number setting for streaming API server.
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
STREAMING_CLUSTER_NUM=1
# Docker mastodon user
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_METHOD=simple_tls
# LDAP_BASE=
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable
# and optional as fallback PAM_DEFAULT_SUFFIX
# The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true
# Fallback email domain for email address generation (LOCAL_DOMAIN by default)
# PAM_EMAIL_DOMAIN=example.com
# Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
# PAM_CONTROLLED_SERVICE=rpam
# Global OAuth settings (optional) :
# If you have only one strategy, you may want to enable this
# OAUTH_REDIRECT_AT_SIGN_IN=true
# Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/
# CAS_HOST=sso.myserver.com/
# CAS_PORT=443
# CAS_SSL=true
# CAS_VALIDATE_URL=
# CAS_CALLBACK_URL=
# CAS_LOGOUT_URL=
# CAS_LOGIN_URL=
# CAS_UID_FIELD='user'
# CAS_CA_PATH=
# CAS_DISABLE_SSL_VERIFICATION=false
# CAS_UID_KEY='user'
# CAS_NAME_KEY='name'
# CAS_EMAIL_KEY='email'
# CAS_NICKNAME_KEY='nickname'
# CAS_FIRST_NAME_KEY='firstname'
# CAS_LAST_NAME_KEY='lastname'
# CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone'
# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
# SAML_ISSUER=https://example.com
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT=
# SAML_NAME_IDENTIFIER_FORMAT=
# SAML_CERT=
# SAML_PRIVATE_KEY=
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# Authorized fetch mode (optional)
# Require remote servers to authentify when fetching toots, see
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
# AUTHORIZED_FETCH=true
# Whitelist mode (optional)
# Only allow federation with whitelisted domains, see
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
# WHITELIST_MODE=true

View File

@@ -1,4 +1,3 @@
VAGRANT=true VAGRANT=true
LOCAL_DOMAIN=mastodon.local LOCAL_DOMAIN=mastodon.local
BIND=0.0.0.0 BIND=0.0.0.0
DB_HOST=/var/run/postgresql/

View File

@@ -199,11 +199,6 @@ module.exports = {
'import/no-unresolved': 'error', 'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error', 'import/no-webpack-loader-syntax': 'error',
'promise/catch-or-return': [ 'promise/catch-or-return': 'error',
'error',
{
allowFinally: true,
},
],
}, },
}; };

2
.github/CODEOWNERS vendored
View File

@@ -1,4 +1,4 @@
# CODEOWNERS for mastodon/mastodon # CODEOWNERS for tootsuite/mastodon
# Translators # Translators
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address. # To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.

1
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
patreon: mastodon patreon: mastodon
open_collective: mastodon open_collective: mastodon
github: [Gargron]

View File

@@ -1,7 +1,7 @@
--- ---
name: Bug Report name: Bug Report
about: If something isn't working as expected about: If something isn't working as expected
labels: bug
--- ---
<!-- Make sure that you are submitting a new bug that was not previously reported or already fixed --> <!-- Make sure that you are submitting a new bug that was not previously reported or already fixed -->

View File

@@ -1,6 +1,7 @@
--- ---
name: Feature Request name: Feature Request
about: I have a suggestion about: I have a suggestion
--- ---
<!-- Please use a concise and distinct title for the issue --> <!-- Please use a concise and distinct title for the issue -->

View File

@@ -1,22 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: direct
- package-ecosystem: bundler
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: direct

View File

@@ -1,34 +0,0 @@
name: Build container image
on:
workflow_dispatch:
push:
branches:
- "main"
tags:
- "*"
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/metadata-action@v3
id: meta
with:
images: tootsuite/mastodon
flavor: |
latest=auto
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
- uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=tootsuite/mastodon:latest
cache-to: type=inline

View File

@@ -1,34 +0,0 @@
name: Check i18n
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
RAILS_ENV: test
jobs:
check-i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '2.7'
bundler-cache: true
- name: Check locale file normalization
run: bundle exec i18n-tasks check-normalized
- name: Check for unused strings
run: bundle exec i18n-tasks unused -l en
- name: Check for wrong string interpolations
run: bundle exec i18n-tasks check-consistent-interpolations
- name: Check that all required locale files exist
run: bundle exec rake repo:check_locales_files

25
.gitignore vendored
View File

@@ -17,34 +17,31 @@
/log/* /log/*
!/log/.keep !/log/.keep
/tmp /tmp
/coverage coverage
/public/system public/system
/public/assets public/assets
/public/packs public/packs
/public/packs-test public/packs-test
.env .env
.env.production .env.production
.env.development .env.development
/node_modules/ node_modules/
/build/ build/
# Ignore Vagrant files # Ignore Vagrant files
.vagrant/ .vagrant/
# Ignore Capistrano customizations # Ignore Capistrano customizations
/config/deploy/* config/deploy/*
# Ignore IDE files # Ignore IDE files
.vscode/ .vscode/
.idea/ .idea/
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose # Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres postgres
/redis redis
/elasticsearch elasticsearch
# ignore Helm dependency charts
/chart/charts/*.tgz
# Ignore Apple files # Ignore Apple files
.DS_Store .DS_Store

View File

@@ -2,8 +2,7 @@ require:
- rubocop-rails - rubocop-rails
AllCops: AllCops:
TargetRubyVersion: 2.5 TargetRubyVersion: 2.4
NewCops: disable
Exclude: Exclude:
- 'spec/**/*' - 'spec/**/*'
- 'db/**/*' - 'db/**/*'
@@ -26,68 +25,26 @@ Layout/AccessModifierIndentation:
Layout/EmptyLineAfterMagicComment: Layout/EmptyLineAfterMagicComment:
Enabled: false Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/HashAlignment:
Enabled: false
# EnforcedHashRocketStyle: table
# EnforcedColonStyle: table
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Layout/SpaceInsideHashLiteralBraces: Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: space EnforcedStyle: space
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/DuplicateElsifCondition:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: true
Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Lint/UselessAccessModifier:
ContextCreatingMethods:
- class_methods
Metrics/AbcSize: Metrics/AbcSize:
Max: 100 Max: 100
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/BlockLength: Metrics/BlockLength:
Max: 55 Max: 35
Exclude: Exclude:
- 'lib/tasks/**/*' - 'lib/tasks/**/*'
- 'lib/mastodon/*_cli.rb'
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 3 Max: 3
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/ClassLength: Metrics/ClassLength:
CountComments: false CountComments: false
Max: 400 Max: 300
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 25 Max: 25
Exclude:
- 'lib/mastodon/*_cli.rb'
Layout/LineLength: Layout/LineLength:
AllowURI: true AllowURI: true
@@ -95,9 +52,7 @@ Layout/LineLength:
Metrics/MethodLength: Metrics/MethodLength:
CountComments: false CountComments: false
Max: 65 Max: 55
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/ModuleLength: Metrics/ModuleLength:
CountComments: false CountComments: false
@@ -108,90 +63,34 @@ Metrics/ParameterLists:
CountKeywordArgs: true CountKeywordArgs: true
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 25 Max: 20
Naming/MemoizedInstanceVariableName: Naming/MemoizedInstanceVariableName:
Enabled: false Enabled: false
Naming/MethodParameterName:
Enabled: true
Rails: Rails:
Enabled: true Enabled: true
Rails/ApplicationController:
Enabled: false
Exclude:
- 'app/controllers/well_known/**/*.rb'
Rails/BelongsTo:
Enabled: false
Rails/ContentTag:
Enabled: false
Rails/EnumHash: Rails/EnumHash:
Enabled: false Enabled: false
Rails/HasAndBelongsToMany:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/HttpStatus:
Enabled: false
Rails/Exit: Rails/Exit:
Exclude: Exclude:
- 'lib/mastodon/*' - 'lib/mastodon/*'
- 'lib/cli.rb' - 'lib/cli.rb'
Rails/FilePath:
Enabled: false
Rails/HasAndBelongsToMany:
Enabled: false
Rails/HasManyOrHasOneDependent:
Enabled: false
Rails/HelperInstanceVariable: Rails/HelperInstanceVariable:
Enabled: false Enabled: false
Rails/HttpStatus:
Enabled: false
Rails/IndexBy:
Enabled: false
Rails/InverseOf:
Enabled: false
Rails/LexicallyScopedActionFilter:
Enabled: false
Rails/OutputSafety:
Enabled: true
Rails/RakeEnvironment:
Enabled: false
Rails/RedundantForeignKey:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/UniqueValidationWithoutIndex:
Enabled: false
Style/AccessorGrouping:
Enabled: true
Style/AccessModifierDeclarations:
Enabled: false
Style/ArrayCoercion:
Enabled: true
Style/BisectedAttrAccessor:
Enabled: true
Style/CaseLikeIf:
Enabled: false
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false
@@ -206,15 +105,6 @@ Style/Documentation:
Style/DoubleNegation: Style/DoubleNegation:
Enabled: true Enabled: true
Style/ExpandPathArguments:
Enabled: false
Style/ExponentialNotation:
Enabled: true
Style/FormatString:
Enabled: false
Style/FormatStringToken: Style/FormatStringToken:
Enabled: false Enabled: false
@@ -224,33 +114,9 @@ Style/FrozenStringLiteralComment:
Style/GuardClause: Style/GuardClause:
Enabled: false Enabled: false
Style/HashAsLastArrayItem:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashLikeCase:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: false
Style/IfUnlessModifier:
Enabled: false
Style/InverseMethods:
Enabled: false
Style/Lambda: Style/Lambda:
Enabled: false Enabled: false
Style/MutableConstant:
Enabled: false
Style/PercentLiteralDelimiters: Style/PercentLiteralDelimiters:
PreferredDelimiters: PreferredDelimiters:
'%i': '()' '%i': '()'
@@ -259,36 +125,9 @@ Style/PercentLiteralDelimiters:
Style/PerlBackrefs: Style/PerlBackrefs:
AutoCorrect: false AutoCorrect: false
Style/RedundantAssignment:
Enabled: false
Style/RedundantFetchBlock:
Enabled: true
Style/RedundantFileExtensionInRequire:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: false
Style/RedundantRegexpEscape:
Enabled: false
Style/RedundantReturn:
Enabled: true
Style/RegexpLiteral: Style/RegexpLiteral:
Enabled: false Enabled: false
Style/RescueStandardError:
Enabled: false
Style/SignalException:
Enabled: false
Style/SlicingWithRange:
Enabled: true
Style/SymbolArray: Style/SymbolArray:
Enabled: false Enabled: false
@@ -297,6 +136,3 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral: Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma' EnforcedStyleForMultiline: 'comma'
Style/UnpackFirst:
Enabled: false

View File

@@ -1 +1 @@
2.7.2 2.6.6

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ libidn11
libidn11-dev libidn11-dev
libpq-dev libpq-dev
libprotobuf-dev libprotobuf-dev
libssl-dev
libxdamage1 libxdamage1
libxfixes3 libxfixes3
protobuf-compiler protobuf-compiler
@@ -22,7 +23,7 @@ libpixman-1-0
librsvg2-2 librsvg2-2
libthai-data libthai-data
libthai0 libthai0
libvpx[5-9] libvpx5
libxcb-render0 libxcb-render0
libxcb-shm0 libxcb-shm0
libxrender1 libxrender1

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
## Bug reports ## Bug reports
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected. Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
## Translations ## Translations
@@ -24,17 +24,9 @@ You can submit translations via [Crowdin](https://crowdin.com/project/mastodon).
## Pull requests ## Pull requests
**Please use clean, concise titles for your pull requests.** Unless the pull request is about refactoring code, updating dependencies or other internal tasks, assume that the person reading the pull request title is not a programmer or Mastodon developer, but instead a Mastodon user or server administrator, and **try to describe your change or fix from their perspective**. We use commit squashing, so the final commit in the main branch will carry the title of the pull request, and commits from the main branch are fed into the changelog. The changelog is separated into [keepachangelog.com categories](https://keepachangelog.com/en/1.0.0/), and while that spec does not prescribe how the entries ought to be named, for easier sorting, start your pull request titles using one of the verbs "Add", "Change", "Deprecate", "Remove", or "Fix" (present tense). Please use clean, concise titles for your pull requests. We use commit squashing, so the final commit in the master branch will carry the title of the pull request.
Example: The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged. Splitting tasks into multiple smaller pull requests is often preferable.
|Not ideal|Better|
|---|----|
|Fixed NoMethodError in RemovalWorker|Fix nil error when removing statuses caused by race condition|
It is not always possible to phrase every change in such a manner, but it is desired.
**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable.
**Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind: **Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind:
@@ -44,4 +36,4 @@ It is not always possible to phrase every change in such a manner, but it is des
## Documentation ## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/docs](https://source.joinmastodon.org/mastodon/docs).

View File

@@ -1,10 +1,10 @@
FROM ubuntu:20.04 as build-dep FROM ubuntu:18.04 as build-dep
# Use bash for the shell # Use bash for the shell
SHELL ["/bin/bash", "-c"] SHELL ["bash", "-c"]
# Install Node v12 (LTS) # Install Node v12 (LTS)
ENV NODE_VER="12.21.0" ENV NODE_VER="12.16.1"
RUN ARCH= && \ RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \ dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \ case "${dpkgArch##*-}" in \
@@ -17,19 +17,34 @@ RUN ARCH= && \
*) echo "unsupported architecture"; exit 1 ;; \ *) echo "unsupported architecture"; exit 1 ;; \
esac && \ esac && \
echo "Etc/UTC" > /etc/localtime && \ echo "Etc/UTC" > /etc/localtime && \
apt-get update && \ apt update && \
apt-get install -y --no-install-recommends ca-certificates wget python && \ apt -y install wget python && \
cd ~ && \ cd ~ && \
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \ wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \ tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \ rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
mv node-v$NODE_VER-linux-$ARCH /opt/node mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install jemalloc
ENV JE_VER="5.2.1"
RUN apt update && \
apt -y install make autoconf gcc g++ && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
cd jemalloc-$JE_VER && \
./autogen.sh && \
./configure --prefix=/opt/jemalloc && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib
# Install Ruby # Install Ruby
ENV RUBY_VER="2.7.2" ENV RUBY_VER="2.6.6"
RUN apt-get update && \ ENV CPPFLAGS="-I/opt/jemalloc/include"
apt-get install -y --no-install-recommends build-essential \ ENV LDFLAGS="-L/opt/jemalloc/lib/"
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \ RUN apt update && \
apt -y install build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \ libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \ cd ~ && \
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \ wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
@@ -39,31 +54,32 @@ RUN apt-get update && \
--with-jemalloc \ --with-jemalloc \
--with-shared \ --with-shared \
--disable-install-doc && \ --disable-install-doc && \
make -j"$(nproc)" > /dev/null && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \
make install && \ make -j$(nproc) > /dev/null && \
rm -rf ../ruby-$RUBY_VER.tar.gz ../ruby-$RUBY_VER make install
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \ RUN npm install -g yarn && \
gem install bundler && \ gem install bundler && \
apt-get update && \ apt update && \
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \ apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info libpq-dev libprotobuf-dev protobuf-compiler
COPY Gemfile* package.json yarn.lock /opt/mastodon/ COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \ RUN cd /opt/mastodon && \
bundle config set --local deployment 'true' && \ bundle config set deployment 'true' && \
bundle config set --local without 'development test' && \ bundle config set without 'development test' && \
bundle install -j"$(nproc)" && \ bundle install -j$(nproc) && \
yarn install --pure-lockfile yarn install --pure-lockfile
FROM ubuntu:20.04 FROM ubuntu:18.04
# Copy over all the langs needed for runtime # Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node COPY --from=build-dep /opt/node /opt/node
COPY --from=build-dep /opt/ruby /opt/ruby COPY --from=build-dep /opt/ruby /opt/ruby
COPY --from=build-dep /opt/jemalloc /opt/jemalloc
# Add more PATHs to the PATH # Add more PATHs to the PATH
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
@@ -71,26 +87,32 @@ ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
# Create the mastodon user # Create the mastodon user
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN apt update && \
RUN apt-get update && \
echo "Etc/UTC" > /etc/localtime && \ echo "Etc/UTC" > /etc/localtime && \
apt-get install -y --no-install-recommends whois wget && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt install -y whois wget && \
addgroup --gid $GID mastodon && \ addgroup --gid $GID mastodon && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256)" | chpasswd && \ echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
rm -rf /var/lib/apt/lists/*
# Install mastodon runtime deps # Install mastodon runtime deps
RUN apt-get update && \ RUN apt -y --no-install-recommends install \
apt-get -y --no-install-recommends install \ libssl1.1 libpq5 imagemagick ffmpeg \
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \ libicu60 libprotobuf10 libidn11 libyaml-0-2 \
libicu66 libprotobuf17 libidn11 libyaml-0-2 \ file ca-certificates tzdata libreadline7 && \
file ca-certificates tzdata libreadline8 gcc tini && \ apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \ ln -s /opt/mastodon /mastodon && \
gem install bundler && \ gem install bundler && \
rm -rf /var/cache && \ rm -rf /var/cache && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Add tini
ENV TINI_VERSION="0.18.0"
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855"
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini
# Copy over mastodon source, and dependencies from building, and set permissions # Copy over mastodon source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
@@ -113,5 +135,5 @@ RUN cd ~ && \
# Set the work dir and the container entry point # Set the work dir and the container entry point
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/tini", "--"]
EXPOSE 3000 4000 EXPOSE 3000 4000

136
Gemfile
View File

@@ -1,111 +1,116 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.5.0', '< 3.1.0' ruby '>= 2.5.0', '< 3.0.0'
gem 'pkg-config', '~> 1.4' gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 5.3' gem 'puma', '~> 4.3'
gem 'rails', '~> 6.1.3' gem 'rails', '~> 5.2.4.2'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.1' gem 'thor', '~> 0.20'
gem 'rack', '~> 2.2.3' gem 'rack', '~> 2.2.2'
gem 'thwait', '~> 0.1.0'
gem 'e2mmap', '~> 0.1.0'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.2' gem 'pg', '~> 1.2'
gem 'makara', '~> 0.5' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.8' gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.95', require: false gem 'aws-sdk-s3', '~> 1.64', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
gem 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0'
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.6.0', require: false gem 'bootsnap', '~> 1.4', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.2' gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.4.2' gem 'cld3', '~> 3.3.0'
gem 'devise', '~> 4.8' gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 4.0' gem 'devise-two-factor', '~> 3.1'
group :pam_authentication, optional: true do group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2' gem 'devise_pam_authenticatable2', '~> 9.2'
end end
gem 'net-ldap', '~> 0.17' gem 'net-ldap', '~> 0.16'
gem 'omniauth-cas', '~> 2.0' gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.5' gem 'doorkeeper', '~> 5.4'
gem 'ed25519', '~> 1.2'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.8' gem 'redis-namespace', '~> 1.7'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.4' gem 'http', '~> 4.4'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.5.0' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
gem 'httplog', '~> 1.4.2'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.11' gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.11' gem 'oj', '~> 3.10'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.13'
gem 'parslet' gem 'parslet'
gem 'parallel', '~> 1.20' gem 'parallel', '~> 1.19'
gem 'posix-spawn' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1' gem 'pundit', '~> 2.1'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.5' gem 'rack-attack', '~> 6.3'
gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 6.0' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.0' gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.11' gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 5.2' gem 'sanitize', '~> 5.1'
gem 'scenic', '~> 1.5' gem 'sidekiq', '~> 6.0'
gem 'sidekiq', '~> 6.2'
gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 7.0' gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-bulk', '~>0.2.0' gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.3' gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.1' gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.1' gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.7' gem 'strong_migrations', '~> 0.6'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-command', '~> 0.9', require: false
gem 'twitter-text', '~> 3.1.0' gem 'tty-prompt', '~> 0.21', require: false
gem 'tzinfo-data', '~> 1.2021' gem 'twitter-text', '~> 1.14'
gem 'webpacker', '~> 5.4' gem 'tzinfo-data', '~> 1.2020'
gem 'webpush', '~> 0.3' gem 'webpacker', '~> 5.1'
gem 'webauthn', '~> 3.0.0.alpha1' gem 'webpush'
gem 'json-ld' gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.1' gem 'json-ld-preloaded', '~> 3.1'
gem 'rdf-normalize', '~> 0.4' gem 'rdf-normalize', '~> 0.4'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.22' gem 'fabrication', '~> 2.21'
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.9' gem 'pry-byebug', '~> 3.9'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 5.0' gem 'rspec-rails', '~> 4.0'
end end
group :production, :test do group :production, :test do
@@ -113,35 +118,35 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.35' gem 'capybara', '~> 3.32'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.18' gem 'faker', '~> 2.11'
gem 'microformats', '~> 4.2' gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.21', require: false gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.13' gem 'webmock', '~> 3.8'
gem 'parallel_tests', '~> 3.7' gem 'parallel_tests', '~> 2.32'
gem 'rspec_junit_formatter', '~> 0.4' gem 'rspec_junit_formatter', '~> 0.4'
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.8' gem 'active_record_query_trace', '~> 1.7'
gem 'annotate', '~> 3.1' gem 'annotate', '~> 3.1'
gem 'better_errors', '~> 2.9' gem 'better_errors', '~> 2.7'
gem 'binding_of_caller', '~> 1.0' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.1' gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4' gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 1.15', require: false gem 'rubocop', '~> 0.82', require: false
gem 'rubocop-rails', '~> 2.10', require: false gem 'rubocop-rails', '~> 2.5', require: false
gem 'brakeman', '~> 5.0', require: false gem 'brakeman', '~> 4.8', require: false
gem 'bundler-audit', '~> 0.8', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'capistrano', '~> 3.16' gem 'capistrano', '~> 3.14'
gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rails', '~> 1.4'
gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
gem 'stackprof' gem 'stackprof'
@@ -149,11 +154,8 @@ end
group :production do group :production do
gem 'lograge', '~> 0.11' gem 'lograge', '~> 0.11'
gem 'redis-rails', '~> 5.0'
end end
gem 'concurrent-ruby', require: false gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'resolv', '~> 0.1.0'

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
web: bin/heroku-web web: if [ "$RUN_STREAMING" != "true" ]; then BIND=0.0.0.0 bundle exec puma -C config/puma.rb; else BIND=0.0.0.0 node ./streaming; fi
worker: bundle exec sidekiq worker: bundle exec sidekiq
# For the streaming API, you need a separate app that shares Postgres and Redis: # For the streaming API, you need a separate app that shares Postgres and Redis:

View File

@@ -1,4 +1,4 @@
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb web: env PORT=3000 bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq sidekiq: env PORT=3000 bundle exec sidekiq
stream: env PORT=4000 yarn run start stream: env PORT=4000 yarn run start
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0

View File

@@ -1,14 +1,14 @@
![Mastodon](https://i.imgur.com/NhZc40l.png) ![Mastodon](https://i.imgur.com/NhZc40l.png)
======== ========
[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases] [![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci] [![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker] [![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/mastodon/mastodon/releases [releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/mastodon/mastodon [circleci]: https://circleci.com/gh/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[crowdin]: https://crowdin.com/project/mastodon [crowdin]: https://crowdin.com/project/mastodon
[docker]: https://hub.docker.com/r/tootsuite/mastodon/ [docker]: https://hub.docker.com/r/tootsuite/mastodon/
@@ -70,7 +70,7 @@ Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Strea
- **PostgreSQL** 9.5+ - **PostgreSQL** 9.5+
- **Redis** 4+ - **Redis** 4+
- **Ruby** 2.5+ - **Ruby** 2.5+
- **Node.js** 12+ - **Node.js** 10.13+
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
@@ -82,11 +82,11 @@ Mastodon is **free, open-source software** licensed under **AGPLv3**.
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Crowdin. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Crowdin. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
**IRC channel**: #mastodon on irc.libera.chat **IRC channel**: #mastodon on irc.freenode.net
## License ## License
Copyright (C) 2016-2021 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) Copyright (C) 2016-2020 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

View File

@@ -1,13 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 3.4.x | :white_check_mark: |
| 3.3.x | :white_check_mark: |
| < 3.3 | :x: |
## Reporting a Vulnerability
hello@joinmastodon.org

4
Vagrantfile vendored
View File

@@ -12,7 +12,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' sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
# Add repo for NodeJS # Add repo for NodeJS
curl -sL https://deb.nodesource.com/setup_12.x | sudo bash - curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -
# Add firewall rule to redirect 80 to PORT and save # 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"]} sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
@@ -72,12 +72,10 @@ bundle install
yarn install yarn install
# Build Mastodon # Build Mastodon
export RAILS_ENV=development
export $(cat ".env.vagrant" | xargs) export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup bundle exec rails db:setup
# Configure automatic loading of environment variable # Configure automatic loading of environment variable
echo 'export RAILS_ENV=development' >> ~/.bash_profile
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
SCRIPT SCRIPT

View File

@@ -1,8 +1,8 @@
{ {
"name": "Mastodon", "name": "Mastodon",
"description": "A GNU Social-compatible microblogging server", "description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/mastodon/mastodon", "repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/mastodon.png", "logo": "https://github.com/tootsuite.png",
"env": { "env": {
"HEROKU": { "HEROKU": {
"description": "Leave this as true", "description": "Leave this as true",
@@ -88,6 +88,9 @@
{ {
"url": "https://github.com/heroku/heroku-buildpack-apt" "url": "https://github.com/heroku/heroku-buildpack-apt"
}, },
{
"url": "heroku/nodejs"
},
{ {
"url": "heroku/ruby" "url": "heroku/ruby"
} }

View File

@@ -31,9 +31,9 @@ class StatusesIndex < Chewy::Index
}, },
} }
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do
crutch :mentions do |collection| crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id) data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end

View File

@@ -1,15 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class AboutController < ApplicationController class AboutController < ApplicationController
include RegistrationSpamConcern
layout 'public' layout 'public'
before_action :require_open_federation!, only: [:show, :more] before_action :require_open_federation!, only: [:show, :more]
before_action :set_body_classes, only: :show before_action :set_body_classes, only: :show
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_expires_in, only: [:more, :terms] before_action :set_expires_in, only: [:show, :more, :terms]
before_action :set_registration_form_time, only: :show
skip_before_action :require_functional!, only: [:more, :terms] skip_before_action :require_functional!, only: [:more, :terms]
@@ -20,7 +17,6 @@ class AboutController < ApplicationController
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description) toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
@rules = Rule.ordered
@contents = toc_generator.html @contents = toc_generator.html
@table_of_contents = toc_generator.toc @table_of_contents = toc_generator.toc
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?

View File

@@ -2,17 +2,15 @@
class AccountsController < ApplicationController class AccountsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
PAGE_SIZE_MAX = 200
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication include SignatureAuthentication
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_body_classes before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!
def show def show
respond_to do |format| respond_to do |format|
@@ -29,7 +27,8 @@ class AccountsController < ApplicationController
end end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = cached_filtered_status_page @statuses = filtered_status_page
@statuses = cache_collection(@statuses, Status)
@rss_url = rss_url @rss_url = rss_url
unless @statuses.empty? unless @statuses.empty?
@@ -41,15 +40,14 @@ class AccountsController < ApplicationController
format.rss do format.rss do
expires_in 1.minute, public: true expires_in 1.minute, public: true
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE @statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE)
@statuses = filtered_statuses.without_reblogs.limit(limit)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end end
format.json do format.json do
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end end
end end
end end
@@ -77,7 +75,11 @@ class AccountsController < ApplicationController
end end
def only_media_scope def only_media_scope
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) Status.where(id: account_media_status_ids)
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end end
def no_replies_scope def no_replies_scope
@@ -98,10 +100,6 @@ class AccountsController < ApplicationController
params[:username] params[:username]
end end
def skip_temporary_suspension_response?
request.format == :json
end
def rss_url def rss_url
if tag_requested? if tag_requested?
short_account_tag_url(@account, params[:tag], format: 'rss') short_account_tag_url(@account, params[:tag], format: 'rss')
@@ -131,27 +129,30 @@ class AccountsController < ApplicationController
end end
def media_requested? def media_requested?
request.path.split('.').first.end_with?('/media') && !tag_requested? request.path.split('.').first.ends_with?('/media') && !tag_requested?
end end
def replies_requested? def replies_requested?
request.path.split('.').first.end_with?('/with_replies') && !tag_requested? request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
end end
def tag_requested? def tag_requested?
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end end
def cached_filtered_status_page def filtered_status_page
cache_collection_paginated_by_id( filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
filtered_statuses,
Status,
PAGE_SIZE,
params_slice(:max_id, :min_id, :since_id)
)
end end
def params_slice(*keys) def params_slice(*keys)
params.slice(*keys).permit(*keys) params.slice(*keys).permit(*keys)
end end
def restrict_fields_to
if signed_request_account.present? || public_fetch_mode?
# Return all fields
else
%i(id type preferred_username inbox public_key endpoints)
end
end
end end

View File

@@ -8,8 +8,4 @@ class ActivityPub::BaseController < Api::BaseController
def set_cache_headers def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end end
def skip_temporary_suspension_response?
false
end
end end

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :authenticate_user!
before_action :require_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

View File

@@ -5,69 +5,51 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include AccountOwnedConcern include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode? before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_items
before_action :set_size before_action :set_size
before_action :set_type before_action :set_statuses
before_action :set_cache_headers before_action :set_cache_headers
def show def show
expires_in 3.minutes, public: public_fetch_mode? expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
end end
private private
def set_items def set_statuses
case params[:id] @statuses = scope_for_collection
when 'featured' @statuses = cache_collection(@statuses, Status)
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
when 'tags'
@items = for_signed_account { @account.featured_tags }
when 'devices'
@items = @account.devices
else
not_found
end
end end
def set_size def set_size
case params[:id] case params[:id]
when 'featured', 'devices', 'tags' when 'featured'
@size = @items.size @size = @account.pinned_statuses.count
else else
not_found not_found
end end
end end
def set_type def scope_for_collection
case params[:id] case params[:id]
when 'featured' when 'featured'
@type = :ordered # Because in public fetch mode we cache the response, there would be no
when 'devices', 'tags' # benefit from performing the check below, since a blocked account or domain
@type = :unordered # would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
Status.none
else else
not_found @account.pinned_statuses
end
end end
end end
def collection_presenter def collection_presenter
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: account_collection_url(@account, params[:id]), id: account_collection_url(@account, params[:id]),
type: @type, type: :ordered,
size: @size, size: @size,
items: @items items: @statuses
) )
end end
def for_signed_account
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
[]
else
yield
end
end
end end

View File

@@ -1,36 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!
before_action :set_items
before_action :set_cache_headers
def show
expires_in 0, public: false
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end
private
def uri_prefix
signed_request_account.uri[Account::URL_PREFIX_RE]
end
def set_items
@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
ActivityPub::CollectionPresenter.new(
id: account_followers_synchronization_url(@account),
type: :ordered,
items: @items
)
end
end

View File

@@ -5,26 +5,25 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
include JsonLdHelper include JsonLdHelper
include AccountOwnedConcern include AccountOwnedConcern
before_action :skip_unknown_actor_activity before_action :skip_unknown_actor_delete
before_action :require_signature! before_action :require_signature!
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
def create def create
upgrade_account upgrade_account
process_collection_synchronization
process_payload process_payload
head 202 head 202
end end
private private
def skip_unknown_actor_activity def skip_unknown_actor_delete
head 202 if unknown_affected_account? head 202 if unknown_deleted_account?
end end
def unknown_affected_account? def unknown_deleted_account?
json = Oj.load(body, mode: :strict) 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.where(uri: json['actor']).exists? json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
rescue Oj::ParseError rescue Oj::ParseError
false false
end end
@@ -33,10 +32,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
params[:account_username].present? params[:account_username].present?
end end
def skip_temporary_suspension_response?
true
end
def body def body
return @body if defined?(@body) return @body if defined?(@body)
@@ -57,19 +52,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
DeliveryFailureTracker.reset!(signed_request_account.inbox_url) DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
end end
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
params = SignatureParamsTransformer.new.apply(tree)
ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params)
rescue Parslet::ParseFailed
Rails.logger.warn 'Error parsing Collection-Synchronization header'
end
def process_payload def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
end end

View File

@@ -11,11 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
if page_requested? expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
expires_in(1.minute, public: public_fetch_mode? && signed_request_account.nil?)
else
expires_in(3.minutes, public: public_fetch_mode?)
end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@@ -24,49 +20,38 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def outbox_presenter def outbox_presenter
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: outbox_url(**page_params), id: account_outbox_url(@account, page_params),
type: :ordered, type: :ordered,
part_of: outbox_url, part_of: account_outbox_url(@account),
prev: prev_page, prev: prev_page,
next: next_page, next: next_page,
items: @statuses items: @statuses
) )
else else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: outbox_url, id: account_outbox_url(@account),
type: :ordered, type: :ordered,
size: @account.statuses_count, size: @account.statuses_count,
first: outbox_url(page: true), first: account_outbox_url(@account, page: true),
last: outbox_url(page: true, min_id: 0) last: account_outbox_url(@account, page: true, min_id: 0)
) )
end end
end end
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 def next_page
outbox_url(page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
end end
def prev_page def prev_page
outbox_url(page: true, min_id: @statuses.first.id) unless @statuses.empty? account_outbox_url(@account, page: true, min_id: @statuses.first.id) unless @statuses.empty?
end end
def set_statuses def set_statuses
return unless page_requested? return unless page_requested?
@statuses = cache_collection_paginated_by_id( @statuses = @account.statuses.permitted_for(@account, signed_request_account)
@account.statuses.permitted_for(@account, signed_request_account), @statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
Status, @statuses = cache_collection(@statuses, Status)
LIMIT,
params_slice(:max_id, :min_id, :since_id)
)
end end
def page_requested? def page_requested?
@@ -76,12 +61,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def page_params def page_params
{ page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact { page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact
end end
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end end

View File

@@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end end
def set_replies def set_replies
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end end

View File

@@ -2,7 +2,7 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, except: [:index] before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:redownload] before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
@@ -14,65 +14,49 @@ module Admin
def show def show
authorize @account, :show? authorize @account, :show?
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom @warnings = @account.targeted_account_warnings.latest.custom
@domain_block = DomainBlock.rule_for(@account.domain)
end end
def memorialize def memorialize
authorize @account, :memorialize? authorize @account, :memorialize?
@account.memorialize! @account.memorialize!
log_action :memorialize, @account log_action :memorialize, @account
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
def enable def enable
authorize @account.user, :enable? authorize @account.user, :enable?
@account.user.enable! @account.user.enable!
log_action :enable, @account.user log_action :enable, @account.user
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) redirect_to admin_pending_accounts_path
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) redirect_to admin_pending_accounts_path
end
def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
end
def unsensitive
authorize @account, :unsensitive?
@account.unsensitize!
log_action :unsensitive, @account
redirect_to admin_account_path(@account.id)
end end
def unsilence def unsilence
authorize @account, :unsilence? authorize @account, :unsilence?
@account.unsilence! @account.unsilence!
log_action :unsilence, @account log_action :unsilence, @account
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
def unsuspend def unsuspend
authorize @account, :unsuspend? authorize @account, :unsuspend?
@account.unsuspend! @account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account log_action :unsuspend, @account
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
def redownload def redownload
@@ -81,7 +65,7 @@ module Admin
@account.update!(last_webfingered_at: nil) @account.update!(last_webfingered_at: nil)
ResolveAccountService.new.call(@account) ResolveAccountService.new.call(@account)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
def remove_avatar def remove_avatar
@@ -92,7 +76,7 @@ module Admin
log_action :remove_avatar, @account.user log_action :remove_avatar, @account.user
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
def remove_header def remove_header
@@ -103,7 +87,7 @@ module Admin
log_action :remove_header, @account.user log_action :remove_header, @account.user
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct) redirect_to admin_account_path(@account.id)
end end
private private

View File

@@ -71,7 +71,7 @@ class Admin::AnnouncementsController < Admin::BaseController
private private
def set_announcements def set_announcements
@announcements = AnnouncementFilter.new(filter_params).results.reverse_chronological.page(params[:page]) @announcements = AnnouncementFilter.new(filter_params).results.page(params[:page])
end end
def set_announcement def set_announcement

View File

@@ -33,8 +33,6 @@ module Admin
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected') flash[:alert] = I18n.t('admin.accounts.no_account_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
ensure ensure
redirect_to admin_custom_emojis_path(filter_params) redirect_to admin_custom_emojis_path(filter_params)
end end

View File

@@ -4,7 +4,6 @@ require 'sidekiq/api'
module Admin module Admin
class DashboardController < BaseController class DashboardController < BaseController
def index def index
@system_checks = Admin::SystemCheck.perform
@users_count = User.count @users_count = User.count
@pending_users_count = User.pending.count @pending_users_count = User.pending.count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0 @registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@@ -35,6 +34,7 @@ module Admin
@whitelist_enabled = whitelist_mode? @whitelist_enabled = whitelist_mode?
@profile_directory = Setting.profile_directory @profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview @timeline_preview = Setting.timeline_preview
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends @trends_enabled = Setting.trends
end end

View File

@@ -22,14 +22,13 @@ module Admin
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save @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 # rubocop:disable Rails/OutputSafety 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 # rubocop:disable Rails/OutputSafety
@domain_block.errors.delete(:domain) @domain_block.errors[:domain].clear
render :new render :new
else else
if existing_domain_block.present? if existing_domain_block.present?
@domain_block = existing_domain_block @domain_block = existing_domain_block
@domain_block.update(resource_params) @domain_block.update(resource_params)
end end
if @domain_block.save if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id) DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block log_action :create, @domain_block
@@ -41,7 +40,7 @@ module Admin
end end
def update def update
authorize :domain_block, :update? authorize :domain_block, :create?
@domain_block.update(update_params) @domain_block.update(update_params)
@@ -49,7 +48,7 @@ module Admin
if @domain_block.save if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id, severity_changed) DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
log_action :update, @domain_block log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else else
render :edit render :edit
@@ -74,11 +73,11 @@ module Admin
end end
def update_params def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment)
end end
def resource_params def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment)
end end
end end
end end

View File

@@ -27,7 +27,7 @@ module Admin
ips = [] ips = []
Resolv::DNS.open do |dns| Resolv::DNS.open do |dns|
dns.timeouts = 5 dns.timeouts = 1
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }

View File

@@ -1,53 +0,0 @@
# frozen_string_literal: true
module Admin
class FollowRecommendationsController < BaseController
before_action :set_language
def show
authorize :follow_recommendation, :show?
@form = Form::AccountBatch.new
@accounts = filtered_follow_recommendations
end
def update
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
# Do nothing
ensure
redirect_to admin_follow_recommendations_path(filter_params)
end
private
def set_language
@language = follow_recommendation_filter.language
end
def filtered_follow_recommendations
follow_recommendation_filter.results
end
def follow_recommendation_filter
@follow_recommendation_filter ||= FollowRecommendationFilter.new(filter_params)
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def filter_params
params.slice(*FollowRecommendationFilter::KEYS).permit(*FollowRecommendationFilter::KEYS)
end
def action_from_button
if params[:suppress]
'suppress_follow_recommendation'
elsif params[:unsuppress]
'unsuppress_follow_recommendation'
end
end
end
end

View File

@@ -2,73 +2,65 @@
module Admin module Admin
class InstancesController < BaseController class InstancesController < BaseController
before_action :set_instances, only: :index before_action :set_domain_block, only: :show
before_action :set_instance, except: :index before_action :set_domain_allow, only: :show
before_action :set_exhausted_deliveries_days, only: :show before_action :set_instance, only: :show
def index def index
authorize :instance, :index? authorize :instance, :index?
@instances = ordered_instances
end end
def show def show
authorize :instance, :show? authorize :instance, :show?
end
def clear_delivery_errors @following_count = Follow.where(account: Account.where(domain: params[:id])).count
authorize :delivery, :clear_delivery_errors? @followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@instance.delivery_failure_tracker.clear_failures! @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
redirect_to admin_instance_path(@instance.domain) @available = DeliveryFailureTracker.available?(params[:id])
end @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@private_comment = @domain_block&.private_comment
def restart_delivery @public_comment = @domain_block&.public_comment
authorize :delivery, :restart_delivery?
last_unavailable_domain = unavailable_domain
if last_unavailable_domain.present?
@instance.delivery_failure_tracker.track_success!
log_action :destroy, last_unavailable_domain
end
redirect_to admin_instance_path(@instance.domain)
end
def stop_delivery
authorize :delivery, :stop_delivery?
UnavailableDomain.create(domain: @instance.domain)
log_action :create, unavailable_domain
redirect_to admin_instance_path(@instance.domain)
end end
private private
def set_domain_block
@domain_block = DomainBlock.rule_for(params[:id])
end
def set_domain_allow
@domain_allow = DomainAllow.rule_for(params[:id])
end
def set_instance def set_instance
@instance = Instance.find(params[:id]) resource = Account.by_domain_accounts.find_by(domain: params[:id])
end resource ||= @domain_block
resource ||= @domain_allow
def set_exhausted_deliveries_days if resource
@exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days @instance = Instance.new(resource)
else
not_found
end end
def set_instances
@instances = filtered_instances.page(params[:page])
warning_domains_map = DeliveryFailureTracker.warning_domains_map
@instances.each do |instance|
instance.failure_days = warning_domains_map[instance.domain]
end
end
def unavailable_domain
UnavailableDomain.find_by(domain: @instance.domain)
end end
def filtered_instances def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end end
def paginated_instances
filtered_instances.page(params[:page])
end
helper_method :paginated_instances
def ordered_instances
paginated_instances.map { |resource| Instance.new(resource) }
end
def filter_params def filter_params
params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS) params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS)
end end

View File

@@ -1,56 +0,0 @@
# frozen_string_literal: true
module Admin
class IpBlocksController < BaseController
def index
authorize :ip_block, :index?
@ip_blocks = IpBlock.page(params[:page])
@form = Form::IpBlockBatch.new
end
def new
authorize :ip_block, :create?
@ip_block = IpBlock.new(ip: '', severity: :no_access, expires_in: 1.year)
end
def create
authorize :ip_block, :create?
@ip_block = IpBlock.new(resource_params)
if @ip_block.save
log_action :create, @ip_block
redirect_to admin_ip_blocks_path, notice: I18n.t('admin.ip_blocks.created_msg')
else
render :new
end
end
def batch
@form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.ip_blocks.no_ip_block_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
ensure
redirect_to admin_ip_blocks_path
end
private
def resource_params
params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in)
end
def action_from_button
'delete' if params[:delete]
end
def form_ip_block_batch_params
params.require(:form_ip_block_batch).permit(ip_block_ids: [])
end
end
end

View File

@@ -1,59 +0,0 @@
# frozen_string_literal: true
module Admin
class RulesController < BaseController
before_action :set_rule, except: [:index, :create]
def index
authorize :rule, :index?
@rules = Rule.ordered
@rule = Rule.new
end
def create
authorize :rule, :create?
@rule = Rule.new(resource_params)
if @rule.save
redirect_to admin_rules_path
else
@rules = Rule.ordered
render :index
end
end
def edit
authorize @rule, :update?
end
def update
authorize @rule, :update?
if @rule.update(resource_params)
redirect_to admin_rules_path
else
render :edit
end
end
def destroy
authorize @rule, :destroy?
@rule.discard
redirect_to admin_rules_path
end
private
def set_rule
@rule = Rule.find(params[:id])
end
def resource_params
params.require(:rule).permit(:text, :priority)
end
end
end

View File

@@ -14,7 +14,8 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted]) @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media] if params[:media]
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc') account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)

View File

@@ -59,8 +59,8 @@ module Admin
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day))) .where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
.joins(:account) .joins(:account)
.group('accounts.domain') .group('accounts.domain')
.reorder(statuses_count: :desc) .reorder('statuses_count desc')
.pluck(Arel.sql('accounts.domain, count(*) AS statuses_count')) .pluck('accounts.domain, count(*) AS statuses_count')
end end
def set_counters def set_counters

View File

@@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers before_action :set_cache_headers
@@ -40,7 +40,7 @@ class Api::BaseController < ApplicationController
render json: { error: 'This action is not allowed' }, status: 403 render json: { error: 'This action is not allowed' }, status: 403
end end
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight do rescue_from Mastodon::RaceConditionError do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end end
@@ -71,7 +71,6 @@ class Api::BaseController < ApplicationController
def limit_param(default_limit) def limit_param(default_limit)
return default_limit unless params[:limit] return default_limit unless params[:limit]
[params[:limit].to_i.abs, default_limit * 2].min [params[:limit].to_i.abs, default_limit * 2].min
end end
@@ -96,14 +95,14 @@ class Api::BaseController < ApplicationController
def require_user! def require_user!
if !current_user if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422 render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed? elsif !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403 render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved? elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403 render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
else else
update_user_sign_in set_user_activity
end end
end end

View File

@@ -1,22 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
before_action :set_account
before_action :set_featured_tags
respond_to :json
def index
render json: @featured_tags, each_serializer: REST::FeaturedTagSerializer
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_featured_tags
@featured_tags = @account.suspended? ? [] : @account.featured_tags
end
end

View File

@@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end end
def hide_results? def hide_results?
@account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end end
def default_accounts def default_accounts

View File

@@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end end
def hide_results? def hide_results?
@account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end end
def default_accounts def default_accounts

View File

@@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :set_account before_action :set_account
def index def index
@proofs = @account.suspended? ? [] : @account.identity_proofs.active @proofs = @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer render json: @proofs, each_serializer: REST::IdentityProofSerializer
end end

View File

@@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController
before_action :set_account before_action :set_account
def index def index
@lists = @account.suspended? ? [] : @account.lists.where(account: current_account) @lists = @account.lists.where(account: current_account)
render json: @lists, each_serializer: REST::ListSerializer render json: @lists, each_serializer: REST::ListSerializer
end end

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::LookupController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_account
def show
render json: @account, serializer: REST::AccountSerializer
end
private
def set_account
@account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
end
end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::NotesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
before_action :set_account
def create
if params[:comment].blank?
AccountNote.find_by(account: current_account, target_account: @account)&.destroy
else
@note = AccountNote.find_or_initialize_by(account: current_account, target_account: @account)
@note.comment = params[:comment]
@note.save! if @note.changed?
end
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.id], current_user.account_id)
end
end

View File

@@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action :require_user! before_action :require_user!
def index def index
accounts = Account.without_suspended.where(id: account_ids).select('id') accounts = Account.where(id: account_ids).select('id')
# .where doesn't guarantee that our results are in the same order # .where doesn't guarantee that our results are in the same order
# we requested them, so return the "right" order to the requestor. # we requested them, so return the "right" order to the requestor.
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact @accounts = accounts.index_by(&:id).values_at(*account_ids).compact

View File

@@ -18,10 +18,14 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end end
def load_statuses def load_statuses
@account.suspended? ? [] : cached_account_statuses cached_account_statuses
end end
def cached_account_statuses def cached_account_statuses
cache_collection account_statuses, Status
end
def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(only_media_scope) if truthy_param?(:only_media)
@@ -29,12 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present? statuses.merge!(hashtag_scope) if params[:tagged].present?
cache_collection_paginated_by_id( statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
statuses,
Status,
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end end
def permitted_account_statuses def permitted_account_statuses
@@ -42,7 +41,17 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end end
def only_media_scope def only_media_scope
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) Status.where(id: account_media_status_ids)
end
def account_media_status_ids
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
# and the table will be joined by `Merge Semi Join`, so the query will be slow.
@account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
.reorder(id: :desc).distinct(:id).pluck(:id)
end end
def pinned_scope def pinned_scope

View File

@@ -9,6 +9,7 @@ class Api::V1::AccountsController < Api::BaseController
before_action :require_user!, except: [:show, :create] before_action :require_user!, except: [:show, :create]
before_action :set_account, except: [:create] before_action :set_account, except: [:create]
before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create] before_action :check_enabled_registrations, only: [:create]
skip_before_action :require_authenticated_user!, only: :create skip_before_action :require_authenticated_user!, only: :create
@@ -20,22 +21,21 @@ class Api::V1::AccountsController < Api::BaseController
end end
def create def create
token = AppSignUpService.new.call(doorkeeper_token.application, request.remote_ip, account_params) token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token) response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers) headers.merge!(response.headers)
self.response_body = Oj.dump(response.body) self.response_body = Oj.dump(response.body)
self.status = response.status self.status = response.status
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
end end
def follow 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, with_rate_limit: true) FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end end
def block def block
@@ -44,7 +44,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def mute def mute
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0)) MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications))
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
@@ -70,7 +70,11 @@ class Api::V1::AccountsController < Api::BaseController
end end
def relationships(**options) def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end
def check_account_suspension
gone if @account.suspended?
end end
def account_params def account_params

View File

@@ -22,7 +22,6 @@ class Api::V1::Admin::AccountsController < Api::BaseController
active active
pending pending
disabled disabled
sensitized
silenced silenced
suspended suspended
username username
@@ -59,20 +58,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def unsensitive
authorize @account, :unsensitive?
@account.unsensitize!
log_action :unsensitive, @account
render json: @account, serializer: REST::Admin::AccountSerializer render json: @account, serializer: REST::Admin::AccountSerializer
end end
@@ -86,7 +72,6 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def unsuspend def unsuspend
authorize @account, :unsuspend? authorize @account, :unsuspend?
@account.unsuspend! @account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account log_action :unsuspend, @account
render json: @account, serializer: REST::Admin::AccountSerializer render json: @account, serializer: REST::Admin::AccountSerializer
end end
@@ -94,7 +79,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
private private
def set_accounts def set_accounts
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def set_account def set_account

View File

@@ -63,7 +63,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController
private private
def set_reports def set_reports
@reports = filtered_reports.order(id: :desc).with_accounts.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def set_report def set_report

View File

@@ -18,8 +18,6 @@ class Api::V1::BlocksController < Api::BaseController
def paginated_blocks def paginated_blocks
@paginated_blocks ||= Block.eager_load(target_account: :account_stat) @paginated_blocks ||= Block.eager_load(target_account: :account_stat)
.joins(:target_account)
.merge(Account.without_suspended)
.where(account: current_account) .where(account: current_account)
.paginate_by_max_id( .paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),

View File

@@ -17,11 +17,14 @@ class Api::V1::BookmarksController < Api::BaseController
end end
def cached_bookmarks def cached_bookmarks
cache_collection(results.map(&:status), Status) cache_collection(
Status.reorder(nil).joins(:bookmarks).merge(results),
Status
)
end end
def results def results
@_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id( @_results ||= account_bookmarks.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )

View File

@@ -32,7 +32,7 @@ class Api::V1::ConversationsController < Api::BaseController
def paginated_conversations def paginated_conversations
AccountConversation.where(account: current_account) AccountConversation.where(account: current_account)
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) .paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def insert_pagination_headers def insert_pagination_headers

View File

@@ -1,30 +0,0 @@
# 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

View File

@@ -1,59 +0,0 @@
# 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

View File

@@ -1,25 +0,0 @@
# 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

View File

@@ -1,17 +0,0 @@
# 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

View File

@@ -1,26 +0,0 @@
# 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

View File

@@ -1,29 +0,0 @@
# 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

View File

@@ -1,24 +0,0 @@
# frozen_string_literal: true
class Api::V1::Emails::ConfirmationsController < Api::BaseController
before_action :doorkeeper_authorize!
before_action :require_user_owned_by_application!
before_action :require_user_not_confirmed!
def create
current_user.update!(email: params[:email]) if params.key?(:email)
current_user.resend_confirmation_instructions
render_empty
end
private
def require_user_owned_by_application!
render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id
end
def require_user_not_confirmed!
render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden if current_user.confirmed? || current_user.unconfirmed_email.blank?
end
end

View File

@@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController
end end
def endorsed_accounts def endorsed_accounts
current_account.endorsed_accounts.includes(:account_stat).without_suspended current_account.endorsed_accounts.includes(:account_stat)
end end
def insert_pagination_headers def insert_pagination_headers

View File

@@ -17,11 +17,14 @@ class Api::V1::FavouritesController < Api::BaseController
end end
def cached_favourites def cached_favourites
cache_collection(results.map(&:status), Status) cache_collection(
Status.reorder(nil).joins(:favourites).merge(results),
Status
)
end end
def results def results
@_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id( @_results ||= account_favourites.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )

View File

@@ -3,15 +3,15 @@
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action :require_user! before_action :require_user!
before_action :set_recently_used_tags, only: :index before_action :set_most_used_tags, only: :index
def index def index
render json: @recently_used_tags, each_serializer: REST::TagSerializer render json: @most_used_tags, each_serializer: REST::TagSerializer
end end
private private
def set_recently_used_tags def set_most_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10) @most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
end end
end end

View File

@@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize def authorize
AuthorizeFollowService.new.call(account, current_account) AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account)) NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
@@ -29,7 +29,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
end end
def relationships(**options) def relationships(**options)
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options) AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options)
end end
def load_accounts def load_accounts
@@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
end end
def default_accounts def default_accounts
Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) Account.includes(:follow_requests, :account_stat).references(:follow_requests)
end end
def paginated_follow_requests def paginated_follow_requests

View File

@@ -8,7 +8,7 @@ class Api::V1::Instances::PeersController < Api::BaseController
def index def index
expires_in 1.day, public: true expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } render_with_cache(expires_in: 1.day) { Account.remote.domains }
end end
private private

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Instances::RulesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
before_action :set_rules
def index
render json: @rules, each_serializer: REST::RuleSerializer
end
private
def set_rules
@rules = Rule.ordered
end
end

View File

@@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
def load_accounts def load_accounts
if unlimited? if unlimited?
@list.accounts.without_suspended.includes(:account_stat).all @list.accounts.includes(:account_stat).all
else else
@list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) @list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end end
end end

View File

@@ -38,6 +38,6 @@ class Api::V1::ListsController < Api::BaseController
end end
def list_params def list_params
params.permit(:title, :replies_policy) params.permit(:title)
end end
end end

View File

@@ -7,7 +7,7 @@ class Api::V1::MarkersController < Api::BaseController
before_action :require_user! before_action :require_user!
def index def index
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline) @markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker }
render json: serialize_map(@markers) render json: serialize_map(@markers)
end end

View File

@@ -39,7 +39,7 @@ class Api::V1::MediaController < Api::BaseController
end end
def media_attachment_params def media_attachment_params
params.permit(:file, :thumbnail, :description, :focus) params.permit(:file, :description, :focus)
end end
def file_type_error def file_type_error

View File

@@ -7,7 +7,7 @@ class Api::V1::MutesController < Api::BaseController
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::MutedAccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end
private private
@@ -18,8 +18,6 @@ class Api::V1::MutesController < Api::BaseController
def paginated_mutes def paginated_mutes
@paginated_mutes ||= Mute.eager_load(:target_account) @paginated_mutes ||= Mute.eager_load(:target_account)
.joins(:target_account)
.merge(Account.without_suspended)
.where(account: current_account) .where(account: current_account)
.paginate_by_max_id( .paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),

View File

@@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController
end end
def show def show
@notification = current_account.notifications.without_suspended.find(params[:id]) @notification = current_account.notifications.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer render json: @notification, serializer: REST::NotificationSerializer
end end
@@ -31,17 +31,18 @@ class Api::V1::NotificationsController < Api::BaseController
private private
def load_notifications def load_notifications
notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id( cache_collection paginated_notifications, Notification
end
def paginated_notifications
browserable_account_notifications.paginate_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT), limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
cache_collection(target_statuses, Status)
end
end end
def browserable_account_notifications def browserable_account_notifications
current_account.notifications.without_suspended.browserable(exclude_types, from_account) current_account.notifications.browserable(exclude_types, from_account)
end end
def target_statuses_from_notifications def target_statuses_from_notifications

View File

@@ -3,13 +3,13 @@
class Api::V1::Push::SubscriptionsController < Api::BaseController class Api::V1::Push::SubscriptionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :push } before_action -> { doorkeeper_authorize! :push }
before_action :require_user! before_action :require_user!
before_action :set_push_subscription before_action :set_web_push_subscription
before_action :check_push_subscription, only: [:show, :update] before_action :check_web_push_subscription, only: [:show, :update]
def create def create
@push_subscription&.destroy! @web_subscription&.destroy!
@push_subscription = Web::PushSubscription.create!( @web_subscription = ::Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint], endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh], key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth], key_auth: subscription_params[:keys][:auth],
@@ -18,31 +18,31 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
access_token_id: doorkeeper_token.id access_token_id: doorkeeper_token.id
) )
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end end
def show def show
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end end
def update def update
@push_subscription.update!(data: data_params) @web_subscription.update!(data: data_params)
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end end
def destroy def destroy
@push_subscription&.destroy! @web_subscription&.destroy!
render_empty render_empty
end end
private private
def set_push_subscription def set_web_push_subscription
@push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
end end
def check_push_subscription def check_web_push_subscription
not_found if @push_subscription.nil? not_found if @web_subscription.nil?
end end
def subscription_params def subscription_params
@@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params def data_params
return {} if params[:data].blank? return {} if params[:data].blank?
params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@@ -32,7 +32,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
private private
def set_statuses def set_statuses
@statuses = current_account.scheduled_statuses.to_a_paginated_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) @statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def set_status def set_status

View File

@@ -5,7 +5,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' } before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
before_action :require_user! before_action :require_user!
before_action :set_status, only: [:create] before_action :set_status
def create def create
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status) current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
@@ -13,20 +13,10 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
end end
def destroy def destroy
bookmark = current_account.bookmarks.find_by(status_id: params[:status_id]) bookmark = current_account.bookmarks.find_by(status: @status)
if bookmark
@status = bookmark.status
else
@status = Status.find(params[:status_id])
authorize @status, :show?
end
bookmark&.destroy! bookmark&.destroy!
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false }) render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
rescue Mastodon::NotPermittedError
not_found
end end
private private

View File

@@ -22,7 +22,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
def default_accounts def default_accounts
Account Account
.without_suspended
.includes(:favourites, :account_stat) .includes(:favourites, :account_stat)
.references(:favourites) .references(:favourites)
.where(favourites: { status_id: @status.id }) .where(favourites: { status_id: @status.id })

View File

@@ -5,7 +5,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
before_action :require_user! before_action :require_user!
before_action :set_status, only: [:create] before_action :set_status
def create def create
FavouriteService.new.call(current_account, @status) FavouriteService.new.call(current_account, @status)
@@ -13,19 +13,8 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
end end
def destroy def destroy
fav = current_account.favourites.find_by(status_id: params[:status_id])
if fav
@status = fav.status
UnfavouriteWorker.perform_async(current_account.id, @status.id) UnfavouriteWorker.perform_async(current_account.id, @status.id)
else
@status = Status.find(params[:status_id])
authorize @status, :show?
end
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }) render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false })
rescue Mastodon::NotPermittedError
not_found
end end
private private

View File

@@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
end end
def default_accounts def default_accounts
Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) Account.includes(:statuses, :account_stat).references(:statuses)
end end
def paginated_statuses def paginated_statuses

View File

@@ -5,7 +5,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user! before_action :require_user!
before_action :set_reblog, only: [:create] before_action :set_reblog
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
@@ -16,21 +16,15 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
end end
def destroy def destroy
@status = current_account.statuses.find_by(reblog_of_id: params[:status_id]) @status = current_account.statuses.find_by(reblog_of_id: @reblog.id)
if @status if @status
authorize @status, :unreblog? authorize @status, :unreblog?
@status.discard @status.discard
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
@reblog = @status.reblog
else
@reblog = Status.find(params[:status_id])
authorize @reblog, :show?
end end
render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }) render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false })
rescue Mastodon::NotPermittedError
not_found
end end
private private

View File

@@ -57,7 +57,6 @@ class Api::V1::StatusesController < Api::BaseController
@status.discard @status.discard
RemovalWorker.perform_async(@status.id, redraft: true) RemovalWorker.perform_async(@status.id, redraft: true)
@status.account.statuses_count = @status.account.statuses_count - 1
render json: @status, serializer: REST::StatusSerializer, source_requested: true render json: @status, serializer: REST::StatusSerializer, source_requested: true
end end

View File

@@ -5,20 +5,20 @@ class Api::V1::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read } before_action -> { doorkeeper_authorize! :read }
before_action :require_user! before_action :require_user!
before_action :set_accounts
def index def index
suggestions = suggestions_source.get(current_account, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT)) render json: @accounts, each_serializer: REST::AccountSerializer
render json: suggestions.map(&:account), each_serializer: REST::AccountSerializer
end end
def destroy def destroy
suggestions_source.remove(current_account, params[:id]) PotentialFriendshipTracker.remove(current_account.id, params[:id])
render_empty render_empty
end end
private private
def suggestions_source def set_accounts
AccountSuggestions::PastInteractionsSource.new @accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT))
end end
end end

View File

@@ -16,29 +16,30 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def load_statuses def load_statuses
cached_public_statuses_page cached_public_statuses
end end
def cached_public_statuses_page def cached_public_statuses
cache_collection(public_statuses, Status) cache_collection public_statuses, Status
end end
def public_statuses def public_statuses
public_feed.get( statuses = public_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params_slice(:max_id, :since_id, :min_id)
params[:since_id],
params[:min_id]
) )
if truthy_param?(:only_media)
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
statuses.where(id: status_ids)
else
statuses
end
end end
def public_feed def public_timeline_statuses
PublicFeed.new( Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
current_account,
local: truthy_param?(:local),
remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media)
)
end end
def insert_pagination_headers def insert_pagination_headers

View File

@@ -20,29 +20,30 @@ class Api::V1::Timelines::TagController < Api::BaseController
end end
def cached_tagged_statuses def cached_tagged_statuses
@tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status) cache_collection tagged_statuses, Status
end
def tagged_statuses
if @tag.nil?
[]
else
statuses = tag_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
if truthy_param?(:only_media)
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
statuses.where(id: status_ids)
else
statuses
end
end
end end
def tag_timeline_statuses def tag_timeline_statuses
tag_feed.get( HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id],
params[:min_id]
)
end
def tag_feed
TagFeed.new(
@tag,
current_account,
any: params[:any],
all: params[:all],
none: params[:none],
local: truthy_param?(:local),
remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media)
)
end end
def insert_pagination_headers def insert_pagination_headers

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