Compare commits

..

20 Commits

Author SHA1 Message Date
Eugen Rochko
a583e54023 Bump version to 3.2.1 2020-10-19 16:07:06 +02:00
ThibG
4ea7193f0a Add support for latest HTTP Signatures spec draft (#14556)
* Add support for latest HTTP Signatures spec draft

https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-00.html

- add support for the “hs2019” signature algorithm (assumed to be equivalent
  to RSA-SHA256, since we do not have a mechanism to specify the algorithm
  within the key metadata yet)
- add support for (created) and (expires) pseudo-headers and related
  signature parameters, when using the hs2019 signature algorithm
- adjust default “headers” parameter while being backwards-compatible with
  previous implementation
- change the acceptable time window logic from 12 hours surrounding the “date”
  header to accepting signatures created up to 1 hour in the future and
  expiring up to 1 hour in the past (but only allowing expiration dates up to
  12 hours after the creation date)
  This doesn't conform with the current draft, as it doesn't permit accounting
  for clock skew.
  This, however, should be addressed in a next version of the draft:
  https://github.com/httpwg/http-extensions/pull/1235

* Add additional signature requirements

* Rewrite signature params parsing using Parslet

* Make apparent which signature algorithm Mastodon on verification failure

Mastodon uses RSASSA-PKCS1-v1_5, which is not recommended for new applications,
and new implementers may thus unknowingly use RSASSA-PSS.

* Add workaround for PeerTube's invalid signature header

The previous parser allowed incorrect Signature headers, such as
those produced by old versions of the `http-signature` node.js package,
and seemingly used by PeerTube.

This commit adds a workaround for that.

* Fix `signature_key_id` raising an exception

Previously, parsing failures would result in `signature_key_id` being nil,
but the parser changes made that result in an exception.

This commit changes the `signature_key_id` method to return `nil` in case
of parsing failures.

* Move extra HTTP signature helper methods to private methods

* Relax (request-target) requirement to (request-target) || digest

This lets requests from Plume work without lowering security significantly.
2020-10-19 15:41:25 +02:00
ThibG
aa98655cf6 Fix dereferencing remote statuses not using the correct account (#14656)
Follow-up to #14359

In the case of limited toots, the receiver may not be explicitly part of the
audience. If a specific user's inbox URI was specified, it makes sense to
dereference the toot from the corresponding user, instead of trying to find
someone in the explicit audience.
2020-10-19 15:41:11 +02:00
Tdxdxoz
dd3a86eb04 Fix: also use custom private boost icon for detailed status (#14471)
* use custom private boost icon for detail status

* only use className
2020-10-19 15:40:51 +02:00
ThibG
aea0161e83 Add support for inlined objects in activity audience (#14514)
* Add support for inlined objects in activity audience

* Add tests
2020-10-19 15:40:42 +02:00
ThibG
8b448aecef Fix tootctl media commands not handling snowflake ids for media_attachments (#14536) 2020-10-19 15:40:34 +02:00
ThibG
6db143e424 Fix crash when failing to load emoji picker (#14525)
Fixes #14523
2020-10-19 15:40:22 +02:00
ThibG
3b699f1732 Fix thumbnail color extraction (#14464)
* Fix contrast calculation for thumbnail color extraction

Luminance calculation was using 0-255 RGB values instead of 0-1 sRGB values,
leading to incorrectly-computed contrast values.

Since we use ColorDiff already, just use its XYZ colorspace conversion code
to get the value.

* Require at least 3:1 contrast for both accent and foreground colors

* Lower required contrast for the accent color
2020-10-19 15:40:14 +02:00
ThibG
1995a5cb34 Fix audio/video player not using CDN_HOST in media paths on public pages (#14486) 2020-10-19 15:40:03 +02:00
ThibG
469c4c78a3 Fix audio player on Safari (#14485) 2020-10-19 15:39:48 +02:00
ThibG
399c5f0900 Change content-type to be always computed from file data (#14452)
* Change content-type to be always computed from file data

Restore previous behavior, detecting the content-type isn't very
expensive, and some instances may serve files as application/octet-stream
regardless of their true type, making fetching media from them fail, while
it used to work pre-3.2.0.

* Add test
2020-10-19 15:39:28 +02:00
ThibG
856cb96a2b Fix new audio player features not working on Safari (#14465)
Fixes #14462
2020-10-19 15:39:18 +02:00
Takeshi Umeda
58c59af573 Fix an error when file_file_size is nil in tootctl media remove (#14657) 2020-10-19 14:49:31 +02:00
Eugen Rochko
3f4cceebd6 Fix videos with near-60 fps being rejected (#14684)
Fix #14668
2020-10-19 14:49:14 +02:00
Eugen Rochko
ce6aaed432 Remove dependency on goldfinger gem (#14919)
There are edge cases where requests to certain hosts timeout when
using the vanilla HTTP.rb gem, which the goldfinger gem uses. Now
that we no longer need to support OStatus servers, webfinger logic
is so simple that there is no point encapsulating it in a gem, so
we can just use our own Request class. With that, we benefit from
more robust timeout code and IPv4/IPv6 resolution.

Fix #14091
2020-10-19 14:48:54 +02:00
Eugen Rochko
8f79ed0487 Fix reported statuses not being included in warning e-mail (#14778) 2020-10-19 14:46:35 +02:00
ThibG
4acfc3ce83 Fix handling of Reject Follow when a matching follow relationship exists (#14479)
* Add tests

* Fix handling of Reject Follow when a matching follow relationship exists

Regression from #12199
2020-10-19 14:46:10 +02:00
Takeshi Umeda
c98b7751ca Fix limited follower id in fan-out-on-write service (#14709) 2020-10-19 14:45:44 +02:00
Eugen Rochko
0abfa06b2f Fix inefficiencies in fan-out-on-write service (#14682) 2020-10-19 14:45:30 +02:00
ThibG
aecdaf5a8c Do not serve account actors at all in limited federation mode (#14800)
* Do not serve account actors at all in limited federation mode

When an account is fetched without a signature from an allowed instance,
return an error.

This isn't really an improvement in security, as the only information that was
previously returned was required protocol-level info, and the only personal bit
was the existence of the account. The existence of the account can still be
checked by issuing a webfinger query, as those are accepted without signatures.

However, this change makes it so that unallowed instances won't create account
records on their end when they find a reference to an unknown account.

The previous behavior of rendering a limited list of fields, instead of not
rendering the actor at all, was in order to prevent situations in which two
instances in Authorized Fetch mode or Limited Federation mode would fail to
reach each other because resolving an account would require a signed query…
from an account which can only be fetched with a signed query itself. However,
this should now be fine as fetching accounts is done by signing on behalf of
the special instance actor, which does not require any kind of valid signature
to be fetched.

* Fix tests
2020-10-19 14:45:12 +02:00
2445 changed files with 43329 additions and 117673 deletions

View File

@@ -1,209 +1,249 @@
version: 2.1 version: 2
orbs: aliases:
ruby: circleci/ruby@1.4.1 - &defaults
node: circleci/node@5.0.1
executors:
default:
parameters:
ruby-version:
type: string
docker: docker:
- image: cimg/ruby:<< parameters.ruby-version >> - image: circleci/ruby:2.7-buster-node
environment: environment: &ruby_environment
BUNDLE_JOBS: 3 BUNDLE_JOBS: 3
BUNDLE_RETRY: 3 BUNDLE_RETRY: 3
CONTINUOUS_INTEGRATION: true BUNDLE_APP_CONFIG: ./.bundle/
BUNDLE_PATH: ./vendor/bundle/
DB_HOST: localhost DB_HOST: localhost
DB_USER: root DB_USER: root
DISABLE_SIMPLECOV: true
RAILS_ENV: test RAILS_ENV: test
- image: cimg/postgres:14.0 ALLOW_NOPAM: true
CONTINUOUS_INTEGRATION: true
DISABLE_SIMPLECOV: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
working_directory: ~/projects/mastodon/
- &attach_workspace
attach_workspace:
at: ~/projects/
- &persist_to_workspace
persist_to_workspace:
root: ~/projects/
paths:
- ./mastodon/
- &restore_ruby_dependencies
restore_cache:
keys:
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
- v3-ruby-dependencies-
- &install_steps
steps:
- checkout
- *attach_workspace
- restore_cache:
keys:
- v2-node-dependencies-{{ checksum "yarn.lock" }}
- v2-node-dependencies-
- run:
name: Install yarn dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v2-node-dependencies-{{ checksum "yarn.lock" }}
paths:
- ./node_modules/
- *persist_to_workspace
- &install_system_dependencies
run:
name: Install system dependencies
command: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
- &install_ruby_dependencies
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Set Ruby version
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- run:
name: Set bundler settings
command: |
bundle config --local clean 'true'
bundle config --local deployment 'true'
bundle config --local with 'pam_authentication'
bundle config --local without 'development production'
bundle config --local frozen 'true'
bundle config --local path $BUNDLE_PATH
- run:
name: Install bundler dependencies
command: bundle check || (bundle install && bundle clean)
- save_cache:
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
paths:
- ./.bundle/
- ./vendor/bundle/
- persist_to_workspace:
root: ~/projects/
paths:
- ./mastodon/.bundle/
- ./mastodon/vendor/bundle/
- &test_steps
parallelism: 4
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Install FFMPEG
command: sudo apt-get install -y ffmpeg
- run:
name: Load database schema
command: ./bin/rails db:create db:schema:load db:seed
- run:
name: Run rspec in parallel
command: |
bundle exec rspec --profile 10 \
--format RspecJunitFormatter \
--out test_results/rspec.xml \
--format progress \
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
- store_test_results:
path: test_results
jobs:
install:
<<: *defaults
<<: *install_steps
install-ruby2.7:
<<: *defaults
<<: *install_ruby_dependencies
install-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies
build:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Precompile assets
command: ./bin/rails assets:precompile
- persist_to_workspace:
root: ~/projects/
paths:
- ./mastodon/public/assets
- ./mastodon/public/packs-test/
test-migrations:
<<: *defaults
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
- image: cimg/redis:6.2 - image: circleci/redis:5-alpine
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Create database
command: ./bin/rails db:create
- run:
name: Run migrations
command: ./bin/rails db:migrate
commands: test-ruby2.7:
install-system-dependencies: <<: *defaults
steps:
- run:
name: Install system dependencies
command: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev
install-ruby-dependencies:
parameters:
ruby-version:
type: string
steps:
- run:
command: |
bundle config clean 'true'
bundle config frozen 'true'
bundle config without 'development production'
name: Set bundler settings
- ruby/install-deps:
bundler-version: '2.3.8'
key: ruby<< parameters.ruby-version >>-gems-v1
wait-db:
steps:
- run:
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
name: Wait for PostgreSQL and Redis
jobs:
build:
docker: docker:
- image: cimg/ruby:3.0-node - image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment: environment:
RAILS_ENV: test POSTGRES_USER: root
steps: POSTGRES_HOST_AUTH_METHOD: trust
- checkout - image: circleci/redis:5-alpine
- install-system-dependencies <<: *test_steps
- install-ruby-dependencies:
ruby-version: '3.0'
- node/install-packages:
cache-version: v1
pkg-manager: yarn
- run:
command: ./bin/rails assets:precompile
name: Precompile assets
- persist_to_workspace:
paths:
- public/assets
- public/packs-test
root: .
test: test-ruby2.6:
parameters: <<: *defaults
ruby-version: docker:
type: string - image: circleci/ruby:2.6-buster-node
executor: environment: *ruby_environment
name: default - image: circleci/postgres:12.2
ruby-version: << parameters.ruby-version >> environment:
environment: POSTGRES_USER: root
ALLOW_NOPAM: true POSTGRES_HOST_AUTH_METHOD: trust
PAM_ENABLED: true - image: circleci/redis:5-alpine
PAM_DEFAULT_SERVICE: pam_test <<: *test_steps
PAM_CONTROLLED_SERVICE: pam_test_controlled
parallelism: 4
steps:
- checkout
- install-system-dependencies
- run:
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
name: Install additional system dependencies
- run:
command: bundle config with 'pam_authentication'
name: Enable PAM authentication
- install-ruby-dependencies:
ruby-version: << parameters.ruby-version >>
- attach_workspace:
at: .
- wait-db
- run:
command: ./bin/rails db:create db:schema:load db:seed
name: Load database schema
- ruby/rspec-test
test-migrations: test-webui:
executor: <<: *defaults
name: default docker:
ruby-version: '3.0' - image: circleci/node:12-buster
steps: steps:
- checkout - *attach_workspace
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run: - run:
command: ./bin/rails db:create name: Run jest
name: Create database command: yarn test:jest
- 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 VERSION=20180514140000
name: Run migrations up to v2.4.0
- run:
command: ./bin/rails tests:migrations:populate_v2_4
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining migrations
- run:
command: ./bin/rails tests:migrations:check_database
name: Check migration result
test-two-step-migrations: check-i18n:
executor: <<: *defaults
name: default
ruby-version: '3.0'
steps: steps:
- checkout - *attach_workspace
- install-system-dependencies - *install_system_dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run: - run:
command: ./bin/rails db:create name: Check locale file normalization
name: Create database command: bundle exec i18n-tasks check-normalized
- run: - run:
command: ./bin/rails db:migrate VERSION=20171010025614 name: Check for unused strings
name: Run migrations up to v2.0.0 command: bundle exec i18n-tasks unused -l en
- run: - run:
command: ./bin/rails tests:migrations:populate_v2 name: Check for wrong string interpolations
name: Populate database with test data command: bundle exec i18n-tasks check-consistent-interpolations
- run: - run:
command: ./bin/rails db:migrate VERSION=20180514140000 name: Check that all required locale files exist
name: Run pre-deployment migrations up to v2.4.0 command: bundle exec rake repo:check_locales_files
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails tests:migrations:populate_v2_4
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all pre-deployment migrations
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
- run:
command: ./bin/rails tests:migrations:check_database
name: Check migration result
workflows: workflows:
version: 2 version: 2
build-and-test: build-and-test:
jobs: jobs:
- build - install
- test: - install-ruby2.7:
matrix:
parameters:
ruby-version:
- '2.7'
- '3.0'
name: test-ruby<< matrix.ruby-version >>
requires: requires:
- build - install
- install-ruby2.6:
requires:
- install
- install-ruby2.7
- build:
requires:
- install-ruby2.7
- test-migrations: - test-migrations:
requires: requires:
- build - install-ruby2.7
- test-two-step-migrations: - test-ruby2.7:
requires: requires:
- install-ruby2.7
- build - build
- node/run: - test-ruby2.6:
cache-version: v1
name: test-webui
pkg-manager: yarn
requires: requires:
- install-ruby2.6
- build - build
version: lts - test-webui:
yarn-run: test:jest requires:
- install
- check-i18n:
requires:
- install-ruby2.7

View File

@@ -1,4 +1,4 @@
version: '2' version: "2"
checks: checks:
argument-count: argument-count:
enabled: false enabled: false
@@ -27,15 +27,12 @@ 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:
- spec/ - spec/
- vendor/asset/ - vendor/asset
- app/javascript/mastodon/locales/**/*.json
- config/locales/**/*.yml

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"
]

View File

@@ -1,24 +0,0 @@
# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster
ARG VARIANT=3.1-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
# Install Rails
# RUN gem install rails webdrivers
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
# The value is a comma-separated list of allowed domains
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev"
# [Choice] Node.js version: lts/*, 16, 14, 12, 10
ARG NODE_VERSION="lts/*"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libpam-dev
# [Optional] Uncomment this line to install additional gems.
RUN gem install foreman
# [Optional] Uncomment this line to install global node packages.
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1

View File

@@ -1,26 +0,0 @@
{
"name": "Mastodon",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/mastodon",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"rebornix.Ruby"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
"forwardPorts": [3000, 4000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bundle install --path vendor/bundle && yarn install && ./bin/rails db:setup",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}

View File

@@ -1,83 +0,0 @@
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
# Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6
# Append -bullseye or -buster to pin to an OS version.
# Use -bullseye variants on local arm64/Apple Silicon.
VARIANT: '3.0-bullseye'
# Optional Node.js version to install
NODE_VERSION: '14'
volumes:
- ..:/workspaces/mastodon:cached
environment:
RAILS_ENV: development
NODE_ENV: development
REDIS_HOST: redis
REDIS_PORT: '6379'
DB_HOST: db
DB_USER: postgres
DB_PASS: postgres
DB_PORT: '5432'
ES_ENABLED: 'true'
ES_HOST: es
ES_PORT: '9200'
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
networks:
- external_network
- internal_network
user: vscode
db:
image: postgres:14-alpine
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
networks:
- internal_network
redis:
image: redis:6-alpine
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- internal_network
es:
image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
restart: unless-stopped
environment:
ES_JAVA_OPTS: -Xms512m -Xmx512m
cluster.name: es-mastodon
discovery.type: single-node
bootstrap.memory_lock: 'true'
volumes:
- es-data:/usr/share/elasticsearch/data
networks:
- internal_network
ulimits:
memlock:
soft: -1
hard: -1
volumes:
postgres-data:
redis-data:
es-data:
networks:
external_network:
internal_network:
internal: true

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
@@ -15,7 +11,5 @@ vendor/bundle
*.swp *.swp
*~ *~
postgres postgres
postgres14
redis redis
elasticsearch elasticsearch
chart

View File

@@ -13,7 +13,7 @@ DB_PORT=5432
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano # DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# Optional Elasticsearch configuration # Optional ElasticSearch configuration
ES_ENABLED=true ES_ENABLED=true
ES_HOST=$DATA_ELASTIC_HOST ES_HOST=$DATA_ELASTIC_HOST
ES_PORT=9200 ES_PORT=9200
@@ -202,6 +202,10 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default) # 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 # 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) : # Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true # CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/ # CAS_URL=https://sso.myserver.com/
@@ -224,7 +228,6 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# CAS_LOCATION_KEY='location' # CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image' # CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone' # CAS_PHONE_KEY='phone'
# CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# Optional SAML authentication (cf. omniauth-saml) # Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true # SAML_ENABLED=true

View File

@@ -4,12 +4,6 @@
# not demonstrate all available configuration options. Please look at # not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation. # 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 # Federation
# ---------- # ----------
# This identifies your server and cannot be changed safely later # This identifies your server and cannot be changed safely later
@@ -29,14 +23,11 @@ DB_NAME=mastodon_production
DB_PASS= DB_PASS=
DB_PORT=5432 DB_PORT=5432
# Elasticsearch (optional) # ElasticSearch (optional)
# ------------------------ # ------------------------
ES_ENABLED=true ES_ENABLED=true
ES_HOST=localhost ES_HOST=localhost
ES_PORT=9200 ES_PORT=9200
# Authentication for ES (optional)
ES_USER=elastic
ES_PASS=password
# Secrets # Secrets
# ------- # -------
@@ -58,7 +49,7 @@ SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com SMTP_FROM_ADDRESS=notificatons@example.com
# File storage (optional) # File storage (optional)
# ----------------------- # -----------------------

View File

@@ -79,11 +79,6 @@ module.exports = {
'no-irregular-whitespace': 'error', 'no-irregular-whitespace': 'error',
'no-mixed-spaces-and-tabs': 'warn', 'no-mixed-spaces-and-tabs': 'warn',
'no-nested-ternary': 'warn', 'no-nested-ternary': 'warn',
'no-restricted-properties': [
'error',
{ property: 'substring', message: 'Use .slice instead of .substring.' },
{ property: 'substr', message: 'Use .slice instead of .substr.' },
],
'no-trailing-spaces': 'warn', 'no-trailing-spaces': 'warn',
'no-undef': 'error', 'no-undef': 'error',
'no-unreachable': 'error', 'no-unreachable': 'error',

32
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,32 @@
# CODEOWNERS for tootsuite/mastodon
# 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.
# /app/javascript/mastodon/locales/fr.json @żelipapą
# /app/views/user_mailer/*.fr.html.erb @żelipapą
# /app/views/user_mailer/*.fr.text.erb @żelipapą
# /config/locales/*.fr.yml @żelipapą
# /config/locales/fr.yml @żelipapą
# Polish
/app/javascript/mastodon/locales/pl.json @m4sk1n
/app/views/user_mailer/*.pl.html.erb @m4sk1n
/app/views/user_mailer/*.pl.text.erb @m4sk1n
/config/locales/*.pl.yml @m4sk1n
/config/locales/pl.yml @m4sk1n
# French
/app/javascript/mastodon/locales/fr.json @aldarone
/app/javascript/mastodon/locales/whitelist_fr.json @aldarone
/app/views/user_mailer/*.fr.html.erb @aldarone
/app/views/user_mailer/*.fr.text.erb @aldarone
/config/locales/*.fr.yml @aldarone
/config/locales/fr.yml @aldarone
# Dutch
/app/javascript/mastodon/locales/nl.json @jeroenpraat
/app/javascript/mastodon/locales/whitelist_nl.json @jeroenpraat
/app/views/user_mailer/*.nl.html.erb @jeroenpraat
/app/views/user_mailer/*.nl.text.erb @jeroenpraat
/config/locales/*.nl.yml @jeroenpraat
/config/locales/nl.yml @jeroenpraat

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
patreon: mastodon patreon: mastodon
open_collective: mastodon open_collective: mastodon
custom: https://sponsor.joinmastodon.org github: [Gargron]

View File

@@ -1,42 +0,0 @@
name: Bug Report
description: If something isn't working as expected
labels: bug
body:
- type: markdown
attributes:
value: |
Make sure that you are submitting a new bug that was not previously reported or already fixed.
Please use a concise and distinct title for the issue.
- type: textarea
attributes:
label: Steps to reproduce the problem
description: What were you trying to do?
value: |
1.
2.
3.
...
validations:
required: true
- type: input
attributes:
label: Expected behaviour
description: What should have happened?
validations:
required: true
- type: input
attributes:
label: Actual behaviour
description: What happened?
validations:
required: true
- type: textarea
attributes:
label: Specifications
description: |
What version or commit hash of Mastodon did you find this bug in?
If a front-end issue, what browser and operating systems were you using?
validations:
required: true

View File

@@ -1,22 +0,0 @@
name: Feature Request
description: I have a suggestion
labels: suggestion
body:
- type: markdown
attributes:
value: |
Please use a concise and distinct title for the issue.
Consider: Could it be implemented as a 3rd party app using the REST API instead?
- type: textarea
attributes:
label: Pitch
description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Why do you think this feature is needed? Who would benefit from it?
validations:
required: true

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Bug Report
about: If something isn't working as expected
---
<!-- Make sure that you are submitting a new bug that was not previously reported or already fixed -->
<!-- Please use a concise and distinct title for the issue -->
### Expected behaviour
<!-- What should have happened? -->
### Actual behaviour
<!-- What happened? -->
### Steps to reproduce the problem
<!-- What were you trying to do? -->
### Specifications
<!-- What version or commit hash of Mastodon did you find this bug in? -->
<!-- If a front-end issue, what browser and operating systems were you using? -->

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: GitHub Discussions - name: Mastodon Meta Discussion Board
url: https://github.com/mastodon/mastodon/discussions url: https://discourse.joinmastodon.org/
about: Please ask and answer questions here. about: Please ask and answer questions here.
- name: Bug Bounty Program
url: https://app.intigriti.com/programs/mastodon/mastodonio/detail
about: Please report security vulnerabilities here.

View File

@@ -0,0 +1,17 @@
---
name: Feature Request
about: I have a suggestion
---
<!-- Please use a concise and distinct title for the issue -->
<!-- Consider: Could it be implemented as a 3rd party app using the REST API instead? -->
### Pitch
<!-- Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before -->
### Motivation
<!-- Why do you think this feature is needed? Who would benefit from it? -->

10
.github/ISSUE_TEMPLATE/support.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Support
about: Ask for help with your deployment
---
We primarily use GitHub as a bug and feature tracker. For usage questions, troubleshooting of deployments and other individual technical assistance, please use one of the resources below:
- https://discourse.joinmastodon.org
- #mastodon on irc.freenode.net

View File

@@ -6,17 +6,17 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: npm - package-ecosystem: npm
directory: '/' directory: "/"
schedule: schedule:
interval: weekly interval: weekly
open-pull-requests-limit: 99 open-pull-requests-limit: 99
allow: allow:
- dependency-type: direct - dependency-type: all
- package-ecosystem: bundler - package-ecosystem: bundler
directory: '/' directory: "/"
schedule: schedule:
interval: weekly interval: weekly
open-pull-requests-limit: 99 open-pull-requests-limit: 99
allow: allow:
- dependency-type: direct - dependency-type: all

View File

@@ -1,42 +0,0 @@
name: Build container image
on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- '*'
pull_request:
paths:
- .github/workflows/build-image.yml
- Dockerfile
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
if: github.event_name != 'pull_request'
- uses: docker/metadata-action@v3
id: meta
with:
images: tootsuite/mastodon
flavor: |
latest=auto
tags: |
type=edge,branch=main
type=match,pattern=v(.*),group=0
type=ref,event=pr
- uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
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
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
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

5
.gitignore vendored
View File

@@ -40,12 +40,13 @@
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose # Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres /postgres
/postgres14
/redis /redis
/elasticsearch /elasticsearch
# ignore Helm dependency charts # ignore Helm lockfile, dependency charts, and local values file
/chart/Chart.lock
/chart/charts/*.tgz /chart/charts/*.tgz
/chart/values.yaml
# Ignore Apple files # Ignore Apple files
.DS_Store .DS_Store

2
.nvmrc
View File

@@ -1 +1 @@
14 12

View File

@@ -1,78 +0,0 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config and downloaded libraries.
/.bundle
/vendor/bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
.eslintcache
/log/*
!/log/.keep
/tmp
/coverage
/public/system
/public/assets
/public/packs
/public/packs-test
.env
.env.production
.env.development
/node_modules/
/build/
# Ignore Vagrant files
.vagrant/
# Ignore Capistrano customizations
/config/deploy/*
# Ignore IDE files
.vscode/
.idea/
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres
/postgres14
/redis
/elasticsearch
# ignore Helm dependency charts
/chart/charts/*.tgz
# Ignore Apple files
.DS_Store
# Ignore vim files
*~
*.swp
# Ignore npm debug log
npm-debug.log
# Ignore yarn log files
yarn-error.log
yarn-debug.log
# Ignore vagrant log files
*-cloudimg-console.log
# Ignore Docker option files
docker-compose.override.yml
# Ignore Helm files
/chart
# Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json
# Ignore locale files
/app/javascript/mastodon/locales
/config/locales

View File

@@ -1,3 +0,0 @@
module.exports = {
singleQuote: true
}

View File

@@ -2,20 +2,19 @@ require:
- rubocop-rails - rubocop-rails
AllCops: AllCops:
TargetRubyVersion: 2.5 TargetRubyVersion: 2.4
NewCops: disable
Exclude: Exclude:
- 'spec/**/*' - 'spec/**/*'
- 'db/**/*' - 'db/**/*'
- 'app/views/**/*' - 'app/views/**/*'
- 'config/**/*' - 'config/**/*'
- 'bin/*' - 'bin/*'
- 'Rakefile' - 'Rakefile'
- 'node_modules/**/*' - 'node_modules/**/*'
- 'Vagrantfile' - 'Vagrantfile'
- 'vendor/**/*' - 'vendor/**/*'
- 'lib/json_ld/*' - 'lib/json_ld/*'
- 'lib/templates/**/*' - 'lib/templates/**/*'
Bundler/OrderedGems: Bundler/OrderedGems:
Enabled: false Enabled: false
@@ -26,72 +25,30 @@ Layout/AccessModifierIndentation:
Layout/EmptyLineAfterMagicComment: Layout/EmptyLineAfterMagicComment:
Enabled: false Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/EmptyLineBetweenDefs:
AllowAdjacentOneLineDefs: true
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Layout/HashAlignment:
Enabled: false
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: Lint/UselessAccessModifier:
ContextCreatingMethods: ContextCreatingMethods:
- class_methods - 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
@@ -99,9 +56,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
@@ -112,90 +67,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
@@ -210,15 +109,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
@@ -228,33 +118,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': '()'
@@ -263,36 +129,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
@@ -301,6 +140,3 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral: Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma' EnforcedStyleForMultiline: 'comma'
Style/UnpackFirst:
Enabled: false

View File

@@ -1 +1 @@
3.0.3 2.6.6

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,11 @@ libicu-dev
libidn11 libidn11
libidn11-dev libidn11-dev
libpq-dev libpq-dev
libprotobuf-dev
libssl-dev
libxdamage1 libxdamage1
libxfixes3 libxfixes3
protobuf-compiler
zlib1g-dev zlib1g-dev
libcairo2 libcairo2
libcroco3 libcroco3
@@ -20,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,11 +1,10 @@
FROM ubuntu:20.04 as build-dep FROM ubuntu:20.04 as build-dep
# Use bash for the shell # Use bash for the shell
SHELL ["/bin/bash", "-c"] SHELL ["bash", "-c"]
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
# Install Node v16 (LTS) # Install Node v12 (LTS)
ENV NODE_VER="16.14.2" ENV NODE_VER="12.16.3"
RUN ARCH= && \ RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \ dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \ case "${dpkgArch##*-}" in \
@@ -18,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-utils && \ 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 Ruby 3.0 # Install jemalloc
ENV RUBY_VER="3.0.3" ENV JE_VER="5.2.1"
RUN apt-get update && \ RUN apt update && \
apt-get install -y --no-install-recommends build-essential \ apt -y install make autoconf gcc g++ && \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \ 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
ENV RUBY_VER="2.6.6"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
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 && \
@@ -40,26 +54,24 @@ 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 npm@latest && \ RUN npm install -g yarn && \
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 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 config set silence_root_warning true && \ bundle install -j$(nproc) && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile yarn install --pure-lockfile
FROM ubuntu:20.04 FROM ubuntu:20.04
@@ -67,6 +79,7 @@ FROM ubuntu:20.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"
@@ -74,27 +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 echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections RUN apt -y --no-install-recommends install \
RUN apt-get update && \ libssl1.1 libpq5 imagemagick ffmpeg \
apt-get -y --no-install-recommends install \ libicu66 libprotobuf17 libidn11 libyaml-0-2 \
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \ file ca-certificates tzdata libreadline8 && \
libicu66 libidn11 libyaml-0-2 \ apt -y install gcc && \
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
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
@@ -117,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

View File

@@ -1,30 +0,0 @@
## ActivityPub federation in Mastodon
Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all.
Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/
### Required extensions
#### Webfinger
In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`).
This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings.
As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger.
More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/
#### HTTP Signatures
In order to authenticate activities, Mastodon relies on HTTP Signatures, signing every `POST` and `GET` request to other ActivityPub implementations on behalf of the user authoring an activity (for `POST` requests) or an actor representing the Mastodon server itself (for most `GET` requests).
Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server.
More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http
### Optional extensions
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/
- Followers collection synchronization: https://git.activitypub.dev/ActivityPubDev/Fediverse-Enhancement-Proposals/src/branch/main/feps/fep-8fcf.md

162
Gemfile
View File

@@ -1,110 +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 'rexml', '~> 3.2'
gem 'puma', '~> 5.6' gem 'puma', '~> 4.3'
gem 'rails', '~> 6.1.5' gem 'rails', '~> 5.2.4.3'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2' gem 'thor', '~> 0.20'
gem 'rack', '~> 2.2.3' gem 'rack', '~> 2.2.3'
gem 'thwait', '~> 0.1.0'
gem 'e2mmap', '~> 0.1.0'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.3' gem 'pg', '~> 1.2'
gem 'makara', '~> 0.5' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.8' gem 'pghero', '~> 2.5'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.113', require: false gem 'aws-sdk-s3', '~> 1.73', 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 'kt-paperclip', '~> 7.1' 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.8' gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.10.3', 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 'chewy', '~> 7.2' gem 'iso-639'
gem 'devise', '~> 4.8' gem 'chewy', '~> 5.1'
gem 'devise-two-factor', '~> 4.0' gem 'cld3', '~> 3.3.0'
gem 'devise', '~> 4.7'
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 'gitlab-omniauth-openid-connect', '~>0.9.1', require: 'omniauth_openid_connect'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'color_diff', '~> 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.3' gem 'ed25519', '~> 1.2'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
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', '~> 5.0' 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.3'
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.4.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.13' 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.13' gem 'oj', '~> 3.10'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.13'
gem 'parslet' gem 'parslet'
gem 'posix-spawn' gem 'parallel', '~> 1.19'
gem 'pundit', '~> 2.2' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.6' 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.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.2', 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.1' gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.11' gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 6.0' gem 'sanitize', '~> 5.2'
gem 'scenic', '~> 1.6' gem 'sidekiq', '~> 6.0'
gem 'sidekiq', '~> 6.4' gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-scheduler', '~> 3.1' gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-unique-jobs', '~> 7.1'
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.4', 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-prompt', '~> 0.21', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2022' gem 'tzinfo-data', '~> 1.2020'
gem 'webpacker', '~> 5.4' gem 'webpacker', '~> 5.1'
gem 'webpush', '~> 0.3' gem 'webpush'
gem 'webauthn', '~> 3.0.0.alpha1'
gem 'json-ld' gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2' gem 'json-ld-preloaded', '~> 3.1'
gem 'rdf-normalize', '~> 0.5' gem 'rdf-normalize', '~> 0.4'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.28' gem 'fabrication', '~> 2.21'
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 1.0', 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.1' gem 'rspec-rails', '~> 4.0'
end end
group :production, :test do group :production, :test do
@@ -112,44 +118,44 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.36' gem 'capybara', '~> 3.33'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.20' gem 'faker', '~> 2.13'
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.1'
gem 'simplecov', '~> 0.21', require: false gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.14' gem 'webmock', '~> 3.8'
gem 'rspec_junit_formatter', '~> 0.5' gem 'parallel_tests', '~> 3.0'
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.2' 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', '~> 7.0' gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.8' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 2.0' gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 1.26', require: false gem 'rubocop', '~> 0.86', require: false
gem 'rubocop-rails', '~> 2.14', require: false gem 'rubocop-rails', '~> 2.6', require: false
gem 'brakeman', '~> 5.2', require: false gem 'brakeman', '~> 4.8', require: false
gem 'bundler-audit', '~> 0.9', require: false gem 'bundler-audit', '~> 0.7', require: false
gem 'capistrano', '~> 3.17' gem 'capistrano', '~> 3.14'
gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rails', '~> 1.5'
gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
gem 'stackprof' gem 'stackprof'
end end
group :production do group :production do
gem 'lograge', '~> 0.12' 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'

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,15 +1,15 @@
![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/mastodon/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/mastodon/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/
@@ -28,7 +28,7 @@ Click below to **learn more** in a video:
- [View sponsors](https://joinmastodon.org/sponsors) - [View sponsors](https://joinmastodon.org/sponsors)
- [Blog](https://blog.joinmastodon.org) - [Blog](https://blog.joinmastodon.org)
- [Documentation](https://docs.joinmastodon.org) - [Documentation](https://docs.joinmastodon.org)
- [Browse Mastodon servers](https://joinmastodon.org/communities) - [Browse Mastodon servers](https://joinmastodon.org/#getting-started)
- [Browse Mastodon apps](https://joinmastodon.org/apps) - [Browse Mastodon apps](https://joinmastodon.org/apps)
[patreon]: https://www.patreon.com/mastodon [patreon]: https://www.patreon.com/mastodon
@@ -37,62 +37,56 @@ Click below to **learn more** in a video:
<img src="https://docs.joinmastodon.org/elephant.svg" align="right" width="30%" /> <img src="https://docs.joinmastodon.org/elephant.svg" align="right" width="30%" />
### No vendor lock-in: Fully interoperable with any conforming platform **No vendor lock-in: Fully interoperable with any conforming platform**
It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/) It doesn't have to be Mastodon, whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
### Real-time, chronological timeline updates **Real-time, chronological timeline updates**
Updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well! See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
### Media attachments like images and short videos **Media attachments like images and short videos**
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously! Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines!
### Safety and moderation tools **Safety and moderation tools**
Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/) Private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
### OAuth2 and a straightforward REST API **OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices! Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choices!
## Deployment ## Deployment
### Tech stack: **Tech stack:**
- **Ruby on Rails** powers the REST API and other web pages - **Ruby on Rails** powers the REST API and other web pages
- **React.js** and Redux are used for the dynamic parts of the interface - **React.js** and Redux are used for the dynamic parts of the interface
- **Node.js** powers the streaming API - **Node.js** powers the streaming API
### Requirements: **Requirements:**
- **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** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**standalone** 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.
A **Vagrant** configuration is included for development purposes. To use it, complete following steps: A **Vagrant** configuration is included for development purposes.
- Install Vagrant and Virtualbox
- Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater`
- Run `vagrant up`
- Run `vagrant ssh -c "cd /vagrant && foreman start"`
- Open `http://mastodon.local` in your browser
## Contributing ## Contributing
Mastodon is **free, open-source software** licensed under **AGPLv3**. 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-2022 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,20 +1,12 @@
# Security Policy # Security Policy
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you should submit the report through our [Bug Bounty Program][bug-bounty]. Alternatively, you can reach us at <hello@joinmastodon.org>.
You should *not* report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
## Scope
A "vulnerability in Mastodon" is a vulnerability in the code distributed through our main source code repository on GitHub. Vulnerabilities that are specific to a given installation (e.g. misconfiguration) should be reported to the owner of that installation and not us.
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 3.5.x | Yes | | 3.1.x | :white_check_mark: |
| 3.4.x | Yes | | < 3.1 | :x: |
| 3.3.x | Yes |
| < 3.3 | No |
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail ## Reporting a Vulnerability
hello@joinmastodon.org

18
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_14.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"]}
@@ -33,9 +33,11 @@ sudo apt-get install \
redis-tools \ redis-tools \
postgresql \ postgresql \
postgresql-contrib \ postgresql-contrib \
protobuf-compiler \
yarn \ yarn \
libicu-dev \ libicu-dev \
libidn11-dev \ libidn11-dev \
libprotobuf-dev \
libreadline-dev \ libreadline-dev \
libpam0g-dev \ libpam0g-dev \
-y -y
@@ -43,8 +45,16 @@ sudo apt-get install \
# Install rvm # Install rvm
read RUBY_VERSION < .ruby-version read RUBY_VERSION < .ruby-version
curl -sSL https://rvm.io/mpapis.asc | gpg --import gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import $($gpg_command)
if [ $? -ne 0 ];then
echo "GPG command failed, This prevented RVM from installing."
echo "Retrying once..." && $($gpg_command)
if [ $? -ne 0 ];then
echo "GPG failed for the second time, please ensure network connectivity."
echo "Exiting..." && exit 1
fi
fi
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm source /home/vagrant/.rvm/scripts/rvm
@@ -62,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",
@@ -95,5 +95,8 @@
"scripts": { "scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
}, },
"addons": ["heroku-postgresql", "heroku-redis"] "addons": [
"heroku-postgresql",
"heroku-redis"
]
} }

View File

@@ -23,21 +23,21 @@ class AccountsIndex < Chewy::Index
}, },
} }
index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
root date_detection: false do
field :id, type: 'long'
root date_detection: false do field :display_name, type: 'text', analyzer: 'content' do
field :id, type: 'long' field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :display_name, type: 'text', analyzer: 'content' do field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end end
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end end
end end

View File

@@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusesIndex < Chewy::Index class StatusesIndex < Chewy::Index
include FormattingHelper
settings index: { refresh_interval: '15m' }, analysis: { settings index: { refresh_interval: '15m' }, analysis: {
filter: { filter: {
english_stop: { english_stop: {
@@ -33,36 +31,36 @@ class StatusesIndex < Chewy::Index
}, },
} }
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) 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, silent: false).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
crutch :favourites do |collection|
data = ::Favourite.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) }
end
crutch :reblogs do |collection|
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :bookmarks do |collection|
data = ::Bookmark.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) }
end
root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long'
field :text, type: 'text', value: ->(status) { status.searchable_text } do
field :stemmed, type: 'text', analyzer: 'content'
end end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } crutch :favourites do |collection|
data = ::Favourite.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) }
end
crutch :reblogs do |collection|
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :bookmarks do |collection|
data = ::Bookmark.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) }
end
root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long'
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content'
end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
end
end end
end end

View File

@@ -23,15 +23,15 @@ class TagsIndex < Chewy::Index
}, },
} }
index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do
root date_detection: false do
field :name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
root date_detection: false do field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :name, type: 'text', analyzer: 'content' do field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
end end
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
end end
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

@@ -28,8 +28,9 @@ class AccountsController < ApplicationController
return return
end end
@pinned_statuses = cached_filtered_status_pins 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?
@@ -64,10 +65,6 @@ class AccountsController < ApplicationController
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end end
def filtered_pinned_statuses
@account.pinned_statuses.where(visibility: [:public, :unlisted])
end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(hashtag_scope) if tag_requested?
@@ -81,7 +78,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
@@ -102,10 +103,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')
@@ -135,31 +132,19 @@ 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_pins def filtered_status_page
cache_collection( filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
filtered_pinned_statuses,
Status
)
end
def cached_filtered_status_page
cache_collection_paginated_by_id(
filtered_statuses,
Status,
PAGE_SIZE,
params_slice(:max_id, :min_id, :since_id)
)
end end
def params_slice(*keys) def params_slice(*keys)

View File

@@ -2,15 +2,10 @@
class ActivityPub::BaseController < Api::BaseController class ActivityPub::BaseController < Api::BaseController
skip_before_action :require_authenticated_user! skip_before_action :require_authenticated_user!
skip_around_action :set_locale
private private
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

@@ -12,7 +12,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
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
@@ -20,10 +20,17 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_items def set_items
case params[:id] case params[:id]
when 'featured' when 'featured'
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) } @items = begin
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } # Because in public fetch mode we cache the response, there would be no
when 'tags' # benefit from performing the check below, since a blocked account or domain
@items = for_signed_account { @account.featured_tags } # 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
cache_collection(@account.pinned_statuses, Status)
end
end
when 'devices' when 'devices'
@items = @account.devices @items = @account.devices
else else
@@ -33,7 +40,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size def set_size
case params[:id] case params[:id]
when 'featured', 'devices', 'tags' when 'featured', 'devices'
@size = @items.size @size = @items.size
else else
not_found not_found
@@ -44,7 +51,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id] case params[:id]
when 'featured' when 'featured'
@type = :ordered @type = :ordered
when 'devices', 'tags' when 'devices'
@type = :unordered @type = :unordered
else else
not_found not_found
@@ -59,16 +66,4 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
items: @items items: @items
) )
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)
AccountStatusesFilter.new(@account, signed_request_account).results, @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
@@ -63,29 +63,15 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end end
def next_page def next_page
if only_other_accounts? only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
# Only consider remote accounts
return nil if @replies.size < DESCENDANTS_LIMIT
account_status_replies_url( account_status_replies_url(
@account, @account,
@status, @status,
page: true, page: true,
min_id: @replies&.last&.id, min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
only_other_accounts: true only_other_accounts: only_other_accounts
) )
else
# For now, we're serving only self-replies, but next page might be other accounts
next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT
account_status_replies_url(
@account,
@status,
page: true,
min_id: next_only_other_accounts ? nil : @replies&.last&.id,
only_other_accounts: next_only_other_accounts
)
end
end end
def page_params def page_params

View File

@@ -14,7 +14,7 @@ module Admin
else else
@account = @account_moderation_note.target_account @account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.strikes.custom.latest @warnings = @account.targeted_account_warnings.latest.custom
render template: 'admin/accounts/show' render template: 'admin/accounts/show'
end end

View File

@@ -2,88 +2,61 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, except: [:index, :batch] 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]
def index def index
authorize :account, :index? authorize :account, :index?
@accounts = filtered_accounts.page(params[:page]) @accounts = filtered_accounts.page(params[:page])
@form = Form::AccountBatch.new
end
def batch
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_accounts_path(filter_params)
end end
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.strikes.includes(:target_account, :account, :appeal).latest @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_accounts_path(status: 'pending'), 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_accounts_path(status: 'pending'), 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
@@ -92,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
@@ -103,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
@@ -114,17 +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
def unblock_email
authorize @account, :unblock_email?
CanonicalEmailBlock.where(reference_account: @account).delete_all
log_action :unblock_email, @account
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unblocked_email_msg', username: @account.acct)
end end
private private
@@ -142,25 +105,11 @@ module Admin
end end
def filtered_accounts def filtered_accounts
AccountFilter.new(filter_params.with_defaults(order: 'recent')).results AccountFilter.new(filter_params).results
end end
def filter_params def filter_params
params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS) params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def action_from_button
if params[:suspend]
'suspend'
elsif params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end end
end end
end end

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

