Sync to 2.4.1

This commit is contained in:
Mike Barnes 2018-06-11 15:23:25 +10:00
commit eebf345440
791 changed files with 22017 additions and 7132 deletions

193
.circleci/config.yml Normal file
View file

@ -0,0 +1,193 @@
version: 2
aliases:
- &defaults
docker:
- image: circleci/ruby:2.5.1-stretch-node
environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost
DB_USER: root
RAILS_ENV: test
PARALLEL_TEST_PROCESSORS: 4
ALLOW_NOPAM: true
CONTINUOUS_INTEGRATION: true
DISABLE_SIMPLECOV: true
working_directory: ~/projects/mastodon/
- &attach_workspace
attach_workspace:
at: ~/projects/
- &persist_to_workspace
persist_to_workspace:
root: ~/projects/
paths:
- ./mastodon/
- &restore_ruby_dependencies
restore_cache:
keys:
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
- v2-ruby-dependencies-
- &install_steps
steps:
- checkout
- *attach_workspace
- restore_cache:
keys:
- v1-node-dependencies-{{ checksum "yarn.lock" }}
- v1-node-dependencies-
- run: yarn install --frozen-lockfile
- save_cache:
key: v1-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: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production
- save_cache:
key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
paths:
- ./.bundle/
- ./vendor/bundle/
- &test_steps
steps:
- *attach_workspace
- *install_system_dependencies
- run: sudo apt-get install -y ffmpeg
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- restore_cache:
keys:
- precompiled-assets-{{ .Branch }}-{{ .Revision }}
- precompiled-assets-{{ .Branch }}-
- precompiled-assets-
- run:
name: Prepare Tests
command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
- run:
name: Run Tests
command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec
jobs:
install:
<<: *defaults
<<: *install_steps
install-ruby2.5:
<<: *defaults
<<: *install_ruby_dependencies
install-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4.4-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
build:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- run: ./bin/rails assets:precompile
- save_cache:
key: precompiled-assets-{{ .Branch }}-{{ .Revision }}
paths:
- ./public/assets
- ./public/packs-test/
test-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5.1-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
<<: *test_steps
test-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4.4-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
<<: *test_steps
test-webui:
<<: *defaults
docker:
- image: circleci/node:8.11.1-stretch
steps:
- *attach_workspace
- run: ./bin/retry yarn test:jest
check-i18n:
<<: *defaults
steps:
- *attach_workspace
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- run: bundle exec i18n-tasks check-normalized
- run: bundle exec i18n-tasks unused
workflows:
version: 2
build-and-test:
jobs:
- install
- install-ruby2.5:
requires:
- install
- install-ruby2.4:
requires:
- install
- build:
requires:
- install-ruby2.5
- test-ruby2.5:
requires:
- install-ruby2.5
- build
- test-ruby2.4:
requires:
- install-ruby2.4
- build
- test-webui:
requires:
- install
- check-i18n:
requires:
- install-ruby2.5

View file

@ -30,6 +30,7 @@ plugins:
channel: eslint-4 channel: eslint-4
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-0-54
scss-lint: scss-lint:
enabled: true enabled: true
exclude_patterns: exclude_patterns:

View file

@ -11,3 +11,4 @@ vendor/bundle
*~ *~
postgres postgres
redis redis
elasticsearch

View file

@ -14,9 +14,9 @@ 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=localhost ES_HOST=$DATA_ELASTIC_HOST
# ES_PORT=9200 ES_PORT=9200
# Optimizations # Optimizations
LD_PRELOAD=/data/lib/libjemalloc.so LD_PRELOAD=/data/lib/libjemalloc.so

View file

@ -81,9 +81,17 @@ SMTP_FROM_ADDRESS=notifications@example.com
# PAPERCLIP_ROOT_URL=/system # PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups # Optional asset host for multi-server setups
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
# if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com # CDN_HOST=https://assets.example.com
# S3 (optional) # S3 (optional)
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# S3_ENABLED=true # S3_ENABLED=true
# S3_BUCKET= # S3_BUCKET=
# AWS_ACCESS_KEY_ID= # AWS_ACCESS_KEY_ID=
@ -93,6 +101,8 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_HOSTNAME=192.168.1.123:9000 # S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details) # S3 (Minio Config (optional) Please check Minio instance for details)
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true # S3_ENABLED=true
# S3_BUCKET= # S3_BUCKET=
# AWS_ACCESS_KEY_ID= # AWS_ACCESS_KEY_ID=
@ -104,11 +114,15 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Swift (optional) # Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true # SWIFT_ENABLED=true
# SWIFT_USERNAME= # SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name # For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT= # SWIFT_TENANT=
# SWIFT_PASSWORD= # SWIFT_PASSWORD=
# Some OpenStack V3 providers require PROJECT_ID (optional)
# SWIFT_PROJECT_ID=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid # Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load. # issues with token rate-limiting during high load.
# SWIFT_AUTH_URL= # SWIFT_AUTH_URL=
@ -210,3 +224,10 @@ STREAMING_CLUSTER_NUM=1
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1" # SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED= # SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL= # SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# If you use transparent proxy to access to hidden service, uncomment following for skipping private address check.
# HIDDEN_SERVICE_VIA_TRANSPARENT_PROXY=true

View file

@ -1,3 +1,9 @@
# Node.js
NODE_ENV=test
# Federation # Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true LOCAL_HTTPS=true
# test pam authentication
PAM_ENABLED=true
PAM_DEFAULT_SERVICE=pam_test
PAM_CONTROLLED_SERVICE=pam_test_controlled

View file

@ -7,12 +7,16 @@ env:
es6: true es6: true
jest: true jest: true
globals:
ATTACHMENT_HOST: false
parser: babel-eslint parser: babel-eslint
plugins: plugins:
- react - react
- jsx-a11y - jsx-a11y
- import - import
- promise
parserOptions: parserOptions:
sourceType: module sourceType: module
@ -109,13 +113,23 @@ rules:
jsx-a11y/accessible-emoji: warn jsx-a11y/accessible-emoji: warn
jsx-a11y/alt-text: warn jsx-a11y/alt-text: warn
jsx-a11y/anchor-has-content: warn jsx-a11y/anchor-has-content: warn
jsx-a11y/anchor-is-valid:
- warn
- components:
- Link
- NavLink
specialLink:
- to
aspect:
- noHref
- invalidHref
- preferButton
jsx-a11y/aria-activedescendant-has-tabindex: warn jsx-a11y/aria-activedescendant-has-tabindex: warn
jsx-a11y/aria-props: warn jsx-a11y/aria-props: warn
jsx-a11y/aria-proptypes: warn jsx-a11y/aria-proptypes: warn
jsx-a11y/aria-role: warn jsx-a11y/aria-role: warn
jsx-a11y/aria-unsupported-elements: warn jsx-a11y/aria-unsupported-elements: warn
jsx-a11y/heading-has-content: warn jsx-a11y/heading-has-content: warn
jsx-a11y/href-no-hash: warn
jsx-a11y/html-has-lang: warn jsx-a11y/html-has-lang: warn
jsx-a11y/iframe-has-title: warn jsx-a11y/iframe-has-title: warn
jsx-a11y/img-redundant-alt: warn jsx-a11y/img-redundant-alt: warn
@ -152,3 +166,5 @@ rules:
- "app/javascript/**/__tests__/**" - "app/javascript/**/__tests__/**"
import/no-unresolved: error import/no-unresolved: error
import/no-webpack-loader-syntax: error import/no-webpack-loader-syntax: error
promise/catch-or-return: error

View file

@ -1,3 +1,9 @@
---
name: Bug Report
about: Create a report to help us improve
---
[Issue text goes here]. [Issue text goes here].
* * * * * * * *

View file

@ -0,0 +1,11 @@
---
name: Feature Request
about: Suggest an idea for this project
---
[Issue text goes here].
* * * *
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.

3
.gitignore vendored
View file

