Merge pull request #1521 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
97da7b7307
100 changed files with 1901 additions and 1077 deletions
|
@ -1 +1 @@
|
||||||
2.7.2
|
2.7.3
|
||||||
|
|
|
@ -26,7 +26,7 @@ RUN ARCH= && \
|
||||||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||||
|
|
||||||
# Install Ruby
|
# Install Ruby
|
||||||
ENV RUBY_VER="2.7.2"
|
ENV RUBY_VER="2.7.3"
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
apt-get install -y --no-install-recommends build-essential \
|
||||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||||
|
|
12
Gemfile
12
Gemfile
|
@ -32,9 +32,9 @@ gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'chewy', '~> 5.2'
|
gem 'chewy', '~> 5.2'
|
||||||
gem 'cld3', '~> 3.4.1'
|
gem 'cld3', '~> 3.4.2'
|
||||||
gem 'devise', '~> 4.7'
|
gem 'devise', '~> 4.7'
|
||||||
gem 'devise-two-factor', git: 'https://github.com/ClearlyClaire/devise-two-factor', ref: '594bb8a32e6f94df7e5ba7c9399eaf9ff25bac0d'
|
gem 'devise-two-factor', '~> 4.0'
|
||||||
|
|
||||||
group :pam_authentication, optional: true do
|
group :pam_authentication, optional: true do
|
||||||
gem 'devise_pam_authenticatable2', '~> 9.2'
|
gem 'devise_pam_authenticatable2', '~> 9.2'
|
||||||
|
@ -62,9 +62,8 @@ gem 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||||
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
|
|
||||||
gem 'nokogiri', '~> 1.11'
|
gem 'nokogiri', '~> 1.11'
|
||||||
gem 'nsa', git: 'https://github.com/Gargron/nsa', ref: 'd1079e0cdafdfed7f9f35478d13b9bdaa65965c0'
|
gem 'nsa', '~> 0.2'
|
||||||
gem 'oj', '~> 3.11'
|
gem 'oj', '~> 3.11'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
|
@ -95,7 +94,7 @@ gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2021'
|
gem 'tzinfo-data', '~> 1.2021'
|
||||||
gem 'webpacker', '~> 5.2'
|
gem 'webpacker', '~> 5.2'
|
||||||
gem 'webpush'
|
gem 'webpush', '~> 0.3'
|
||||||
gem 'webauthn', '~> 3.0.0.alpha1'
|
gem 'webauthn', '~> 3.0.0.alpha1'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
|
@ -126,7 +125,7 @@ group :test do
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.21', require: false
|
gem 'simplecov', '~> 0.21', require: false
|
||||||
gem 'webmock', '~> 3.12'
|
gem 'webmock', '~> 3.12'
|
||||||
gem 'parallel_tests', '~> 3.6'
|
gem 'parallel_tests', '~> 3.7'
|
||||||
gem 'rspec_junit_formatter', '~> 0.4'
|
gem 'rspec_junit_formatter', '~> 0.4'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -160,4 +159,3 @@ gem 'concurrent-ruby', require: false
|
||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
|
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
gem 'pluck_each', git: 'https://github.com/nsommer/pluck_each', ref: '73be0947c52fc54bf6d7085378db008358aac5eb'
|
|
||||||
|
|
100
Gemfile.lock
100
Gemfile.lock
|
@ -1,42 +1,3 @@
|
||||||
GIT
|
|
||||||
remote: https://github.com/ClearlyClaire/devise-two-factor
|
|
||||||
revision: 594bb8a32e6f94df7e5ba7c9399eaf9ff25bac0d
|
|
||||||
ref: 594bb8a32e6f94df7e5ba7c9399eaf9ff25bac0d
|
|
||||||
specs:
|
|
||||||
devise-two-factor (3.1.0)
|
|
||||||
activesupport (< 7.0)
|
|
||||||
attr_encrypted (>= 1.3, < 4, != 2)
|
|
||||||
devise
|
|
||||||
railties (< 7.0)
|
|
||||||
rotp (~> 6)
|
|
||||||
|
|
||||||
GIT
|
|
||||||
remote: https://github.com/Gargron/nsa
|
|
||||||
revision: d1079e0cdafdfed7f9f35478d13b9bdaa65965c0
|
|
||||||
ref: d1079e0cdafdfed7f9f35478d13b9bdaa65965c0
|
|
||||||
specs:
|
|
||||||
nsa (0.2.8)
|
|
||||||
activesupport (>= 4.2, < 7)
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
||||||
sidekiq (>= 3.5)
|
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
|
||||||
|
|
||||||
GIT
|
|
||||||
remote: https://github.com/nsommer/pluck_each
|
|
||||||
revision: 73be0947c52fc54bf6d7085378db008358aac5eb
|
|
||||||
ref: 73be0947c52fc54bf6d7085378db008358aac5eb
|
|
||||||
specs:
|
|
||||||
pluck_each (0.1.3)
|
|
||||||
activerecord (>= 6.1.0)
|
|
||||||
activesupport (>= 6.1.0)
|
|
||||||
|
|
||||||
GIT
|
|
||||||
remote: https://github.com/witgo/nilsimsa
|
|
||||||
revision: fd184883048b922b176939f851338d0a4971a532
|
|
||||||
ref: fd184883048b922b176939f851338d0a4971a532
|
|
||||||
specs:
|
|
||||||
nilsimsa (1.1.2)
|
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
@ -120,8 +81,8 @@ GEM
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.1)
|
aws-eventstream (1.1.1)
|
||||||
aws-partitions (1.436.0)
|
aws-partitions (1.445.0)
|
||||||
aws-sdk-core (3.113.0)
|
aws-sdk-core (3.114.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)
|
||||||
|
@ -129,7 +90,7 @@ GEM
|
||||||
aws-sdk-kms (1.43.0)
|
aws-sdk-kms (1.43.0)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.93.0)
|
aws-sdk-s3 (1.93.1)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
|
@ -192,15 +153,15 @@ GEM
|
||||||
elasticsearch (>= 2.0.0)
|
elasticsearch (>= 2.0.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.3.15)
|
chunky_png (1.3.15)
|
||||||
cld3 (3.4.1)
|
cld3 (3.4.2)
|
||||||
ffi (>= 1.1.0, < 1.15.0)
|
ffi (>= 1.1.0, < 1.16.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
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.8)
|
concurrent-ruby (1.1.8)
|
||||||
connection_pool (2.2.3)
|
connection_pool (2.2.5)
|
||||||
cose (1.0.0)
|
cose (1.0.0)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
openssl-signature_algorithm (~> 0.4.0)
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
|
@ -216,6 +177,12 @@ GEM
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
|
devise-two-factor (4.0.0)
|
||||||
|
activesupport (< 6.2)
|
||||||
|
attr_encrypted (>= 1.3, < 4, != 2)
|
||||||
|
devise (~> 4.0)
|
||||||
|
railties (< 6.2)
|
||||||
|
rotp (~> 6.0)
|
||||||
devise_pam_authenticatable2 (9.2.0)
|
devise_pam_authenticatable2 (9.2.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
|
@ -225,7 +192,7 @@ GEM
|
||||||
docile (1.3.4)
|
docile (1.3.4)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.5.0)
|
doorkeeper (5.5.1)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
|
@ -257,7 +224,7 @@ GEM
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
fastimage (2.2.3)
|
fastimage (2.2.3)
|
||||||
ffi (1.14.2)
|
ffi (1.15.0)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
|
@ -313,7 +280,7 @@ GEM
|
||||||
httplog (1.4.3)
|
httplog (1.4.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.8.9)
|
i18n (1.8.10)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.34)
|
i18n-tasks (0.9.34)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
|
@ -369,7 +336,7 @@ GEM
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.9.0)
|
loofah (2.9.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -401,12 +368,17 @@ GEM
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.7)
|
nio4r (2.5.7)
|
||||||
nokogiri (1.11.2)
|
nokogiri (1.11.3)
|
||||||
mini_portile2 (~> 2.5.0)
|
mini_portile2 (~> 2.5.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogumbo (2.0.4)
|
nokogumbo (2.0.4)
|
||||||
nokogiri (~> 1.8, >= 1.8.4)
|
nokogiri (~> 1.8, >= 1.8.4)
|
||||||
oj (3.11.3)
|
nsa (0.2.8)
|
||||||
|
activesupport (>= 4.2, < 7)
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
sidekiq (>= 3.5)
|
||||||
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
|
oj (3.11.5)
|
||||||
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)
|
||||||
|
@ -434,9 +406,9 @@ GEM
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.20.1)
|
parallel (1.20.1)
|
||||||
parallel_tests (3.6.0)
|
parallel_tests (3.7.0)
|
||||||
parallel
|
parallel
|
||||||
parser (3.0.0.0)
|
parser (3.0.1.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
|
@ -444,7 +416,7 @@ GEM
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pghero (2.8.1)
|
pghero (2.8.1)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.5)
|
pkg-config (1.4.6)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.14.2)
|
premailer (1.14.2)
|
||||||
addressable
|
addressable
|
||||||
|
@ -530,7 +502,7 @@ GEM
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.4)
|
rexml (3.2.5)
|
||||||
rotp (6.2.0)
|
rotp (6.2.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (1.2.0)
|
rqrcode (1.2.0)
|
||||||
|
@ -591,7 +563,7 @@ GEM
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (2.3.0)
|
semantic_range (2.3.0)
|
||||||
sidekiq (6.2.0)
|
sidekiq (6.2.1)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
|
@ -604,7 +576,7 @@ GEM
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
thwait
|
thwait
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.0.7)
|
sidekiq-unique-jobs (7.0.8)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 5.0, < 7.0)
|
sidekiq (>= 5.0, < 7.0)
|
||||||
|
@ -651,7 +623,7 @@ GEM
|
||||||
openssl-signature_algorithm (~> 0.4.0)
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
tty-color (0.6.0)
|
tty-color (0.6.0)
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-prompt (0.23.0)
|
tty-prompt (0.23.1)
|
||||||
pastel (~> 0.8)
|
pastel (~> 0.8)
|
||||||
tty-reader (~> 0.8)
|
tty-reader (~> 0.8)
|
||||||
tty-reader (0.9.0)
|
tty-reader (0.9.0)
|
||||||
|
@ -728,13 +700,13 @@ DEPENDENCIES
|
||||||
capybara (~> 3.35)
|
capybara (~> 3.35)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 5.2)
|
chewy (~> 5.2)
|
||||||
cld3 (~> 3.4.1)
|
cld3 (~> 3.4.2)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
color_diff (~> 0.1)
|
color_diff (~> 0.1)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
connection_pool
|
connection_pool
|
||||||
devise (~> 4.7)
|
devise (~> 4.7)
|
||||||
devise-two-factor!
|
devise-two-factor (~> 4.0)
|
||||||
devise_pam_authenticatable2 (~> 9.2)
|
devise_pam_authenticatable2 (~> 9.2)
|
||||||
discard (~> 1.2)
|
discard (~> 1.2)
|
||||||
doorkeeper (~> 5.5)
|
doorkeeper (~> 5.5)
|
||||||
|
@ -769,9 +741,8 @@ DEPENDENCIES
|
||||||
microformats (~> 4.2)
|
microformats (~> 4.2)
|
||||||
mime-types (~> 3.3.1)
|
mime-types (~> 3.3.1)
|
||||||
net-ldap (~> 0.17)
|
net-ldap (~> 0.17)
|
||||||
nilsimsa!
|
|
||||||
nokogiri (~> 1.11)
|
nokogiri (~> 1.11)
|
||||||
nsa!
|
nsa (~> 0.2)
|
||||||
oj (~> 3.11)
|
oj (~> 3.11)
|
||||||
omniauth (~> 1.9)
|
omniauth (~> 1.9)
|
||||||
omniauth-cas (~> 2.0)
|
omniauth-cas (~> 2.0)
|
||||||
|
@ -781,12 +752,11 @@ DEPENDENCIES
|
||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel (~> 1.20)
|
parallel (~> 1.20)
|
||||||
parallel_tests (~> 3.6)
|
parallel_tests (~> 3.7)
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.2)
|
pg (~> 1.2)
|
||||||
pghero (~> 2.8)
|
pghero (~> 2.8)
|
||||||
pkg-config (~> 1.4)
|
pkg-config (~> 1.4)
|
||||||
pluck_each!
|
|
||||||
posix-spawn
|
posix-spawn
|
||||||
premailer-rails
|
premailer-rails
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
|
@ -834,5 +804,5 @@ DEPENDENCIES
|
||||||
webauthn (~> 3.0.0.alpha1)
|
webauthn (~> 3.0.0.alpha1)
|
||||||
webmock (~> 3.12)
|
webmock (~> 3.12)
|
||||||
webpacker (~> 5.2)
|
webpacker (~> 5.2)
|
||||||
webpush
|
webpush (~> 0.3)
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
|
@ -36,7 +36,6 @@ module Admin
|
||||||
@profile_directory = Setting.profile_directory
|
@profile_directory = Setting.profile_directory
|
||||||
@timeline_preview = Setting.timeline_preview
|
@timeline_preview = Setting.timeline_preview
|
||||||
@keybase_integration = Setting.enable_keybase
|
@keybase_integration = Setting.enable_keybase
|
||||||
@spam_check_enabled = Setting.spam_check_enabled
|
|
||||||
@trends_enabled = Setting.trends
|
@trends_enabled = Setting.trends
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
53
app/controllers/admin/follow_recommendations_controller.rb
Normal file
53
app/controllers/admin/follow_recommendations_controller.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class FollowRecommendationsController < BaseController
|
||||||
|
before_action :set_language
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize :follow_recommendation, :show?
|
||||||
|
|
||||||
|
@form = Form::AccountBatch.new
|
||||||
|
@accounts = filtered_follow_recommendations
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
# Do nothing
|
||||||
|
ensure
|
||||||
|
redirect_to admin_follow_recommendations_path(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_language
|
||||||
|
@language = follow_recommendation_filter.language
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_follow_recommendations
|
||||||
|
follow_recommendation_filter.results
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_recommendation_filter
|
||||||
|
@follow_recommendation_filter ||= FollowRecommendationFilter.new(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_account_batch_params
|
||||||
|
params.require(:form_account_batch).permit(:action, account_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(*FollowRecommendationFilter::KEYS).permit(*FollowRecommendationFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:suppress]
|
||||||
|
'suppress_follow_recommendation'
|
||||||
|
elsif params[:unsuppress]
|
||||||
|
'unsuppress_follow_recommendation'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,13 +3,13 @@
|
||||||
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :push }
|
before_action -> { doorkeeper_authorize! :push }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_web_push_subscription
|
before_action :set_push_subscription
|
||||||
before_action :check_web_push_subscription, only: [:show, :update]
|
before_action :check_push_subscription, only: [:show, :update]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@web_subscription&.destroy!
|
@push_subscription&.destroy!
|
||||||
|
|
||||||
@web_subscription = ::Web::PushSubscription.create!(
|
@push_subscription = Web::PushSubscription.create!(
|
||||||
endpoint: subscription_params[:endpoint],
|
endpoint: subscription_params[:endpoint],
|
||||||
key_p256dh: subscription_params[:keys][:p256dh],
|
key_p256dh: subscription_params[:keys][:p256dh],
|
||||||
key_auth: subscription_params[:keys][:auth],
|
key_auth: subscription_params[:keys][:auth],
|
||||||
|
@ -18,31 +18,31 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
access_token_id: doorkeeper_token.id
|
access_token_id: doorkeeper_token.id
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@web_subscription.update!(data: data_params)
|
@push_subscription.update!(data: data_params)
|
||||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@web_subscription&.destroy!
|
@push_subscription&.destroy!
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_web_push_subscription
|
def set_push_subscription
|
||||||
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
@push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_web_push_subscription
|
def check_push_subscription
|
||||||
not_found if @web_subscription.nil?
|
not_found if @push_subscription.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscription_params
|
def subscription_params
|
||||||
|
@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
def data_params
|
def data_params
|
||||||
return {} if params[:data].blank?
|
return {} if params[:data].blank?
|
||||||
|
|
||||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,6 @@ class Api::V1::SuggestionsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
@accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
@accounts = PotentialFriendshipTracker.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
19
app/controllers/api/v2/suggestions_controller.rb
Normal file
19
app/controllers/api/v2/suggestions_controller.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V2::SuggestionsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :read }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_suggestions
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @suggestions, each_serializer: REST::SuggestionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_suggestions
|
||||||
|
@suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
before_action :set_push_subscription, only: :update
|
||||||
|
|
||||||
def create
|
def create
|
||||||
active_session = current_session
|
active_session = current_session
|
||||||
|
@ -15,9 +16,11 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
|
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
policy: 'all',
|
||||||
|
|
||||||
alerts: {
|
alerts: {
|
||||||
follow: alerts_enabled,
|
follow: alerts_enabled,
|
||||||
follow_request: false,
|
follow_request: alerts_enabled,
|
||||||
favourite: alerts_enabled,
|
favourite: alerts_enabled,
|
||||||
reblog: alerts_enabled,
|
reblog: alerts_enabled,
|
||||||
mention: alerts_enabled,
|
mention: alerts_enabled,
|
||||||
|
@ -28,7 +31,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
|
|
||||||
data.deep_merge!(data_params) if params[:data]
|
data.deep_merge!(data_params) if params[:data]
|
||||||
|
|
||||||
web_subscription = ::Web::PushSubscription.create!(
|
push_subscription = ::Web::PushSubscription.create!(
|
||||||
endpoint: subscription_params[:endpoint],
|
endpoint: subscription_params[:endpoint],
|
||||||
key_p256dh: subscription_params[:keys][:p256dh],
|
key_p256dh: subscription_params[:keys][:p256dh],
|
||||||
key_auth: subscription_params[:keys][:auth],
|
key_auth: subscription_params[:keys][:auth],
|
||||||
|
@ -37,27 +40,27 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
access_token_id: active_session.access_token_id
|
access_token_id: active_session.access_token_id
|
||||||
)
|
)
|
||||||
|
|
||||||
active_session.update!(web_push_subscription: web_subscription)
|
active_session.update!(web_push_subscription: push_subscription)
|
||||||
|
|
||||||
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
params.require([:id])
|
@push_subscription.update!(data: data_params)
|
||||||
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
web_subscription = ::Web::PushSubscription.find(params[:id])
|
|
||||||
web_subscription.update!(data: data_params)
|
|
||||||
|
|
||||||
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_push_subscription
|
||||||
|
@push_subscription = ::Web::PushSubscription.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
def subscription_params
|
def subscription_params
|
||||||
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||||
end
|
end
|
||||||
|
|
||||||
def data_params
|
def data_params
|
||||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
@data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -91,8 +91,6 @@ module ApplicationHelper
|
||||||
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
|
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
|
||||||
elsif status.private_visibility? || status.limited_visibility?
|
elsif status.private_visibility? || status.limited_visibility?
|
||||||
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
|
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
|
||||||
elsif status.direct_visibility?
|
|
||||||
fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
18
app/helpers/email_helper.rb
Normal file
18
app/helpers/email_helper.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EmailHelper
|
||||||
|
def self.included(base)
|
||||||
|
base.extend(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_to_canonical_email(str)
|
||||||
|
username, domain = str.downcase.split('@', 2)
|
||||||
|
username, = username.gsub('.', '').split('+', 2)
|
||||||
|
|
||||||
|
"#{username}@#{domain}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_to_canonical_email_hash(str)
|
||||||
|
Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str))
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,6 +24,7 @@ export function normalizeAccount(account) {
|
||||||
|
|
||||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
||||||
account.note_emojified = emojify(account.note, emojiMap);
|
account.note_emojified = emojify(account.note, emojiMap);
|
||||||
|
account.note_plain = unescapeHTML(account.note);
|
||||||
|
|
||||||
if (account.fields) {
|
if (account.fields) {
|
||||||
account.fields = account.fields.map(pair => ({
|
account.fields = account.fields.map(pair => ({
|
||||||
|
|
|
@ -1,21 +1,8 @@
|
||||||
import { changeSetting, saveSettings } from './settings';
|
import { changeSetting, saveSettings } from './settings';
|
||||||
import { requestBrowserPermission } from './notifications';
|
|
||||||
|
|
||||||
export const INTRODUCTION_VERSION = 20181216044202;
|
export const INTRODUCTION_VERSION = 20181216044202;
|
||||||
|
|
||||||
export const closeOnboarding = () => dispatch => {
|
export const closeOnboarding = () => dispatch => {
|
||||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
|
|
||||||
dispatch(requestBrowserPermission((permission) => {
|
|
||||||
if (permission === 'granted') {
|
|
||||||
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
|
|
||||||
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
|
|
||||||
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
|
|
||||||
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
|
|
||||||
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
|
|
||||||
dispatch(changeSetting(['notifications', 'alerts', 'status'], true));
|
|
||||||
dispatch(saveSettings());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
|
||||||
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
|
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
|
||||||
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
||||||
|
@ -7,13 +8,17 @@ export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
|
||||||
|
|
||||||
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
|
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
|
||||||
|
|
||||||
export function fetchSuggestions() {
|
export function fetchSuggestions(withRelationships = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchSuggestionsRequest());
|
dispatch(fetchSuggestionsRequest());
|
||||||
|
|
||||||
api(getState).get('/api/v1/suggestions').then(response => {
|
api(getState).get('/api/v2/suggestions').then(response => {
|
||||||
dispatch(importFetchedAccounts(response.data));
|
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||||
dispatch(fetchSuggestionsSuccess(response.data));
|
dispatch(fetchSuggestionsSuccess(response.data));
|
||||||
|
|
||||||
|
if (withRelationships) {
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.account.id)));
|
||||||
|
}
|
||||||
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -25,10 +30,10 @@ export function fetchSuggestionsRequest() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchSuggestionsSuccess(accounts) {
|
export function fetchSuggestionsSuccess(suggestions) {
|
||||||
return {
|
return {
|
||||||
type: SUGGESTIONS_FETCH_SUCCESS,
|
type: SUGGESTIONS_FETCH_SUCCESS,
|
||||||
accounts,
|
suggestions,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -48,5 +53,12 @@ export const dismissSuggestion = accountId => (dispatch, getState) => {
|
||||||
id: accountId,
|
id: accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/suggestions/${accountId}`);
|
api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => {
|
||||||
|
dispatch(fetchSuggestionsRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v2/suggestions').then(response => {
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||||
|
dispatch(fetchSuggestionsSuccess(response.data));
|
||||||
|
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
||||||
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
|
@ -78,8 +78,10 @@ class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
|
|
||||||
if (onActionClick && actionIcon) {
|
if (actionIcon) {
|
||||||
|
if (onActionClick) {
|
||||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||||
|
}
|
||||||
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||||
const following = account.getIn(['relationship', 'following']);
|
const following = account.getIn(['relationship', 'following']);
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
|
|
9
app/javascript/mastodon/components/logo.js
Normal file
9
app/javascript/mastodon/components/logo.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Logo = () => (
|
||||||
|
<svg viewBox='0 0 216.4144 232.00976' className='logo'>
|
||||||
|
<use xlinkHref='#mastodon-svg-logo' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Logo;
|
|
@ -1,12 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider, connect } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import configureStore from '../store/configureStore';
|
import configureStore from '../store/configureStore';
|
||||||
import { INTRODUCTION_VERSION } from '../actions/onboarding';
|
|
||||||
import { BrowserRouter, Route } from 'react-router-dom';
|
import { BrowserRouter, Route } from 'react-router-dom';
|
||||||
import { ScrollContext } from 'react-router-scroll-4';
|
import { ScrollContext } from 'react-router-scroll-4';
|
||||||
import UI from '../features/ui';
|
import UI from '../features/ui';
|
||||||
import Introduction from '../features/introduction';
|
|
||||||
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||||
import { hydrateStore } from '../actions/store';
|
import { hydrateStore } from '../actions/store';
|
||||||
import { connectUserStream } from '../actions/streaming';
|
import { connectUserStream } from '../actions/streaming';
|
||||||
|
@ -26,39 +24,6 @@ const hydrateAction = hydrateStore(initialState);
|
||||||
store.dispatch(hydrateAction);
|
store.dispatch(hydrateAction);
|
||||||
store.dispatch(fetchCustomEmojis());
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
|
||||||
});
|
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
|
||||||
class MastodonMount extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
showIntroduction: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
shouldUpdateScroll (_, { location }) {
|
|
||||||
return location.state !== previewMediaState && location.state !== previewVideoState;
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { showIntroduction } = this.props;
|
|
||||||
|
|
||||||
if (showIntroduction) {
|
|
||||||
return <Introduction />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BrowserRouter basename='/web'>
|
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
|
||||||
<Route path='/' component={UI} />
|
|
||||||
</ScrollContext>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Mastodon extends React.PureComponent {
|
export default class Mastodon extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -76,6 +41,10 @@ export default class Mastodon extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldUpdateScroll (_, { location }) {
|
||||||
|
return location.state !== previewMediaState && location.state !== previewVideoState;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { locale } = this.props;
|
const { locale } = this.props;
|
||||||
|
|
||||||
|
@ -83,7 +52,11 @@ export default class Mastodon extends React.PureComponent {
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MastodonMount />
|
<BrowserRouter basename='/web'>
|
||||||
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
|
<Route path='/' component={UI} />
|
||||||
|
</ScrollContext>
|
||||||
|
</BrowserRouter>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Provider>
|
</Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
|
|
@ -51,12 +51,12 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
|
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{suggestions && suggestions.map(accountId => (
|
{suggestions && suggestions.map(suggestion => (
|
||||||
<AccountContainer
|
<AccountContainer
|
||||||
key={accountId}
|
key={suggestion.get('account')}
|
||||||
id={accountId}
|
id={suggestion.get('account')}
|
||||||
actionIcon='times'
|
actionIcon={suggestion.get('source') === 'past_interaction' ? 'times' : null}
|
||||||
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
|
actionTitle={suggestion.get('source') === 'past_interaction' ? intl.formatMessage(messages.dismissSuggestion) : null}
|
||||||
onActionClick={dismissSuggestion}
|
onActionClick={dismissSuggestion}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -11,7 +11,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) => {
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import Avatar from 'mastodon/components/avatar';
|
||||||
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
|
import Permalink from 'mastodon/components/permalink';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
account: getAccount(state, props.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFirstSentence = str => {
|
||||||
|
const arr = str.split(/(([\.\?!]+\s)|[.。?!\n•])/);
|
||||||
|
|
||||||
|
return arr[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(makeMapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFollow = () => {
|
||||||
|
const { account, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(followAccount(account.get('id')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, intl } = this.props;
|
||||||
|
|
||||||
|
let button;
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'following'])) {
|
||||||
|
button = <IconButton icon='check' title={intl.formatMessage(messages.unfollow)} active onClick={this.handleFollow} />;
|
||||||
|
} else {
|
||||||
|
button = <IconButton icon='plus' title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account follow-recommendations-account'>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||||
|
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||||
|
|
||||||
|
<DisplayName account={account} />
|
||||||
|
|
||||||
|
<div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
<div className='account__relationship'>
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||||
|
import { changeSetting, saveSettings } from 'mastodon/actions/settings';
|
||||||
|
import { requestBrowserPermission } from 'mastodon/actions/notifications';
|
||||||
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import Account from './components/account';
|
||||||
|
import Logo from 'mastodon/components/logo';
|
||||||
|
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
|
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class FollowRecommendations extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
suggestions: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch, suggestions } = this.props;
|
||||||
|
|
||||||
|
// Don't re-fetch if we're e.g. navigating backwards to this page,
|
||||||
|
// since we don't want followed accounts to disappear from the list
|
||||||
|
|
||||||
|
if (suggestions.size === 0) {
|
||||||
|
dispatch(fetchSuggestions(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDone = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { router } = this.context;
|
||||||
|
|
||||||
|
dispatch(requestBrowserPermission((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
|
||||||
|
dispatch(changeSetting(['notifications', 'alerts', 'status'], true));
|
||||||
|
dispatch(saveSettings());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.history.push('/timelines/home');
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { suggestions, isLoading } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<div className='scrollable'>
|
||||||
|
<div className='column-title'>
|
||||||
|
<Logo />
|
||||||
|
<h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3>
|
||||||
|
<p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLoading && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div>
|
||||||
|
{suggestions.map(suggestion => (
|
||||||
|
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='column-actions'>
|
||||||
|
<img src={imageGreeting} alt='' className='column-actions__background' />
|
||||||
|
<Button onClick={this.handleDone}><FormattedMessage id='follow_recommendations.done' defaultMessage='Done' /></Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -51,10 +51,12 @@ import {
|
||||||
Lists,
|
Lists,
|
||||||
Search,
|
Search,
|
||||||
Directory,
|
Directory,
|
||||||
|
FollowRecommendations,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { me } from '../../initial_state';
|
import { me } from '../../initial_state';
|
||||||
import { previewState as previewMediaState } from './components/media_modal';
|
import { previewState as previewMediaState } from './components/media_modal';
|
||||||
import { previewState as previewVideoState } from './components/video_modal';
|
import { previewState as previewVideoState } from './components/video_modal';
|
||||||
|
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
|
@ -71,6 +73,7 @@ const mapStateToProps = state => ({
|
||||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||||
|
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
|
@ -167,6 +170,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||||
|
|
||||||
|
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
|
||||||
<WrappedRoute path='/search' component={Search} content={children} />
|
<WrappedRoute path='/search' component={Search} content={children} />
|
||||||
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||||
|
|
||||||
|
@ -215,6 +219,7 @@ class UI extends React.PureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
dropdownMenuIsOpen: PropTypes.bool,
|
dropdownMenuIsOpen: PropTypes.bool,
|
||||||
layout: PropTypes.string.isRequired,
|
layout: PropTypes.string.isRequired,
|
||||||
|
firstLaunch: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -350,6 +355,12 @@ class UI extends React.PureComponent {
|
||||||
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On first launch, redirect to the follow recommendations page
|
||||||
|
if (this.props.firstLaunch) {
|
||||||
|
this.context.router.history.replace('/start');
|
||||||
|
this.props.dispatch(closeOnboarding());
|
||||||
|
}
|
||||||
|
|
||||||
this.props.dispatch(fetchMarkers());
|
this.props.dispatch(fetchMarkers());
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
|
|
@ -153,3 +153,7 @@ export function Audio () {
|
||||||
export function Directory () {
|
export function Directory () {
|
||||||
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FollowRecommendations () {
|
||||||
|
return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
|
||||||
|
}
|
||||||
|
|
|
@ -19,18 +19,18 @@ export default function suggestionsReducer(state = initialState, action) {
|
||||||
return state.set('isLoading', true);
|
return state.set('isLoading', true);
|
||||||
case SUGGESTIONS_FETCH_SUCCESS:
|
case SUGGESTIONS_FETCH_SUCCESS:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('items', fromJS(action.accounts.map(x => x.id)));
|
map.set('items', fromJS(action.suggestions.map(x => ({ ...x, account: x.account.id }))));
|
||||||
map.set('isLoading', false);
|
map.set('isLoading', false);
|
||||||
});
|
});
|
||||||
case SUGGESTIONS_FETCH_FAIL:
|
case SUGGESTIONS_FETCH_FAIL:
|
||||||
return state.set('isLoading', false);
|
return state.set('isLoading', false);
|
||||||
case SUGGESTIONS_DISMISS:
|
case SUGGESTIONS_DISMISS:
|
||||||
return state.update('items', list => list.filterNot(id => id === action.id));
|
return state.update('items', list => list.filterNot(x => x.account === action.id));
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case ACCOUNT_MUTE_SUCCESS:
|
||||||
return state.update('items', list => list.filterNot(id => id === action.relationship.id));
|
return state.update('items', list => list.filterNot(x => x.account === action.relationship.id));
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case DOMAIN_BLOCK_SUCCESS:
|
||||||
return state.update('items', list => list.filterNot(id => action.accounts.includes(id)));
|
return state.update('items', list => list.filterNot(x => action.accounts.includes(x.account)));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1307,6 +1307,29 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
|
&--with-note {
|
||||||
|
strong {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__note {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: $ui-secondary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-recommendations-account {
|
||||||
|
.icon-button {
|
||||||
|
color: $ui-primary-color;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $valid-value-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2459,6 +2482,49 @@ a.account__display-name {
|
||||||
border-color: darken($ui-base-color, 8%);
|
border-color: darken($ui-base-color, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-title {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
fill: $primary-text-color;
|
||||||
|
width: 50px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 200px;
|
||||||
|
|
||||||
|
&__background {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 220px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.compose-panel {
|
.compose-panel {
|
||||||
width: 285px;
|
width: 285px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
25
app/lib/account_reach_finder.rb
Normal file
25
app/lib/account_reach_finder.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountReachFinder
|
||||||
|
def initialize(account)
|
||||||
|
@account = account
|
||||||
|
end
|
||||||
|
|
||||||
|
def inboxes
|
||||||
|
(followers_inboxes + reporters_inboxes + relay_inboxes).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def followers_inboxes
|
||||||
|
@account.followers.inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def reporters_inboxes
|
||||||
|
Account.where(id: @account.targeted_reports.select(:account_id)).inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def relay_inboxes
|
||||||
|
Relay.enabled.pluck(:inbox_url)
|
||||||
|
end
|
||||||
|
end
|
|
@ -88,7 +88,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
resolve_thread(@status)
|
resolve_thread(@status)
|
||||||
fetch_replies(@status)
|
fetch_replies(@status)
|
||||||
check_for_spam
|
|
||||||
distribute(@status)
|
distribute(@status)
|
||||||
forward_for_reply
|
forward_for_reply
|
||||||
end
|
end
|
||||||
|
@ -498,10 +497,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
Tombstone.exists?(uri: object_uri)
|
Tombstone.exists?(uri: object_uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_for_spam
|
|
||||||
SpamCheck.perform(@status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def forward_for_reply
|
def forward_for_reply
|
||||||
return unless @status.distributable? && @json['signature'].present? && reply_to_local?
|
return unless @status.distributable? && @json['signature'].present? && reply_to_local?
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
||||||
target_accounts.each do |target_account|
|
target_accounts.each do |target_account|
|
||||||
target_statuses = target_statuses_by_account[target_account.id]
|
target_statuses = target_statuses_by_account[target_account.id]
|
||||||
|
|
||||||
|
next if target_account.suspended?
|
||||||
|
|
||||||
ReportService.new.call(
|
ReportService.new.call(
|
||||||
@account,
|
@account,
|
||||||
target_account,
|
target_account,
|
||||||
|
|
|
@ -7,7 +7,6 @@ class Admin::SystemCheck::SidekiqProcessCheck < Admin::SystemCheck::BaseCheck
|
||||||
mailers
|
mailers
|
||||||
pull
|
pull
|
||||||
scheduler
|
scheduler
|
||||||
ingress
|
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
def pass?
|
def pass?
|
||||||
|
|
|
@ -4,6 +4,8 @@ module ApplicationExtension
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
validates :website, url: true, if: :website?
|
validates :name, length: { maximum: 60 }
|
||||||
|
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
||||||
|
validates :redirect_uri, length: { maximum: 2_000 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -118,7 +118,7 @@ class Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_field(account, str, **options)
|
def format_field(account, str, **options)
|
||||||
html = account.local? ? encode_and_link_urls(str, me: true) : reformat(str)
|
html = account.local? ? encode_and_link_urls(str, me: true, with_domain: true) : reformat(str)
|
||||||
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
|
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
|
||||||
html.html_safe # rubocop:disable Rails/OutputSafety
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
end
|
end
|
||||||
|
@ -187,7 +187,7 @@ class Formatter
|
||||||
elsif entity[:hashtag]
|
elsif entity[:hashtag]
|
||||||
link_to_hashtag(entity)
|
link_to_hashtag(entity)
|
||||||
elsif entity[:screen_name]
|
elsif entity[:screen_name]
|
||||||
link_to_mention(entity, accounts)
|
link_to_mention(entity, accounts, options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -352,22 +352,37 @@ class Formatter
|
||||||
encode(entity[:url])
|
encode(entity[:url])
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_mention(entity, linkable_accounts)
|
def link_to_mention(entity, linkable_accounts, options = {})
|
||||||
acct = entity[:screen_name]
|
acct = entity[:screen_name]
|
||||||
|
|
||||||
return link_to_account(acct) unless linkable_accounts
|
return link_to_account(acct, options) unless linkable_accounts
|
||||||
|
|
||||||
account = linkable_accounts.find { |item| TagManager.instance.same_acct?(item.acct, acct) }
|
same_username_hits = 0
|
||||||
account ? mention_html(account) : "@#{encode(acct)}"
|
account = nil
|
||||||
|
username, domain = acct.split('@')
|
||||||
|
domain = nil if TagManager.instance.local_domain?(domain)
|
||||||
|
|
||||||
|
linkable_accounts.each do |item|
|
||||||
|
same_username = item.username.casecmp(username).zero?
|
||||||
|
same_domain = item.domain.nil? ? domain.nil? : item.domain.casecmp(domain)&.zero?
|
||||||
|
|
||||||
|
if same_username && !same_domain
|
||||||
|
same_username_hits += 1
|
||||||
|
elsif same_username && same_domain
|
||||||
|
account = item
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_account(acct)
|
account ? mention_html(account, with_domain: same_username_hits.positive? || options[:with_domain]) : "@#{encode(acct)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_account(acct, options = {})
|
||||||
username, domain = acct.split('@')
|
username, domain = acct.split('@')
|
||||||
|
|
||||||
domain = nil if TagManager.instance.local_domain?(domain)
|
domain = nil if TagManager.instance.local_domain?(domain)
|
||||||
account = EntityCache.instance.mention(username, domain)
|
account = EntityCache.instance.mention(username, domain)
|
||||||
|
|
||||||
account ? mention_html(account) : "@#{encode(acct)}"
|
account ? mention_html(account, with_domain: options[:with_domain]) : "@#{encode(acct)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_hashtag(entity)
|
def link_to_hashtag(entity)
|
||||||
|
@ -388,7 +403,7 @@ class Formatter
|
||||||
"<a href=\"#{encode(tag_url(tag))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
|
"<a href=\"#{encode(tag_url(tag))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def mention_html(account)
|
def mention_html(account, with_domain: false)
|
||||||
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"
|
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(with_domain ? account.pretty_acct : account.username)}</span></a></span>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,10 +28,14 @@ class PotentialFriendshipTracker
|
||||||
redis.zrem("interactions:#{account_id}", target_account_id)
|
redis.zrem("interactions:#{account_id}", target_account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(account_id, limit: 20, offset: 0)
|
def get(account, limit)
|
||||||
account_ids = redis.zrevrange("interactions:#{account_id}", offset, limit)
|
account_ids = redis.zrevrange("interactions:#{account.id}", 0, limit)
|
||||||
return [] if account_ids.empty?
|
|
||||||
Account.searchable.where(id: account_ids)
|
return [] if account_ids.empty? || limit < 1
|
||||||
|
|
||||||
|
accounts = Account.searchable.where(id: account_ids).index_by(&:id)
|
||||||
|
|
||||||
|
account_ids.map { |id| accounts[id.to_i] }.compact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,198 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class SpamCheck
|
|
||||||
include Redisable
|
|
||||||
include ActionView::Helpers::TextHelper
|
|
||||||
|
|
||||||
# Threshold over which two Nilsimsa values are considered
|
|
||||||
# to refer to the same text
|
|
||||||
NILSIMSA_COMPARE_THRESHOLD = 95
|
|
||||||
|
|
||||||
# Nilsimsa doesn't work well on small inputs, so below
|
|
||||||
# this size, we check only for exact matches with MD5
|
|
||||||
NILSIMSA_MIN_SIZE = 10
|
|
||||||
|
|
||||||
# How long to keep the trail of digests between updates,
|
|
||||||
# there is no reason to store it forever
|
|
||||||
EXPIRE_SET_AFTER = 1.week.seconds
|
|
||||||
|
|
||||||
# How many digests to keep in an account's trail. If it's
|
|
||||||
# too small, spam could rotate around different message templates
|
|
||||||
MAX_TRAIL_SIZE = 10
|
|
||||||
|
|
||||||
# How many detected duplicates to allow through before
|
|
||||||
# considering the message as spam
|
|
||||||
THRESHOLD = 5
|
|
||||||
|
|
||||||
def initialize(status)
|
|
||||||
@account = status.account
|
|
||||||
@status = status
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip?
|
|
||||||
disabled? || already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply?
|
|
||||||
end
|
|
||||||
|
|
||||||
def spam?
|
|
||||||
if insufficient_data?
|
|
||||||
false
|
|
||||||
elsif nilsimsa?
|
|
||||||
digests_over_threshold?('nilsimsa') { |_, other_digest| nilsimsa_compare_value(digest, other_digest) >= NILSIMSA_COMPARE_THRESHOLD }
|
|
||||||
else
|
|
||||||
digests_over_threshold?('md5') { |_, other_digest| other_digest == digest }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def flag!
|
|
||||||
auto_report_status!
|
|
||||||
end
|
|
||||||
|
|
||||||
def remember!
|
|
||||||
# The scores in sorted sets don't actually have enough bits to hold an exact
|
|
||||||
# value of our snowflake IDs, so we use it only for its ordering property. To
|
|
||||||
# get the correct status ID back, we have to save it in the string value
|
|
||||||
|
|
||||||
redis.zadd(redis_key, @status.id, digest_with_algorithm)
|
|
||||||
redis.zremrangebyrank(redis_key, 0, -(MAX_TRAIL_SIZE + 1))
|
|
||||||
redis.expire(redis_key, EXPIRE_SET_AFTER)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset!
|
|
||||||
redis.del(redis_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
def hashable_text
|
|
||||||
return @hashable_text if defined?(@hashable_text)
|
|
||||||
|
|
||||||
@hashable_text = @status.text
|
|
||||||
@hashable_text = remove_mentions(@hashable_text)
|
|
||||||
@hashable_text = strip_tags(@hashable_text) unless @status.local?
|
|
||||||
@hashable_text = normalize_unicode(@status.spoiler_text + ' ' + @hashable_text)
|
|
||||||
@hashable_text = remove_whitespace(@hashable_text)
|
|
||||||
end
|
|
||||||
|
|
||||||
def insufficient_data?
|
|
||||||
hashable_text.blank?
|
|
||||||
end
|
|
||||||
|
|
||||||
def digest
|
|
||||||
@digest ||= begin
|
|
||||||
if nilsimsa?
|
|
||||||
Nilsimsa.new(hashable_text).hexdigest
|
|
||||||
else
|
|
||||||
Digest::MD5.hexdigest(hashable_text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def digest_with_algorithm
|
|
||||||
if nilsimsa?
|
|
||||||
['nilsimsa', digest, @status.id].join(':')
|
|
||||||
else
|
|
||||||
['md5', digest, @status.id].join(':')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def perform(status)
|
|
||||||
spam_check = new(status)
|
|
||||||
|
|
||||||
return if spam_check.skip?
|
|
||||||
|
|
||||||
if spam_check.spam?
|
|
||||||
spam_check.flag!
|
|
||||||
else
|
|
||||||
spam_check.remember!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def disabled?
|
|
||||||
!Setting.spam_check_enabled
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_mentions(text)
|
|
||||||
return text.gsub(Account::MENTION_RE, '') if @status.local?
|
|
||||||
|
|
||||||
Nokogiri::HTML.fragment(text).tap do |html|
|
|
||||||
mentions = @status.mentions.map { |mention| ActivityPub::TagManager.instance.url_for(mention.account) }
|
|
||||||
|
|
||||||
html.traverse do |element|
|
|
||||||
element.unlink if element.name == 'a' && mentions.include?(element['href'])
|
|
||||||
end
|
|
||||||
end.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_unicode(text)
|
|
||||||
text.unicode_normalize(:nfkc).downcase
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_whitespace(text)
|
|
||||||
text.gsub(/\s+/, ' ').strip
|
|
||||||
end
|
|
||||||
|
|
||||||
def auto_report_status!
|
|
||||||
status_ids = Status.where(visibility: %i(public unlisted)).where(id: matching_status_ids).pluck(:id) + [@status.id] if @status.distributable?
|
|
||||||
ReportService.new.call(Account.representative, @account, status_ids: status_ids, comment: I18n.t('spam_check.spam_detected'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def already_flagged?
|
|
||||||
@account.silenced? || @account.targeted_reports.unresolved.where(account_id: -99).exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def trusted?
|
|
||||||
@account.trust_level > Account::TRUST_LEVELS[:untrusted] || (@account.local? && @account.user_staff?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def no_unsolicited_mentions?
|
|
||||||
@status.mentions.all? { |mention| mention.silent? || (!@account.local? && !mention.account.local?) || mention.account.following?(@account) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def solicited_reply?
|
|
||||||
!@status.thread.nil? && @status.thread.mentions.where(account: @account).exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
def nilsimsa_compare_value(first, second)
|
|
||||||
first = [first].pack('H*')
|
|
||||||
second = [second].pack('H*')
|
|
||||||
bits = 0
|
|
||||||
|
|
||||||
0.upto(31) do |i|
|
|
||||||
bits += Nilsimsa::POPC[255 & (first[i].ord ^ second[i].ord)].ord
|
|
||||||
end
|
|
||||||
|
|
||||||
128 - bits # -128 <= Nilsimsa Compare Value <= 128
|
|
||||||
end
|
|
||||||
|
|
||||||
def nilsimsa?
|
|
||||||
hashable_text.size > NILSIMSA_MIN_SIZE
|
|
||||||
end
|
|
||||||
|
|
||||||
def other_digests
|
|
||||||
redis.zrange(redis_key, 0, -1)
|
|
||||||
end
|
|
||||||
|
|
||||||
def digests_over_threshold?(filter_algorithm)
|
|
||||||
other_digests.select do |record|
|
|
||||||
algorithm, other_digest, status_id = record.split(':')
|
|
||||||
|
|
||||||
next unless algorithm == filter_algorithm
|
|
||||||
|
|
||||||
yield algorithm, other_digest, status_id
|
|
||||||
end.size >= THRESHOLD
|
|
||||||
end
|
|
||||||
|
|
||||||
def matching_status_ids
|
|
||||||
if nilsimsa?
|
|
||||||
other_digests.filter_map { |record| record.split(':')[2] if record.start_with?('nilsimsa') && nilsimsa_compare_value(digest, record.split(':')[1]) >= NILSIMSA_COMPARE_THRESHOLD }
|
|
||||||
else
|
|
||||||
other_digests.filter_map { |record| record.split(':')[2] if record.start_with?('md5') && record.split(':')[1] == digest }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def redis_key
|
|
||||||
@redis_key ||= "spam_check:#{@account.id}"
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -6,11 +6,22 @@ class StatusReachFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
Account.where(id: reached_account_ids).inboxes
|
(reached_account_inboxes + followers_inboxes + relay_inboxes).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def reached_account_inboxes
|
||||||
|
# When the status is a reblog, there are no interactions with it
|
||||||
|
# directly, we assume all interactions are with the original one
|
||||||
|
|
||||||
|
if @status.reblog?
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
Account.where(id: reached_account_ids).inboxes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def reached_account_ids
|
def reached_account_ids
|
||||||
[
|
[
|
||||||
replied_to_account_id,
|
replied_to_account_id,
|
||||||
|
@ -49,4 +60,16 @@ class StatusReachFinder
|
||||||
def replies_account_ids
|
def replies_account_ids
|
||||||
@status.replies.pluck(:account_id)
|
@status.replies.pluck(:account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def followers_inboxes
|
||||||
|
@status.account.followers.inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def relay_inboxes
|
||||||
|
if @status.public_visibility?
|
||||||
|
Relay.enabled.pluck(:inbox_url)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,14 +22,6 @@ class TagManager
|
||||||
uri.normalized_host
|
uri.normalized_host
|
||||||
end
|
end
|
||||||
|
|
||||||
def same_acct?(canonical, needle)
|
|
||||||
return true if canonical.casecmp(needle).zero?
|
|
||||||
|
|
||||||
username, domain = needle.split('@')
|
|
||||||
|
|
||||||
local_domain?(domain) && canonical.casecmp(username).zero?
|
|
||||||
end
|
|
||||||
|
|
||||||
def local_url?(url)
|
def local_url?(url)
|
||||||
uri = Addressable::URI.parse(url).normalize
|
uri = Addressable::URI.parse(url).normalize
|
||||||
return false unless uri.host
|
return false unless uri.host
|
||||||
|
|
|
@ -114,6 +114,7 @@ class Account < ApplicationRecord
|
||||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
|
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
|
||||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
||||||
|
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
||||||
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
|
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
|
||||||
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
|
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
|
||||||
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
|
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
|
||||||
|
@ -238,6 +239,7 @@ class Account < ApplicationRecord
|
||||||
transaction do
|
transaction do
|
||||||
create_deletion_request!
|
create_deletion_request!
|
||||||
update!(suspended_at: date, suspension_origin: origin)
|
update!(suspended_at: date, suspension_origin: origin)
|
||||||
|
create_canonical_email_block!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -245,6 +247,7 @@ class Account < ApplicationRecord
|
||||||
transaction do
|
transaction do
|
||||||
deletion_request&.destroy!
|
deletion_request&.destroy!
|
||||||
update!(suspended_at: nil, suspension_origin: nil)
|
update!(suspended_at: nil, suspension_origin: nil)
|
||||||
|
destroy_canonical_email_block!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -365,7 +368,7 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def excluded_from_timeline_account_ids
|
def excluded_from_timeline_account_ids
|
||||||
Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
|
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def excluded_from_timeline_domains
|
def excluded_from_timeline_domains
|
||||||
|
@ -570,4 +573,16 @@ class Account < ApplicationRecord
|
||||||
def clean_feed_manager
|
def clean_feed_manager
|
||||||
FeedManager.instance.clean_feeds!(:home, [id])
|
FeedManager.instance.clean_feeds!(:home, [id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_canonical_email_block!
|
||||||
|
return unless local? && user_email.present?
|
||||||
|
|
||||||
|
CanonicalEmailBlock.create(reference_account: self, email: user_email)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_canonical_email_block!
|
||||||
|
return unless local?
|
||||||
|
|
||||||
|
CanonicalEmailBlock.where(reference_account: self).delete_all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
17
app/models/account_suggestions.rb
Normal file
17
app/models/account_suggestions.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountSuggestions
|
||||||
|
class Suggestion < ActiveModelSerializers::Model
|
||||||
|
attributes :account, :source
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get(account, limit)
|
||||||
|
suggestions = PotentialFriendshipTracker.get(account, limit).map { |target_account| Suggestion.new(account: target_account, source: :past_interaction) }
|
||||||
|
suggestions.concat(FollowRecommendation.get(account, limit - suggestions.size, suggestions.map { |suggestion| suggestion.account.id }).map { |target_account| Suggestion.new(account: target_account, source: :global) }) if suggestions.size < limit
|
||||||
|
suggestions
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.remove(account, target_account_id)
|
||||||
|
PotentialFriendshipTracker.remove(account.id, target_account_id)
|
||||||
|
end
|
||||||
|
end
|
25
app/models/account_summary.rb
Normal file
25
app/models/account_summary.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: account_summaries
|
||||||
|
#
|
||||||
|
# account_id :bigint(8) primary key
|
||||||
|
# language :string
|
||||||
|
# sensitive :boolean
|
||||||
|
#
|
||||||
|
|
||||||
|
class AccountSummary < ApplicationRecord
|
||||||
|
self.primary_key = :account_id
|
||||||
|
|
||||||
|
scope :safe, -> { where(sensitive: false) }
|
||||||
|
scope :localized, ->(locale) { where(language: locale) }
|
||||||
|
scope :filtered, -> { joins(arel_table.join(FollowRecommendationSuppression.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:account_id].eq(FollowRecommendationSuppression.arel_table[:account_id])).join_sources).where(FollowRecommendationSuppression.arel_table[:id].eq(nil)) }
|
||||||
|
|
||||||
|
def self.refresh
|
||||||
|
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def readonly?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
27
app/models/canonical_email_block.rb
Normal file
27
app/models/canonical_email_block.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: canonical_email_blocks
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# canonical_email_hash :string default(""), not null
|
||||||
|
# reference_account_id :bigint(8) not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class CanonicalEmailBlock < ApplicationRecord
|
||||||
|
include EmailHelper
|
||||||
|
|
||||||
|
belongs_to :reference_account, class_name: 'Account'
|
||||||
|
|
||||||
|
validates :canonical_email_hash, presence: true
|
||||||
|
|
||||||
|
def email=(email)
|
||||||
|
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.block?(email)
|
||||||
|
where(canonical_email_hash: email_to_canonical_email_hash(email)).exists?
|
||||||
|
end
|
||||||
|
end
|
|
@ -63,5 +63,8 @@ module AccountAssociations
|
||||||
|
|
||||||
# Account deletion requests
|
# Account deletion requests
|
||||||
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
||||||
|
|
||||||
|
# Follow recommendations
|
||||||
|
has_one :follow_recommendation_suppression, inverse_of: :account, dependent: :destroy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
39
app/models/follow_recommendation.rb
Normal file
39
app/models/follow_recommendation.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: follow_recommendations
|
||||||
|
#
|
||||||
|
# account_id :bigint(8) primary key
|
||||||
|
# rank :decimal(, )
|
||||||
|
# reason :text is an Array
|
||||||
|
#
|
||||||
|
|
||||||
|
class FollowRecommendation < ApplicationRecord
|
||||||
|
self.primary_key = :account_id
|
||||||
|
|
||||||
|
belongs_to :account_summary, foreign_key: :account_id
|
||||||
|
belongs_to :account, foreign_key: :account_id
|
||||||
|
|
||||||
|
scope :safe, -> { joins(:account_summary).merge(AccountSummary.safe) }
|
||||||
|
scope :localized, ->(locale) { joins(:account_summary).merge(AccountSummary.localized(locale)) }
|
||||||
|
scope :filtered, -> { joins(:account_summary).merge(AccountSummary.filtered) }
|
||||||
|
|
||||||
|
def readonly?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get(account, limit, exclude_account_ids = [])
|
||||||
|
account_ids = Redis.current.zrevrange("follow_recommendations:#{account.user_locale}", 0, -1).map(&:to_i) - exclude_account_ids - [account.id]
|
||||||
|
|
||||||
|
return [] if account_ids.empty? || limit < 1
|
||||||
|
|
||||||
|
accounts = Account.followable_by(account)
|
||||||
|
.not_excluded_by_account(account)
|
||||||
|
.not_domain_blocked_by_account(account)
|
||||||
|
.where(id: account_ids)
|
||||||
|
.limit(limit)
|
||||||
|
.index_by(&:id)
|
||||||
|
|
||||||
|
account_ids.map { |id| accounts[id] }.compact
|
||||||
|
end
|
||||||
|
end
|
26
app/models/follow_recommendation_filter.rb
Normal file
26
app/models/follow_recommendation_filter.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FollowRecommendationFilter
|
||||||
|
KEYS = %i(
|
||||||
|
language
|
||||||
|
status
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
attr_reader :params, :language
|
||||||
|
|
||||||
|
def initialize(params)
|
||||||
|
@language = params.delete('language') || I18n.locale
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def results
|
||||||
|
if params['status'] == 'suppressed'
|
||||||
|
Account.joins(:follow_recommendation_suppression).order(FollowRecommendationSuppression.arel_table[:id].desc).to_a
|
||||||
|
else
|
||||||
|
account_ids = Redis.current.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i)
|
||||||
|
accounts = Account.where(id: account_ids).index_by(&:id)
|
||||||
|
|
||||||
|
account_ids.map { |id| accounts[id] }.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
app/models/follow_recommendation_suppression.rb
Normal file
28
app/models/follow_recommendation_suppression.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: follow_recommendation_suppressions
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class FollowRecommendationSuppression < ApplicationRecord
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
after_commit :remove_follow_recommendations, on: :create
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_follow_recommendations
|
||||||
|
redis.pipelined do
|
||||||
|
I18n.available_locales.each do |locale|
|
||||||
|
redis.zrem("follow_recommendations:#{locale}", account_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,6 +21,10 @@ class Form::AccountBatch
|
||||||
approve!
|
approve!
|
||||||
when 'reject'
|
when 'reject'
|
||||||
reject!
|
reject!
|
||||||
|
when 'suppress_follow_recommendation'
|
||||||
|
suppress_follow_recommendation!
|
||||||
|
when 'unsuppress_follow_recommendation'
|
||||||
|
unsuppress_follow_recommendation!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -79,4 +83,18 @@ class Form::AccountBatch
|
||||||
records.each { |account| authorize(account.user, :reject?) }
|
records.each { |account| authorize(account.user, :reject?) }
|
||||||
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def suppress_follow_recommendation!
|
||||||
|
authorize(:follow_recommendation, :suppress?)
|
||||||
|
|
||||||
|
accounts.each do |account|
|
||||||
|
FollowRecommendationSuppression.create(account: account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsuppress_follow_recommendation!
|
||||||
|
authorize(:follow_recommendation, :unsuppress?)
|
||||||
|
|
||||||
|
FollowRecommendationSuppression.where(account_id: account_ids).destroy_all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,6 @@ class Form::AdminSettings
|
||||||
mascot
|
mascot
|
||||||
show_reblogs_in_public_timelines
|
show_reblogs_in_public_timelines
|
||||||
show_replies_in_public_timelines
|
show_replies_in_public_timelines
|
||||||
spam_check_enabled
|
|
||||||
trends
|
trends
|
||||||
trendable_by_default
|
trendable_by_default
|
||||||
show_domain_blocks
|
show_domain_blocks
|
||||||
|
@ -59,7 +58,6 @@ class Form::AdminSettings
|
||||||
enable_keybase
|
enable_keybase
|
||||||
show_reblogs_in_public_timelines
|
show_reblogs_in_public_timelines
|
||||||
show_replies_in_public_timelines
|
show_replies_in_public_timelines
|
||||||
spam_check_enabled
|
|
||||||
trends
|
trends
|
||||||
trendable_by_default
|
trendable_by_default
|
||||||
noindex
|
noindex
|
||||||
|
|
|
@ -24,81 +24,101 @@ class Web::PushSubscription < ApplicationRecord
|
||||||
validates :key_p256dh, presence: true
|
validates :key_p256dh, presence: true
|
||||||
validates :key_auth, presence: true
|
validates :key_auth, presence: true
|
||||||
|
|
||||||
def push(notification)
|
delegate :locale, to: :associated_user
|
||||||
I18n.with_locale(associated_user&.locale || I18n.default_locale) do
|
|
||||||
push_payload(payload_for_notification(notification), 48.hours.seconds)
|
def encrypt(payload)
|
||||||
|
Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def audience
|
||||||
|
@audience ||= Addressable::URI.parse(endpoint).normalized_site
|
||||||
|
end
|
||||||
|
|
||||||
|
def crypto_key_header
|
||||||
|
p256ecdsa = vapid_key.public_key_for_push_header
|
||||||
|
|
||||||
|
"p256ecdsa=#{p256ecdsa}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorization_header
|
||||||
|
jwt = JWT.encode({ aud: audience, exp: 24.hours.from_now.to_i, sub: "mailto:#{contact_email}" }, vapid_key.curve, 'ES256', typ: 'JWT')
|
||||||
|
|
||||||
|
"WebPush #{jwt}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def pushable?(notification)
|
def pushable?(notification)
|
||||||
data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s])
|
policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
def associated_user
|
def associated_user
|
||||||
return @associated_user if defined?(@associated_user)
|
return @associated_user if defined?(@associated_user)
|
||||||
|
|
||||||
@associated_user = if user_id.nil?
|
@associated_user = begin
|
||||||
|
if user_id.nil?
|
||||||
session_activation.user
|
session_activation.user
|
||||||
else
|
else
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def associated_access_token
|
def associated_access_token
|
||||||
return @associated_access_token if defined?(@associated_access_token)
|
return @associated_access_token if defined?(@associated_access_token)
|
||||||
|
|
||||||
@associated_access_token = if access_token_id.nil?
|
@associated_access_token = begin
|
||||||
|
if access_token_id.nil?
|
||||||
find_or_create_access_token.token
|
find_or_create_access_token.token
|
||||||
else
|
else
|
||||||
access_token.token
|
access_token.token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def unsubscribe_for(application_id, resource_owner)
|
def unsubscribe_for(application_id, resource_owner)
|
||||||
access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil)
|
access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil).pluck(:id)
|
||||||
.pluck(:id)
|
|
||||||
|
|
||||||
where(access_token_id: access_token_ids).delete_all
|
where(access_token_id: access_token_ids).delete_all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def push_payload(message, ttl = 5.minutes.seconds)
|
|
||||||
Webpush.payload_send(
|
|
||||||
message: Oj.dump(message),
|
|
||||||
endpoint: endpoint,
|
|
||||||
p256dh: key_p256dh,
|
|
||||||
auth: key_auth,
|
|
||||||
ttl: ttl,
|
|
||||||
ssl_timeout: 10,
|
|
||||||
open_timeout: 10,
|
|
||||||
read_timeout: 10,
|
|
||||||
vapid: {
|
|
||||||
subject: "mailto:#{::Setting.site_contact_email}",
|
|
||||||
private_key: Rails.configuration.x.vapid_private_key,
|
|
||||||
public_key: Rails.configuration.x.vapid_public_key,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def payload_for_notification(notification)
|
|
||||||
ActiveModelSerializers::SerializableResource.new(
|
|
||||||
notification,
|
|
||||||
serializer: Web::NotificationSerializer,
|
|
||||||
scope: self,
|
|
||||||
scope_name: :current_push_subscription
|
|
||||||
).as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_or_create_access_token
|
def find_or_create_access_token
|
||||||
Doorkeeper::AccessToken.find_or_create_for(
|
Doorkeeper::AccessToken.find_or_create_for(
|
||||||
application: Doorkeeper::Application.find_by(superapp: true),
|
application: Doorkeeper::Application.find_by(superapp: true),
|
||||||
resource_owner: session_activation.user_id,
|
resource_owner: user_id || session_activation.user_id,
|
||||||
scopes: Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
|
scopes: Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
|
||||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
||||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
|
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vapid_key
|
||||||
|
@vapid_key ||= Webpush::VapidKey.from_keys(Rails.configuration.x.vapid_public_key, Rails.configuration.x.vapid_private_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_email
|
||||||
|
@contact_email ||= ::Setting.site_contact_email
|
||||||
|
end
|
||||||
|
|
||||||
|
def alert_enabled_for_notification_type?(notification)
|
||||||
|
truthy?(data&.dig('alerts', notification.type.to_s))
|
||||||
|
end
|
||||||
|
|
||||||
|
def policy_allows_notification?(notification)
|
||||||
|
case data&.dig('policy')
|
||||||
|
when nil, 'all'
|
||||||
|
true
|
||||||
|
when 'none'
|
||||||
|
false
|
||||||
|
when 'followed'
|
||||||
|
notification.account.following?(notification.from_account)
|
||||||
|
when 'follower'
|
||||||
|
notification.from_account.following?(notification.account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def truthy?(val)
|
||||||
|
ActiveModel::Type::Boolean.new.cast(val)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
15
app/policies/follow_recommendation_policy.rb
Normal file
15
app/policies/follow_recommendation_policy.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FollowRecommendationPolicy < ApplicationPolicy
|
||||||
|
def show?
|
||||||
|
staff?
|
||||||
|
end
|
||||||
|
|
||||||
|
def suppress?
|
||||||
|
staff?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsuppress?
|
||||||
|
staff?
|
||||||
|
end
|
||||||
|
end
|
7
app/serializers/rest/suggestion_serializer.rb
Normal file
7
app/serializers/rest/suggestion_serializer.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::SuggestionSerializer < ActiveModel::Serializer
|
||||||
|
attributes :source
|
||||||
|
|
||||||
|
has_one :account, serializer: REST::AccountSerializer
|
||||||
|
end
|
|
@ -43,7 +43,6 @@ class ProcessMentionsService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
status.save!
|
status.save!
|
||||||
check_for_spam(status)
|
|
||||||
|
|
||||||
mentions.each { |mention| create_notification(mention) }
|
mentions.each { |mention| create_notification(mention) }
|
||||||
end
|
end
|
||||||
|
@ -72,8 +71,4 @@ class ProcessMentionsService < BaseService
|
||||||
def resolve_account_service
|
def resolve_account_service
|
||||||
ResolveAccountService.new
|
ResolveAccountService.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_for_spam(status)
|
|
||||||
SpamCheck.perform(status)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,10 +27,7 @@ class RemoveStatusService < BaseService
|
||||||
# original object being removed implicitly removes reblogs
|
# original object being removed implicitly removes reblogs
|
||||||
# of it. The Delete activity of the original is forwarded
|
# of it. The Delete activity of the original is forwarded
|
||||||
# separately.
|
# separately.
|
||||||
if @account.local? && !@options[:original_removed]
|
remove_from_remote_reach if @account.local? && !@options[:original_removed]
|
||||||
remove_from_remote_followers
|
|
||||||
remove_from_remote_reach
|
|
||||||
end
|
|
||||||
|
|
||||||
# Since reblogs don't mention anyone, don't get reblogged,
|
# Since reblogs don't mention anyone, don't get reblogged,
|
||||||
# favourited and don't contain their own media attachments
|
# favourited and don't contain their own media attachments
|
||||||
|
@ -42,7 +39,6 @@ class RemoveStatusService < BaseService
|
||||||
remove_from_public
|
remove_from_public
|
||||||
remove_from_media if @status.media_attachments.any?
|
remove_from_media if @status.media_attachments.any?
|
||||||
remove_from_direct if status.direct_visibility?
|
remove_from_direct if status.direct_visibility?
|
||||||
remove_from_spam_check
|
|
||||||
remove_media
|
remove_media
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -85,13 +81,10 @@ class RemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_remote_reach
|
def remove_from_remote_reach
|
||||||
return if @status.reblog?
|
# Followers, relays, people who got mentioned in the status,
|
||||||
|
# or who reblogged it from someone else might not follow
|
||||||
# People who got mentioned in the status, or who
|
# the author and wouldn't normally receive the delete
|
||||||
# reblogged it from someone else might not follow
|
# notification - so here, we explicitly send it to them
|
||||||
# the author and wouldn't normally receive the
|
|
||||||
# delete notification - so here, we explicitly
|
|
||||||
# send it to them
|
|
||||||
|
|
||||||
status_reach_finder = StatusReachFinder.new(@status)
|
status_reach_finder = StatusReachFinder.new(@status)
|
||||||
|
|
||||||
|
@ -100,24 +93,6 @@ class RemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_remote_followers
|
|
||||||
ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url|
|
|
||||||
[signed_activity_json, @account.id, inbox_url]
|
|
||||||
end
|
|
||||||
|
|
||||||
relay! if relayable?
|
|
||||||
end
|
|
||||||
|
|
||||||
def relayable?
|
|
||||||
@status.public_visibility?
|
|
||||||
end
|
|
||||||
|
|
||||||
def relay!
|
|
||||||
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
|
|
||||||
[signed_activity_json, @account.id, inbox_url]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def signed_activity_json
|
def signed_activity_json
|
||||||
@signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
|
@signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
|
||||||
end
|
end
|
||||||
|
@ -171,10 +146,6 @@ class RemoveStatusService < BaseService
|
||||||
@status.media_attachments.destroy_all
|
@status.media_attachments.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_spam_check
|
|
||||||
redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "distribute:#{@status.id}" }
|
{ redis: Redis.current, key: "distribute:#{@status.id}" }
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,8 @@ class ReportService < BaseService
|
||||||
@comment = options.delete(:comment) || ''
|
@comment = options.delete(:comment) || ''
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
raise ActiveRecord::RecordNotFound if @target_account.suspended?
|
||||||
|
|
||||||
create_report!
|
create_report!
|
||||||
notify_staff!
|
notify_staff!
|
||||||
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||||
|
|
|
@ -42,7 +42,13 @@ class SuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def distribute_update_actor!
|
def distribute_update_actor!
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id) if @account.local?
|
return unless @account.local?
|
||||||
|
|
||||||
|
account_reach_finder = AccountReachFinder.new(@account)
|
||||||
|
|
||||||
|
ActivityPub::DeliveryWorker.push_bulk(account_reach_finder.inboxes) do |inbox_url|
|
||||||
|
[signed_activity_json, @account.id, inbox_url]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unmerge_from_home_timelines!
|
def unmerge_from_home_timelines!
|
||||||
|
@ -90,4 +96,8 @@ class SuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def signed_activity_json
|
||||||
|
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,7 @@ class UnsuspendAccountService < BaseService
|
||||||
merge_into_home_timelines!
|
merge_into_home_timelines!
|
||||||
merge_into_list_timelines!
|
merge_into_list_timelines!
|
||||||
publish_media_attachments!
|
publish_media_attachments!
|
||||||
|
distribute_update_actor!
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -36,6 +37,16 @@ class UnsuspendAccountService < BaseService
|
||||||
# @account would now be nil.
|
# @account would now be nil.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def distribute_update_actor!
|
||||||
|
return unless @account.local?
|
||||||
|
|
||||||
|
account_reach_finder = AccountReachFinder.new(@account)
|
||||||
|
|
||||||
|
ActivityPub::DeliveryWorker.push_bulk(account_reach_finder.inboxes) do |inbox_url|
|
||||||
|
[signed_activity_json, @account.id, inbox_url]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def merge_into_home_timelines!
|
def merge_into_home_timelines!
|
||||||
@account.followers_for_local_distribution.find_each do |follower|
|
@account.followers_for_local_distribution.find_each do |follower|
|
||||||
FeedManager.instance.merge_into_home(@account, follower)
|
FeedManager.instance.merge_into_home(@account, follower)
|
||||||
|
@ -81,4 +92,8 @@ class UnsuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def signed_activity_json
|
||||||
|
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,26 +6,25 @@ class BlacklistedEmailValidator < ActiveModel::Validator
|
||||||
|
|
||||||
@email = user.email
|
@email = user.email
|
||||||
|
|
||||||
user.errors.add(:email, :blocked) if blocked_email?
|
user.errors.add(:email, :blocked) if blocked_email_provider?
|
||||||
|
user.errors.add(:email, :taken) if blocked_canonical_email?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def blocked_email?
|
def blocked_email_provider?
|
||||||
on_blacklist? || not_on_whitelist?
|
disallowed_through_email_domain_block? || disallowed_through_configuration? || not_allowed_through_configuration?
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_blacklist?
|
def blocked_canonical_email?
|
||||||
return true if EmailDomainBlock.block?(@email)
|
CanonicalEmailBlock.block?(@email)
|
||||||
return false if Rails.configuration.x.email_domains_blacklist.blank?
|
|
||||||
|
|
||||||
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
|
|
||||||
regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
|
|
||||||
|
|
||||||
regexp.match?(@email)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def not_on_whitelist?
|
def disallowed_through_email_domain_block?
|
||||||
|
EmailDomainBlock.block?(@email)
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_allowed_through_configuration?
|
||||||
return false if Rails.configuration.x.email_domains_whitelist.blank?
|
return false if Rails.configuration.x.email_domains_whitelist.blank?
|
||||||
|
|
||||||
domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.')
|
domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.')
|
||||||
|
@ -33,4 +32,13 @@ class BlacklistedEmailValidator < ActiveModel::Validator
|
||||||
|
|
||||||
@email !~ regexp
|
@email !~ regexp
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disallowed_through_configuration?
|
||||||
|
return false if Rails.configuration.x.email_domains_blacklist.blank?
|
||||||
|
|
||||||
|
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
|
||||||
|
regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
|
||||||
|
|
||||||
|
regexp.match?(@email)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -79,8 +79,6 @@
|
||||||
= feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled)
|
= feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled)
|
||||||
%li
|
%li
|
||||||
= feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
|
= feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
|
||||||
%li
|
|
||||||
= feature_hint(link_to(t('admin.dashboard.feature_spam_check'), edit_admin_settings_path), @spam_check_enabled)
|
|
||||||
|
|
||||||
.dashboard__widgets__versions
|
.dashboard__widgets__versions
|
||||||
%div
|
%div
|
||||||
|
|
20
app/views/admin/follow_recommendations/_account.html.haml
Normal file
20
app/views/admin/follow_recommendations/_account.html.haml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.batch-table__row
|
||||||
|
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||||
|
= f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
|
||||||
|
.batch-table__row__content.batch-table__row__content--unpadded
|
||||||
|
%table.accounts-table
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td= account_link_to account
|
||||||
|
%td.accounts-table__count.optional
|
||||||
|
= number_to_human account.statuses_count, strip_insignificant_zeros: true
|
||||||
|
%small= t('accounts.posts', count: account.statuses_count).downcase
|
||||||
|
%td.accounts-table__count.optional
|
||||||
|
= number_to_human account.followers_count, strip_insignificant_zeros: true
|
||||||
|
%small= t('accounts.followers', count: account.followers_count).downcase
|
||||||
|
%td.accounts-table__count
|
||||||
|
- if account.last_status_at.present?
|
||||||
|
%time.time-ago{ datetime: account.last_status_at.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at
|
||||||
|
- else
|
||||||
|
\-
|
||||||
|
%small= t('accounts.last_active')
|
38
app/views/admin/follow_recommendations/show.html.haml
Normal file
38
app/views/admin/follow_recommendations/show.html.haml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.follow_recommendations.title')
|
||||||
|
|
||||||
|
%p= t('admin.follow_recommendations.description_html')
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
= form_tag admin_follow_recommendations_path, method: 'GET', class: 'simple_form' do
|
||||||
|
.filters
|
||||||
|
.filter-subset.filter-subset--with-select
|
||||||
|
%strong= t('admin.follow_recommendations.language')
|
||||||
|
.input.select.optional
|
||||||
|
= select_tag :language, options_for_select(I18n.available_locales.map { |key| [human_locale(key), key]}, @language)
|
||||||
|
|
||||||
|
.filter-subset
|
||||||
|
%strong= t('admin.follow_recommendations.status')
|
||||||
|
%ul
|
||||||
|
%li= filter_link_to t('admin.accounts.moderation.active'), status: nil
|
||||||
|
%li= filter_link_to t('admin.follow_recommendations.suppressed'), status: 'suppressed'
|
||||||
|
|
||||||
|
= form_for(@form, url: admin_follow_recommendations_path, method: :patch) do |f|
|
||||||
|
- RelationshipFilter::KEYS.each do |key|
|
||||||
|
= hidden_field_tag key, params[key] if params[key].present?
|
||||||
|
|
||||||
|
.batch-table
|
||||||
|
.batch-table__toolbar
|
||||||
|
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||||
|
= check_box_tag :batch_checkbox_all, nil, false
|
||||||
|
.batch-table__toolbar__actions
|
||||||
|
- if params[:status].blank? && can?(:suppress, :follow_recommendation)
|
||||||
|
= f.button safe_join([fa_icon('times'), t('admin.follow_recommendations.suppress')]), name: :suppress, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||||
|
- if params[:status] == 'suppressed' && can?(:unsuppress, :follow_recommendation)
|
||||||
|
= f.button safe_join([fa_icon('plus'), t('admin.follow_recommendations.unsuppress')]), name: :unsuppress, class: 'table-action-link', type: :submit
|
||||||
|
.batch-table__body
|
||||||
|
- if @accounts.empty?
|
||||||
|
= nothing_here 'nothing-here--under-tabs'
|
||||||
|
- else
|
||||||
|
= render partial: 'account', collection: @accounts, locals: { f: f }
|
|
@ -1,8 +1,9 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('admin.rules.title')
|
= t('admin.rules.title')
|
||||||
|
|
||||||
.simple_form
|
%p= t('admin.rules.description_html')
|
||||||
%p.hint= t('admin.rules.description')
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
- if can? :create, :rule
|
- if can? :create, :rule
|
||||||
= simple_form_for @rule, url: admin_rules_path do |f|
|
= simple_form_for @rule, url: admin_rules_path do |f|
|
||||||
|
|
|
@ -101,9 +101,6 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :show_replies_in_public_timelines, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_replies_in_public_timelines.title'), hint: t('admin.settings.show_replies_in_public_timelines.desc_html')
|
= f.input :show_replies_in_public_timelines, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_replies_in_public_timelines.title'), hint: t('admin.settings.show_replies_in_public_timelines.desc_html')
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html')
|
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<%= t 'devise.mailer.webauthn_credentia.added.title' %>
|
<%= t 'devise.mailer.webauthn_credential.added.title' %>
|
||||||
|
|
||||||
===
|
===
|
||||||
|
|
||||||
<%= t 'devise.mailer.webauthn_credentia.added.explanation' %>
|
<%= t 'devise.mailer.webauthn_credential.added.explanation' %>
|
||||||
|
|
||||||
=> <%= edit_user_registration_url %>
|
=> <%= edit_user_registration_url %>
|
||||||
|
|
61
app/workers/scheduler/follow_recommendations_scheduler.rb
Normal file
61
app/workers/scheduler/follow_recommendations_scheduler.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Scheduler::FollowRecommendationsScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
# The maximum number of accounts that can be requested in one page from the
|
||||||
|
# API is 80, and the suggestions API does not allow pagination. This number
|
||||||
|
# leaves some room for accounts being filtered during live access
|
||||||
|
SET_SIZE = 100
|
||||||
|
|
||||||
|
def perform
|
||||||
|
# Maintaining a materialized view speeds-up subsequent queries significantly
|
||||||
|
AccountSummary.refresh
|
||||||
|
|
||||||
|
fallback_recommendations = FollowRecommendation.safe.filtered.limit(SET_SIZE).index_by(&:account_id)
|
||||||
|
|
||||||
|
I18n.available_locales.each do |locale|
|
||||||
|
recommendations = begin
|
||||||
|
if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist
|
||||||
|
FollowRecommendation.safe.filtered.localized(locale).limit(SET_SIZE).index_by(&:account_id)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use language-agnostic results if there are not enough language-specific ones
|
||||||
|
missing = SET_SIZE - recommendations.keys.size
|
||||||
|
|
||||||
|
if missing.positive?
|
||||||
|
added = 0
|
||||||
|
|
||||||
|
# Avoid duplicate results
|
||||||
|
fallback_recommendations.each_value do |recommendation|
|
||||||
|
next if recommendations.key?(recommendation.account_id)
|
||||||
|
|
||||||
|
recommendations[recommendation.account_id] = recommendation
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
break if added >= missing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
redis.pipelined do
|
||||||
|
redis.del(key(locale))
|
||||||
|
|
||||||
|
recommendations.each_value do |recommendation|
|
||||||
|
redis.zadd(key(locale), recommendation.rank, recommendation.account_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def key(locale)
|
||||||
|
"follow_recommendations:#{locale}"
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,22 +3,67 @@
|
||||||
class Web::PushNotificationWorker
|
class Web::PushNotificationWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
sidekiq_options backtrace: true, retry: 5
|
sidekiq_options queue: 'push', retry: 5
|
||||||
|
|
||||||
|
TTL = 48.hours.to_s
|
||||||
|
URGENCY = 'normal'
|
||||||
|
|
||||||
def perform(subscription_id, notification_id)
|
def perform(subscription_id, notification_id)
|
||||||
subscription = ::Web::PushSubscription.find(subscription_id)
|
@subscription = Web::PushSubscription.find(subscription_id)
|
||||||
notification = Notification.find(notification_id)
|
@notification = Notification.find(notification_id)
|
||||||
|
|
||||||
subscription.push(notification) unless notification.activity.nil?
|
# Polymorphically associated activity could have been deleted
|
||||||
rescue Webpush::ResponseError => e
|
# in the meantime, so we have to double-check before proceeding
|
||||||
code = e.response.code.to_i
|
return unless @notification.activity.present? && @subscription.pushable?(@notification)
|
||||||
|
|
||||||
if (400..499).cover?(code) && ![408, 429].include?(code)
|
payload = @subscription.encrypt(push_notification_json)
|
||||||
subscription.destroy!
|
|
||||||
else
|
request_pool.with(@subscription.audience) do |http_client|
|
||||||
raise e
|
request = Request.new(:post, @subscription.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
|
||||||
|
|
||||||
|
request.add_headers(
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Ttl' => TTL,
|
||||||
|
'Urgency' => URGENCY,
|
||||||
|
'Content-Encoding' => 'aesgcm',
|
||||||
|
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
|
||||||
|
'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{@subscription.crypto_key_header}",
|
||||||
|
'Authorization' => @subscription.authorization_header
|
||||||
|
)
|
||||||
|
|
||||||
|
request.perform do |response|
|
||||||
|
# If the server responds with an error in the 4xx range
|
||||||
|
# that isn't about rate-limiting or timeouts, we can
|
||||||
|
# assume that the subscription is invalid or expired
|
||||||
|
# and must be removed
|
||||||
|
|
||||||
|
if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
|
||||||
|
@subscription.destroy!
|
||||||
|
elsif !(200...300).cover?(response.code)
|
||||||
|
raise Mastodon::UnexpectedResponseError, response
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def push_notification_json
|
||||||
|
json = I18n.with_locale(@subscription.locale || I18n.default_locale) do
|
||||||
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
|
@notification,
|
||||||
|
serializer: Web::NotificationSerializer,
|
||||||
|
scope: @subscription,
|
||||||
|
scope_name: :current_push_subscription
|
||||||
|
).as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
Oj.dump(json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_pool
|
||||||
|
RequestPool.current
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,7 @@ require_relative '../lib/webpacker/helper_extensions'
|
||||||
require_relative '../lib/action_dispatch/cookie_jar_extensions'
|
require_relative '../lib/action_dispatch/cookie_jar_extensions'
|
||||||
require_relative '../lib/rails/engine_extensions'
|
require_relative '../lib/rails/engine_extensions'
|
||||||
require_relative '../lib/active_record/database_tasks_extensions'
|
require_relative '../lib/active_record/database_tasks_extensions'
|
||||||
|
require_relative '../lib/active_record/batches'
|
||||||
|
|
||||||
Dotenv::Railtie.load
|
Dotenv::Railtie.load
|
||||||
|
|
||||||
|
|
|
@ -90,9 +90,12 @@ Rails.application.configure do
|
||||||
config.action_mailer.perform_caching = false
|
config.action_mailer.perform_caching = false
|
||||||
|
|
||||||
# E-mails
|
# E-mails
|
||||||
|
outgoing_email_address = ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost')
|
||||||
|
outgoing_mail_domain = Mail::Address.new(outgoing_email_address).domain
|
||||||
config.action_mailer.default_options = {
|
config.action_mailer.default_options = {
|
||||||
from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost'),
|
from: outgoing_email_address,
|
||||||
reply_to: ENV['SMTP_REPLY_TO']
|
reply_to: ENV['SMTP_REPLY_TO'],
|
||||||
|
'Message-ID': -> { "<#{Mail.random_tag}@#{outgoing_mail_domain}>" },
|
||||||
}
|
}
|
||||||
|
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
|
@ -116,10 +119,10 @@ Rails.application.configure do
|
||||||
'X-Frame-Options' => 'DENY',
|
'X-Frame-Options' => 'DENY',
|
||||||
'X-Content-Type-Options' => 'nosniff',
|
'X-Content-Type-Options' => 'nosniff',
|
||||||
'X-XSS-Protection' => '1; mode=block',
|
'X-XSS-Protection' => '1; mode=block',
|
||||||
|
'Permissions-Policy' => 'interest-cohort=()',
|
||||||
'Referrer-Policy' => 'same-origin',
|
'Referrer-Policy' => 'same-origin',
|
||||||
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
|
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
|
||||||
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
|
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.x.otp_secret = ENV.fetch('OTP_SECRET')
|
config.x.otp_secret = ENV.fetch('OTP_SECRET')
|
||||||
|
|
|
@ -53,6 +53,7 @@ Rails.application.config.content_security_policy_nonce_generator = -> request {
|
||||||
|
|
||||||
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
|
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
|
||||||
|
|
||||||
|
Rails.application.reloader.to_prepare do
|
||||||
PgHero::HomeController.content_security_policy do |p|
|
PgHero::HomeController.content_security_policy do |p|
|
||||||
p.script_src :self, :unsafe_inline, assets_host
|
p.script_src :self, :unsafe_inline, assets_host
|
||||||
p.style_src :self, :unsafe_inline, assets_host
|
p.style_src :self, :unsafe_inline, assets_host
|
||||||
|
@ -61,3 +62,4 @@ end
|
||||||
PgHero::HomeController.after_action do
|
PgHero::HomeController.after_action do
|
||||||
request.content_security_policy_nonce_generator = nil
|
request.content_security_policy_nonce_generator = nil
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -52,6 +52,11 @@ Doorkeeper.configure do
|
||||||
# Issue access tokens with refresh token (disabled by default)
|
# Issue access tokens with refresh token (disabled by default)
|
||||||
# use_refresh_token
|
# use_refresh_token
|
||||||
|
|
||||||
|
# Forbids creating/updating applications with arbitrary scopes that are
|
||||||
|
# not in configuration, i.e. `default_scopes` or `optional_scopes`.
|
||||||
|
# (Disabled by default)
|
||||||
|
enforce_configured_scopes
|
||||||
|
|
||||||
# Provide support for an owner to be assigned to each registered application (disabled by default)
|
# Provide support for an owner to be assigned to each registered application (disabled by default)
|
||||||
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
|
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
|
||||||
# a registered application
|
# a registered application
|
||||||
|
|
|
@ -112,7 +112,9 @@ else
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Rails.application.reloader.to_prepare do
|
||||||
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
|
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
|
||||||
|
end
|
||||||
|
|
||||||
# In some places in the code, we rescue this exception, but we don't always
|
# In some places in the code, we rescue this exception, but we don't always
|
||||||
# load the S3 library, so it may be an undefined constant:
|
# load the S3 library, so it may be an undefined constant:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Rails.application.reloader.to_prepare do
|
||||||
ActionController::Base.log_warning_on_csrf_failure = false
|
ActionController::Base.log_warning_on_csrf_failure = false
|
||||||
|
end
|
||||||
|
|
|
@ -315,10 +315,12 @@ en:
|
||||||
new:
|
new:
|
||||||
create: Create announcement
|
create: Create announcement
|
||||||
title: New announcement
|
title: New announcement
|
||||||
|
publish: Publish
|
||||||
published_msg: Announcement successfully published!
|
published_msg: Announcement successfully published!
|
||||||
scheduled_for: Scheduled for %{time}
|
scheduled_for: Scheduled for %{time}
|
||||||
scheduled_msg: Announcement scheduled for publication!
|
scheduled_msg: Announcement scheduled for publication!
|
||||||
title: Announcements
|
title: Announcements
|
||||||
|
unpublish: Unpublish
|
||||||
unpublished_msg: Announcement successfully unpublished!
|
unpublished_msg: Announcement successfully unpublished!
|
||||||
updated_msg: Announcement successfully updated!
|
updated_msg: Announcement successfully updated!
|
||||||
custom_emojis:
|
custom_emojis:
|
||||||
|
@ -363,7 +365,6 @@ en:
|
||||||
feature_profile_directory: Profile directory
|
feature_profile_directory: Profile directory
|
||||||
feature_registrations: Registrations
|
feature_registrations: Registrations
|
||||||
feature_relay: Federation relay
|
feature_relay: Federation relay
|
||||||
feature_spam_check: Anti-spam
|
|
||||||
feature_timeline_preview: Timeline preview
|
feature_timeline_preview: Timeline preview
|
||||||
features: Features
|
features: Features
|
||||||
hidden_service: Federation with hidden services
|
hidden_service: Federation with hidden services
|
||||||
|
@ -441,6 +442,14 @@ en:
|
||||||
create: Add domain
|
create: Add domain
|
||||||
title: Block new e-mail domain
|
title: Block new e-mail domain
|
||||||
title: Blocked e-mail domains
|
title: Blocked e-mail domains
|
||||||
|
follow_recommendations:
|
||||||
|
description_html: "<strong>Follow recommendations help new users quickly find interesting content</strong>. When a user has not interacted with others enough to form personalized follow recommendations, these accounts are recommended instead. They are re-calculated on a daily basis from a mix of accounts with the highest recent engagements and highest local follower counts for a given language."
|
||||||
|
language: For language
|
||||||
|
status: Status
|
||||||
|
suppress: Suppress follow recommendation
|
||||||
|
suppressed: Suppressed
|
||||||
|
title: Follow recommendations
|
||||||
|
unsuppress: Restore follow recommendation
|
||||||
instances:
|
instances:
|
||||||
by_domain: Domain
|
by_domain: Domain
|
||||||
delivery_available: Delivery is available
|
delivery_available: Delivery is available
|
||||||
|
@ -545,8 +554,10 @@ en:
|
||||||
updated_at: Updated
|
updated_at: Updated
|
||||||
rules:
|
rules:
|
||||||
add_new: Add rule
|
add_new: Add rule
|
||||||
description: While most claim to have read and agree to the terms of service, usually people do not read through until after a problem arises. Make it easier to see your server's rules at a glance by providing them in a flat bullet point list. Try to keep individual rules short and simple, but try not to split them up into many separate items either.
|
delete: Delete
|
||||||
|
description_html: While most claim to have read and agree to the terms of service, usually people do not read through until after a problem arises. <strong>Make it easier to see your server's rules at a glance by providing them in a flat bullet point list.</strong> Try to keep individual rules short and simple, but try not to split them up into many separate items either.
|
||||||
edit: Edit rule
|
edit: Edit rule
|
||||||
|
empty: No server rules have been defined yet.
|
||||||
title: Server rules
|
title: Server rules
|
||||||
settings:
|
settings:
|
||||||
activity_api_enabled:
|
activity_api_enabled:
|
||||||
|
@ -627,9 +638,6 @@ en:
|
||||||
desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
|
desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
|
||||||
title: Custom terms of service
|
title: Custom terms of service
|
||||||
site_title: Server name
|
site_title: Server name
|
||||||
spam_check_enabled:
|
|
||||||
desc_html: Mastodon can auto-report accounts that send repeated unsolicited messages. There may be false positives.
|
|
||||||
title: Anti-spam automation
|
|
||||||
thumbnail:
|
thumbnail:
|
||||||
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
||||||
title: Server thumbnail
|
title: Server thumbnail
|
||||||
|
@ -691,6 +699,7 @@ en:
|
||||||
add_new: Add new
|
add_new: Add new
|
||||||
delete: Delete
|
delete: Delete
|
||||||
edit_preset: Edit warning preset
|
edit_preset: Edit warning preset
|
||||||
|
empty: You haven't defined any warning presets yet.
|
||||||
title: Manage warning presets
|
title: Manage warning presets
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
new_pending_account:
|
new_pending_account:
|
||||||
|
@ -1209,8 +1218,6 @@ en:
|
||||||
relationships: Follows and followers
|
relationships: Follows and followers
|
||||||
two_factor_authentication: Two-factor Auth
|
two_factor_authentication: Two-factor Auth
|
||||||
webauthn_authentication: Security keys
|
webauthn_authentication: Security keys
|
||||||
spam_check:
|
|
||||||
spam_detected: This is an automated report. Spam has been detected.
|
|
||||||
statuses:
|
statuses:
|
||||||
attached:
|
attached:
|
||||||
audio:
|
audio:
|
||||||
|
|
|
@ -30,19 +30,19 @@ en:
|
||||||
defaults:
|
defaults:
|
||||||
autofollow: People who sign up through the invite will automatically follow you
|
autofollow: People who sign up through the invite will automatically follow you
|
||||||
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
||||||
bot: This account mainly performs automated actions and might not be monitored
|
bot: Signal to others that the account mainly performs automated actions and might not be monitored
|
||||||
context: One or multiple contexts where the filter should apply
|
context: One or multiple contexts where the filter should apply
|
||||||
current_password: For security purposes please enter the password of the current account
|
current_password: For security purposes please enter the password of the current account
|
||||||
current_username: To confirm, please enter the username of the current account
|
current_username: To confirm, please enter the username of the current account
|
||||||
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
|
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
|
||||||
discoverable: The profile directory is another way by which your account can reach a wider audience
|
discoverable: Allow your account to be discovered by strangers through recommendations and other features
|
||||||
email: You will be sent a confirmation e-mail
|
email: You will be sent a confirmation e-mail
|
||||||
fields: You can have up to 4 items displayed as a table on your profile
|
fields: You can have up to 4 items displayed as a table on your profile
|
||||||
header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
||||||
inbox_url: Copy the URL from the frontpage of the relay you want to use
|
inbox_url: Copy the URL from the frontpage of the relay you want to use
|
||||||
irreversible: Filtered toots will disappear irreversibly, even if filter is later removed
|
irreversible: Filtered toots will disappear irreversibly, even if filter is later removed
|
||||||
locale: The language of the user interface, e-mails and push notifications
|
locale: The language of the user interface, e-mails and push notifications
|
||||||
locked: Requires you to manually approve followers
|
locked: Manually control who can follow you by approving follow requests
|
||||||
password: Use at least 8 characters
|
password: Use at least 8 characters
|
||||||
phrase: Will be matched regardless of casing in text or content warning of a toot
|
phrase: Will be matched regardless of casing in text or content warning of a toot
|
||||||
scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
|
scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
|
||||||
|
@ -51,7 +51,7 @@ en:
|
||||||
setting_display_media_default: Hide media marked as sensitive
|
setting_display_media_default: Hide media marked as sensitive
|
||||||
setting_display_media_hide_all: Always hide media
|
setting_display_media_hide_all: Always hide media
|
||||||
setting_display_media_show_all: Always show media
|
setting_display_media_show_all: Always show media
|
||||||
setting_hide_network: Who you follow and who follows you will not be shown on your profile
|
setting_hide_network: Who you follow and who follows you will be hidden on your profile
|
||||||
setting_noindex: Affects your public profile and status pages
|
setting_noindex: Affects your public profile and status pages
|
||||||
setting_show_application: The application you use to toot will be displayed in the detailed view of your toots
|
setting_show_application: The application you use to toot will be displayed in the detailed view of your toots
|
||||||
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
|
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
|
||||||
|
@ -128,7 +128,7 @@ en:
|
||||||
context: Filter contexts
|
context: Filter contexts
|
||||||
current_password: Current password
|
current_password: Current password
|
||||||
data: Data
|
data: Data
|
||||||
discoverable: List this account on the directory
|
discoverable: Suggest account to others
|
||||||
display_name: Display name
|
display_name: Display name
|
||||||
email: E-mail address
|
email: E-mail address
|
||||||
expires_in: Expire after
|
expires_in: Expire after
|
||||||
|
@ -138,7 +138,7 @@ en:
|
||||||
inbox_url: URL of the relay inbox
|
inbox_url: URL of the relay inbox
|
||||||
irreversible: Drop instead of hide
|
irreversible: Drop instead of hide
|
||||||
locale: Interface language
|
locale: Interface language
|
||||||
locked: Lock account
|
locked: Require follow requests
|
||||||
max_uses: Max number of uses
|
max_uses: Max number of uses
|
||||||
new_password: New password
|
new_password: New password
|
||||||
note: Bio
|
note: Bio
|
||||||
|
@ -160,7 +160,7 @@ en:
|
||||||
setting_display_media_hide_all: Hide all
|
setting_display_media_hide_all: Hide all
|
||||||
setting_display_media_show_all: Show all
|
setting_display_media_show_all: Show all
|
||||||
setting_expand_spoilers: Always expand toots marked with content warnings
|
setting_expand_spoilers: Always expand toots marked with content warnings
|
||||||
setting_hide_network: Hide your network
|
setting_hide_network: Hide your social graph
|
||||||
setting_noindex: Opt-out of search engine indexing
|
setting_noindex: Opt-out of search engine indexing
|
||||||
setting_reduce_motion: Reduce motion in animations
|
setting_reduce_motion: Reduce motion in animations
|
||||||
setting_show_application: Disclose application used to send toots
|
setting_show_application: Disclose application used to send toots
|
||||||
|
|
|
@ -45,6 +45,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
||||||
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts}
|
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts}
|
||||||
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
|
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
|
||||||
s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags}
|
s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags}
|
||||||
|
s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
|
||||||
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
|
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
|
||||||
s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
|
s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
|
||||||
s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_url, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.admin? }
|
s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_url, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.admin? }
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
require 'sidekiq_unique_jobs/web'
|
require 'sidekiq_unique_jobs/web'
|
||||||
require 'sidekiq-scheduler/web'
|
require 'sidekiq-scheduler/web'
|
||||||
|
|
||||||
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
|
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
root 'home#index'
|
root 'home#index'
|
||||||
|
|
||||||
|
@ -296,6 +294,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :account_moderation_notes, only: [:create, :destroy]
|
resources :account_moderation_notes, only: [:create, :destroy]
|
||||||
|
resource :follow_recommendations, only: [:show, :update]
|
||||||
|
|
||||||
resources :tags, only: [:index, :show, :update] do
|
resources :tags, only: [:index, :show, :update] do
|
||||||
collection do
|
collection do
|
||||||
|
@ -513,6 +512,7 @@ Rails.application.routes.draw do
|
||||||
namespace :v2 do
|
namespace :v2 do
|
||||||
resources :media, only: [:create]
|
resources :media, only: [:create]
|
||||||
get '/search', to: 'search#index', as: :search
|
get '/search', to: 'search#index', as: :search
|
||||||
|
resources :suggestions, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :web do
|
namespace :web do
|
||||||
|
|
|
@ -75,7 +75,6 @@ defaults: &defaults
|
||||||
show_reblogs_in_public_timelines: false
|
show_reblogs_in_public_timelines: false
|
||||||
show_replies_in_public_timelines: false
|
show_replies_in_public_timelines: false
|
||||||
default_content_type: 'text/plain'
|
default_content_type: 'text/plain'
|
||||||
spam_check_enabled: true
|
|
||||||
show_domain_blocks: 'disabled'
|
show_domain_blocks: 'disabled'
|
||||||
show_domain_blocks_rationale: 'disabled'
|
show_domain_blocks_rationale: 'disabled'
|
||||||
outgoing_spoilers: ''
|
outgoing_spoilers: ''
|
||||||
|
|
|
@ -25,6 +25,10 @@
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
|
||||||
class: Scheduler::FeedCleanupScheduler
|
class: Scheduler::FeedCleanupScheduler
|
||||||
queue: scheduler
|
queue: scheduler
|
||||||
|
follow_recommendations_scheduler:
|
||||||
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(6..9) %> * * *'
|
||||||
|
class: Scheduler::FollowRecommendationsScheduler
|
||||||
|
queue: scheduler
|
||||||
doorkeeper_cleanup_scheduler:
|
doorkeeper_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
|
||||||
class: Scheduler::DoorkeeperCleanupScheduler
|
class: Scheduler::DoorkeeperCleanupScheduler
|
||||||
|
|
17
db/migrate/20210306164523_account_ids_to_timestamp_ids.rb
Normal file
17
db/migrate/20210306164523_account_ids_to_timestamp_ids.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class AccountIdsToTimestampIds < ActiveRecord::Migration[5.1]
|
||||||
|
def up
|
||||||
|
# Set up the accounts.id column to use our timestamp-based IDs.
|
||||||
|
safety_assured do
|
||||||
|
execute("ALTER TABLE accounts ALTER COLUMN id SET DEFAULT timestamp_id('accounts')")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure we have a sequence to use.
|
||||||
|
Mastodon::Snowflake.ensure_id_sequences_exist
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute("LOCK accounts")
|
||||||
|
execute("SELECT setval('accounts_id_seq', (SELECT MAX(id) FROM accounts))")
|
||||||
|
execute("ALTER TABLE accounts ALTER COLUMN id SET DEFAULT nextval('accounts_id_seq')")
|
||||||
|
end
|
||||||
|
end
|
9
db/migrate/20210322164601_create_account_summaries.rb
Normal file
9
db/migrate/20210322164601_create_account_summaries.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
class CreateAccountSummaries < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_view :account_summaries, materialized: true
|
||||||
|
|
||||||
|
# To be able to refresh the view concurrently,
|
||||||
|
# at least one unique index is required
|
||||||
|
safety_assured { add_index :account_summaries, :account_id, unique: true }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class CreateFollowRecommendations < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_view :follow_recommendations
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
class CreateFollowRecommendationSuppressions < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :follow_recommendation_suppressions do |t|
|
||||||
|
t.references :account, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
db/migrate/20210416200740_create_canonical_email_blocks.rb
Normal file
10
db/migrate/20210416200740_create_canonical_email_blocks.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
class CreateCanonicalEmailBlocks < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :canonical_email_blocks do |t|
|
||||||
|
t.string :canonical_email_hash, null: false, default: '', index: { unique: true }
|
||||||
|
t.belongs_to :reference_account, null: false, foreign_key: { on_cascade: :delete, to_table: 'accounts' }
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
75
db/schema.rb
75
db/schema.rb
|
@ -2,15 +2,15 @@
|
||||||
# of editing this file, please use the migrations feature of Active Record to
|
# of editing this file, please use the migrations feature of Active Record to
|
||||||
# incrementally modify your database, and then regenerate this schema definition.
|
# incrementally modify your database, and then regenerate this schema definition.
|
||||||
#
|
#
|
||||||
# Note that this schema.rb definition is the authoritative source for your
|
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||||
# database schema. If you need to create the application database on another
|
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||||
# system, you should be using db:schema:load, not running all the migrations
|
# be faster and is potentially less error prone than running all of your
|
||||||
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||||
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
# migrations use external dependencies or application code.
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
ActiveRecord::Schema.define(version: 2021_04_16_200740) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -142,7 +142,7 @@ ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
||||||
t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id"
|
t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "accounts", force: :cascade do |t|
|
create_table "accounts", id: :bigint, default: -> { "timestamp_id('accounts'::text)" }, force: :cascade do |t|
|
||||||
t.string "username", default: "", null: false
|
t.string "username", default: "", null: false
|
||||||
t.string "domain"
|
t.string "domain"
|
||||||
t.string "secret", default: "", null: false
|
t.string "secret", default: "", null: false
|
||||||
|
@ -280,6 +280,15 @@ ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
||||||
t.index ["status_id"], name: "index_bookmarks_on_status_id"
|
t.index ["status_id"], name: "index_bookmarks_on_status_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "canonical_email_blocks", force: :cascade do |t|
|
||||||
|
t.string "canonical_email_hash", default: "", null: false
|
||||||
|
t.bigint "reference_account_id", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["canonical_email_hash"], name: "index_canonical_email_blocks_on_canonical_email_hash", unique: true
|
||||||
|
t.index ["reference_account_id"], name: "index_canonical_email_blocks_on_reference_account_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "conversation_mutes", force: :cascade do |t|
|
create_table "conversation_mutes", force: :cascade do |t|
|
||||||
t.bigint "conversation_id", null: false
|
t.bigint "conversation_id", null: false
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
|
@ -406,6 +415,13 @@ ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
||||||
t.index ["tag_id"], name: "index_featured_tags_on_tag_id"
|
t.index ["tag_id"], name: "index_featured_tags_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "follow_recommendation_suppressions", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["account_id"], name: "index_follow_recommendation_suppressions_on_account_id", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "follow_requests", force: :cascade do |t|
|
create_table "follow_requests", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
@ -986,6 +1002,7 @@ ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
||||||
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
|
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
|
||||||
add_foreign_key "bookmarks", "accounts", on_delete: :cascade
|
add_foreign_key "bookmarks", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "bookmarks", "statuses", on_delete: :cascade
|
add_foreign_key "bookmarks", "statuses", on_delete: :cascade
|
||||||
|
add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id"
|
||||||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||||
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
||||||
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
||||||
|
@ -998,6 +1015,7 @@ ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
||||||
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
||||||
add_foreign_key "featured_tags", "accounts", on_delete: :cascade
|
add_foreign_key "featured_tags", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "featured_tags", "tags", on_delete: :cascade
|
add_foreign_key "featured_tags", "tags", on_delete: :cascade
|
||||||
|
add_foreign_key "follow_recommendation_suppressions", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade
|
add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade
|
||||||
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
|
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
|
||||||
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
||||||
|
@ -1081,4 +1099,47 @@ ActiveRecord::Schema.define(version: 2021_03_08_133107) do
|
||||||
SQL
|
SQL
|
||||||
add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true
|
add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true
|
||||||
|
|
||||||
|
create_view "account_summaries", materialized: true, sql_definition: <<-SQL
|
||||||
|
SELECT accounts.id AS account_id,
|
||||||
|
mode() WITHIN GROUP (ORDER BY t0.language) AS language,
|
||||||
|
mode() WITHIN GROUP (ORDER BY t0.sensitive) AS sensitive
|
||||||
|
FROM (accounts
|
||||||
|
CROSS JOIN LATERAL ( SELECT statuses.account_id,
|
||||||
|
statuses.language,
|
||||||
|
statuses.sensitive
|
||||||
|
FROM statuses
|
||||||
|
WHERE ((statuses.account_id = accounts.id) AND (statuses.deleted_at IS NULL))
|
||||||
|
ORDER BY statuses.id DESC
|
||||||
|
LIMIT 20) t0)
|
||||||
|
WHERE ((accounts.suspended_at IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.discoverable = true) AND (accounts.locked = false))
|
||||||
|
GROUP BY accounts.id;
|
||||||
|
SQL
|
||||||
|
add_index "account_summaries", ["account_id"], name: "index_account_summaries_on_account_id", unique: true
|
||||||
|
|
||||||
|
create_view "follow_recommendations", sql_definition: <<-SQL
|
||||||
|
SELECT t0.account_id,
|
||||||
|
sum(t0.rank) AS rank,
|
||||||
|
array_agg(t0.reason) AS reason
|
||||||
|
FROM ( SELECT accounts.id AS account_id,
|
||||||
|
((count(follows.id))::numeric / (1.0 + (count(follows.id))::numeric)) AS rank,
|
||||||
|
'most_followed'::text AS reason
|
||||||
|
FROM ((follows
|
||||||
|
JOIN accounts ON ((accounts.id = follows.target_account_id)))
|
||||||
|
JOIN users ON ((users.account_id = follows.account_id)))
|
||||||
|
WHERE ((users.current_sign_in_at >= (now() - 'P30D'::interval)) AND (accounts.suspended_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.locked = false) AND (accounts.discoverable = true))
|
||||||
|
GROUP BY accounts.id
|
||||||
|
HAVING (count(follows.id) >= 5)
|
||||||
|
UNION ALL
|
||||||
|
SELECT accounts.id AS account_id,
|
||||||
|
(sum((status_stats.reblogs_count + status_stats.favourites_count)) / (1.0 + sum((status_stats.reblogs_count + status_stats.favourites_count)))) AS rank,
|
||||||
|
'most_interactions'::text AS reason
|
||||||
|
FROM ((status_stats
|
||||||
|
JOIN statuses ON ((statuses.id = status_stats.status_id)))
|
||||||
|
JOIN accounts ON ((accounts.id = statuses.account_id)))
|
||||||
|
WHERE ((statuses.id >= (((date_part('epoch'::text, (now() - 'P30D'::interval)) * (1000)::double precision))::bigint << 16)) AND (accounts.suspended_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.locked = false) AND (accounts.discoverable = true))
|
||||||
|
GROUP BY accounts.id
|
||||||
|
HAVING (sum((status_stats.reblogs_count + status_stats.favourites_count)) >= (5)::numeric)) t0
|
||||||
|
GROUP BY t0.account_id
|
||||||
|
ORDER BY (sum(t0.rank)) DESC;
|
||||||
|
SQL
|
||||||
end
|
end
|
||||||
|
|
22
db/views/account_summaries_v01.sql
Normal file
22
db/views/account_summaries_v01.sql
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
SELECT
|
||||||
|
accounts.id AS account_id,
|
||||||
|
mode() WITHIN GROUP (ORDER BY language ASC) AS language,
|
||||||
|
mode() WITHIN GROUP (ORDER BY sensitive ASC) AS sensitive
|
||||||
|
FROM accounts
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT
|
||||||
|
statuses.account_id,
|
||||||
|
statuses.language,
|
||||||
|
statuses.sensitive
|
||||||
|
FROM statuses
|
||||||
|
WHERE statuses.account_id = accounts.id
|
||||||
|
AND statuses.deleted_at IS NULL
|
||||||
|
ORDER BY statuses.id DESC
|
||||||
|
LIMIT 20
|
||||||
|
) t0
|
||||||
|
WHERE accounts.suspended_at IS NULL
|
||||||
|
AND accounts.silenced_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
AND accounts.discoverable = 't'
|
||||||
|
AND accounts.locked = 'f'
|
||||||
|
GROUP BY accounts.id
|
38
db/views/follow_recommendations_v01.sql
Normal file
38
db/views/follow_recommendations_v01.sql
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
SELECT
|
||||||
|
account_id,
|
||||||
|
sum(rank) AS rank,
|
||||||
|
array_agg(reason) AS reason
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
accounts.id AS account_id,
|
||||||
|
count(follows.id) / (1.0 + count(follows.id)) AS rank,
|
||||||
|
'most_followed' AS reason
|
||||||
|
FROM follows
|
||||||
|
INNER JOIN accounts ON accounts.id = follows.target_account_id
|
||||||
|
INNER JOIN users ON users.account_id = follows.account_id
|
||||||
|
WHERE users.current_sign_in_at >= (now() - interval '30 days')
|
||||||
|
AND accounts.suspended_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
AND accounts.silenced_at IS NULL
|
||||||
|
AND accounts.locked = 'f'
|
||||||
|
AND accounts.discoverable = 't'
|
||||||
|
GROUP BY accounts.id
|
||||||
|
HAVING count(follows.id) >= 5
|
||||||
|
UNION ALL
|
||||||
|
SELECT accounts.id AS account_id,
|
||||||
|
sum(reblogs_count + favourites_count) / (1.0 + sum(reblogs_count + favourites_count)) AS rank,
|
||||||
|
'most_interactions' AS reason
|
||||||
|
FROM status_stats
|
||||||
|
INNER JOIN statuses ON statuses.id = status_stats.status_id
|
||||||
|
INNER JOIN accounts ON accounts.id = statuses.account_id
|
||||||
|
WHERE statuses.id >= ((date_part('epoch', now() - interval '30 days') * 1000)::bigint << 16)
|
||||||
|
AND accounts.suspended_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
AND accounts.silenced_at IS NULL
|
||||||
|
AND accounts.locked = 'f'
|
||||||
|
AND accounts.discoverable = 't'
|
||||||
|
GROUP BY accounts.id
|
||||||
|
HAVING sum(reblogs_count + favourites_count) >= 5
|
||||||
|
) t0
|
||||||
|
GROUP BY account_id
|
||||||
|
ORDER BY rank DESC
|
44
lib/active_record/batches.rb
Normal file
44
lib/active_record/batches.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveRecord
|
||||||
|
module Batches
|
||||||
|
def pluck_each(*column_names)
|
||||||
|
relation = self
|
||||||
|
|
||||||
|
options = column_names.extract_options!
|
||||||
|
|
||||||
|
flatten = column_names.size == 1
|
||||||
|
batch_limit = options[:batch_limit] || 1_000
|
||||||
|
order = options[:order] || :asc
|
||||||
|
|
||||||
|
column_names.unshift(primary_key)
|
||||||
|
|
||||||
|
relation = relation.reorder(batch_order(order)).limit(batch_limit)
|
||||||
|
relation.skip_query_cache!
|
||||||
|
|
||||||
|
batch_relation = relation
|
||||||
|
|
||||||
|
loop do
|
||||||
|
batch = batch_relation.pluck(*column_names)
|
||||||
|
|
||||||
|
break if batch.empty?
|
||||||
|
|
||||||
|
primary_key_offset = batch.last[0]
|
||||||
|
|
||||||
|
batch.each do |record|
|
||||||
|
if flatten
|
||||||
|
yield record[1]
|
||||||
|
else
|
||||||
|
yield record[1..-1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
break if batch.size < batch_limit
|
||||||
|
|
||||||
|
batch_relation = relation.where(
|
||||||
|
predicate_builder[primary_key, primary_key_offset, order == :desc ? :lt : :gt]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -91,7 +91,7 @@ namespace :emojis do
|
||||||
desc 'Generate emoji variants with white borders'
|
desc 'Generate emoji variants with white borders'
|
||||||
task :generate_borders do
|
task :generate_borders do
|
||||||
src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json')
|
src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json')
|
||||||
emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'
|
emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲🚲👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'
|
||||||
|
|
||||||
map = Oj.load(File.read(src))
|
map = Oj.load(File.read(src))
|
||||||
|
|
||||||
|
|
34
package.json
34
package.json
|
@ -60,12 +60,12 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.13.14",
|
"@babel/core": "^7.13.15",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-decorators": "^7.13.5",
|
"@babel/plugin-proposal-decorators": "^7.13.15",
|
||||||
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
||||||
"@babel/plugin-transform-runtime": "^7.13.10",
|
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||||
"@babel/preset-env": "^7.13.12",
|
"@babel/preset-env": "^7.13.15",
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.13.13",
|
||||||
"@babel/runtime": "^7.13.10",
|
"@babel/runtime": "^7.13.10",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
|
@ -83,12 +83,12 @@
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.3.1",
|
||||||
"color-blend": "^3.0.1",
|
"color-blend": "^3.0.1",
|
||||||
"compression-webpack-plugin": "^6.1.1",
|
"compression-webpack-plugin": "^6.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^5.2.0",
|
"css-loader": "^5.2.2",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.11",
|
||||||
"detect-passive-events": "^2.0.3",
|
"detect-passive-events": "^2.0.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"emoji-mart": "Gargron/emoji-mart#build",
|
"emoji-mart": "Gargron/emoji-mart#build",
|
||||||
|
@ -109,11 +109,11 @@
|
||||||
"intl-messageformat": "^2.2.0",
|
"intl-messageformat": "^2.2.0",
|
||||||
"intl-relativeformat": "^6.4.3",
|
"intl-relativeformat": "^6.4.3",
|
||||||
"is-nan": "^1.3.2",
|
"is-nan": "^1.3.2",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mark-loader": "^0.1.6",
|
"mark-loader": "^0.1.6",
|
||||||
"marky": "^1.2.1",
|
"marky": "^1.2.1",
|
||||||
"mini-css-extract-plugin": "^1.4.0",
|
"mini-css-extract-plugin": "^1.5.0",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"npmlog": "^4.1.2",
|
"npmlog": "^4.1.2",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
@ -146,7 +146,7 @@
|
||||||
"react-swipeable-views": "^0.13.9",
|
"react-swipeable-views": "^0.13.9",
|
||||||
"react-textarea-autosize": "^8.3.2",
|
"react-textarea-autosize": "^8.3.2",
|
||||||
"react-toggle": "^4.1.2",
|
"react-toggle": "^4.1.2",
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.1.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-immutable": "^4.0.0",
|
"redux-immutable": "^4.0.0",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
|
@ -155,7 +155,7 @@
|
||||||
"requestidlecallback": "^0.3.0",
|
"requestidlecallback": "^0.3.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.10",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "^10.1.1",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"stringz": "^2.1.0",
|
"stringz": "^2.1.0",
|
||||||
|
@ -167,23 +167,23 @@
|
||||||
"twitter-text": "3.1.0",
|
"twitter-text": "3.1.0",
|
||||||
"uuid": "^8.3.1",
|
"uuid": "^8.3.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-assets-manifest": "^4.0.2",
|
"webpack-assets-manifest": "^4.0.5",
|
||||||
"webpack-bundle-analyzer": "^4.4.0",
|
"webpack-bundle-analyzer": "^4.4.1",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.7.3",
|
||||||
"wicg-inert": "^3.1.1",
|
"wicg-inert": "^3.1.1",
|
||||||
"ws": "^7.4.4"
|
"ws": "^7.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^7.24.0",
|
||||||
"eslint-plugin-import": "~2.22.1",
|
"eslint-plugin-import": "~2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "~6.4.1",
|
"eslint-plugin-jsx-a11y": "~6.4.1",
|
||||||
"eslint-plugin-promise": "~4.3.1",
|
"eslint-plugin-promise": "~5.1.0",
|
||||||
"eslint-plugin-react": "~7.23.1",
|
"eslint-plugin-react": "~7.23.2",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
"react-intl-translations-manager": "^5.0.3",
|
"react-intl-translations-manager": "^5.0.3",
|
||||||
|
|
19
public/emoji/1f6b2_border.svg
Normal file
19
public/emoji/1f6b2_border.svg
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 40 40">
|
||||||
|
<g>
|
||||||
|
<path d="M7 24c1.957 0 3.633 1.135 4.455 2.772l3.477-1.739C13.488 22.058 10.446 20 6.916 20c-1.301 0-2.534.285-3.649.787l1.668 3.67C5.566 24.17 6.262 24 7 24zm22 0c1.467 0 2.772.643 3.688 1.648l2.897-2.635C33.952 21.169 31.573 20 28.916 20c-3.576 0-6.652 2.111-8.073 5.15l3.648 1.722C25.293 25.18 27.003 24 29 24z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M7 22c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5 5 2.238 5 5-2.238 5-5 5zm22-12c-3.865 0-7 3.134-7 7s3.135 7 7 7c3.867 0 7-3.134 7-7s-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5c2.762 0 5 2.238 5 5s-2.238 5-5 5z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M29.984 28.922c-.005-.067-.021-.132-.04-.198-.019-.065-.04-.126-.071-.186-.013-.024-.015-.052-.029-.075l-7-11c-.297-.466-.914-.604-1.381-.307-.299.19-.444.513-.445.843H12c-.552 0-1 .447-1 1 0 .553.448 1 1 1h10c.027 0 .05-.014.077-.016L27.178 28H18c-.552 0-1 .447-1 1s.448 1 1 1h11.001c.116 0 .23-.028.343-.069.034-.013.064-.027.097-.043.031-.017.066-.024.097-.044.03-.02.048-.051.075-.072.055-.044.103-.089.147-.143.041-.049.074-.099.104-.154.03-.056.055-.11.075-.172.021-.066.033-.132.04-.201.004-.036.021-.066.021-.102 0-.027-.014-.051-.016-.078z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M21.581 16l-2.899 8.117-5.929-6.775c-.364-.415-.996-.459-1.411-.094-.415.364-.457.995-.094 1.411l6.664 7.615-.854 2.39c-.185.519.086 1.092.606 1.277.111.04.224.059.336.059.411 0 .796-.255.942-.664L23.705 16h-2.124z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M7 30c-.15 0-.303-.034-.446-.105-.494-.247-.694-.848-.447-1.342l3.062-6.106C9.186 22.419 11 19.651 11 17c0-3.242-2.293-4.043-2.316-4.051-.524-.175-.807-.741-.632-1.265.174-.524.739-.81 1.265-.632C9.467 11.102 13 12.333 13 17c0 3.068-1.836 6.042-2.131 6.497l-2.974 5.949C7.72 29.798 7.367 30 7 30z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M14.612 13.663c-.054 0-.11-.004-.165-.014l-6-1c-.544-.091-.913-.606-.822-1.151.091-.544.601-.913 1.151-.822l6 1c.544.091.913.606.822 1.151-.082.489-.506.836-.986.836zM26.383 17c-.03 0-.059-.002-.089-.006l-5.672-.708c-.372-.046-.644-.374-.62-.748.023-.374.333-.665.707-.665.041 0 4.067-.018 5.989-1.299.25-.167.582-.157.824.026.239.185.337.501.241.788l-.709 2.127c-.096.293-.369.485-.671.485z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M20 29c0 1.104-.895 2-2 2-1.104 0-2-.896-2-2s.896-2 2-2c1.105 0 2 .896 2 2z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#EA596E" d="M7 24c1.957 0 3.633 1.135 4.455 2.772l3.477-1.739C13.488 22.058 10.446 20 6.916 20c-1.301 0-2.534.285-3.649.787l1.668 3.67C5.566 24.17 6.262 24 7 24zm22 0c1.467 0 2.772.643 3.688 1.648l2.897-2.635C33.952 21.169 31.573 20 28.916 20c-3.576 0-6.652 2.111-8.073 5.15l3.648 1.722C25.293 25.18 27.003 24 29 24z"/>
|
||||||
|
<path fill="#292F33" d="M7 22c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5 5 2.238 5 5-2.238 5-5 5zm22-12c-3.865 0-7 3.134-7 7s3.135 7 7 7c3.867 0 7-3.134 7-7s-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5c2.762 0 5 2.238 5 5s-2.238 5-5 5z"/>
|
||||||
|
<path fill="#DD2E44" d="M29.984 28.922c-.005-.067-.021-.132-.04-.198-.019-.065-.04-.126-.071-.186-.013-.024-.015-.052-.029-.075l-7-11c-.297-.466-.914-.604-1.381-.307-.299.19-.444.513-.445.843H12c-.552 0-1 .447-1 1 0 .553.448 1 1 1h10c.027 0 .05-.014.077-.016L27.178 28H18c-.552 0-1 .447-1 1s.448 1 1 1h11.001c.116 0 .23-.028.343-.069.034-.013.064-.027.097-.043.031-.017.066-.024.097-.044.03-.02.048-.051.075-.072.055-.044.103-.089.147-.143.041-.049.074-.099.104-.154.03-.056.055-.11.075-.172.021-.066.033-.132.04-.201.004-.036.021-.066.021-.102 0-.027-.014-.051-.016-.078z"/>
|
||||||
|
<path fill="#DD2E44" d="M21.581 16l-2.899 8.117-5.929-6.775c-.364-.415-.996-.459-1.411-.094-.415.364-.457.995-.094 1.411l6.664 7.615-.854 2.39c-.185.519.086 1.092.606 1.277.111.04.224.059.336.059.411 0 .796-.255.942-.664L23.705 16h-2.124z"/>
|
||||||
|
<path fill="#DD2E44" d="M7 30c-.15 0-.303-.034-.446-.105-.494-.247-.694-.848-.447-1.342l3.062-6.106C9.186 22.419 11 19.651 11 17c0-3.242-2.293-4.043-2.316-4.051-.524-.175-.807-.741-.632-1.265.174-.524.739-.81 1.265-.632C9.467 11.102 13 12.333 13 17c0 3.068-1.836 6.042-2.131 6.497l-2.974 5.949C7.72 29.798 7.367 30 7 30z"/>
|
||||||
|
<path fill="#292F33" d="M14.612 13.663c-.054 0-.11-.004-.165-.014l-6-1c-.544-.091-.913-.606-.822-1.151.091-.544.601-.913 1.151-.822l6 1c.544.091.913.606.822 1.151-.082.489-.506.836-.986.836zM26.383 17c-.03 0-.059-.002-.089-.006l-5.672-.708c-.372-.046-.644-.374-.62-.748.023-.374.333-.665.707-.665.041 0 4.067-.018 5.989-1.299.25-.167.582-.157.824.026.239.185.337.501.241.788l-.709 2.127c-.096.293-.369.485-.671.485z"/>
|
||||||
|
<path fill="#66757F" d="M20 29c0 1.104-.895 2-2 2-1.104 0-2-.896-2-2s.896-2 2-2c1.105 0 2 .896 2 2z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
|
@ -4,16 +4,31 @@ RSpec.describe Api::V1::AppsController, type: :controller do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
describe 'POST #create' do
|
describe 'POST #create' do
|
||||||
before do
|
let(:client_name) { 'Test app' }
|
||||||
post :create, params: { client_name: 'Test app', redirect_uris: 'urn:ietf:wg:oauth:2.0:oob' }
|
let(:scopes) { nil }
|
||||||
|
let(:redirect_uris) { 'urn:ietf:wg:oauth:2.0:oob' }
|
||||||
|
let(:website) { nil }
|
||||||
|
|
||||||
|
let(:app_params) do
|
||||||
|
{
|
||||||
|
client_name: client_name,
|
||||||
|
redirect_uris: redirect_uris,
|
||||||
|
scopes: scopes,
|
||||||
|
website: website,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
post :create, params: app_params
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid params' do
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates an OAuth app' do
|
it 'creates an OAuth app' do
|
||||||
expect(Doorkeeper::Application.find_by(name: 'Test app')).to_not be nil
|
expect(Doorkeeper::Application.find_by(name: client_name)).to_not be nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns client ID and client secret' do
|
it 'returns client ID and client secret' do
|
||||||
|
@ -23,4 +38,49 @@ RSpec.describe Api::V1::AppsController, type: :controller do
|
||||||
expect(json[:client_secret]).to_not be_blank
|
expect(json[:client_secret]).to_not be_blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an unsupported scope' do
|
||||||
|
let(:scopes) { 'hoge' }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with many duplicate scopes' do
|
||||||
|
let(:scopes) { (%w(read) * 40).join(' ') }
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only saves the scope once' do
|
||||||
|
expect(Doorkeeper::Application.find_by(name: client_name).scopes.to_s).to eq 'read'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a too-long name' do
|
||||||
|
let(:client_name) { 'hoge' * 20 }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a too-long website' do
|
||||||
|
let(:website) { 'https://foo.bar/' + ('hoge' * 2_000) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a too-long redirect_uris' do
|
||||||
|
let(:redirect_uris) { 'https://foo.bar/' + ('hoge' * 2_000) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,20 +27,27 @@ describe Api::V1::Push::SubscriptionsController do
|
||||||
let(:alerts_payload) do
|
let(:alerts_payload) do
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
|
policy: 'all',
|
||||||
|
|
||||||
alerts: {
|
alerts: {
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: true,
|
||||||
favourite: false,
|
favourite: false,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: false,
|
mention: false,
|
||||||
|
poll: true,
|
||||||
|
status: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST #create' do
|
describe 'POST #create' do
|
||||||
it 'saves push subscriptions' do
|
before do
|
||||||
post :create, params: create_payload
|
post :create, params: create_payload
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'saves push subscriptions' do
|
||||||
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
||||||
|
|
||||||
expect(push_subscription.endpoint).to eq(create_payload[:subscription][:endpoint])
|
expect(push_subscription.endpoint).to eq(create_payload[:subscription][:endpoint])
|
||||||
|
@ -52,31 +59,34 @@ describe Api::V1::Push::SubscriptionsController do
|
||||||
|
|
||||||
it 'replaces old subscription on repeat calls' do
|
it 'replaces old subscription on repeat calls' do
|
||||||
post :create, params: create_payload
|
post :create, params: create_payload
|
||||||
post :create, params: create_payload
|
|
||||||
|
|
||||||
expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1
|
expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT #update' do
|
describe 'PUT #update' do
|
||||||
it 'changes alert settings' do
|
before do
|
||||||
post :create, params: create_payload
|
post :create, params: create_payload
|
||||||
put :update, params: alerts_payload
|
put :update, params: alerts_payload
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes alert settings' do
|
||||||
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
||||||
|
|
||||||
expect(push_subscription.data.dig('alerts', 'follow')).to eq(alerts_payload[:data][:alerts][:follow].to_s)
|
expect(push_subscription.data['policy']).to eq(alerts_payload[:data][:policy])
|
||||||
expect(push_subscription.data.dig('alerts', 'favourite')).to eq(alerts_payload[:data][:alerts][:favourite].to_s)
|
|
||||||
expect(push_subscription.data.dig('alerts', 'reblog')).to eq(alerts_payload[:data][:alerts][:reblog].to_s)
|
%w(follow follow_request favourite reblog mention poll status).each do |type|
|
||||||
expect(push_subscription.data.dig('alerts', 'mention')).to eq(alerts_payload[:data][:alerts][:mention].to_s)
|
expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
it 'removes the subscription' do
|
before do
|
||||||
post :create, params: create_payload
|
post :create, params: create_payload
|
||||||
delete :destroy
|
delete :destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the subscription' do
|
||||||
expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil
|
expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,11 +22,16 @@ describe Api::Web::PushSubscriptionsController do
|
||||||
let(:alerts_payload) do
|
let(:alerts_payload) do
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
|
policy: 'all',
|
||||||
|
|
||||||
alerts: {
|
alerts: {
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: false,
|
favourite: false,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: false,
|
mention: false,
|
||||||
|
poll: true,
|
||||||
|
status: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,10 +64,11 @@ describe Api::Web::PushSubscriptionsController do
|
||||||
|
|
||||||
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
||||||
|
|
||||||
expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s)
|
expect(push_subscription.data['policy']).to eq 'all'
|
||||||
expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s)
|
|
||||||
expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s)
|
%w(follow follow_request favourite reblog mention poll status).each do |type|
|
||||||
expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s)
|
expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -81,10 +87,11 @@ describe Api::Web::PushSubscriptionsController do
|
||||||
|
|
||||||
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
|
||||||
|
|
||||||
expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s)
|
expect(push_subscription.data['policy']).to eq 'all'
|
||||||
expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s)
|
|
||||||
expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s)
|
%w(follow follow_request favourite reblog mention poll status).each do |type|
|
||||||
expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s)
|
expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
4
spec/fabricators/canonical_email_block_fabricator.rb
Normal file
4
spec/fabricators/canonical_email_block_fabricator.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Fabricator(:canonical_email_block) do
|
||||||
|
email "test@example.com"
|
||||||
|
reference_account { Fabricate(:account) }
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fabricator(:follow_recommendation_suppression) do
|
||||||
|
account
|
||||||
|
end
|
|
@ -1,192 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe SpamCheck do
|
|
||||||
let!(:sender) { Fabricate(:account) }
|
|
||||||
let!(:alice) { Fabricate(:account, username: 'alice') }
|
|
||||||
let!(:bob) { Fabricate(:account, username: 'bob') }
|
|
||||||
|
|
||||||
def status_with_html(text, options = {})
|
|
||||||
status = PostStatusService.new.call(sender, { text: text }.merge(options))
|
|
||||||
status.update_columns(text: Formatter.instance.format(status), local: false)
|
|
||||||
status
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#hashable_text' do
|
|
||||||
it 'removes mentions from HTML for remote statuses' do
|
|
||||||
status = status_with_html('@alice Hello')
|
|
||||||
expect(described_class.new(status).hashable_text).to eq 'hello'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes mentions from text for local statuses' do
|
|
||||||
status = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
|
|
||||||
expect(described_class.new(status).hashable_text).to eq 'hey , how are you?'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#insufficient_data?' do
|
|
||||||
it 'returns true when there is no text' do
|
|
||||||
status = status_with_html('@alice')
|
|
||||||
expect(described_class.new(status).insufficient_data?).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false when there is text' do
|
|
||||||
status = status_with_html('@alice h')
|
|
||||||
expect(described_class.new(status).insufficient_data?).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#digest' do
|
|
||||||
it 'returns a string' do
|
|
||||||
status = status_with_html('@alice Hello world')
|
|
||||||
expect(described_class.new(status).digest).to be_a String
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#spam?' do
|
|
||||||
it 'returns false for a unique status' do
|
|
||||||
status = status_with_html('@alice Hello')
|
|
||||||
expect(described_class.new(status).spam?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false for different statuses to the same recipient' do
|
|
||||||
status1 = status_with_html('@alice Hello')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
status2 = status_with_html('@alice Are you available to talk?')
|
|
||||||
expect(described_class.new(status2).spam?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false for statuses with different content warnings' do
|
|
||||||
status1 = status_with_html('@alice Are you available to talk?')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
status2 = status_with_html('@alice Are you available to talk?', spoiler_text: 'This is a completely different matter than what I was talking about previously, I swear!')
|
|
||||||
expect(described_class.new(status2).spam?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false for different statuses to different recipients' do
|
|
||||||
status1 = status_with_html('@alice How is it going?')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
status2 = status_with_html('@bob Are you okay?')
|
|
||||||
expect(described_class.new(status2).spam?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false for very short different statuses to different recipients' do
|
|
||||||
status1 = status_with_html('@alice 🙄')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
status2 = status_with_html('@bob Huh?')
|
|
||||||
expect(described_class.new(status2).spam?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false for statuses with no text' do
|
|
||||||
status1 = status_with_html('@alice')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
status2 = status_with_html('@bob')
|
|
||||||
expect(described_class.new(status2).spam?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true for duplicate statuses to the same recipient' do
|
|
||||||
described_class::THRESHOLD.times do
|
|
||||||
status1 = status_with_html('@alice Hello')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
end
|
|
||||||
|
|
||||||
status2 = status_with_html('@alice Hello')
|
|
||||||
expect(described_class.new(status2).spam?).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true for duplicate statuses to different recipients' do
|
|
||||||
described_class::THRESHOLD.times do
|
|
||||||
status1 = status_with_html('@alice Hello')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
end
|
|
||||||
|
|
||||||
status2 = status_with_html('@bob Hello')
|
|
||||||
expect(described_class.new(status2).spam?).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true for nearly identical statuses with random numbers' do
|
|
||||||
source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.'
|
|
||||||
|
|
||||||
described_class::THRESHOLD.times do
|
|
||||||
status1 = status_with_html('@alice ' + source_text + ' 1234')
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
end
|
|
||||||
|
|
||||||
status2 = status_with_html('@bob ' + source_text + ' 9568')
|
|
||||||
expect(described_class.new(status2).spam?).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#skip?' do
|
|
||||||
it 'returns true when the sender is already silenced' do
|
|
||||||
status = status_with_html('@alice Hello')
|
|
||||||
sender.silence!
|
|
||||||
expect(described_class.new(status).skip?).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true when the mentioned person follows the sender' do
|
|
||||||
status = status_with_html('@alice Hello')
|
|
||||||
alice.follow!(sender)
|
|
||||||
expect(described_class.new(status).skip?).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false when even one mentioned person doesn\'t follow the sender' do
|
|
||||||
status = status_with_html('@alice @bob Hello')
|
|
||||||
alice.follow!(sender)
|
|
||||||
expect(described_class.new(status).skip?).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true when the sender is replying to a status that mentions the sender' do
|
|
||||||
parent = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
|
|
||||||
status = status_with_html('@alice @bob Hello', thread: parent)
|
|
||||||
expect(described_class.new(status).skip?).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#remember!' do
|
|
||||||
let(:status) { status_with_html('@alice') }
|
|
||||||
let(:spam_check) { described_class.new(status) }
|
|
||||||
let(:redis_key) { spam_check.send(:redis_key) }
|
|
||||||
|
|
||||||
it 'remembers' do
|
|
||||||
expect(Redis.current.exists?(redis_key)).to be true
|
|
||||||
spam_check.remember!
|
|
||||||
expect(Redis.current.exists?(redis_key)).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#reset!' do
|
|
||||||
let(:status) { status_with_html('@alice') }
|
|
||||||
let(:spam_check) { described_class.new(status) }
|
|
||||||
let(:redis_key) { spam_check.send(:redis_key) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
spam_check.remember!
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resets' do
|
|
||||||
expect(Redis.current.exists?(redis_key)).to be true
|
|
||||||
spam_check.reset!
|
|
||||||
expect(Redis.current.exists?(redis_key)).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#flag!' do
|
|
||||||
let!(:status1) { status_with_html('@alice General Kenobi you are a bold one') }
|
|
||||||
let!(:status2) { status_with_html('@alice @bob General Kenobi, you are a bold one') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
described_class.new(status1).remember!
|
|
||||||
described_class.new(status2).flag!
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a report about the account' do
|
|
||||||
expect(sender.targeted_reports.unresolved.count).to eq 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'attaches both matching statuses to the report' do
|
|
||||||
expect(sender.targeted_reports.first.status_ids).to include(status1.id, status2.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -83,40 +83,4 @@ RSpec.describe TagManager do
|
||||||
expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
|
expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#same_acct?' do
|
|
||||||
# The following comparisons MUST be case-insensitive.
|
|
||||||
|
|
||||||
it 'returns true if the needle has a correct username and domain for remote user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@DoMaIn.Test')).to eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if the needle is missing a domain for remote user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe')).to eq false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if the needle has an incorrect domain for remote user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@incorrect.test')).to eq false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if the needle has an incorrect username for remote user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username@domain.test', 'incorrect@DoMaIn.test')).to eq false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true if the needle has a correct username and domain for local user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username', 'UsErNaMe@Cb6E6126.nGrOk.Io')).to eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true if the needle is missing a domain for local user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username', 'UsErNaMe')).to eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if the needle has an incorrect username for local user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username', 'UsErNaM@Cb6E6126.nGrOk.Io')).to eq false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false if the needle has an incorrect domain for local user' do
|
|
||||||
expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
47
spec/models/canonical_email_block_spec.rb
Normal file
47
spec/models/canonical_email_block_spec.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe CanonicalEmailBlock, type: :model do
|
||||||
|
describe '#email=' do
|
||||||
|
let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' }
|
||||||
|
|
||||||
|
it 'sets canonical_email_hash' do
|
||||||
|
subject.email = 'test@example.com'
|
||||||
|
expect(subject.canonical_email_hash).to eq target_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the same hash even with dot permutations' do
|
||||||
|
subject.email = 't.e.s.t@example.com'
|
||||||
|
expect(subject.canonical_email_hash).to eq target_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the same hash even with extensions' do
|
||||||
|
subject.email = 'test+mastodon1@example.com'
|
||||||
|
expect(subject.canonical_email_hash).to eq target_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the same hash with different casing' do
|
||||||
|
subject.email = 'Test@EXAMPLE.com'
|
||||||
|
expect(subject.canonical_email_hash).to eq target_hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.block?' do
|
||||||
|
let!(:canonical_email_block) { Fabricate(:canonical_email_block, email: 'foo@bar.com') }
|
||||||
|
|
||||||
|
it 'returns true for the same email' do
|
||||||
|
expect(described_class.block?('foo@bar.com')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for the same email with dots' do
|
||||||
|
expect(described_class.block?('f.oo@bar.com')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for the same email with extensions' do
|
||||||
|
expect(described_class.block?('foo+spam@bar.com')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for different email' do
|
||||||
|
expect(described_class.block?('hoge@bar.com')).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
spec/models/follow_recommendation_suppression_spec.rb
Normal file
4
spec/models/follow_recommendation_suppression_spec.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe FollowRecommendationSuppression, type: :model do
|
||||||
|
end
|
|
@ -1,16 +1,94 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Web::PushSubscription, type: :model do
|
RSpec.describe Web::PushSubscription, type: :model do
|
||||||
let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } }
|
let(:account) { Fabricate(:account) }
|
||||||
let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) }
|
|
||||||
|
let(:policy) { 'all' }
|
||||||
|
|
||||||
|
let(:data) do
|
||||||
|
{
|
||||||
|
policy: policy,
|
||||||
|
|
||||||
|
alerts: {
|
||||||
|
mention: true,
|
||||||
|
reblog: false,
|
||||||
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
|
favourite: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(data: data) }
|
||||||
|
|
||||||
describe '#pushable?' do
|
describe '#pushable?' do
|
||||||
it 'obeys alert settings' do
|
let(:notification_type) { :mention }
|
||||||
expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true
|
let(:notification) { Fabricate(:notification, account: account, type: notification_type) }
|
||||||
expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Status'))).to eq false
|
|
||||||
expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Follow'))).to eq true
|
%i(mention reblog follow follow_request favourite).each do |type|
|
||||||
expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'FollowRequest'))).to eq false
|
context "when notification is a #{type}" do
|
||||||
expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Favourite'))).to eq true
|
let(:notification_type) { type }
|
||||||
|
|
||||||
|
it "returns boolean corresonding to alert setting" do
|
||||||
|
expect(subject.pushable?(notification)).to eq data[:alerts][type]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when policy is all' do
|
||||||
|
let(:policy) { 'all' }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.pushable?(notification)).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when policy is none' do
|
||||||
|
let(:policy) { 'none' }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.pushable?(notification)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when policy is followed' do
|
||||||
|
let(:policy) { 'followed' }
|
||||||
|
|
||||||
|
context 'and notification is from someone you follow' do
|
||||||
|
before do
|
||||||
|
account.follow!(notification.from_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.pushable?(notification)).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and notification is not from someone you follow' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.pushable?(notification)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when policy is follower' do
|
||||||
|
let(:policy) { 'follower' }
|
||||||
|
|
||||||
|
context 'and notification is from someone who follows you' do
|
||||||
|
before do
|
||||||
|
notification.from_account.follow!(account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(subject.pushable?(notification)).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and notification is not from someone who follows you' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.pushable?(notification)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,23 +9,36 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(user).to receive(:valid_invitation?) { false }
|
allow(user).to receive(:valid_invitation?) { false }
|
||||||
allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
|
allow_any_instance_of(described_class).to receive(:blocked_email_provider?) { blocked_email }
|
||||||
described_class.new.validate(user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'blocked_email?' do
|
subject { described_class.new.validate(user); errors }
|
||||||
|
|
||||||
|
context 'when e-mail provider is blocked' do
|
||||||
let(:blocked_email) { true }
|
let(:blocked_email) { true }
|
||||||
|
|
||||||
it 'calls errors.add' do
|
it 'adds error' do
|
||||||
expect(errors).to have_received(:add).with(:email, :blocked)
|
expect(subject).to have_received(:add).with(:email, :blocked)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context '!blocked_email?' do
|
context 'when e-mail provider is not blocked' do
|
||||||
let(:blocked_email) { false }
|
let(:blocked_email) { false }
|
||||||
|
|
||||||
it 'not calls errors.add' do
|
it 'does not add errors' do
|
||||||
expect(errors).not_to have_received(:add).with(:email, :blocked)
|
expect(subject).not_to have_received(:add).with(:email, :blocked)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when canonical e-mail is blocked' do
|
||||||
|
let(:other_user) { Fabricate(:user, email: 'i.n.f.o@mail.com') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
other_user.account.suspend!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds error' do
|
||||||
|
expect(subject).to have_received(:add).with(:email, :taken)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
48
spec/workers/web/push_notification_worker_spec.rb
Normal file
48
spec/workers/web/push_notification_worker_spec.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Web::PushNotificationWorker do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
let(:p256dh) { 'BN4GvZtEZiZuqFxSKVZfSfluwKBD7UxHNBmWkfiZfCtgDE8Bwh-_MtLXbBxTBAWH9r7IPKL0lhdcaqtL1dfxU5E=' }
|
||||||
|
let(:auth) { 'Q2BoAjC09xH3ywDLNJr-dA==' }
|
||||||
|
let(:endpoint) { 'https://updates.push.services.mozilla.com/push/v1/subscription-id' }
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:notification) { Fabricate(:notification) }
|
||||||
|
let(:subscription) { Fabricate(:web_push_subscription, user_id: user.id, key_p256dh: p256dh, key_auth: auth, endpoint: endpoint, data: { alerts: { notification.type => true } }) }
|
||||||
|
let(:vapid_public_key) { 'BB37UCyc8LLX4PNQSe-04vSFvpUWGrENubUaslVFM_l5TxcGVMY0C3RXPeUJAQHKYlcOM2P4vTYmkoo0VZGZTM4=' }
|
||||||
|
let(:vapid_private_key) { 'OPrw1Sum3gRoL4-DXfSCC266r-qfFSRZrnj8MgIhRHg=' }
|
||||||
|
let(:vapid_key) { Webpush::VapidKey.from_keys(vapid_public_key, vapid_private_key) }
|
||||||
|
let(:contact_email) { 'sender@example.com' }
|
||||||
|
let(:ciphertext) { "+\xB8\xDBT}\x13\xB6\xDD.\xF9\xB0\xA7\xC8\xD2\x80\xFD\x99#\xF7\xAC\x83\xA4\xDB,\x1F\xB5\xB9w\x85>\xF7\xADr" }
|
||||||
|
let(:salt) { "X\x97\x953\xE4X\xF8_w\xE7T\x95\xC51q\xFE" }
|
||||||
|
let(:server_public_key) { "\x04\b-RK9w\xDD$\x16lFz\xF9=\xB4~\xC6\x12k\xF3\xF40t\xA9\xC1\fR\xC3\x81\x80\xAC\f\x7F\xE4\xCC\x8E\xC2\x88 n\x8BB\xF1\x9C\x14\a\xFA\x8D\xC9\x80\xA1\xDDyU\\&c\x01\x88#\x118Ua" }
|
||||||
|
let(:shared_secret) { "\t\xA7&\x85\t\xC5m\b\xA8\xA7\xF8B{1\xADk\xE1y'm\xEDE\xEC\xDD\xEDj\xB3$s\xA9\xDA\xF0" }
|
||||||
|
let(:payload) { { ciphertext: ciphertext, salt: salt, server_public_key: server_public_key, shared_secret: shared_secret } }
|
||||||
|
|
||||||
|
describe 'perform' do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(subscription.class).to receive(:contact_email).and_return(contact_email)
|
||||||
|
allow_any_instance_of(subscription.class).to receive(:vapid_key).and_return(vapid_key)
|
||||||
|
allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
|
||||||
|
allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
|
||||||
|
|
||||||
|
stub_request(:post, endpoint).to_return(status: 201, body: '')
|
||||||
|
|
||||||
|
subject.perform(subscription.id, notification.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls the relevant service with the correct headers' do
|
||||||
|
expect(a_request(:post, endpoint).with(headers: {
|
||||||
|
'Content-Encoding' => 'aesgcm',
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Crypto-Key' => 'dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=' + vapid_public_key.delete('='),
|
||||||
|
'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g',
|
||||||
|
'Ttl' => '172800',
|
||||||
|
'Urgency' => 'normal',
|
||||||
|
'Authorization' => 'WebPush jwt.encoded.payload',
|
||||||
|
}, body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr")).to have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
427
yarn.lock
427
yarn.lock
|
@ -16,24 +16,24 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/highlight" "^7.12.13"
|
"@babel/highlight" "^7.12.13"
|
||||||
|
|
||||||
"@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8":
|
"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.15", "@babel/compat-data@^7.13.8":
|
||||||
version "7.13.12"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1"
|
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4"
|
||||||
integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==
|
integrity sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==
|
||||||
|
|
||||||
"@babel/core@^7.1.0", "@babel/core@^7.13.14", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
|
"@babel/core@^7.1.0", "@babel/core@^7.13.15", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
|
||||||
version "7.13.14"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06"
|
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.15.tgz#a6d40917df027487b54312202a06812c4f7792d0"
|
||||||
integrity sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==
|
integrity sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.12.13"
|
"@babel/code-frame" "^7.12.13"
|
||||||
"@babel/generator" "^7.13.9"
|
"@babel/generator" "^7.13.9"
|
||||||
"@babel/helper-compilation-targets" "^7.13.13"
|
"@babel/helper-compilation-targets" "^7.13.13"
|
||||||
"@babel/helper-module-transforms" "^7.13.14"
|
"@babel/helper-module-transforms" "^7.13.14"
|
||||||
"@babel/helpers" "^7.13.10"
|
"@babel/helpers" "^7.13.10"
|
||||||
"@babel/parser" "^7.13.13"
|
"@babel/parser" "^7.13.15"
|
||||||
"@babel/template" "^7.12.13"
|
"@babel/template" "^7.12.13"
|
||||||
"@babel/traverse" "^7.13.13"
|
"@babel/traverse" "^7.13.15"
|
||||||
"@babel/types" "^7.13.14"
|
"@babel/types" "^7.13.14"
|
||||||
convert-source-map "^1.7.0"
|
convert-source-map "^1.7.0"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
"@babel/helper-annotate-as-pure" "^7.12.13"
|
"@babel/helper-annotate-as-pure" "^7.12.13"
|
||||||
"@babel/types" "^7.12.13"
|
"@babel/types" "^7.12.13"
|
||||||
|
|
||||||
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.13", "@babel/helper-compilation-targets@^7.13.8":
|
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.13", "@babel/helper-compilation-targets@^7.13.8":
|
||||||
version "7.13.13"
|
version "7.13.13"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5"
|
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5"
|
||||||
integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==
|
integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==
|
||||||
|
@ -91,10 +91,10 @@
|
||||||
browserslist "^4.14.5"
|
browserslist "^4.14.5"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
"@babel/helper-create-class-features-plugin@^7.13.0":
|
"@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.13.11":
|
||||||
version "7.13.0"
|
version "7.13.11"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.0.tgz#28d04ad9cfbd1ed1d8b988c9ea7b945263365846"
|
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6"
|
||||||
integrity sha512-twwzhthM4/+6o9766AW2ZBHpIHPSGrPGk1+WfHiu13u/lBnggXGNYCpeAyVfNwGDKfkhEDp+WOD/xafoJ2iLjA==
|
integrity sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-function-name" "^7.12.13"
|
"@babel/helper-function-name" "^7.12.13"
|
||||||
"@babel/helper-member-expression-to-functions" "^7.13.0"
|
"@babel/helper-member-expression-to-functions" "^7.13.0"
|
||||||
|
@ -110,10 +110,10 @@
|
||||||
"@babel/helper-annotate-as-pure" "^7.12.13"
|
"@babel/helper-annotate-as-pure" "^7.12.13"
|
||||||
regexpu-core "^4.7.1"
|
regexpu-core "^4.7.1"
|
||||||
|
|
||||||
"@babel/helper-define-polyfill-provider@^0.1.2":
|
"@babel/helper-define-polyfill-provider@^0.2.0":
|
||||||
version "0.1.2"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.2.tgz#619f01afe1deda460676c25c463b42eaefdb71a2"
|
resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz#a640051772045fedaaecc6f0c6c69f02bdd34bf1"
|
||||||
integrity sha512-hWeolZJivTNGHXHzJjQz/NwDaG4mGXf22ZroOP8bQYgvHNzaQ5tylsVbAcAS2oDjXBwpu8qH2I/654QFS2rDpw==
|
integrity sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-compilation-targets" "^7.13.0"
|
"@babel/helper-compilation-targets" "^7.13.0"
|
||||||
"@babel/helper-module-imports" "^7.12.13"
|
"@babel/helper-module-imports" "^7.12.13"
|
||||||
|
@ -176,21 +176,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.13.12"
|
"@babel/types" "^7.13.12"
|
||||||
|
|
||||||
"@babel/helper-module-imports@^7.0.0-beta.49":
|
"@babel/helper-module-imports@^7.0.0-beta.49", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12":
|
||||||
version "7.12.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb"
|
|
||||||
integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.12.5"
|
|
||||||
|
|
||||||
"@babel/helper-module-imports@^7.12.13":
|
|
||||||
version "7.12.13"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0"
|
|
||||||
integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.12.13"
|
|
||||||
|
|
||||||
"@babel/helper-module-imports@^7.13.12":
|
|
||||||
version "7.13.12"
|
version "7.13.12"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
|
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
|
||||||
integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
|
integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
|
||||||
|
@ -328,10 +314,10 @@
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.13", "@babel/parser@^7.7.0":
|
"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.15", "@babel/parser@^7.7.0":
|
||||||
version "7.13.13"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8"
|
||||||
integrity sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==
|
integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==
|
||||||
|
|
||||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12":
|
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12":
|
||||||
version "7.13.12"
|
version "7.13.12"
|
||||||
|
@ -342,10 +328,10 @@
|
||||||
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
|
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
|
||||||
"@babel/plugin-proposal-optional-chaining" "^7.13.12"
|
"@babel/plugin-proposal-optional-chaining" "^7.13.12"
|
||||||
|
|
||||||
"@babel/plugin-proposal-async-generator-functions@^7.13.8":
|
"@babel/plugin-proposal-async-generator-functions@^7.13.15":
|
||||||
version "7.13.8"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz#80e549df273a3b3050431b148c892491df1bcc5b"
|
||||||
integrity sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==
|
integrity sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.13.0"
|
"@babel/helper-plugin-utils" "^7.13.0"
|
||||||
"@babel/helper-remap-async-to-generator" "^7.13.0"
|
"@babel/helper-remap-async-to-generator" "^7.13.0"
|
||||||
|
@ -359,12 +345,12 @@
|
||||||
"@babel/helper-create-class-features-plugin" "^7.13.0"
|
"@babel/helper-create-class-features-plugin" "^7.13.0"
|
||||||
"@babel/helper-plugin-utils" "^7.13.0"
|
"@babel/helper-plugin-utils" "^7.13.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-decorators@^7.13.5":
|
"@babel/plugin-proposal-decorators@^7.13.15":
|
||||||
version "7.13.5"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.13.5.tgz#d28071457a5ba8ee1394b23e38d5dcf32ea20ef7"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.13.15.tgz#e91ccfef2dc24dd5bd5dcc9fc9e2557c684ecfb8"
|
||||||
integrity sha512-i0GDfVNuoapwiheevUOuSW67mInqJ8qw7uWfpjNVeHMn143kXblEy/bmL9AdZ/0yf/4BMQeWXezK0tQIvNPqag==
|
integrity sha512-ibAMAqUm97yzi+LPgdr5Nqb9CMkeieGHvwPg1ywSGjZrZHQEGqE01HmOio8kxRpA/+VtOHouIVy2FMpBbtltjA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-create-class-features-plugin" "^7.13.0"
|
"@babel/helper-create-class-features-plugin" "^7.13.11"
|
||||||
"@babel/helper-plugin-utils" "^7.13.0"
|
"@babel/helper-plugin-utils" "^7.13.0"
|
||||||
"@babel/plugin-syntax-decorators" "^7.12.13"
|
"@babel/plugin-syntax-decorators" "^7.12.13"
|
||||||
|
|
||||||
|
@ -796,10 +782,10 @@
|
||||||
"@babel/helper-annotate-as-pure" "^7.10.4"
|
"@babel/helper-annotate-as-pure" "^7.10.4"
|
||||||
"@babel/helper-plugin-utils" "^7.10.4"
|
"@babel/helper-plugin-utils" "^7.10.4"
|
||||||
|
|
||||||
"@babel/plugin-transform-regenerator@^7.12.13":
|
"@babel/plugin-transform-regenerator@^7.13.15":
|
||||||
version "7.12.13"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz#b628bcc9c85260ac1aeb05b45bde25210194a2f5"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz#e5eb28945bf8b6563e7f818945f966a8d2997f39"
|
||||||
integrity sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==
|
integrity sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-transform "^0.14.2"
|
regenerator-transform "^0.14.2"
|
||||||
|
|
||||||
|
@ -810,16 +796,16 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.12.13"
|
"@babel/helper-plugin-utils" "^7.12.13"
|
||||||
|
|
||||||
"@babel/plugin-transform-runtime@^7.13.10":
|
"@babel/plugin-transform-runtime@^7.13.15":
|
||||||
version "7.13.10"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz#2eddf585dd066b84102517e10a577f24f76a9cd7"
|
||||||
integrity sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==
|
integrity sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-imports" "^7.12.13"
|
"@babel/helper-module-imports" "^7.13.12"
|
||||||
"@babel/helper-plugin-utils" "^7.13.0"
|
"@babel/helper-plugin-utils" "^7.13.0"
|
||||||
babel-plugin-polyfill-corejs2 "^0.1.4"
|
babel-plugin-polyfill-corejs2 "^0.2.0"
|
||||||
babel-plugin-polyfill-corejs3 "^0.1.3"
|
babel-plugin-polyfill-corejs3 "^0.2.0"
|
||||||
babel-plugin-polyfill-regenerator "^0.1.2"
|
babel-plugin-polyfill-regenerator "^0.2.0"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-shorthand-properties@^7.12.13":
|
"@babel/plugin-transform-shorthand-properties@^7.12.13":
|
||||||
|
@ -873,17 +859,17 @@
|
||||||
"@babel/helper-create-regexp-features-plugin" "^7.12.13"
|
"@babel/helper-create-regexp-features-plugin" "^7.12.13"
|
||||||
"@babel/helper-plugin-utils" "^7.12.13"
|
"@babel/helper-plugin-utils" "^7.12.13"
|
||||||
|
|
||||||
"@babel/preset-env@^7.13.12":
|
"@babel/preset-env@^7.13.15":
|
||||||
version "7.13.12"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.12.tgz#6dff470478290582ac282fb77780eadf32480237"
|
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.15.tgz#c8a6eb584f96ecba183d3d414a83553a599f478f"
|
||||||
integrity sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==
|
integrity sha512-D4JAPMXcxk69PKe81jRJ21/fP/uYdcTZ3hJDF5QX2HSI9bBxxYw/dumdR6dGumhjxlprHPE4XWoPaqzZUVy2MA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/compat-data" "^7.13.12"
|
"@babel/compat-data" "^7.13.15"
|
||||||
"@babel/helper-compilation-targets" "^7.13.10"
|
"@babel/helper-compilation-targets" "^7.13.13"
|
||||||
"@babel/helper-plugin-utils" "^7.13.0"
|
"@babel/helper-plugin-utils" "^7.13.0"
|
||||||
"@babel/helper-validator-option" "^7.12.17"
|
"@babel/helper-validator-option" "^7.12.17"
|
||||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.13.12"
|
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.13.12"
|
||||||
"@babel/plugin-proposal-async-generator-functions" "^7.13.8"
|
"@babel/plugin-proposal-async-generator-functions" "^7.13.15"
|
||||||
"@babel/plugin-proposal-class-properties" "^7.13.0"
|
"@babel/plugin-proposal-class-properties" "^7.13.0"
|
||||||
"@babel/plugin-proposal-dynamic-import" "^7.13.8"
|
"@babel/plugin-proposal-dynamic-import" "^7.13.8"
|
||||||
"@babel/plugin-proposal-export-namespace-from" "^7.12.13"
|
"@babel/plugin-proposal-export-namespace-from" "^7.12.13"
|
||||||
|
@ -931,7 +917,7 @@
|
||||||
"@babel/plugin-transform-object-super" "^7.12.13"
|
"@babel/plugin-transform-object-super" "^7.12.13"
|
||||||
"@babel/plugin-transform-parameters" "^7.13.0"
|
"@babel/plugin-transform-parameters" "^7.13.0"
|
||||||
"@babel/plugin-transform-property-literals" "^7.12.13"
|
"@babel/plugin-transform-property-literals" "^7.12.13"
|
||||||
"@babel/plugin-transform-regenerator" "^7.12.13"
|
"@babel/plugin-transform-regenerator" "^7.13.15"
|
||||||
"@babel/plugin-transform-reserved-words" "^7.12.13"
|
"@babel/plugin-transform-reserved-words" "^7.12.13"
|
||||||
"@babel/plugin-transform-shorthand-properties" "^7.12.13"
|
"@babel/plugin-transform-shorthand-properties" "^7.12.13"
|
||||||
"@babel/plugin-transform-spread" "^7.13.0"
|
"@babel/plugin-transform-spread" "^7.13.0"
|
||||||
|
@ -941,10 +927,10 @@
|
||||||
"@babel/plugin-transform-unicode-escapes" "^7.12.13"
|
"@babel/plugin-transform-unicode-escapes" "^7.12.13"
|
||||||
"@babel/plugin-transform-unicode-regex" "^7.12.13"
|
"@babel/plugin-transform-unicode-regex" "^7.12.13"
|
||||||
"@babel/preset-modules" "^0.1.4"
|
"@babel/preset-modules" "^0.1.4"
|
||||||
"@babel/types" "^7.13.12"
|
"@babel/types" "^7.13.14"
|
||||||
babel-plugin-polyfill-corejs2 "^0.1.4"
|
babel-plugin-polyfill-corejs2 "^0.2.0"
|
||||||
babel-plugin-polyfill-corejs3 "^0.1.3"
|
babel-plugin-polyfill-corejs3 "^0.2.0"
|
||||||
babel-plugin-polyfill-regenerator "^0.1.2"
|
babel-plugin-polyfill-regenerator "^0.2.0"
|
||||||
core-js-compat "^3.9.0"
|
core-js-compat "^3.9.0"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
|
@ -1002,21 +988,21 @@
|
||||||
"@babel/parser" "^7.12.13"
|
"@babel/parser" "^7.12.13"
|
||||||
"@babel/types" "^7.12.13"
|
"@babel/types" "^7.12.13"
|
||||||
|
|
||||||
"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.13", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.7.0":
|
"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.13", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.13.15", "@babel/traverse@^7.7.0":
|
||||||
version "7.13.13"
|
version "7.13.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.13.tgz#39aa9c21aab69f74d948a486dd28a2dbdbf5114d"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.15.tgz#c38bf7679334ddd4028e8e1f7b3aa5019f0dada7"
|
||||||
integrity sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==
|
integrity sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.12.13"
|
"@babel/code-frame" "^7.12.13"
|
||||||
"@babel/generator" "^7.13.9"
|
"@babel/generator" "^7.13.9"
|
||||||
"@babel/helper-function-name" "^7.12.13"
|
"@babel/helper-function-name" "^7.12.13"
|
||||||
"@babel/helper-split-export-declaration" "^7.12.13"
|
"@babel/helper-split-export-declaration" "^7.12.13"
|
||||||
"@babel/parser" "^7.13.13"
|
"@babel/parser" "^7.13.15"
|
||||||
"@babel/types" "^7.13.13"
|
"@babel/types" "^7.13.14"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.13", "@babel/types@^7.13.14", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
|
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.14", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
|
||||||
version "7.13.14"
|
version "7.13.14"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
|
||||||
integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
|
integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
|
||||||
|
@ -1025,15 +1011,6 @@
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@babel/types@^7.12.5":
|
|
||||||
version "7.13.13"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.13.tgz#dcd8b815b38f537a3697ce84c8e3cc62197df96f"
|
|
||||||
integrity sha512-kt+EpC6qDfIaqlP+DIbIJOclYy/A1YXs9dAf/ljbi+39Bcbc073H6jKVpXEr/EoIh5anGn5xq/yRVzKl+uIc9w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-validator-identifier" "^7.12.11"
|
|
||||||
lodash "^4.17.19"
|
|
||||||
to-fast-properties "^2.0.0"
|
|
||||||
|
|
||||||
"@bcoe/v8-coverage@^0.2.3":
|
"@bcoe/v8-coverage@^0.2.3":
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
|
@ -2287,29 +2264,29 @@ babel-plugin-macros@^2.8.0:
|
||||||
cosmiconfig "^6.0.0"
|
cosmiconfig "^6.0.0"
|
||||||
resolve "^1.12.0"
|
resolve "^1.12.0"
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs2@^0.1.4:
|
babel-plugin-polyfill-corejs2@^0.2.0:
|
||||||
version "0.1.5"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.5.tgz#8fc4779965311393594a1b9ad3adefab3860c8fe"
|
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz#686775bf9a5aa757e10520903675e3889caeedc4"
|
||||||
integrity sha512-5IzdFIjYWqlOFVr/hMYUpc+5fbfuvJTAISwIY58jhH++ZtawtNlcJnxAixlk8ahVwHCz1ipW/kpXYliEBp66wg==
|
integrity sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/compat-data" "^7.13.0"
|
"@babel/compat-data" "^7.13.11"
|
||||||
"@babel/helper-define-polyfill-provider" "^0.1.2"
|
"@babel/helper-define-polyfill-provider" "^0.2.0"
|
||||||
semver "^6.1.1"
|
semver "^6.1.1"
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs3@^0.1.3:
|
babel-plugin-polyfill-corejs3@^0.2.0:
|
||||||
version "0.1.4"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.4.tgz#2ae290200e953bade30907b7a3bebcb696e6c59d"
|
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz#f4b4bb7b19329827df36ff56f6e6d367026cb7a2"
|
||||||
integrity sha512-ysSzFn/qM8bvcDAn4mC7pKk85Y5dVaoa9h4u0mHxOEpDzabsseONhUpR7kHxpUinfj1bjU7mUZqD23rMZBoeSg==
|
integrity sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-define-polyfill-provider" "^0.1.2"
|
"@babel/helper-define-polyfill-provider" "^0.2.0"
|
||||||
core-js-compat "^3.8.1"
|
core-js-compat "^3.9.1"
|
||||||
|
|
||||||
babel-plugin-polyfill-regenerator@^0.1.2:
|
babel-plugin-polyfill-regenerator@^0.2.0:
|
||||||
version "0.1.3"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.1.3.tgz#350f857225fc640ae1ec78d1536afcbb457db841"
|
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz#853f5f5716f4691d98c84f8069c7636ea8da7ab8"
|
||||||
integrity sha512-hRjTJQiOYt/wBKEc+8V8p9OJ9799blAJcuKzn1JXh3pApHoWl1Emxh2BHc6MC7Qt6bbr3uDpNxaYQnATLIudEg==
|
integrity sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-define-polyfill-provider" "^0.1.2"
|
"@babel/helper-define-polyfill-provider" "^0.2.0"
|
||||||
|
|
||||||
babel-plugin-preval@^5.0.0:
|
babel-plugin-preval@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
|
@ -2882,10 +2859,10 @@ char-regex@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||||
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
||||||
|
|
||||||
"chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.1:
|
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1:
|
||||||
version "3.4.1"
|
version "3.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
|
||||||
integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==
|
integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch "~3.1.1"
|
anymatch "~3.1.1"
|
||||||
braces "~3.0.2"
|
braces "~3.0.2"
|
||||||
|
@ -2893,9 +2870,9 @@ char-regex@^1.0.2:
|
||||||
is-binary-path "~2.1.0"
|
is-binary-path "~2.1.0"
|
||||||
is-glob "~4.0.1"
|
is-glob "~4.0.1"
|
||||||
normalize-path "~3.0.0"
|
normalize-path "~3.0.0"
|
||||||
readdirp "~3.4.0"
|
readdirp "~3.5.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.1.2"
|
fsevents "~2.3.1"
|
||||||
|
|
||||||
chokidar@^2.1.8:
|
chokidar@^2.1.8:
|
||||||
version "2.1.8"
|
version "2.1.8"
|
||||||
|
@ -2966,10 +2943,10 @@ class-utils@^0.3.5:
|
||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
classnames@^2.2.5:
|
classnames@^2.2.5, classnames@^2.3.1:
|
||||||
version "2.2.6"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
|
||||||
clean-stack@^2.0.0:
|
clean-stack@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
|
@ -3255,10 +3232,10 @@ copy-descriptor@^0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||||
|
|
||||||
core-js-compat@^3.8.1, core-js-compat@^3.9.0:
|
core-js-compat@^3.9.0, core-js-compat@^3.9.1:
|
||||||
version "3.9.0"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.0.tgz#29da39385f16b71e1915565aa0385c4e0963ad56"
|
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.10.1.tgz#62183a3a77ceeffcc420d907a3e6fc67d9b27f1c"
|
||||||
integrity sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ==
|
integrity sha512-ZHQTdTPkqvw2CeHiZC970NNJcnwzT6YIueDMASKt+p3WbZsLXOcoD392SkcWhkC0wBBHhlfhqGKKsNCQUozYtg==
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist "^4.16.3"
|
browserslist "^4.16.3"
|
||||||
semver "7.0.0"
|
semver "7.0.0"
|
||||||
|
@ -3424,23 +3401,22 @@ css-list-helpers@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
tcomb "^2.5.0"
|
tcomb "^2.5.0"
|
||||||
|
|
||||||
css-loader@^5.2.0:
|
css-loader@^5.2.2:
|
||||||
version "5.2.0"
|
version "5.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.0.tgz#a9ecda190500863673ce4434033710404efbff00"
|
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.2.tgz#65f2c1482255f15847ecad6cbc515cae8a5b234e"
|
||||||
integrity sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==
|
integrity sha512-IS722y7Lh2Yq+acMR74tdf3faMOLRP2RfLwS0VzSS7T98IHtacMWJLku3A0OBTFHB07zAa4nWBhA8gfxwQVWGQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase "^6.2.0"
|
camelcase "^6.2.0"
|
||||||
cssesc "^3.0.0"
|
|
||||||
icss-utils "^5.1.0"
|
icss-utils "^5.1.0"
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
postcss "^8.2.8"
|
postcss "^8.2.10"
|
||||||
postcss-modules-extract-imports "^3.0.0"
|
postcss-modules-extract-imports "^3.0.0"
|
||||||
postcss-modules-local-by-default "^4.0.0"
|
postcss-modules-local-by-default "^4.0.0"
|
||||||
postcss-modules-scope "^3.0.0"
|
postcss-modules-scope "^3.0.0"
|
||||||
postcss-modules-values "^4.0.0"
|
postcss-modules-values "^4.0.0"
|
||||||
postcss-value-parser "^4.1.0"
|
postcss-value-parser "^4.1.0"
|
||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
semver "^7.3.4"
|
semver "^7.3.5"
|
||||||
|
|
||||||
css-select-base-adapter@^0.1.1:
|
css-select-base-adapter@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
|
@ -3502,10 +3478,10 @@ cssesc@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||||
|
|
||||||
cssnano-preset-default@^4.0.7:
|
cssnano-preset-default@^4.0.8:
|
||||||
version "4.0.7"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
|
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
|
||||||
integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
|
integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
css-declaration-sorter "^4.0.1"
|
css-declaration-sorter "^4.0.1"
|
||||||
cssnano-util-raw-cache "^4.0.1"
|
cssnano-util-raw-cache "^4.0.1"
|
||||||
|
@ -3535,7 +3511,7 @@ cssnano-preset-default@^4.0.7:
|
||||||
postcss-ordered-values "^4.1.2"
|
postcss-ordered-values "^4.1.2"
|
||||||
postcss-reduce-initial "^4.0.3"
|
postcss-reduce-initial "^4.0.3"
|
||||||
postcss-reduce-transforms "^4.0.2"
|
postcss-reduce-transforms "^4.0.2"
|
||||||
postcss-svgo "^4.0.2"
|
postcss-svgo "^4.0.3"
|
||||||
postcss-unique-selectors "^4.0.1"
|
postcss-unique-selectors "^4.0.1"
|
||||||
|
|
||||||
cssnano-util-get-arguments@^4.0.0:
|
cssnano-util-get-arguments@^4.0.0:
|
||||||
|
@ -3560,13 +3536,13 @@ cssnano-util-same-parent@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
|
resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
|
||||||
integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
|
integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
|
||||||
|
|
||||||
cssnano@^4.1.10:
|
cssnano@^4.1.11:
|
||||||
version "4.1.10"
|
version "4.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
|
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.11.tgz#c7b5f5b81da269cb1fd982cb960c1200910c9a99"
|
||||||
integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
|
integrity sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==
|
||||||
dependencies:
|
dependencies:
|
||||||
cosmiconfig "^5.0.0"
|
cosmiconfig "^5.0.0"
|
||||||
cssnano-preset-default "^4.0.7"
|
cssnano-preset-default "^4.0.8"
|
||||||
is-resolvable "^1.0.0"
|
is-resolvable "^1.0.0"
|
||||||
postcss "^7.0.0"
|
postcss "^7.0.0"
|
||||||
|
|
||||||
|
@ -3761,10 +3737,10 @@ delegates@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||||
|
|
||||||
denque@^1.4.1:
|
denque@^1.5.0:
|
||||||
version "1.4.1"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
|
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
|
||||||
integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
|
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
|
||||||
|
|
||||||
depd@~1.1.2:
|
depd@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
|
@ -4298,15 +4274,15 @@ eslint-plugin-jsx-a11y@~6.4.1:
|
||||||
jsx-ast-utils "^3.1.0"
|
jsx-ast-utils "^3.1.0"
|
||||||
language-tags "^1.0.5"
|
language-tags "^1.0.5"
|
||||||
|
|
||||||
eslint-plugin-promise@~4.3.1:
|
eslint-plugin-promise@~5.1.0:
|
||||||
version "4.3.1"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz#61485df2a359e03149fdafc0a68b0e030ad2ac45"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz#fb2188fb734e4557993733b41aa1a688f46c6f24"
|
||||||
integrity sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==
|
integrity sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==
|
||||||
|
|
||||||
eslint-plugin-react@~7.23.1:
|
eslint-plugin-react@~7.23.2:
|
||||||
version "7.23.1"
|
version "7.23.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.23.1.tgz#f1a2e844c0d1967c822388204a8bc4dee8415b11"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz#2d2291b0f95c03728b55869f01102290e792d494"
|
||||||
integrity sha512-MvFGhZjI8Z4HusajmSw0ougGrq3Gs4vT/0WgwksZgf5RrLrRa2oYAw56okU4tZJl8+j7IYNuTM+2RnFEuTSdRQ==
|
integrity sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes "^3.1.3"
|
array-includes "^3.1.3"
|
||||||
array.prototype.flatmap "^1.2.4"
|
array.prototype.flatmap "^1.2.4"
|
||||||
|
@ -4393,10 +4369,10 @@ eslint@^2.7.0:
|
||||||
text-table "~0.2.0"
|
text-table "~0.2.0"
|
||||||
user-home "^2.0.0"
|
user-home "^2.0.0"
|
||||||
|
|
||||||
eslint@^7.23.0:
|
eslint@^7.24.0:
|
||||||
version "7.23.0"
|
version "7.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.23.0.tgz#8d029d252f6e8cf45894b4bee08f5493f8e94325"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.24.0.tgz#2e44fa62d93892bfdb100521f17345ba54b8513a"
|
||||||
integrity sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==
|
integrity sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "7.12.11"
|
"@babel/code-frame" "7.12.11"
|
||||||
"@eslint/eslintrc" "^0.4.0"
|
"@eslint/eslintrc" "^0.4.0"
|
||||||
|
@ -4992,11 +4968,16 @@ fsevents@^1.2.7:
|
||||||
bindings "^1.5.0"
|
bindings "^1.5.0"
|
||||||
nan "^2.12.1"
|
nan "^2.12.1"
|
||||||
|
|
||||||
fsevents@^2.1.2, fsevents@~2.1.2:
|
fsevents@^2.1.2:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
|
||||||
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
|
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
|
||||||
|
|
||||||
|
fsevents@~2.3.1:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||||
|
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||||
|
|
||||||
function-bind@^1.1.1:
|
function-bind@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||||
|
@ -5402,11 +5383,6 @@ hsla-regex@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
|
resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
|
||||||
integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
|
integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
|
||||||
|
|
||||||
html-comment-regex@^1.1.0:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
|
|
||||||
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
|
|
||||||
|
|
||||||
html-encoding-sniffer@^2.0.1:
|
html-encoding-sniffer@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
|
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
|
||||||
|
@ -6074,13 +6050,6 @@ is-string@^1.0.5:
|
||||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
||||||
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
|
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
|
||||||
|
|
||||||
is-svg@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
|
|
||||||
integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
|
|
||||||
dependencies:
|
|
||||||
html-comment-regex "^1.1.0"
|
|
||||||
|
|
||||||
is-symbol@^1.0.2:
|
is-symbol@^1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
||||||
|
@ -6603,10 +6572,10 @@ js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4:
|
||||||
argparse "^1.0.7"
|
argparse "^1.0.7"
|
||||||
esprima "^4.0.0"
|
esprima "^4.0.0"
|
||||||
|
|
||||||
js-yaml@^4.0.0:
|
js-yaml@^4.1.0:
|
||||||
version "4.0.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||||
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
|
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse "^2.0.1"
|
argparse "^2.0.1"
|
||||||
|
|
||||||
|
@ -6916,11 +6885,6 @@ lodash.defaults@^4.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
|
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
|
||||||
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
|
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
|
||||||
|
|
||||||
lodash.escaperegexp@^4.0:
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
|
||||||
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
|
|
||||||
|
|
||||||
lodash.get@^4.0:
|
lodash.get@^4.0:
|
||||||
version "4.4.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||||
|
@ -7183,10 +7147,10 @@ min-indent@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||||
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
||||||
|
|
||||||
mini-css-extract-plugin@^1.4.0:
|
mini-css-extract-plugin@^1.5.0:
|
||||||
version "1.4.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.4.0.tgz#c8e571c4b6d63afa56c47260343adf623349c473"
|
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.5.0.tgz#69bee3b273d2d4ee8649a2eb409514b7df744a27"
|
||||||
integrity sha512-DyQr5DhXXARKZoc4kwvCvD95kh69dUupfuKOmBUqZ4kBTmRaRZcU32lYu3cLd6nEGXhQ1l7LzZ3F/CjItaY6VQ==
|
integrity sha512-SIbuLMv6jsk1FnLIU5OUG/+VMGUprEjM1+o2trOAx8i5KOKMrhyezb1dJ4Ugsykb8Jgq8/w5NEopy6escV9G7g==
|
||||||
dependencies:
|
dependencies:
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
|
@ -7346,10 +7310,10 @@ nan@^2.12.1:
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
||||||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
||||||
|
|
||||||
nanoid@^3.1.20:
|
nanoid@^3.1.22:
|
||||||
version "3.1.20"
|
version "3.1.22"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
|
||||||
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
|
integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
|
||||||
|
|
||||||
nanomatch@^1.2.9:
|
nanomatch@^1.2.9:
|
||||||
version "1.2.13"
|
version "1.2.13"
|
||||||
|
@ -8485,12 +8449,11 @@ postcss-selector-parser@^6.0.4:
|
||||||
uniq "^1.0.1"
|
uniq "^1.0.1"
|
||||||
util-deprecate "^1.0.2"
|
util-deprecate "^1.0.2"
|
||||||
|
|
||||||
postcss-svgo@^4.0.2:
|
postcss-svgo@^4.0.3:
|
||||||
version "4.0.2"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
|
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e"
|
||||||
integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
|
integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==
|
||||||
dependencies:
|
dependencies:
|
||||||
is-svg "^3.0.0"
|
|
||||||
postcss "^7.0.0"
|
postcss "^7.0.0"
|
||||||
postcss-value-parser "^3.0.0"
|
postcss-value-parser "^3.0.0"
|
||||||
svgo "^1.0.0"
|
svgo "^1.0.0"
|
||||||
|
@ -8533,13 +8496,13 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27, postcss@^7.0.32:
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
supports-color "^6.1.0"
|
supports-color "^6.1.0"
|
||||||
|
|
||||||
postcss@^8.2.8:
|
postcss@^8.2.10:
|
||||||
version "8.2.8"
|
version "8.2.10"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b"
|
||||||
integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
|
integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
colorette "^1.2.2"
|
colorette "^1.2.2"
|
||||||
nanoid "^3.1.20"
|
nanoid "^3.1.22"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
postgres-array@~2.0.0:
|
postgres-array@~2.0.0:
|
||||||
|
@ -9150,10 +9113,10 @@ readdirp@^2.2.1:
|
||||||
micromatch "^3.1.10"
|
micromatch "^3.1.10"
|
||||||
readable-stream "^2.0.2"
|
readable-stream "^2.0.2"
|
||||||
|
|
||||||
readdirp@~3.4.0:
|
readdirp@~3.5.0:
|
||||||
version "3.4.0"
|
version "3.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
|
||||||
integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
|
integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
|
@ -9174,10 +9137,10 @@ redent@^3.0.0:
|
||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
redis-commands@^1.5.0:
|
redis-commands@^1.7.0:
|
||||||
version "1.6.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23"
|
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
|
||||||
integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==
|
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
|
||||||
|
|
||||||
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
|
@ -9191,13 +9154,13 @@ redis-parser@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
redis-errors "^1.0.0"
|
redis-errors "^1.0.0"
|
||||||
|
|
||||||
redis@^3.0.2:
|
redis@^3.1.1:
|
||||||
version "3.0.2"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a"
|
resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.1.tgz#a44bee7c072dcf685e139048d6a1a4d3b00f5d01"
|
||||||
integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==
|
integrity sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg==
|
||||||
dependencies:
|
dependencies:
|
||||||
denque "^1.4.1"
|
denque "^1.5.0"
|
||||||
redis-commands "^1.5.0"
|
redis-commands "^1.7.0"
|
||||||
redis-errors "^1.2.0"
|
redis-errors "^1.2.0"
|
||||||
redis-parser "^3.0.0"
|
redis-parser "^3.0.0"
|
||||||
|
|
||||||
|
@ -9633,12 +9596,12 @@ sass-loader@^10.1.1:
|
||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
|
|
||||||
sass@^1.32.8:
|
sass@^1.32.10:
|
||||||
version "1.32.8"
|
version "1.32.10"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.8.tgz#f16a9abd8dc530add8834e506878a2808c037bdc"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.10.tgz#d40da4e20031b450359ee1c7e69bc8cc89569241"
|
||||||
integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==
|
integrity sha512-Nx0pcWoonAkn7CRp0aE/hket1UP97GiR1IFw3kcjV3pnenhWgZEWUf0ZcfPOV2fK52fnOcK3JdC/YYZ9E47DTQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=2.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
sax@~1.2.4:
|
sax@~1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
|
@ -9722,10 +9685,10 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
|
semver@^7.2.1, semver@^7.3.2, semver@^7.3.5:
|
||||||
version "7.3.4"
|
version "7.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||||
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
|
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
|
@ -10118,9 +10081,9 @@ sshpk@^1.7.0:
|
||||||
tweetnacl "~0.14.0"
|
tweetnacl "~0.14.0"
|
||||||
|
|
||||||
ssri@^6.0.1:
|
ssri@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
|
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
|
||||||
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
|
integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
figgy-pudding "^3.5.1"
|
figgy-pudding "^3.5.1"
|
||||||
|
|
||||||
|
@ -11213,15 +11176,14 @@ webidl-conversions@^6.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
|
||||||
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
|
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
|
||||||
|
|
||||||
webpack-assets-manifest@^4.0.2:
|
webpack-assets-manifest@^4.0.5:
|
||||||
version "4.0.2"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-4.0.2.tgz#ead6e6dbdcd1c2af45d11a382246fcc79a286372"
|
resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-4.0.5.tgz#802d45fd58203fc7a70ac557636a93605a218d3f"
|
||||||
integrity sha512-bBb9PvEGDOCFvW5/t6Yp9MEE0fymNJ0OvEud9nPvQegDbQEUZ/2WTeHnNoALwWMu1x3JHPyqHVYh8SwtYZ/dww==
|
integrity sha512-cvvr0AtTHyi7D9otmLkv0Bv8j5KKwwD5Wwt6MNxLxgc3U3XmIZnNykw2PMChzUvPr9Ibiv9ceROIc0mS1C7MeA==
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^4.0"
|
chalk "^4.0"
|
||||||
deepmerge "^4.0"
|
deepmerge "^4.0"
|
||||||
lockfile "^1.0"
|
lockfile "^1.0"
|
||||||
lodash.escaperegexp "^4.0"
|
|
||||||
lodash.get "^4.0"
|
lodash.get "^4.0"
|
||||||
lodash.has "^4.0"
|
lodash.has "^4.0"
|
||||||
mkdirp "^1.0"
|
mkdirp "^1.0"
|
||||||
|
@ -11229,10 +11191,10 @@ webpack-assets-manifest@^4.0.2:
|
||||||
tapable "^1.0"
|
tapable "^1.0"
|
||||||
webpack-sources "^1.0"
|
webpack-sources "^1.0"
|
||||||
|
|
||||||
webpack-bundle-analyzer@^4.4.0:
|
webpack-bundle-analyzer@^4.4.1:
|
||||||
version "4.4.0"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7"
|
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz#c71fb2eaffc10a4754d7303b224adb2342069da1"
|
||||||
integrity sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==
|
integrity sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.0.4"
|
acorn "^8.0.4"
|
||||||
acorn-walk "^8.0.0"
|
acorn-walk "^8.0.0"
|
||||||
|
@ -11512,15 +11474,10 @@ ws@^6.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
async-limiter "~1.0.0"
|
async-limiter "~1.0.0"
|
||||||
|
|
||||||
ws@^7.2.3, ws@^7.3.1:
|
ws@^7.2.3, ws@^7.3.1, ws@^7.4.5:
|
||||||
version "7.4.0"
|
version "7.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1"
|
||||||
integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==
|
integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==
|
||||||
|
|
||||||
ws@^7.4.4:
|
|
||||||
version "7.4.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
|
|
||||||
integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
|
|
||||||
|
|
||||||
xml-name-validator@^3.0.0:
|
xml-name-validator@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
|
|
Loading…
Reference in a new issue