mirror of
https://framagit.org/tykayn/mastodon.git
synced 2023-08-25 08:33:12 +02:00
Merge branch 'master' of https://github.com/tootsuite/mastodon
# Conflicts: # app/javascript/mastodon/components/status_action_bar.js # app/javascript/mastodon/features/account/components/header.js # config/webpack/production.js # db/schema.rb # yarn.lock
This commit is contained in:
commit
aff743695a
@ -30,7 +30,7 @@ plugins:
|
|||||||
channel: eslint-7
|
channel: eslint-7
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
channel: rubocop-0-82
|
channel: rubocop-0-88
|
||||||
sass-lint:
|
sass-lint:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
|
185
.rubocop.yml
185
.rubocop.yml
@ -25,30 +25,68 @@ Layout/AccessModifierIndentation:
|
|||||||
Layout/EmptyLineAfterMagicComment:
|
Layout/EmptyLineAfterMagicComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLineAfterGuardClause:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLinesAroundAttributeAccessor:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Layout/HashAlignment:
|
||||||
|
Enabled: false
|
||||||
|
# EnforcedHashRocketStyle: table
|
||||||
|
# EnforcedColonStyle: table
|
||||||
|
|
||||||
|
Layout/SpaceAroundMethodCallOperator:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Layout/SpaceInsideHashLiteralBraces:
|
Layout/SpaceInsideHashLiteralBraces:
|
||||||
EnforcedStyle: space
|
EnforcedStyle: space
|
||||||
|
|
||||||
|
Lint/DeprecatedOpenSSLConstant:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Lint/DuplicateElsifCondition:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Lint/MixedRegexpCaptureTypes:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Lint/RaiseException:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Lint/StructNewOverride:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Lint/UselessAccessModifier:
|
Lint/UselessAccessModifier:
|
||||||
ContextCreatingMethods:
|
ContextCreatingMethods:
|
||||||
- class_methods
|
- class_methods
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 100
|
Max: 100
|
||||||
|
Exclude:
|
||||||
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Max: 35
|
Max: 55
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/tasks/**/*'
|
- 'lib/tasks/**/*'
|
||||||
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 3
|
Max: 3
|
||||||
|
Exclude:
|
||||||
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
Max: 300
|
Max: 400
|
||||||
|
Exclude:
|
||||||
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 25
|
Max: 25
|
||||||
|
Exclude:
|
||||||
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
AllowURI: true
|
AllowURI: true
|
||||||
@ -56,7 +94,9 @@ Layout/LineLength:
|
|||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
Max: 55
|
Max: 65
|
||||||
|
Exclude:
|
||||||
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
@ -67,34 +107,90 @@ Metrics/ParameterLists:
|
|||||||
CountKeywordArgs: true
|
CountKeywordArgs: true
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 20
|
Max: 25
|
||||||
|
|
||||||
Naming/MemoizedInstanceVariableName:
|
Naming/MemoizedInstanceVariableName:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Naming/MethodParameterName:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Rails:
|
Rails:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Rails/ApplicationController:
|
||||||
|
Enabled: false
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/well_known/**/*.rb'
|
||||||
|
|
||||||
|
Rails/BelongsTo:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/ContentTag:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/EnumHash:
|
Rails/EnumHash:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/HasAndBelongsToMany:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Rails/SkipsModelValidations:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Rails/HttpStatus:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Rails/Exit:
|
Rails/Exit:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/*'
|
- 'lib/mastodon/*'
|
||||||
- 'lib/cli.rb'
|
- 'lib/cli.rb'
|
||||||
|
|
||||||
|
Rails/FilePath:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/HasAndBelongsToMany:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/HasManyOrHasOneDependent:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/HelperInstanceVariable:
|
Rails/HelperInstanceVariable:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/HttpStatus:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/IndexBy:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/InverseOf:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/LexicallyScopedActionFilter:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/OutputSafety:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Rails/RakeEnvironment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RedundantForeignKey:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/UniqueValidationWithoutIndex:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/AccessorGrouping:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/AccessModifierDeclarations:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/ArrayCoercion:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/BisectedAttrAccessor:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/CaseLikeIf:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@ -109,6 +205,15 @@ Style/Documentation:
|
|||||||
Style/DoubleNegation:
|
Style/DoubleNegation:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Style/ExpandPathArguments:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/ExponentialNotation:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/FormatString:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/FormatStringToken:
|
Style/FormatStringToken:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@ -118,9 +223,33 @@ Style/FrozenStringLiteralComment:
|
|||||||
Style/GuardClause:
|
Style/GuardClause:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/HashAsLastArrayItem:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/HashEachMethods:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/HashLikeCase:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/HashTransformKeys:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/HashTransformValues:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/IfUnlessModifier:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/InverseMethods:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/Lambda:
|
Style/Lambda:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MutableConstant:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/PercentLiteralDelimiters:
|
Style/PercentLiteralDelimiters:
|
||||||
PreferredDelimiters:
|
PreferredDelimiters:
|
||||||
'%i': '()'
|
'%i': '()'
|
||||||
@ -129,9 +258,36 @@ Style/PercentLiteralDelimiters:
|
|||||||
Style/PerlBackrefs:
|
Style/PerlBackrefs:
|
||||||
AutoCorrect: false
|
AutoCorrect: false
|
||||||
|
|
||||||
|
Style/RedundantAssignment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantFetchBlock:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/RedundantFileExtensionInRequire:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/RedundantRegexpCharacterClass:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantRegexpEscape:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantReturn:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Style/RegexpLiteral:
|
Style/RegexpLiteral:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RescueStandardError:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/SignalException:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/SlicingWithRange:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Style/SymbolArray:
|
Style/SymbolArray:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@ -140,3 +296,6 @@ Style/TrailingCommaInArrayLiteral:
|
|||||||
|
|
||||||
Style/TrailingCommaInHashLiteral:
|
Style/TrailingCommaInHashLiteral:
|
||||||
EnforcedStyleForMultiline: 'comma'
|
EnforcedStyleForMultiline: 'comma'
|
||||||
|
|
||||||
|
Style/UnpackFirst:
|
||||||
|
Enabled: false
|
||||||
|
1
Aptfile
1
Aptfile
@ -5,7 +5,6 @@ libidn11
|
|||||||
libidn11-dev
|
libidn11-dev
|
||||||
libpq-dev
|
libpq-dev
|
||||||
libprotobuf-dev
|
libprotobuf-dev
|
||||||
libssl-dev
|
|
||||||
libxdamage1
|
libxdamage1
|
||||||
libxfixes3
|
libxfixes3
|
||||||
protobuf-compiler
|
protobuf-compiler
|
||||||
|
19
Dockerfile
19
Dockerfile
@ -36,7 +36,8 @@ RUN apt update && \
|
|||||||
./autogen.sh && \
|
./autogen.sh && \
|
||||||
./configure --prefix=/opt/jemalloc && \
|
./configure --prefix=/opt/jemalloc && \
|
||||||
make -j$(nproc) > /dev/null && \
|
make -j$(nproc) > /dev/null && \
|
||||||
make install_bin install_include install_lib
|
make install_bin install_include install_lib && \
|
||||||
|
cd .. && rm -rf jemalloc-$JE_VER $JE_VER.tar.gz
|
||||||
|
|
||||||
# Install Ruby
|
# Install Ruby
|
||||||
ENV RUBY_VER="2.6.6"
|
ENV RUBY_VER="2.6.6"
|
||||||
@ -56,7 +57,8 @@ RUN apt update && \
|
|||||||
--disable-install-doc && \
|
--disable-install-doc && \
|
||||||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
||||||
make -j$(nproc) > /dev/null && \
|
make -j$(nproc) > /dev/null && \
|
||||||
make install
|
make install && \
|
||||||
|
cd .. && rm -rf ruby-$RUBY_VER.tar.gz ruby-$RUBY_VER
|
||||||
|
|
||||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
||||||
|
|
||||||
@ -107,11 +109,14 @@ RUN apt -y --no-install-recommends install \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Add tini
|
# Add tini
|
||||||
ENV TINI_VERSION="0.18.0"
|
ENV TINI_VERSION="0.19.0"
|
||||||
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855"
|
RUN dpkgArch="$(dpkg --print-architecture)" && \
|
||||||
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
|
ARCH=$dpkgArch && \
|
||||||
RUN echo "$TINI_SUM tini" | sha256sum -c -
|
wget https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH \
|
||||||
RUN chmod +x /tini
|
https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH.sha256sum && \
|
||||||
|
cat tini-$ARCH.sha256sum | sha256sum -c - && \
|
||||||
|
mv tini-$ARCH /tini && rm tini-$ARCH.sha256sum && \
|
||||||
|
chmod +x /tini
|
||||||
|
|
||||||
# Copy over mastodon source, and dependencies from building, and set permissions
|
# Copy over mastodon source, and dependencies from building, and set permissions
|
||||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
COPY --chown=mastodon:mastodon . /opt/mastodon
|
||||||
|
30
Gemfile
30
Gemfile
@ -6,9 +6,9 @@ ruby '>= 2.5.0', '< 3.0.0'
|
|||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
|
|
||||||
gem 'puma', '~> 4.3'
|
gem 'puma', '~> 4.3'
|
||||||
gem 'rails', '~> 5.2.4.3'
|
gem 'rails', '~> 5.2.4.4'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 0.20'
|
gem 'thor', '~> 1.0'
|
||||||
gem 'rack', '~> 2.2.3'
|
gem 'rack', '~> 2.2.3'
|
||||||
|
|
||||||
gem 'thwait', '~> 0.2.0'
|
gem 'thwait', '~> 0.2.0'
|
||||||
@ -17,10 +17,10 @@ gem 'e2mmap', '~> 0.1.0'
|
|||||||
gem 'hamlit-rails', '~> 0.2'
|
gem 'hamlit-rails', '~> 0.2'
|
||||||
gem 'pg', '~> 1.2'
|
gem 'pg', '~> 1.2'
|
||||||
gem 'makara', '~> 0.4'
|
gem 'makara', '~> 0.4'
|
||||||
gem 'pghero', '~> 2.6'
|
gem 'pghero', '~> 2.7'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.75', require: false
|
gem 'aws-sdk-s3', '~> 1.80', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
@ -56,12 +56,11 @@ 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.7'
|
gem 'redis-namespace', '~> 1.8'
|
||||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 4.4'
|
gem 'http', '~> 4.4'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
|
|
||||||
gem 'httplog', '~> 1.4.3'
|
gem 'httplog', '~> 1.4.3'
|
||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
@ -98,8 +97,9 @@ gem 'strong_migrations', '~> 0.7'
|
|||||||
gem 'tty-prompt', '~> 0.22', require: false
|
gem 'tty-prompt', '~> 0.22', require: false
|
||||||
gem 'twitter-text', '~> 1.14'
|
gem 'twitter-text', '~> 1.14'
|
||||||
gem 'tzinfo-data', '~> 1.2020'
|
gem 'tzinfo-data', '~> 1.2020'
|
||||||
gem 'webpacker', '~> 5.1'
|
gem 'webpacker', '~> 5.2'
|
||||||
gem 'webpush'
|
gem 'webpush'
|
||||||
|
gem 'webauthn', '~> 3.0.0.alpha1'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
gem 'json-ld-preloaded', '~> 3.1'
|
gem 'json-ld-preloaded', '~> 3.1'
|
||||||
@ -121,28 +121,28 @@ end
|
|||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.33'
|
gem 'capybara', '~> 3.33'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.13'
|
gem 'faker', '~> 2.14'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.18', require: false
|
gem 'simplecov', '~> 0.19', require: false
|
||||||
gem 'webmock', '~> 3.8'
|
gem 'webmock', '~> 3.9'
|
||||||
gem 'parallel_tests', '~> 3.1'
|
gem 'parallel_tests', '~> 3.2'
|
||||||
gem 'rspec_junit_formatter', '~> 0.4'
|
gem 'rspec_junit_formatter', '~> 0.4'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'active_record_query_trace', '~> 1.7'
|
gem 'active_record_query_trace', '~> 1.7'
|
||||||
gem 'annotate', '~> 3.1'
|
gem 'annotate', '~> 3.1'
|
||||||
gem 'better_errors', '~> 2.7'
|
gem 'better_errors', '~> 2.8'
|
||||||
gem 'binding_of_caller', '~> 0.7'
|
gem 'binding_of_caller', '~> 0.7'
|
||||||
gem 'bullet', '~> 6.1'
|
gem 'bullet', '~> 6.1'
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 1.4'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.86', require: false
|
gem 'rubocop', '~> 0.90', require: false
|
||||||
gem 'rubocop-rails', '~> 2.6', require: false
|
gem 'rubocop-rails', '~> 2.8', require: false
|
||||||
gem 'brakeman', '~> 4.8', require: false
|
gem 'brakeman', '~> 4.9', require: false
|
||||||
gem 'bundler-audit', '~> 0.7', require: false
|
gem 'bundler-audit', '~> 0.7', require: false
|
||||||
|
|
||||||
gem 'capistrano', '~> 3.14'
|
gem 'capistrano', '~> 3.14'
|
||||||
|
249
Gemfile.lock
249
Gemfile.lock
@ -6,14 +6,6 @@ GIT
|
|||||||
health_check (4.0.0.pre)
|
health_check (4.0.0.pre)
|
||||||
rails (>= 4.0)
|
rails (>= 4.0)
|
||||||
|
|
||||||
GIT
|
|
||||||
remote: https://github.com/tmm1/http_parser.rb
|
|
||||||
revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
|
|
||||||
ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
|
|
||||||
submodules: true
|
|
||||||
specs:
|
|
||||||
http_parser.rb (0.6.1)
|
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/witgo/nilsimsa
|
remote: https://github.com/witgo/nilsimsa
|
||||||
revision: fd184883048b922b176939f851338d0a4971a532
|
revision: fd184883048b922b176939f851338d0a4971a532
|
||||||
@ -24,25 +16,25 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.2.4.3)
|
actioncable (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailer (5.2.4.3)
|
actionmailer (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
actionview (= 5.2.4.3)
|
actionview (= 5.2.4.4)
|
||||||
activejob (= 5.2.4.3)
|
activejob (= 5.2.4.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (5.2.4.3)
|
actionpack (5.2.4.4)
|
||||||
actionview (= 5.2.4.3)
|
actionview (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.0.8)
|
||||||
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.2.4.3)
|
actionview (5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
@ -53,20 +45,20 @@ 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.7)
|
active_record_query_trace (1.7)
|
||||||
activejob (5.2.4.3)
|
activejob (5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.2.4.3)
|
activemodel (5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
activerecord (5.2.4.3)
|
activerecord (5.2.4.4)
|
||||||
activemodel (= 5.2.4.3)
|
activemodel (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
arel (>= 9.0)
|
arel (>= 9.0)
|
||||||
activestorage (5.2.4.3)
|
activestorage (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
activerecord (= 5.2.4.3)
|
activerecord (= 5.2.4.4)
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (5.2.4.3)
|
activesupport (5.2.4.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
@ -75,6 +67,7 @@ GEM
|
|||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
airbrussh (1.4.0)
|
airbrussh (1.4.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
|
android_key_attestation (0.3.0)
|
||||||
annotate (3.1.1)
|
annotate (3.1.1)
|
||||||
activerecord (>= 3.2, < 7.0)
|
activerecord (>= 3.2, < 7.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
@ -84,34 +77,36 @@ GEM
|
|||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.345.0)
|
aws-partitions (1.368.0)
|
||||||
aws-sdk-core (3.104.3)
|
aws-sdk-core (3.105.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.36.0)
|
aws-sdk-kms (1.37.0)
|
||||||
aws-sdk-core (~> 3, >= 3.99.0)
|
aws-sdk-core (~> 3, >= 3.99.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.75.0)
|
aws-sdk-s3 (1.80.0)
|
||||||
aws-sdk-core (~> 3, >= 3.104.1)
|
aws-sdk-core (~> 3, >= 3.104.3)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.2.1)
|
aws-sigv4 (1.2.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
bcrypt (3.1.15)
|
bcrypt (3.1.16)
|
||||||
better_errors (2.7.1)
|
better_errors (2.8.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
|
bindata (2.4.8)
|
||||||
binding_of_caller (0.8.0)
|
binding_of_caller (0.8.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.4)
|
blurhash (0.1.4)
|
||||||
ffi (~> 1.10.0)
|
ffi (~> 1.10.0)
|
||||||
bootsnap (1.4.7)
|
bootsnap (1.4.8)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.8.2)
|
brakeman (4.9.1)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.0)
|
bullet (6.1.0)
|
||||||
@ -146,12 +141,13 @@ GEM
|
|||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
|
cbor (0.5.9.6)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (5.1.0)
|
chewy (5.1.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
elasticsearch (>= 2.0.0)
|
elasticsearch (>= 2.0.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.3.11)
|
chunky_png (1.3.12)
|
||||||
cld3 (3.3.0)
|
cld3 (3.3.0)
|
||||||
ffi (>= 1.1.0, < 1.12.0)
|
ffi (>= 1.1.0, < 1.12.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
@ -159,8 +155,11 @@ GEM
|
|||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.1.6)
|
concurrent-ruby (1.1.7)
|
||||||
connection_pool (2.2.3)
|
connection_pool (2.2.3)
|
||||||
|
cose (1.0.0)
|
||||||
|
cbor (~> 0.5.9)
|
||||||
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
@ -196,22 +195,22 @@ GEM
|
|||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
ed25519 (1.2.4)
|
ed25519 (1.2.4)
|
||||||
elasticsearch (7.8.0)
|
elasticsearch (7.9.0)
|
||||||
elasticsearch-api (= 7.8.0)
|
elasticsearch-api (= 7.9.0)
|
||||||
elasticsearch-transport (= 7.8.0)
|
elasticsearch-transport (= 7.9.0)
|
||||||
elasticsearch-api (7.8.0)
|
elasticsearch-api (7.9.0)
|
||||||
multi_json
|
multi_json
|
||||||
elasticsearch-dsl (0.1.9)
|
elasticsearch-dsl (0.1.9)
|
||||||
elasticsearch-transport (7.8.0)
|
elasticsearch-transport (7.9.0)
|
||||||
faraday (~> 1)
|
faraday (~> 1)
|
||||||
multi_json
|
multi_json
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubi (1.9.0)
|
erubi (1.9.0)
|
||||||
et-orbi (1.2.4)
|
et-orbi (1.2.4)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.75.0)
|
excon (0.76.0)
|
||||||
fabrication (2.21.1)
|
fabrication (2.21.1)
|
||||||
faker (2.13.0)
|
faker (2.14.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.0.1)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
@ -234,7 +233,7 @@ GEM
|
|||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.3.6)
|
fugit (1.3.8)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.3)
|
raabro (~> 1.3)
|
||||||
fuubar (2.5.0)
|
fuubar (2.5.0)
|
||||||
@ -247,7 +246,7 @@ GEM
|
|||||||
http (~> 4.0)
|
http (~> 4.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
oj (~> 3.0)
|
oj (~> 3.0)
|
||||||
hamlit (2.11.0)
|
hamlit (2.11.1)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
@ -307,7 +306,7 @@ GEM
|
|||||||
json-ld (~> 3.1)
|
json-ld (~> 3.1)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.1)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.2.1)
|
jwt (2.2.2)
|
||||||
kaminari (1.2.1)
|
kaminari (1.2.1)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.1)
|
kaminari-actionview (= 1.2.1)
|
||||||
@ -334,7 +333,7 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.6.0)
|
loofah (2.7.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
@ -347,7 +346,7 @@ GEM
|
|||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
memory_profiler (0.9.14)
|
memory_profiler (0.9.14)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
microformats (4.2.0)
|
microformats (4.2.1)
|
||||||
json (~> 2.2)
|
json (~> 2.2)
|
||||||
nokogiri (~> 1.10)
|
nokogiri (~> 1.10)
|
||||||
mime-types (3.3.1)
|
mime-types (3.3.1)
|
||||||
@ -356,15 +355,15 @@ GEM
|
|||||||
mimemagic (0.3.5)
|
mimemagic (0.3.5)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.14.1)
|
minitest (5.14.2)
|
||||||
msgpack (1.3.3)
|
msgpack (1.3.3)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-ldap (0.16.2)
|
net-ldap (0.16.3)
|
||||||
net-scp (3.0.0)
|
net-scp (3.0.0)
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.2)
|
nio4r (2.5.3)
|
||||||
nokogiri (1.10.10)
|
nokogiri (1.10.10)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
nokogumbo (2.0.2)
|
nokogumbo (2.0.2)
|
||||||
@ -374,7 +373,7 @@ GEM
|
|||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (>= 3.5)
|
sidekiq (>= 3.5)
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.10.8)
|
oj (3.10.14)
|
||||||
omniauth (1.9.1)
|
omniauth (1.9.1)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
@ -385,8 +384,10 @@ GEM
|
|||||||
omniauth-saml (1.10.2)
|
omniauth-saml (1.10.2)
|
||||||
omniauth (~> 1.3, >= 1.3.2)
|
omniauth (~> 1.3, >= 1.3.2)
|
||||||
ruby-saml (~> 1.9)
|
ruby-saml (~> 1.9)
|
||||||
|
openssl (2.2.0)
|
||||||
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.13.2)
|
ox (2.13.4)
|
||||||
paperclip (6.0.0)
|
paperclip (6.0.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@ -397,7 +398,7 @@ GEM
|
|||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.19.2)
|
parallel (1.19.2)
|
||||||
parallel_tests (3.1.0)
|
parallel_tests (3.2.0)
|
||||||
parallel
|
parallel
|
||||||
parser (2.7.1.4)
|
parser (2.7.1.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
@ -405,11 +406,11 @@ GEM
|
|||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pghero (2.6.0)
|
pghero (2.7.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.1)
|
pkg-config (1.4.2)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.12.1)
|
premailer (1.13.1)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.6.0)
|
css_parser (>= 1.6.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
@ -425,8 +426,8 @@ GEM
|
|||||||
pry (~> 0.13.0)
|
pry (~> 0.13.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.5)
|
public_suffix (4.0.6)
|
||||||
puma (4.3.5)
|
puma (4.3.6)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -440,18 +441,18 @@ GEM
|
|||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (5.2.4.3)
|
rails (5.2.4.4)
|
||||||
actioncable (= 5.2.4.3)
|
actioncable (= 5.2.4.4)
|
||||||
actionmailer (= 5.2.4.3)
|
actionmailer (= 5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
actionview (= 5.2.4.3)
|
actionview (= 5.2.4.4)
|
||||||
activejob (= 5.2.4.3)
|
activejob (= 5.2.4.4)
|
||||||
activemodel (= 5.2.4.3)
|
activemodel (= 5.2.4.4)
|
||||||
activerecord (= 5.2.4.3)
|
activerecord (= 5.2.4.4)
|
||||||
activestorage (= 5.2.4.3)
|
activestorage (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 5.2.4.3)
|
railties (= 5.2.4.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
@ -467,20 +468,20 @@ GEM
|
|||||||
railties (>= 5.0, < 6)
|
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.2.4.3)
|
railties (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.1)
|
rake (13.0.1)
|
||||||
rdf (3.1.4)
|
rdf (3.1.6)
|
||||||
hamster (~> 3.0)
|
hamster (~> 3.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.4.0)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.1)
|
||||||
redis (4.2.1)
|
redis (4.2.2)
|
||||||
redis-actionpack (5.2.0)
|
redis-actionpack (5.2.0)
|
||||||
actionpack (>= 5, < 7)
|
actionpack (>= 5, < 7)
|
||||||
redis-rack (>= 2.1.0, < 3)
|
redis-rack (>= 2.1.0, < 3)
|
||||||
@ -488,9 +489,9 @@ GEM
|
|||||||
redis-activesupport (5.2.0)
|
redis-activesupport (5.2.0)
|
||||||
activesupport (>= 3, < 7)
|
activesupport (>= 3, < 7)
|
||||||
redis-store (>= 1.3, < 2)
|
redis-store (>= 1.3, < 2)
|
||||||
redis-namespace (1.7.0)
|
redis-namespace (1.8.0)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
redis-rack (2.1.2)
|
redis-rack (2.1.3)
|
||||||
rack (>= 2.0.8, < 3)
|
rack (>= 2.0.8, < 3)
|
||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-rails (5.0.2)
|
redis-rails (5.0.2)
|
||||||
@ -534,33 +535,36 @@ GEM
|
|||||||
rspec-support (3.9.3)
|
rspec-support (3.9.3)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.4.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (0.86.0)
|
rubocop (0.90.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.1.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.7)
|
regexp_parser (>= 1.7)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.0.3, < 1.0)
|
rubocop-ast (>= 0.3.0, < 1.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-ast (0.2.0)
|
rubocop-ast (0.4.0)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.1.4)
|
||||||
rubocop-rails (2.6.0)
|
rubocop-rails (2.8.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.82.0)
|
rubocop (>= 0.87.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
ruby-saml (1.11.0)
|
ruby-saml (1.11.0)
|
||||||
nokogiri (>= 1.5.10)
|
nokogiri (>= 1.5.10)
|
||||||
rufus-scheduler (3.6.0)
|
rufus-scheduler (3.6.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safe_yaml (1.0.5)
|
safe_yaml (1.0.5)
|
||||||
|
safety_net_attestation (0.4.0)
|
||||||
|
jwt (~> 2.0)
|
||||||
sanitize (5.2.1)
|
sanitize (5.2.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.8.0)
|
nokogiri (>= 1.8.0)
|
||||||
nokogumbo (~> 2.0)
|
nokogumbo (~> 2.0)
|
||||||
|
securecompare (1.0.0)
|
||||||
semantic_range (2.3.0)
|
semantic_range (2.3.0)
|
||||||
sidekiq (6.1.1)
|
sidekiq (6.1.2)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
@ -573,23 +577,23 @@ GEM
|
|||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
thwait
|
thwait
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (6.0.22)
|
sidekiq-unique-jobs (6.0.23)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 4.0, < 7.0)
|
sidekiq (>= 4.0, < 7.0)
|
||||||
thor (~> 0)
|
thor (>= 0.20, < 2.0)
|
||||||
simple-navigation (4.1.0)
|
simple-navigation (4.1.0)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
simple_form (5.0.2)
|
simple_form (5.0.2)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
activemodel (>= 5.0)
|
activemodel (>= 5.0)
|
||||||
simplecov (0.18.5)
|
simplecov (0.19.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov-html (0.12.2)
|
simplecov-html (0.12.2)
|
||||||
sprockets (3.7.2)
|
sprockets (3.7.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.1)
|
sprockets-rails (3.2.2)
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 4.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
@ -608,12 +612,15 @@ GEM
|
|||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (0.20.3)
|
thor (1.0.1)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
thwait (0.2.0)
|
thwait (0.2.0)
|
||||||
e2mmap
|
e2mmap
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
tty-color (0.5.1)
|
tpm-key_attestation (0.9.0)
|
||||||
|
bindata (~> 2.4)
|
||||||
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
|
tty-color (0.5.2)
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-prompt (0.22.0)
|
tty-prompt (0.22.0)
|
||||||
pastel (~> 0.8)
|
pastel (~> 0.8)
|
||||||
@ -634,13 +641,23 @@ GEM
|
|||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.7.7)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.7.0)
|
||||||
uniform_notifier (1.13.0)
|
uniform_notifier (1.13.0)
|
||||||
warden (1.2.8)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.9)
|
||||||
webmock (3.8.3)
|
webauthn (3.0.0.alpha1)
|
||||||
|
android_key_attestation (~> 0.3.0)
|
||||||
|
awrence (~> 1.1)
|
||||||
|
bindata (~> 2.4)
|
||||||
|
cbor (~> 0.5.9)
|
||||||
|
cose (~> 1.0)
|
||||||
|
openssl (~> 2.0)
|
||||||
|
safety_net_attestation (~> 0.4.0)
|
||||||
|
securecompare (~> 1.0)
|
||||||
|
tpm-key_attestation (~> 0.9.0)
|
||||||
|
webmock (3.9.1)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webpacker (5.1.1)
|
webpacker (5.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
@ -663,12 +680,12 @@ DEPENDENCIES
|
|||||||
active_record_query_trace (~> 1.7)
|
active_record_query_trace (~> 1.7)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.75)
|
aws-sdk-s3 (~> 1.80)
|
||||||
better_errors (~> 2.7)
|
better_errors (~> 2.8)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.4)
|
bootsnap (~> 1.4)
|
||||||
brakeman (~> 4.8)
|
brakeman (~> 4.9)
|
||||||
browser
|
browser
|
||||||
bullet (~> 6.1)
|
bullet (~> 6.1)
|
||||||
bundler-audit (~> 0.7)
|
bundler-audit (~> 0.7)
|
||||||
@ -693,7 +710,7 @@ DEPENDENCIES
|
|||||||
e2mmap (~> 0.1.0)
|
e2mmap (~> 0.1.0)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
fabrication (~> 2.21)
|
fabrication (~> 2.21)
|
||||||
faker (~> 2.13)
|
faker (~> 2.14)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
@ -706,7 +723,6 @@ DEPENDENCIES
|
|||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
http (~> 4.4)
|
http (~> 4.4)
|
||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
http_parser.rb (~> 0.6)!
|
|
||||||
httplog (~> 1.4.3)
|
httplog (~> 1.4.3)
|
||||||
i18n-tasks (~> 0.9)
|
i18n-tasks (~> 0.9)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
@ -735,10 +751,10 @@ DEPENDENCIES
|
|||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel (~> 1.19)
|
parallel (~> 1.19)
|
||||||
parallel_tests (~> 3.1)
|
parallel_tests (~> 3.2)
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.2)
|
pg (~> 1.2)
|
||||||
pghero (~> 2.6)
|
pghero (~> 2.7)
|
||||||
pkg-config (~> 1.4)
|
pkg-config (~> 1.4)
|
||||||
posix-spawn
|
posix-spawn
|
||||||
premailer-rails
|
premailer-rails
|
||||||
@ -750,20 +766,20 @@ DEPENDENCIES
|
|||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.3)
|
rack-attack (~> 6.3)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
rails (~> 5.2.4.3)
|
rails (~> 5.2.4.4)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 5.1)
|
rails-i18n (~> 5.1)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
rdf-normalize (~> 0.4)
|
rdf-normalize (~> 0.4)
|
||||||
redis (~> 4.2)
|
redis (~> 4.2)
|
||||||
redis-namespace (~> 1.7)
|
redis-namespace (~> 1.8)
|
||||||
redis-rails (~> 5.0)
|
redis-rails (~> 5.0)
|
||||||
rqrcode (~> 1.1)
|
rqrcode (~> 1.1)
|
||||||
rspec-rails (~> 4.0)
|
rspec-rails (~> 4.0)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.4)
|
||||||
rubocop (~> 0.86)
|
rubocop (~> 0.90)
|
||||||
rubocop-rails (~> 2.6)
|
rubocop-rails (~> 2.8)
|
||||||
ruby-progressbar (~> 1.10)
|
ruby-progressbar (~> 1.10)
|
||||||
sanitize (~> 5.2)
|
sanitize (~> 5.2)
|
||||||
sidekiq (~> 6.1)
|
sidekiq (~> 6.1)
|
||||||
@ -772,20 +788,21 @@ DEPENDENCIES
|
|||||||
sidekiq-unique-jobs (~> 6.0)
|
sidekiq-unique-jobs (~> 6.0)
|
||||||
simple-navigation (~> 4.1)
|
simple-navigation (~> 4.1)
|
||||||
simple_form (~> 5.0)
|
simple_form (~> 5.0)
|
||||||
simplecov (~> 0.18)
|
simplecov (~> 0.19)
|
||||||
sprockets (~> 3.7.2)
|
sprockets (~> 3.7.2)
|
||||||
sprockets-rails (~> 3.2)
|
sprockets-rails (~> 3.2)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 2.2.1)
|
stoplight (~> 2.2.1)
|
||||||
streamio-ffmpeg (~> 3.0)
|
streamio-ffmpeg (~> 3.0)
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 0.20)
|
thor (~> 1.0)
|
||||||
thwait (~> 0.2.0)
|
thwait (~> 0.2.0)
|
||||||
tty-prompt (~> 0.22)
|
tty-prompt (~> 0.22)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2020)
|
tzinfo-data (~> 1.2020)
|
||||||
webmock (~> 3.8)
|
webauthn (~> 3.0.0.alpha1)
|
||||||
webpacker (~> 5.1)
|
webmock (~> 3.9)
|
||||||
|
webpacker (~> 5.2)
|
||||||
webpush
|
webpush
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
|
2
Procfile
2
Procfile
@ -1,4 +1,4 @@
|
|||||||
web: if [ "$RUN_STREAMING" != "true" ]; then BIND=0.0.0.0 bundle exec puma -C config/puma.rb; else BIND=0.0.0.0 node ./streaming; fi
|
web: bin/heroku-web
|
||||||
worker: bundle exec sidekiq
|
worker: bundle exec sidekiq
|
||||||
|
|
||||||
# For the streaming API, you need a separate app that shares Postgres and Redis:
|
# For the streaming API, you need a separate app that shares Postgres and Redis:
|
||||||
|
@ -7,6 +7,7 @@ class AccountsController < ApplicationController
|
|||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureAuthentication
|
include SignatureAuthentication
|
||||||
|
|
||||||
|
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
@ -28,8 +29,7 @@ class AccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
||||||
@statuses = filtered_status_page
|
@statuses = cached_filtered_status_page
|
||||||
@statuses = cache_collection(@statuses, Status)
|
|
||||||
@rss_url = rss_url
|
@rss_url = rss_url
|
||||||
|
|
||||||
unless @statuses.empty?
|
unless @statuses.empty?
|
||||||
@ -49,7 +49,7 @@ class AccountsController < ApplicationController
|
|||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
||||||
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
|
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -142,19 +142,16 @@ class AccountsController < ApplicationController
|
|||||||
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_status_page
|
def cached_filtered_status_page
|
||||||
filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
|
cache_collection_paginated_by_id(
|
||||||
|
filtered_statuses,
|
||||||
|
Status,
|
||||||
|
PAGE_SIZE,
|
||||||
|
params_slice(:max_id, :min_id, :since_id)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def params_slice(*keys)
|
def params_slice(*keys)
|
||||||
params.slice(*keys).permit(*keys)
|
params.slice(*keys).permit(*keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_fields_to
|
|
||||||
if signed_request_account.present? || public_fetch_mode?
|
|
||||||
# Return all fields
|
|
||||||
else
|
|
||||||
%i(id type preferred_username inbox public_key endpoints)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -12,7 +12,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in 3.minutes, public: public_fetch_mode?
|
expires_in 3.minutes, public: public_fetch_mode?
|
||||||
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
|
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -20,17 +20,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||||||
def set_items
|
def set_items
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@items = begin
|
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
|
||||||
# Because in public fetch mode we cache the response, there would be no
|
when 'tags'
|
||||||
# benefit from performing the check below, since a blocked account or domain
|
@items = for_signed_account { @account.featured_tags }
|
||||||
# would likely be served the cache from the reverse proxy anyway
|
|
||||||
|
|
||||||
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
|
||||||
[]
|
|
||||||
else
|
|
||||||
cache_collection(@account.pinned_statuses, Status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
when 'devices'
|
when 'devices'
|
||||||
@items = @account.devices
|
@items = @account.devices
|
||||||
else
|
else
|
||||||
@ -40,7 +32,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||||||
|
|
||||||
def set_size
|
def set_size
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured', 'devices'
|
when 'featured', 'devices', 'tags'
|
||||||
@size = @items.size
|
@size = @items.size
|
||||||
else
|
else
|
||||||
not_found
|
not_found
|
||||||
@ -51,7 +43,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@type = :ordered
|
@type = :ordered
|
||||||
when 'devices'
|
when 'devices', 'tags'
|
||||||
@type = :unordered
|
@type = :unordered
|
||||||
else
|
else
|
||||||
not_found
|
not_found
|
||||||
@ -66,4 +58,16 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||||||
items: @items
|
items: @items
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def for_signed_account
|
||||||
|
# Because in public fetch mode we cache the response, there would be no
|
||||||
|
# benefit from performing the check below, since a blocked account or domain
|
||||||
|
# would likely be served the cache from the reverse proxy anyway
|
||||||
|
|
||||||
|
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -20,9 +20,9 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||||||
def outbox_presenter
|
def outbox_presenter
|
||||||
if page_requested?
|
if page_requested?
|
||||||
ActivityPub::CollectionPresenter.new(
|
ActivityPub::CollectionPresenter.new(
|
||||||
id: account_outbox_url(@account, page_params),
|
id: outbox_url(page_params),
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
part_of: account_outbox_url(@account),
|
part_of: outbox_url,
|
||||||
prev: prev_page,
|
prev: prev_page,
|
||||||
next: next_page,
|
next: next_page,
|
||||||
items: @statuses
|
items: @statuses
|
||||||
@ -32,12 +32,20 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||||||
id: account_outbox_url(@account),
|
id: account_outbox_url(@account),
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
size: @account.statuses_count,
|
size: @account.statuses_count,
|
||||||
first: account_outbox_url(@account, page: true),
|
first: outbox_url(page: true),
|
||||||
last: account_outbox_url(@account, page: true, min_id: 0)
|
last: outbox_url(page: true, min_id: 0)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def outbox_url(**kwargs)
|
||||||
|
if params[:account_username].present?
|
||||||
|
account_outbox_url(@account, **kwargs)
|
||||||
|
else
|
||||||
|
instance_actor_outbox_url(**kwargs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def next_page
|
def next_page
|
||||||
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
|
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
|
||||||
end
|
end
|
||||||
@ -49,9 +57,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||||||
def set_statuses
|
def set_statuses
|
||||||
return unless page_requested?
|
return unless page_requested?
|
||||||
|
|
||||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
@statuses = cache_collection_paginated_by_id(
|
||||||
@statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
|
@account.statuses.permitted_for(@account, signed_request_account),
|
||||||
@statuses = cache_collection(@statuses, Status)
|
Status,
|
||||||
|
LIMIT,
|
||||||
|
params_slice(:max_id, :min_id, :since_id)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_requested?
|
def page_requested?
|
||||||
@ -61,4 +72,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||||||
def page_params
|
def page_params
|
||||||
{ page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact
|
{ page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
class AccountsController < BaseController
|
||||||
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
before_action :set_account, except: [:index]
|
||||||
before_action :require_remote_account!, only: [:redownload]
|
before_action :require_remote_account!, only: [:redownload]
|
||||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||||
|
|
||||||
@ -14,49 +14,58 @@ module Admin
|
|||||||
def show
|
def show
|
||||||
authorize @account, :show?
|
authorize @account, :show?
|
||||||
|
|
||||||
|
@deletion_request = @account.deletion_request
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.targeted_account_warnings.latest.custom
|
@warnings = @account.targeted_account_warnings.latest.custom
|
||||||
|
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def memorialize
|
def memorialize
|
||||||
authorize @account, :memorialize?
|
authorize @account, :memorialize?
|
||||||
@account.memorialize!
|
@account.memorialize!
|
||||||
log_action :memorialize, @account
|
log_action :memorialize, @account
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable
|
def enable
|
||||||
authorize @account.user, :enable?
|
authorize @account.user, :enable?
|
||||||
@account.user.enable!
|
@account.user.enable!
|
||||||
log_action :enable, @account.user
|
log_action :enable, @account.user
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def approve
|
def approve
|
||||||
authorize @account.user, :approve?
|
authorize @account.user, :approve?
|
||||||
@account.user.approve!
|
@account.user.approve!
|
||||||
redirect_to admin_pending_accounts_path
|
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||||
redirect_to admin_pending_accounts_path
|
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @account, :destroy?
|
||||||
|
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||||
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsilence
|
def unsilence
|
||||||
authorize @account, :unsilence?
|
authorize @account, :unsilence?
|
||||||
@account.unsilence!
|
@account.unsilence!
|
||||||
log_action :unsilence, @account
|
log_action :unsilence, @account
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsuspend
|
def unsuspend
|
||||||
authorize @account, :unsuspend?
|
authorize @account, :unsuspend?
|
||||||
@account.unsuspend!
|
@account.unsuspend!
|
||||||
|
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||||
log_action :unsuspend, @account
|
log_action :unsuspend, @account
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redownload
|
def redownload
|
||||||
@ -65,7 +74,7 @@ module Admin
|
|||||||
@account.update!(last_webfingered_at: nil)
|
@account.update!(last_webfingered_at: nil)
|
||||||
ResolveAccountService.new.call(@account)
|
ResolveAccountService.new.call(@account)
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_avatar
|
def remove_avatar
|
||||||
@ -76,7 +85,7 @@ module Admin
|
|||||||
|
|
||||||
log_action :remove_avatar, @account.user
|
log_action :remove_avatar, @account.user
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_header
|
def remove_header
|
||||||
@ -87,7 +96,7 @@ module Admin
|
|||||||
|
|
||||||
log_action :remove_header, @account.user
|
log_action :remove_header, @account.user
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -27,7 +27,7 @@ module Admin
|
|||||||
ips = []
|
ips = []
|
||||||
|
|
||||||
Resolv::DNS.open do |dns|
|
Resolv::DNS.open do |dns|
|
||||||
dns.timeouts = 1
|
dns.timeouts = 5
|
||||||
|
|
||||||
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
|
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ class Api::BaseController < ApplicationController
|
|||||||
|
|
||||||
def limit_param(default_limit)
|
def limit_param(default_limit)
|
||||||
return default_limit unless params[:limit]
|
return default_limit unless params[:limit]
|
||||||
|
|
||||||
[params[:limit].to_i.abs, default_limit * 2].min
|
[params[:limit].to_i.abs, default_limit * 2].min
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -95,12 +96,12 @@ class Api::BaseController < ApplicationController
|
|||||||
def require_user!
|
def require_user!
|
||||||
if !current_user
|
if !current_user
|
||||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||||
elsif current_user.disabled?
|
|
||||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
|
||||||
elsif !current_user.confirmed?
|
elsif !current_user.confirmed?
|
||||||
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
||||||
elsif !current_user.approved?
|
elsif !current_user.approved?
|
||||||
render json: { error: 'Your login is currently pending approval' }, status: 403
|
render json: { error: 'Your login is currently pending approval' }, status: 403
|
||||||
|
elsif !current_user.functional?
|
||||||
|
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||||
else
|
else
|
||||||
set_user_activity
|
set_user_activity
|
||||||
end
|
end
|
||||||
|
22
app/controllers/api/v1/accounts/featured_tags_controller.rb
Normal file
22
app/controllers/api/v1/accounts/featured_tags_controller.rb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
|
||||||
|
before_action :set_account
|
||||||
|
before_action :set_featured_tags
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @featured_tags, each_serializer: REST::AccountFeaturedTagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_featured_tags
|
||||||
|
@featured_tags = @account.suspended? ? @account.featured_tags : []
|
||||||
|
end
|
||||||
|
end
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hide_results?
|
def hide_results?
|
||||||
(@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
@account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hide_results?
|
def hide_results?
|
||||||
(@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
@account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@proofs = @account.identity_proofs.active
|
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
||||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@lists = @account.lists.where(account: current_account)
|
@lists = @account.suspended? ? [] : @account.lists.where(account: current_account)
|
||||||
render json: @lists, each_serializer: REST::ListSerializer
|
render json: @lists, each_serializer: REST::ListSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
accounts = Account.where(id: account_ids).select('id')
|
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||||
# .where doesn't guarantee that our results are in the same order
|
# .where doesn't guarantee that our results are in the same order
|
||||||
# we requested them, so return the "right" order to the requestor.
|
# we requested them, so return the "right" order to the requestor.
|
||||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||||
|
@ -18,14 +18,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_account_statuses
|
@account.suspended? ? [] : cached_account_statuses
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_account_statuses
|
def cached_account_statuses
|
||||||
cache_collection account_statuses, Status
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_statuses
|
|
||||||
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
|
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
|
||||||
|
|
||||||
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
||||||
@ -33,7 +29,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
||||||
statuses.merge!(hashtag_scope) if params[:tagged].present?
|
statuses.merge!(hashtag_scope) if params[:tagged].present?
|
||||||
|
|
||||||
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
cache_collection_paginated_by_id(
|
||||||
|
statuses,
|
||||||
|
Status,
|
||||||
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_account_statuses
|
def permitted_account_statuses
|
||||||
@ -41,17 +42,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def only_media_scope
|
def only_media_scope
|
||||||
Status.where(id: account_media_status_ids)
|
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
|
||||||
end
|
|
||||||
|
|
||||||
def account_media_status_ids
|
|
||||||
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
|
|
||||||
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
|
|
||||||
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
|
|
||||||
# and the table will be joined by `Merge Semi Join`, so the query will be slow.
|
|
||||||
@account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
|
|
||||||
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
|
||||||
.reorder(id: :desc).distinct(:id).pluck(:id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def pinned_scope
|
def pinned_scope
|
||||||
|
@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
before_action :require_user!, except: [:show, :create]
|
before_action :require_user!, except: [:show, :create]
|
||||||
before_action :set_account, except: [:create]
|
before_action :set_account, except: [:create]
|
||||||
before_action :check_account_suspension, only: [:show]
|
|
||||||
before_action :check_enabled_registrations, only: [:create]
|
before_action :check_enabled_registrations, only: [:create]
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!, only: :create
|
skip_before_action :require_authenticated_user!, only: :create
|
||||||
@ -31,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
|
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
|
||||||
|
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
|
||||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
|
||||||
|
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||||
end
|
end
|
||||||
@ -73,10 +71,6 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
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
|
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
||||||
end
|
end
|
||||||
|
@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @account, :destroy?
|
||||||
|
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
def unsuspend
|
def unsuspend
|
||||||
authorize @account, :unsuspend?
|
authorize @account, :unsuspend?
|
||||||
@account.unsuspend!
|
@account.unsuspend!
|
||||||
|
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||||
log_action :unsuspend, @account
|
log_action :unsuspend, @account
|
||||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
end
|
end
|
||||||
@ -79,7 +86,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
@ -63,7 +63,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_reports
|
def set_reports
|
||||||
@reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@reports = filtered_reports.order(id: :desc).with_accounts.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_report
|
def set_report
|
||||||
|
@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController
|
|||||||
|
|
||||||
def paginated_blocks
|
def paginated_blocks
|
||||||
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
|
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
|
||||||
|
.joins(:target_account)
|
||||||
|
.merge(Account.without_suspended)
|
||||||
.where(account: current_account)
|
.where(account: current_account)
|
||||||
.paginate_by_max_id(
|
.paginate_by_max_id(
|
||||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
|
@ -17,14 +17,11 @@ class Api::V1::BookmarksController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cached_bookmarks
|
def cached_bookmarks
|
||||||
cache_collection(
|
cache_collection(results.map(&:status), Status)
|
||||||
Status.reorder(nil).joins(:bookmarks).merge(results),
|
|
||||||
Status
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@_results ||= account_bookmarks.paginate_by_id(
|
@_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
@ -32,7 +32,7 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||||||
|
|
||||||
def paginated_conversations
|
def paginated_conversations
|
||||||
AccountConversation.where(account: current_account)
|
AccountConversation.where(account: current_account)
|
||||||
.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
@ -26,7 +26,7 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_encrypted_messages
|
def set_encrypted_messages
|
||||||
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def endorsed_accounts
|
def endorsed_accounts
|
||||||
current_account.endorsed_accounts.includes(:account_stat)
|
current_account.endorsed_accounts.includes(:account_stat).without_suspended
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
@ -17,14 +17,11 @@ class Api::V1::FavouritesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cached_favourites
|
def cached_favourites
|
||||||
cache_collection(
|
cache_collection(results.map(&:status), Status)
|
||||||
Status.reorder(nil).joins(:favourites).merge(results),
|
|
||||||
Status
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@_results ||= account_favourites.paginate_by_id(
|
@_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_most_used_tags, only: :index
|
before_action :set_recently_used_tags, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @most_used_tags, each_serializer: REST::TagSerializer
|
render json: @recently_used_tags, each_serializer: REST::TagSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_most_used_tags
|
def set_recently_used_tags
|
||||||
@most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||||||
|
|
||||||
def authorize
|
def authorize
|
||||||
AuthorizeFollowService.new.call(account, current_account)
|
AuthorizeFollowService.new.call(account, current_account)
|
||||||
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
|
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
|
||||||
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account.includes(:follow_requests, :account_stat).references(:follow_requests)
|
Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests)
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_follow_requests
|
def paginated_follow_requests
|
||||||
|
@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def load_accounts
|
def load_accounts
|
||||||
if unlimited?
|
if unlimited?
|
||||||
@list.accounts.includes(:account_stat).all
|
@list.accounts.without_suspended.includes(:account_stat).all
|
||||||
else
|
else
|
||||||
@list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
@list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -38,6 +38,6 @@ class Api::V1::ListsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:title)
|
params.permit(:title, :replies_policy)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -18,6 +18,8 @@ class Api::V1::MutesController < Api::BaseController
|
|||||||
|
|
||||||
def paginated_mutes
|
def paginated_mutes
|
||||||
@paginated_mutes ||= Mute.eager_load(:target_account)
|
@paginated_mutes ||= Mute.eager_load(:target_account)
|
||||||
|
.joins(:target_account)
|
||||||
|
.merge(Account.without_suspended)
|
||||||
.where(account: current_account)
|
.where(account: current_account)
|
||||||
.paginate_by_max_id(
|
.paginate_by_max_id(
|
||||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
|
@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.find(params[:id])
|
@notification = current_account.notifications.without_suspended.find(params[:id])
|
||||||
render json: @notification, serializer: REST::NotificationSerializer
|
render json: @notification, serializer: REST::NotificationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -31,18 +31,16 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def load_notifications
|
def load_notifications
|
||||||
cache_collection paginated_notifications, Notification
|
cache_collection_paginated_by_id(
|
||||||
end
|
browserable_account_notifications,
|
||||||
|
Notification,
|
||||||
def paginated_notifications
|
|
||||||
browserable_account_notifications.paginate_by_id(
|
|
||||||
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
current_account.notifications.browserable(exclude_types, from_account)
|
current_account.notifications.without_suspended.browserable(exclude_types, from_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def target_statuses_from_notifications
|
def target_statuses_from_notifications
|
||||||
|
@ -32,7 +32,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_statuses
|
def set_statuses
|
||||||
@statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@statuses = current_account.scheduled_statuses.to_a_paginated_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_status
|
def set_status
|
||||||
|
@ -5,7 +5,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
|
|||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
|
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_status
|
before_action :set_status, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
|
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
|
||||||
@ -13,10 +13,20 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
bookmark = current_account.bookmarks.find_by(status: @status)
|
bookmark = current_account.bookmarks.find_by(status_id: params[:status_id])
|
||||||
|
|
||||||
|
if bookmark
|
||||||
|
@status = bookmark.status
|
||||||
|
else
|
||||||
|
@status = Status.find(params[:status_id])
|
||||||
|
authorize @status, :show?
|
||||||
|
end
|
||||||
|
|
||||||
bookmark&.destroy!
|
bookmark&.destroy!
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
|||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account
|
Account
|
||||||
|
.without_suspended
|
||||||
.includes(:favourites, :account_stat)
|
.includes(:favourites, :account_stat)
|
||||||
.references(:favourites)
|
.references(:favourites)
|
||||||
.where(favourites: { status_id: @status.id })
|
.where(favourites: { status_id: @status.id })
|
||||||
|
@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account.includes(:statuses, :account_stat).references(:statuses)
|
Account.without_suspended.includes(:statuses, :account_stat).references(:statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_statuses
|
def paginated_statuses
|
||||||
|
@ -16,30 +16,29 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_public_statuses
|
cached_public_statuses_page
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_public_statuses
|
def cached_public_statuses_page
|
||||||
cache_collection public_statuses, Status
|
cache_collection(public_statuses, Status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_statuses
|
def public_statuses
|
||||||
statuses = public_timeline_statuses.paginate_by_id(
|
public_feed.get(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params[:max_id],
|
||||||
|
params[:since_id],
|
||||||
|
params[:min_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
if truthy_param?(:only_media)
|
|
||||||
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
|
|
||||||
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
|
|
||||||
statuses.where(id: status_ids)
|
|
||||||
else
|
|
||||||
statuses
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_timeline_statuses
|
def public_feed
|
||||||
Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
|
PublicFeed.new(
|
||||||
|
current_account,
|
||||||
|
local: truthy_param?(:local),
|
||||||
|
remote: truthy_param?(:remote),
|
||||||
|
only_media: truthy_param?(:only_media)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
@ -20,30 +20,29 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cached_tagged_statuses
|
def cached_tagged_statuses
|
||||||
cache_collection tagged_statuses, Status
|
@tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status)
|
||||||
end
|
|
||||||
|
|
||||||
def tagged_statuses
|
|
||||||
if @tag.nil?
|
|
||||||
[]
|
|
||||||
else
|
|
||||||
statuses = tag_timeline_statuses.paginate_by_id(
|
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if truthy_param?(:only_media)
|
|
||||||
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
|
|
||||||
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
|
|
||||||
statuses.where(id: status_ids)
|
|
||||||
else
|
|
||||||
statuses
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_timeline_statuses
|
def tag_timeline_statuses
|
||||||
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
|
tag_feed.get(
|
||||||
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
|
params[:max_id],
|
||||||
|
params[:since_id],
|
||||||
|
params[:min_id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_feed
|
||||||
|
TagFeed.new(
|
||||||
|
@tag,
|
||||||
|
current_account,
|
||||||
|
any: params[:any],
|
||||||
|
all: params[:all],
|
||||||
|
none: params[:none],
|
||||||
|
local: truthy_param?(:local),
|
||||||
|
remote: truthy_param?(:remote),
|
||||||
|
only_media: truthy_param?(:only_media)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
@ -37,6 +37,22 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
store_location_for(:user, tmp_stored_location) if continue_after?
|
store_location_for(:user, tmp_stored_location) if continue_after?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def webauthn_options
|
||||||
|
user = find_user
|
||||||
|
|
||||||
|
if user.webauthn_enabled?
|
||||||
|
options_for_get = WebAuthn::Credential.options_for_get(
|
||||||
|
allow: user.webauthn_credentials.pluck(:external_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
session[:webauthn_challenge] = options_for_get.challenge
|
||||||
|
|
||||||
|
render json: options_for_get, status: :ok
|
||||||
|
else
|
||||||
|
render json: { error: t('webauthn_credentials.not_enabled') }, status: :unauthorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def find_user
|
def find_user
|
||||||
@ -51,7 +67,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
|
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_in_path_for(resource)
|
def after_sign_in_path_for(resource)
|
||||||
|
@ -47,4 +47,8 @@ module CacheConcern
|
|||||||
|
|
||||||
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
|
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
||||||
|
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,7 +32,6 @@ module ChallengableConcern
|
|||||||
if params.key?(:form_challenge)
|
if params.key?(:form_challenge)
|
||||||
if challenge_passed?
|
if challenge_passed?
|
||||||
session[:challenge_passed_at] = Time.now.utc
|
session[:challenge_passed_at] = Time.now.utc
|
||||||
return
|
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('challenge.invalid_password')
|
flash.now[:alert] = I18n.t('challenge.invalid_password')
|
||||||
render_challenge
|
render_challenge
|
||||||
|
@ -5,7 +5,6 @@ module ExportControllerConcern
|
|||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :require_not_suspended!
|
|
||||||
before_action :load_export
|
before_action :load_export
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
@ -30,8 +29,4 @@ module ExportControllerConcern
|
|||||||
def export_filename
|
def export_filename
|
||||||
"#{controller_name}.csv"
|
"#{controller_name}.csv"
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -7,6 +7,44 @@ module SignatureVerification
|
|||||||
|
|
||||||
include DomainControlHelper
|
include DomainControlHelper
|
||||||
|
|
||||||
|
EXPIRATION_WINDOW_LIMIT = 12.hours
|
||||||
|
CLOCK_SKEW_MARGIN = 1.hour
|
||||||
|
|
||||||
|
class SignatureVerificationError < StandardError; end
|
||||||
|
|
||||||
|
class SignatureParamsParser < Parslet::Parser
|
||||||
|
rule(:token) { match("[0-9a-zA-Z!#$%&'*+.^_`|~-]").repeat(1).as(:token) }
|
||||||
|
rule(:quoted_string) { str('"') >> (qdtext | quoted_pair).repeat.as(:quoted_string) >> str('"') }
|
||||||
|
# qdtext and quoted_pair are not exactly according to spec but meh
|
||||||
|
rule(:qdtext) { match('[^\\\\"]') }
|
||||||
|
rule(:quoted_pair) { str('\\') >> any }
|
||||||
|
rule(:bws) { match('\s').repeat }
|
||||||
|
rule(:param) { (token.as(:key) >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:param) }
|
||||||
|
rule(:comma) { bws >> str(',') >> bws }
|
||||||
|
# Old versions of node-http-signature add an incorrect "Signature " prefix to the header
|
||||||
|
rule(:buggy_prefix) { str('Signature ') }
|
||||||
|
rule(:params) { buggy_prefix.maybe >> (param >> (comma >> param).repeat).as(:params) }
|
||||||
|
root(:params)
|
||||||
|
end
|
||||||
|
|
||||||
|
class SignatureParamsTransformer < Parslet::Transform
|
||||||
|
rule(params: subtree(:p)) do
|
||||||
|
(p.is_a?(Array) ? p : [p]).each_with_object({}) { |(key, val), h| h[key] = val }
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(param: { key: simple(:key), value: simple(:val) }) do
|
||||||
|
[key, val]
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(quoted_string: simple(:string)) do
|
||||||
|
string.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(token: simple(:string)) do
|
||||||
|
string.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def require_signature!
|
def require_signature!
|
||||||
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||||
end
|
end
|
||||||
@ -24,72 +62,40 @@ module SignatureVerification
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signature_key_id
|
def signature_key_id
|
||||||
raw_signature = request.headers['Signature']
|
|
||||||
signature_params = {}
|
|
||||||
|
|
||||||
raw_signature.split(',').each do |part|
|
|
||||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
|
||||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
|
||||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
|
||||||
end
|
|
||||||
|
|
||||||
signature_params['keyId']
|
signature_params['keyId']
|
||||||
|
rescue SignatureVerificationError
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_request_account
|
def signed_request_account
|
||||||
return @signed_request_account if defined?(@signed_request_account)
|
return @signed_request_account if defined?(@signed_request_account)
|
||||||
|
|
||||||
unless signed_request?
|
raise SignatureVerificationError, 'Request not signed' unless signed_request?
|
||||||
@signature_verification_failure_reason = 'Request not signed'
|
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
|
||||||
@signed_request_account = nil
|
raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
|
||||||
return
|
raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
|
||||||
end
|
|
||||||
|
|
||||||
if request.headers['Date'].present? && !matches_time_window?
|
verify_signature_strength!
|
||||||
@signature_verification_failure_reason = 'Signed request date outside acceptable time window'
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
raw_signature = request.headers['Signature']
|
|
||||||
signature_params = {}
|
|
||||||
|
|
||||||
raw_signature.split(',').each do |part|
|
|
||||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
|
||||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
|
||||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
|
||||||
end
|
|
||||||
|
|
||||||
if incompatible_signature?(signature_params)
|
|
||||||
@signature_verification_failure_reason = 'Incompatible request signature'
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
account = account_from_key_id(signature_params['keyId'])
|
account = account_from_key_id(signature_params['keyId'])
|
||||||
|
|
||||||
if account.nil?
|
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
signature = Base64.decode64(signature_params['signature'])
|
signature = Base64.decode64(signature_params['signature'])
|
||||||
compare_signed_string = build_signed_string(signature_params['headers'])
|
compare_signed_string = build_signed_string
|
||||||
|
|
||||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||||
|
|
||||||
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||||
|
|
||||||
if account.nil?
|
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||||
|
|
||||||
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
|
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
|
||||||
|
@signed_request_account = nil
|
||||||
|
rescue SignatureVerificationError => e
|
||||||
|
@signature_verification_failure_reason = e.message
|
||||||
@signed_request_account = nil
|
@signed_request_account = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -99,8 +105,33 @@ module SignatureVerification
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def signature_params
|
||||||
|
@signature_params ||= begin
|
||||||
|
raw_signature = request.headers['Signature']
|
||||||
|
tree = SignatureParamsParser.new.parse(raw_signature)
|
||||||
|
SignatureParamsTransformer.new.apply(tree)
|
||||||
|
end
|
||||||
|
rescue Parslet::ParseFailed
|
||||||
|
raise SignatureVerificationError, 'Error parsing signature parameters'
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature_algorithm
|
||||||
|
signature_params.fetch('algorithm', 'hs2019')
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_headers
|
||||||
|
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_signature_strength!
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed' unless signed_headers.include?('host')
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
||||||
|
end
|
||||||
|
|
||||||
def verify_signature(account, signature, compare_signed_string)
|
def verify_signature(account, signature, compare_signed_string)
|
||||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
|
||||||
@signed_request_account = account
|
@signed_request_account = account
|
||||||
@signed_request_account
|
@signed_request_account
|
||||||
end
|
end
|
||||||
@ -108,12 +139,20 @@ module SignatureVerification
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_signed_string(signed_headers)
|
def build_signed_string
|
||||||
signed_headers = 'date' if signed_headers.blank?
|
signed_headers.map do |signed_header|
|
||||||
|
|
||||||
signed_headers.downcase.split(' ').map do |signed_header|
|
|
||||||
if signed_header == Request::REQUEST_TARGET
|
if signed_header == Request::REQUEST_TARGET
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
|
elsif signed_header == '(created)'
|
||||||
|
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
|
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||||
|
|
||||||
|
"(created): #{signature_params['created']}"
|
||||||
|
elsif signed_header == '(expires)'
|
||||||
|
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
|
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
|
||||||
|
|
||||||
|
"(expires): #{signature_params['expires']}"
|
||||||
elsif signed_header == 'digest'
|
elsif signed_header == 'digest'
|
||||||
"digest: #{body_digest}"
|
"digest: #{body_digest}"
|
||||||
else
|
else
|
||||||
@ -123,13 +162,28 @@ module SignatureVerification
|
|||||||
end
|
end
|
||||||
|
|
||||||
def matches_time_window?
|
def matches_time_window?
|
||||||
|
created_time = nil
|
||||||
|
expires_time = nil
|
||||||
|
|
||||||
begin
|
begin
|
||||||
time_sent = Time.httpdate(request.headers['Date'])
|
if signature_algorithm == 'hs2019' && signature_params['created'].present?
|
||||||
|
created_time = Time.at(signature_params['created'].to_i).utc
|
||||||
|
elsif request.headers['Date'].present?
|
||||||
|
created_time = Time.httpdate(request.headers['Date']).utc
|
||||||
|
end
|
||||||
|
|
||||||
|
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
(Time.now.utc - time_sent).abs <= 12.hours
|
expires_time ||= created_time + 5.minutes unless created_time.nil?
|
||||||
|
expires_time = [expires_time, created_time + EXPIRATION_WINDOW_LIMIT].min unless created_time.nil?
|
||||||
|
|
||||||
|
return false if created_time.present? && created_time > Time.now.utc + CLOCK_SKEW_MARGIN
|
||||||
|
return false if expires_time.present? && Time.now.utc > expires_time + CLOCK_SKEW_MARGIN
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def body_digest
|
def body_digest
|
||||||
@ -140,9 +194,8 @@ module SignatureVerification
|
|||||||
name.split(/-/).map(&:capitalize).join('-')
|
name.split(/-/).map(&:capitalize).join('-')
|
||||||
end
|
end
|
||||||
|
|
||||||
def incompatible_signature?(signature_params)
|
def missing_required_signature_parameters?
|
||||||
signature_params['keyId'].blank? ||
|
signature_params['keyId'].blank? || signature_params['signature'].blank?
|
||||||
signature_params['signature'].blank?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_from_key_id(key_id)
|
def account_from_key_id(key_id)
|
||||||
|
@ -8,7 +8,23 @@ module TwoFactorAuthenticationConcern
|
|||||||
end
|
end
|
||||||
|
|
||||||
def two_factor_enabled?
|
def two_factor_enabled?
|
||||||
find_user&.otp_required_for_login?
|
find_user&.two_factor_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_webauthn_credential?(user, webauthn_credential)
|
||||||
|
user_credential = user.webauthn_credentials.find_by!(external_id: webauthn_credential.id)
|
||||||
|
|
||||||
|
begin
|
||||||
|
webauthn_credential.verify(
|
||||||
|
session[:webauthn_challenge],
|
||||||
|
public_key: user_credential.public_key,
|
||||||
|
sign_count: user_credential.sign_count
|
||||||
|
)
|
||||||
|
|
||||||
|
user_credential.update!(sign_count: webauthn_credential.sign_count)
|
||||||
|
rescue WebAuthn::Error
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_otp_attempt?(user)
|
def valid_otp_attempt?(user)
|
||||||
@ -21,14 +37,29 @@ module TwoFactorAuthenticationConcern
|
|||||||
def authenticate_with_two_factor
|
def authenticate_with_two_factor
|
||||||
user = self.resource = find_user
|
user = self.resource = find_user
|
||||||
|
|
||||||
if user_params[:otp_attempt].present? && session[:attempt_user_id]
|
if user.webauthn_enabled? && user_params[:credential].present? && session[:attempt_user_id]
|
||||||
authenticate_with_two_factor_attempt(user)
|
authenticate_with_two_factor_via_webauthn(user)
|
||||||
|
elsif user_params[:otp_attempt].present? && session[:attempt_user_id]
|
||||||
|
authenticate_with_two_factor_via_otp(user)
|
||||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||||
prompt_for_two_factor(user)
|
prompt_for_two_factor(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_two_factor_attempt(user)
|
def authenticate_with_two_factor_via_webauthn(user)
|
||||||
|
webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential])
|
||||||
|
|
||||||
|
if valid_webauthn_credential?(user, webauthn_credential)
|
||||||
|
session.delete(:attempt_user_id)
|
||||||
|
remember_me(user)
|
||||||
|
sign_in(user)
|
||||||
|
render json: { redirect_path: root_path }, status: :ok
|
||||||
|
else
|
||||||
|
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_two_factor_via_otp(user)
|
||||||
if valid_otp_attempt?(user)
|
if valid_otp_attempt?(user)
|
||||||
session.delete(:attempt_user_id)
|
session.delete(:attempt_user_id)
|
||||||
remember_me(user)
|
remember_me(user)
|
||||||
@ -43,6 +74,12 @@ module TwoFactorAuthenticationConcern
|
|||||||
set_locale do
|
set_locale do
|
||||||
session[:attempt_user_id] = user.id
|
session[:attempt_user_id] = user.id
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
|
@webauthn_enabled = user.webauthn_enabled?
|
||||||
|
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
||||||
|
'webauthn'
|
||||||
|
else
|
||||||
|
'totp'
|
||||||
|
end
|
||||||
render :two_factor
|
render :two_factor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -17,6 +17,6 @@ class InstanceActorsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def restrict_fields_to
|
def restrict_fields_to
|
||||||
%i(id type preferred_username inbox public_key endpoints url manually_approves_followers)
|
%i(id type preferred_username inbox outbox public_key endpoints url manually_approves_followers)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
|
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
|
before_action :require_not_suspended!, only: :destroy
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
@ -25,4 +26,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
def store_current_location
|
def store_current_location
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::AliasesController < Settings::BaseController
|
class Settings::AliasesController < Settings::BaseController
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :require_not_suspended!
|
||||||
before_action :set_aliases, except: :destroy
|
before_action :set_aliases, except: :destroy
|
||||||
before_action :set_alias, only: :destroy
|
before_action :set_alias, only: :destroy
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::ApplicationsController < Settings::BaseController
|
class Settings::ApplicationsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
||||||
before_action :prepare_scopes, only: [:create, :update]
|
before_action :prepare_scopes, only: [:create, :update]
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::BaseController < ApplicationController
|
class Settings::BaseController < ApplicationController
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
@ -13,4 +16,8 @@ class Settings::BaseController < ApplicationController
|
|||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::DeletesController < Settings::BaseController
|
class Settings::DeletesController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :check_enabled_deletion
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
before_action :check_enabled_deletion
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@confirmation = Form::DeleteConfirmation.new
|
@confirmation = Form::DeleteConfirmation.new
|
||||||
end
|
end
|
||||||
@ -46,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController
|
|||||||
|
|
||||||
def destroy_account!
|
def destroy_account!
|
||||||
current_account.suspend!
|
current_account.suspend!
|
||||||
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
|
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||||
sign_out
|
sign_out
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class BlockedAccountsController < ApplicationController
|
class BlockedAccountsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class BlockedDomainsController < ApplicationController
|
class BlockedDomainsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class FollowingAccountsController < ApplicationController
|
class FollowingAccountsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class ListsController < ApplicationController
|
class ListsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class MutedAccountsController < ApplicationController
|
class MutedAccountsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -3,11 +3,6 @@
|
|||||||
class Settings::ExportsController < Settings::BaseController
|
class Settings::ExportsController < Settings::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@ -16,8 +11,6 @@ class Settings::ExportsController < Settings::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
raise Mastodon::NotPermittedError unless user_signed_in?
|
|
||||||
|
|
||||||
backup = nil
|
backup = nil
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
@ -37,8 +30,4 @@ class Settings::ExportsController < Settings::BaseController
|
|||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::FeaturedTagsController < Settings::BaseController
|
class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_featured_tags, only: :index
|
before_action :set_featured_tags, only: :index
|
||||||
before_action :set_featured_tag, except: [:index, :create]
|
before_action :set_featured_tag, except: [:index, :create]
|
||||||
before_action :set_most_used_tags, only: :index
|
before_action :set_recently_used_tags, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@featured_tag = FeaturedTag.new
|
@featured_tag = FeaturedTag.new
|
||||||
@ -20,7 +17,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||||||
redirect_to settings_featured_tags_path
|
redirect_to settings_featured_tags_path
|
||||||
else
|
else
|
||||||
set_featured_tags
|
set_featured_tags
|
||||||
set_most_used_tags
|
set_recently_used_tags
|
||||||
|
|
||||||
render :index
|
render :index
|
||||||
end
|
end
|
||||||
@ -41,8 +38,8 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||||||
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
|
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_most_used_tags
|
def set_recently_used_tags
|
||||||
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||||
end
|
end
|
||||||
|
|
||||||
def featured_tag_params
|
def featured_tag_params
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::IdentityProofsController < Settings::BaseController
|
class Settings::IdentityProofsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :check_required_params, only: :new
|
before_action :check_required_params, only: :new
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::ImportsController < Settings::BaseController
|
class Settings::ImportsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::Migration::RedirectsController < Settings::BaseController
|
class Settings::Migration::RedirectsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@redirect = Form::Redirect.new
|
@redirect = Form::Redirect.new
|
||||||
end
|
end
|
||||||
@ -38,8 +35,4 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||||||
def resource_params
|
def resource_params
|
||||||
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
|
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::MigrationsController < Settings::BaseController
|
class Settings::MigrationsController < Settings::BaseController
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
before_action :require_not_suspended!
|
||||||
before_action :set_migrations
|
before_action :set_migrations
|
||||||
before_action :set_cooldown
|
before_action :set_cooldown
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@migration = current_account.migrations.build
|
@migration = current_account.migrations.build
|
||||||
end
|
end
|
||||||
@ -44,8 +41,4 @@ class Settings::MigrationsController < Settings::BaseController
|
|||||||
def on_cooldown?
|
def on_cooldown?
|
||||||
@cooldown.present?
|
@cooldown.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
class PicturesController < BaseController
|
class PicturesController < BaseController
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
before_action :set_picture
|
before_action :set_picture
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::PreferencesController < Settings::BaseController
|
class Settings::PreferencesController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::ProfilesController < Settings::BaseController
|
class Settings::ProfilesController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::SessionsController < Settings::BaseController
|
class Settings::SessionsController < Settings::BaseController
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_session, only: :destroy
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
before_action :set_session, only: :destroy
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@session.destroy!
|
@session.destroy!
|
||||||
flash[:notice] = I18n.t('sessions.revoke_success')
|
flash[:notice] = I18n.t('sessions.revoke_success')
|
||||||
|
@ -5,31 +5,31 @@ module Settings
|
|||||||
class ConfirmationsController < BaseController
|
class ConfirmationsController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_challenge!
|
before_action :require_challenge!
|
||||||
before_action :ensure_otp_secret
|
before_action :ensure_otp_secret
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def new
|
def new
|
||||||
prepare_two_factor_form
|
prepare_two_factor_form
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt])
|
if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt], otp_secret: session[:new_otp_secret])
|
||||||
flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success')
|
flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success')
|
||||||
|
|
||||||
current_user.otp_required_for_login = true
|
current_user.otp_required_for_login = true
|
||||||
|
current_user.otp_secret = session[:new_otp_secret]
|
||||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
||||||
UserMailer.two_factor_enabled(current_user).deliver_later!
|
UserMailer.two_factor_enabled(current_user).deliver_later!
|
||||||
|
|
||||||
|
session.delete(:new_otp_secret)
|
||||||
|
|
||||||
render 'settings/two_factor_authentication/recovery_codes/index'
|
render 'settings/two_factor_authentication/recovery_codes/index'
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
flash.now[:alert] = I18n.t('otp_authentication.wrong_code')
|
||||||
prepare_two_factor_form
|
prepare_two_factor_form
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
@ -43,12 +43,15 @@ module Settings
|
|||||||
|
|
||||||
def prepare_two_factor_form
|
def prepare_two_factor_form
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain)
|
@new_otp_secret = session[:new_otp_secret]
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email,
|
||||||
|
otp_secret: @new_otp_secret,
|
||||||
|
issuer: Rails.configuration.x.local_domain)
|
||||||
@qrcode = RQRCode::QRCode.new(@provision_url)
|
@qrcode = RQRCode::QRCode.new(@provision_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_otp_secret
|
def ensure_otp_secret
|
||||||
redirect_to settings_two_factor_authentication_path unless current_user.otp_secret
|
redirect_to settings_otp_authentication_path if session[:new_otp_secret].blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
module TwoFactorAuthentication
|
||||||
|
class OtpAuthenticationController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :verify_otp_not_enabled, only: [:show]
|
||||||
|
before_action :require_challenge!, only: [:create]
|
||||||
|
|
||||||
|
def show
|
||||||
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
session[:new_otp_secret] = User.generate_otp_secret(32)
|
||||||
|
|
||||||
|
redirect_to new_settings_two_factor_authentication_confirmation_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def confirmation_params
|
||||||
|
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_otp_not_enabled
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def acceptable_code?
|
||||||
|
current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) ||
|
||||||
|
current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,13 +5,10 @@ module Settings
|
|||||||
class RecoveryCodesController < BaseController
|
class RecoveryCodesController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_challenge!, on: :create
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_challenge!, on: :create
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
module TwoFactorAuthentication
|
||||||
|
class WebauthnCredentialsController < BaseController
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_otp_enabled
|
||||||
|
before_action :require_webauthn_enabled, only: [:index, :destroy]
|
||||||
|
|
||||||
|
def new; end
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def options
|
||||||
|
current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id
|
||||||
|
|
||||||
|
options_for_create = WebAuthn::Credential.options_for_create(
|
||||||
|
user: {
|
||||||
|
name: current_user.account.username,
|
||||||
|
display_name: current_user.account.username,
|
||||||
|
id: current_user.webauthn_id,
|
||||||
|
},
|
||||||
|
exclude: current_user.webauthn_credentials.pluck(:external_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
session[:webauthn_challenge] = options_for_create.challenge
|
||||||
|
|
||||||
|
render json: options_for_create, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
webauthn_credential = WebAuthn::Credential.from_create(params[:credential])
|
||||||
|
|
||||||
|
if webauthn_credential.verify(session[:webauthn_challenge])
|
||||||
|
user_credential = current_user.webauthn_credentials.build(
|
||||||
|
external_id: webauthn_credential.id,
|
||||||
|
public_key: webauthn_credential.public_key,
|
||||||
|
nickname: params[:nickname],
|
||||||
|
sign_count: webauthn_credential.sign_count
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_credential.save
|
||||||
|
flash[:success] = I18n.t('webauthn_credentials.create.success')
|
||||||
|
status = :ok
|
||||||
|
|
||||||
|
if current_user.webauthn_credentials.size == 1
|
||||||
|
UserMailer.webauthn_enabled(current_user).deliver_later!
|
||||||
|
else
|
||||||
|
UserMailer.webauthn_credential_added(current_user, user_credential).deliver_later!
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('webauthn_credentials.create.error')
|
||||||
|
status = :internal_server_error
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = t('webauthn_credentials.create.error')
|
||||||
|
status = :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { redirect_path: settings_two_factor_authentication_methods_path }, status: status
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
credential = current_user.webauthn_credentials.find_by(id: params[:id])
|
||||||
|
if credential
|
||||||
|
credential.destroy
|
||||||
|
if credential.destroyed?
|
||||||
|
flash[:success] = I18n.t('webauthn_credentials.destroy.success')
|
||||||
|
|
||||||
|
if current_user.webauthn_credentials.empty?
|
||||||
|
UserMailer.webauthn_disabled(current_user).deliver_later!
|
||||||
|
else
|
||||||
|
UserMailer.webauthn_credential_deleted(current_user, credential).deliver_later!
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('webauthn_credentials.destroy.error')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('webauthn_credentials.destroy.error')
|
||||||
|
end
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_otp_enabled
|
||||||
|
unless current_user.otp_enabled?
|
||||||
|
flash[:error] = t('webauthn_credentials.otp_required')
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_webauthn_enabled
|
||||||
|
unless current_user.webauthn_enabled?
|
||||||
|
flash[:error] = t('webauthn_credentials.not_enabled')
|
||||||
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
class TwoFactorAuthenticationMethodsController < BaseController
|
||||||
|
include ChallengableConcern
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_challenge!, only: :disable
|
||||||
|
before_action :require_otp_enabled
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def disable
|
||||||
|
current_user.disable_two_factor!
|
||||||
|
UserMailer.two_factor_disabled(current_user).deliver_later!
|
||||||
|
|
||||||
|
redirect_to settings_otp_authentication_path, flash: { notice: I18n.t('two_factor_authentication.disabled_success') }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_otp_enabled
|
||||||
|
redirect_to settings_otp_authentication_path unless current_user.otp_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,53 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Settings
|
|
||||||
class TwoFactorAuthenticationsController < BaseController
|
|
||||||
include ChallengableConcern
|
|
||||||
|
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :verify_otp_required, only: [:create]
|
|
||||||
before_action :require_challenge!, only: [:create]
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def show
|
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
current_user.otp_secret = User.generate_otp_secret(32)
|
|
||||||
current_user.save!
|
|
||||||
redirect_to new_settings_two_factor_authentication_confirmation_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
if acceptable_code?
|
|
||||||
current_user.otp_required_for_login = false
|
|
||||||
current_user.save!
|
|
||||||
UserMailer.two_factor_disabled(current_user).deliver_later!
|
|
||||||
redirect_to settings_two_factor_authentication_path
|
|
||||||
else
|
|
||||||
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
|
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
|
||||||
render :show
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def confirmation_params
|
|
||||||
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_otp_required
|
|
||||||
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
|
||||||
end
|
|
||||||
|
|
||||||
def acceptable_code?
|
|
||||||
current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) ||
|
|
||||||
current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -10,8 +10,9 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :authenticate_user!, if: :whitelist_mode?
|
before_action :authenticate_user!, if: :whitelist_mode?
|
||||||
before_action :set_tag
|
|
||||||
before_action :set_local
|
before_action :set_local
|
||||||
|
before_action :set_tag
|
||||||
|
before_action :set_statuses
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
@ -25,20 +26,11 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
expires_in 0, public: true
|
expires_in 0, public: true
|
||||||
|
|
||||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
|
||||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit)
|
|
||||||
@statuses = cache_collection(@statuses, Status)
|
|
||||||
|
|
||||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in 3.minutes, public: public_fetch_mode?
|
expires_in 3.minutes, public: public_fetch_mode?
|
||||||
|
|
||||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
|
||||||
@statuses = cache_collection(@statuses, Status)
|
|
||||||
|
|
||||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -54,6 +46,15 @@ class TagsController < ApplicationController
|
|||||||
@local = truthy_param?(:local)
|
@local = truthy_param?(:local)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_statuses
|
||||||
|
case request.format&.to_sym
|
||||||
|
when :json
|
||||||
|
@statuses = cache_collection(TagFeed.new(@tag, current_account, local: @local).get(PAGE_SIZE, params[:max_id], params[:since_id], params[:min_id]), Status)
|
||||||
|
when :rss
|
||||||
|
@statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'with-modals'
|
@body_classes = 'with-modals'
|
||||||
end
|
end
|
||||||
@ -62,16 +63,16 @@ class TagsController < ApplicationController
|
|||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def limit_param
|
||||||
|
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||||
|
end
|
||||||
|
|
||||||
def collection_presenter
|
def collection_presenter
|
||||||
ActivityPub::CollectionPresenter.new(
|
ActivityPub::CollectionPresenter.new(
|
||||||
id: tag_url(@tag, filter_params),
|
id: tag_url(@tag),
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
size: @tag.statuses.count,
|
size: @tag.statuses.count,
|
||||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
|
||||||
params.slice(:any, :all, :none).permit(:any, :all, :none)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -162,6 +162,8 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
|
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import openDB from '../storage/db';
|
import { 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';
|
||||||
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
||||||
@ -74,45 +73,13 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
|
|||||||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
||||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||||
|
|
||||||
function getFromDB(dispatch, getState, index, id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = index.get(id);
|
|
||||||
|
|
||||||
request.onerror = reject;
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
if (!request.result) {
|
|
||||||
reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(importAccount(request.result));
|
|
||||||
resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchRelationships([id]));
|
dispatch(fetchRelationships([id]));
|
||||||
|
|
||||||
if (getState().getIn(['accounts', id], null) !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(fetchAccountRequest(id));
|
dispatch(fetchAccountRequest(id));
|
||||||
|
|
||||||
openDB().then(db => getFromDB(
|
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
db.transaction('accounts', 'read').objectStore('accounts').index('id'),
|
|
||||||
id,
|
|
||||||
).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(() => {
|
|
||||||
dispatch(fetchAccountSuccess());
|
dispatch(fetchAccountSuccess());
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchAccountFail(id, error));
|
dispatch(fetchAccountFail(id, error));
|
||||||
@ -142,14 +109,14 @@ export function fetchAccountFail(id, error) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function followAccount(id, reblogs = true) {
|
export function followAccount(id, options = { reblogs: true }) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||||
|
|
||||||
dispatch(followAccountRequest(id, locked));
|
dispatch(followAccountRequest(id, locked));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(followAccountFail(error, locked));
|
dispatch(followAccountFail(error, locked));
|
||||||
|
@ -150,10 +150,10 @@ export const createListFail = error => ({
|
|||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
|
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
|
||||||
dispatch(updateListRequest(id));
|
dispatch(updateListRequest(id));
|
||||||
|
|
||||||
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
|
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
|
||||||
dispatch(updateListSuccess(data));
|
dispatch(updateListSuccess(data));
|
||||||
|
|
||||||
if (shouldReset) {
|
if (shouldReset) {
|
||||||
|
@ -57,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
|||||||
const _buildParams = (state) => {
|
const _buildParams = (state) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
|
||||||
const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
|
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
|
||||||
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
|
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
|
||||||
|
|
||||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||||
|
@ -59,7 +59,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (notification.type === 'mention') {
|
if (['mention', 'status'].includes(notification.type)) {
|
||||||
const dropRegex = filters[0];
|
const dropRegex = filters[0];
|
||||||
const regex = filters[1];
|
const regex = filters[1];
|
||||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
const searchIndex = searchTextFromRawStatus(notification.status);
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
import openDB from '../storage/db';
|
|
||||||
import { evictStatus } from '../storage/modifier';
|
|
||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer';
|
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||||
import { ensureComposeIsVisible } from './compose';
|
import { ensureComposeIsVisible } from './compose';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
@ -41,48 +39,6 @@ export function fetchStatusRequest(id, skipLoading) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFromDB(dispatch, getState, accountIndex, index, id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = index.get(id);
|
|
||||||
|
|
||||||
request.onerror = reject;
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
if (!request.result) {
|
|
||||||
reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(importStatus(request.result));
|
|
||||||
|
|
||||||
if (getState().getIn(['accounts', request.result.account], null) === null) {
|
|
||||||
promises.push(new Promise((accountResolve, accountReject) => {
|
|
||||||
const accountRequest = accountIndex.get(request.result.account);
|
|
||||||
|
|
||||||
accountRequest.onerror = accountReject;
|
|
||||||
accountRequest.onsuccess = () => {
|
|
||||||
if (!request.result) {
|
|
||||||
accountReject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(importAccount(accountRequest.result));
|
|
||||||
accountResolve();
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) {
|
|
||||||
promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog));
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(Promise.all(promises));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchStatus(id) {
|
export function fetchStatus(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
||||||
@ -95,23 +51,10 @@ export function fetchStatus(id) {
|
|||||||
|
|
||||||
dispatch(fetchStatusRequest(id, skipLoading));
|
dispatch(fetchStatusRequest(id, skipLoading));
|
||||||
|
|
||||||
openDB().then(db => {
|
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
||||||
const transaction = db.transaction(['accounts', 'statuses'], 'read');
|
|
||||||
const accountIndex = transaction.objectStore('accounts').index('id');
|
|
||||||
const index = transaction.objectStore('statuses').index('id');
|
|
||||||
|
|
||||||
return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
|
|
||||||
db.close();
|
|
||||||
}, error => {
|
|
||||||
db.close();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
dispatch(fetchStatusSuccess(skipLoading));
|
|
||||||
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
})).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -153,7 +96,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||||
evictStatus(id);
|
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
dispatch(importFetchedAccount(response.data.account));
|
dispatch(importFetchedAccount(response.data.account));
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
import { connectStream } from '../stream';
|
import { connectStream } from '../stream';
|
||||||
import {
|
import {
|
||||||
updateTimeline,
|
updateTimeline,
|
||||||
@ -19,24 +21,59 @@ import { getLocale } from '../locales';
|
|||||||
|
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
|
||||||
export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) {
|
/**
|
||||||
|
* @param {number} max
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
const randomUpTo = max =>
|
||||||
|
Math.floor(Math.random() * Math.floor(max));
|
||||||
|
|
||||||
return connectStream (path, pollingRefresh, (dispatch, getState) => {
|
/**
|
||||||
|
* @param {string} timelineId
|
||||||
|
* @param {string} channelName
|
||||||
|
* @param {Object.<string, string>} params
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {function(Function, Function): void} [options.fallback]
|
||||||
|
* @param {function(object): boolean} [options.accept]
|
||||||
|
* @return {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) =>
|
||||||
|
connectStream(channelName, params, (dispatch, getState) => {
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
|
|
||||||
|
let pollingId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {function(Function, Function): void} fallback
|
||||||
|
*/
|
||||||
|
const useFallback = fallback => {
|
||||||
|
fallback(dispatch, () => {
|
||||||
|
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onConnect() {
|
onConnect() {
|
||||||
dispatch(connectTimeline(timelineId));
|
dispatch(connectTimeline(timelineId));
|
||||||
|
|
||||||
|
if (pollingId) {
|
||||||
|
clearTimeout(pollingId);
|
||||||
|
pollingId = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onDisconnect() {
|
onDisconnect() {
|
||||||
dispatch(disconnectTimeline(timelineId));
|
dispatch(disconnectTimeline(timelineId));
|
||||||
|
|
||||||
|
if (options.fallback) {
|
||||||
|
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onReceive (data) {
|
onReceive (data) {
|
||||||
switch(data.event) {
|
switch(data.event) {
|
||||||
case 'update':
|
case 'update':
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
|
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
@ -63,17 +100,59 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Function} dispatch
|
||||||
|
* @param {function(): void} done
|
||||||
|
*/
|
||||||
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||||
dispatch(expandHomeTimeline({}, () =>
|
dispatch(expandHomeTimeline({}, () =>
|
||||||
dispatch(expandNotifications({}, () =>
|
dispatch(expandNotifications({}, () =>
|
||||||
dispatch(fetchAnnouncements(done))))));
|
dispatch(fetchAnnouncements(done))))));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
/**
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
* @return {function(): void}
|
||||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
|
*/
|
||||||
export const connectHashtagStream = (id, tag, local, accept) => connectTimelineStream(`hashtag:${id}${local ? ':local' : ''}`, `hashtag${local ? ':local' : ''}&tag=${tag}`, null, accept);
|
export const connectUserStream = () =>
|
||||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification });
|
||||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} [options.onlyMedia]
|
||||||
|
* @return {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} [options.onlyMedia]
|
||||||
|
* @param {boolean} [options.onlyRemote]
|
||||||
|
* @return {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
||||||
|
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} columnId
|
||||||
|
* @param {string} tagName
|
||||||
|
* @param {boolean} onlyLocal
|
||||||
|
* @param {function(object): boolean} accept
|
||||||
|
* @return {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) =>
|
||||||
|
connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectDirectStream = () =>
|
||||||
|
connectTimelineStream('direct', 'direct');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} listId
|
||||||
|
* @return {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectListStream = listId =>
|
||||||
|
connectTimelineStream(`list:${listId}`, 'list', { list: listId });
|
||||||
|
@ -205,7 +205,7 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
if (this.activeElement) {
|
if (this.activeElement) {
|
||||||
this.activeElement.focus();
|
this.activeElement.focus({ preventScroll: true });
|
||||||
this.activeElement = null;
|
this.activeElement = null;
|
||||||
}
|
}
|
||||||
this.props.onClose(this.state.id);
|
this.props.onClose(this.state.id);
|
||||||
|
@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hasError, copied } = this.state;
|
const { hasError, copied, errorMessage } = this.state;
|
||||||
|
|
||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='error-boundary'>
|
<div className='error-boundary'>
|
||||||
<div>
|
<div>
|
||||||
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
|
<p className='error-boundary__error'>
|
||||||
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
|
{ likelyBrowserAddonIssue ? (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ likelyBrowserAddonIssue ? (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,8 +54,6 @@ export default class GIFV extends React.PureComponent {
|
|||||||
|
|
||||||
<video
|
<video
|
||||||
src={src}
|
src={src}
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
aria-label={alt}
|
aria-label={alt}
|
||||||
|
@ -7,6 +7,7 @@ import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me, isStaff } from '../initial_state';
|
import { me, isStaff } from '../initial_state';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
@ -20,7 +21,7 @@ const messages = defineMessages({
|
|||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
@ -328,13 +329,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} />
|
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||||
<span className='status__action-bar__counter__label' >
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
||||||
{/*obfuscatedCount {obfuscatedCount(status.get('replies_count'))}*/}
|
|
||||||
{ (status.get('replies_count'))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<IconButton className='status__action-bar-button' disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import Button from 'mastodon/components/button';
|
import Button from 'mastodon/components/button';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { autoPlayGif, isStaff, me } from 'mastodon/initial_state';
|
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import Avatar from 'mastodon/components/avatar';
|
import Avatar from 'mastodon/components/avatar';
|
||||||
import { counterRenderer } from 'mastodon/components/common_counter';
|
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
@ -15,38 +16,40 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
|||||||
import AccountNoteContainer from '../containers/account_note_container';
|
import AccountNoteContainer from '../containers/account_note_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow : { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
follow : { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
cancel_follow_request : { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
linkVerifiedOn : { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||||
account_locked : { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||||
mention : { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
||||||
direct : { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
block : { id: 'account.block', defaultMessage: 'Block @{name}' },
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
mute : { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
report : { id: 'account.report', defaultMessage: 'Report @{name}' },
|
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
|
||||||
share : { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
|
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
|
||||||
media : { id: 'account.media', defaultMessage: 'Media' },
|
media: { id: 'account.media', defaultMessage: 'Media' },
|
||||||
blockDomain : { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||||
unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||||
hideReblogs : { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||||
showReblogs : { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||||
pins : { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
||||||
preferences : { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
||||||
follow_requests : { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||||
favourites : { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
lists : { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
blocks : { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
domain_blocks : { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
mutes : { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
endorse : { id: 'account.endorse', defaultMessage: 'Feature on profile' },
|
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||||
unendorse : { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
|
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||||
|
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
|
||||||
|
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
|
||||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
||||||
admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const dateFormatOptions = {
|
const dateFormatOptions = {
|
||||||
@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
|
|||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onReport: PropTypes.func.isRequired,
|
|
||||||
onReblogToggle: PropTypes.func.isRequired,
|
onReblogToggle: PropTypes.func.isRequired,
|
||||||
|
onNotifyToggle: PropTypes.func.isRequired,
|
||||||
|
onReport: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
onBlockDomain: PropTypes.func.isRequired,
|
onBlockDomain: PropTypes.func.isRequired,
|
||||||
onUnblockDomain: PropTypes.func.isRequired,
|
onUnblockDomain: PropTypes.func.isRequired,
|
||||||
@ -82,7 +86,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
openEditProfile = () => {
|
openEditProfile = () => {
|
||||||
window.open('/settings/profile', '_blank');
|
window.open('/settings/profile', '_blank');
|
||||||
};
|
}
|
||||||
|
|
||||||
isStatusesPageActive = (match, location) => {
|
isStatusesPageActive = (match, location) => {
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@ -90,7 +94,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return !location.pathname.match(/\/(followers|following)\/?$/);
|
return !location.pathname.match(/\/(followers|following)\/?$/);
|
||||||
};
|
}
|
||||||
|
|
||||||
_updateEmojis () {
|
_updateEmojis () {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
@ -123,15 +127,15 @@ class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
handleEmojiMouseEnter = ({ target }) => {
|
handleEmojiMouseEnter = ({ target }) => {
|
||||||
target.src = target.getAttribute('data-original');
|
target.src = target.getAttribute('data-original');
|
||||||
};
|
}
|
||||||
|
|
||||||
handleEmojiMouseLeave = ({ target }) => {
|
handleEmojiMouseLeave = ({ target }) => {
|
||||||
target.src = target.getAttribute('data-static');
|
target.src = target.getAttribute('data-static');
|
||||||
};
|
}
|
||||||
|
|
||||||
setRef = (c) => {
|
setRef = (c) => {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, domain, identity_proofs } = this.props;
|
const { account, intl, domain, identity_proofs } = this.props;
|
||||||
@ -140,8 +144,11 @@ class Header extends ImmutablePureComponent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const suspended = account.get('suspended');
|
||||||
|
|
||||||
let info = [];
|
let info = [];
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
|
let bellBtn = '';
|
||||||
let lockedIcon = '';
|
let lockedIcon = '';
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
@ -154,13 +161,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
|
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
|
||||||
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
|
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
|
||||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
|
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
|
||||||
info.push(<span
|
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
|
||||||
key='domain_blocked'
|
|
||||||
className='relationship-tag'
|
|
||||||
><FormattedMessage
|
|
||||||
id='account.domain_blocked'
|
|
||||||
defaultMessage='Domain blocked'
|
|
||||||
/></span >);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
@ -177,6 +178,10 @@ class Header extends ImmutablePureComponent {
|
|||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||||
|
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||||
actionBtn = '';
|
actionBtn = '';
|
||||||
}
|
}
|
||||||
@ -274,7 +279,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
<div className='account__header__info'>
|
<div className='account__header__info'>
|
||||||
{info}
|
{!suspended && info}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||||
@ -288,11 +293,14 @@ class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
|
|
||||||
<div className='account__header__tabs__buttons'>
|
{!suspended && (
|
||||||
{actionBtn}
|
<div className='account__header__tabs__buttons'>
|
||||||
|
{actionBtn}
|
||||||
|
{bellBtn}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
<div className='account__header__tabs__name'>
|
||||||
@ -304,7 +312,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
<div className='account__header__bio'>
|
<div className='account__header__bio'>
|
||||||
{ (fields.size > 0 || identity_proofs.size > 0) && (
|
{(fields.size > 0 || identity_proofs.size > 0) && (
|
||||||
<div className='account__header__fields'>
|
<div className='account__header__fields'>
|
||||||
{identity_proofs.map((proof, i) => (
|
{identity_proofs.map((proof, i) => (
|
||||||
<dl key={i}>
|
<dl key={i}>
|
||||||
@ -330,33 +338,35 @@ class Header extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{account.get('id') !== me && <AccountNoteContainer account={account} />}
|
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
|
||||||
|
|
||||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__extra__links'>
|
{!suspended && (
|
||||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
<div className='account__header__extra__links'>
|
||||||
<ShortNumber
|
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||||
value={account.get('statuses_count')}
|
<ShortNumber
|
||||||
renderer={counterRenderer('statuses')}
|
value={account.get('statuses_count')}
|
||||||
/>
|
renderer={counterRenderer('statuses')}
|
||||||
</NavLink>
|
/>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
value={account.get('following_count')}
|
value={account.get('following_count')}
|
||||||
renderer={counterRenderer('following')}
|
renderer={counterRenderer('following')}
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
value={account.get('followers_count')}
|
value={account.get('followers_count')}
|
||||||
renderer={counterRenderer('followers')}
|
renderer={counterRenderer('followers')}
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,12 +15,15 @@ import { ScrollContainer } from 'react-router-scroll-4';
|
|||||||
import LoadMore from 'mastodon/components/load_more';
|
import LoadMore from 'mastodon/components/load_more';
|
||||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||||
attachments: getAccountGallery(state, props.params.accountId),
|
attachments: getAccountGallery(state, props.params.accountId),
|
||||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||||
|
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
|
||||||
|
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||||
});
|
});
|
||||||
|
|
||||||
class LoadMoreMedia extends ImmutablePureComponent {
|
class LoadMoreMedia extends ImmutablePureComponent {
|
||||||
@ -56,6 +59,8 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
blockedBy: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +124,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props;
|
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
|
||||||
const { width } = this.state;
|
const { width } = this.state;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
@ -152,15 +157,21 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||||
<HeaderContainer accountId={this.props.params.accountId} />
|
<HeaderContainer accountId={this.props.params.accountId} />
|
||||||
|
|
||||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
{(suspended || blockedBy) ? (
|
||||||
{attachments.map((attachment, index) => attachment === null ? (
|
<div className='empty-column-indicator'>
|
||||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||||
) : (
|
</div>
|
||||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
) : (
|
||||||
))}
|
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||||
|
{attachments.map((attachment, index) => attachment === null ? (
|
||||||
|
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||||
|
) : (
|
||||||
|
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||||
|
))}
|
||||||
|
|
||||||
{loadOlder}
|
{loadOlder}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading && attachments.size === 0 && (
|
{isLoading && attachments.size === 0 && (
|
||||||
<div className='scrollable__append'>
|
<div className='scrollable__append'>
|
||||||
|
@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
this.props.onReblogToggle(this.props.account);
|
this.props.onReblogToggle(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNotifyToggle = () => {
|
||||||
|
this.props.onNotifyToggle(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
handleMute = () => {
|
handleMute = () => {
|
||||||
this.props.onMute(this.props.account);
|
this.props.onMute(this.props.account);
|
||||||
}
|
}
|
||||||
@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
onDirect={this.handleDirect}
|
onDirect={this.handleDirect}
|
||||||
onReblogToggle={this.handleReblogToggle}
|
onReblogToggle={this.handleReblogToggle}
|
||||||
|
onNotifyToggle={this.handleNotifyToggle}
|
||||||
onReport={this.handleReport}
|
onReport={this.handleReport}
|
||||||
onMute={this.handleMute}
|
onMute={this.handleMute}
|
||||||
onBlockDomain={this.handleBlockDomain}
|
onBlockDomain={this.handleBlockDomain}
|
||||||
|
@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
|
|
||||||
onReblogToggle (account) {
|
onReblogToggle (account) {
|
||||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||||
dispatch(followAccount(account.get('id'), false));
|
dispatch(followAccount(account.get('id'), { reblogs: false }));
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id'), true));
|
dispatch(followAccount(account.get('id'), { reblogs: true }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onNotifyToggle (account) {
|
||||||
|
if (account.getIn(['relationship', 'notifying'])) {
|
||||||
|
dispatch(followAccount(account.get('id'), { notify: false }));
|
||||||
|
} else {
|
||||||
|
dispatch(followAccount(account.get('id'), { notify: true }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onReport (account) {
|
onReport (account) {
|
||||||
dispatch(initReport(account));
|
dispatch(initReport(account));
|
||||||
},
|
},
|
||||||
|
@ -31,6 +31,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
|
|||||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -57,6 +58,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
withReplies: PropTypes.bool,
|
withReplies: PropTypes.bool,
|
||||||
blockedBy: PropTypes.bool,
|
blockedBy: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
@ -113,7 +115,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
@ -134,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (blockedBy) {
|
if (suspended || blockedBy) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
} else if (remote && statusIds.isEmpty()) {
|
} else if (remote && statusIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
@ -153,7 +155,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
statusIds={blockedBy ? emptyList : statusIds}
|
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
|
||||||
featuredStatusIds={featuredStatusIds}
|
featuredStatusIds={featuredStatusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
@ -315,7 +315,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||||||
|
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false, active: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||||||
} else {
|
} else {
|
||||||
const { top } = target.getBoundingClientRect();
|
const { top } = target.getBoundingClientRect();
|
||||||
if (this.state.open && this.activeElement) {
|
if (this.state.open && this.activeElement) {
|
||||||
this.activeElement.focus();
|
this.activeElement.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||||
this.setState({ open: !this.state.open });
|
this.setState({ open: !this.state.open });
|
||||||
@ -220,7 +220,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
if (this.state.open && this.activeElement) {
|
if (this.state.open && this.activeElement) {
|
||||||
this.activeElement.focus();
|
this.activeElement.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
this.setState({ open: false });
|
this.setState({ open: false });
|
||||||
}
|
}
|
||||||
|
@ -5,22 +5,30 @@ import PropTypes from 'prop-types';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { me } from '../../../initial_state';
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
const HASHTAG_SEPARATORS = "_\\u00b7\\u200c";
|
const buildHashtagRE = () => {
|
||||||
const ALPHA = '\\p{L}\\p{M}';
|
try {
|
||||||
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
|
const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
|
||||||
const APPROX_HASHTAG_RE = new RegExp(
|
const ALPHA = '\\p{L}\\p{M}';
|
||||||
'(?:^|[^\\/\\)\\w])#((' +
|
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
|
||||||
'[' + WORD + '_]' +
|
return new RegExp(
|
||||||
'[' + WORD + HASHTAG_SEPARATORS + ']*' +
|
'(?:^|[^\\/\\)\\w])#((' +
|
||||||
'[' + ALPHA + HASHTAG_SEPARATORS + ']' +
|
'[' + WORD + '_]' +
|
||||||
'[' + WORD + HASHTAG_SEPARATORS +']*' +
|
'[' + WORD + HASHTAG_SEPARATORS + ']*' +
|
||||||
'[' + WORD + '_]' +
|
'[' + ALPHA + HASHTAG_SEPARATORS + ']' +
|
||||||
')|(' +
|
'[' + WORD + HASHTAG_SEPARATORS +']*' +
|
||||||
'[' + WORD + '_]*' +
|
'[' + WORD + '_]' +
|
||||||
'[' + ALPHA + ']' +
|
')|(' +
|
||||||
'[' + WORD + '_]*' +
|
'[' + WORD + '_]*' +
|
||||||
'))', 'iu'
|
'[' + ALPHA + ']' +
|
||||||
);
|
'[' + WORD + '_]*' +
|
||||||
|
'))', 'iu',
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const APPROX_HASHTAG_RE = buildHashtagRE();
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
||||||
|
@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Emoji requiring extra borders depending on theme
|
// Emoji requiring extra borders depending on theme
|
||||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴']);
|
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']);
|
||||||
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
||||||
|
|
||||||
const emojiFilename = (filename) => {
|
const emojiFilename = (filename) => {
|
||||||
|
@ -40,6 +40,7 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
myAccount: state.getIn(['accounts', me]),
|
myAccount: state.getIn(['accounts', me]),
|
||||||
|
columns: state.getIn(['settings', 'columns']),
|
||||||
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -89,60 +90,66 @@ class GettingStarted extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
|
const { intl, myAccount, columns, multiColumn, unreadFollowRequests } = this.props;
|
||||||
|
|
||||||
const navItems = [];
|
const navItems = [];
|
||||||
let i = 1;
|
|
||||||
let height = (multiColumn) ? 0 : 60;
|
let height = (multiColumn) ? 0 : 60;
|
||||||
|
|
||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
|
<ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
|
||||||
<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
|
<ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
|
||||||
<ColumnLink key={i++} icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
|
<ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 34 + 48*2;
|
height += 34 + 48*2;
|
||||||
|
|
||||||
if (profile_directory) {
|
if (profile_directory) {
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 48;
|
height += 48;
|
||||||
}
|
}
|
||||||
|
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />,
|
<ColumnSubheading key='header-personal' text={intl.formatMessage(messages.personal)} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 34;
|
height += 34;
|
||||||
} else if (profile_directory) {
|
} else if (profile_directory) {
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 48;
|
height += 48;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
|
||||||
|
navItems.push(
|
||||||
|
<ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />,
|
||||||
|
);
|
||||||
|
height += 48;
|
||||||
|
}
|
||||||
|
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
|
<ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
|
||||||
<ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
|
<ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
|
||||||
<ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
<ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||||
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
|
<ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 48*4;
|
height += 48*4;
|
||||||
|
|
||||||
if (myAccount.get('locked') || unreadFollowRequests > 0) {
|
if (myAccount.get('locked') || unreadFollowRequests > 0) {
|
||||||
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||||
height += 48;
|
height += 48;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!multiColumn) {
|
if (!multiColumn) {
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnSubheading key={i++} text={intl.formatMessage(messages.settings_subheading)} />,
|
<ColumnSubheading key='header-settings' text={intl.formatMessage(messages.settings_subheading)} />,
|
||||||
<ColumnLink key={i++} icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
|
<ColumnLink key='preferences' icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 34 + 48;
|
height += 34 + 48;
|
||||||
|
@ -10,15 +10,19 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
|||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import { connectListStream } from '../../actions/streaming';
|
import { connectListStream } from '../../actions/streaming';
|
||||||
import { expandListTimeline } from '../../actions/timelines';
|
import { expandListTimeline } from '../../actions/timelines';
|
||||||
import { fetchList, deleteList } from '../../actions/lists';
|
import { fetchList, deleteList, updateList } from '../../actions/lists';
|
||||||
import { openModal } from '../../actions/modal';
|
import { openModal } from '../../actions/modal';
|
||||||
import MissingIndicator from '../../components/missing_indicator';
|
import MissingIndicator from '../../components/missing_indicator';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import RadioButton from 'mastodon/components/radio_button';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
||||||
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
||||||
|
all_replies: { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' },
|
||||||
|
no_replies: { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' },
|
||||||
|
list_replies: { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
@ -131,11 +135,18 @@ class ListTimeline extends React.PureComponent {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRepliesPolicyChange = ({ target }) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
dispatch(updateList(id, undefined, false, target.value));
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list } = this.props;
|
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const title = list ? list.get('title') : id;
|
const title = list ? list.get('title') : id;
|
||||||
|
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||||
|
|
||||||
if (typeof list === 'undefined') {
|
if (typeof list === 'undefined') {
|
||||||
return (
|
return (
|
||||||
@ -166,7 +177,7 @@ class ListTimeline extends React.PureComponent {
|
|||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
>
|
>
|
||||||
<div className='column-header__links'>
|
<div className='column-settings__row column-header__links'>
|
||||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
|
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
|
||||||
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||||
</button>
|
</button>
|
||||||
@ -175,6 +186,19 @@ class ListTimeline extends React.PureComponent {
|
|||||||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{ replies_policy !== undefined && (
|
||||||
|
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
||||||
|
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
||||||
|
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
|
||||||
|
</span>
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
{ ['no_replies', 'list_replies', 'all_replies'].map(policy => (
|
||||||
|
<RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
@ -9,6 +9,7 @@ const tooltips = defineMessages({
|
|||||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||||
|
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<Icon id='tasks' fixedWidth />
|
<Icon id='tasks' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedFilter === 'status' ? 'active' : ''}
|
||||||
|
onClick={this.onClick('status')}
|
||||||
|
title={intl.formatMessage(tooltips.statuses)}
|
||||||
|
>
|
||||||
|
<Icon id='home' fixedWidth />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'follow' ? 'active' : ''}
|
className={selectedFilter === 'follow' ? 'active' : ''}
|
||||||
onClick={this.onClick('follow')}
|
onClick={this.onClick('follow')}
|
||||||
|
@ -17,6 +17,7 @@ const messages = defineMessages({
|
|||||||
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
||||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||||
|
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||||
@ -237,6 +238,38 @@ class Notification extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderStatus (notification, link) {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className='notification notification-status focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='home' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderPoll (notification, account) {
|
renderPoll (notification, account) {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
const ownPoll = me === account.get('id');
|
const ownPoll = me === account.get('id');
|
||||||
@ -292,6 +325,8 @@ class Notification extends ImmutablePureComponent {
|
|||||||
return this.renderFavourite(notification, link);
|
return this.renderFavourite(notification, link);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
|
case 'status':
|
||||||
|
return this.renderStatus(notification, link);
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return this.renderPoll(notification, account);
|
return this.renderPoll(notification, account);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ const getNotifications = createSelector([
|
|||||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||||
}
|
}
|
||||||
return notifications.filter(item => item !== null && allowedType === item.get('type'));
|
return notifications.filter(item => item === null || allowedType === item.get('type'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user