@@ -1,18 +1,49 @@
# frozen_string_literal: true # frozen_string_literal: true
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
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
@pending_users_count = User.pending.count @pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count @registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
@relay_enabled = Relay.enabled.exists?
@single_user_mode = Rails.configuration.x.single_user_mode
@registrations_enabled = Setting.registrations_mode != 'none'
@deletions_enabled = Setting.open_deletion
@invites_enabled = Setting.min_invite_role == 'user'
@search_enabled = Chewy.enabled?
@version = Mastodon::Version.to_s
@database_version = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
@redis_version = redis_info['redis_version']
@reports_count = Report.unresolved.count
@queue_backlog = Sidekiq::Stats.new.enqueued
@recent_users = User.confirmed.recent.includes(:account).limit(8)
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
@redis_size = redis_info['used_memory']
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
@cas_enabled = ENV['CAS_ENABLED'] == 'true'
@saml_enabled = ENV['SAML_ENABLED'] == 'true'
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(10, filtered: false)
@pending_tags_count = Tag.pending_review.count @pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count @authorized_fetch = authorized_fetch_mode?
@whitelist_enabled = whitelist_mode?
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
end end
private private
def current_week
@current_week ||= Time.now.utc.to_date.cweek
end
def redis_info def redis_info
@redis_info ||= begin @redis_info ||= begin
if Redis.current.is_a?(Redis::Namespace) if Redis.current.is_a?(Redis::Namespace)

