Merge branch 'main' of https://github.com/glitch-soc/mastodon
commit
5e26274583
|
@ -1,255 +1,189 @@
|
||||||
version: 2
|
version: 2.1
|
||||||
|
|
||||||
aliases:
|
orbs:
|
||||||
- &defaults
|
ruby: circleci/ruby@1.2.0
|
||||||
|
node: circleci/node@4.7.0
|
||||||
|
|
||||||
|
executors:
|
||||||
|
default:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.7-buster-node
|
- image: cimg/ruby:<< parameters.ruby-version >>
|
||||||
environment: &ruby_environment
|
environment:
|
||||||
BUNDLE_JOBS: 3
|
BUNDLE_JOBS: 3
|
||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
BUNDLE_APP_CONFIG: ./.bundle/
|
CONTINUOUS_INTEGRATION: true
|
||||||
BUNDLE_PATH: ./vendor/bundle/
|
|
||||||
DB_HOST: localhost
|
DB_HOST: localhost
|
||||||
DB_USER: root
|
DB_USER: root
|
||||||
RAILS_ENV: test
|
|
||||||
ALLOW_NOPAM: true
|
|
||||||
CONTINUOUS_INTEGRATION: true
|
|
||||||
DISABLE_SIMPLECOV: true
|
DISABLE_SIMPLECOV: true
|
||||||
PAM_ENABLED: true
|
RAILS_ENV: test
|
||||||
PAM_DEFAULT_SERVICE: pam_test
|
- image: cimg/postgres:14.0
|
||||||
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
environment:
|
||||||
working_directory: ~/projects/mastodon/
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
- image: circleci/redis:6-alpine
|
||||||
|
|
||||||
- &attach_workspace
|
commands:
|
||||||
attach_workspace:
|
install-system-dependencies:
|
||||||
at: ~/projects/
|
steps:
|
||||||
|
- 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:
|
||||||
|
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.2.31'
|
||||||
|
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
|
||||||
|
|
||||||
- &persist_to_workspace
|
jobs:
|
||||||
persist_to_workspace:
|
build:
|
||||||
root: ~/projects/
|
docker:
|
||||||
paths:
|
- image: cimg/ruby:3.0-node
|
||||||
- ./mastodon/
|
environment:
|
||||||
|
RAILS_ENV: test
|
||||||
- &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:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- install-system-dependencies
|
||||||
- restore_cache:
|
- install-ruby-dependencies:
|
||||||
keys:
|
ruby-version: '3.0'
|
||||||
- v2-node-dependencies-{{ checksum "yarn.lock" }}
|
- node/install-packages:
|
||||||
- v2-node-dependencies-
|
cache-version: v1
|
||||||
|
pkg-manager: yarn
|
||||||
- run:
|
- 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
|
|
||||||
|
|
||||||
install-ruby3.0:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/ruby:3.0-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
|
command: ./bin/rails assets:precompile
|
||||||
|
name: Precompile assets
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/projects/
|
|
||||||
paths:
|
paths:
|
||||||
- ./mastodon/public/assets
|
- public/assets
|
||||||
- ./mastodon/public/packs-test/
|
- 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:
|
test-migrations:
|
||||||
<<: *defaults
|
executor:
|
||||||
docker:
|
name: default
|
||||||
- image: circleci/ruby:2.7-buster-node
|
ruby-version: '3.0'
|
||||||
environment: *ruby_environment
|
|
||||||
- image: circleci/postgres:12.2
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: root
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
- image: circleci/redis:5-alpine
|
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- checkout
|
||||||
- *install_system_dependencies
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- wait-db
|
||||||
- run:
|
- run:
|
||||||
name: Create database
|
|
||||||
command: ./bin/rails db:create
|
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:
|
- run:
|
||||||
name: Run migrations
|
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all remaining migrations
|
||||||
|
|
||||||
test-ruby2.7:
|
test-two-step-migrations:
|
||||||
<<: *defaults
|
executor:
|
||||||
docker:
|
name: default
|
||||||
- image: circleci/ruby:2.7-buster-node
|
ruby-version: '3.0'
|
||||||
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-ruby3.0:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/ruby:3.0-buster-node
|
|
||||||
environment: *ruby_environment
|
|
||||||
- image: circleci/postgres:12.2
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: root
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
- image: circleci/redis:5-alpine
|
|
||||||
<<: *test_steps
|
|
||||||
|
|
||||||
test-webui:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:14-buster
|
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- wait-db
|
||||||
- run:
|
- run:
|
||||||
name: Run jest
|
command: ./bin/rails db:create
|
||||||
command: yarn test:jest
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all pre-deployment migrations
|
||||||
|
evironment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all post-deployment remaining migrations
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
build-and-test:
|
build-and-test:
|
||||||
jobs:
|
jobs:
|
||||||
- install
|
- build
|
||||||
- install-ruby2.7:
|
- test:
|
||||||
|
matrix:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
- '2.7'
|
||||||
|
- '3.0'
|
||||||
|
name: test-ruby<< matrix.ruby-version >>
|
||||||
requires:
|
requires:
|
||||||
- install
|
- build
|
||||||
- install-ruby2.6:
|
|
||||||
requires:
|
|
||||||
- install
|
|
||||||
- install-ruby2.7
|
|
||||||
- install-ruby3.0:
|
|
||||||
requires:
|
|
||||||
- install
|
|
||||||
- install-ruby2.7
|
|
||||||
- build:
|
|
||||||
requires:
|
|
||||||
- install-ruby2.7
|
|
||||||
- test-migrations:
|
- test-migrations:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.7
|
|
||||||
- test-ruby2.7:
|
|
||||||
requires:
|
|
||||||
- install-ruby2.7
|
|
||||||
- build
|
- build
|
||||||
- test-ruby2.6:
|
- test-two-step-migrations:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.6
|
|
||||||
- build
|
- build
|
||||||
- test-ruby3.0:
|
- node/run:
|
||||||
|
cache-version: v1
|
||||||
|
name: test-webui
|
||||||
|
pkg-manager: yarn
|
||||||
requires:
|
requires:
|
||||||
- install-ruby3.0
|
|
||||||
- build
|
- build
|
||||||
- test-webui:
|
version: lts
|
||||||
requires:
|
yarn-run: test:jest
|
||||||
- install
|
|
||||||
|
|
|
@ -35,4 +35,7 @@ plugins:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- spec/
|
- spec/
|
||||||
- vendor/asset
|
- vendor/asset/
|
||||||
|
|
||||||
|
- app/javascript/mastodon/locales/**/*.json
|
||||||
|
- config/locales/**/*.yml
|
||||||
|
|
|
@ -15,6 +15,7 @@ vendor/bundle
|
||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
postgres
|
postgres
|
||||||
|
postgres14
|
||||||
redis
|
redis
|
||||||
elasticsearch
|
elasticsearch
|
||||||
chart
|
chart
|
||||||
|
|
|
@ -13,7 +13,7 @@ DB_PORT=5432
|
||||||
|
|
||||||
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
|
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
|
||||||
|
|
||||||
# Optional ElasticSearch configuration
|
# Optional Elasticsearch configuration
|
||||||
ES_ENABLED=true
|
ES_ENABLED=true
|
||||||
ES_HOST=$DATA_ELASTIC_HOST
|
ES_HOST=$DATA_ELASTIC_HOST
|
||||||
ES_PORT=9200
|
ES_PORT=9200
|
||||||
|
@ -202,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)
|
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
|
||||||
# PAM_CONTROLLED_SERVICE=rpam
|
# PAM_CONTROLLED_SERVICE=rpam
|
||||||
|
|
||||||
# Global OAuth settings (optional) :
|
|
||||||
# If you have only one strategy, you may want to enable this
|
|
||||||
# OAUTH_REDIRECT_AT_SIGN_IN=true
|
|
||||||
|
|
||||||
# Optional CAS authentication (cf. omniauth-cas) :
|
# Optional CAS authentication (cf. omniauth-cas) :
|
||||||
# CAS_ENABLED=true
|
# CAS_ENABLED=true
|
||||||
# CAS_URL=https://sso.myserver.com/
|
# CAS_URL=https://sso.myserver.com/
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
# not demonstrate all available configuration options. Please look at
|
# not demonstrate all available configuration options. Please look at
|
||||||
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
||||||
|
|
||||||
|
# Note that this file accepts slightly different syntax depending on whether
|
||||||
|
# you are using `docker-compose` or not. In particular, if you use
|
||||||
|
# `docker-compose`, the value of each declared variable will be taken verbatim,
|
||||||
|
# including surrounding quotes.
|
||||||
|
# See: https://github.com/mastodon/mastodon/issues/16895
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
# ----------
|
# ----------
|
||||||
# This identifies your server and cannot be changed safely later
|
# This identifies your server and cannot be changed safely later
|
||||||
|
@ -50,11 +56,14 @@ DB_PASS=
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
|
|
||||||
|
|
||||||
# ElasticSearch (optional)
|
# Elasticsearch (optional)
|
||||||
# ------------------------
|
# ------------------------
|
||||||
#ES_ENABLED=true
|
#ES_ENABLED=true
|
||||||
#ES_HOST=localhost
|
#ES_HOST=localhost
|
||||||
#ES_PORT=9200
|
#ES_PORT=9200
|
||||||
|
# Authentication for ES (optional)
|
||||||
|
#ES_USER=elastic
|
||||||
|
#ES_PASS=password
|
||||||
|
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
|
@ -276,3 +285,7 @@ MAX_POLL_OPTION_CHARS=100
|
||||||
# Units are in bytes
|
# Units are in bytes
|
||||||
MAX_EMOJI_SIZE=51200
|
MAX_EMOJI_SIZE=51200
|
||||||
MAX_REMOTE_EMOJI_SIZE=204800
|
MAX_REMOTE_EMOJI_SIZE=204800
|
||||||
|
|
||||||
|
# Optional hCaptcha support
|
||||||
|
# HCAPTCHA_SECRET_KEY=
|
||||||
|
# HCAPTCHA_SITE_KEY=
|
||||||
|
|
|
@ -8,6 +8,17 @@ body:
|
||||||
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||||
|
|
||||||
Please use a concise and distinct title for the issue.
|
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
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Expected behaviour
|
label: Expected behaviour
|
||||||
|
@ -20,17 +31,6 @@ body:
|
||||||
description: What happened?
|
description: What happened?
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce the problem
|
|
||||||
description: What were you trying to do?
|
|
||||||
value: |
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Specifications
|
label: Specifications
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: Build container image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: docker/metadata-action@v3
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
|
flavor: |
|
||||||
|
latest=true
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=main
|
||||||
|
type=semver,pattern={{ raw }}
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest
|
||||||
|
cache-to: type=inline
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '2.7'
|
ruby-version: '3.0'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
- name: Check locale file normalization
|
- name: Check locale file normalization
|
||||||
run: bundle exec i18n-tasks check-normalized
|
run: bundle exec i18n-tasks check-normalized
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
|
|
||||||
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
||||||
/postgres
|
/postgres
|
||||||
|
/postgres14
|
||||||
/redis
|
/redis
|
||||||
/elasticsearch
|
/elasticsearch
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2.7.4
|
3.0.3
|
||||||
|
|
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -3,6 +3,95 @@ Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [3.4.6] - 2022-02-03
|
||||||
|
### Fixed
|
||||||
|
- Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338))
|
||||||
|
- Fix spurious errors when receiving an Add activity for a private post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17425))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Fix error-prone SQL queries ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15828))
|
||||||
|
- Fix not compacting incoming signed JSON-LD activities ([puckipedia](https://github.com/mastodon/mastodon/pull/17426), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17428)) (CVE-2022-24307)
|
||||||
|
- Fix insufficient sanitization of report comments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17430))
|
||||||
|
- Fix stop condition of a Common Table Expression ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17427))
|
||||||
|
- Disable legacy XSS filtering ([Wonderfall](https://github.com/mastodon/mastodon/pull/17289))
|
||||||
|
|
||||||
|
## [3.4.5] - 2022-01-31
|
||||||
|
### Added
|
||||||
|
- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393))
|
||||||
|
- Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394))
|
||||||
|
- Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398))
|
||||||
|
- Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188))
|
||||||
|
- Fix Ruby 3.0 dependencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16723))
|
||||||
|
- Fix followers synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16510))
|
||||||
|
|
||||||
|
## [3.4.4] - 2021-11-26
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036))
|
||||||
|
- Fix overflow of long profile fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17010))
|
||||||
|
- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986))
|
||||||
|
- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998))
|
||||||
|
- Fix logout link not working in Safari ([noellabo](https://github.com/mastodon/mastodon/pull/16574))
|
||||||
|
- Fix “open” link of media modal not closing modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16524))
|
||||||
|
- Fix replying from modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16516))
|
||||||
|
- Fix `mastodon:setup` command crashing in some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042))
|
||||||
|
- Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041))
|
||||||
|
|
||||||
|
## [3.4.3] - 2021-11-06
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix login being broken due to inaccurately applied backport fix in 3.4.2 ([Gargron](https://github.com/mastodon/mastodon/commit/5c47a18c8df3231aa25c6d1f140a71a7fac9cbf9))
|
||||||
|
|
||||||
|
## [3.4.2] - 2021-11-06
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `configuration` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/16485))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix handling of back button with modal windows in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16499))
|
||||||
|
- Fix pop-in player when author has long username in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16468))
|
||||||
|
- Fix crash when a status with a playing video gets deleted in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16384))
|
||||||
|
- Fix crash with Microsoft Translate in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16525))
|
||||||
|
- Fix PWA not being usable from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714))
|
||||||
|
- Fix locale-specific number rounding errors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16469))
|
||||||
|
- Fix scheduling a status decreasing status count ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16791))
|
||||||
|
- Fix user's canonical email address being blocked when user deletes own account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16503))
|
||||||
|
- Fix not being able to suspend users that already have their canonical e-mail blocked ([Gargron](https://github.com/mastodon/mastodon/pull/16455))
|
||||||
|
- Fix anonymous access to outbox not being cached by the reverse proxy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16458))
|
||||||
|
- Fix followers synchronization mechanism not working when URI has empty path ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16744))
|
||||||
|
- Fix serialization of counts in REST API when user hides their network ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16418))
|
||||||
|
- Fix inefficiencies in auto-linking code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16506))
|
||||||
|
- Fix `tootctl self-destruct` not sending delete activities for recently-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16688))
|
||||||
|
- Fix suspicious sign-in e-mail text being out of date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16690))
|
||||||
|
- Fix some frameworks being unnecessarily loaded ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16725))
|
||||||
|
- Fix canonical e-mail blocks missing foreign key constraints ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16448))
|
||||||
|
- Fix inconsistent order on account's statuses page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/16937))
|
||||||
|
- Fix media from blocked domains being redownloaded by `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/16914))
|
||||||
|
- Fix `mastodon:setup` generated env-file syntax ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16896))
|
||||||
|
- Fix link previews being incorrectly generated from earlier links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16885))
|
||||||
|
- Fix wrong `to`/`cc` values for remote groups in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16700))
|
||||||
|
- Fix mentions with non-ascii TLDs not being processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16689))
|
||||||
|
- Fix authentication failures halfway through a sign-in attempt ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16792))
|
||||||
|
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
|
||||||
|
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
|
||||||
|
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
|
||||||
|
- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
|
||||||
|
- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
|
||||||
|
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix user notes not having a length limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16942))
|
||||||
|
- Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||||
|
|
||||||
## [3.4.1] - 2021-06-03
|
## [3.4.1] - 2021-06-03
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
22
Dockerfile
22
Dockerfile
|
@ -2,9 +2,10 @@ FROM ubuntu:20.04 as build-dep
|
||||||
|
|
||||||
# Use bash for the shell
|
# Use bash for the shell
|
||||||
SHELL ["/bin/bash", "-c"]
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
|
|
||||||
# Install Node v14 (LTS)
|
# Install Node v16 (LTS)
|
||||||
ENV NODE_VER="14.17.6"
|
ENV NODE_VER="16.13.2"
|
||||||
RUN ARCH= && \
|
RUN ARCH= && \
|
||||||
dpkgArch="$(dpkg --print-architecture)" && \
|
dpkgArch="$(dpkg --print-architecture)" && \
|
||||||
case "${dpkgArch##*-}" in \
|
case "${dpkgArch##*-}" in \
|
||||||
|
@ -18,15 +19,15 @@ RUN ARCH= && \
|
||||||
esac && \
|
esac && \
|
||||||
echo "Etc/UTC" > /etc/localtime && \
|
echo "Etc/UTC" > /etc/localtime && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ca-certificates wget python && \
|
apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
|
||||||
cd ~ && \
|
cd ~ && \
|
||||||
wget -q 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 && \
|
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||||
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||||
|
|
||||||
# Install Ruby
|
# Install Ruby 3.0
|
||||||
ENV RUBY_VER="2.7.4"
|
ENV RUBY_VER="3.0.3"
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
apt-get install -y --no-install-recommends build-essential \
|
||||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||||
|
@ -45,7 +46,8 @@ RUN apt-get update && \
|
||||||
|
|
||||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
||||||
|
|
||||||
RUN npm install -g yarn && \
|
RUN npm install -g npm@latest && \
|
||||||
|
npm install -g yarn && \
|
||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
||||||
|
@ -54,8 +56,9 @@ RUN npm install -g yarn && \
|
||||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||||
|
|
||||||
RUN cd /opt/mastodon && \
|
RUN cd /opt/mastodon && \
|
||||||
bundle config set deployment 'true' && \
|
bundle config set --local deployment 'true' && \
|
||||||
bundle config set without 'development test' && \
|
bundle config set --local without 'development test' && \
|
||||||
|
bundle config set silence_root_warning true && \
|
||||||
bundle install -j"$(nproc)" && \
|
bundle install -j"$(nproc)" && \
|
||||||
yarn install --pure-lockfile
|
yarn install --pure-lockfile
|
||||||
|
|
||||||
|
@ -81,11 +84,12 @@ RUN apt-get update && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install mastodon runtime deps
|
# Install mastodon runtime deps
|
||||||
|
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -y --no-install-recommends install \
|
apt-get -y --no-install-recommends install \
|
||||||
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
||||||
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
||||||
file ca-certificates tzdata libreadline8 gcc tini && \
|
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
|
||||||
ln -s /opt/mastodon /mastodon && \
|
ln -s /opt/mastodon /mastodon && \
|
||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
rm -rf /var/cache && \
|
rm -rf /var/cache && \
|
||||||
|
|
|
@ -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
|
51
Gemfile
51
Gemfile
|
@ -4,20 +4,21 @@ source 'https://rubygems.org'
|
||||||
ruby '>= 2.5.0', '< 3.1.0'
|
ruby '>= 2.5.0', '< 3.1.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
|
gem 'rexml', '~> 3.2'
|
||||||
|
|
||||||
gem 'puma', '~> 5.4'
|
gem 'puma', '~> 5.5'
|
||||||
gem 'rails', '~> 6.1.4'
|
gem 'rails', '~> 6.1.4'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.1'
|
gem 'thor', '~> 1.2'
|
||||||
gem 'rack', '~> 2.2.3'
|
gem 'rack', '~> 2.2.3'
|
||||||
|
|
||||||
gem 'hamlit-rails', '~> 0.2'
|
gem 'hamlit-rails', '~> 0.2'
|
||||||
gem 'pg', '~> 1.2'
|
gem 'pg', '~> 1.3'
|
||||||
gem 'makara', '~> 0.5'
|
gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.8'
|
gem 'pghero', '~> 2.8'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.103', require: false
|
gem 'aws-sdk-s3', '~> 1.111', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.0'
|
gem 'kt-paperclip', '~> 7.0'
|
||||||
|
@ -25,12 +26,12 @@ gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.9.1', require: false
|
gem 'bootsnap', '~> 1.10.2', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'chewy', '~> 5.2'
|
gem 'chewy', '~> 7.2'
|
||||||
gem 'cld3', '~> 3.4.2'
|
gem 'cld3', '~> 3.4.4'
|
||||||
gem 'devise', '~> 4.8'
|
gem 'devise', '~> 4.8'
|
||||||
gem 'devise-two-factor', '~> 4.0'
|
gem 'devise-two-factor', '~> 4.0'
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
||||||
gem 'color_diff', '~> 0.1'
|
gem 'color_diff', '~> 0.1'
|
||||||
gem 'discard', '~> 1.2'
|
gem 'discard', '~> 1.2'
|
||||||
gem 'doorkeeper', '~> 5.5'
|
gem 'doorkeeper', '~> 5.5'
|
||||||
gem 'ed25519', '~> 1.2'
|
gem 'ed25519', '~> 1.3'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
|
@ -59,13 +60,12 @@ gem 'httplog', '~> 1.5.0'
|
||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||||
gem 'nokogiri', '~> 1.12'
|
gem 'nokogiri', '~> 1.13'
|
||||||
gem 'nsa', '~> 0.2'
|
gem 'nsa', '~> 0.2'
|
||||||
gem 'oj', '~> 3.13'
|
gem 'oj', '~> 3.13'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
gem 'parallel', '~> 1.21'
|
|
||||||
gem 'posix-spawn'
|
gem 'posix-spawn'
|
||||||
gem 'pundit', '~> 2.1'
|
gem 'pundit', '~> 2.1'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
|
@ -73,19 +73,19 @@ gem 'rack-attack', '~> 6.5'
|
||||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||||
gem 'rails-i18n', '~> 6.0'
|
gem 'rails-i18n', '~> 6.0'
|
||||||
gem 'rails-settings-cached', '~> 0.6'
|
gem 'rails-settings-cached', '~> 0.6'
|
||||||
gem 'redis', '~> 4.4', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'rqrcode', '~> 2.1'
|
gem 'rqrcode', '~> 2.1'
|
||||||
gem 'ruby-progressbar', '~> 1.11'
|
gem 'ruby-progressbar', '~> 1.11'
|
||||||
gem 'sanitize', '~> 6.0'
|
gem 'sanitize', '~> 6.0'
|
||||||
gem 'scenic', '~> 1.5'
|
gem 'scenic', '~> 1.5'
|
||||||
gem 'sidekiq', '~> 6.2'
|
gem 'sidekiq', '~> 6.4'
|
||||||
gem 'sidekiq-scheduler', '~> 3.1'
|
gem 'sidekiq-scheduler', '~> 3.1'
|
||||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||||
gem 'sidekiq-bulk', '~>0.2.0'
|
gem 'sidekiq-bulk', '~>0.2.0'
|
||||||
gem 'simple-navigation', '~> 4.3'
|
gem 'simple-navigation', '~> 4.3'
|
||||||
gem 'simple_form', '~> 5.1'
|
gem 'simple_form', '~> 5.1'
|
||||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.2.1'
|
gem 'stoplight', '~> 2.2.1'
|
||||||
gem 'strong_migrations', '~> 0.7'
|
gem 'strong_migrations', '~> 0.7'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
|
@ -96,13 +96,13 @@ gem 'webpush', '~> 0.3'
|
||||||
gem 'webauthn', '~> 3.0.0.alpha1'
|
gem 'webauthn', '~> 3.0.0.alpha1'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
gem 'json-ld-preloaded', '~> 3.1'
|
gem 'json-ld-preloaded', '~> 3.2'
|
||||||
gem 'rdf-normalize', '~> 0.4'
|
gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
gem 'redcarpet', '~> 3.5'
|
gem 'redcarpet', '~> 3.5'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.22'
|
gem 'fabrication', '~> 2.24'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.9'
|
gem 'pry-byebug', '~> 3.9'
|
||||||
|
@ -115,7 +115,7 @@ group :production, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.35'
|
gem 'capybara', '~> 3.36'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.19'
|
gem 'faker', '~> 2.19'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
|
@ -123,8 +123,7 @@ group :test do
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.21', require: false
|
gem 'simplecov', '~> 0.21', require: false
|
||||||
gem 'webmock', '~> 3.14'
|
gem 'webmock', '~> 3.14'
|
||||||
gem 'parallel_tests', '~> 3.7'
|
gem 'rspec_junit_formatter', '~> 0.5'
|
||||||
gem 'rspec_junit_formatter', '~> 0.4'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
@ -132,13 +131,13 @@ group :development do
|
||||||
gem 'annotate', '~> 3.1'
|
gem 'annotate', '~> 3.1'
|
||||||
gem 'better_errors', '~> 2.9'
|
gem 'better_errors', '~> 2.9'
|
||||||
gem 'binding_of_caller', '~> 1.0'
|
gem 'binding_of_caller', '~> 1.0'
|
||||||
gem 'bullet', '~> 6.1'
|
gem 'bullet', '~> 7.0'
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 2.0'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 1.21', require: false
|
gem 'rubocop', '~> 1.25', require: false
|
||||||
gem 'rubocop-rails', '~> 2.12', require: false
|
gem 'rubocop-rails', '~> 2.13', require: false
|
||||||
gem 'brakeman', '~> 5.1', require: false
|
gem 'brakeman', '~> 5.2', require: false
|
||||||
gem 'bundler-audit', '~> 0.9', require: false
|
gem 'bundler-audit', '~> 0.9', require: false
|
||||||
|
|
||||||
gem 'capistrano', '~> 3.16'
|
gem 'capistrano', '~> 3.16'
|
||||||
|
@ -157,3 +156,5 @@ gem 'concurrent-ruby', require: false
|
||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
|
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
|
|
||||||
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
396
Gemfile.lock
396
Gemfile.lock
|
@ -1,66 +1,66 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.4.1)
|
actioncable (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.4.1)
|
actionmailbox (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activestorage (= 6.1.4.1)
|
activestorage (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.4.1)
|
actionmailer (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
actionview (= 6.1.4.1)
|
actionview (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.4.1)
|
actionpack (6.1.4.4)
|
||||||
actionview (= 6.1.4.1)
|
actionview (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.4.1)
|
actiontext (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activestorage (= 6.1.4.1)
|
activestorage (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.4.1)
|
actionview (6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_model_serializers (0.10.12)
|
active_model_serializers (0.10.13)
|
||||||
actionpack (>= 4.1, < 6.2)
|
actionpack (>= 4.1, < 7.1)
|
||||||
activemodel (>= 4.1, < 6.2)
|
activemodel (>= 4.1, < 7.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.4.1)
|
activejob (6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.4.1)
|
activemodel (6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
activerecord (6.1.4.1)
|
activerecord (6.1.4.4)
|
||||||
activemodel (= 6.1.4.1)
|
activemodel (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
activestorage (6.1.4.1)
|
activestorage (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.4.1)
|
activesupport (6.1.4.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
@ -79,17 +79,17 @@ GEM
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.503.0)
|
aws-partitions (1.549.0)
|
||||||
aws-sdk-core (3.121.0)
|
aws-sdk-core (3.125.5)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.48.0)
|
aws-sdk-kms (1.53.0)
|
||||||
aws-sdk-core (~> 3, >= 3.120.0)
|
aws-sdk-core (~> 3, >= 3.125.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.103.0)
|
aws-sdk-s3 (1.111.3)
|
||||||
aws-sdk-core (~> 3, >= 3.120.0)
|
aws-sdk-core (~> 3, >= 3.125.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.4.0)
|
aws-sigv4 (1.4.0)
|
||||||
|
@ -104,15 +104,15 @@ GEM
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.5)
|
blurhash (0.1.5)
|
||||||
ffi (~> 1.14)
|
ffi (~> 1.14)
|
||||||
bootsnap (1.9.1)
|
bootsnap (1.10.2)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.1.1)
|
brakeman (5.2.0)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
brpoplpush-redis_script (0.1.2)
|
brpoplpush-redis_script (0.1.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
redis (>= 1.0, <= 5.0)
|
redis (>= 1.0, <= 5.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.5)
|
bullet (7.0.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundler-audit (0.9.0.1)
|
bundler-audit (0.9.0.1)
|
||||||
|
@ -134,8 +134,9 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.35.3)
|
capybara (3.36.0)
|
||||||
addressable
|
addressable
|
||||||
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
|
@ -146,12 +147,12 @@ GEM
|
||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.6)
|
cbor (0.5.9.6)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (5.2.0)
|
chewy (7.2.3)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 2.0.0)
|
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cld3 (3.4.2)
|
cld3 (3.4.4)
|
||||||
ffi (>= 1.1.0, < 1.16.0)
|
ffi (>= 1.1.0, < 1.16.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
|
@ -167,7 +168,7 @@ GEM
|
||||||
css_parser (1.7.1)
|
css_parser (1.7.1)
|
||||||
addressable
|
addressable
|
||||||
debug_inspector (1.0.0)
|
debug_inspector (1.0.0)
|
||||||
devise (4.8.0)
|
devise (4.8.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
|
@ -183,26 +184,26 @@ GEM
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.4.4)
|
diff-lcs (1.4.4)
|
||||||
discard (1.2.0)
|
discard (1.2.1)
|
||||||
activerecord (>= 4.2, < 7)
|
activerecord (>= 4.2, < 8)
|
||||||
docile (1.3.4)
|
docile (1.3.4)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.5.3)
|
doorkeeper (5.5.4)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
ed25519 (1.2.4)
|
ed25519 (1.3.0)
|
||||||
elasticsearch (7.10.1)
|
elasticsearch (7.13.3)
|
||||||
elasticsearch-api (= 7.10.1)
|
elasticsearch-api (= 7.13.3)
|
||||||
elasticsearch-transport (= 7.10.1)
|
elasticsearch-transport (= 7.13.3)
|
||||||
elasticsearch-api (7.10.1)
|
elasticsearch-api (7.13.3)
|
||||||
multi_json
|
multi_json
|
||||||
elasticsearch-dsl (0.1.9)
|
elasticsearch-dsl (0.1.10)
|
||||||
elasticsearch-transport (7.10.1)
|
elasticsearch-transport (7.13.3)
|
||||||
faraday (~> 1)
|
faraday (~> 1)
|
||||||
multi_json
|
multi_json
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
|
@ -210,17 +211,31 @@ GEM
|
||||||
et-orbi (1.2.4)
|
et-orbi (1.2.4)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.76.0)
|
excon (0.76.0)
|
||||||
fabrication (2.22.0)
|
fabrication (2.24.0)
|
||||||
faker (2.19.0)
|
faker (2.19.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.3.0)
|
faraday (1.8.0)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0.1)
|
||||||
faraday-net_http (~> 1.0)
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.1)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ruby2_keywords
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.2.5)
|
fastimage (2.2.6)
|
||||||
ffi (1.15.4)
|
ffi (1.15.5)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
|
@ -243,7 +258,7 @@ GEM
|
||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.5.2)
|
globalid (1.0.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
hamlit (2.13.0)
|
hamlit (2.13.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
|
@ -254,15 +269,15 @@ GEM
|
||||||
activesupport (>= 4.0.1)
|
activesupport (>= 4.0.1)
|
||||||
hamlit (>= 1.2.0)
|
hamlit (>= 1.2.0)
|
||||||
railties (>= 4.0.1)
|
railties (>= 4.0.1)
|
||||||
hamster (3.0.0)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashie (4.1.0)
|
hashie (4.1.0)
|
||||||
|
hcaptcha (7.1.0)
|
||||||
|
json
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.0.2)
|
http (5.0.4)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 2.2)
|
http-form_data (~> 2.2)
|
||||||
|
@ -274,9 +289,9 @@ GEM
|
||||||
httplog (1.5.0)
|
httplog (1.5.0)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.8.10)
|
i18n (1.8.11)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.34)
|
i18n-tasks (0.9.37)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
erubi
|
erubi
|
||||||
|
@ -286,37 +301,37 @@ GEM
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.2)
|
idn-ruby (0.1.4)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
iso-639 (0.3.5)
|
iso-639 (0.3.5)
|
||||||
jmespath (1.4.0)
|
jmespath (1.5.0)
|
||||||
json (2.5.1)
|
json (2.5.1)
|
||||||
json-canonicalization (0.2.1)
|
json-canonicalization (0.3.0)
|
||||||
json-ld (3.1.10)
|
json-ld (3.2.0)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 0.2)
|
json-canonicalization (~> 0.3)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
multi_json (~> 1.14)
|
multi_json (~> 1.15)
|
||||||
rack (~> 2.0)
|
rack (~> 2.2)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.2)
|
||||||
json-ld-preloaded (3.1.6)
|
json-ld-preloaded (3.2.0)
|
||||||
json-ld (~> 3.1)
|
json-ld (~> 3.2)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.2)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.2.2)
|
jwt (2.2.2)
|
||||||
kaminari (1.2.1)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.1)
|
kaminari-actionview (= 1.2.2)
|
||||||
kaminari-activerecord (= 1.2.1)
|
kaminari-activerecord (= 1.2.2)
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-actionview (1.2.1)
|
kaminari-actionview (1.2.2)
|
||||||
actionview
|
actionview
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-activerecord (1.2.1)
|
kaminari-activerecord (1.2.2)
|
||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.1)
|
kaminari-core (1.2.2)
|
||||||
kt-paperclip (7.0.0)
|
kt-paperclip (7.0.1)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
marcel (~> 1.0.1)
|
marcel (~> 1.0.1)
|
||||||
|
@ -326,10 +341,11 @@ GEM
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
letter_opener_web (1.4.0)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 3.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.0)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 3.2)
|
railties (>= 5.2)
|
||||||
|
rexml
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
|
@ -339,7 +355,7 @@ GEM
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.12.0)
|
loofah (2.13.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -349,18 +365,19 @@ GEM
|
||||||
marcel (1.0.1)
|
marcel (1.0.1)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
|
matrix (0.4.2)
|
||||||
memory_profiler (1.0.0)
|
memory_profiler (1.0.0)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
microformats (4.3.1)
|
microformats (4.3.1)
|
||||||
json (~> 2.2)
|
json (~> 2.2)
|
||||||
nokogiri (~> 1.10)
|
nokogiri (~> 1.10)
|
||||||
mime-types (3.3.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2020.0512)
|
mime-types-data (3.2021.1115)
|
||||||
mini_mime (1.1.1)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.6.1)
|
mini_portile2 (2.7.1)
|
||||||
minitest (5.14.4)
|
minitest (5.15.0)
|
||||||
msgpack (1.4.2)
|
msgpack (1.4.4)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-ldap (0.17.0)
|
net-ldap (0.17.0)
|
||||||
|
@ -368,15 +385,15 @@ GEM
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.12.5)
|
nokogiri (1.13.1)
|
||||||
mini_portile2 (~> 2.6.1)
|
mini_portile2 (~> 2.7.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
activesupport (>= 4.2, < 7)
|
activesupport (>= 4.2, < 7)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (>= 3.5)
|
sidekiq (>= 3.5)
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.13.7)
|
oj (3.13.11)
|
||||||
omniauth (1.9.1)
|
omniauth (1.9.1)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
|
@ -393,19 +410,17 @@ GEM
|
||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
openssl-signature_algorithm (0.4.0)
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.5)
|
ox (2.14.6)
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parallel_tests (3.7.3)
|
parser (3.1.0.0)
|
||||||
parallel
|
|
||||||
parser (3.0.2.0)
|
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.2.3)
|
pg (1.3.0)
|
||||||
pghero (2.8.1)
|
pghero (2.8.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.6)
|
pkg-config (1.4.7)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.14.2)
|
premailer (1.14.2)
|
||||||
addressable
|
addressable
|
||||||
|
@ -424,12 +439,12 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.4.0)
|
puma (5.5.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.1)
|
pundit (2.1.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.5.2)
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-attack (6.5.0)
|
rack-attack (6.5.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
@ -439,20 +454,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.1.4.1)
|
rails (6.1.4.4)
|
||||||
actioncable (= 6.1.4.1)
|
actioncable (= 6.1.4.4)
|
||||||
actionmailbox (= 6.1.4.1)
|
actionmailbox (= 6.1.4.4)
|
||||||
actionmailer (= 6.1.4.1)
|
actionmailer (= 6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
actiontext (= 6.1.4.1)
|
actiontext (= 6.1.4.4)
|
||||||
actionview (= 6.1.4.1)
|
actionview (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activemodel (= 6.1.4.1)
|
activemodel (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activestorage (= 6.1.4.1)
|
activestorage (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.4.1)
|
railties (= 6.1.4.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
@ -468,24 +483,23 @@ GEM
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (6.1.4.1)
|
railties (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.13)
|
rake (>= 0.13)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
rdf (3.1.15)
|
rdf (3.2.3)
|
||||||
hamster (~> 3.0)
|
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.5.0)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.2)
|
||||||
redcarpet (3.5.1)
|
redcarpet (3.5.1)
|
||||||
redis (4.4.0)
|
redis (4.5.1)
|
||||||
redis-namespace (1.8.1)
|
redis-namespace (1.8.1)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
regexp_parser (2.1.1)
|
regexp_parser (2.2.0)
|
||||||
request_store (1.5.0)
|
request_store (1.5.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
|
@ -517,21 +531,21 @@ GEM
|
||||||
rspec-sidekiq (3.1.0)
|
rspec-sidekiq (3.1.0)
|
||||||
rspec-core (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.10.2)
|
rspec-support (3.10.3)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.5.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.21.0)
|
rubocop (1.25.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.0.0.0)
|
parser (>= 3.1.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 1.9.1, < 2.0)
|
rubocop-ast (>= 1.15.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.11.0)
|
rubocop-ast (1.15.1)
|
||||||
parser (>= 3.0.1.1)
|
parser (>= 3.0.1.1)
|
||||||
rubocop-rails (2.12.2)
|
rubocop-rails (2.13.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
|
@ -539,7 +553,7 @@ GEM
|
||||||
ruby-saml (1.13.0)
|
ruby-saml (1.13.0)
|
||||||
nokogiri (>= 1.10.5)
|
nokogiri (>= 1.10.5)
|
||||||
rexml
|
rexml
|
||||||
ruby2_keywords (0.0.4)
|
ruby2_keywords (0.0.5)
|
||||||
rufus-scheduler (3.7.0)
|
rufus-scheduler (3.7.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
|
@ -547,12 +561,12 @@ GEM
|
||||||
sanitize (6.0.0)
|
sanitize (6.0.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.5.4)
|
scenic (1.5.5)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.2.2)
|
sidekiq (6.4.0)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
|
@ -565,7 +579,7 @@ GEM
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
thwait
|
thwait
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.7)
|
sidekiq-unique-jobs (7.1.12)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 5.0, < 8.0)
|
sidekiq (>= 5.0, < 8.0)
|
||||||
|
@ -584,9 +598,9 @@ GEM
|
||||||
sprockets (3.7.2)
|
sprockets (3.7.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.2)
|
sprockets-rails (3.4.2)
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sshkit (1.21.2)
|
sshkit (1.21.2)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
|
@ -594,14 +608,14 @@ GEM
|
||||||
stackprof (0.2.17)
|
stackprof (0.2.17)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (2.2.1)
|
stoplight (2.2.1)
|
||||||
strong_migrations (0.7.8)
|
strong_migrations (0.7.9)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
temple (0.8.2)
|
temple (0.8.2)
|
||||||
terminal-table (3.0.0)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (1.1.0)
|
thor (1.2.1)
|
||||||
thwait (0.2.0)
|
thwait (0.2.0)
|
||||||
e2mmap
|
e2mmap
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
|
@ -623,12 +637,12 @@ GEM
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (2.0.4)
|
tzinfo (2.0.4)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2021.2)
|
tzinfo-data (1.2021.5)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.8)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (2.1.0)
|
||||||
uniform_notifier (1.14.2)
|
uniform_notifier (1.14.2)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
|
@ -661,7 +675,7 @@ GEM
|
||||||
xorcist (1.1.2)
|
xorcist (1.1.2)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.5.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -671,23 +685,23 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.103)
|
aws-sdk-s3 (~> 1.111)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.9.1)
|
bootsnap (~> 1.10.2)
|
||||||
brakeman (~> 5.1)
|
brakeman (~> 5.2)
|
||||||
browser
|
browser
|
||||||
bullet (~> 6.1)
|
bullet (~> 7.0)
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
capistrano (~> 3.16)
|
capistrano (~> 3.16)
|
||||||
capistrano-rails (~> 1.6)
|
capistrano-rails (~> 1.6)
|
||||||
capistrano-rbenv (~> 2.2)
|
capistrano-rbenv (~> 2.2)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.35)
|
capybara (~> 3.36)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 5.2)
|
chewy (~> 7.2)
|
||||||
cld3 (~> 3.4.2)
|
cld3 (~> 3.4.4)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
color_diff (~> 0.1)
|
color_diff (~> 0.1)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
|
@ -698,8 +712,8 @@ DEPENDENCIES
|
||||||
discard (~> 1.2)
|
discard (~> 1.2)
|
||||||
doorkeeper (~> 5.5)
|
doorkeeper (~> 5.5)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.3)
|
||||||
fabrication (~> 2.22)
|
fabrication (~> 2.24)
|
||||||
faker (~> 2.19)
|
faker (~> 2.19)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
|
@ -707,6 +721,7 @@ DEPENDENCIES
|
||||||
fog-openstack (~> 0.3)
|
fog-openstack (~> 0.3)
|
||||||
fuubar (~> 2.5)
|
fuubar (~> 2.5)
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
|
hcaptcha (~> 7.1)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
http (~> 5.0)
|
http (~> 5.0)
|
||||||
|
@ -716,20 +731,20 @@ DEPENDENCIES
|
||||||
idn-ruby
|
idn-ruby
|
||||||
iso-639
|
iso-639
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.1)
|
json-ld-preloaded (~> 3.2)
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kt-paperclip (~> 7.0)
|
kt-paperclip (~> 7.0)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
letter_opener_web (~> 1.4)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.11)
|
lograge (~> 0.11)
|
||||||
makara (~> 0.5)
|
makara (~> 0.5)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
microformats (~> 4.2)
|
microformats (~> 4.2)
|
||||||
mime-types (~> 3.3.1)
|
mime-types (~> 3.4.1)
|
||||||
net-ldap (~> 0.17)
|
net-ldap (~> 0.17)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.13)
|
||||||
nsa (~> 0.2)
|
nsa (~> 0.2)
|
||||||
oj (~> 3.13)
|
oj (~> 3.13)
|
||||||
omniauth (~> 1.9)
|
omniauth (~> 1.9)
|
||||||
|
@ -737,10 +752,8 @@ DEPENDENCIES
|
||||||
omniauth-rails_csrf_protection (~> 0.1)
|
omniauth-rails_csrf_protection (~> 0.1)
|
||||||
omniauth-saml (~> 1.10)
|
omniauth-saml (~> 1.10)
|
||||||
ox (~> 2.14)
|
ox (~> 2.14)
|
||||||
parallel (~> 1.21)
|
|
||||||
parallel_tests (~> 3.7)
|
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.2)
|
pg (~> 1.3)
|
||||||
pghero (~> 2.8)
|
pghero (~> 2.8)
|
||||||
pkg-config (~> 1.4)
|
pkg-config (~> 1.4)
|
||||||
posix-spawn
|
posix-spawn
|
||||||
|
@ -748,7 +761,7 @@ DEPENDENCIES
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.9)
|
pry-byebug (~> 3.9)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 5.4)
|
puma (~> 5.5)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.1)
|
||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.5)
|
rack-attack (~> 6.5)
|
||||||
|
@ -757,20 +770,21 @@ DEPENDENCIES
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 6.0)
|
rails-i18n (~> 6.0)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
rdf-normalize (~> 0.4)
|
rdf-normalize (~> 0.5)
|
||||||
redcarpet (~> 3.5)
|
redcarpet (~> 3.5)
|
||||||
redis (~> 4.4)
|
redis (~> 4.5)
|
||||||
redis-namespace (~> 1.8)
|
redis-namespace (~> 1.8)
|
||||||
|
rexml (~> 3.2)
|
||||||
rqrcode (~> 2.1)
|
rqrcode (~> 2.1)
|
||||||
rspec-rails (~> 5.0)
|
rspec-rails (~> 5.0)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.5)
|
||||||
rubocop (~> 1.21)
|
rubocop (~> 1.25)
|
||||||
rubocop-rails (~> 2.12)
|
rubocop-rails (~> 2.13)
|
||||||
ruby-progressbar (~> 1.11)
|
ruby-progressbar (~> 1.11)
|
||||||
sanitize (~> 6.0)
|
sanitize (~> 6.0)
|
||||||
scenic (~> 1.5)
|
scenic (~> 1.5)
|
||||||
sidekiq (~> 6.2)
|
sidekiq (~> 6.4)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.1)
|
sidekiq-scheduler (~> 3.1)
|
||||||
sidekiq-unique-jobs (~> 7.1)
|
sidekiq-unique-jobs (~> 7.1)
|
||||||
|
@ -778,11 +792,11 @@ DEPENDENCIES
|
||||||
simple_form (~> 5.1)
|
simple_form (~> 5.1)
|
||||||
simplecov (~> 0.21)
|
simplecov (~> 0.21)
|
||||||
sprockets (~> 3.7.2)
|
sprockets (~> 3.7.2)
|
||||||
sprockets-rails (~> 3.2)
|
sprockets-rails (~> 3.4)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 2.2.1)
|
stoplight (~> 2.2.1)
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 1.1)
|
thor (~> 1.2)
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
twitter-text (~> 3.1.0)
|
twitter-text (~> 3.1.0)
|
||||||
tzinfo-data (~> 1.2021)
|
tzinfo-data (~> 1.2021)
|
||||||
|
|
|
@ -45,16 +45,8 @@ sudo apt-get install \
|
||||||
# Install rvm
|
# Install rvm
|
||||||
read RUBY_VERSION < .ruby-version
|
read RUBY_VERSION < .ruby-version
|
||||||
|
|
||||||
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
|
curl -sSL https://rvm.io/mpapis.asc | gpg --import
|
||||||
$($gpg_command)
|
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
|
||||||
if [ $? -ne 0 ];then
|
|
||||||
echo "GPG command failed, This prevented RVM from installing."
|
|
||||||
echo "Retrying once..." && $($gpg_command)
|
|
||||||
if [ $? -ne 0 ];then
|
|
||||||
echo "GPG failed for the second time, please ensure network connectivity."
|
|
||||||
echo "Exiting..." && exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
||||||
source /home/vagrant/.rvm/scripts/rvm
|
source /home/vagrant/.rvm/scripts/rvm
|
||||||
|
|
|
@ -23,21 +23,21 @@ 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'
|
|
||||||
|
|
||||||
field :display_name, type: 'text', analyzer: 'content' do
|
root date_detection: false do
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field :id, type: 'long'
|
||||||
end
|
|
||||||
|
|
||||||
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
|
field :display_name, type: 'text', analyzer: 'content' do
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||||
end
|
|
||||||
|
|
||||||
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
|
|
||||||
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
|
||||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
|
||||||
|
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||||
|
end
|
||||||
|
|
||||||
|
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
|
||||||
|
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
||||||
|
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,36 +31,36 @@ 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)
|
crutch :mentions do |collection|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
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) }
|
||||||
|
end
|
||||||
|
|
||||||
|
crutch :favourites do |collection|
|
||||||
|
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||||
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
crutch :reblogs do |collection|
|
||||||
|
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
||||||
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
crutch :bookmarks do |collection|
|
||||||
|
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||||
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
root date_detection: false do
|
||||||
|
field :id, type: 'long'
|
||||||
|
field :account_id, type: 'long'
|
||||||
|
|
||||||
|
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
||||||
|
field :stemmed, type: 'text', analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
crutch :favourites do |collection|
|
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||||
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
crutch :reblogs do |collection|
|
|
||||||
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
crutch :bookmarks do |collection|
|
|
||||||
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
root date_detection: false do
|
|
||||||
field :id, type: 'long'
|
|
||||||
field :account_id, type: 'long'
|
|
||||||
|
|
||||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
|
|
||||||
field :stemmed, type: 'text', analyzer: 'content'
|
|
||||||
end
|
|
||||||
|
|
||||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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? }
|
root date_detection: false do
|
||||||
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
|
field :name, type: 'text', analyzer: 'content' do
|
||||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||||
|
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } }
|
||||||
|
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ class AccountsController < ApplicationController
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@pinned_statuses = cache_collection(@account.pinned_statuses.not_local_only, Status) if show_pinned_statuses?
|
@pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
|
||||||
@statuses = cached_filtered_status_page
|
@statuses = cached_filtered_status_page
|
||||||
@rss_url = rss_url
|
@rss_url = rss_url
|
||||||
|
|
||||||
|
@ -65,6 +65,10 @@ class AccountsController < ApplicationController
|
||||||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filtered_pinned_statuses
|
||||||
|
@account.pinned_statuses.not_local_only.where(visibility: [:public, :unlisted])
|
||||||
|
end
|
||||||
|
|
||||||
def filtered_statuses
|
def filtered_statuses
|
||||||
default_statuses.tap do |statuses|
|
default_statuses.tap do |statuses|
|
||||||
statuses.merge!(hashtag_scope) if tag_requested?
|
statuses.merge!(hashtag_scope) if tag_requested?
|
||||||
|
@ -143,6 +147,13 @@ class AccountsController < ApplicationController
|
||||||
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cached_filtered_status_pins
|
||||||
|
cache_collection(
|
||||||
|
filtered_pinned_statuses,
|
||||||
|
Status
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def cached_filtered_status_page
|
def cached_filtered_status_page
|
||||||
cache_collection_paginated_by_id(
|
cache_collection_paginated_by_id(
|
||||||
filtered_statuses,
|
filtered_statuses,
|
||||||
|
|
|
@ -21,6 +21,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) }
|
@items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) }
|
||||||
|
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
|
||||||
when 'tags'
|
when 'tags'
|
||||||
@items = for_signed_account { @account.featured_tags }
|
@items = for_signed_account { @account.featured_tags }
|
||||||
when 'devices'
|
when 'devices'
|
||||||
|
|
|
@ -14,7 +14,7 @@ module Admin
|
||||||
else
|
else
|
||||||
@account = @account_moderation_note.target_account
|
@account = @account_moderation_note.target_account
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.targeted_account_warnings.latest.custom
|
@warnings = @account.strikes.custom.latest
|
||||||
|
|
||||||
render template: 'admin/accounts/show'
|
render template: 'admin/accounts/show'
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,13 +2,24 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
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_remote_account!, only: [:redownload]
|
||||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :account, :index?
|
authorize :account, :index?
|
||||||
|
|
||||||
@accounts = filtered_accounts.page(params[:page])
|
@accounts = filtered_accounts.page(params[:page])
|
||||||
|
@form = Form::AccountBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_accounts_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -17,7 +28,7 @@ module Admin
|
||||||
@deletion_request = @account.deletion_request
|
@deletion_request = @account.deletion_request
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.targeted_account_warnings.latest.custom
|
@warnings = @account.strikes.custom.latest
|
||||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -38,13 +49,13 @@ module Admin
|
||||||
def approve
|
def approve
|
||||||
authorize @account.user, :approve?
|
authorize @account.user, :approve?
|
||||||
@account.user.approve!
|
@account.user.approve!
|
||||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
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
|
end
|
||||||
|
|
||||||
def destroy
|
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)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
@ -121,11 +142,25 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_accounts
|
def filtered_accounts
|
||||||
AccountFilter.new(filter_params).results
|
AccountFilter.new(filter_params.with_defaults(order: 'recent')).results
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def form_account_batch_params
|
||||||
|
params.require(:form_account_batch).permit(:action, account_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:suspend]
|
||||||
|
'suspend'
|
||||||
|
elsif params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,50 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'sidekiq/api'
|
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class DashboardController < BaseController
|
class DashboardController < BaseController
|
||||||
def index
|
def index
|
||||||
@system_checks = Admin::SystemCheck.perform
|
@system_checks = Admin::SystemCheck.perform
|
||||||
@users_count = User.count
|
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||||
@pending_users_count = User.pending.count
|
@pending_users_count = User.pending.count
|
||||||
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
|
@pending_reports_count = Report.unresolved.count
|
||||||
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
|
|
||||||
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
|
||||||
@relay_enabled = Relay.enabled.exists?
|
|
||||||
@single_user_mode = Rails.configuration.x.single_user_mode
|
|
||||||
@registrations_enabled = Setting.registrations_mode != 'none'
|
|
||||||
@deletions_enabled = Setting.open_deletion
|
|
||||||
@invites_enabled = Setting.min_invite_role == 'user'
|
|
||||||
@search_enabled = Chewy.enabled?
|
|
||||||
@version = Mastodon::Version.to_s
|
|
||||||
@database_version = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
|
|
||||||
@redis_version = redis_info['redis_version']
|
|
||||||
@reports_count = Report.unresolved.count
|
|
||||||
@queue_backlog = Sidekiq::Stats.new.enqueued
|
|
||||||
@recent_users = User.confirmed.recent.includes(:account).limit(8)
|
|
||||||
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
|
|
||||||
@redis_size = redis_info['used_memory']
|
|
||||||
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
|
|
||||||
@cas_enabled = ENV['CAS_ENABLED'] == 'true'
|
|
||||||
@saml_enabled = ENV['SAML_ENABLED'] == 'true'
|
|
||||||
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
|
|
||||||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
|
||||||
@trending_hashtags = TrendingTags.get(10, filtered: false)
|
|
||||||
@pending_tags_count = Tag.pending_review.count
|
@pending_tags_count = Tag.pending_review.count
|
||||||
@authorized_fetch = authorized_fetch_mode?
|
|
||||||
@whitelist_enabled = whitelist_mode?
|
|
||||||
@profile_directory = Setting.profile_directory
|
|
||||||
@timeline_preview = Setting.timeline_preview
|
|
||||||
@keybase_integration = Setting.enable_keybase
|
|
||||||
@trends_enabled = Setting.trends
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def current_week
|
|
||||||
@current_week ||= Time.now.utc.to_date.cweek
|
|
||||||
end
|
|
||||||
|
|
||||||
def redis_info
|
def redis_info
|
||||||
@redis_info ||= begin
|
@redis_info ||= begin
|
||||||
if Redis.current.is_a?(Redis::Namespace)
|
if Redis.current.is_a?(Redis::Namespace)
|
||||||
|
|
|
@ -14,6 +14,15 @@ module Admin
|
||||||
authorize :instance, :show?
|
authorize :instance, :show?
|
||||||
end
|
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
|
def clear_delivery_errors
|
||||||
authorize :delivery, :clear_delivery_errors?
|
authorize :delivery, :clear_delivery_errors?
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -14,20 +14,17 @@ module Admin
|
||||||
if params[:create_and_resolve]
|
if params[:create_and_resolve]
|
||||||
@report.resolve!(current_account)
|
@report.resolve!(current_account)
|
||||||
log_action :resolve, @report
|
log_action :resolve, @report
|
||||||
|
elsif params[:create_and_unresolve]
|
||||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:create_and_unresolve]
|
|
||||||
@report.unresolve!
|
@report.unresolve!
|
||||||
log_action :reopen, @report
|
log_action :reopen, @report
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to 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
|
else
|
||||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
||||||
@form = Form::StatusBatch.new
|
@action_logs = @report.history.includes(:target)
|
||||||
|
@form = Admin::StatusBatchAction.new
|
||||||
|
@statuses = @report.statuses.with_includes
|
||||||
|
|
||||||
render template: 'admin/reports/show'
|
render template: 'admin/reports/show'
|
||||||
end
|
end
|
||||||
|
@ -41,6 +38,14 @@ module Admin
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def after_create_redirect_path
|
||||||
|
if params[:create_and_resolve]
|
||||||
|
admin_reports_path
|
||||||
|
else
|
||||||
|
admin_report_path(@report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:report_note).permit(
|
params.require(:report_note).permit(
|
||||||
:content,
|
:content,
|
||||||
|
|
|
@ -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
|
|
|
@ -13,8 +13,10 @@ module Admin
|
||||||
authorize @report, :show?
|
authorize @report, :show?
|
||||||
|
|
||||||
@report_note = @report.notes.new
|
@report_note = @report.notes.new
|
||||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
||||||
@form = Form::StatusBatch.new
|
@action_logs = @report.history.includes(:target)
|
||||||
|
@form = Admin::StatusBatchAction.new
|
||||||
|
@statuses = @report.statuses.with_includes
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_to_self
|
def assign_to_self
|
||||||
|
|
|
@ -2,71 +2,57 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class StatusesController < BaseController
|
class StatusesController < BaseController
|
||||||
helper_method :current_params
|
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
before_action :set_statuses
|
||||||
|
|
||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :status, :index?
|
authorize :status, :index?
|
||||||
|
|
||||||
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
|
@status_batch_action = Admin::StatusBatchAction.new
|
||||||
|
|
||||||
if params[:media]
|
|
||||||
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id))
|
|
||||||
end
|
|
||||||
|
|
||||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
|
||||||
@form = Form::StatusBatch.new
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def batch
|
||||||
authorize :status, :index?
|
@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!
|
||||||
@statuses = @account.statuses.where(id: params[:id])
|
|
||||||
authorize @statuses.first, :show?
|
|
||||||
|
|
||||||
@form = Form::StatusBatch.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
authorize :status, :update?
|
|
||||||
|
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
|
||||||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
|
||||||
|
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
||||||
|
ensure
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
redirect_to after_create_redirect_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def form_status_batch_params
|
def admin_status_batch_action_params
|
||||||
params.require(:form_status_batch).permit(:action, status_ids: [])
|
params.require(:admin_status_batch_action).permit(status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_create_redirect_path
|
||||||
|
if @status_batch_action.report_id.present?
|
||||||
|
admin_report_path(@status_batch_action.report_id)
|
||||||
|
else
|
||||||
|
admin_account_statuses_path(params[:account_id], current_params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find(params[:account_id])
|
@account = Account.find(params[:account_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_params
|
def set_statuses
|
||||||
page = (params[:page] || 1).to_i
|
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
{
|
def filter_params
|
||||||
media: params[:media],
|
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
|
||||||
page: page > 1 && page,
|
|
||||||
}.select { |_, value| value.present? }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_from_button
|
def action_from_button
|
||||||
if params[:nsfw_on]
|
if params[:report]
|
||||||
'nsfw_on'
|
'report'
|
||||||
elsif params[:nsfw_off]
|
elsif params[:remove_from_report]
|
||||||
'nsfw_off'
|
'remove_from_report'
|
||||||
elsif params[:delete]
|
elsif params[:delete]
|
||||||
'delete'
|
'delete'
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,38 +2,12 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class TagsController < BaseController
|
class TagsController < BaseController
|
||||||
before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
|
before_action :set_tag
|
||||||
before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
|
|
||||||
before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
|
|
||||||
|
|
||||||
def index
|
|
||||||
authorize :tag, :index?
|
|
||||||
|
|
||||||
@tags = filtered_tags.page(params[:page])
|
|
||||||
@form = Form::TagBatch.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def batch
|
|
||||||
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
|
||||||
@form.save
|
|
||||||
rescue ActionController::ParameterMissing
|
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
|
||||||
ensure
|
|
||||||
redirect_to admin_tags_path(filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def approve_all
|
|
||||||
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
|
|
||||||
redirect_to admin_tags_path(filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reject_all
|
|
||||||
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save
|
|
||||||
redirect_to admin_tags_path(filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize @tag, :show?
|
authorize @tag, :show?
|
||||||
|
|
||||||
|
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -52,52 +26,8 @@ module Admin
|
||||||
@tag = Tag.find(params[:id])
|
@tag = Tag.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_usage_by_domain
|
|
||||||
@usage_by_domain = @tag.statuses
|
|
||||||
.with_public_visibility
|
|
||||||
.excluding_silenced_accounts
|
|
||||||
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
|
|
||||||
.joins(:account)
|
|
||||||
.group('accounts.domain')
|
|
||||||
.reorder(statuses_count: :desc)
|
|
||||||
.pluck(Arel.sql('accounts.domain, count(*) AS statuses_count'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_counters
|
|
||||||
@accounts_today = @tag.history.first[:accounts]
|
|
||||||
@accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_tags
|
|
||||||
TagFilter.new(filter_params).results
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_params
|
|
||||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_params
|
def tag_params
|
||||||
params.require(:tag).permit(:name, :trendable, :usable, :listable)
|
params.require(:tag).permit(:name, :trendable, :usable, :listable)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_week_days
|
|
||||||
now = Time.now.utc.beginning_of_day.to_date
|
|
||||||
|
|
||||||
(Date.commercial(now.cwyear, now.cweek)..now).map do |date|
|
|
||||||
date.to_time(:utc).beginning_of_day.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def form_tag_batch_params
|
|
||||||
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def action_from_button
|
|
||||||
if params[:approve]
|
|
||||||
'approve'
|
|
||||||
elsif params[:reject]
|
|
||||||
'reject'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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 = Form::PreviewCardProviderBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::PreviewCardProviderBatch.new(form_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
|
||||||
|
PreviewCardProviderFilter.new(filter_params).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_preview_card_provider_batch_params
|
||||||
|
params.require(:form_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
|
|
@ -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 = Form::PreviewCardBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::PreviewCardBatch.new(form_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
|
||||||
|
PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_preview_card_batch_params
|
||||||
|
params.require(:form_preview_card_batch).permit(:action, preview_card_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:approve_all]
|
||||||
|
'approve_all'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
elsif params[:reject_all]
|
||||||
|
'reject_all'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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 = 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_trends_tags_path(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_tags
|
||||||
|
TagFilter.new(filter_params).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||||
|
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
|
|
@ -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
|
|
|
@ -5,8 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
render json: []
|
||||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -46,9 +46,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def pinned_scope
|
def pinned_scope
|
||||||
return Status.none if @account.blocking?(current_account)
|
@account.pinned_statuses.permitted_for(@account, current_account)
|
||||||
|
|
||||||
@account.pinned_statuses
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def no_replies_scope
|
def no_replies_scope
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::AccountsController < Api::BaseController
|
class Api::V1::AccountsController < Api::BaseController
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
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:follows' }, only: [:follow, :unfollow]
|
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
|
||||||
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
||||||
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||||
|
@ -53,6 +53,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_from_followers
|
||||||
|
RemoveFromFollowersService.new.call(current_user.account, @account)
|
||||||
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
|
end
|
||||||
|
|
||||||
def unblock
|
def unblock
|
||||||
UnblockService.new.call(current_user.account, @account)
|
UnblockService.new.call(current_user.account, @account)
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
|
@ -78,10 +83,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
forbidden if single_user_mode? || !allowed_registrations?
|
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.registrations_mode != 'none'
|
Setting.registrations_mode != 'none'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::AccountsController < Api::BaseController
|
class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'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:write', :'admin:write:accounts' }, except: [:index, :show]
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_accounts, only: :index
|
before_action :set_accounts, only: :index
|
||||||
before_action :set_account, except: :index
|
before_action :set_account, except: :index
|
||||||
|
@ -94,7 +96,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite, :ips]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::DimensionsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::MeasuresController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
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
|
|
@ -1,13 +1,15 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::ReportsController < Api::BaseController
|
class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'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:write', :'admin:write:reports' }, except: [:index, :show]
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_reports, only: :index
|
before_action :set_reports, only: :index
|
||||||
before_action :set_report, except: :index
|
before_action :set_report, except: :index
|
||||||
|
@ -32,6 +34,12 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
render json: @report, serializer: REST::Admin::ReportSerializer
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @report, :update?
|
||||||
|
@report.update!(report_params)
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def assign_to_self
|
def assign_to_self
|
||||||
authorize @report, :update?
|
authorize @report, :update?
|
||||||
@report.update!(assigned_account_id: current_account.id)
|
@report.update!(assigned_account_id: current_account.id)
|
||||||
|
@ -74,6 +82,10 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
ReportFilter.new(filter_params).results
|
ReportFilter.new(filter_params).results
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def report_params
|
||||||
|
params.permit(:category, rule_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.permit(*FILTER_PARAMS)
|
params.permit(*FILTER_PARAMS)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::RetentionController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
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.get(false, limit_param(10))
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,22 +14,21 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def activity
|
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|
|
(0...12).map do |i|
|
||||||
day = i.weeks.ago.to_date
|
start_of_week = i.weeks.ago
|
||||||
week_id = day.cweek
|
end_of_week = start_of_week + 6.days
|
||||||
week = Date.commercial(day.cwyear, week_id)
|
|
||||||
|
|
||||||
weeks << {
|
{
|
||||||
week: week.to_time.to_i.to_s,
|
week: start_of_week.to_i.to_s,
|
||||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
|
statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s,
|
||||||
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
|
logins: logins_tracker.sum(start_of_week, end_of_week).to_s,
|
||||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
weeks
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_enabled_api!
|
def require_enabled_api!
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
|
before_action :set_status
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @status.edits, each_serializer: REST::StatusEditSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = Status.find(params[:status_id])
|
||||||
|
authorize @status, :show?
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::SourcesController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||||
|
before_action :set_status
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @status, serializer: REST::StatusSourceSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = Status.find(params[:status_id])
|
||||||
|
authorize @status, :show?
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
|
@ -57,7 +57,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
@status.discard
|
@status.discard
|
||||||
RemovalWorker.perform_async(@status.id, redraft: true)
|
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Trends::LinksController < Api::BaseController
|
||||||
|
before_action :set_links
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @links, each_serializer: REST::Trends::LinkSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_links
|
||||||
|
@links = begin
|
||||||
|
if Setting.trends
|
||||||
|
Trends.links.get(true, limit_param(10))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Trends::TagsController < Api::BaseController
|
||||||
|
before_action :set_tags
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @tags, each_serializer: REST::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_tags
|
||||||
|
@tags = begin
|
||||||
|
if Setting.trends
|
||||||
|
Trends.tags.get(true, limit_param(10))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::V1::TrendsController < Api::BaseController
|
|
||||||
before_action :set_tags
|
|
||||||
|
|
||||||
def index
|
|
||||||
render json: @tags, each_serializer: REST::TagSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_tags
|
|
||||||
@tags = TrendingTags.get(limit_param(10))
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
|
||||||
include SessionTrackingConcern
|
include SessionTrackingConcern
|
||||||
include CacheConcern
|
include CacheConcern
|
||||||
include DomainControlHelper
|
include DomainControlHelper
|
||||||
|
include ThemingConcern
|
||||||
|
|
||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
|
@ -73,75 +74,6 @@ class ApplicationController < ActionController::Base
|
||||||
new_user_session_path
|
new_user_session_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def pack(data, pack_name, skin = 'default')
|
|
||||||
return nil unless pack?(data, pack_name)
|
|
||||||
pack_data = {
|
|
||||||
common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
|
|
||||||
flavour: data['name'],
|
|
||||||
pack: pack_name,
|
|
||||||
preload: nil,
|
|
||||||
skin: nil,
|
|
||||||
supported_locales: data['locales'],
|
|
||||||
}
|
|
||||||
if data['pack'][pack_name].is_a?(Hash)
|
|
||||||
pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false
|
|
||||||
pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
|
|
||||||
if data['pack'][pack_name]['preload']
|
|
||||||
pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String)
|
|
||||||
pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array)
|
|
||||||
end
|
|
||||||
if skin != 'default' && data['skin'][skin]
|
|
||||||
pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
|
|
||||||
else # default skin
|
|
||||||
pack_data[:skin] = 'default' if data['pack'][pack_name]['stylesheet']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
pack_data
|
|
||||||
end
|
|
||||||
|
|
||||||
def pack?(data, pack_name)
|
|
||||||
if data['pack'].is_a?(Hash) && data['pack'].key?(pack_name)
|
|
||||||
return true if data['pack'][pack_name].is_a?(String) || data['pack'][pack_name].is_a?(Hash)
|
|
||||||
end
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def nil_pack(data, pack_name, skin = 'default')
|
|
||||||
{
|
|
||||||
common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
|
|
||||||
flavour: data['name'],
|
|
||||||
pack: nil,
|
|
||||||
preload: nil,
|
|
||||||
skin: nil,
|
|
||||||
supported_locales: data['locales'],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def resolve_pack(data, pack_name, skin = 'default')
|
|
||||||
result = pack(data, pack_name, skin)
|
|
||||||
unless result
|
|
||||||
if data['name'] && data.key?('fallback')
|
|
||||||
if data['fallback'].nil?
|
|
||||||
return nil_pack(data, pack_name, skin)
|
|
||||||
elsif data['fallback'].is_a?(String) && Themes.instance.flavour(data['fallback'])
|
|
||||||
return resolve_pack(Themes.instance.flavour(data['fallback']), pack_name)
|
|
||||||
elsif data['fallback'].is_a?(Array)
|
|
||||||
data['fallback'].each do |fallback|
|
|
||||||
return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil_pack(data, pack_name, skin)
|
|
||||||
end
|
|
||||||
return data.key?('name') && data['name'] != Setting.default_settings['flavour'] ? resolve_pack(Themes.instance.flavour(Setting.default_settings['flavour']), pack_name) : nil_pack(data, pack_name, skin)
|
|
||||||
end
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def use_pack(pack_name)
|
|
||||||
@core = resolve_pack(Themes.instance.core, pack_name)
|
|
||||||
@theme = resolve_pack(Themes.instance.flavour(current_flavour), pack_name, current_skin)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def truthy_param?(key)
|
def truthy_param?(key)
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
include CaptchaConcern
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
||||||
before_action :require_unconfirmed!
|
before_action :require_unconfirmed!
|
||||||
|
|
||||||
|
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
|
||||||
|
before_action :require_captcha_if_needed!, only: [:show]
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
@ -15,8 +21,46 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
|
resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
old_session_values = session.to_hash
|
||||||
|
reset_session
|
||||||
|
session.update old_session_values.except('session_id')
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_captcha
|
||||||
|
check_captcha! do |message|
|
||||||
|
flash.now[:alert] = message
|
||||||
|
render :captcha
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
show
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def require_captcha_if_needed!
|
||||||
|
render :captcha if captcha_required?
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_confirmation_user!
|
||||||
|
# We need to reimplement looking up the user because
|
||||||
|
# Devise::ConfirmationsController#show looks up and confirms in one
|
||||||
|
# step.
|
||||||
|
confirmation_token = params[:confirmation_token]
|
||||||
|
return if confirmation_token.nil?
|
||||||
|
@confirmation_user = User.find_first_by_auth_conditions(confirmation_token: confirmation_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def captcha_user_bypass?
|
||||||
|
return true if @confirmation_user.nil? || @confirmation_user.confirmed?
|
||||||
|
|
||||||
|
invite = Invite.find(@confirmation_user.invite_id) if @confirmation_user.invite_id.present?
|
||||||
|
invite.present? && !invite.max_uses.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def set_pack
|
def set_pack
|
||||||
use_pack 'auth'
|
use_pack 'auth'
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,6 @@ class Auth::PasswordsController < Devise::PasswordsController
|
||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.errors.empty?
|
if resource.errors.empty?
|
||||||
resource.session_activations.destroy_all
|
resource.session_activations.destroy_all
|
||||||
resource.forget_me!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
include Devise::Controllers::Rememberable
|
|
||||||
include RegistrationSpamConcern
|
include RegistrationSpamConcern
|
||||||
|
|
||||||
layout :determine_layout
|
layout :determine_layout
|
||||||
|
@ -31,8 +30,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.saved_change_to_encrypted_password?
|
if resource.saved_change_to_encrypted_password?
|
||||||
resource.clear_other_sessions(current_session.session_id)
|
resource.clear_other_sessions(current_session.session_id)
|
||||||
resource.forget_me!
|
|
||||||
remember_me(resource)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -85,13 +82,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
def invite_code
|
def invite_code
|
||||||
if params[:user]
|
if params[:user]
|
||||||
params[:user][:invite_code]
|
params[:user][:invite_code]
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
include Devise::Controllers::Rememberable
|
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
skip_before_action :require_no_authentication, only: [:create]
|
skip_before_action :require_no_authentication, only: [:create]
|
||||||
|
@ -17,14 +15,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
def new
|
|
||||||
Devise.omniauth_configs.each do |provider, config|
|
|
||||||
return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
# We only need to call this if this hasn't already been
|
# We only need to call this if this hasn't already been
|
||||||
|
@ -91,14 +81,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_out_path_for(_resource_or_scope)
|
|
||||||
Devise.omniauth_configs.each_value do |config|
|
|
||||||
return root_path if config.strategy.redirect_at_sign_in
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
|
|
||||||
|
@ -155,8 +137,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
|
|
||||||
user.update_sign_in!(request, new_sign_in: true)
|
user.update_sign_in!(new_sign_in: true)
|
||||||
remember_me(user)
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
flash.delete(:notice)
|
flash.delete(:notice)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ module AccountOwnedConcern
|
||||||
before_action :set_account, if: :account_required?
|
before_action :set_account, if: :account_required?
|
||||||
before_action :check_account_approval, if: :account_required?
|
before_action :check_account_approval, if: :account_required?
|
||||||
before_action :check_account_suspension, if: :account_required?
|
before_action :check_account_suspension, if: :account_required?
|
||||||
|
before_action :check_account_confirmation, if: :account_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -28,6 +29,10 @@ module AccountOwnedConcern
|
||||||
not_found if @account.local? && @account.user_pending?
|
not_found if @account.local? && @account.user_pending?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_account_confirmation
|
||||||
|
not_found if @account.local? && !@account.user_confirmed?
|
||||||
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
if @account.suspended_permanently?
|
if @account.suspended_permanently?
|
||||||
permanent_suspension_response
|
permanent_suspension_response
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module AccountableConcern
|
module AccountableConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def log_action(action, target)
|
def log_action(action, target, options = {})
|
||||||
Admin::ActionLog.create(account: current_account, action: action, target: target)
|
Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module CaptchaConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include Hcaptcha::Adapters::ViewMethods
|
||||||
|
|
||||||
|
included do
|
||||||
|
helper_method :render_captcha
|
||||||
|
end
|
||||||
|
|
||||||
|
def captcha_available?
|
||||||
|
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def captcha_enabled?
|
||||||
|
captcha_available? && Setting.captcha_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
def captcha_user_bypass?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def captcha_required?
|
||||||
|
captcha_enabled? && !captcha_user_bypass?
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_captcha!
|
||||||
|
return true unless captcha_required?
|
||||||
|
|
||||||
|
if verify_hcaptcha
|
||||||
|
true
|
||||||
|
else
|
||||||
|
if block_given?
|
||||||
|
message = flash[:hcaptcha_error]
|
||||||
|
flash.delete(:hcaptcha_error)
|
||||||
|
yield message
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extend_csp_for_captcha!
|
||||||
|
policy = request.content_security_policy
|
||||||
|
return unless captcha_required? && policy.present?
|
||||||
|
|
||||||
|
%w(script_src frame_src style_src connect_src).each do |directive|
|
||||||
|
values = policy.send(directive)
|
||||||
|
values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:')
|
||||||
|
values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:')
|
||||||
|
policy.send(directive, *values)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_captcha
|
||||||
|
return unless captcha_required?
|
||||||
|
|
||||||
|
hcaptcha_tags
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,80 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ThemingConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def use_pack(pack_name)
|
||||||
|
@core = resolve_pack_with_common(Themes.instance.core, pack_name)
|
||||||
|
@theme = resolve_pack_with_common(Themes.instance.flavour(current_flavour), pack_name, current_skin)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def valid_pack_data?(data, pack_name)
|
||||||
|
data['pack'].is_a?(Hash) && [String, Hash].any? { |c| data['pack'][pack_name].is_a?(c) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def nil_pack(data)
|
||||||
|
{
|
||||||
|
use_common: true,
|
||||||
|
flavour: data['name'],
|
||||||
|
pack: nil,
|
||||||
|
preload: nil,
|
||||||
|
skin: nil,
|
||||||
|
supported_locales: data['locales'],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def pack(data, pack_name, skin)
|
||||||
|
pack_data = {
|
||||||
|
use_common: true,
|
||||||
|
flavour: data['name'],
|
||||||
|
pack: pack_name,
|
||||||
|
preload: nil,
|
||||||
|
skin: nil,
|
||||||
|
supported_locales: data['locales'],
|
||||||
|
}
|
||||||
|
|
||||||
|
return pack_data unless data['pack'][pack_name].is_a?(Hash)
|
||||||
|
|
||||||
|
pack_data[:use_common] = false if data['pack'][pack_name]['use_common'] == false
|
||||||
|
pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
|
||||||
|
|
||||||
|
preloads = data['pack'][pack_name]['preload']
|
||||||
|
pack_data[:preload] = [preloads] if preloads.is_a?(String)
|
||||||
|
pack_data[:preload] = preloads if preloads.is_a?(Array)
|
||||||
|
|
||||||
|
if skin != 'default' && data['skin'][skin]
|
||||||
|
pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
|
||||||
|
elsif data['pack'][pack_name]['stylesheet']
|
||||||
|
pack_data[:skin] = 'default'
|
||||||
|
end
|
||||||
|
|
||||||
|
pack_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_pack(data, pack_name, skin)
|
||||||
|
return pack(data, pack_name, skin) if valid_pack_data?(data, pack_name)
|
||||||
|
return if data['name'].blank?
|
||||||
|
|
||||||
|
fallbacks = []
|
||||||
|
if data.key?('fallback')
|
||||||
|
fallbacks = data['fallback'] if data['fallback'].is_a?(Array)
|
||||||
|
fallbacks = [data['fallback']] if data['fallback'].is_a?(String)
|
||||||
|
elsif data['name'] != Setting.default_settings['flavour']
|
||||||
|
fallbacks = [Setting.default_settings['flavour']]
|
||||||
|
end
|
||||||
|
|
||||||
|
fallbacks.each do |fallback|
|
||||||
|
return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback)
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_pack_with_common(data, pack_name, skin = 'default')
|
||||||
|
result = resolve_pack(data, pack_name, skin) || nil_pack(data)
|
||||||
|
result[:common] = resolve_pack(data, 'common', skin) if result.delete(:use_common)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
|
@ -57,7 +57,7 @@ module TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
if valid_webauthn_credential?(user, webauthn_credential)
|
if valid_webauthn_credential?(user, webauthn_credential)
|
||||||
on_authentication_success(user, :webauthn)
|
on_authentication_success(user, :webauthn)
|
||||||
render json: { redirect_path: root_path }, status: :ok
|
render json: { redirect_path: after_sign_in_path_for(user) }, status: :ok
|
||||||
else
|
else
|
||||||
on_authentication_failure(user, :webauthn, :invalid_credential)
|
on_authentication_failure(user, :webauthn, :invalid_credential)
|
||||||
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
|
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module UserTrackingConcern
|
module UserTrackingConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
UPDATE_SIGN_IN_HOURS = 24
|
UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :update_user_sign_in
|
before_action :update_user_sign_in
|
||||||
|
@ -12,10 +12,10 @@ module UserTrackingConcern
|
||||||
private
|
private
|
||||||
|
|
||||||
def update_user_sign_in
|
def update_user_sign_in
|
||||||
current_user.update_sign_in!(request) if user_needs_sign_in_update?
|
current_user.update_sign_in! if user_needs_sign_in_update?
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_needs_sign_in_update?
|
def user_needs_sign_in_update?
|
||||||
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
|
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,12 @@ class MediaController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_media_attachment
|
def set_media_attachment
|
||||||
@media_attachment = MediaAttachment.attached.find_by!(shortcode: params[:id] || params[:medium_id])
|
id = params[:id] || params[:medium_id]
|
||||||
|
return if id.nil?
|
||||||
|
|
||||||
|
scope = MediaAttachment.local.attached
|
||||||
|
# If id is 19 characters long, it's a shortcode, otherwise it's an identifier
|
||||||
|
@media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find_by!(id: id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_permitted_status!
|
def verify_permitted_status!
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Settings::IdentityProofsController < Settings::BaseController
|
|
||||||
before_action :check_required_params, only: :new
|
|
||||||
before_action :check_enabled, only: :new
|
|
||||||
|
|
||||||
def index
|
|
||||||
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
|
|
||||||
@proofs.each(&:refresh!)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new
|
|
||||||
@proof = current_account.identity_proofs.new(
|
|
||||||
token: params[:token],
|
|
||||||
provider: params[:provider],
|
|
||||||
provider_username: params[:provider_username]
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_account.username.casecmp(params[:username]).zero?
|
|
||||||
render layout: 'auth'
|
|
||||||
else
|
|
||||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
|
|
||||||
@proof.token = resource_params[:token]
|
|
||||||
|
|
||||||
if @proof.save
|
|
||||||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
|
|
||||||
redirect_to @proof.on_success_path(params[:user_agent])
|
|
||||||
else
|
|
||||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@proof = current_account.identity_proofs.find(params[:id])
|
|
||||||
@proof.destroy!
|
|
||||||
redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_enabled
|
|
||||||
not_found unless Setting.enable_keybase
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_required_params
|
|
||||||
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? }
|
|
||||||
end
|
|
||||||
|
|
||||||
def resource_params
|
|
||||||
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def publish_proof?
|
|
||||||
ActiveModel::Type::Boolean.new.cast(post_params[:post_status])
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_params
|
|
||||||
params.require(:account_identity_proof).permit(:post_status, :status_text)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,17 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module WellKnown
|
|
||||||
class KeybaseProofConfigController < ActionController::Base
|
|
||||||
before_action :check_enabled
|
|
||||||
|
|
||||||
def show
|
|
||||||
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer, root: 'keybase_config'
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_enabled
|
|
||||||
head 404 unless Setting.enable_keybase
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -31,11 +31,15 @@ module Admin::ActionLogsHelper
|
||||||
link_to truncate(record.text), edit_admin_announcement_path(record.id)
|
link_to truncate(record.text), edit_admin_announcement_path(record.id)
|
||||||
when 'IpBlock'
|
when 'IpBlock'
|
||||||
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
||||||
|
when 'Instance'
|
||||||
|
record.domain
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_target_from_history(type, attributes)
|
def log_target_from_history(type, attributes)
|
||||||
case type
|
case type
|
||||||
|
when 'User'
|
||||||
|
attributes['username']
|
||||||
when 'CustomEmoji'
|
when 'CustomEmoji'
|
||||||
attributes['shortcode']
|
attributes['shortcode']
|
||||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
||||||
|
@ -52,6 +56,8 @@ module Admin::ActionLogsHelper
|
||||||
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
||||||
when 'IpBlock'
|
when 'IpBlock'
|
||||||
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
|
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
|
||||||
|
when 'Instance'
|
||||||
|
attributes['domain']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,41 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::DashboardHelper
|
module Admin::DashboardHelper
|
||||||
def feature_hint(feature, enabled)
|
def relevant_account_ip(account, ip_query)
|
||||||
indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ')
|
ips = account.user.present? ? account.user.ips.to_a : []
|
||||||
class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint'
|
|
||||||
|
|
||||||
safe_join([feature, content_tag(:span, indicator, class: class_names)])
|
matched_ip = begin
|
||||||
|
ip_query_addr = IPAddr.new(ip_query)
|
||||||
|
ips.find { |ip| ip_query_addr.include?(ip.ip) } || ips.first
|
||||||
|
rescue IPAddr::Error
|
||||||
|
ips.first
|
||||||
|
end
|
||||||
|
|
||||||
|
if matched_ip
|
||||||
|
link_to matched_ip.ip, admin_accounts_path(ip: matched_ip.ip)
|
||||||
|
else
|
||||||
|
'-'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def relevant_account_timestamp(account)
|
||||||
|
timestamp, exact = begin
|
||||||
|
if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
|
||||||
|
[account.user_current_sign_in_at, true]
|
||||||
|
elsif account.user_current_sign_in_at
|
||||||
|
[account.user_current_sign_in_at, false]
|
||||||
|
elsif account.user_pending?
|
||||||
|
[account.user_created_at, true]
|
||||||
|
elsif account.last_status_at.present?
|
||||||
|
[account.last_status_at, true]
|
||||||
|
else
|
||||||
|
[nil, false]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return '-' if timestamp.nil?
|
||||||
|
return t('generic.today') unless exact
|
||||||
|
|
||||||
|
content_tag(:time, l(timestamp), class: 'time-ago', datetime: timestamp.iso8601, title: l(timestamp))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,11 +6,14 @@ module Admin::FilterHelper
|
||||||
CustomEmojiFilter::KEYS,
|
CustomEmojiFilter::KEYS,
|
||||||
ReportFilter::KEYS,
|
ReportFilter::KEYS,
|
||||||
TagFilter::KEYS,
|
TagFilter::KEYS,
|
||||||
|
PreviewCardProviderFilter::KEYS,
|
||||||
|
PreviewCardFilter::KEYS,
|
||||||
InstanceFilter::KEYS,
|
InstanceFilter::KEYS,
|
||||||
InviteFilter::KEYS,
|
InviteFilter::KEYS,
|
||||||
RelationshipFilter::KEYS,
|
RelationshipFilter::KEYS,
|
||||||
AnnouncementFilter::KEYS,
|
AnnouncementFilter::KEYS,
|
||||||
Admin::ActionLogFilter::KEYS,
|
Admin::ActionLogFilter::KEYS,
|
||||||
|
Admin::StatusFilter::KEYS,
|
||||||
].flatten.freeze
|
].flatten.freeze
|
||||||
|
|
||||||
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||||
|
|
|
@ -8,4 +8,8 @@ module Admin::SettingsHelper
|
||||||
link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
|
link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
|
||||||
safe_join([hint, link], '<br/>'.html_safe)
|
safe_join([hint, link], '<br/>'.html_safe)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def captcha_available?
|
||||||
|
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,13 +50,39 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def available_sign_up_path
|
def available_sign_up_path
|
||||||
if closed_registrations?
|
if closed_registrations? || omniauth_only?
|
||||||
'https://joinmastodon.org/#getting-started'
|
'https://joinmastodon.org/#getting-started'
|
||||||
else
|
else
|
||||||
new_user_registration_path
|
new_user_registration_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_login(name = nil, html_options = nil, &block)
|
||||||
|
target = new_user_session_path
|
||||||
|
|
||||||
|
html_options = name if block_given?
|
||||||
|
|
||||||
|
if omniauth_only? && Devise.mappings[:user].omniauthable? && User.omniauth_providers.size == 1
|
||||||
|
target = omniauth_authorize_path(:user, User.omniauth_providers[0])
|
||||||
|
html_options ||= {}
|
||||||
|
html_options[:method] = :post
|
||||||
|
end
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
link_to(target, html_options, &block)
|
||||||
|
else
|
||||||
|
link_to(name, target, html_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_sign_in_link(provider)
|
||||||
|
link_to I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize), omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
||||||
|
end
|
||||||
|
|
||||||
def open_deletion?
|
def open_deletion?
|
||||||
Setting.open_deletion
|
Setting.open_deletion
|
||||||
end
|
end
|
||||||
|
@ -137,6 +163,10 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def react_admin_component(name, props = {})
|
||||||
|
content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) })
|
||||||
|
end
|
||||||
|
|
||||||
def body_classes
|
def body_classes
|
||||||
output = (@body_classes || '').split(' ')
|
output = (@body_classes || '').split(' ')
|
||||||
output << "flavour-#{current_flavour.parameterize}"
|
output << "flavour-#{current_flavour.parameterize}"
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ContextHelper
|
||||||
|
NAMED_CONTEXT_MAP = {
|
||||||
|
activitystreams: 'https://www.w3.org/ns/activitystreams',
|
||||||
|
security: 'https://w3id.org/security/v1',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
CONTEXT_EXTENSION_MAP = {
|
||||||
|
direct_message: { 'litepub' => 'http://litepub.social/ns#', 'directMessage' => 'litepub:directMessage' },
|
||||||
|
manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' },
|
||||||
|
sensitive: { 'sensitive' => 'as:sensitive' },
|
||||||
|
hashtag: { 'Hashtag' => 'as:Hashtag' },
|
||||||
|
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
|
||||||
|
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
|
||||||
|
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
|
||||||
|
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
|
||||||
|
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
|
||||||
|
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
|
||||||
|
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
|
||||||
|
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
|
||||||
|
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||||
|
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||||
|
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||||
|
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
||||||
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def full_context
|
||||||
|
serialized_context(NAMED_CONTEXT_MAP, CONTEXT_EXTENSION_MAP)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialized_context(named_contexts_map, context_extensions_map)
|
||||||
|
context_array = []
|
||||||
|
|
||||||
|
named_contexts = named_contexts_map.keys
|
||||||
|
context_extensions = context_extensions_map.keys
|
||||||
|
|
||||||
|
named_contexts.each do |key|
|
||||||
|
context_array << NAMED_CONTEXT_MAP[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
extensions = context_extensions.each_with_object({}) do |key, h|
|
||||||
|
h.merge!(CONTEXT_EXTENSION_MAP[key])
|
||||||
|
end
|
||||||
|
|
||||||
|
context_array << extensions unless extensions.empty?
|
||||||
|
|
||||||
|
if context_array.size == 1
|
||||||
|
context_array.first
|
||||||
|
else
|
||||||
|
context_array
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module JsonLdHelper
|
module JsonLdHelper
|
||||||
|
include ContextHelper
|
||||||
|
|
||||||
def equals_or_includes?(haystack, needle)
|
def equals_or_includes?(haystack, needle)
|
||||||
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
||||||
end
|
end
|
||||||
|
@ -34,7 +36,13 @@ module JsonLdHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_array(value)
|
def as_array(value)
|
||||||
value.is_a?(Array) ? value : [value]
|
if value.nil?
|
||||||
|
[]
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
value
|
||||||
|
else
|
||||||
|
[value]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def value_or_id(value)
|
def value_or_id(value)
|
||||||
|
@ -63,6 +71,84 @@ module JsonLdHelper
|
||||||
graph.dump(:normalize)
|
graph.dump(:normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def compact(json)
|
||||||
|
compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context))
|
||||||
|
compacted['signature'] = json['signature']
|
||||||
|
compacted
|
||||||
|
end
|
||||||
|
|
||||||
|
# Patches a JSON-LD document to avoid compatibility issues on redistribution
|
||||||
|
#
|
||||||
|
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
|
||||||
|
# means other extension namespaces will be expanded, malformed JSON-LD
|
||||||
|
# attributes lost, and some values “unexpectedly” compacted this method
|
||||||
|
# patches the following likely sources of incompatibility:
|
||||||
|
# - 'https://www.w3.org/ns/activitystreams#Public' being compacted to
|
||||||
|
# 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand
|
||||||
|
# 'as:Public')
|
||||||
|
# - single-item arrays being compacted to the item itself (`[foo]` being
|
||||||
|
# compacted to `foo`)
|
||||||
|
#
|
||||||
|
# It is not always possible for `patch_for_forwarding!` to produce a document
|
||||||
|
# deemed safe for forwarding. Use `safe_for_forwarding?` to check the status
|
||||||
|
# of the output document.
|
||||||
|
#
|
||||||
|
# @param original [Hash] The original JSON-LD document used as reference
|
||||||
|
# @param compacted [Hash] The compacted JSON-LD document to be patched
|
||||||
|
# @return [void]
|
||||||
|
def patch_for_forwarding!(original, compacted)
|
||||||
|
original.without('@context', 'signature').each do |key, value|
|
||||||
|
next if value.nil? || !compacted.key?(key)
|
||||||
|
|
||||||
|
compacted_value = compacted[key]
|
||||||
|
if value.is_a?(Hash) && compacted_value.is_a?(Hash)
|
||||||
|
patch_for_forwarding!(value, compacted_value)
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
|
||||||
|
return if value.size != compacted_value.size
|
||||||
|
|
||||||
|
compacted[key] = value.zip(compacted_value).map do |v, vc|
|
||||||
|
if v.is_a?(Hash) && vc.is_a?(Hash)
|
||||||
|
patch_for_forwarding!(v, vc)
|
||||||
|
vc
|
||||||
|
elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public'
|
||||||
|
v
|
||||||
|
else
|
||||||
|
vc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public'
|
||||||
|
compacted[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tests whether a JSON-LD compaction is deemed safe for redistribution,
|
||||||
|
# that is, if it doesn't change its meaning to consumers that do not actually
|
||||||
|
# handle JSON-LD, but rely on values being serialized in a certain way.
|
||||||
|
#
|
||||||
|
# See `patch_for_forwarding!` for details.
|
||||||
|
#
|
||||||
|
# @param original [Hash] The original JSON-LD document used as reference
|
||||||
|
# @param compacted [Hash] The compacted JSON-LD document to be patched
|
||||||
|
# @return [Boolean] Whether the patched document is deemed safe
|
||||||
|
def safe_for_forwarding?(original, compacted)
|
||||||
|
original.without('@context', 'signature').all? do |key, value|
|
||||||
|
compacted_value = compacted[key]
|
||||||
|
return false unless value.class == compacted_value.class
|
||||||
|
|
||||||
|
if value.is_a?(Hash)
|
||||||
|
safe_for_forwarding?(value, compacted_value)
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
value.zip(compacted_value).all? do |v, vc|
|
||||||
|
v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc
|
||||||
|
end
|
||||||
|
else
|
||||||
|
value == compacted_value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||||
unless id
|
unless id
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module LanguagesHelper
|
||||||
|
HUMAN_LOCALES = {
|
||||||
|
af: 'Afrikaans',
|
||||||
|
ar: 'العربية',
|
||||||
|
ast: 'Asturianu',
|
||||||
|
bg: 'Български',
|
||||||
|
bn: 'বাংলা',
|
||||||
|
br: 'Breton',
|
||||||
|
ca: 'Català',
|
||||||
|
co: 'Corsu',
|
||||||
|
cs: 'Čeština',
|
||||||
|
cy: 'Cymraeg',
|
||||||
|
da: 'Dansk',
|
||||||
|
de: 'Deutsch',
|
||||||
|
el: 'Ελληνικά',
|
||||||
|
en: 'English',
|
||||||
|
eo: 'Esperanto',
|
||||||
|
'es-AR': 'Español (Argentina)',
|
||||||
|
'es-MX': 'Español (México)',
|
||||||
|
es: 'Español',
|
||||||
|
et: 'Eesti',
|
||||||
|
eu: 'Euskara',
|
||||||
|
fa: 'فارسی',
|
||||||
|
fi: 'Suomi',
|
||||||
|
fr: 'Français',
|
||||||
|
ga: 'Gaeilge',
|
||||||
|
gd: 'Gàidhlig',
|
||||||
|
gl: 'Galego',
|
||||||
|
he: 'עברית',
|
||||||
|
hi: 'हिन्दी',
|
||||||
|
hr: 'Hrvatski',
|
||||||
|
hu: 'Magyar',
|
||||||
|
hy: 'Հայերեն',
|
||||||
|
id: 'Bahasa Indonesia',
|
||||||
|
io: 'Ido',
|
||||||
|
is: 'Íslenska',
|
||||||
|
it: 'Italiano',
|
||||||
|
ja: '日本語',
|
||||||
|
ka: 'ქართული',
|
||||||
|
kab: 'Taqbaylit',
|
||||||
|
kk: 'Қазақша',
|
||||||
|
kmr: 'Kurmancî',
|
||||||
|
kn: 'ಕನ್ನಡ',
|
||||||
|
ko: '한국어',
|
||||||
|
ku: 'سۆرانی',
|
||||||
|
lt: 'Lietuvių',
|
||||||
|
lv: 'Latviešu',
|
||||||
|
mk: 'Македонски',
|
||||||
|
ml: 'മലയാളം',
|
||||||
|
mr: 'मराठी',
|
||||||
|
ms: 'Bahasa Melayu',
|
||||||
|
nl: 'Nederlands',
|
||||||
|
nn: 'Nynorsk',
|
||||||
|
no: 'Norsk',
|
||||||
|
oc: 'Occitan',
|
||||||
|
pl: 'Polski',
|
||||||
|
'pt-BR': 'Português (Brasil)',
|
||||||
|
'pt-PT': 'Português (Portugal)',
|
||||||
|
pt: 'Português',
|
||||||
|
ro: 'Română',
|
||||||
|
ru: 'Русский',
|
||||||
|
sa: 'संस्कृतम्',
|
||||||
|
sc: 'Sardu',
|
||||||
|
si: 'සිංහල',
|
||||||
|
sk: 'Slovenčina',
|
||||||
|
sl: 'Slovenščina',
|
||||||
|
sq: 'Shqip',
|
||||||
|
'sr-Latn': 'Srpski (latinica)',
|
||||||
|
sr: 'Српски',
|
||||||
|
sv: 'Svenska',
|
||||||
|
ta: 'தமிழ்',
|
||||||
|
te: 'తెలుగు',
|
||||||
|
th: 'ไทย',
|
||||||
|
tr: 'Türkçe',
|
||||||
|
uk: 'Українська',
|
||||||
|
ur: 'اُردُو',
|
||||||
|
vi: 'Tiếng Việt',
|
||||||
|
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
||||||
|
'zh-CN': '简体中文',
|
||||||
|
'zh-HK': '繁體中文(香港)',
|
||||||
|
'zh-TW': '繁體中文(臺灣)',
|
||||||
|
zh: '中文',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def human_locale(locale)
|
||||||
|
if locale == 'und'
|
||||||
|
I18n.t('generic.none')
|
||||||
|
else
|
||||||
|
HUMAN_LOCALES[locale.to_sym] || locale
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,94 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SettingsHelper
|
module SettingsHelper
|
||||||
HUMAN_LOCALES = {
|
|
||||||
af: 'Afrikaans',
|
|
||||||
ar: 'العربية',
|
|
||||||
ast: 'Asturianu',
|
|
||||||
bg: 'Български',
|
|
||||||
bn: 'বাংলা',
|
|
||||||
br: 'Breton',
|
|
||||||
ca: 'Català',
|
|
||||||
co: 'Corsu',
|
|
||||||
cs: 'Čeština',
|
|
||||||
cy: 'Cymraeg',
|
|
||||||
da: 'Dansk',
|
|
||||||
de: 'Deutsch',
|
|
||||||
el: 'Ελληνικά',
|
|
||||||
en: 'English',
|
|
||||||
eo: 'Esperanto',
|
|
||||||
'es-AR': 'Español (Argentina)',
|
|
||||||
'es-MX': 'Español (México)',
|
|
||||||
es: 'Español',
|
|
||||||
et: 'Eesti',
|
|
||||||
eu: 'Euskara',
|
|
||||||
fa: 'فارسی',
|
|
||||||
fi: 'Suomi',
|
|
||||||
fr: 'Français',
|
|
||||||
ga: 'Gaeilge',
|
|
||||||
gd: 'Gàidhlig',
|
|
||||||
gl: 'Galego',
|
|
||||||
he: 'עברית',
|
|
||||||
hi: 'हिन्दी',
|
|
||||||
hr: 'Hrvatski',
|
|
||||||
hu: 'Magyar',
|
|
||||||
hy: 'Հայերեն',
|
|
||||||
id: 'Bahasa Indonesia',
|
|
||||||
io: 'Ido',
|
|
||||||
is: 'Íslenska',
|
|
||||||
it: 'Italiano',
|
|
||||||
ja: '日本語',
|
|
||||||
ka: 'ქართული',
|
|
||||||
kab: 'Taqbaylit',
|
|
||||||
kk: 'Қазақша',
|
|
||||||
kn: 'ಕನ್ನಡ',
|
|
||||||
ko: '한국어',
|
|
||||||
ku: 'سۆرانی',
|
|
||||||
lt: 'Lietuvių',
|
|
||||||
lv: 'Latviešu',
|
|
||||||
mk: 'Македонски',
|
|
||||||
ml: 'മലയാളം',
|
|
||||||
mr: 'मराठी',
|
|
||||||
ms: 'Bahasa Melayu',
|
|
||||||
nl: 'Nederlands',
|
|
||||||
nn: 'Nynorsk',
|
|
||||||
no: 'Norsk',
|
|
||||||
oc: 'Occitan',
|
|
||||||
pl: 'Polski',
|
|
||||||
'pt-BR': 'Português (Brasil)',
|
|
||||||
'pt-PT': 'Português (Portugal)',
|
|
||||||
pt: 'Português',
|
|
||||||
ro: 'Română',
|
|
||||||
ru: 'Русский',
|
|
||||||
sa: 'संस्कृतम्',
|
|
||||||
sc: 'Sardu',
|
|
||||||
si: 'සිංහල',
|
|
||||||
sk: 'Slovenčina',
|
|
||||||
sl: 'Slovenščina',
|
|
||||||
sq: 'Shqip',
|
|
||||||
'sr-Latn': 'Srpski (latinica)',
|
|
||||||
sr: 'Српски',
|
|
||||||
sv: 'Svenska',
|
|
||||||
ta: 'தமிழ்',
|
|
||||||
te: 'తెలుగు',
|
|
||||||
th: 'ไทย',
|
|
||||||
tr: 'Türkçe',
|
|
||||||
uk: 'Українська',
|
|
||||||
ur: 'اُردُو',
|
|
||||||
vi: 'Tiếng Việt',
|
|
||||||
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
|
||||||
'zh-CN': '简体中文',
|
|
||||||
'zh-HK': '繁體中文(香港)',
|
|
||||||
'zh-TW': '繁體中文(臺灣)',
|
|
||||||
zh: '中文',
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def human_locale(locale)
|
|
||||||
HUMAN_LOCALES[locale]
|
|
||||||
end
|
|
||||||
|
|
||||||
def filterable_languages
|
def filterable_languages
|
||||||
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
|
LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
|
||||||
end
|
end
|
||||||
|
|
||||||
def hash_to_object(hash)
|
def hash_to_object(hash)
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
|
||||||
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||||
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
||||||
|
export const COMPOSE_SUGGESTION_IGNORE = 'COMPOSE_SUGGESTION_IGNORE';
|
||||||
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
||||||
|
|
||||||
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
||||||
|
@ -285,12 +286,15 @@ export function uploadCompose(files) {
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
dispatch(uploadComposeSuccess(data, f));
|
dispatch(uploadComposeSuccess(data, f));
|
||||||
} else if (status === 202) {
|
} else if (status === 202) {
|
||||||
|
let tryCount = 1;
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
dispatch(uploadComposeSuccess(response.data, f));
|
dispatch(uploadComposeSuccess(response.data, f));
|
||||||
} else if (response.status === 206) {
|
} else if (response.status === 206) {
|
||||||
setTimeout(() => poll(), 1000);
|
let retryAfter = (Math.log2(tryCount) || 1) * 1000;
|
||||||
|
tryCount += 1;
|
||||||
|
setTimeout(() => poll(), retryAfter);
|
||||||
}
|
}
|
||||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
|
@ -562,13 +566,25 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||||
completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']);
|
completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
||||||
type: COMPOSE_SUGGESTION_SELECT,
|
// the suggestions are dismissed and the cursor moves forward.
|
||||||
position,
|
if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) {
|
||||||
token,
|
dispatch({
|
||||||
completion,
|
type: COMPOSE_SUGGESTION_SELECT,
|
||||||
path,
|
position,
|
||||||
});
|
token,
|
||||||
|
completion,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_SUGGESTION_IGNORE,
|
||||||
|
position,
|
||||||
|
token,
|
||||||
|
completion,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -54,15 +54,16 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.poll = status.poll.id;
|
normalStatus.poll = status.poll.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only calculate these values when status first encountered
|
// Only calculate these values when status first encountered and
|
||||||
// Otherwise keep the ones already in the reducer
|
// when the underlying values change. Otherwise keep the ones
|
||||||
if (normalOldStatus) {
|
// already in the reducer
|
||||||
|
if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) {
|
||||||
normalStatus.search_index = normalOldStatus.get('search_index');
|
normalStatus.search_index = normalOldStatus.get('search_index');
|
||||||
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
} else {
|
} else {
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.spoiler_text || '';
|
||||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus);
|
const emojiMap = makeEmojiMap(normalStatus);
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
|
|
|
@ -128,6 +128,9 @@ export function deleteStatusFail(id, error) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateStatus = status => dispatch =>
|
||||||
|
dispatch(importFetchedStatus(status));
|
||||||
|
|
||||||
export function fetchContext(id) {
|
export function fetchContext(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchContextRequest(id));
|
dispatch(fetchContextRequest(id));
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
|
import { updateStatus } from './statuses';
|
||||||
import {
|
import {
|
||||||
fetchAnnouncements,
|
fetchAnnouncements,
|
||||||
updateAnnouncements,
|
updateAnnouncements,
|
||||||
|
@ -75,6 +76,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
case 'update':
|
case 'update':
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
||||||
break;
|
break;
|
||||||
|
case 'status.update':
|
||||||
|
dispatch(updateStatus(JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'flavours/glitch/util/api';
|
||||||
|
import { FormattedNumber } from 'react-intl';
|
||||||
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
|
|
||||||
|
const percIncrease = (a, b) => {
|
||||||
|
let percent;
|
||||||
|
|
||||||
|
if (b !== 0) {
|
||||||
|
if (a !== 0) {
|
||||||
|
percent = (b - a) / a;
|
||||||
|
} else {
|
||||||
|
percent = 1;
|
||||||
|
}
|
||||||
|
} else if (b === 0 && a === 0) {
|
||||||
|
percent = 0;
|
||||||
|
} else {
|
||||||
|
percent = - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return percent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Counter extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
measure: PropTypes.string.isRequired,
|
||||||
|
start_at: PropTypes.string.isRequired,
|
||||||
|
end_at: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
href: PropTypes.string,
|
||||||
|
params: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { measure, start_at, end_at, params } = this.props;
|
||||||
|
|
||||||
|
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { label, href } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className='sparkline__value__total'><Skeleton width={43} /></span>
|
||||||
|
<span className='sparkline__value__change'><Skeleton width={43} /></span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const measure = data[0];
|
||||||
|
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
|
||||||
|
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inner = (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='sparkline__value'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sparkline__label'>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sparkline__graph'>
|
||||||
|
{!loading && (
|
||||||
|
<Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
|
||||||
|
<SparklinesCurve />
|
||||||
|
</Sparklines>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<a href={href} className='sparkline'>
|
||||||
|
{inner}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className='sparkline'>
|
||||||
|
{inner}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'flavours/glitch/util/api';
|
||||||
|
import { FormattedNumber } from 'react-intl';
|
||||||
|
import { roundTo10 } from 'flavours/glitch/util/numbers';
|
||||||
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
|
|
||||||
|
export default class Dimension extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dimension: PropTypes.string.isRequired,
|
||||||
|
start_at: PropTypes.string.isRequired,
|
||||||
|
end_at: PropTypes.string.isRequired,
|
||||||
|
limit: PropTypes.number.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
params: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { start_at, end_at, dimension, limit, params } = this.props;
|
||||||
|
|
||||||
|
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { label, limit } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Array.from(Array(limit)).map((_, i) => (
|
||||||
|
<tr className='dimension__item' key={i}>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<Skeleton width={100} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
<Skeleton width={60} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{data[0].data.map(item => (
|
||||||
|
<tr className='dimension__item' key={item.key}>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
|
||||||
|
<span title={item.key}>{item.human_key}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
{typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dimension'>
|
||||||
|
<h4>{label}</h4>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'flavours/glitch/util/api';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
||||||
|
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
||||||
|
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class Category extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { id, disabled, onSelect } = this.props;
|
||||||
|
|
||||||
|
if (!disabled) {
|
||||||
|
onSelect(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { id, text, disabled, selected, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
|
||||||
|
{selected && <input type='hidden' name='report[category]' value={id} />}
|
||||||
|
|
||||||
|
<div className='report-reason-selector__category__label'>
|
||||||
|
<span className={classNames('poll__input', { active: selected, disabled })} />
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(selected && children) && (
|
||||||
|
<div className='report-reason-selector__category__rules'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rule extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
onToggle: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { id, disabled, onToggle } = this.props;
|
||||||
|
|
||||||
|
if (!disabled) {
|
||||||
|
onToggle(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { id, text, disabled, selected } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
|
||||||
|
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
|
||||||
|
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class ReportReasonSelector extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
category: PropTypes.string.isRequired,
|
||||||
|
rule_ids: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
category: this.props.category,
|
||||||
|
rule_ids: this.props.rule_ids || [],
|
||||||
|
rules: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
api().get('/api/v1/instance').then(res => {
|
||||||
|
this.setState({
|
||||||
|
rules: res.data.rules,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_save = () => {
|
||||||
|
const { id, disabled } = this.props;
|
||||||
|
const { category, rule_ids } = this.state;
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api().put(`/api/v1/admin/reports/${id}`, {
|
||||||
|
category,
|
||||||
|
rule_ids,
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelect = id => {
|
||||||
|
this.setState({ category: id }, () => this._save());
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggle = id => {
|
||||||
|
const { rule_ids } = this.state;
|
||||||
|
|
||||||
|
if (rule_ids.includes(id)) {
|
||||||
|
this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
|
||||||
|
} else {
|
||||||
|
this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { disabled, intl } = this.props;
|
||||||
|
const { rules, category, rule_ids } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='report-reason-selector'>
|
||||||
|
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
|
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
|
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
||||||
|
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
||||||
|
</Category>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'flavours/glitch/util/api';
|
||||||
|
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { roundTo10 } from 'flavours/glitch/util/numbers';
|
||||||
|
|
||||||
|
const dateForCohort = cohort => {
|
||||||
|
switch(cohort.frequency) {
|
||||||
|
case 'day':
|
||||||
|
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||||
|
default:
|
||||||
|
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Retention extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
start_at: PropTypes.string,
|
||||||
|
end_at: PropTypes.string,
|
||||||
|
frequency: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { start_at, end_at, frequency } = this.props;
|
||||||
|
|
||||||
|
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
const { frequency } = this.props;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<table className='retention__table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div className='retention__table__date retention__table__label'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
<div className='retention__table__number retention__table__label'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{data[0].data.slice(1).map((retention, i) => (
|
||||||
|
<th key={retention.date}>
|
||||||
|
<div className='retention__table__number retention__table__label'>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__date retention__table__average'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__size'>
|
||||||
|
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{data[0].data.slice(1).map((retention, i) => {
|
||||||
|
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td key={retention.date}>
|
||||||
|
<div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
|
||||||
|
<FormattedNumber value={average} style='percent' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{data.slice(0, -1).map(cohort => (
|
||||||
|
<tr key={cohort.period}>
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__date'>
|
||||||
|
{dateForCohort(cohort)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__size'>
|
||||||
|
<FormattedNumber value={cohort.data[0].value} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{cohort.data.slice(1).map(retention => (
|
||||||
|
<td key={retention.date}>
|
||||||
|
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
|
||||||
|
<FormattedNumber value={retention.rate} style='percent' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = null;
|
||||||
|
switch(frequency) {
|
||||||
|
case 'day':
|
||||||
|
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='retention'>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'flavours/glitch/util/api';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||||
|
|
||||||
|
export default class Trends extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
limit: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { limit } = this.props;
|
||||||
|
|
||||||
|
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { limit } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
{Array.from(Array(limit)).map((_, i) => (
|
||||||
|
<Hashtag key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
{data.map(hashtag => (
|
||||||
|
<Hashtag
|
||||||
|
key={hashtag.name}
|
||||||
|
name={hashtag.name}
|
||||||
|
href={`/admin/tags/${hashtag.id}`}
|
||||||
|
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
||||||
|
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
||||||
|
history={hashtag.history.reverse().map(day => day.uses)}
|
||||||
|
className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='trends trends--compact'>
|
||||||
|
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||||
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
class SilentErrorBoundary extends React.Component {
|
class SilentErrorBoundary extends React.Component {
|
||||||
|
|
||||||
|
@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Hashtag = ({ hashtag }) => (
|
export const ImmutableHashtag = ({ hashtag }) => (
|
||||||
<div className='trends__item'>
|
<Hashtag
|
||||||
|
name={hashtag.get('name')}
|
||||||
|
href={hashtag.get('url')}
|
||||||
|
to={`/tags/${hashtag.get('name')}`}
|
||||||
|
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
||||||
|
uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
|
||||||
|
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
ImmutableHashtag.propTypes = {
|
||||||
|
hashtag: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Hashtag = ({ name, href, to, people, uses, history, className }) => (
|
||||||
|
<div className={classNames('trends__item', className)}>
|
||||||
<div className='trends__item__name'>
|
<div className='trends__item__name'>
|
||||||
<Permalink
|
<Permalink href={href} to={to}>
|
||||||
href={hashtag.get('url')}
|
{name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
|
||||||
to={`/tags/${hashtag.get('name')}`}
|
|
||||||
>
|
|
||||||
#<span>{hashtag.get('name')}</span>
|
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<ShortNumber
|
{typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
|
||||||
value={
|
|
||||||
hashtag.getIn(['history', 0, 'accounts']) * 1 +
|
|
||||||
hashtag.getIn(['history', 1, 'accounts']) * 1
|
|
||||||
}
|
|
||||||
renderer={accountsCountRenderer}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__current'>
|
<div className='trends__item__current'>
|
||||||
<ShortNumber
|
{typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
|
||||||
value={
|
|
||||||
hashtag.getIn(['history', 0, 'uses']) * 1 +
|
|
||||||
hashtag.getIn(['history', 1, 'uses']) * 1
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
<div className='trends__item__sparkline'>
|
||||||
<SilentErrorBoundary>
|
<SilentErrorBoundary>
|
||||||
<Sparklines
|
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
||||||
width={50}
|
|
||||||
height={28}
|
|
||||||
data={hashtag
|
|
||||||
.get('history')
|
|
||||||
.reverse()
|
|
||||||
.map((day) => day.get('uses'))
|
|
||||||
.toArray()}
|
|
||||||
>
|
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
</SilentErrorBoundary>
|
</SilentErrorBoundary>
|
||||||
|
@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
Hashtag.propTypes = {
|
Hashtag.propTypes = {
|
||||||
hashtag: ImmutablePropTypes.map.isRequired,
|
name: PropTypes.string,
|
||||||
|
href: PropTypes.string,
|
||||||
|
to: PropTypes.string,
|
||||||
|
people: PropTypes.number,
|
||||||
|
uses: PropTypes.number,
|
||||||
|
history: PropTypes.arrayOf(PropTypes.number),
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Hashtag;
|
export default Hashtag;
|
||||||
|
|
|
@ -12,8 +12,18 @@ import RelativeTimestamp from './relative_timestamp';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
closed: {
|
||||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
|
id: 'poll.closed',
|
||||||
|
defaultMessage: 'Closed',
|
||||||
|
},
|
||||||
|
voted: {
|
||||||
|
id: 'poll.voted',
|
||||||
|
defaultMessage: 'You voted for this answer',
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
id: 'poll.votes',
|
||||||
|
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||||
|
@ -148,9 +158,16 @@ class Poll extends ImmutablePureComponent {
|
||||||
data-index={optionIndex}
|
data-index={optionIndex}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showResults && <span className='poll__number'>
|
{showResults && (
|
||||||
{Math.round(percent)}%
|
<span
|
||||||
</span>}
|
className='poll__number'
|
||||||
|
title={intl.formatMessage(messages.votes, {
|
||||||
|
votes: option.get('votes_count'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Math.round(percent)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
|
||||||
|
|
||||||
|
Skeleton.propTypes = {
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Skeleton;
|
|
@ -425,6 +425,7 @@ class Status extends ImmutablePureComponent {
|
||||||
handleHotkeyOpen = () => {
|
handleHotkeyOpen = () => {
|
||||||
let state = {...this.context.router.history.location.state};
|
let state = {...this.context.router.history.location.state};
|
||||||
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
|
||||||
|
const status = this.props.status;
|
||||||
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
|
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ const messages = defineMessages({
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||||
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
||||||
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -196,6 +197,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
@ -212,7 +214,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
||||||
if (writtenByMe && publicStatus) {
|
if (writtenByMe && pinnableStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
@ -323,7 +325,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
</div>,
|
</div>,
|
||||||
]}
|
]}
|
||||||
|
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
|
||||||
|
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
|
import { getLocale } from 'mastodon/locales';
|
||||||
|
|
||||||
|
const { localeData, messages } = getLocale();
|
||||||
|
addLocaleData(localeData);
|
||||||
|
|
||||||
|
export default class AdminComponent extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
locale: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { locale, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales';
|
||||||
import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
|
import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
|
||||||
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
||||||
import Poll from 'flavours/glitch/components/poll';
|
import Poll from 'flavours/glitch/components/poll';
|
||||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||||
import ModalRoot from 'flavours/glitch/components/modal_root';
|
import ModalRoot from 'flavours/glitch/components/modal_root';
|
||||||
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
||||||
import Video from 'flavours/glitch/features/video';
|
import Video from 'flavours/glitch/features/video';
|
||||||
|
|
|
@ -58,6 +58,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onPickEmoji: PropTypes.func,
|
onPickEmoji: PropTypes.func,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
|
isInReply: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
|
|
||||||
advancedOptions: ImmutablePropTypes.map,
|
advancedOptions: ImmutablePropTypes.map,
|
||||||
|
@ -233,7 +234,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
// Caret/selection handling.
|
// Caret/selection handling.
|
||||||
if (focusDate !== prevProps.focusDate) {
|
if (focusDate !== prevProps.focusDate) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case preselectDate !== prevProps.preselectDate && preselectOnReply:
|
case preselectDate !== prevProps.preselectDate && this.props.isInReply && preselectOnReply:
|
||||||
selectionStart = text.search(/\s/) + 1;
|
selectionStart = text.search(/\s/) + 1;
|
||||||
selectionEnd = text.length;
|
selectionEnd = text.length;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import { searchEnabled } from 'flavours/glitch/util/initial_state';
|
import { searchEnabled } from 'flavours/glitch/util/initial_state';
|
||||||
import LoadMore from 'flavours/glitch/components/load_more';
|
import LoadMore from 'flavours/glitch/components/load_more';
|
||||||
|
|
|
@ -68,6 +68,7 @@ function mapStateToProps (state) {
|
||||||
spoilersAlwaysOn: spoilersAlwaysOn,
|
spoilersAlwaysOn: spoilersAlwaysOn,
|
||||||
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
|
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
|
||||||
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
|
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
|
||||||
|
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default class Trends extends ImmutablePureComponent {
|
export default class Trends extends ImmutablePureComponent {
|
||||||
|
|
|
@ -33,8 +33,8 @@ class ColumnSettings extends React.PureComponent {
|
||||||
tags (mode) {
|
tags (mode) {
|
||||||
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
||||||
|
|
||||||
if (tags.toJSON) {
|
if (tags.toJS) {
|
||||||
return tags.toJSON();
|
return tags.toJS();
|
||||||
} else {
|
} else {
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,21 +11,22 @@ const mapStateToProps = (state, { columnId }) => {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { settings: columns.get(index).get('params') };
|
return {
|
||||||
|
settings: columns.get(index).get('params'),
|
||||||
|
onLoad (value) {
|
||||||
|
return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||||
|
return (response.data.hashtags || []).map((tag) => {
|
||||||
|
return { value: tag.name, label: `#${tag.name}` };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { columnId }) => ({
|
const mapDispatchToProps = (dispatch, { columnId }) => ({
|
||||||
onChange (key, value) {
|
onChange (key, value) {
|
||||||
dispatch(changeColumnParams(columnId, key, value));
|
dispatch(changeColumnParams(columnId, key, value));
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad (value) {
|
|
||||||
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
|
||||||
return (response.data.hashtags || []).map((tag) => {
|
|
||||||
return { value: tag.name, label: `#${tag.name}` };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
||||||
|
|
|
@ -27,11 +27,12 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
||||||
|
|
||||||
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
||||||
|
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
||||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||||
|
|
||||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||||
|
@ -58,11 +59,11 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
<div role='group' aria-labelledby='notifications-unread-markers'>
|
||||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
<span id='notifications-unread-markers' className='column-settings__section'>
|
||||||
<FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' />
|
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} />
|
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -146,6 +146,7 @@ class ActionBar extends React.PureComponent {
|
||||||
const { status, intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
|
@ -158,7 +159,7 @@ class ActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
if (publicStatus) {
|
if (pinnableStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content';
|
||||||
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { FormattedDate } from 'react-intl';
|
import { injectIntl, FormattedDate, FormattedMessage } from 'react-intl';
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Video from 'flavours/glitch/features/video';
|
import Video from 'flavours/glitch/features/video';
|
||||||
|
@ -20,7 +20,8 @@ import Icon from 'flavours/glitch/components/icon';
|
||||||
import AnimatedNumber from 'flavours/glitch/components/animated_number';
|
import AnimatedNumber from 'flavours/glitch/components/animated_number';
|
||||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||||
|
|
||||||
export default class DetailedStatus extends ImmutablePureComponent {
|
export default @injectIntl
|
||||||
|
class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
@ -40,6 +41,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
showMedia: PropTypes.bool,
|
showMedia: PropTypes.bool,
|
||||||
usingPiP: PropTypes.bool,
|
usingPiP: PropTypes.bool,
|
||||||
onToggleMediaVisibility: PropTypes.func,
|
onToggleMediaVisibility: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -111,7 +113,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const { expanded, onToggleHidden, settings, usingPiP } = this.props;
|
const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { compact } = this.props;
|
const { compact } = this.props;
|
||||||
|
|
||||||
|
@ -125,6 +127,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
let favouriteLink = '';
|
let favouriteLink = '';
|
||||||
|
let edited = '';
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
if (this.props.measureHeight) {
|
||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
|
@ -258,6 +261,15 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.get('edited_at')) {
|
||||||
|
edited = (
|
||||||
|
<React.Fragment>
|
||||||
|
<React.Fragment> · </React.Fragment>
|
||||||
|
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
|
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
|
||||||
|
@ -283,7 +295,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
</a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue