Merge upstream tag 'v3.5.2'

This commit is contained in:
Mike Barnes 2022-05-23 19:28:18 +10:00
commit d161ca885c
2205 changed files with 91260 additions and 41616 deletions

View File

@ -1,187 +1,155 @@
version: 2
version: 2.1
aliases:
- &defaults
orbs:
ruby: circleci/ruby@1.4.1
node: circleci/node@5.0.1
executors:
default:
parameters:
ruby-version:
type: string
docker:
- image: circleci/ruby:2.7-buster-node
environment: &ruby_environment
- image: cimg/ruby:<< parameters.ruby-version >>
environment:
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
BUNDLE_APP_CONFIG: ./.bundle/
BUNDLE_PATH: ./vendor/bundle/
CONTINUOUS_INTEGRATION: true
DB_HOST: localhost
DB_USER: root
RAILS_ENV: test
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
RAILS_ENV: test
- image: cimg/postgres:14.0
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
- image: cimg/redis:6.2
commands:
install-system-dependencies:
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Create database
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:
- image: cimg/ruby:3.0-node
environment:
RAILS_ENV: test
steps:
- checkout
- install-system-dependencies
- 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:
parameters:
ruby-version:
type: string
executor:
name: default
ruby-version: << parameters.ruby-version >>
environment:
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
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:
executor:
name: default
ruby-version: '3.0'
steps:
- checkout
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run:
command: ./bin/rails db:create
name: Create database
- run:
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate 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:
<<: *defaults
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
executor:
name: default
ruby-version: '3.0'
steps:
- *attach_workspace
- *install_system_dependencies
- checkout
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run:
command: ./bin/rails db:create
name: Create database
@ -191,99 +159,51 @@ jobs:
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180514140000
name: Run pre-deployment migrations up to v2.4.0
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
evironment:
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
test-ruby2.7:
<<: *defaults
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps
test-webui:
<<: *defaults
docker:
- image: circleci/node:12-buster
steps:
- *attach_workspace
- run:
name: Run jest
command: yarn test:jest
check-i18n:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Check locale file normalization
command: bundle exec i18n-tasks check-normalized
- run:
name: Check for unused strings
command: bundle exec i18n-tasks unused -l en
- run:
name: Check for wrong string interpolations
command: bundle exec i18n-tasks check-consistent-interpolations
- run:
name: Check that all required locale files exist
command: bundle exec rake repo:check_locales_files
command: ./bin/rails tests:migrations:check_database
name: Check migration result
workflows:
version: 2
build-and-test:
jobs:
- install
- install-ruby2.7:
- build
- test:
matrix:
parameters:
ruby-version:
- '2.7'
- '3.0'
name: test-ruby<< matrix.ruby-version >>
requires:
- install
- install-ruby2.6:
requires:
- install
- install-ruby2.7
- build:
requires:
- install-ruby2.7
- build
- test-migrations:
requires:
- install-ruby2.7
- build
- test-two-step-migrations:
requires:
- install-ruby2.7
- test-ruby2.7:
requires:
- install-ruby2.7
- build
- test-ruby2.6:
- node/run:
cache-version: v1
name: test-webui
pkg-manager: yarn
requires:
- install-ruby2.6
- build
- test-webui:
requires:
- install
- check-i18n:
requires:
- install-ruby2.7
version: lts
yarn-run: test:jest

View File

@ -1,4 +1,4 @@
version: "2"
version: '2'
checks:
argument-count:
enabled: false
@ -30,9 +30,12 @@ plugins:
channel: eslint-7
rubocop:
enabled: true
channel: rubocop-0-92
channel: rubocop-1-9-1
sass-lint:
enabled: true
exclude_patterns:
- spec/
- vendor/asset
- spec/
- vendor/asset/
- app/javascript/mastodon/locales/**/*.json
- config/locales/**/*.yml

23
.deepsource.toml Normal file
View File

@ -0,0 +1,23 @@
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"
]

24
.devcontainer/Dockerfile Normal file
View File

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

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