View File

@@ -1,40 +0,0 @@
# frozen_string_literal: true
class Admin::Disputes::AppealsController < Admin::BaseController
before_action :set_appeal, except: :index
def index
authorize :appeal, :index?
@appeals = filtered_appeals.page(params[:page])
end
def approve
authorize @appeal, :approve?
log_action :approve, @appeal
ApproveAppealService.new.call(@appeal, current_account)
redirect_to disputes_strike_path(@appeal.strike)
end
def reject
authorize @appeal, :approve?
log_action :reject, @appeal
@appeal.reject!(current_account)
UserMailer.appeal_rejected(@appeal.account.user, @appeal)
redirect_to disputes_strike_path(@appeal.strike)
end
private
def filtered_appeals
Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account)
end
def filter_params
params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS)
end
def set_appeal
@appeal = Appeal.find(params[:id])
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,13 +48,17 @@ 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
end end
end end
def show
authorize @domain_block, :show?
end
def destroy def destroy
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block) UnblockDomainService.new.call(@domain_block)
@@ -70,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

@@ -6,20 +6,7 @@ module Admin
def index def index
authorize :email_domain_block, :index? authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
@form = Form::EmailDomainBlockBatch.new
end
def batch
@form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.email_domain_blocks.no_email_domain_block_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
ensure
redirect_to admin_email_domain_blocks_path
end end
def new def new
@@ -32,27 +19,41 @@ module Admin
@email_domain_block = EmailDomainBlock.new(resource_params) @email_domain_block = EmailDomainBlock.new(resource_params)
if action_from_button == 'save' if @email_domain_block.save
EmailDomainBlock.transaction do log_action :create, @email_domain_block
@email_domain_block.save!
log_action :create, @email_domain_block
(@email_domain_block.other_domains || []).uniq.each do |domain| if @email_domain_block.with_dns_records?
next if EmailDomainBlock.where(domain: domain).exists? hostnames = []
ips = []
other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block) Resolv::DNS.open do |dns|
log_action :create, other_email_domain_block dns.timeouts = 1
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
([@email_domain_block.domain] + hostnames).uniq.each do |hostname|
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s })
end
end
(hostnames + ips).each do |hostname|
another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block)
log_action :create, another_email_domain_block if another_email_domain_block.save
end end
end end
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else else
set_resolved_records
render :new render :new
end end
rescue ActiveRecord::RecordInvalid end
set_resolved_records
render :new def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end end
private private
@@ -61,27 +62,8 @@ module Admin
@email_domain_block = EmailDomainBlock.find(params[:id]) @email_domain_block = EmailDomainBlock.find(params[:id])
end end
def set_resolved_records
Resolv::DNS.open do |dns|
dns.timeouts = 5
@resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a
end
end
def resource_params def resource_params
params.require(:email_domain_block).permit(:domain, other_domains: []) params.require(:email_domain_block).permit(:domain, :with_dns_records)
end
def form_email_domain_block_batch_params
params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: [])
end
def action_from_button
if params[:delete]
'delete'
elsif params[:save]
'save'
end
end end
end end
end end

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,65 +2,48 @@
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_instance, only: :show
def index def index
authorize :instance, :index? authorize :instance, :index?
preload_delivery_failures!
@instances = ordered_instances
end end
def show def show
authorize :instance, :show? authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end
def destroy @following_count = Follow.where(account: Account.where(domain: params[:id])).count
authorize :instance, :destroy? @followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
Admin::DomainPurgeWorker.perform_async(@instance.domain) @reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
log_action :destroy, @instance @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @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 clear_delivery_errors @public_comment = @domain_block&.public_comment
authorize :delivery, :clear_delivery_errors?
@instance.delivery_failure_tracker.clear_failures!
redirect_to admin_instance_path(@instance.domain)
end
def restart_delivery
authorize :delivery, :restart_delivery?
if @instance.unavailable?
@instance.delivery_failure_tracker.track_success!
log_action :destroy, @instance.unavailable_domain
end
redirect_to admin_instance_path(@instance.domain)
end
def stop_delivery
authorize :delivery, :stop_delivery?
unavailable_domain = 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_instances if resource
@instances = filtered_instances.page(params[:page]) @instance = Instance.new(resource)
end else
not_found
def preload_delivery_failures!
warning_domains_map = DeliveryFailureTracker.warning_domains_map
@instances.each do |instance|
instance.failure_days = warning_domains_map[instance.domain]
end end
end end
@@ -68,6 +51,16 @@ module Admin
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

