Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `README.md`:
  Upstream updated copyright year, we don't mention it so kept our version.
- `app/controllers/admin/dashboard_controller.rb`:
  Not really a conflict, upstream change (removing the spam checker) too close
  to glitch-soc changes. Ported upstream changes.
- `app/models/form/admin_settings.rb`:
  Same.
- `app/services/remove_status_service.rb`:
  Same.
- `app/views/admin/settings/edit.html.haml`:
  Same.
- `config/settings.yml`:
  Same.
- `config/environments/production.rb`:
  Not a real conflict, upstream added a default HTTP header, but we have
  extra headers in glitch-soc.
  Added the header.
master
Claire 3 years ago
commit e2a2bc9021
  1. 2
      .ruby-version
  2. 2
      Dockerfile
  3. 12
      Gemfile
  4. 100
      Gemfile.lock
  5. 1
      app/controllers/admin/dashboard_controller.rb
  6. 53
      app/controllers/admin/follow_recommendations_controller.rb
  7. 28
      app/controllers/api/v1/push/subscriptions_controller.rb
  8. 2
      app/controllers/api/v1/suggestions_controller.rb
  9. 19
      app/controllers/api/v2/suggestions_controller.rb
  10. 25
      app/controllers/api/web/push_subscriptions_controller.rb
  11. 2
      app/helpers/application_helper.rb
  12. 18
      app/helpers/email_helper.rb
  13. 1
      app/javascript/mastodon/actions/importer/normalizer.js
  14. 13
      app/javascript/mastodon/actions/onboarding.js
  15. 24
      app/javascript/mastodon/actions/suggestions.js
  16. 6
      app/javascript/mastodon/components/account.js
  17. 9
      app/javascript/mastodon/components/logo.js
  18. 47
      app/javascript/mastodon/containers/mastodon.js
  19. 10
      app/javascript/mastodon/features/compose/components/search_results.js
  20. 2
      app/javascript/mastodon/features/emoji/emoji.js
  21. 85
      app/javascript/mastodon/features/follow_recommendations/components/account.js
  22. 95
      app/javascript/mastodon/features/follow_recommendations/index.js
  23. 11
      app/javascript/mastodon/features/ui/index.js
  24. 4
      app/javascript/mastodon/features/ui/util/async-components.js
  25. 8
      app/javascript/mastodon/reducers/suggestions.js
  26. 66
      app/javascript/styles/mastodon/components.scss
  27. 25
      app/lib/account_reach_finder.rb
  28. 5
      app/lib/activitypub/activity/create.rb
  29. 2
      app/lib/activitypub/activity/flag.rb
  30. 1
      app/lib/admin/system_check/sidekiq_process_check.rb
  31. 4
      app/lib/application_extension.rb
  32. 35
      app/lib/formatter.rb
  33. 12
      app/lib/potential_friendship_tracker.rb
  34. 198
      app/lib/spam_check.rb
  35. 25
      app/lib/status_reach_finder.rb
  36. 8
      app/lib/tag_manager.rb
  37. 17
      app/models/account.rb
  38. 17
      app/models/account_suggestions.rb
  39. 25
      app/models/account_summary.rb
  40. 27
      app/models/canonical_email_block.rb
  41. 3
      app/models/concerns/account_associations.rb
  42. 39
      app/models/follow_recommendation.rb
  43. 26
      app/models/follow_recommendation_filter.rb
  44. 28
      app/models/follow_recommendation_suppression.rb
  45. 18
      app/models/form/account_batch.rb
  46. 2
      app/models/form/admin_settings.rb
  47. 112
      app/models/web/push_subscription.rb
  48. 15
      app/policies/follow_recommendation_policy.rb
  49. 7
      app/serializers/rest/suggestion_serializer.rb
  50. 5
      app/services/process_mentions_service.rb
  51. 39
      app/services/remove_status_service.rb
  52. 2
      app/services/report_service.rb
  53. 12
      app/services/suspend_account_service.rb
  54. 15
      app/services/unsuspend_account_service.rb
  55. 30
      app/validators/blacklisted_email_validator.rb
  56. 2
      app/views/admin/dashboard/index.html.haml
  57. 20
      app/views/admin/follow_recommendations/_account.html.haml
  58. 41
      app/views/admin/follow_recommendations/show.html.haml
  59. 5
      app/views/admin/rules/index.html.haml
  60. 3
      app/views/admin/settings/edit.html.haml
  61. 4
      app/views/user_mailer/webauthn_enabled.text.erb
  62. 61
      app/workers/scheduler/follow_recommendations_scheduler.rb
  63. 65
      app/workers/web/push_notification_worker.rb
  64. 1
      config/application.rb
  65. 9
      config/environments/production.rb
  66. 14
      config/initializers/content_security_policy.rb
  67. 5
      config/initializers/doorkeeper.rb
  68. 4
      config/initializers/paperclip.rb
  69. 4
      config/initializers/suppress_csrf_warnings.rb
  70. 21
      config/locales/en.yml
  71. 14
      config/locales/simple_form.en.yml
  72. 1
      config/navigation.rb
  73. 4
      config/routes.rb
  74. 1
      config/settings.yml
  75. 4
      config/sidekiq.yml
  76. 17
      db/migrate/20210306164523_account_ids_to_timestamp_ids.rb
  77. 9
      db/migrate/20210322164601_create_account_summaries.rb
  78. 5
      db/migrate/20210323114347_create_follow_recommendations.rb
  79. 9
      db/migrate/20210324171613_create_follow_recommendation_suppressions.rb
  80. 10
      db/migrate/20210416200740_create_canonical_email_blocks.rb
  81. 75
      db/schema.rb
  82. 22
      db/views/account_summaries_v01.sql
  83. 38
      db/views/follow_recommendations_v01.sql
  84. 44
      lib/active_record/batches.rb
  85. 2
      lib/tasks/emojis.rake
  86. 34
      package.json
  87. 19
      public/emoji/1f6b2_border.svg
  88. 78
      spec/controllers/api/v1/apps_controller_spec.rb
  89. 28
      spec/controllers/api/v1/push/subscriptions_controller_spec.rb
  90. 23
      spec/controllers/api/web/push_subscriptions_controller_spec.rb
  91. 4
      spec/fabricators/canonical_email_block_fabricator.rb
  92. 3
      spec/fabricators/follow_recommendation_suppression_fabricator.rb
  93. 192
      spec/lib/spam_check_spec.rb
  94. 36
      spec/lib/tag_manager_spec.rb
  95. 47
      spec/models/canonical_email_block_spec.rb
  96. 4
      spec/models/follow_recommendation_suppression_spec.rb
  97. 94
      spec/models/web/push_subscription_spec.rb
  98. 29
      spec/validators/blacklisted_email_validator_spec.rb
  99. 48
      spec/workers/web/push_notification_worker_spec.rb
  100. 427
      yarn.lock

@ -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 \

@ -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'

@ -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

@ -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

@ -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

@ -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) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />; if (onActionClick) {
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']);

@ -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;

@ -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
account ? mention_html(account, with_domain: same_username_hits.positive? || options[:with_domain]) : "@#{encode(acct)}"
end end
def link_to_account(acct) 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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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)
end Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
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
session_activation.user if user_id.nil?
else session_activation.user
user else
end user
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
find_or_create_access_token.token if access_token_id.nil?
else find_or_create_access_token.token
access_token.token else
end access_token.token
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

@ -0,0 +1,15 @@
# frozen_string_literal: true
class FollowRecommendationPolicy < ApplicationPolicy
def show?
staff?
end
def suppress?
staff?
end
def unsuppress?
staff?
end
end

@ -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? end
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
regexp.match?(@email) def disallowed_through_email_domain_block?
EmailDomainBlock.block?(@email)
end end
def not_on_whitelist? 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

@ -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')

@ -0,0 +1,41 @@
- content_for :page_title do
= t('admin.follow_recommendations.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
%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 %>

@ -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)
# Polymorphically associated activity could have been deleted
# in the meantime, so we have to double-check before proceeding
return unless @notification.activity.present? && @subscription.pushable?(@notification)
payload = @subscription.encrypt(push_notification_json)
subscription.push(notification) unless notification.activity.nil? request_pool.with(@subscription.audience) do |http_client|
rescue Webpush::ResponseError => e request = Request.new(:post, @subscription.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
code = e.response.code.to_i
if (400..499).cover?(code) && ![408, 429].include?(code) request.add_headers(
subscription.destroy! 'Content-Type' => 'application/octet-stream',
else 'Ttl' => TTL,
raise e '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,11 +53,13 @@ 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)
PgHero::HomeController.content_security_policy do |p| Rails.application.reloader.to_prepare do
p.script_src :self, :unsafe_inline, assets_host PgHero::HomeController.content_security_policy do |p|
p.style_src :self, :unsafe_inline, assets_host p.script_src :self, :unsafe_inline, assets_host
end p.style_src :self, :unsafe_inline, assets_host
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
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES } Rails.application.reloader.to_prepare do
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
ActionController::Base.log_warning_on_csrf_failure = false Rails.application.reloader.to_prepare do
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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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))

@ -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",

@ -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,23 +4,83 @@ RSpec.describe Api::V1::AppsController, type: :controller do
render_views render_views
describe 'POST #create' do describe 'POST #create' do
let(:client_name) { 'Test app' }
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
before do before do
post :create, params: { client_name: 'Test app', redirect_uris: 'urn:ietf:wg:oauth:2.0:oob' } post :create, params: app_params
end end
it 'returns http success' do context 'with valid params' do
expect(response).to have_http_status(200) it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'creates an OAuth app' do
expect(Doorkeeper::Application.find_by(name: client_name)).to_not be nil
end
it 'returns client ID and client secret' do
json = body_as_json
expect(json[:client_id]).to_not be_blank
expect(json[:client_secret]).to_not be_blank
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 end
it 'creates an OAuth app' do context 'with many duplicate scopes' do
expect(Doorkeeper::Application.find_by(name: 'Test app')).to_not be nil 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 end
it 'returns client ID and client secret' do context 'with a too-long redirect_uris' do
json = body_as_json let(:redirect_uris) { 'https://foo.bar/' + ('hoge' * 2_000) }
expect(json[:client_id]).to_not be_blank it 'returns http unprocessable entity' do
expect(json[:client_secret]).to_not be_blank expect(response).to have_http_status(422)
end
end 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

@ -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

@ -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

@ -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

@ -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

@ -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…
Cancel
Save