@ -0,0 +1,83 @@
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,6 +1,10 @@
.bundle
.env
.env.*
.git
.gitattributes
.gitignore
.github
public/system
public/assets
public/packs
@ -11,5 +15,7 @@ vendor/bundle
*.swp
*~
postgres
postgres14
redis
elasticsearch
chart

View File

@ -13,7 +13,7 @@ DB_PORT=5432
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# Optional ElasticSearch configuration
# Optional Elasticsearch configuration
ES_ENABLED=true
ES_HOST=$DATA_ELASTIC_HOST
ES_PORT=9200
@ -202,10 +202,6 @@ 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)
# PAM_CONTROLLED_SERVICE=rpam
# Global OAuth settings (optional) :
# If you have only one strategy, you may want to enable this
# OAUTH_REDIRECT_AT_SIGN_IN=true
# Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/
@ -228,6 +224,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone'
# CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true

View File

@ -29,11 +29,14 @@ DB_NAME=mastodon_production
DB_PASS=
DB_PORT=5432
# ElasticSearch (optional)
# Elasticsearch (optional)
# ------------------------
ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200
# Authentication for ES (optional)
ES_USER=elastic
ES_PASS=password
# Secrets
# -------
@ -55,7 +58,7 @@ SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notificatons@example.com
SMTP_FROM_ADDRESS=notifications@example.com
# File storage (optional)
# -----------------------

View File

@ -79,6 +79,11 @@ module.exports = {
'no-irregular-whitespace': 'error',
'no-mixed-spaces-and-tabs': '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-undef': 'error',
'no-unreachable': 'error',

32
.github/CODEOWNERS vendored
View File

@ -1,32 +0,0 @@
# 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
open_collective: mastodon
github: [Gargron]
custom: https://sponsor.joinmastodon.org

42
.github/ISSUE_TEMPLATE/1.bug_report.yml vendored Normal file
View File

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

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

View File

@ -1,27 +0,0 @@
---
name: Bug Report
about: If something isn't working as expected
labels: bug
---
<!-- Make sure that you are submitting a new bug that was not previously reported or already fixed -->
<!-- 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,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Mastodon Meta Discussion Board
url: https://discourse.joinmastodon.org/
- name: GitHub Discussions
url: https://github.com/mastodon/mastodon/discussions
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

@ -1,16 +0,0 @@
---
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? -->

View File

@ -1,10 +0,0 @@
---
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,7 +6,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 99
@ -14,7 +14,7 @@ updates:
- dependency-type: direct
- package-ecosystem: bundler
directory: "/"
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 99

View File

@ -3,32 +3,40 @@ on:
workflow_dispatch:
push:
branches:
- "main"
- '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=false
latest=auto
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
type=match,pattern=v(.*),group=0
type=ref,event=pr
- uses: docker/build-push-action@v2
with:
context: .
push: true
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

34
.github/workflows/check-i18n.yml vendored Normal file
View File

@ -0,0 +1,34 @@
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,13 +40,12 @@
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres
/postgres14
/redis
/elasticsearch
# ignore Helm lockfile, dependency charts, and local values file
/chart/Chart.lock
# ignore Helm dependency charts
/chart/charts/*.tgz
/chart/values.yaml
# Ignore Apple files
.DS_Store

2
.nvmrc
View File

@ -1 +1 @@
12
14

78
.prettierignore Normal file
View File

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

3
.prettierrc.js Normal file
View File

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

View File

@ -2,7 +2,8 @@ require:
- rubocop-rails
AllCops:
TargetRubyVersion: 2.4
TargetRubyVersion: 2.5
NewCops: disable
Exclude:
- 'spec/**/*'
- 'db/**/*'
@ -28,13 +29,17 @@ Layout/EmptyLineAfterMagicComment:
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/EmptyLineBetweenDefs:
AllowAdjacentOneLineDefs: true
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Layout/HashAlignment:
Enabled: false
# EnforcedHashRocketStyle: table
# EnforcedColonStyle: table
Layout/SpaceAroundMethodCallOperator:
Enabled: true

View File

@ -1 +1 @@
2.7.2
3.0.3

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,8 @@ libicu-dev
libidn11
libidn11-dev
libpq-dev
libprotobuf-dev
libxdamage1
libxfixes3
protobuf-compiler
zlib1g-dev
libcairo2
libcroco3
@ -22,7 +20,7 @@ libpixman-1-0
librsvg2-2
libthai-data
libthai0
libvpx5
libvpx[5-9]
libxcb-render0
libxcb-shm0
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 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.
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.
## Translations
@ -24,9 +24,17 @@ You can submit translations via [Crowdin](https://crowdin.com/project/mastodon).
## Pull requests
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.
**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).
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.
Example:
|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:
@ -36,4 +44,4 @@ The smaller the set of changes in the pull request is, the quicker it can be rev
## 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).
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).