@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Admin
class PendingAccountsController < BaseController
before_action :set_accounts, only: :index
def index
@form = Form::AccountBatch.new
end
def batch
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_pending_accounts_path(current_params)
end
def approve_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
redirect_to admin_pending_accounts_path(current_params)
end
def reject_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
redirect_to admin_pending_accounts_path(current_params)
end
private
def set_accounts
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
def current_params
params.slice(:page).permit(:page)
end
end
end

View File

@@ -9,8 +9,7 @@ module Admin
def index def index
authorize :account, :index? authorize :account, :index?
@accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE) @accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE)
@form = Form::AccountBatch.new
end end
private private

View File

@@ -14,17 +14,20 @@ module Admin
if params[:create_and_resolve] if params[:create_and_resolve]
@report.resolve!(current_account) @report.resolve!(current_account)
log_action :resolve, @report log_action :resolve, @report
elsif params[:create_and_unresolve]
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
return
end
if params[:create_and_unresolve]
@report.unresolve! @report.unresolve!
log_action :reopen, @report log_action :reopen, @report
end end
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg') redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
else else
@report_notes = @report.notes.includes(:account).order(id: :desc) @report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
@action_logs = @report.history.includes(:target) @form = Form::StatusBatch.new
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes
render template: 'admin/reports/show' render template: 'admin/reports/show'
end end
@@ -38,14 +41,6 @@ module Admin
private private
def after_create_redirect_path
if params[:create_and_resolve]
admin_reports_path
else
admin_report_path(@report)
end
end
def resource_params def resource_params
params.require(:report_note).permit( params.require(:report_note).permit(
:content, :content,

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
module Admin
class ReportedStatusesController < BaseController
before_action :set_report
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_report_path(@report)
end
private
def status_params
params.require(:status).permit(:sensitive)
end
def form_status_batch_params
params.require(:form_status_batch).permit(status_ids: [])
end
def action_from_button
if params[:nsfw_on]
'nsfw_on'
elsif params[:nsfw_off]
'nsfw_off'
elsif params[:delete]
'delete'
end
end
def set_report
@report = Report.find(params[:report_id])
end
end
end

View File

@@ -1,52 +0,0 @@
# frozen_string_literal: true
class Admin::Reports::ActionsController < Admin::BaseController
before_action :set_report
def create
authorize @report, :show?
case action_from_button
when 'delete', 'mark_as_sensitive'
status_batch_action = Admin::StatusBatchAction.new(
type: action_from_button,
status_ids: @report.status_ids,
current_account: current_account,
report_id: @report.id,
send_email_notification: !@report.spam?
)
status_batch_action.save!
when 'silence', 'suspend'
account_action = Admin::AccountAction.new(
type: action_from_button,
report_id: @report.id,
target_account: @report.target_account,
current_account: current_account,
send_email_notification: !@report.spam?
)
account_action.save!
end
redirect_to admin_reports_path
end
private
def set_report
@report = Report.find(params[:report_id])
end
def action_from_button
if params[:delete]
'delete'
elsif params[:mark_as_sensitive]
'mark_as_sensitive'
elsif params[:silence]
'silence'
elsif params[:suspend]
'suspend'
end
end
end

View File

@@ -13,10 +13,8 @@ module Admin
authorize @report, :show? authorize @report, :show?
@report_note = @report.notes.new @report_note = @report.notes.new
@report_notes = @report.notes.includes(:account).order(id: :desc) @report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
@action_logs = @report.history.includes(:target) @form = Form::StatusBatch.new
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes
end end
def assign_to_self def assign_to_self

View File

@@ -6,9 +6,9 @@ module Admin
def create def create
authorize @user, :reset_password? authorize @user, :reset_password?
@user.reset_password! @user.send_reset_password_instructions
log_action :reset_password, @user log_action :reset_password, @user
redirect_to admin_account_path(@user.account_id) redirect_to admin_accounts_path
end end
end 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

@@ -2,62 +2,72 @@
module Admin module Admin
class StatusesController < BaseController class StatusesController < BaseController
helper_method :current_params
before_action :set_account before_action :set_account
before_action :set_statuses
PER_PAGE = 20 PER_PAGE = 20
def index def index
authorize :status, :index? authorize :status, :index?
@status_batch_action = Admin::StatusBatchAction.new @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
end end
def batch def show
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button)) authorize :status, :index?
@status_batch_action.save!
@statuses = @account.statuses.where(id: params[:id])
authorize @statuses.first, :show?
@form = Form::StatusBatch.new
end
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected') flash[:alert] = I18n.t('admin.statuses.no_status_selected')
ensure
redirect_to after_create_redirect_path redirect_to admin_account_statuses_path(@account.id, current_params)
end end
private private
def admin_status_batch_action_params def form_status_batch_params
params.require(:admin_status_batch_action).permit(status_ids: []) params.require(:form_status_batch).permit(:action, status_ids: [])
end
def after_create_redirect_path
report_id = @status_batch_action&.report_id || params[:report_id]
if report_id.present?
admin_report_path(report_id)
else
admin_account_statuses_path(params[:account_id], current_params)
end
end end
def set_account def set_account
@account = Account.find(params[:account_id]) @account = Account.find(params[:account_id])
end end
def set_statuses
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
end
def filter_params
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
end
def current_params def current_params
params.slice(:media, :page).permit(:media, :page) page = (params[:page] || 1).to_i
{
media: params[:media],
page: page > 1 && page,
}.select { |_, value| value.present? }
end end
def action_from_button def action_from_button
if params[:report] if params[:nsfw_on]
'report' 'nsfw_on'
elsif params[:remove_from_report] elsif params[:nsfw_off]
'remove_from_report' 'nsfw_off'
elsif params[:delete] elsif params[:delete]
'delete' 'delete'
end end