@ -36,9 +36,10 @@ config/deploy/*
.vscode/ .vscode/
.idea/ .idea/
# Ignore postgres + redis volume optionally created by docker-compose # Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
postgres postgres
redis redis
elasticsearch
# Ignore Apple files # Ignore Apple files
.DS_Store .DS_Store

View file

@ -107,5 +107,8 @@ Style/RegexpLiteral:
Style/SymbolArray: Style/SymbolArray:
Enabled: false Enabled: false
Style/TrailingCommaInLiteral: Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: 'comma'
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma' EnforcedStyleForMultiline: 'comma'

View file

@ -1 +1 @@
2.5.0 2.5.1

View file

@ -1,59 +0,0 @@
language: ruby
cache:
bundler: true
yarn: true
directories:
- node_modules
- public/assets
- public/packs-test
- tmp/cache/babel-loader
dist: trusty
sudo: false
branches:
only:
- master
notifications:
email: false
env:
global:
- LOCAL_DOMAIN=cb6e6126.ngrok.io
- LOCAL_HTTPS=true
- RAILS_ENV=test
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
- PARALLEL_TEST_PROCESSORS=2
addons:
postgresql: 9.4
apt:
sources:
- trusty-media
- sourceline: deb https://dl.yarnpkg.com/debian/ stable main
key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
packages:
- ffmpeg
- libicu-dev
- libprotobuf-dev
- protobuf-compiler
- yarn
rvm:
- 2.4.2
- 2.5.0
services:
- redis-server
install:
- nvm install
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
- yarn install
before_script:
- ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile
script:
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
- yarn run test:jest
- bundle exec i18n-tasks check-normalized && bundle exec i18n-tasks unused

View file

@ -49,3 +49,8 @@ It is expected that you have a working development environment set up (see back-
* If you are introducing new strings, they must be using localization methods * If you are introducing new strings, they must be using localization methods
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet. If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
## Translate
You can contribute to translating Mastodon via Weblate at [weblate.joinmastodon.org](https://weblate.joinmastodon.org/).
[![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/)

View file

@ -1,4 +1,4 @@
FROM ruby:2.4.3-alpine3.6 FROM ruby:2.4.4-alpine3.6
LABEL maintainer="https://github.com/tootsuite/mastodon" \ LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="Your self-hosted, globally interconnected microblogging community" description="Your self-hosted, globally interconnected microblogging community"

95
Gemfile
View file

@ -3,19 +3,19 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.6.0' ruby '>= 2.3.0', '< 2.6.0'
gem 'pkg-config', '~> 1.2' gem 'pkg-config', '~> 1.3'
gem 'puma', '~> 3.10' gem 'puma', '~> 3.11'
gem 'rails', '~> 5.1.4' gem 'rails', '~> 5.2.0'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 0.20' gem 'pg', '~> 1.0'
gem 'pghero', '~> 1.7' gem 'pghero', '~> 2.1'
gem 'dotenv-rails', '~> 2.2' gem 'dotenv-rails', '~> 2.2', '< 2.3'
gem 'aws-sdk', '~> 2.10', require: false gem 'aws-sdk-s3', '~> 1.9', require: false
gem 'fog-core', '~> 1.45' gem 'fog-core', '~> 1.45'
gem 'fog-local', '~> 0.4', require: false gem 'fog-local', '~> 0.5', require: false
gem 'fog-openstack', '~> 0.1', require: false gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 5.1' gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
@ -23,9 +23,9 @@ gem 'streamio-ffmpeg', '~> 3.0'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5' gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap', '~> 1.3'
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.0' gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0' gem 'cld3', '~> 3.2.0'
@ -33,67 +33,71 @@ gem 'devise', '~> 4.4'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
group :pam_authentication, optional: true do group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.0' gem 'devise_pam_authenticatable2', '~> 9.1'
end end
gem 'net-ldap', '~> 0.10' gem 'net-ldap', '~> 0.10'
gem 'omniauth-cas', '~> 1.1' gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.2' gem 'omniauth', '~> 1.2'
gem 'doorkeeper', '~> 4.2' gem 'doorkeeper', '~> 4.2', '< 4.3'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1' gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5' gem 'redis-namespace', '~> 1.5'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.0' gem 'http', '~> 3.2'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
gem 'httplog', '~> 1.0'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1' gem 'mime-types', '~> 3.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.8' gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.3' gem 'oj', '~> 3.5'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.8' gem 'ox', '~> 2.9'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 1.1' gem 'pundit', '~> 1.1'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 5.0' gem 'rack-attack', '~> 5.2'
gem 'rack-cors', '~> 0.4', require: 'rack/cors' gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rack-timeout', '~> 0.4' gem 'rack-timeout', '~> 0.4'
gem 'rails-i18n', '~> 5.0' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.0', 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', '~> 0.10' gem 'rqrcode', '~> 0.10'
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
gem 'ruby-progressbar', '~> 1.4' gem 'ruby-progressbar', '~> 1.4'
gem 'sanitize', '~> 4.6.4' gem 'sanitize', '~> 4.6'
gem 'sidekiq', '~> 5.0' gem 'sidekiq', '~> 5.1'
gem 'sidekiq-scheduler', '~> 2.1' gem 'sidekiq-scheduler', '~> 2.2'
gem 'sidekiq-unique-jobs', '~> 5.0' gem 'sidekiq-unique-jobs', '~> 5.0'
gem 'sidekiq-bulk', '~>0.1.1' gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0' gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 3.4' gem 'simple_form', '~> 4.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'strong_migrations' gem 'stoplight', '~> 2.1.3'
gem 'tty-command' gem 'strong_migrations', '~> 0.2'
gem 'tty-prompt' gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.16', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017' gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.0' gem 'webpacker', '~> 3.4'
gem 'webpush' gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1' gem 'json-ld', '~> 2.2'
gem 'rdf-normalize', '~> 0.3.1' gem 'rdf-normalize', '~> 0.3'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.18' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.2' gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.6'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.7' gem 'rspec-rails', '~> 3.7'
end end
@ -103,15 +107,15 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 2.15' gem 'capybara', '~> 2.18'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7' gem 'faker', '~> 1.8'
gem 'microformats', '~> 4.0' gem 'microformats', '~> 4.0'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.0' gem 'webmock', '~> 3.3'
gem 'parallel_tests', '~> 2.17' gem 'parallel_tests', '~> 2.21'
end end
group :development do group :development do
@ -119,22 +123,25 @@ group :development do
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.4' gem 'better_errors', '~> 2.4'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.5' gem 'bullet', '~> 5.7'
gem 'letter_opener', '~> 1.4' gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', require: false gem 'rubocop', '~> 0.55', require: false
gem 'brakeman', '~> 4.0', require: false gem 'brakeman', '~> 4.2', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.55', require: false gem 'scss_lint', '~> 0.57', require: false
gem 'capistrano', '~> 3.10' gem 'capistrano', '~> 3.10'
gem 'capistrano-rails', '~> 1.3' gem 'capistrano-rails', '~> 1.3'
gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
gem 'derailed_benchmarks'
gem 'stackprof'
end end
group :production do group :production do
gem 'lograge', '~> 0.7' gem 'lograge', '~> 0.10'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end

View file

@ -1,25 +1,39 @@
GIT
remote: https://github.com/rtomayko/posix-spawn
revision: 58465d2e213991f8afb13b984854a49fcdcc980c
ref: 58465d2e213991f8afb13b984854a49fcdcc980c
specs:
posix-spawn (0.3.13)
GIT
remote: https://github.com/tmm1/http_parser.rb
revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
specs:
http_parser.rb (0.6.1)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.1.4) actioncable (5.2.0)
actionpack (= 5.1.4) actionpack (= 5.2.0)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (~> 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.1.4) actionmailer (5.2.0)
actionpack (= 5.1.4) actionpack (= 5.2.0)
actionview (= 5.1.4) actionview (= 5.2.0)
activejob (= 5.1.4) activejob (= 5.2.0)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.1.4) actionpack (5.2.0)
actionview (= 5.1.4) actionview (= 5.2.0)
activesupport (= 5.1.4) activesupport (= 5.2.0)
rack (~> 2.0) rack (~> 2.0)
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.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.4) actionview (5.2.0)
activesupport (= 5.1.4) activesupport (= 5.2.0)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -30,60 +44,71 @@ GEM
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.5.4) active_record_query_trace (1.5.4)
activejob (5.1.4) activejob (5.2.0)
activesupport (= 5.1.4) activesupport (= 5.2.0)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.1.4) activemodel (5.2.0)
activesupport (= 5.1.4) activesupport (= 5.2.0)
activerecord (5.1.4) activerecord (5.2.0)
activemodel (= 5.1.4) activemodel (= 5.2.0)
activesupport (= 5.1.4) activesupport (= 5.2.0)
arel (~> 8.0) arel (>= 9.0)
activesupport (5.1.4) activestorage (5.2.0)
actionpack (= 5.2.0)
activerecord (= 5.2.0)
marcel (~> 0.3.1)
activesupport (5.2.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.2) addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.2) annotate (2.7.3)
activerecord (>= 3.2, < 6.0) activerecord (>= 3.2, < 6.0)
rake (>= 10.4, < 13.0) rake (>= 10.4, < 13.0)
arel (8.0.0) arel (9.0.0)
ast (2.3.0) ast (2.4.0)
attr_encrypted (3.0.3) attr_encrypted (3.1.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-sdk (2.10.100) aws-partitions (1.80.0)
aws-sdk-resources (= 2.10.100) aws-sdk-core (3.19.0)
aws-sdk-core (2.10.100) aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-resources (2.10.100) aws-sdk-kms (1.5.0)
aws-sdk-core (= 2.10.100) aws-sdk-core (~> 3)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.9.1)
aws-sdk-core (~> 3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2) aws-sigv4 (1.0.2)
bcrypt (3.1.11) bcrypt (3.1.11)
benchmark-ips (2.7.2)
better_errors (2.4.0) better_errors (2.4.0)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.5) bootsnap (1.3.0)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.0.1) brakeman (4.2.1)
browser (2.5.2) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.6.1) bullet (5.7.5)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.11.0)
bundler-audit (0.6.0) bundler-audit (0.6.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
capistrano (3.10.0) byebug (10.0.2)
capistrano (3.10.2)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@ -99,21 +124,21 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (2.16.1) capybara (2.18.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (~> 2.0) xpath (>= 2.0, < 4.0)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
charlock_holmes (0.7.5) charlock_holmes (0.7.6)
chewy (5.0.0) chewy (5.0.0)
activesupport (>= 4.0) activesupport (>= 4.0)
elasticsearch (>= 2.0.0) elasticsearch (>= 2.0.0)
elasticsearch-dsl elasticsearch-dsl
chunky_png (1.3.8) chunky_png (1.3.10)
cld3 (3.2.2) cld3 (3.2.2)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0) climate_control (0.2.0)
@ -125,62 +150,69 @@ GEM
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.3) crass (1.0.4)
css_parser (1.6.0) css_parser (1.6.0)
addressable addressable
debug_inspector (0.0.3) debug_inspector (0.0.3)
devise (4.4.0) derailed_benchmarks (1.3.4)
benchmark-ips (~> 2)
get_process_mem (~> 0)
heapy (~> 0)
memory_profiler (~> 0)
rack (>= 1)
rake (> 10, < 13)
thor (~> 0.19)
devise (4.4.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.2) railties (>= 4.1.0, < 6.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (3.0.2) devise-two-factor (3.0.3)
activesupport (< 5.2) activesupport (< 5.3)
attr_encrypted (>= 1.3, < 4, != 2) attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0) devise (~> 4.0)
railties (< 5.2) railties (< 5.3)
rotp (~> 2.0) rotp (~> 2.0)
devise_pam_authenticatable2 (9.0.0) devise_pam_authenticatable2 (9.1.0)
devise (>= 4.0.0) devise (>= 4.0.0)
rpam2 (~> 3.0) rpam2 (~> 4.0)
diff-lcs (1.3) diff-lcs (1.3)
docile (1.1.5) docile (1.3.0)
domain_name (0.5.20170404) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.6) doorkeeper (4.2.6)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.2.1) dotenv (2.2.2)
dotenv-rails (2.2.1) dotenv-rails (2.2.2)
dotenv (= 2.2.1) dotenv (= 2.2.2)
railties (>= 3.2, < 5.2) railties (>= 3.2, < 6.0)
easy_translate (0.5.0) easy_translate (0.5.1)
json
thread thread
thread_safe thread_safe
elasticsearch (6.0.1) elasticsearch (6.0.2)
elasticsearch-api (= 6.0.1) elasticsearch-api (= 6.0.2)
elasticsearch-transport (= 6.0.1) elasticsearch-transport (= 6.0.2)
elasticsearch-api (6.0.1) elasticsearch-api (6.0.2)
multi_json multi_json
elasticsearch-dsl (0.1.5) elasticsearch-dsl (0.1.5)
elasticsearch-transport (6.0.1) elasticsearch-transport (6.0.2)
faraday faraday
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
equatable (0.5.0) equatable (0.5.0)
erubi (1.7.0) erubi (1.7.1)
et-orbi (1.0.8) et-orbi (1.1.0)
tzinfo tzinfo
excon (0.59.0) excon (0.62.0)
fabrication (2.18.0) fabrication (2.20.1)
faker (1.8.4) faker (1.8.7)
i18n (~> 0.5) i18n (>= 0.7)
faraday (0.14.0) faraday (0.15.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
fast_blank (1.0.0) fast_blank (1.0.0)
fastimage (2.1.1) fastimage (2.1.1)
ffi (1.9.18) ffi (1.9.23)
fog-core (1.45.0) fog-core (1.45.0)
builder builder
excon (~> 0.58) excon (~> 0.58)
@ -188,16 +220,17 @@ GEM
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
fog-local (0.4.0) fog-local (0.5.0)
fog-core (~> 1.27) fog-core (>= 1.27, < 3.0)
fog-openstack (0.1.22) fog-openstack (0.1.25)
fog-core (>= 1.40) fog-core (~> 1.40)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
formatador (0.2.5) formatador (0.2.5)
fuubar (2.2.0) fuubar (2.3.1)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
get_process_mem (0.2.1)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.1.0) goldfinger (2.1.0)
@ -205,7 +238,7 @@ GEM
http (~> 3.0) http (~> 3.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
oj (~> 3.0) oj (~> 3.0)
hamlit (2.8.5) hamlit (2.8.8)
temple (>= 0.8.0) temple (>= 0.8.0)
thor thor
tilt tilt
@ -218,48 +251,44 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
hashdiff (0.3.7) hashdiff (0.3.7)
hashie (3.5.7) hashie (3.5.7)
heapy (0.1.3)
highline (1.7.10) highline (1.7.10)
hiredis (0.6.1) hiredis (0.6.1)
hitimes (1.2.6) hitimes (1.2.6)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (3.0.0) http (3.2.0)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (>= 2.0.0.pre.pre2, < 3) http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.0.0) http-form_data (2.1.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
http_parser.rb (0.6.0) httplog (1.0.2)
httplog (0.99.7) colorize (~> 0.8)
colorize rack (>= 1.0)
rack i18n (1.0.1)
i18n (0.9.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.19) i18n-tasks (0.9.21)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
easy_translate (>= 0.5.0) easy_translate (>= 0.5.1)
erubi erubi
highline (>= 1.7.3) highline (>= 1.7.3)
i18n i18n
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
rainbow (~> 2.2) rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.2.8) iso-639 (0.2.8)
jmespath (1.3.1) jmespath (1.4.0)
json (2.1.0) json (2.1.0)
json-ld (2.1.7) json-ld (2.2.1)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 2.2, >= 2.2.8) rdf (>= 2.2.8, < 4.0)
json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.2.0) jsonapi-renderer (0.2.0)
jwt (2.1.0) jwt (2.1.0)
kaminari (1.1.1) kaminari (1.1.1)
@ -276,25 +305,27 @@ GEM
kaminari-core (1.1.1) kaminari-core (1.1.1)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.6.0)
launchy (~> 2.2) launchy (~> 2.2)
letter_opener_web (1.3.1) letter_opener_web (1.3.4)
actionmailer (>= 3.2) actionmailer (>= 3.2)
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
link_header (0.0.8) link_header (0.0.8)
lograge (0.7.1) lograge (0.10.0)
actionpack (>= 4, < 5.2) actionpack (>= 4)
activesupport (>= 4, < 5.2) activesupport (>= 4)
railties (>= 4, < 5.2) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.2.1) loofah (2.2.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.0) mail (2.7.0)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
mario-redis-lock (1.2.0) marcel (0.3.2)
redis (~> 3, >= 3.0.5) mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
memory_profiler (0.9.10) memory_profiler (0.9.10)
method_source (0.9.0) method_source (0.9.0)
microformats (4.0.7) microformats (4.0.7)
@ -307,15 +338,15 @@ GEM
mini_mime (1.0.0) mini_mime (1.0.0)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.11.3) minitest (5.11.3)
msgpack (1.1.0) msgpack (1.2.4)
multi_json (1.12.2) multi_json (1.13.1)
multipart-post (2.0.0) multipart-post (2.0.0)
necromancer (0.4.0) necromancer (0.4.0)
net-ldap (0.16.1) net-ldap (0.16.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.2.0) net-ssh (4.2.0)
nio4r (2.1.0) nio4r (2.3.0)
nokogiri (1.8.2) nokogiri (1.8.2)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0) nokogumbo (1.5.0)
@ -325,7 +356,7 @@ GEM
concurrent-ruby (~> 1.0.0) concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0) sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0) statsd-ruby (~> 1.2.0)
oj (3.3.10) oj (3.5.1)
omniauth (1.8.1) omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0) hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -341,8 +372,8 @@ GEM
addressable (~> 2.5) addressable (~> 2.5)
http (~> 3.0) http (~> 3.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
ox (2.8.2) ox (2.9.2)
paperclip (5.2.1) paperclip (6.0.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
cocaine (~> 0.5.5) cocaine (~> 0.5.5)
@ -351,58 +382,62 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.12.0) parallel (1.12.1)
parallel_tests (2.19.0) parallel_tests (2.21.3)
parallel parallel
parser (2.4.0.2) parser (2.5.1.0)
ast (~> 2.3) ast (~> 2.4.0)
pastel (0.7.2) pastel (0.7.2)
equatable (~> 0.5.0) equatable (~> 0.5.0)
tty-color (~> 0.4.0) tty-color (~> 0.4.0)
pg (0.21.0) pg (1.0.0)
pghero (1.7.0) pghero (2.1.0)
activerecord activerecord
pkg-config (1.2.8) pkg-config (1.3.0)
powerpack (0.1.1) powerpack (0.1.1)
premailer (1.11.1) premailer (1.11.1)
addressable addressable
css_parser (>= 1.6.0) css_parser (>= 1.6.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.10.1) premailer-rails (1.10.2)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.4.1) private_address_check (0.4.1)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
pry-byebug (3.6.0)
byebug (~> 10.0)
pry (~> 0.10)
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.1) public_suffix (3.0.2)
puma (3.11.0) puma (3.11.4)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (2.0.3) rack (2.0.4)
rack-attack (5.0.1) rack-attack (5.2.0)
rack rack
rack-cors (0.4.1) rack-cors (1.0.2)
rack-protection (2.0.0) rack-protection (2.0.1)
rack rack
rack-proxy (0.6.2) rack-proxy (0.6.4)
rack rack
rack-test (0.8.2) rack-test (1.0.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-timeout (0.4.2) rack-timeout (0.4.2)
rails (5.1.4) rails (5.2.0)
actioncable (= 5.1.4) actioncable (= 5.2.0)
actionmailer (= 5.1.4) actionmailer (= 5.2.0)
actionpack (= 5.1.4) actionpack (= 5.2.0)
actionview (= 5.1.4) actionview (= 5.2.0)
activejob (= 5.1.4) activejob (= 5.2.0)
activemodel (= 5.1.4) activemodel (= 5.2.0)
activerecord (= 5.1.4) activerecord (= 5.2.0)
activesupport (= 5.1.4) activestorage (= 5.2.0)
activesupport (= 5.2.0)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.1.4) railties (= 5.2.0)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1) actionpack (~> 5.x, >= 5.0.1)
@ -411,31 +446,30 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.4)
loofah (~> 2.0) loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.0.4) rails-i18n (5.1.1)
i18n (~> 0.7) i18n (>= 0.7, < 2)
railties (~> 5.0) railties (>= 5.0, < 6)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.1.4) railties (5.2.0)
actionpack (= 5.1.4) actionpack (= 5.2.0)
activesupport (= 5.1.4) activesupport (= 5.2.0)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (3.0.0)
rake rake (12.3.1)
rake (12.3.0) rb-fsevent (0.10.3)
rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
rdf (2.2.12) rdf (3.0.2)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2) rdf-normalize (0.3.3)
rdf (~> 2.0) rdf (>= 2.2, < 4.0)
redis (3.3.5) redis (4.0.1)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
@ -445,24 +479,25 @@ GEM
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.6.0) redis-namespace (1.6.0)
redis (>= 3.0.4) redis (>= 3.0.4)
redis-rack (2.0.3) redis-rack (2.0.4)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-rails (5.0.2) redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6) redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.4.1) redis-store (1.5.0)
redis (>= 2.2, < 5) redis (>= 2.2, < 5)
request_store (1.3.2) request_store (1.4.1)
rack (>= 1.4)
responders (2.4.0) responders (2.4.0)
actionpack (>= 4.2.0, < 5.3) actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3)
rotp (2.1.2) rotp (2.1.2)
rpam2 (3.1.0) rpam2 (4.0.2)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rspec-core (3.7.0) rspec-core (3.7.1)
rspec-support (~> 3.7.0) rspec-support (~> 3.7.0)
rspec-expectations (3.7.0) rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
@ -481,15 +516,14 @@ GEM
rspec-sidekiq (3.0.3) rspec-sidekiq (3.0.3)
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.7.0) rspec-support (3.7.1)
rubocop (0.51.0) rubocop (0.55.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0) parser (>= 2.5)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 2.2.2, < 3.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
ruby-progressbar (1.9.0) ruby-progressbar (1.9.0)
ruby-saml (1.7.2) ruby-saml (1.7.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
@ -500,23 +534,23 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4) nokogumbo (~> 1.4)
sass (3.5.3) sass (3.5.6)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.56.0) scss_lint (0.57.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.5.3) sass (~> 3.5.5)
sidekiq (5.0.5) sidekiq (5.1.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (>= 3.3.4, < 5) redis (>= 3.3.5, < 5)
sidekiq-bulk (0.1.1) sidekiq-bulk (0.1.1)
activesupport activesupport
sidekiq sidekiq
sidekiq-scheduler (2.1.10) sidekiq-scheduler (2.2.1)
redis (>= 3, < 5) redis (>= 3, < 5)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
@ -526,11 +560,11 @@ GEM
thor (~> 0) thor (~> 0)
simple-navigation (4.0.5) simple-navigation (4.0.5)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (3.5.0) simple_form (4.0.0)
actionpack (> 4, < 5.2) actionpack (> 4)
activemodel (> 4, < 5.2) activemodel (> 4)
simplecov (0.15.1) simplecov (0.16.1)
docile (~> 1.1.0) docile (~> 1.1)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.2) simplecov-html (0.10.2)
@ -541,13 +575,15 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.15.1) sshkit (1.16.0)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.11)
statsd-ruby (1.2.1) statsd-ruby (1.2.1)
stoplight (2.1.3)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.1.9) strong_migrations (0.2.2)
activerecord (>= 3.2.0) activerecord (>= 3.2.0)
temple (0.8.0) temple (0.8.0)
terminal-table (1.8.0) terminal-table (1.8.0)
@ -559,10 +595,10 @@ GEM
timers (4.1.2) timers (4.1.2)
hitimes hitimes
tty-color (0.4.2) tty-color (0.4.2)
tty-command (0.7.0) tty-command (0.8.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-cursor (0.5.0) tty-cursor (0.5.0)
tty-prompt (0.15.0) tty-prompt (0.16.0)
necromancer (~> 0.4.0) necromancer (~> 0.4.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
timers (~> 4.0) timers (~> 4.0)
@ -577,32 +613,32 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.4) tzinfo (1.2.4)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2017.3) tzinfo-data (1.2018.4)
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.4) unf_ext (0.0.7.5)
unicode-display_width (1.3.0) unicode-display_width (1.3.2)
uniform_notifier (1.10.0) uniform_notifier (1.11.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (3.1.1) webmock (3.3.0)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (3.0.2) webpacker (3.4.3)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.3) webpush (0.3.3)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.6.5) websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.3)
wisper (2.0.0) wisper (2.0.0)
xpath (2.1.0) xpath (3.0.0)
nokogiri (~> 1.3) nokogiri (~> 1.8)
PLATFORMS PLATFORMS
ruby ruby
@ -612,52 +648,54 @@ DEPENDENCIES
active_record_query_trace (~> 1.5) active_record_query_trace (~> 1.5)
addressable (~> 2.5) addressable (~> 2.5)
annotate (~> 2.7) annotate (~> 2.7)
aws-sdk (~> 2.10) aws-sdk-s3 (~> 1.9)
better_errors (~> 2.4) better_errors (~> 2.4)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap bootsnap (~> 1.3)
brakeman (~> 4.0) brakeman (~> 4.2)
browser browser
bullet (~> 5.5) bullet (~> 5.7)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
capistrano (~> 3.10) capistrano (~> 3.10)
capistrano-rails (~> 1.3) capistrano-rails (~> 1.3)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.15) capybara (~> 2.18)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.0) cld3 (~> 3.2.0)
climate_control (~> 0.2) climate_control (~> 0.2)
derailed_benchmarks
devise (~> 4.4) devise (~> 4.4)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.0) devise_pam_authenticatable2 (~> 9.1)
doorkeeper (~> 4.2) doorkeeper (~> 4.2, < 4.3)
dotenv-rails (~> 2.2) dotenv-rails (~> 2.2, < 2.3)
fabrication (~> 2.18) fabrication (~> 2.20)
faker (~> 1.7) faker (~> 1.8)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (~> 1.45) fog-core (~> 1.45)
fog-local (~> 0.4) fog-local (~> 0.5)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fuubar (~> 2.2) fuubar (~> 2.2)
goldfinger (~> 2.1) goldfinger (~> 2.1)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 3.0) http (~> 3.2)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
httplog (~> 0.99) http_parser.rb (~> 0.6)!
httplog (~> 1.0)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639 iso-639
json-ld-preloaded (~> 2.2.1) json-ld (~> 2.2)
kaminari (~> 1.1) kaminari (~> 1.1)
letter_opener (~> 1.4) letter_opener (~> 1.4)
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.7) lograge (~> 0.10)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.0) microformats (~> 4.0)
@ -665,58 +703,61 @@ DEPENDENCIES
net-ldap (~> 0.10) net-ldap (~> 0.10)
nokogiri (~> 1.8) nokogiri (~> 1.8)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.3) oj (~> 3.5)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-cas (~> 1.1) omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
ostatus2 (~> 2.0) ostatus2 (~> 2.0)
ox (~> 2.8) ox (~> 2.9)
paperclip (~> 5.1) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.17) parallel_tests (~> 2.21)
pg (~> 0.20) pg (~> 1.0)
pghero (~> 1.7) pghero (~> 2.1)
pkg-config (~> 1.2) pkg-config (~> 1.3)
posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.4.1) private_address_check (~> 0.4.1)
pry-byebug (~> 3.6)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.10) puma (~> 3.11)
pundit (~> 1.1) pundit (~> 1.1)
rack-attack (~> 5.0) rack-attack (~> 5.2)
rack-cors (~> 0.4) rack-cors (~> 1.0)
rack-timeout (~> 0.4) rack-timeout (~> 0.4)
rails (~> 5.1.4) rails (~> 5.2.0)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0) rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3.1) rdf-normalize (~> 0.3)
redis (~> 3.3) redis (~> 4.0)
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.7) rspec-rails (~> 3.7)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop rubocop (~> 0.55)
ruby-oembed (~> 0.12)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
sanitize (~> 4.6.4) sanitize (~> 4.6)
scss_lint (~> 0.55) scss_lint (~> 0.57)
sidekiq (~> 5.0) sidekiq (~> 5.1)
sidekiq-bulk (~> 0.1.1) sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1) sidekiq-scheduler (~> 2.2)
sidekiq-unique-jobs (~> 5.0) sidekiq-unique-jobs (~> 5.0)
simple-navigation (~> 4.0) simple-navigation (~> 4.0)
simple_form (~> 3.4) simple_form (~> 4.0)
simplecov (~> 0.14) simplecov (~> 0.16)
sprockets-rails (~> 3.2) sprockets-rails (~> 3.2)
stackprof
stoplight (~> 2.1.3)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations strong_migrations (~> 0.2)
tty-command tty-command (~> 0.8)
tty-prompt tty-prompt (~> 0.16)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2017) tzinfo-data (~> 1.2018)
webmock (~> 3.0) webmock (~> 3.3)
webpacker (~> 3.0) webpacker (~> 3.4)
webpush webpush
RUBY VERSION RUBY VERSION

View file

@ -1,10 +1,10 @@
![Mastodon](https://i.imgur.com/NhZc40l.png) ![Mastodon](https://i.imgur.com/NhZc40l.png)
======== ========
[![Build Status](https://img.shields.io/travis/tootsuite/mastodon.svg)][travis] [![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon [circleci]: https://circleci.com/gh/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
Mastodon is a **free, open-source social network server** based on **open web protocols** like ActivityPub and OStatus. The social focus of the project is a viable decentralized alternative to commercial social media silos that returns the control of the content distribution channels to the people. The technical focus of the project is a good user interface, a clean REST API for 3rd party apps and robust anti-abuse tools. Mastodon is a **free, open-source social network server** based on **open web protocols** like ActivityPub and OStatus. The social focus of the project is a viable decentralized alternative to commercial social media silos that returns the control of the content distribution channels to the people. The technical focus of the project is a good user interface, a clean REST API for 3rd party apps and robust anti-abuse tools.

View file

@ -20,9 +20,10 @@ class AccountsController < ApplicationController
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params) @statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
unless @statuses.empty? unless @statuses.empty?
@older_url = older_url if @statuses.last.id > filtered_statuses.last.id @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
@newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
end end
end end
@ -31,6 +32,11 @@ class AccountsController < ApplicationController
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end end
format.rss do
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses)
end
format.json do format.json do
skip_session! skip_session!

View file

@ -22,7 +22,7 @@ class ActivityPub::CollectionsController < Api::BaseController
end end
def set_statuses def set_statuses
@statuses = scope_for_collection.paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = scope_for_collection
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
end end

View file

@ -1,14 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::OutboxesController < Api::BaseController class ActivityPub::OutboxesController < Api::BaseController
LIMIT = 20
include SignatureVerification include SignatureVerification
before_action :set_account before_action :set_account
before_action :set_statuses
def show def show
@statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@ -19,11 +19,47 @@ class ActivityPub::OutboxesController < Api::BaseController
end end
def outbox_presenter def outbox_presenter
ActivityPub::CollectionPresenter.new( if page_requested?
id: account_outbox_url(@account), ActivityPub::CollectionPresenter.new(
type: :ordered, id: account_outbox_url(@account, page_params),
size: @account.statuses_count, type: :ordered,
items: @statuses part_of: account_outbox_url(@account),
) prev: prev_page,
next: next_page,
items: @statuses
)
else
ActivityPub::CollectionPresenter.new(
id: account_outbox_url(@account),
type: :ordered,
size: @account.statuses_count,
first: account_outbox_url(@account, page: true),
last: account_outbox_url(@account, page: true, min_id: 0)
)
end
end
def next_page
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
end
def prev_page
account_outbox_url(@account, page: true, min_id: @statuses.first.id) unless @statuses.empty?
end
def set_statuses
return unless page_requested?
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
@statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
@statuses = cache_collection(@statuses, Status)
end
def page_requested?
params[:page] == 'true'
end
def page_params
{ page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact
end end
end end

View file

@ -2,7 +2,7 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :enable, :disable, :memorialize]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :disable, :memorialize] before_action :require_local_account!, only: [:enable, :disable, :memorialize]
@ -60,6 +60,17 @@ module Admin
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def remove_avatar
authorize @account, :remove_avatar?
@account.avatar = nil
@account.save!
log_action :remove_avatar, @account.user
redirect_to admin_account_path(@account.id)
end
private private
def set_account def set_account

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
module Admin
class ChangeEmailsController < BaseController
before_action :set_account
before_action :require_local_account!
def show
authorize @user, :change_email?
end
def update
authorize @user, :change_email?
new_email = resource_params.fetch(:unconfirmed_email)
if new_email != @user.email
@user.update!(
unconfirmed_email: new_email,
# Regenerate the confirmation token:
confirmation_token: nil
)
log_action :change_email, @user
@user.send_confirmation_instructions
end
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.change_email.changed_msg')
end
private
def set_account
@account = Account.find(params[:account_id])
@user = @account.user
end
def require_local_account!
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
end
def resource_params
params.require(:user).permit(
:unconfirmed_email
)
end
end
end

View file

@ -3,6 +3,7 @@
module Admin module Admin
class ConfirmationsController < BaseController class ConfirmationsController < BaseController
before_action :set_user before_action :set_user
before_action :check_confirmation, only: [:resend]
def create def create
authorize @user, :confirm? authorize @user, :confirm?
@ -11,10 +12,28 @@ module Admin
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def resend
authorize @user, :confirm?
@user.resend_confirmation_instructions
log_action :confirm, @user
flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success')
redirect_to admin_accounts_path
end
private private
def set_user def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end
def check_confirmation
if @user.confirmed?
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
redirect_to admin_accounts_path
end
end
end end
end end

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Admin
class ReportNotesController < BaseController
before_action :set_report_note, only: [:destroy]
def create
authorize ReportNote, :create?
@report_note = current_account.report_notes.new(resource_params)
@report = @report_note.report
if @report_note.save
if params[:create_and_resolve]
@report.resolve!(current_account)
log_action :resolve, @report
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
return
end
if params[:create_and_unresolve]
@report.unresolve!
log_action :reopen, @report
end
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
else
@report_notes = @report.notes.latest
@report_history = @report.history
@form = Form::StatusBatch.new
render template: 'admin/reports/show'
end
end
def destroy
authorize @report_note, :destroy?
@report_note.destroy!
redirect_to admin_report_path(@report_note.report_id), notice: I18n.t('admin.report_notes.destroyed_msg')
end
private
def resource_params
params.require(:report_note).permit(
:content,
:report_id
)
end
def set_report_note
@report_note = ReportNote.find(params[:id])
end
end
end

View file

@ -3,31 +3,16 @@
module Admin module Admin
class ReportedStatusesController < BaseController class ReportedStatusesController < BaseController
before_action :set_report before_action :set_report
before_action :set_status, only: [:update, :destroy]
def create def create
authorize :status, :update? authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) @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 flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def update
authorize @status, :update?
@status.update!(status_params)
log_action :update, @status
redirect_to admin_report_path(@report)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status
end
private private
def status_params def status_params
@ -35,15 +20,21 @@ module Admin
end end
def form_status_batch_params def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: []) 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 end
def set_report def set_report
@report = Report.find(params[:report_id]) @report = Report.find(params[:report_id])
end end
def set_status
@status = @report.statuses.find(params[:id])
end
end end
end end

View file

@ -11,45 +11,61 @@ module Admin
def show def show
authorize @report, :show? authorize @report, :show?
@form = Form::StatusBatch.new
@report_note = @report.notes.new
@report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
@form = Form::StatusBatch.new
end end
def update def update
authorize @report, :update? authorize @report, :update?
process_report process_report
redirect_to admin_report_path(@report)
if @report.action_taken?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
else
redirect_to admin_report_path(@report)
end
end end
private private
def process_report def process_report
case params[:outcome].to_s case params[:outcome].to_s
when 'assign_to_self'
@report.update!(assigned_account_id: current_account.id)
log_action :assigned_to_self, @report
when 'unassign'
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
when 'reopen'
@report.unresolve!
log_action :reopen, @report
when 'resolve' when 'resolve'
@report.update!(action_taken_by_current_attributes) @report.resolve!(current_account)
log_action :resolve, @report log_action :resolve, @report
when 'suspend' when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id) Admin::SuspensionWorker.perform_async(@report.target_account.id)
log_action :resolve, @report log_action :resolve, @report
log_action :suspend, @report.target_account log_action :suspend, @report.target_account
resolve_all_target_account_reports resolve_all_target_account_reports
when 'silence' when 'silence'
@report.target_account.update!(silenced: true) @report.target_account.update!(silenced: true)
log_action :resolve, @report log_action :resolve, @report
log_action :silence, @report.target_account log_action :silence, @report.target_account
resolve_all_target_account_reports resolve_all_target_account_reports
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end
end @report.reload
def action_taken_by_current_attributes
{ action_taken: true, action_taken_by_account_id: current_account.id }
end end
def resolve_all_target_account_reports def resolve_all_target_account_reports
unresolved_reports_for_target_account.update_all( unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
action_taken_by_current_attributes
)
end end
def unresolved_reports_for_target_account def unresolved_reports_for_target_account

View file

@ -5,14 +5,13 @@ module Admin
helper_method :current_params helper_method :current_params
before_action :set_account before_action :set_account
before_action :set_status, only: [:update, :destroy]
PER_PAGE = 20 PER_PAGE = 20
def index def index
authorize :status, :index? authorize :status, :index?
@statuses = @account.statuses @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media] if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@ -26,40 +25,18 @@ module Admin
def create def create
authorize :status, :update? authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) @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 flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
def update
authorize @status, :update?
@status.update!(status_params)
log_action :update, @status
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status
end
private private
def status_params
params.require(:status).permit(:sensitive)
end
def form_status_batch_params def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: []) params.require(:form_status_batch).permit(:action, status_ids: [])
end end
def set_status
@status = @account.statuses.find(params[:id])
end
def set_account def set_account
@account = Account.find(params[:account_id]) @account = Account.find(params[:account_id])
end end
@ -72,5 +49,15 @@ module Admin
page: page > 1 && page, page: page > 1 && page,
}.select { |_, value| value.present? } }.select { |_, value| value.present? }
end end
def action_from_button
if params[:nsfw_on]
'nsfw_on'
elsif params[:nsfw_off]
'nsfw_off'
elsif params[:delete]
'delete'
end
end
end end
end end

View file

@ -66,8 +66,10 @@ class Api::BaseController < ApplicationController
end end
def require_user! def require_user!
if current_user if current_user && !current_user.disabled?
set_user_activity set_user_activity
elsif current_user
render json: { error: 'Your login is currently disabled' }, status: 403
else else
render json: { error: 'This method requires an authenticated user' }, status: 422 render json: { error: 'This method requires an authenticated user' }, status: 422
end end

View file

@ -13,6 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
def update def update
@account = current_account @account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true) UpdateAccountService.new.call(@account, account_params, raise_error: true)
UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id) ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer render json: @account, serializer: REST::CredentialAccountSerializer
end end
@ -20,6 +21,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private private
def account_params def account_params
params.permit(:display_name, :note, :avatar, :header, :locked) params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
end
def user_settings_params
return nil unless params.key?(:source)
source_params = params.require(:source)
{
'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy),
'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
}
end end
end end

View file

@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end end
def load_accounts def load_accounts
return [] if @account.user_hides_network? && current_account.id != @account.id
default_accounts.merge(paginated_follows).to_a default_accounts.merge(paginated_follows).to_a
end end
@ -63,6 +65,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end end
def load_accounts def load_accounts
return [] if @account.user_hides_network? && current_account.id != @account.id
default_accounts.merge(paginated_follows).to_a default_accounts.merge(paginated_follows).to_a
end end
@ -63,6 +65,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -27,19 +27,17 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end end
def account_statuses def account_statuses
default_statuses.tap do |statuses| statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses = statuses.paginate_by_max_id(
statuses.merge!(pinned_scope) if truthy_param?(:pinned)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
end
end
def default_statuses
permitted_account_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id]
) )
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses
end end
def permitted_account_statuses def permitted_account_statuses
@ -69,7 +67,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit, :only_media, :exclude_replies).merge(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end end
def insert_pagination_headers def insert_pagination_headers

View file

@ -5,6 +5,7 @@ class Api::V1::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action :require_user!, except: [:show] before_action :require_user!, except: [:show]
before_action :set_account before_action :set_account
before_action :check_account_suspension, only: [:show]
respond_to :json respond_to :json
@ -54,4 +55,8 @@ class Api::V1::AccountsController < Api::BaseController
def relationships(**options) def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end end
def check_account_suspension
gone if @account.suspended?
end
end end

View file

@ -57,6 +57,6 @@ class Api::V1::BlocksController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -15,7 +15,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
end end
def create def create
BlockDomainFromAccountService.new.call(current_account, domain_block_params[:domain]) current_account.block_domain!(domain_block_params[:domain])
AfterAccountDomainBlockWorker.perform_async(current_account.id, domain_block_params[:domain])
render_empty render_empty
end end
@ -67,7 +68,7 @@ class Api::V1::DomainBlocksController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
def domain_block_params def domain_block_params

View file

@ -66,6 +66,6 @@ class Api::V1::FavouritesController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -71,6 +71,6 @@ class Api::V1::FollowRequestsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -88,7 +88,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
def unlimited? def unlimited?

View file

@ -59,6 +59,6 @@ class Api::V1::MutesController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -82,6 +82,6 @@ class Api::V1::NotificationsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit, exclude_types: []).merge(core_params) params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
end end
end end

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
class Api::V1::Push::SubscriptionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :push }
before_action :require_user!
before_action :set_web_push_subscription
def create
@web_subscription&.destroy!
@web_subscription = ::Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
data: data_params,
user_id: current_user.id,
access_token_id: doorkeeper_token.id
)
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def show
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def update
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
@web_subscription.update!(data: data_params)
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def destroy
@web_subscription&.destroy!
render_empty
end
private
def set_web_push_subscription
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
end
def subscription_params
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end
def data_params
return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
end
end

View file

@ -77,6 +77,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -39,7 +39,7 @@ class Api::V1::Statuses::PinsController < Api::BaseController
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).as_json ).as_json
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id)
end end
def distribute_remove_activity! def distribute_remove_activity!
@ -49,6 +49,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
adapter: ActivityPub::Adapter adapter: ActivityPub::Adapter
).as_json ).as_json
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id)
end end
end end

View file

@ -74,6 +74,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
end end

View file

@ -10,6 +10,12 @@ class Api::V1::StatusesController < Api::BaseController
respond_to :json respond_to :json
# This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most
# conversations as quasi-unlimited, it would be too much work to render more
# than this anyway
CONTEXT_LIMIT = 4_096
def show def show
cached = Rails.cache.read(@status.cache_key) cached = Rails.cache.read(@status.cache_key)
@status = cached unless cached.nil? @status = cached unless cached.nil?
@ -17,8 +23,8 @@ class Api::V1::StatusesController < Api::BaseController
end end
def context def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(current_account) ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
descendants_results = @status.descendants(current_account) descendants_results = @status.descendants(CONTEXT_LIMIT, current_account)
loaded_ancestors = cache_collection(ancestors_results, Status) loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status) loaded_descendants = cache_collection(descendants_results, Status)
@ -76,7 +82,7 @@ class Api::V1::StatusesController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
def authorize_if_got_token def authorize_if_got_token

View file

@ -0,0 +1,63 @@
# frozen_string_literal: true
class Api::V1::Timelines::DirectController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_direct_statuses
end
def cached_direct_statuses
cache_collection direct_statuses, Status
end
def direct_statuses
direct_timeline_statuses
end
def direct_timeline_statuses
# this query requires built in pagination.
Status.as_direct_timeline(
current_account,
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id],
true # returns array of cache_ids object
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View file

@ -43,7 +43,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params) params.slice(:local, :limit).permit(:local, :limit).merge(core_params)
end end
def next_path def next_path

View file

@ -45,7 +45,7 @@ class Api::V1::Timelines::ListController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
def next_path def next_path

View file

@ -45,7 +45,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:local, :limit, :only_media).merge(core_params) params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
end end
def next_path def next_path

View file

@ -54,7 +54,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:local, :limit, :only_media).merge(core_params) params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
end end
def next_path def next_path

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Api::V2::SearchController < Api::V1::SearchController
def index
@search = Search.new(search)
render json: @search, serializer: REST::V2::SearchSerializer
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Api::Web::BaseController < Api::BaseController
protect_from_forgery with: :exception
rescue_from ActionController::InvalidAuthenticityToken do
render json: { error: "Can't verify CSRF token authenticity." }, status: 422
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::EmbedsController < Api::BaseController class Api::Web::EmbedsController < Api::Web::BaseController
respond_to :json respond_to :json
before_action :require_user! before_action :require_user!
@ -9,9 +9,12 @@ class Api::Web::EmbedsController < Api::BaseController
status = StatusFinder.new(params[:url]).status status = StatusFinder.new(params[:url]).status
render json: status, serializer: OEmbedSerializer, width: 400 render json: status, serializer: OEmbedSerializer, width: 400
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
oembed = OEmbed::Providers.get(params[:url]) oembed = FetchOEmbedService.new.call(params[:url])
render json: Oj.dump(oembed.fields)
rescue OEmbed::NotFound if oembed
render json: {}, status: :not_found render json: oembed
else
render json: {}, status: :not_found
end
end end
end end

View file

@ -1,15 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::BaseController class Api::Web::PushSubscriptionsController < Api::Web::BaseController
respond_to :json respond_to :json
before_action :require_user! before_action :require_user!
protect_from_forgery with: :exception
def create def create
params.require(:subscription).require(:endpoint)
params.require(:subscription).require(:keys).require([:auth, :p256dh])
active_session = current_session active_session = current_session
unless active_session.web_push_subscription.nil? unless active_session.web_push_subscription.nil?
@ -29,27 +25,38 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
}, },
} }
data.deep_merge!(params[:data]) if params[:data] data.deep_merge!(data_params) if params[:data]
web_subscription = ::Web::PushSubscription.create!( web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint], endpoint: subscription_params[:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh], key_p256dh: subscription_params[:keys][:p256dh],
key_auth: params[:subscription][:keys][:auth], key_auth: subscription_params[:keys][:auth],
data: data data: data,
user_id: active_session.user_id,
access_token_id: active_session.access_token_id
) )
active_session.update!(web_push_subscription: web_subscription) active_session.update!(web_push_subscription: web_subscription)
render json: web_subscription.as_payload render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
end end
def update def update
params.require([:id, :data]) params.require([:id])
web_subscription = ::Web::PushSubscription.find(params[:id]) web_subscription = ::Web::PushSubscription.find(params[:id])
web_subscription.update!(data: data_params)
web_subscription.update!(data: params[:data]) render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
end
render json: web_subscription.as_payload private
def subscription_params
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end
def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
end end
end end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::SettingsController < Api::BaseController class Api::Web::SettingsController < Api::Web::BaseController
respond_to :json respond_to :json
before_action :require_user! before_action :require_user!

View file

@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
include Localized include Localized
include UserTrackingConcern include UserTrackingConcern
include SessionTrackingConcern
helper_method :current_account helper_method :current_account
helper_method :current_session helper_method :current_session
@ -19,6 +20,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from ActionController::UnknownFormat, with: :not_acceptable
rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
@ -39,11 +41,11 @@ class ApplicationController < ActionController::Base
end end
def require_admin! def require_admin!
redirect_to root_path unless current_user&.admin? forbidden unless current_user&.admin?
end end
def require_staff! def require_staff!
redirect_to root_path unless current_user&.staff? forbidden unless current_user&.staff?
end end
def check_suspension def check_suspension
@ -72,6 +74,10 @@ class ApplicationController < ActionController::Base
respond_with_error(422) respond_with_error(422)
end end
def not_acceptable
respond_with_error(406)
end
def single_user_mode? def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
end end

View file

@ -29,10 +29,14 @@ module Localized
end end
def preferred_locale def preferred_locale
http_accept_language.preferred_language_from(I18n.available_locales) http_accept_language.preferred_language_from(available_locales)
end end
def compatible_locale def compatible_locale
http_accept_language.compatible_language_from(I18n.available_locales) http_accept_language.compatible_language_from(available_locales)
end
def available_locales
I18n.available_locales.reverse
end end
end end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module RemoteAccountControllerConcern
extend ActiveSupport::Concern
included do
layout 'public'
before_action :set_account
before_action :check_account_suspension
end
private
def set_account
@account = Account.find_remote!(params[:acct])
end
def check_account_suspension
gone if @account.suspended?
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
module SessionTrackingConcern
extend ActiveSupport::Concern
UPDATE_SIGN_IN_HOURS = 24
included do
before_action :set_session_activity
end
private
def set_session_activity
return unless session_needs_update?
current_session.touch
end
def session_needs_update?
!current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
end
end

View file

@ -107,9 +107,7 @@ module SignatureVerification
def incompatible_signature?(signature_params) def incompatible_signature?(signature_params)
signature_params['keyId'].blank? || signature_params['keyId'].blank? ||
signature_params['signature'].blank? || signature_params['signature'].blank?
signature_params['algorithm'].blank? ||
signature_params['algorithm'] != 'rsa-sha256'
end end
def account_from_key_id(key_id) def account_from_key_id(key_id)

View file

@ -4,14 +4,17 @@ class FollowerAccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
def index def index
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
respond_to do |format| respond_to do |format|
format.html do format.html do
@relationships = AccountRelationshipsPresenter.new(@follows.map(&:account_id), current_user.account_id) if user_signed_in? next if @account.user_hides_network?
follows
@relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in?
end end
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter, adapter: ActivityPub::Adapter,
@ -22,28 +25,31 @@ class FollowerAccountsController < ApplicationController
private private
def follows
@follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
def page_url(page) def page_url(page)
account_followers_url(@account, page: page) unless page.nil? account_followers_url(@account, page: page) unless page.nil?
end end
def collection_presenter def collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.followers_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
part_of: account_followers_url(@account),
next: page_url(@follows.next_page),
prev: page_url(@follows.prev_page)
)
if params[:page].present? if params[:page].present?
page ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.followers_count,
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
part_of: account_followers_url(@account),
next: page_url(follows.next_page),
prev: page_url(follows.prev_page)
)
else else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account), id: account_followers_url(@account),
type: :ordered, type: :ordered,
size: @account.followers_count, size: @account.followers_count,
first: page first: page_url(1)
) )
end end
end end

View file

@ -4,14 +4,17 @@ class FollowingAccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
def index def index
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
respond_to do |format| respond_to do |format|
format.html do format.html do
@relationships = AccountRelationshipsPresenter.new(@follows.map(&:target_account_id), current_user.account_id) if user_signed_in? next if @account.user_hides_network?
follows
@relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
end end
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter, adapter: ActivityPub::Adapter,
@ -22,28 +25,31 @@ class FollowingAccountsController < ApplicationController
private private
def follows
@follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
def page_url(page) def page_url(page)
account_following_index_url(@account, page: page) unless page.nil? account_following_index_url(@account, page: page) unless page.nil?
end end
def collection_presenter def collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.following_count,
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) },
part_of: account_following_index_url(@account),
next: page_url(@follows.next_page),
prev: page_url(@follows.prev_page)
)
if params[:page].present? if params[:page].present?
page ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.following_count,
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) },
part_of: account_following_index_url(@account),
next: page_url(follows.next_page),
prev: page_url(follows.prev_page)
)
else else
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account), id: account_following_index_url(@account),
type: :ordered, type: :ordered,
size: @account.following_count, size: @account.following_count,
first: page first: page_url(1)
) )
end end
end end

View file

@ -2,6 +2,7 @@
class HomeController < ApplicationController class HomeController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_referrer_policy_header
before_action :set_initial_state_json before_action :set_initial_state_json
def index def index
@ -62,4 +63,8 @@ class HomeController < ApplicationController
about_path about_path
end end
end end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'origin'
end
end end

View file

@ -1,9 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class IntentsController < ApplicationController class IntentsController < ApplicationController
def show before_action :check_uri
uri = Addressable::URI.parse(params[:uri]) rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show
if uri.scheme == 'web+mastodon' if uri.scheme == 'web+mastodon'
case uri.host case uri.host
when 'follow' when 'follow'
@ -15,4 +16,18 @@ class IntentsController < ApplicationController
not_found not_found
end end
private
def check_uri
not_found if uri.blank?
end
def handle_invalid_uri
not_found
end
def uri
@uri ||= Addressable::URI.parse(params[:uri])
end
end end

View file

@ -10,7 +10,7 @@ class InvitesController < ApplicationController
def index def index
authorize :invite, :create? authorize :invite, :create?
@invites = Invite.where(user: current_user) @invites = invites
@invite = Invite.new(expires_in: 1.day.to_i) @invite = Invite.new(expires_in: 1.day.to_i)
end end
@ -23,13 +23,13 @@ class InvitesController < ApplicationController
if @invite.save if @invite.save
redirect_to invites_path redirect_to invites_path
else else
@invites = Invite.where(user: current_user) @invites = invites
render :index render :index
end end
end end
def destroy def destroy
@invite = Invite.where(user: current_user).find(params[:id]) @invite = invites.find(params[:id])
authorize @invite, :destroy? authorize @invite, :destroy?
@invite.expire! @invite.expire!
redirect_to invites_path redirect_to invites_path
@ -37,6 +37,10 @@ class InvitesController < ApplicationController
private private
def invites
Invite.where(user: current_user)
end
def resource_params def resource_params
params.require(:invite).permit(:max_uses, :expires_in) params.require(:invite).permit(:max_uses, :expires_in)
end end

View file

@ -8,6 +8,8 @@ class MediaProxyController < ApplicationController
if lock.acquired? if lock.acquired?
@media_attachment = MediaAttachment.remote.find(params[:id]) @media_attachment = MediaAttachment.remote.find(params[:id])
redownload! if @media_attachment.needs_redownload? && !reject_media? redownload! if @media_attachment.needs_redownload? && !reject_media?
else
raise Mastodon::RaceConditionError
end end
end end

View file

@ -8,6 +8,11 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
include Localized include Localized
def destroy
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
super
end
private private
def store_current_location def store_current_location

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
class Oauth::TokensController < Doorkeeper::TokensController
def revoke
unsubscribe_for_token if authorized? && token.accessible?
super
end
private
def unsubscribe_for_token
Web::PushSubscription.where(access_token_id: token.id).delete_all
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
class RemoteUnfollowsController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_body_classes
def create
@account = unfollow_attempt.try(:target_account)
if @account.nil?
render :error
else
render :success
end
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render :error
end
private
def unfollow_attempt
username, domain = acct_without_prefix.split('@')
UnfollowService.new.call(current_account, Account.find_remote!(username, domain))
end
def acct_without_prefix
acct_params.gsub(/\Aacct:/, '')
end
def acct_params
params.fetch(:acct, '')
end
def set_body_classes
@body_classes = 'modal-layout'
end
end

View file

@ -8,7 +8,7 @@ class Settings::ApplicationsController < ApplicationController
before_action :prepare_scopes, only: [:create, :update] before_action :prepare_scopes, only: [:create, :update]
def index def index
@applications = current_user.applications.page(params[:page]) @applications = current_user.applications.order(id: :desc).page(params[:page])
end end
def new def new

View file

@ -1,7 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'sidekiq-bulk'
class Settings::FollowerDomainsController < ApplicationController class Settings::FollowerDomainsController < ApplicationController
layout 'admin' layout 'admin'
@ -9,13 +7,13 @@ class Settings::FollowerDomainsController < ApplicationController
def show def show
@account = current_account @account = current_account
@domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10) @domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
end end
def update def update
domains = bulk_params[:select] || [] domains = bulk_params[:select] || []
SoftBlockDomainFollowersWorker.push_bulk(domains) do |domain| AfterAccountDomainBlockWorker.push_bulk(domains) do |domain|
[current_account.id, domain] [current_account.id, domain]
end end

View file

@ -44,6 +44,7 @@ class Settings::PreferencesController < ApplicationController
:setting_system_font_ui, :setting_system_font_ui,
:setting_noindex, :setting_noindex,
:setting_theme, :setting_theme,
:setting_hide_network,
notification_emails: %i(follow follow_request reblog favourite mention digest), notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )

View file

@ -11,13 +11,16 @@ class Settings::ProfilesController < ApplicationController
obfuscate_filename [:account, :avatar] obfuscate_filename [:account, :avatar]
obfuscate_filename [:account, :header] obfuscate_filename [:account, :header]
def show; end def show
@account.build_fields
end
def update def update
if UpdateAccountService.new.call(@account, account_params) if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id) ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else else
@account.build_fields
render :show render :show
end end
end end
@ -25,7 +28,7 @@ class Settings::ProfilesController < ApplicationController
private private
def account_params def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked) params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
end end
def set_account def set_account

View file

@ -15,6 +15,7 @@ class SharesController < ApplicationController
def initial_state_params def initial_state_params
text = [params[:title], params[:text], params[:url]].compact.join(' ') text = [params[:title], params[:text], params[:url]].compact.join(' ')
{ {
settings: Web::Setting.find_by(user: current_user)&.data || {}, settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session), push_subscription: current_account.user.web_push_subscription(current_session),

View file

@ -4,6 +4,10 @@ class StatusesController < ApplicationController
include SignatureAuthentication include SignatureAuthentication
include Authorization include Authorization
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
layout 'public' layout 'public'
before_action :set_account before_action :set_account
@ -11,13 +15,14 @@ class StatusesController < ApplicationController
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension before_action :check_account_suspension
before_action :redirect_to_original, only: [:show] before_action :redirect_to_original, only: [:show]
before_action :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers before_action :set_cache_headers
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : [] set_ancestors
@descendants = cache_collection(@status.descendants(current_account), Status) set_descendants
render 'stream_entries/show' render 'stream_entries/show'
end end
@ -47,10 +52,77 @@ class StatusesController < ApplicationController
private private
def create_descendant_thread(depth, statuses)
if depth < DESCENDANTS_DEPTH_LIMIT
{ statuses: statuses }
else
next_status = statuses.pop
{ statuses: statuses, next_status: next_status }
end
end
def set_account def set_account
@account = Account.find_local!(params[:account_username]) @account = Account.find_local!(params[:account_username])
end end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
depth = 1
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
depth += 1
statuses << descendant
else
@descendant_threads << create_descendant_thread(depth, statuses)
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
depth += index - statuses.size
break
end
depth -= statuses.size
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[ [
@ -78,4 +150,9 @@ class StatusesController < ApplicationController
def redirect_to_original def redirect_to_original
redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog? redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end end
def set_referrer_policy_header
return if @status.public_visibility? || @status.unlisted_visibility?
response.headers['Referrer-Policy'] = 'origin'
end
end end

View file

@ -15,8 +15,7 @@ class StreamEntriesController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
@ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : [] redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
@descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
end end
format.atom do format.atom do
@ -24,6 +23,7 @@ class StreamEntriesController < ApplicationController
skip_session! skip_session!
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
end end
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end end
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagsController < ApplicationController class TagsController < ApplicationController
PAGE_SIZE = 20
before_action :set_body_classes before_action :set_body_classes
before_action :set_instance_presenter before_action :set_instance_presenter
@ -13,8 +15,15 @@ class TagsController < ApplicationController
@initial_state_json = serializable_resource.to_json @initial_state_json = serializable_resource.to_json
end end
format.rss do
@statuses = Status.as_tag_timeline(@tag).limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status)
render xml: RSS::TagSerializer.render(@tag, @statuses)
end
format.json do format.json do
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id]) @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render json: collection_presenter, render json: collection_presenter,

View file

@ -1,4 +1,26 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin::AccountModerationNotesHelper module Admin::AccountModerationNotesHelper
def admin_account_link_to(account)
link_to admin_account_path(account.id), class: name_tag_classes(account) do
safe_join([
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
content_tag(:span, account.acct, class: 'username'),
], ' ')
end
end
def admin_account_inline_link_to(account)
link_to admin_account_path(account.id), class: name_tag_classes(account, true) do
content_tag(:span, account.acct, class: 'username')
end
end
private
def name_tag_classes(account, inline = false)
classes = [inline ? 'inline-name-tag' : 'name-tag']
classes << 'suspended' if account.suspended?
classes.join(' ')
end
end end

View file

@ -45,6 +45,8 @@ module Admin::ActionLogsHelper
log.recorded_changes.slice('domain', 'visible_in_picker') log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action) elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin') log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'User' && [:change_email].include?(log.action)
log.recorded_changes.slice('email', 'unconfirmed_email')
elsif log.target_type == 'DomainBlock' elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media') log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update elsif log.target_type == 'Status' && log.action == :update
@ -84,9 +86,9 @@ module Admin::ActionLogsHelper
'positive' 'positive'
when :create when :create
opposite_verbs?(log) ? 'negative' : 'positive' opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize when :update, :reset_password, :disable_2fa, :memorialize, :change_email
'neutral' 'neutral'
when :demote, :silence, :disable, :suspend when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
'negative' 'negative'
when :destroy when :destroy
opposite_verbs?(log) ? 'positive' : 'negative' opposite_verbs?(log) ? 'positive' : 'negative'

View file

@ -63,4 +63,8 @@ module ApplicationHelper
def opengraph(property, content) def opengraph(property, content)
tag(:meta, content: content, property: property) tag(:meta, content: content, property: property)
end end
def react_component(name, props = {})
content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) })
end
end end

View file

@ -5,6 +5,10 @@ module JsonLdHelper
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end end
def equals_or_includes_any?(haystack, needles)
needles.any? { |needle| equals_or_includes?(haystack, needle) }
end
def first_of_value(value) def first_of_value(value)
value.is_a?(Array) ? value.first : value value.is_a?(Array) ? value.first : value
end end
@ -44,22 +48,26 @@ module JsonLdHelper
end end
def canonicalize(json) def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json) graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize) graph.dump(:normalize)
end end
def fetch_resource(uri, id) def fetch_resource(uri, id, on_behalf_of = nil)
unless id unless id
json = fetch_resource_without_id_validation(uri) json = fetch_resource_without_id_validation(uri, on_behalf_of)
return unless json return unless json
uri = json['id'] uri = json['id']
end end
json = fetch_resource_without_id_validation(uri) json = fetch_resource_without_id_validation(uri, on_behalf_of)
json.present? && json['id'] == uri ? json : nil json.present? && json['id'] == uri ? json : nil
end end
def fetch_resource_without_id_validation(uri) def fetch_resource_without_id_validation(uri, on_behalf_of = nil)
build_request(uri, on_behalf_of).perform do |response|
return body_to_json(response.body_with_limit) if response.code == 200
end
# If request failed, retry without doing it on behalf of a user
build_request(uri).perform do |response| build_request(uri).perform do |response|
response.code == 200 ? body_to_json(response.body_with_limit) : nil response.code == 200 ? body_to_json(response.body_with_limit) : nil
end end
@ -81,9 +89,25 @@ module JsonLdHelper
private private
def build_request(uri) def build_request(uri, on_behalf_of = nil)
request = Request.new(:get, uri) request = Request.new(:get, uri)
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json') request.add_headers('Accept' => 'application/activity+json, application/ld+json')
request request
end end
def load_jsonld_context(url, _options = {}, &_block)
json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do
request = Request.new(:get, url)
request.add_headers('Accept' => 'application/ld+json')
request.perform do |res|
raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json'
res.body_with_limit
end
end
doc = JSON::LD::API::RemoteDocument.new(url, json)
block_given? ? yield(doc) : doc
end
end end

View file

@ -6,13 +6,16 @@ module SettingsHelper
ar: 'العربية', ar: 'العربية',
bg: 'Български', bg: 'Български',
ca: 'Català', ca: 'Català',
co: 'Corsu',
de: 'Deutsch', de: 'Deutsch',
el: 'Ελληνικά',
eo: 'Esperanto', eo: 'Esperanto',
es: 'Español', es: 'Español',
eu: 'Euskara',
fa: 'فارسی', fa: 'فارسی',
gl: 'Galego',
fi: 'Suomi', fi: 'Suomi',
fr: 'Français', fr: 'Français',
gl: 'Galego',
he: 'עברית', he: 'עברית',
hr: 'Hrvatski', hr: 'Hrvatski',
hu: 'Magyar', hu: 'Magyar',
@ -30,9 +33,11 @@ module SettingsHelper
'pt-BR': 'Português do Brasil', 'pt-BR': 'Português do Brasil',
ru: 'Русский', ru: 'Русский',
sk: 'Slovensky', sk: 'Slovensky',
sl: 'Slovenščina',
sr: 'Српски', sr: 'Српски',
'sr-Latn': 'Srpski (latinica)', 'sr-Latn': 'Srpski (latinica)',
sv: 'Svenska', sv: 'Svenska',
te: 'తెలుగు',
th: 'ภาษาไทย', th: 'ภาษาไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',

View file

@ -4,25 +4,29 @@ module StreamEntriesHelper
EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed' EMBEDDED_ACTION = 'embed'
def display_name(account) def display_name(account, **options)
account.display_name.presence || account.username if options[:custom_emojify]
Formatter.instance.format_display_name(account, options)
else
account.display_name.presence || account.username
end
end end
def account_description(account) def account_description(account)
prepend_str = [ prepend_str = [
[ [
number_to_human(account.statuses_count, strip_insignificant_zeros: true), number_to_human(account.statuses_count, strip_insignificant_zeros: true),
t('accounts.posts'), I18n.t('accounts.posts'),
].join(' '), ].join(' '),
[ [
number_to_human(account.following_count, strip_insignificant_zeros: true), number_to_human(account.following_count, strip_insignificant_zeros: true),
t('accounts.following'), I18n.t('accounts.following'),
].join(' '), ].join(' '),
[ [
number_to_human(account.followers_count, strip_insignificant_zeros: true), number_to_human(account.followers_count, strip_insignificant_zeros: true),
t('accounts.followers'), I18n.t('accounts.followers'),
].join(' '), ].join(' '),
].join(', ') ].join(', ')
@ -40,16 +44,16 @@ module StreamEntriesHelper
end end
end end
text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| t("statuses.attached.#{key}", count: value) }.join(' · ') text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ')
return if text.blank? return if text.blank?
t('statuses.attached.description', attached: text) I18n.t('statuses.attached.description', attached: text)
end end
def status_text_summary(status) def status_text_summary(status)
return if status.spoiler_text.blank? return if status.spoiler_text.blank?
t('statuses.content_warning', warning: status.spoiler_text) I18n.t('statuses.content_warning', warning: status.spoiler_text)
end end
def status_description(status) def status_description(status)
@ -113,6 +117,19 @@ module StreamEntriesHelper
end end
end end
def fa_visibility_icon(status)
case status.visibility
when 'public'
fa_icon 'globe fw'
when 'unlisted'
fa_icon 'unlock-alt fw'
when 'private'
fa_icon 'lock fw'
when 'direct'
fa_icon 'envelope fw'
end
end
private private
def simplified_text(text) def simplified_text(text)

View file

@ -1,5 +1,5 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import asyncDB from '../db/async'; import openDB from '../storage/db';
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
@ -94,16 +94,19 @@ export function fetchAccount(id) {
dispatch(fetchAccountRequest(id)); dispatch(fetchAccountRequest(id));
asyncDB.then(db => getFromDB( openDB().then(db => getFromDB(
dispatch, dispatch,
getState, getState,
db.transaction('accounts', 'read').objectStore('accounts').index('id'), db.transaction('accounts', 'read').objectStore('accounts').index('id'),
id id
)).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => { ).then(() => db.close(), error => {
db.close();
throw error;
})).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data)); dispatch(importFetchedAccount(response.data));
})).then(() => { })).then(() => {
dispatch(fetchAccountSuccess()); dispatch(fetchAccountSuccess());
}, error => { }).catch(error => {
dispatch(fetchAccountFail(id, error)); dispatch(fetchAccountFail(id, error));
}); });
}; };

View file

@ -1,3 +1,10 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
});
export const ALERT_SHOW = 'ALERT_SHOW'; export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS'; export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR'; export const ALERT_CLEAR = 'ALERT_CLEAR';
@ -22,3 +29,21 @@ export function showAlert(title, message) {
message, message,
}; };
}; };
export function showAlertForError(error) {
if (error.response) {
const { data, status, statusText } = error.response;
let message = statusText;
let title = `${status}`;
if (data.error) {
message = data.error;
}
return showAlert(title, message);
} else {
console.error(error);
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
}
}

View file

@ -1,8 +1,9 @@
import { saveSettings } from './settings'; import { saveSettings } from './settings';
export const COLUMN_ADD = 'COLUMN_ADD'; export const COLUMN_ADD = 'COLUMN_ADD';
export const COLUMN_REMOVE = 'COLUMN_REMOVE'; export const COLUMN_REMOVE = 'COLUMN_REMOVE';
export const COLUMN_MOVE = 'COLUMN_MOVE'; export const COLUMN_MOVE = 'COLUMN_MOVE';
export const COLUMN_PARAMS_CHANGE = 'COLUMN_PARAMS_CHANGE';
export function addColumn(id, params) { export function addColumn(id, params) {
return dispatch => { return dispatch => {
@ -38,3 +39,15 @@ export function moveColumn(uuid, direction) {
dispatch(saveSettings()); dispatch(saveSettings());
}; };
}; };
export function changeColumnParams(uuid, params) {
return dispatch => {
dispatch({
type: COLUMN_PARAMS_CHANGE,
uuid,
params,
});
dispatch(saveSettings());
};
}

View file

@ -1,11 +1,13 @@
import api from '../api'; import api from '../api';
import { CancelToken } from 'axios'; import { CancelToken, isCancel } from 'axios';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings'; import { tagHistory } from '../settings';
import { useEmoji } from './emojis'; import { useEmoji } from './emojis';
import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts';
let cancelFetchComposeSuggestionsAccounts; let cancelFetchComposeSuggestionsAccounts;
@ -15,6 +17,7 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET'; export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
@ -91,6 +94,19 @@ export function mentionCompose(account, router) {
}; };
}; };
export function directCompose(account, router) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
account: account,
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
};
};
export function submitCompose() { export function submitCompose() {
return function (dispatch, getState) { return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], ''); const status = getState().getIn(['compose', 'text'], '');
@ -130,6 +146,8 @@ export function submitCompose() {
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community'); insertIfOnline('community');
insertIfOnline('public'); insertIfOnline('public');
} else if (response.data.visibility === 'direct') {
insertIfOnline('direct');
} }
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
@ -165,18 +183,14 @@ export function uploadCompose(files) {
dispatch(uploadComposeRequest()); dispatch(uploadComposeRequest());
let data = new FormData(); resizeImage(files[0]).then(file => {
data.append('file', files[0]); const data = new FormData();
data.append('file', file);
api(getState).post('/api/v1/media', data, { return api(getState).post('/api/v1/media', data, {
onUploadProgress: function (e) { onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
dispatch(uploadComposeProgress(e.loaded, e.total)); }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}, }).catch(error => dispatch(uploadComposeFail(error)));
}).then(function (response) {
dispatch(uploadComposeSuccess(response.data));
}).catch(function (error) {
dispatch(uploadComposeFail(error));
});
}; };
}; };
@ -277,6 +291,10 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
}).then(response => { }).then(response => {
dispatch(importFetchedAccounts(response.data)); dispatch(importFetchedAccounts(response.data));
dispatch(readyComposeSuggestionsAccounts(token, response.data)); dispatch(readyComposeSuggestionsAccounts(token, response.data));
}).catch(error => {
if (!isCancel(error)) {
dispatch(showAlertForError(error));
}
}); });
}, 200, { leading: true, trailing: true }); }, 200, { leading: true, trailing: true });
@ -427,11 +445,12 @@ export function changeComposeVisibility(value) {
}; };
}; };
export function insertEmojiCompose(position, emoji) { export function insertEmojiCompose(position, emoji, needsSpace) {
return { return {
type: COMPOSE_EMOJI_INSERT, type: COMPOSE_EMOJI_INSERT,
position, position,
emoji, emoji,
needsSpace,
}; };
}; };

View file

@ -0,0 +1,37 @@
import api from '../api';
export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
export function fetchCustomEmojis() {
return (dispatch, getState) => {
dispatch(fetchCustomEmojisRequest());
api(getState).get('/api/v1/custom_emojis').then(response => {
dispatch(fetchCustomEmojisSuccess(response.data));
}).catch(error => {
dispatch(fetchCustomEmojisFail(error));
});
};
};
export function fetchCustomEmojisRequest() {
return {
type: CUSTOM_EMOJIS_FETCH_REQUEST,
};
};
export function fetchCustomEmojisSuccess(custom_emojis) {
return {
type: CUSTOM_EMOJIS_FETCH_SUCCESS,
custom_emojis,
};
};
export function fetchCustomEmojisFail(error) {
return {
type: CUSTOM_EMOJIS_FETCH_FAIL,
error,
};
};

View file

@ -12,12 +12,18 @@ export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL';
export function blockDomain(domain, accountId) { export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL';
export function blockDomain(domain) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(blockDomainRequest(domain)); dispatch(blockDomainRequest(domain));
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
dispatch(blockDomainSuccess(domain, accountId)); const at_domain = '@' + domain;
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
dispatch(blockDomainSuccess(domain, accounts));
}).catch(err => { }).catch(err => {
dispatch(blockDomainFail(domain, err)); dispatch(blockDomainFail(domain, err));
}); });
@ -31,11 +37,11 @@ export function blockDomainRequest(domain) {
}; };
}; };
export function blockDomainSuccess(domain, accountId) { export function blockDomainSuccess(domain, accounts) {
return { return {
type: DOMAIN_BLOCK_SUCCESS, type: DOMAIN_BLOCK_SUCCESS,
domain, domain,
accountId, accounts,
}; };
}; };
@ -47,12 +53,14 @@ export function blockDomainFail(domain, error) {
}; };
}; };
export function unblockDomain(domain, accountId) { export function unblockDomain(domain) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain)); dispatch(unblockDomainRequest(domain));
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
dispatch(unblockDomainSuccess(domain, accountId)); const at_domain = '@' + domain;
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
dispatch(unblockDomainSuccess(domain, accounts));
}).catch(err => { }).catch(err => {
dispatch(unblockDomainFail(domain, err)); dispatch(unblockDomainFail(domain, err));
}); });
@ -66,11 +74,11 @@ export function unblockDomainRequest(domain) {
}; };
}; };
export function unblockDomainSuccess(domain, accountId) { export function unblockDomainSuccess(domain, accounts) {
return { return {
type: DOMAIN_UNBLOCK_SUCCESS, type: DOMAIN_UNBLOCK_SUCCESS,
domain, domain,
accountId, accounts,
}; };
}; };
@ -86,7 +94,7 @@ export function fetchDomainBlocks() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchDomainBlocksRequest()); dispatch(fetchDomainBlocksRequest());
api(getState).get().then(response => { api(getState).get('/api/v1/domain_blocks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => { }).catch(err => {
@ -115,3 +123,43 @@ export function fetchDomainBlocksFail(error) {
error, error,
}; };
}; };
export function expandDomainBlocks() {
return (dispatch, getState) => {
const url = getState().getIn(['domain_lists', 'blocks', 'next']);
if (url === null) {
return;
}
dispatch(expandDomainBlocksRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(expandDomainBlocksFail(err));
});
};
};
export function expandDomainBlocksRequest() {
return {
type: DOMAIN_BLOCKS_EXPAND_REQUEST,
};
};
export function expandDomainBlocksSuccess(domains, next) {
return {
type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
domains,
next,
};
};
export function expandDomainBlocksFail(error) {
return {
type: DOMAIN_BLOCKS_EXPAND_FAIL,
error,
};
};

View file

@ -1,4 +1,5 @@
import { putAccounts, putStatuses } from '../../db/modifier'; import { autoPlayGif } from '../../initial_state';
import { putAccounts, putStatuses } from '../../storage/modifier';
import { normalizeAccount, normalizeStatus } from './normalizer'; import { normalizeAccount, normalizeStatus } from './normalizer';
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
@ -44,7 +45,7 @@ export function importFetchedAccounts(accounts) {
} }
accounts.forEach(processAccount); accounts.forEach(processAccount);
putAccounts(normalAccounts); putAccounts(normalAccounts, !autoPlayGif);
return importAccounts(normalAccounts); return importAccounts(normalAccounts);
} }

View file

@ -1,14 +1,31 @@
import escapeTextContentForBrowser from 'escape-html'; import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji'; import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
const domParser = new DOMParser(); const domParser = new DOMParser();
const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
export function normalizeAccount(account) { export function normalizeAccount(account) {
account = { ...account }; account = { ...account };
const emojiMap = makeEmojiMap(account);
const displayName = account.display_name.length === 0 ? account.username : account.display_name; const displayName = account.display_name.length === 0 ? account.username : account.display_name;
account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
account.note_emojified = emojify(account.note); account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
if (account.fields) {
account.fields = account.fields.map(pair => ({
...pair,
name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
value_emojified: emojify(pair.value, emojiMap),
value_plain: unescapeHTML(pair.value),
}));
}
if (account.moved) { if (account.moved) {
account.moved = account.moved.id; account.moved = account.moved.id;
@ -34,11 +51,7 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.hidden = normalOldStatus.get('hidden'); normalStatus.hidden = normalOldStatus.get('hidden');
} else { } else {
const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);

View file

@ -1,5 +1,6 @@
import api from '../api'; import api from '../api';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { showAlertForError } from './alerts';
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
@ -236,7 +237,7 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
dispatch(importFetchedAccounts(data)); dispatch(importFetchedAccounts(data));
dispatch(fetchListSuggestionsReady(q, data)); dispatch(fetchListSuggestionsReady(q, data));
}); }).catch(error => dispatch(showAlertForError(error)));
}; };
export const fetchListSuggestionsReady = (query, accounts) => ({ export const fetchListSuggestionsReady = (query, accounts) => ({

View file

@ -8,8 +8,10 @@ import {
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { unescapeHTML } from '../utils/html';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
@ -20,6 +22,7 @@ export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
defineMessages({ defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
}); });
const fetchRelatedRelationships = (dispatch, notifications) => { const fetchRelatedRelationships = (dispatch, notifications) => {
@ -30,28 +33,32 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
} }
}; };
const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
html = html.replace(/<br \/>|<br>|\n/g, ' ');
wrapper.innerHTML = html;
return wrapper.textContent;
};
export function updateNotifications(notification, intlMessages, intlLocale) { export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => { return (dispatch, getState) => {
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
dispatch(importFetchedAccount(notification.account)); if (showInColumn) {
dispatch(importFetchedStatus(notification.status)); dispatch(importFetchedAccount(notification.account));
dispatch({ if (notification.status) {
type: NOTIFICATIONS_UPDATE, dispatch(importFetchedStatus(notification.status));
notification, }
meta: playSound ? { sound: 'boop' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]); dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
meta: playSound ? { sound: 'boop' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'boop' },
});
}
// Desktop notifications // Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert) { if (typeof window.Notification !== 'undefined' && showAlert) {
@ -59,6 +66,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : ''); const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id }); const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
notify.addEventListener('click', () => { notify.addEventListener('click', () => {
window.focus(); window.focus();
notify.close(); notify.close();
@ -69,9 +77,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
export function expandNotifications({ maxId } = {}) { const noOp = () => {};
export function expandNotifications({ maxId } = {}, done = noOp) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (getState().getIn(['notifications', 'isLoading'])) { const notifications = getState().get('notifications');
if (notifications.get('isLoading')) {
done();
return; return;
} }
@ -80,6 +93,10 @@ export function expandNotifications({ maxId } = {}) {
exclude_types: excludeTypesFromSettings(getState()), exclude_types: excludeTypesFromSettings(getState()),
}; };
if (!maxId && notifications.get('items').size > 0) {
params.since_id = notifications.getIn(['items', 0]);
}
dispatch(expandNotificationsRequest()); dispatch(expandNotificationsRequest());
api(getState).get('/api/v1/notifications', { params }).then(response => { api(getState).get('/api/v1/notifications', { params }).then(response => {
@ -90,8 +107,10 @@ export function expandNotifications({ maxId } = {}) {
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
fetchRelatedRelationships(dispatch, response.data); fetchRelatedRelationships(dispatch, response.data);
done();
}).catch(error => { }).catch(error => {
dispatch(expandNotificationsFail(error)); dispatch(expandNotificationsFail(error));
done();
}); });
}; };
}; };

View file

@ -1,4 +1,5 @@
import api from '../../api'; import api from '../../api';
import { decode as decodeBase64 } from '../../utils/base64';
import { pushNotificationsSetting } from '../../settings'; import { pushNotificationsSetting } from '../../settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
import { me } from '../../initial_state'; import { me } from '../../initial_state';
@ -10,13 +11,7 @@ const urlBase64ToUint8Array = (base64String) => {
.replace(/\-/g, '+') .replace(/\-/g, '+')
.replace(/_/g, '/'); .replace(/_/g, '/');
const rawData = window.atob(base64); return decodeBase64(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}; };
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
@ -36,7 +31,7 @@ const subscribe = (registration) =>
const unsubscribe = ({ registration, subscription }) => const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration; subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (getState, subscription) => { const sendSubscriptionToBackend = (subscription) => {
const params = { subscription }; const params = { subscription };
if (me) { if (me) {
@ -46,7 +41,7 @@ const sendSubscriptionToBackend = (getState, subscription) => {
} }
} }
return api(getState).post('/api/web/push_subscriptions', params).then(response => response.data); return api().post('/api/web/push_subscriptions', params).then(response => response.data);
}; };
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
@ -56,13 +51,6 @@ export function register () {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(setBrowserSupport(supportsPushNotifications)); dispatch(setBrowserSupport(supportsPushNotifications));
if (me && !pushNotificationsSetting.get(me)) {
const alerts = getState().getIn(['push_notifications', 'alerts']);
if (alerts) {
pushNotificationsSetting.set(me, { alerts: alerts });
}
}
if (supportsPushNotifications) { if (supportsPushNotifications) {
if (!getApplicationServerKey()) { if (!getApplicationServerKey()) {
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
@ -85,13 +73,13 @@ export function register () {
} else { } else {
// Something went wrong, try to subscribe again // Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then( return unsubscribe({ registration, subscription }).then(subscribe).then(
subscription => sendSubscriptionToBackend(getState, subscription)); subscription => sendSubscriptionToBackend(subscription));
} }
} }
// No subscription, try to subscribe // No subscription, try to subscribe
return subscribe(registration).then( return subscribe(registration).then(
subscription => sendSubscriptionToBackend(getState, subscription)); subscription => sendSubscriptionToBackend(subscription));
}) })
.then(subscription => { .then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend) // If we got a PushSubscription (and not a subscription object from the backend)
@ -116,14 +104,11 @@ export function register () {
pushNotificationsSetting.remove(me); pushNotificationsSetting.remove(me);
} }
try { return getRegistration()
getRegistration() .then(getPushSubscription)
.then(getPushSubscription) .then(unsubscribe);
.then(unsubscribe); })
} catch (e) { .catch(console.warn);
}
});
} else { } else {
console.warn('Your browser does not support Web Push Notifications.'); console.warn('Your browser does not support Web Push Notifications.');
} }
@ -137,12 +122,12 @@ export function saveSettings() {
const alerts = state.get('alerts'); const alerts = state.get('alerts');
const data = { alerts }; const data = { alerts };
api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, { api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data, data,
}).then(() => { }).then(() => {
if (me) { if (me) {
pushNotificationsSetting.set(me, data); pushNotificationsSetting.set(me, data);
} }
}); }).catch(console.warn);
}; };
} }

View file

@ -33,7 +33,7 @@ export function submitSearch() {
dispatch(fetchSearchRequest()); dispatch(fetchSearchRequest());
api(getState).get('/api/v1/search', { api(getState).get('/api/v2/search', {
params: { params: {
q: value, q: value,
resolve: true, resolve: true,

View file

@ -1,5 +1,6 @@
import api from '../api'; import api from '../api';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { showAlertForError } from './alerts';
export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_CHANGE = 'SETTING_CHANGE';
export const SETTING_SAVE = 'SETTING_SAVE'; export const SETTING_SAVE = 'SETTING_SAVE';
@ -23,7 +24,9 @@ const debouncedSave = debounce((dispatch, getState) => {
const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS(); const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS();
api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); api().put('/api/web/settings', { data })
.then(() => dispatch({ type: SETTING_SAVE }))
.catch(error => dispatch(showAlertForError(error)));
}, 5000, { trailing: true }); }, 5000, { trailing: true });
export function saveSettings() { export function saveSettings() {

View file

@ -1,6 +1,6 @@
import api from '../api'; import api from '../api';
import asyncDB from '../db/async'; import openDB from '../storage/db';
import { evictStatus } from '../db/modifier'; import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import { fetchStatusCard } from './cards'; import { fetchStatusCard } from './cards';
@ -29,6 +29,8 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_REVEAL = 'STATUS_REVEAL'; export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE'; export const STATUS_HIDE = 'STATUS_HIDE';
export const REDRAFT = 'REDRAFT';
export function fetchStatusRequest(id, skipLoading) { export function fetchStatusRequest(id, skipLoading) {
return { return {
type: STATUS_FETCH_REQUEST, type: STATUS_FETCH_REQUEST,
@ -92,12 +94,17 @@ export function fetchStatus(id) {
dispatch(fetchStatusRequest(id, skipLoading)); dispatch(fetchStatusRequest(id, skipLoading));
asyncDB.then(db => { openDB().then(db => {
const transaction = db.transaction(['accounts', 'statuses'], 'read'); const transaction = db.transaction(['accounts', 'statuses'], 'read');
const accountIndex = transaction.objectStore('accounts').index('id'); const accountIndex = transaction.objectStore('accounts').index('id');
const index = transaction.objectStore('statuses').index('id'); const index = transaction.objectStore('statuses').index('id');
return getFromDB(dispatch, getState, accountIndex, index, id); return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
db.close();
}, error => {
db.close();
throw error;
});
}).then(() => { }).then(() => {
dispatch(fetchStatusSuccess(skipLoading)); dispatch(fetchStatusSuccess(skipLoading));
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
@ -126,14 +133,27 @@ export function fetchStatusFail(id, error, skipLoading) {
}; };
}; };
export function deleteStatus(id) { export function redraft(status) {
return {
type: REDRAFT,
status,
};
};
export function deleteStatus(id, withRedraft = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
const status = getState().getIn(['statuses', id]);
dispatch(deleteStatusRequest(id)); dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(() => { api(getState).delete(`/api/v1/statuses/${id}`).then(() => {
evictStatus(id); evictStatus(id);
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
if (withRedraft) {
dispatch(redraft(status));
}
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));
}); });

View file

@ -36,14 +36,13 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
}); });
} }
function refreshHomeTimelineAndNotification (dispatch) { const refreshHomeTimelineAndNotification = (dispatch, done) => {
dispatch(expandHomeTimeline()); dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done))));
dispatch(expandNotifications()); };
}
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = () => connectTimelineStream('public', 'public'); export const connectHashtagStream = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);

View file

@ -1,6 +1,6 @@
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE';
@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
export function updateTimeline(timeline, status) { export function updateTimeline(timeline, status) {
return (dispatch, getState) => { return (dispatch, getState) => {
const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
const parents = [];
if (status.in_reply_to_id) {
let parent = getState().getIn(['statuses', status.in_reply_to_id]);
while (parent && parent.get('in_reply_to_id')) {
parents.push(parent.get('id'));
parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]);
}
}
dispatch(importFetchedStatus(status)); dispatch(importFetchedStatus(status));
@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) {
status, status,
references, references,
}); });
if (parents.length > 0) {
dispatch({
type: TIMELINE_CONTEXT_UPDATE,
status,
references: parents,
});
}
}; };
}; };
@ -64,34 +44,44 @@ export function deleteFromTimelines(id) {
}; };
}; };
export function expandTimeline(timelineId, path, params = {}) { const noOp = () => {};
export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => { return (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
if (timeline.get('isLoading')) { if (timeline.get('isLoading')) {
done();
return; return;
} }
if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) {
params.since_id = timeline.getIn(['items', 0]);
}
dispatch(expandTimelineRequest(timelineId)); dispatch(expandTimelineRequest(timelineId));
api(getState).get(path, { params }).then(response => { api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); dispatch(importFetchedStatuses(response.data));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206));
done();
}).catch(error => { }).catch(error => {
dispatch(expandTimelineFail(timelineId, error)); dispatch(expandTimelineFail(timelineId, error));
done();
}); });
}; };
}; };
export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }); export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }); export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
export const expandHashtagTimeline = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }); export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export function expandTimelineRequest(timeline) { export function expandTimelineRequest(timeline) {
return { return {

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