View File

@ -1,10 +1,11 @@
FROM ubuntu:20.04 as build-dep
# Use bash for the shell
SHELL ["bash", "-c"]
SHELL ["/bin/bash", "-c"]
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
# Install Node v12 (LTS)
ENV NODE_VER="12.20.0"
# Install Node v16 (LTS)
ENV NODE_VER="16.14.2"
RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \
@ -17,35 +18,19 @@ RUN ARCH= && \
*) echo "unsupported architecture"; exit 1 ;; \
esac && \
echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget python && \
apt-get update && \
apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
wget -q 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 && \
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install jemalloc
ENV JE_VER="5.2.1"
RUN apt update && \
apt -y install make autoconf gcc g++ && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
cd jemalloc-$JE_VER && \
./autogen.sh && \
./configure --prefix=/opt/jemalloc && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib && \
cd .. && rm -rf jemalloc-$JE_VER $JE_VER.tar.gz
# Install Ruby
ENV RUBY_VER="2.7.2"
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 \
# Install Ruby 3.0
ENV RUBY_VER="3.0.3"
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
@ -55,25 +40,26 @@ RUN apt update && \
--with-jemalloc \
--with-shared \
--disable-install-doc && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
make -j$(nproc) > /dev/null && \
make -j"$(nproc)" > /dev/null && \
make install && \
cd .. && rm -rf ruby-$RUBY_VER.tar.gz ruby-$RUBY_VER
rm -rf ../ruby-$RUBY_VER.tar.gz ../ruby-$RUBY_VER
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \
RUN npm install -g npm@latest && \
npm install -g yarn && \
gem install bundler && \
apt update && \
apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler
apt-get update && \
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
libpq-dev shared-mime-info
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \
bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install -j$(nproc) && \
bundle config set silence_root_warning true && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile
FROM ubuntu:20.04
@ -81,7 +67,6 @@ FROM ubuntu:20.04
# Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node
COPY --from=build-dep /opt/ruby /opt/ruby
COPY --from=build-dep /opt/jemalloc /opt/jemalloc
# Add more PATHs to the PATH
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
@ -89,35 +74,27 @@ ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
# Create the mastodon user
ARG UID=991
ARG GID=991
RUN apt update && \
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && \
echo "Etc/UTC" > /etc/localtime && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt install -y whois wget && \
apt-get install -y --no-install-recommends whois wget && \
addgroup --gid $GID 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
RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline8 && \
apt -y install gcc && \
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
libicu66 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
ln -s /opt/mastodon /mastodon && \
gem install bundler && \
rm -rf /var/cache && \
rm -rf /var/lib/apt/lists/*
# Add tini
ENV TINI_VERSION="0.19.0"
RUN dpkgArch="$(dpkg --print-architecture)" && \
ARCH=$dpkgArch && \
wget https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH \
https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH.sha256sum && \
cat tini-$ARCH.sha256sum | sha256sum -c - && \
mv tini-$ARCH /tini && rm tini-$ARCH.sha256sum && \
chmod +x /tini
# Copy over mastodon source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
@ -140,5 +117,5 @@ RUN cd ~ && \
# Set the work dir and the container entry point
WORKDIR /opt/mastodon
ENTRYPOINT ["/tini", "--"]
ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000 4000

30
FEDERATION.md Normal file
View File

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

136
Gemfile
View File

@ -1,113 +1,110 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 2.5.0', '< 3.0.0'
ruby '>= 2.6.0', '< 3.1.0'
gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
gem 'puma', '~> 5.0'
gem 'rails', '~> 5.2.4.4'
gem 'puma', '~> 5.6'
gem 'rails', '~> 6.1.5'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.0'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.3'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.2'
gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.7'
gem 'pg', '~> 1.3'
gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.85', require: false
gem 'aws-sdk-s3', '~> 1.113', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
gem 'kt-paperclip', '~> 7.1'
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.5', require: false
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.11.1', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.3.0'
gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 3.1'
gem 'chewy', '~> 7.2'
gem 'devise', '~> 4.8'
gem 'devise-two-factor', '~> 4.0'
group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2'
end
gem 'net-ldap', '~> 0.16'
gem 'net-ldap', '~> 0.17'
gem 'omniauth-cas', '~> 2.0'
gem 'omniauth-saml', '~> 1.10'
gem 'gitlab-omniauth-openid-connect', '~>0.9.1', require: 'omniauth_openid_connect'
gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.4'
gem 'ed25519', '~> 1.2'
gem 'doorkeeper', '~> 5.5'
gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.8'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.4'
gem 'http', '~> 5.0'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.4.3'
gem 'httplog', '~> 1.5.0'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10'
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.13'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.10'
gem 'ox', '~> 2.13'
gem 'oj', '~> 3.13'
gem 'ox', '~> 2.14'
gem 'parslet'
gem 'parallel', '~> 1.20'
gem 'posix-spawn'
gem 'pundit', '~> 2.1'
gem 'pundit', '~> 2.2'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.3'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-i18n', '~> 6.0'
gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis']
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 5.2'
gem 'scenic', '~> 1.5'
gem 'sidekiq', '~> 6.1'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'rqrcode', '~> 2.1'
gem 'ruby-progressbar', '~> 1.11'
gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.6'
gem 'sidekiq', '~> 6.4'
gem 'sidekiq-scheduler', '~> 3.2'
gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'simple-navigation', '~> 4.3'
gem 'simple_form', '~> 5.1'
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.1'
gem 'strong_migrations', '~> 0.7'
gem 'tty-prompt', '~> 0.22', require: false
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2020'
gem 'webpacker', '~> 5.2'
gem 'webpush'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2022'
gem 'webpacker', '~> 5.4'
gem 'webpush', '~> 0.3'
gem 'webauthn', '~> 3.0.0.alpha1'
gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.1'
gem 'rdf-normalize', '~> 0.4'
gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5'
group :development, :test do
gem 'fabrication', '~> 2.21'
gem 'fabrication', '~> 2.28'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'i18n-tasks', '~> 1.0', require: false
gem 'pry-byebug', '~> 3.9'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 4.0'
gem 'rspec-rails', '~> 5.1'
end
group :production, :test do
@ -115,33 +112,32 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 3.33'
gem 'capybara', '~> 3.36'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.14'
gem 'faker', '~> 2.20'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.19', require: false
gem 'webmock', '~> 3.10'
gem 'parallel_tests', '~> 3.4'
gem 'rspec_junit_formatter', '~> 0.4'
gem 'simplecov', '~> 0.21', require: false
gem 'webmock', '~> 3.14'
gem 'rspec_junit_formatter', '~> 0.5'
end
group :development do
gem 'active_record_query_trace', '~> 1.8'
gem 'annotate', '~> 3.1'
gem 'annotate', '~> 3.2'
gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4'
gem 'binding_of_caller', '~> 1.0'
gem 'bullet', '~> 7.0'
gem 'letter_opener', '~> 1.8'
gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler'
gem 'rubocop', '~> 1.3', require: false
gem 'rubocop-rails', '~> 2.8', require: false
gem 'brakeman', '~> 4.10', require: false
gem 'bundler-audit', '~> 0.7', require: false
gem 'rubocop', '~> 1.27', require: false
gem 'rubocop-rails', '~> 2.14', require: false
gem 'brakeman', '~> 5.2', require: false
gem 'bundler-audit', '~> 0.9', require: false
gem 'capistrano', '~> 3.14'
gem 'capistrano', '~> 3.17'
gem 'capistrano-rails', '~> 1.6'
gem 'capistrano-rbenv', '~> 2.2'
gem 'capistrano-yarn', '~> 2.0'
@ -150,12 +146,10 @@ group :development do
end
group :production do
gem 'lograge', '~> 0.11'
gem 'redis-rails', '~> 5.0'
gem 'lograge', '~> 0.12'
end
gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'pluck_each', '~> 0.1.3'

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,15 +1,15 @@
![Mastodon](https://i.imgur.com/NhZc40l.png)
========
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/mastodon/mastodon.svg)][code_climate]
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[releases]: https://github.com/mastodon/mastodon/releases
[circleci]: https://circleci.com/gh/mastodon/mastodon
[code_climate]: https://codeclimate.com/github/mastodon/mastodon
[crowdin]: https://crowdin.com/project/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)
- [Blog](https://blog.joinmastodon.org)
- [Documentation](https://docs.joinmastodon.org)
- [Browse Mastodon servers](https://joinmastodon.org/#getting-started)
- [Browse Mastodon servers](https://joinmastodon.org/communities)
- [Browse Mastodon apps](https://joinmastodon.org/apps)
[patreon]: https://www.patreon.com/mastodon
@ -37,56 +37,62 @@ Click below to **learn more** in a video:
<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
See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
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 are looped - like vines!
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!
**Safety and moderation tools**
### Safety and moderation tools
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/)
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/)
**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, resulting 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. This results in a rich app ecosystem with a lot of choices!
## Deployment
**Tech stack:**
### Tech stack:
- **Ruby on Rails** powers the REST API and other web pages
- **React.js** and Redux are used for the dynamic parts of the interface
- **Node.js** powers the streaming API
**Requirements:**
### Requirements:
- **PostgreSQL** 9.5+
- **Redis** 4+
- **Ruby** 2.5+
- **Node.js** 10.13+
- **Node.js** 12+
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
The repository includes deployment configurations for **Docker and docker-compose** 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.
A **Vagrant** configuration is included for development purposes.
A **Vagrant** configuration is included for development purposes. To use it, complete following steps:
- 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
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.freenode.net
**IRC channel**: #mastodon on irc.libera.chat
## License
Copyright (C) 2016-2020 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
Copyright (C) 2016-2022 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.

View File

@ -1,12 +1,20 @@
# 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
| Version | Supported |
| ------- | ------------------ |
| 3.1.x | :white_check_mark: |
| < 3.1 | :x: |
| 3.5.x | Yes |
| 3.4.x | Yes |
| 3.3.x | Yes |
| < 3.3 | No |
## Reporting a Vulnerability
hello@joinmastodon.org
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail

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'
# Add repo for NodeJS
curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -
curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -
# Add firewall rule to redirect 80 to PORT and save
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
@ -33,11 +33,9 @@ sudo apt-get install \
redis-tools \
postgresql \
postgresql-contrib \
protobuf-compiler \
yarn \
libicu-dev \
libidn11-dev \
libprotobuf-dev \
libreadline-dev \
libpam0g-dev \
-y
@ -45,16 +43,8 @@ sudo apt-get install \
# Install rvm
read RUBY_VERSION < .ruby-version
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
$($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://rvm.io/mpapis.asc | gpg --import
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm
@ -72,10 +62,12 @@ bundle install
yarn install
# Build Mastodon
export RAILS_ENV=development
export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup
# Configure automatic loading of environment variable
echo 'export RAILS_ENV=development' >> ~/.bash_profile
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
SCRIPT

View File

@ -1,8 +1,8 @@
{
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite.png",
"repository": "https://github.com/mastodon/mastodon",
"logo": "https://github.com/mastodon.png",
"env": {
"HEROKU": {
"description": "Leave this as true",
@ -95,8 +95,5 @@
"scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
},
"addons": [
"heroku-postgresql",
"heroku-redis"
]
"addons": ["heroku-postgresql", "heroku-redis"]
}

View File

@ -23,7 +23,8 @@ class AccountsIndex < Chewy::Index
},
}
define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? }
root date_detection: false do
field :id, type: 'long'
@ -39,5 +40,4 @@ class AccountsIndex < Chewy::Index
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

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class StatusesIndex < Chewy::Index
include FormattingHelper
settings index: { refresh_interval: '15m' }, analysis: {
filter: {
english_stop: {
@ -31,7 +33,8 @@ class StatusesIndex < Chewy::Index
},
}
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
crutch :mentions do |collection|
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) }
@ -52,15 +55,19 @@ class StatusesIndex < Chewy::Index
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :votes do |collection|
data = ::PollVote.joins(:poll).where(poll: { 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 :text, type: 'text', value: ->(status) { status.searchable_text } do
field :stemmed, type: 'text', analyzer: 'content'
end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
end
end
end

View File

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

View File

@ -20,6 +20,7 @@ class AboutController < ApplicationController
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
@rules = Rule.ordered
@contents = toc_generator.html
@table_of_contents = toc_generator.toc
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?

View File

@ -28,7 +28,7 @@ class AccountsController < ApplicationController
return
end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
@statuses = cached_filtered_status_page
@rss_url = rss_url
@ -64,6 +64,10 @@ class AccountsController < ApplicationController
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end
def filtered_pinned_statuses
@account.pinned_statuses.where(visibility: [:public, :unlisted])
end
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
@ -77,11 +81,7 @@ class AccountsController < ApplicationController
end
def only_media_scope
Status.where(id: account_media_status_ids)
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
end
def no_replies_scope
@ -135,15 +135,22 @@ class AccountsController < ApplicationController
end
def media_requested?
request.path.split('.').first.ends_with?('/media') && !tag_requested?
request.path.split('.').first.end_with?('/media') && !tag_requested?
end
def replies_requested?
request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
request.path.split('.').first.end_with?('/with_replies') && !tag_requested?
end
def tag_requested?
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
def cached_filtered_status_pins
cache_collection(
filtered_pinned_statuses,
Status
)
end
def cached_filtered_status_page

View File

@ -2,6 +2,7 @@
class ActivityPub::BaseController < Api::BaseController
skip_before_action :require_authenticated_user!
skip_around_action :set_locale
private

View File

@ -21,6 +21,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id]
when 'featured'
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
when 'tags'
@items = for_signed_account { @account.featured_tags }
when 'devices'

View File

@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers
def show
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
if 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'
end
@ -20,7 +24,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def outbox_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(
id: outbox_url(page_params),
id: outbox_url(**page_params),
type: :ordered,
part_of: outbox_url,
prev: prev_page,
@ -58,7 +62,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
return unless page_requested?
@statuses = cache_collection_paginated_by_id(
@account.statuses.permitted_for(@account, signed_request_account),
AccountStatusesFilter.new(@account, signed_request_account).results,
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
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

View File

@ -63,15 +63,29 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end
def next_page
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
if only_other_accounts?
# Only consider remote accounts
return nil if @replies.size < DESCENDANTS_LIMIT
account_status_replies_url(
@account,
@status,
page: true,
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
only_other_accounts: only_other_accounts
min_id: @replies&.last&.id,
only_other_accounts: true
)
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
def page_params

View File

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

View File

@ -2,13 +2,24 @@
module Admin
class AccountsController < BaseController
before_action :set_account, except: [:index]
before_action :set_account, except: [:index, :batch]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
def index
authorize :account, :index?
@accounts = filtered_accounts.page(params[:page])
@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
def show
@ -17,7 +28,7 @@ module Admin
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
@domain_block = DomainBlock.rule_for(@account.domain)
end
@ -38,13 +49,13 @@ module Admin
def approve
authorize @account.user, :approve?
@account.user.approve!
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
end
def reject
authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
end
def destroy
@ -106,6 +117,16 @@ module Admin
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
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
private
def set_account
@ -121,11 +142,25 @@ module Admin
end
def filtered_accounts
AccountFilter.new(filter_params).results
AccountFilter.new(filter_params.with_defaults(order: 'recent')).results
end
def filter_params
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
params.slice(:page, *AccountFilter::KEYS).permit(:page, *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

View File

@ -1,55 +1,26 @@
# frozen_string_literal: true
require 'sidekiq/api'
module Admin
class DashboardController < BaseController
include Redisable
def index
@users_count = User.count
@system_checks = Admin::SystemCheck.perform
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
@pending_users_count = User.pending.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_reports_count = Report.unresolved.count
@pending_tags_count = Tag.pending_review.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
@pending_appeals_count = Appeal.pending.count
end
private
def current_week
@current_week ||= Time.now.utc.to_date.cweek
end
def redis_info
@redis_info ||= begin
if Redis.current.is_a?(Redis::Namespace)
Redis.current.redis.info
if redis.is_a?(Redis::Namespace)
redis.redis.info
else
Redis.current.info
redis.info
end
end
end

View File

@ -0,0 +1,40 @@
# 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,7 +22,7 @@ module Admin
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@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
@domain_block.errors[:domain].clear
@domain_block.errors.delete(:domain)
render :new
else
if existing_domain_block.present?
@ -56,10 +56,6 @@ module Admin
end
end
def show
authorize @domain_block, :show?
end
def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block)

View File

@ -6,7 +6,20 @@ module Admin
def index
authorize :email_domain_block, :index?
@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
def new
@ -19,41 +32,27 @@ module Admin
@email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save
if action_from_button == 'save'
EmailDomainBlock.transaction do
@email_domain_block.save!
log_action :create, @email_domain_block
if @email_domain_block.with_dns_records?
hostnames = []
ips = []
(@email_domain_block.other_domains || []).uniq.each do |domain|
next if EmailDomainBlock.where(domain: domain).exists?
Resolv::DNS.open do |dns|
dns.timeouts = 5
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
other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block)
log_action :create, other_email_domain_block
end
end
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else
set_resolved_records
render :new
end
end
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')
rescue ActiveRecord::RecordInvalid
set_resolved_records
render :new
end
private
@ -62,8 +61,27 @@ module Admin
@email_domain_block = EmailDomainBlock.find(params[:id])
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
params.require(:email_domain_block).permit(:domain, :with_dns_records)
params.require(:email_domain_block).permit(:domain, other_domains: [])
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

View File

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

@ -3,14 +3,47 @@
module Admin
class InstancesController < BaseController
before_action :set_instances, only: :index
before_action :set_instance, only: :show
before_action :set_instance, except: :index
def index
authorize :instance, :index?
preload_delivery_failures!
end
def show
authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end
def destroy
authorize :instance, :destroy?
Admin::DomainPurgeWorker.perform_async(@instance.domain)
log_action :destroy, @instance
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain)
end
def clear_delivery_errors
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
private
@ -23,6 +56,14 @@ module Admin
@instances = filtered_instances.page(params[:page])
end
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
def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end

View File

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

View File

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

View File

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

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

View File

@ -6,9 +6,9 @@ module Admin
def create
authorize @user, :reset_password?
@user.send_reset_password_instructions
@user.reset_password!
log_action :reset_password, @user
redirect_to admin_accounts_path
redirect_to admin_account_path(@user.account_id)
end
end
end

View File

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

View File

@ -2,38 +2,12 @@
module Admin
class TagsController < BaseController
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
before_action :set_tag
def show
authorize @tag, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end
def update
@ -52,52 +26,8 @@ module Admin
@tag = Tag.find(params[:id])
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
params.require(:tag).permit(:name, :trendable, :usable, :listable)
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

View File

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

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

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

@ -0,0 +1,41 @@
# 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!
log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_accounts_path
redirect_to admin_account_path(@user.account_id)
end
private

View File

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

View File

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

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

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

View File

@ -0,0 +1,18 @@
# 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)
rescue Addressable::URI::InvalidURIError
raise(ActiveRecord::RecordNotFound)
end
end

View File

@ -22,55 +22,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def cached_account_statuses
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?
cache_collection_paginated_by_id(
statuses,
AccountStatusesFilter.new(@account, current_account, params).results,
Status,
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.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(: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
def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
end
def insert_pagination_headers

View File

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

View File

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

View File

@ -6,8 +6,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff!
before_action :set_accounts, only: :index
before_action :set_account, except: :index
@ -65,8 +65,9 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def destroy
authorize @account, :destroy?
json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer
Admin::AccountDeletionWorker.perform_async(@account.id)
render json: @account, serializer: REST::Admin::AccountSerializer
render json: json
end
def unsensitive
@ -94,7 +95,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
private
def set_accounts
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite, :ips]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_account
@ -102,13 +103,27 @@ class Api::V1::Admin::AccountsController < Api::BaseController
end
def filtered_accounts
AccountFilter.new(filter_params).results
AccountFilter.new(translated_filter_params).results
end
def filter_params
params.permit(*FILTER_PARAMS)
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
set_pagination_headers(next_path, prev_path)
end

View File

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

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

View File

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

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

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

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

View File

@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController
end
def results
@_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id(
@_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)

View File

@ -12,7 +12,7 @@ class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
private
def set_claim_results
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
@claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }
end
def resource_params

View File

@ -17,7 +17,7 @@ class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
end
def set_query_results
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
@query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) }
end
def account_ids

View File

@ -3,8 +3,8 @@
class Api::V1::DomainBlocksController < Api::BaseController
BLOCK_LIMIT = 100
before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }, only: :show
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, except: :show
before_action -> { doorkeeper_authorize! :follow, :read, :'read:blocks' }, only: :show
before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, except: :show
before_action :require_user!
after_action :insert_pagination_headers, only: :show

View File

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

View File

@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController
end
def results
@_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id(
@_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true
class Api::V1::FollowRequestsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :'read:follows' }, only: :index
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, except: :index
before_action -> { doorkeeper_authorize! :follow, :read, :'read:follows' }, only: :index
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, except: :index
before_action :require_user!
after_action :insert_pagination_headers, only: :index
@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize
AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
LocalNotificationWorker.perform_async(current_account.id, Follow.find_by(account: account, target_account: current_account).id, 'Follow', 'follow')
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end
@ -29,7 +29,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
end
def relationships(**options)
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options)
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options)
end
def load_accounts

View File

@ -14,22 +14,21 @@ class Api::V1::Instances::ActivityController < Api::BaseController
private
def activity
weeks = []
statuses_tracker = ActivityTracker.new('activity:statuses:local', :basic)
logins_tracker = ActivityTracker.new('activity:logins', :unique)
registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic)
12.times do |i|
day = i.weeks.ago.to_date
week_id = day.cweek
week = Date.commercial(day.cwyear, week_id)
(0...12).map do |i|
start_of_week = i.weeks.ago
end_of_week = start_of_week + 6.days
weeks << {
week: week.to_time.to_i.to_s,
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
{
week: start_of_week.to_i.to_s,
statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s,
logins: logins_tracker.sum(start_of_week, end_of_week).to_s,
registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s,
}
end
weeks
end
def require_enabled_api!

View File

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

View File

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

View File

@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
end
def update
@media_attachment.update!(media_attachment_params)
@media_attachment.update!(updateable_media_attachment_params)
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
@ -31,7 +31,7 @@ class Api::V1::MediaController < Api::BaseController
end
def set_media_attachment
@media_attachment = current_account.media_attachments.unattached.find(params[:id])
@media_attachment = current_account.media_attachments.where(status_id: nil).find(params[:id])
end
def check_processing
@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
params.permit(:file, :thumbnail, :description, :focus)
end
def updateable_media_attachment_params
params.permit(:thumbnail, :description, :focus)
end
def file_type_error
{ error: 'File type of uploaded media could not be verified' }
end

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