View File

@@ -2,12 +2,38 @@
module Admin module Admin
class TagsController < BaseController class TagsController < BaseController
before_action :set_tag before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
def index
authorize :tag, :index?
@tags = filtered_tags.page(params[:page])
@form = Form::TagBatch.new
end
def batch
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_tags_path(filter_params)
end
def approve_all
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
redirect_to admin_tags_path(filter_params)
end
def reject_all
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save
redirect_to admin_tags_path(filter_params)
end
def show def show
authorize @tag, :show? authorize @tag, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end end
def update def update
@@ -26,8 +52,52 @@ module Admin
@tag = Tag.find(params[:id]) @tag = Tag.find(params[:id])
end end
def set_usage_by_domain
@usage_by_domain = @tag.statuses
.with_public_visibility
.excluding_silenced_accounts
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
.joins(:account)
.group('accounts.domain')
.reorder('statuses_count desc')
.pluck('accounts.domain, count(*) AS statuses_count')
end
def set_counters
@accounts_today = @tag.history.first[:accounts]
@accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
end
def filtered_tags
TagFilter.new(filter_params).results
end
def filter_params
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
end
def tag_params def tag_params
params.require(:tag).permit(:name, :trendable, :usable, :listable) params.require(:tag).permit(:name, :trendable, :usable, :listable)
end end
def current_week_days
now = Time.now.utc.beginning_of_day.to_date
(Date.commercial(now.cwyear, now.cweek)..now).map do |date|
date.to_time(:utc).beginning_of_day.to_i
end
end
def form_tag_batch_params
params.require(:form_tag_batch).permit(:action, tag_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
end end
end end

View File

@@ -1,41 +0,0 @@
# frozen_string_literal: true
class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseController
def index
authorize :preview_card_provider, :index?
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
@form = Trends::PreviewCardProviderBatch.new
end
def batch
@form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_trends_links_preview_card_providers_path(filter_params)
end
private
def filtered_preview_card_providers
Trends::PreviewCardProviderFilter.new(filter_params).results
end
def filter_params
params.slice(:page, *Trends::PreviewCardProviderFilter::KEYS).permit(:page, *Trends::PreviewCardProviderFilter::KEYS)
end
def trends_preview_card_provider_batch_params
params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
end

View File

@@ -1,45 +0,0 @@
# frozen_string_literal: true
class Admin::Trends::LinksController < Admin::BaseController
def index
authorize :preview_card, :index?
@preview_cards = filtered_preview_cards.page(params[:page])
@form = Trends::PreviewCardBatch.new
end
def batch
@form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_trends_links_path(filter_params)
end
private
def filtered_preview_cards
Trends::PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
end
def filter_params
params.slice(:page, *Trends::PreviewCardFilter::KEYS).permit(:page, *Trends::PreviewCardFilter::KEYS)
end
def trends_preview_card_batch_params
params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:approve_providers]
'approve_providers'
elsif params[:reject]
'reject'
elsif params[:reject_providers]
'reject_providers'
end
end
end

View File

@@ -1,45 +0,0 @@
# frozen_string_literal: true
class Admin::Trends::StatusesController < Admin::BaseController
def index
authorize :status, :index?
@statuses = filtered_statuses.page(params[:page])
@form = Trends::StatusBatch.new
end
def batch
@form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_trends_statuses_path(filter_params)
end
private
def filtered_statuses
Trends::StatusFilter.new(filter_params.with_defaults(trending: 'all')).results.includes(:account, :media_attachments, :active_mentions)
end
def filter_params
params.slice(:page, *Trends::StatusFilter::KEYS).permit(:page, *Trends::StatusFilter::KEYS)
end
def trends_status_batch_params
params.require(:trends_status_batch).permit(:action, status_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:approve_accounts]
'approve_accounts'
elsif params[:reject]
'reject'
elsif params[:reject_accounts]
'reject_accounts'
end
end
end

View File

@@ -1,41 +0,0 @@
# frozen_string_literal: true
class Admin::Trends::TagsController < Admin::BaseController
def index
authorize :tag, :index?
@tags = filtered_tags.page(params[:page])
@form = Trends::TagBatch.new
end
def batch
@form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_trends_tags_path(filter_params)
end
private
def filtered_tags
Trends::TagFilter.new(filter_params).results
end
def filter_params
params.slice(:page, *Trends::TagFilter::KEYS).permit(:page, *Trends::TagFilter::KEYS)
end
def trends_tag_batch_params
params.require(:trends_tag_batch).permit(:action, tag_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
end

View File

@@ -9,7 +9,7 @@ module Admin
@user.disable_two_factor! @user.disable_two_factor!
log_action :disable_2fa, @user log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later! UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_account_path(@user.account_id) redirect_to admin_accounts_path
end end
private private

View File

@@ -5,7 +5,6 @@ class Api::BaseController < ApplicationController
DEFAULT_ACCOUNTS_LIMIT = 40 DEFAULT_ACCOUNTS_LIMIT = 40
include RateLimitHeaders include RateLimitHeaders
include AccessTokenTrackingConcern
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!, unless: :whitelist_mode?
@@ -15,6 +14,8 @@ class Api::BaseController < ApplicationController
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
skip_around_action :set_locale
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422 render json: { error: e.to_s }, status: 422
end end
@@ -39,12 +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 Seahorse::Client::NetworkingError do |e| rescue_from Mastodon::RaceConditionError do
Rails.logger.warn "Storage server error: #{e}"
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end end
@@ -75,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
@@ -100,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

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::ProofsController < Api::BaseController
include AccountOwnedConcern
skip_before_action :require_authenticated_user!
before_action :set_provider
def index
render json: @account, serializer: @provider.serializer_class
end
private
def set_provider
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end
def username_param
params[:username]
end
end

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }
before_action :require_user!
before_action :set_accounts
def index
render json: familiar_followers.accounts, each_serializer: REST::FamiliarFollowersSerializer
end
private
def set_accounts
@accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact
end
def familiar_followers
FamiliarFollowersPresenter.new(@accounts, current_user.account_id)
end
def account_ids
Array(params[:id]).map(&:to_i)
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,8 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :set_account before_action :set_account
def index def index
render json: [] @proofs = @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer
end end
private private

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

@@ -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,20 +18,68 @@ 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_paginated_by_id( cache_collection account_statuses, Status
AccountStatusesFilter.new(@account, current_account, params).results, end
Status,
limit_param(DEFAULT_STATUSES_LIMIT), def account_statuses
params_slice(:max_id, :since_id, :min_id) statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
)
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def permitted_account_statuses
@account.statuses.permitted_for(@account, current_account)
end
def only_media_scope
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
def pinned_scope
return Status.none if @account.blocking?(current_account)
@account.pinned_statuses
end
def no_replies_scope
Status.without_replies
end
def no_reblogs_scope
Status.without_reblogs
end
def hashtag_scope
tag = Tag.find_normalized(params[:tagged])
if tag
Status.tagged_with(tag.id)
else
Status.none
end
end end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end end
def insert_pagination_headers def insert_pagination_headers

View File

@@ -1,14 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AccountsController < Api::BaseController class Api::V1::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute] before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers] before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, only: [:block, :unblock] before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
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
@@ -53,11 +53,6 @@ class Api::V1::AccountsController < Api::BaseController
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
def remove_from_followers
RemoveFromFollowersService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
def unblock def unblock
UnblockService.new.call(current_user.account, @account) UnblockService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
@@ -75,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
@@ -83,14 +82,10 @@ class Api::V1::AccountsController < Api::BaseController
end end
def check_enabled_registrations def check_enabled_registrations
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? forbidden if single_user_mode? || !allowed_registrations?
end end
def allowed_registrations? def allowed_registrations?
Setting.registrations_mode != 'none' Setting.registrations_mode != 'none'
end end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
end end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Admin::AccountActionsController < Api::BaseController class Api::V1::Admin::AccountActionsController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' } before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
before_action :require_staff! before_action :require_staff!
before_action :set_account before_action :set_account

View File

@@ -6,8 +6,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
LIMIT = 100 LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff! before_action :require_staff!
before_action :set_accounts, only: :index before_action :set_accounts, only: :index
before_action :set_account, except: :index before_action :set_account, except: :index
@@ -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,21 +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?
json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer
Admin::AccountDeletionWorker.perform_async(@account.id)
render json: json
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
@@ -87,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
@@ -95,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, :ips]).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
@@ -103,27 +87,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
end end
def filtered_accounts def filtered_accounts
AccountFilter.new(translated_filter_params).results AccountFilter.new(filter_params).results
end end
def filter_params def filter_params
params.permit(*FILTER_PARAMS) params.permit(*FILTER_PARAMS)
end end
def translated_filter_params
translated_params = { origin: 'local', status: 'active' }.merge(filter_params.slice(*AccountFilter::KEYS))
translated_params[:origin] = 'remote' if params[:remote].present?
%i(active pending disabled silenced suspended).each do |status|
translated_params[:status] = status.to_s if params[status].present?
end
translated_params[:permissions] = 'staff' if params[:staff].present?
translated_params
end
def insert_pagination_headers def insert_pagination_headers
set_pagination_headers(next_path, prev_path) set_pagination_headers(next_path, prev_path)
end end

View File

@@ -1,23 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::DimensionsController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_dimensions
def create
render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer
end
private
def set_dimensions
@dimensions = Admin::Metrics::Dimension.retrieve(
params[:keys],
params[:start_at],
params[:end_at],
params[:limit],
params
)
end
end

View File

@@ -1,22 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::MeasuresController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_measures
def create
render json: @measures, each_serializer: REST::Admin::MeasureSerializer
end
private
def set_measures
@measures = Admin::Metrics::Measure.retrieve(
params[:keys],
params[:start_at],
params[:end_at],
params
)
end
end

View File

@@ -6,8 +6,8 @@ class Api::V1::Admin::ReportsController < Api::BaseController
LIMIT = 100 LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:reports' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:reports' }, except: [:index, :show] before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
before_action :require_staff! before_action :require_staff!
before_action :set_reports, only: :index before_action :set_reports, only: :index
before_action :set_report, except: :index before_action :set_report, except: :index
@@ -32,12 +32,6 @@ class Api::V1::Admin::ReportsController < Api::BaseController
render json: @report, serializer: REST::Admin::ReportSerializer render json: @report, serializer: REST::Admin::ReportSerializer
end end
def update
authorize @report, :update?
@report.update!(report_params)
render json: @report, serializer: REST::Admin::ReportSerializer
end
def assign_to_self def assign_to_self
authorize @report, :update? authorize @report, :update?
@report.update!(assigned_account_id: current_account.id) @report.update!(assigned_account_id: current_account.id)
@@ -69,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
@@ -80,10 +74,6 @@ class Api::V1::Admin::ReportsController < Api::BaseController
ReportFilter.new(filter_params).results ReportFilter.new(filter_params).results
end end
def report_params
params.permit(:category, rule_ids: [])
end
def filter_params def filter_params
params.permit(*FILTER_PARAMS) params.permit(*FILTER_PARAMS)
end end

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::RetentionController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_cohorts
def create
render json: @cohorts, each_serializer: REST::Admin::CohortSerializer
end
private
def set_cohorts
@cohorts = Admin::Metrics::Retention.new(
params[:start_at],
params[:end_at],
params[:frequency]
).cohorts
end
end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::Trends::LinksController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_links
def index
render json: @links, each_serializer: REST::Trends::LinkSerializer
end
private
def set_links
@links = Trends.links.query.limit(limit_param(10))
end
end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::Trends::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_statuses
def index
render json: @statuses, each_serializer: REST::StatusSerializer
end
private
def set_statuses
@statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status)
end
end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::Trends::TagsController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_tags
def index
render json: @tags, each_serializer: REST::Admin::TagSerializer
end
private
def set_tags
@tags = Trends.tags.query.limit(limit_param(10))
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::BlocksController < Api::BaseController class Api::V1::BlocksController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :read, :'read:blocks' } before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers after_action :insert_pagination_headers
@@ -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)
) )

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