diff --git a/.ruby-version b/.ruby-version
index 37c2961c24..2c9b4ef42e 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.7.2
+2.7.3
diff --git a/Dockerfile b/Dockerfile
index 962e5a8c9b..ee0fc6e691 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,7 +26,7 @@ RUN ARCH= && \
mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install Ruby
-ENV RUBY_VER="2.7.2"
+ENV RUBY_VER="2.7.3"
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
diff --git a/Gemfile b/Gemfile
index c866625c06..059ec195b4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,9 +32,9 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 5.2'
-gem 'cld3', '~> 3.4.1'
+gem 'cld3', '~> 3.4.2'
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
gem 'devise_pam_authenticatable2', '~> 9.2'
@@ -62,9 +62,8 @@ gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
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 'nsa', git: 'https://github.com/Gargron/nsa', ref: 'd1079e0cdafdfed7f9f35478d13b9bdaa65965c0'
+gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.11'
gem 'ox', '~> 2.14'
gem 'parslet'
@@ -95,7 +94,7 @@ gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2021'
gem 'webpacker', '~> 5.2'
-gem 'webpush'
+gem 'webpush', '~> 0.3'
gem 'webauthn', '~> 3.0.0.alpha1'
gem 'json-ld'
@@ -126,7 +125,7 @@ group :test do
gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.21', require: false
gem 'webmock', '~> 3.12'
- gem 'parallel_tests', '~> 3.6'
+ gem 'parallel_tests', '~> 3.7'
gem 'rspec_junit_formatter', '~> 0.4'
end
@@ -160,4 +159,3 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
-gem 'pluck_each', git: 'https://github.com/nsommer/pluck_each', ref: '73be0947c52fc54bf6d7085378db008358aac5eb'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1d6480a7b0..a43a2c1feb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,42 +1,3 @@
-GIT
- remote: https://github.com/ClearlyClaire/devise-two-factor
- revision: 594bb8a32e6f94df7e5ba7c9399eaf9ff25bac0d
- ref: 594bb8a32e6f94df7e5ba7c9399eaf9ff25bac0d
- specs:
- devise-two-factor (3.1.0)
- activesupport (< 7.0)
- attr_encrypted (>= 1.3, < 4, != 2)
- devise
- railties (< 7.0)
- rotp (~> 6)
-
-GIT
- remote: https://github.com/Gargron/nsa
- revision: d1079e0cdafdfed7f9f35478d13b9bdaa65965c0
- ref: d1079e0cdafdfed7f9f35478d13b9bdaa65965c0
- specs:
- nsa (0.2.8)
- activesupport (>= 4.2, < 7)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- sidekiq (>= 3.5)
- statsd-ruby (~> 1.4, >= 1.4.0)
-
-GIT
- remote: https://github.com/nsommer/pluck_each
- revision: 73be0947c52fc54bf6d7085378db008358aac5eb
- ref: 73be0947c52fc54bf6d7085378db008358aac5eb
- specs:
- pluck_each (0.1.3)
- activerecord (>= 6.1.0)
- activesupport (>= 6.1.0)
-
-GIT
- remote: https://github.com/witgo/nilsimsa
- revision: fd184883048b922b176939f851338d0a4971a532
- ref: fd184883048b922b176939f851338d0a4971a532
- specs:
- nilsimsa (1.1.2)
-
GEM
remote: https://rubygems.org/
specs:
@@ -120,8 +81,8 @@ GEM
cocaine (~> 0.5.3)
awrence (1.1.1)
aws-eventstream (1.1.1)
- aws-partitions (1.436.0)
- aws-sdk-core (3.113.0)
+ aws-partitions (1.445.0)
+ aws-sdk-core (3.114.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
@@ -129,7 +90,7 @@ GEM
aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0)
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-kms (~> 1)
aws-sigv4 (~> 1.1)
@@ -192,15 +153,15 @@ GEM
elasticsearch (>= 2.0.0)
elasticsearch-dsl
chunky_png (1.3.15)
- cld3 (3.4.1)
- ffi (>= 1.1.0, < 1.15.0)
+ cld3 (3.4.2)
+ ffi (>= 1.1.0, < 1.16.0)
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.1.8)
- connection_pool (2.2.3)
+ connection_pool (2.2.5)
cose (1.0.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.4.0)
@@ -216,6 +177,12 @@ GEM
railties (>= 4.1.0)
responders
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 (>= 4.0.0)
rpam2 (~> 4.0)
@@ -225,7 +192,7 @@ GEM
docile (1.3.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (5.5.0)
+ doorkeeper (5.5.1)
railties (>= 5)
dotenv (2.7.6)
dotenv-rails (2.7.6)
@@ -257,7 +224,7 @@ GEM
faraday-net_http (1.0.1)
fast_blank (1.0.0)
fastimage (2.2.3)
- ffi (1.14.2)
+ ffi (1.15.0)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
@@ -313,7 +280,7 @@ GEM
httplog (1.4.3)
rack (>= 1.0)
rainbow (>= 2.0.0)
- i18n (1.8.9)
+ i18n (1.8.10)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.34)
activesupport (>= 4.0.2)
@@ -369,7 +336,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.9.0)
+ loofah (2.9.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -401,12 +368,17 @@ GEM
net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0)
nio4r (2.5.7)
- nokogiri (1.11.2)
+ nokogiri (1.11.3)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogumbo (2.0.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)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@@ -434,9 +406,9 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.20.1)
- parallel_tests (3.6.0)
+ parallel_tests (3.7.0)
parallel
- parser (3.0.0.0)
+ parser (3.0.1.0)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
@@ -444,7 +416,7 @@ GEM
pg (1.2.3)
pghero (2.8.1)
activerecord (>= 5)
- pkg-config (1.4.5)
+ pkg-config (1.4.6)
posix-spawn (0.3.15)
premailer (1.14.2)
addressable
@@ -530,7 +502,7 @@ GEM
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
- rexml (3.2.4)
+ rexml (3.2.5)
rotp (6.2.0)
rpam2 (4.0.2)
rqrcode (1.2.0)
@@ -591,7 +563,7 @@ GEM
railties (>= 4.0.0)
securecompare (1.0.0)
semantic_range (2.3.0)
- sidekiq (6.2.0)
+ sidekiq (6.2.1)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
@@ -604,7 +576,7 @@ GEM
sidekiq (>= 3)
thwait
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)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 7.0)
@@ -651,7 +623,7 @@ GEM
openssl-signature_algorithm (~> 0.4.0)
tty-color (0.6.0)
tty-cursor (0.7.1)
- tty-prompt (0.23.0)
+ tty-prompt (0.23.1)
pastel (~> 0.8)
tty-reader (~> 0.8)
tty-reader (0.9.0)
@@ -728,13 +700,13 @@ DEPENDENCIES
capybara (~> 3.35)
charlock_holmes (~> 0.7.7)
chewy (~> 5.2)
- cld3 (~> 3.4.1)
+ cld3 (~> 3.4.2)
climate_control (~> 0.2)
color_diff (~> 0.1)
concurrent-ruby
connection_pool
devise (~> 4.7)
- devise-two-factor!
+ devise-two-factor (~> 4.0)
devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2)
doorkeeper (~> 5.5)
@@ -769,9 +741,8 @@ DEPENDENCIES
microformats (~> 4.2)
mime-types (~> 3.3.1)
net-ldap (~> 0.17)
- nilsimsa!
nokogiri (~> 1.11)
- nsa!
+ nsa (~> 0.2)
oj (~> 3.11)
omniauth (~> 1.9)
omniauth-cas (~> 2.0)
@@ -781,12 +752,11 @@ DEPENDENCIES
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.20)
- parallel_tests (~> 3.6)
+ parallel_tests (~> 3.7)
parslet
pg (~> 1.2)
pghero (~> 2.8)
pkg-config (~> 1.4)
- pluck_each!
posix-spawn
premailer-rails
private_address_check (~> 0.5)
@@ -834,5 +804,5 @@ DEPENDENCIES
webauthn (~> 3.0.0.alpha1)
webmock (~> 3.12)
webpacker (~> 5.2)
- webpush
+ webpush (~> 0.3)
xorcist (~> 1.1)
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 9e921fb954..a00d7ed96d 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -36,7 +36,6 @@ module Admin
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@keybase_integration = Setting.enable_keybase
- @spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
end
diff --git a/app/controllers/admin/follow_recommendations_controller.rb b/app/controllers/admin/follow_recommendations_controller.rb
new file mode 100644
index 0000000000..e3eac62b36
--- /dev/null
+++ b/app/controllers/admin/follow_recommendations_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb
index 0918c61e97..47f2e6440c 100644
--- a/app/controllers/api/v1/push/subscriptions_controller.rb
+++ b/app/controllers/api/v1/push/subscriptions_controller.rb
@@ -3,13 +3,13 @@
class Api::V1::Push::SubscriptionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :push }
before_action :require_user!
- before_action :set_web_push_subscription
- before_action :check_web_push_subscription, only: [:show, :update]
+ before_action :set_push_subscription
+ before_action :check_push_subscription, only: [:show, :update]
def create
- @web_subscription&.destroy!
+ @push_subscription&.destroy!
- @web_subscription = ::Web::PushSubscription.create!(
+ @push_subscription = Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
@@ -18,31 +18,31 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
access_token_id: doorkeeper_token.id
)
- render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
+ render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def show
- render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
+ render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def update
- @web_subscription.update!(data: data_params)
- render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
+ @push_subscription.update!(data: data_params)
+ render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def destroy
- @web_subscription&.destroy!
+ @push_subscription&.destroy!
render_empty
end
private
- def set_web_push_subscription
- @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
+ def set_push_subscription
+ @push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
end
- def check_web_push_subscription
- not_found if @web_subscription.nil?
+ def check_push_subscription
+ not_found if @push_subscription.nil?
end
def subscription_params
@@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params
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
diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb
index 52054160df..b2788cc76c 100644
--- a/app/controllers/api/v1/suggestions_controller.rb
+++ b/app/controllers/api/v1/suggestions_controller.rb
@@ -19,6 +19,6 @@ class Api::V1::SuggestionsController < Api::BaseController
private
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
diff --git a/app/controllers/api/v2/suggestions_controller.rb b/app/controllers/api/v2/suggestions_controller.rb
new file mode 100644
index 0000000000..35eb276c01
--- /dev/null
+++ b/app/controllers/api/v2/suggestions_controller.rb
@@ -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
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index 1dce3e70f2..bed57fc54c 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -2,6 +2,7 @@
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
before_action :require_user!
+ before_action :set_push_subscription, only: :update
def create
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?
data = {
+ policy: 'all',
+
alerts: {
follow: alerts_enabled,
- follow_request: false,
+ follow_request: alerts_enabled,
favourite: alerts_enabled,
reblog: alerts_enabled,
mention: alerts_enabled,
@@ -28,7 +31,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
data.deep_merge!(data_params) if params[:data]
- web_subscription = ::Web::PushSubscription.create!(
+ push_subscription = ::Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
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
)
- 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
def update
- params.require([:id])
-
- web_subscription = ::Web::PushSubscription.find(params[:id])
- web_subscription.update!(data: data_params)
-
- render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
+ @push_subscription.update!(data: data_params)
+ render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
private
+ def set_push_subscription
+ @push_subscription = ::Web::PushSubscription.find(params[:id])
+ end
+
def subscription_params
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end
def data_params
- @data_params ||= params.require(:data).permit(alerts: [:follow, :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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5a9496bd42..9be3419b0c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -91,8 +91,6 @@ module ApplicationHelper
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
elsif status.private_visibility? || status.limited_visibility?
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
- elsif status.direct_visibility?
- fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
end
end
diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb
new file mode 100644
index 0000000000..360783c628
--- /dev/null
+++ b/app/helpers/email_helper.rb
@@ -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
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index dca44917a5..087f264910 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -24,6 +24,7 @@ export function normalizeAccount(account) {
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
+ account.note_plain = unescapeHTML(account.note);
if (account.fields) {
account.fields = account.fields.map(pair => ({
diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js
index 42d8ea33fd..a1dd3a731e 100644
--- a/app/javascript/mastodon/actions/onboarding.js
+++ b/app/javascript/mastodon/actions/onboarding.js
@@ -1,21 +1,8 @@
import { changeSetting, saveSettings } from './settings';
-import { requestBrowserPermission } from './notifications';
export const INTRODUCTION_VERSION = 20181216044202;
export const closeOnboarding = () => dispatch => {
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
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());
- }
- }));
};
diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js
index b15bd916ba..e3a5497597 100644
--- a/app/javascript/mastodon/actions/suggestions.js
+++ b/app/javascript/mastodon/actions/suggestions.js
@@ -1,5 +1,6 @@
import api from '../api';
import { importFetchedAccounts } from './importer';
+import { fetchRelationships } from './accounts';
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
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 function fetchSuggestions() {
+export function fetchSuggestions(withRelationships = false) {
return (dispatch, getState) => {
dispatch(fetchSuggestionsRequest());
- api(getState).get('/api/v1/suggestions').then(response => {
- dispatch(importFetchedAccounts(response.data));
+ api(getState).get('/api/v2/suggestions').then(response => {
+ dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchSuggestionsSuccess(response.data));
+
+ if (withRelationships) {
+ dispatch(fetchRelationships(response.data.map(item => item.account.id)));
+ }
}).catch(error => dispatch(fetchSuggestionsFail(error)));
};
};
@@ -25,10 +30,10 @@ export function fetchSuggestionsRequest() {
};
};
-export function fetchSuggestionsSuccess(accounts) {
+export function fetchSuggestionsSuccess(suggestions) {
return {
type: SUGGESTIONS_FETCH_SUCCESS,
- accounts,
+ suggestions,
skipLoading: true,
};
};
@@ -48,5 +53,12 @@ export const dismissSuggestion = accountId => (dispatch, getState) => {
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(() => {});
};
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 0e40ee1d6a..a85d683a7b 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -78,8 +78,10 @@ class Account extends ImmutablePureComponent {
let buttons;
- if (onActionClick && actionIcon) {
- buttons = ;
+ if (actionIcon) {
+ if (onActionClick) {
+ buttons = ;
+ }
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
diff --git a/app/javascript/mastodon/components/logo.js b/app/javascript/mastodon/components/logo.js
new file mode 100644
index 0000000000..d1c7f08a91
--- /dev/null
+++ b/app/javascript/mastodon/components/logo.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const Logo = () => (
+
+);
+
+export default Logo;
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 3ac58cf7c5..513b599087 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -1,12 +1,10 @@
import React from 'react';
-import { Provider, connect } from 'react-redux';
+import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
-import { INTRODUCTION_VERSION } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
-import Introduction from '../features/introduction';
import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
@@ -26,39 +24,6 @@ const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
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 ;
- }
-
- return (
-
-
-
-
-
- );
- }
-
-}
-
export default class Mastodon extends React.PureComponent {
static propTypes = {
@@ -76,6 +41,10 @@ export default class Mastodon extends React.PureComponent {
}
}
+ shouldUpdateScroll (_, { location }) {
+ return location.state !== previewMediaState && location.state !== previewVideoState;
+ }
+
render () {
const { locale } = this.props;
@@ -83,7 +52,11 @@ export default class Mastodon extends React.PureComponent {
-
+
+
+
+
+
diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js
index 4b4cdff743..a8b31b677b 100644
--- a/app/javascript/mastodon/features/compose/components/search_results.js
+++ b/app/javascript/mastodon/features/compose/components/search_results.js
@@ -51,12 +51,12 @@ class SearchResults extends ImmutablePureComponent {
- {suggestions && suggestions.map(accountId => (
+ {suggestions && suggestions.map(suggestion => (
))}
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index 3de79ac9ba..fb1a3804c5 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -11,7 +11,7 @@ const emojiFilenames = (emojis) => {
};
// Emoji requiring extra borders depending on theme
-const darkEmoji = emojiFilenames(['๐ฑ', '๐', 'โซ', '๐ค', 'โฌ', 'โผ๏ธ', 'โพ', 'โผ๏ธ', 'โ๏ธ', 'โช๏ธ', '๐ฃ', '๐ณ', '๐ท', '๐ธ', 'โฃ๏ธ', '๐ถ๏ธ', 'โด๏ธ', '๐', '๐โโ๏ธ', '๐ฝ๏ธ', '๐ณ', '๐ฆ', '๐', '๐ช', '๐ณ๏ธ', '๐น๏ธ', '๐', '๐๏ธ', '๐๏ธ', '๐โโ๏ธ', '๐ค', '๐', '๐ฅ', '๐ผ', 'โ ๏ธ', '๐ฉ', '๐ฆ', '๐ผ', '๐น', '๐ฎ', '๐', '๐ด', '๐', '๐บ', '๐ฑ', '๐ฒ']);
+const darkEmoji = emojiFilenames(['๐ฑ', '๐', 'โซ', '๐ค', 'โฌ', 'โผ๏ธ', 'โพ', 'โผ๏ธ', 'โ๏ธ', 'โช๏ธ', '๐ฃ', '๐ณ', '๐ท', '๐ธ', 'โฃ๏ธ', '๐ถ๏ธ', 'โด๏ธ', '๐', '๐โโ๏ธ', '๐ฝ๏ธ', '๐ณ', '๐ฆ', '๐', '๐ช', '๐ณ๏ธ', '๐น๏ธ', '๐', '๐๏ธ', '๐๏ธ', '๐โโ๏ธ', '๐ค', '๐', '๐ฅ', '๐ผ', 'โ ๏ธ', '๐ฉ', '๐ฆ', '๐ผ', '๐น', '๐ฎ', '๐', '๐ด', '๐', '๐บ', '๐ฑ', '๐ฒ', '๐ฒ']);
const lightEmoji = emojiFilenames(['๐ฝ', 'โพ', '๐', 'โ๏ธ', '๐จ', '๐๏ธ', '๐', '๐ฅ', '๐ป', '๐', 'โ', 'โ', 'โธ๏ธ', '๐ฉ๏ธ', '๐', '๐', '๐', '๐ง๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', 'โ ๏ธ', '๐จ๏ธ', '๐', '๐', '๐ฌ', '๐ญ', '๐', '๐ณ๏ธ', 'โช', 'โฌ', 'โฝ', 'โป๏ธ', 'โซ๏ธ']);
const emojiFilename = (filename) => {
diff --git a/app/javascript/mastodon/features/follow_recommendations/components/account.js b/app/javascript/mastodon/features/follow_recommendations/components/account.js
new file mode 100644
index 0000000000..bd855aab0c
--- /dev/null
+++ b/app/javascript/mastodon/features/follow_recommendations/components/account.js
@@ -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 = ;
+ } else {
+ button = ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {getFirstSentence(account.get('note_plain'))}
+
+
+
+ {button}
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/follow_recommendations/index.js b/app/javascript/mastodon/features/follow_recommendations/index.js
new file mode 100644
index 0000000000..1231a27eab
--- /dev/null
+++ b/app/javascript/mastodon/features/follow_recommendations/index.js
@@ -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 (
+
+
+
+
+ {!isLoading && (
+
+
+ {suggestions.map(suggestion => (
+
+ ))}
+
+
+
+
+
+
+
+ )}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 507ac1df14..078a69f0f3 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -51,10 +51,12 @@ import {
Lists,
Search,
Directory,
+ FollowRecommendations,
} from './util/async-components';
import { me } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal';
+import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
// Dummy import, to make sure that ends up in the application bundle.
// 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,
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,
+ firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
});
const keyMap = {
@@ -167,6 +170,7 @@ class SwitchingColumnsArea extends React.PureComponent {
+
@@ -215,6 +219,7 @@ class UI extends React.PureComponent {
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
layout: PropTypes.string.isRequired,
+ firstLaunch: PropTypes.bool,
};
state = {
@@ -350,6 +355,12 @@ class UI extends React.PureComponent {
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(expandHomeTimeline());
this.props.dispatch(expandNotifications());
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 986efda1ec..aa90b226ad 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -153,3 +153,7 @@ export function Audio () {
export function Directory () {
return import(/* webpackChunkName: "features/directory" */'../../directory');
}
+
+export function FollowRecommendations () {
+ return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
+}
diff --git a/app/javascript/mastodon/reducers/suggestions.js b/app/javascript/mastodon/reducers/suggestions.js
index 834be728f1..1a6e66ee70 100644
--- a/app/javascript/mastodon/reducers/suggestions.js
+++ b/app/javascript/mastodon/reducers/suggestions.js
@@ -19,18 +19,18 @@ export default function suggestionsReducer(state = initialState, action) {
return state.set('isLoading', true);
case SUGGESTIONS_FETCH_SUCCESS:
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);
});
case SUGGESTIONS_FETCH_FAIL:
return state.set('isLoading', false);
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_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:
- 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:
return state;
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2059aa8f36..a359af2ca7 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1307,6 +1307,29 @@
overflow: hidden;
text-decoration: none;
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%);
}
+.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 {
width: 285px;
margin-top: 10px;
diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb
new file mode 100644
index 0000000000..706ce8c1fb
--- /dev/null
+++ b/app/lib/account_reach_finder.rb
@@ -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
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index f10fc5f432..3a73f29aed 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -88,7 +88,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
resolve_thread(@status)
fetch_replies(@status)
- check_for_spam
distribute(@status)
forward_for_reply
end
@@ -498,10 +497,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
Tombstone.exists?(uri: object_uri)
end
- def check_for_spam
- SpamCheck.perform(@status)
- end
-
def forward_for_reply
return unless @status.distributable? && @json['signature'].present? && reply_to_local?
diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb
index 8dfc76f0a6..b0443849a6 100644
--- a/app/lib/activitypub/activity/flag.rb
+++ b/app/lib/activitypub/activity/flag.rb
@@ -10,6 +10,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
target_accounts.each do |target_account|
target_statuses = target_statuses_by_account[target_account.id]
+ next if target_account.suspended?
+
ReportService.new.call(
@account,
target_account,
diff --git a/app/lib/admin/system_check/sidekiq_process_check.rb b/app/lib/admin/system_check/sidekiq_process_check.rb
index c44d86c448..22446edaf7 100644
--- a/app/lib/admin/system_check/sidekiq_process_check.rb
+++ b/app/lib/admin/system_check/sidekiq_process_check.rb
@@ -7,7 +7,6 @@ class Admin::SystemCheck::SidekiqProcessCheck < Admin::SystemCheck::BaseCheck
mailers
pull
scheduler
- ingress
).freeze
def pass?
diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb
index 1d80b8c6d3..e61cd07212 100644
--- a/app/lib/application_extension.rb
+++ b/app/lib/application_extension.rb
@@ -4,6 +4,8 @@ module ApplicationExtension
extend ActiveSupport::Concern
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
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 02ebe6f896..b261386421 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -118,7 +118,7 @@ class Formatter
end
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.html_safe # rubocop:disable Rails/OutputSafety
end
@@ -187,7 +187,7 @@ class Formatter
elsif entity[:hashtag]
link_to_hashtag(entity)
elsif entity[:screen_name]
- link_to_mention(entity, accounts)
+ link_to_mention(entity, accounts, options)
end
end
end
@@ -352,22 +352,37 @@ class Formatter
encode(entity[:url])
end
- def link_to_mention(entity, linkable_accounts)
+ def link_to_mention(entity, linkable_accounts, options = {})
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) }
- account ? mention_html(account) : "@#{encode(acct)}"
+ same_username_hits = 0
+ 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
- def link_to_account(acct)
+ def link_to_account(acct, options = {})
username, domain = acct.split('@')
domain = nil if TagManager.instance.local_domain?(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
def link_to_hashtag(entity)
@@ -388,7 +403,7 @@ class Formatter
"##{encode(tag)}"
end
- def mention_html(account)
- "@#{encode(account.username)}"
+ def mention_html(account, with_domain: false)
+ "@#{encode(with_domain ? account.pretty_acct : account.username)}"
end
end
diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb
index 188aa4a275..e72d454b67 100644
--- a/app/lib/potential_friendship_tracker.rb
+++ b/app/lib/potential_friendship_tracker.rb
@@ -28,10 +28,14 @@ class PotentialFriendshipTracker
redis.zrem("interactions:#{account_id}", target_account_id)
end
- def get(account_id, limit: 20, offset: 0)
- account_ids = redis.zrevrange("interactions:#{account_id}", offset, limit)
- return [] if account_ids.empty?
- Account.searchable.where(id: account_ids)
+ def get(account, limit)
+ account_ids = redis.zrevrange("interactions:#{account.id}", 0, limit)
+
+ 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
diff --git a/app/lib/spam_check.rb b/app/lib/spam_check.rb
deleted file mode 100644
index dcb2db9ca9..0000000000
--- a/app/lib/spam_check.rb
+++ /dev/null
@@ -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
diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb
index 35b191dadb..3aab3bde0c 100644
--- a/app/lib/status_reach_finder.rb
+++ b/app/lib/status_reach_finder.rb
@@ -6,11 +6,22 @@ class StatusReachFinder
end
def inboxes
- Account.where(id: reached_account_ids).inboxes
+ (reached_account_inboxes + followers_inboxes + relay_inboxes).uniq
end
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
[
replied_to_account_id,
@@ -49,4 +60,16 @@ class StatusReachFinder
def replies_account_ids
@status.replies.pluck(:account_id)
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
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 29dde128c9..a1d12a654e 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -22,14 +22,6 @@ class TagManager
uri.normalized_host
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)
uri = Addressable::URI.parse(url).normalize
return false unless uri.host
diff --git a/app/models/account.rb b/app/models/account.rb
index 2e7d9f5436..8f042c9319 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -114,6 +114,7 @@ class Account < ApplicationRecord
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
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 :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')) }
@@ -238,6 +239,7 @@ class Account < ApplicationRecord
transaction do
create_deletion_request!
update!(suspended_at: date, suspension_origin: origin)
+ create_canonical_email_block!
end
end
@@ -245,6 +247,7 @@ class Account < ApplicationRecord
transaction do
deletion_request&.destroy!
update!(suspended_at: nil, suspension_origin: nil)
+ destroy_canonical_email_block!
end
end
@@ -365,7 +368,7 @@ class Account < ApplicationRecord
end
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
def excluded_from_timeline_domains
@@ -570,4 +573,16 @@ class Account < ApplicationRecord
def clean_feed_manager
FeedManager.instance.clean_feeds!(:home, [id])
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
diff --git a/app/models/account_suggestions.rb b/app/models/account_suggestions.rb
new file mode 100644
index 0000000000..7fe9d618ef
--- /dev/null
+++ b/app/models/account_suggestions.rb
@@ -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
diff --git a/app/models/account_summary.rb b/app/models/account_summary.rb
new file mode 100644
index 0000000000..6a7e17c6c1
--- /dev/null
+++ b/app/models/account_summary.rb
@@ -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
diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb
new file mode 100644
index 0000000000..a8546d65af
--- /dev/null
+++ b/app/models/canonical_email_block.rb
@@ -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
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 98849f8fcf..aaf371ebdd 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -63,5 +63,8 @@ module AccountAssociations
# Account deletion requests
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
diff --git a/app/models/follow_recommendation.rb b/app/models/follow_recommendation.rb
new file mode 100644
index 0000000000..c4355224d9
--- /dev/null
+++ b/app/models/follow_recommendation.rb
@@ -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
diff --git a/app/models/follow_recommendation_filter.rb b/app/models/follow_recommendation_filter.rb
new file mode 100644
index 0000000000..acf03cd844
--- /dev/null
+++ b/app/models/follow_recommendation_filter.rb
@@ -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
diff --git a/app/models/follow_recommendation_suppression.rb b/app/models/follow_recommendation_suppression.rb
new file mode 100644
index 0000000000..170506b853
--- /dev/null
+++ b/app/models/follow_recommendation_suppression.rb
@@ -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
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index 26d6d3abfc..698933c9fb 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -21,6 +21,10 @@ class Form::AccountBatch
approve!
when 'reject'
reject!
+ when 'suppress_follow_recommendation'
+ suppress_follow_recommendation!
+ when 'unsuppress_follow_recommendation'
+ unsuppress_follow_recommendation!
end
end
@@ -79,4 +83,18 @@ class Form::AccountBatch
records.each { |account| authorize(account.user, :reject?) }
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
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
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 999d835e65..558a906d23 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -35,7 +35,6 @@ class Form::AdminSettings
mascot
show_reblogs_in_public_timelines
show_replies_in_public_timelines
- spam_check_enabled
trends
trendable_by_default
show_domain_blocks
@@ -59,7 +58,6 @@ class Form::AdminSettings
enable_keybase
show_reblogs_in_public_timelines
show_replies_in_public_timelines
- spam_check_enabled
trends
trendable_by_default
noindex
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index c407a7789b..6e46573ae0 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -24,81 +24,101 @@ class Web::PushSubscription < ApplicationRecord
validates :key_p256dh, presence: true
validates :key_auth, presence: true
- def push(notification)
- I18n.with_locale(associated_user&.locale || I18n.default_locale) do
- push_payload(payload_for_notification(notification), 48.hours.seconds)
- end
+ delegate :locale, to: :associated_user
+
+ def encrypt(payload)
+ 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
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
def associated_user
return @associated_user if defined?(@associated_user)
- @associated_user = if user_id.nil?
- session_activation.user
- else
- user
- end
+ @associated_user = begin
+ if user_id.nil?
+ session_activation.user
+ else
+ user
+ end
+ end
end
def associated_access_token
return @associated_access_token if defined?(@associated_access_token)
- @associated_access_token = if access_token_id.nil?
- find_or_create_access_token.token
- else
- access_token.token
- end
+ @associated_access_token = begin
+ if access_token_id.nil?
+ find_or_create_access_token.token
+ else
+ access_token.token
+ end
+ end
end
class << self
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)
- .pluck(:id)
-
+ access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil).pluck(:id)
where(access_token_id: access_token_ids).delete_all
end
end
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
Doorkeeper::AccessToken.find_or_create_for(
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'),
expires_in: Doorkeeper.configuration.access_token_expires_in,
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
)
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
diff --git a/app/policies/follow_recommendation_policy.rb b/app/policies/follow_recommendation_policy.rb
new file mode 100644
index 0000000000..68cd0e547f
--- /dev/null
+++ b/app/policies/follow_recommendation_policy.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class FollowRecommendationPolicy < ApplicationPolicy
+ def show?
+ staff?
+ end
+
+ def suppress?
+ staff?
+ end
+
+ def unsuppress?
+ staff?
+ end
+end
diff --git a/app/serializers/rest/suggestion_serializer.rb b/app/serializers/rest/suggestion_serializer.rb
new file mode 100644
index 0000000000..3d697fd9f1
--- /dev/null
+++ b/app/serializers/rest/suggestion_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class REST::SuggestionSerializer < ActiveModel::Serializer
+ attributes :source
+
+ has_one :account, serializer: REST::AccountSerializer
+end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 570cd82728..ec4cb11f9a 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -43,7 +43,6 @@ class ProcessMentionsService < BaseService
end
status.save!
- check_for_spam(status)
mentions.each { |mention| create_notification(mention) }
end
@@ -72,8 +71,4 @@ class ProcessMentionsService < BaseService
def resolve_account_service
ResolveAccountService.new
end
-
- def check_for_spam(status)
- SpamCheck.perform(status)
- end
end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 764ed288d3..17868d4fd8 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -27,10 +27,7 @@ class RemoveStatusService < BaseService
# original object being removed implicitly removes reblogs
# of it. The Delete activity of the original is forwarded
# separately.
- if @account.local? && !@options[:original_removed]
- remove_from_remote_followers
- remove_from_remote_reach
- end
+ remove_from_remote_reach if @account.local? && !@options[:original_removed]
# Since reblogs don't mention anyone, don't get reblogged,
# favourited and don't contain their own media attachments
@@ -42,7 +39,6 @@ class RemoveStatusService < BaseService
remove_from_public
remove_from_media if @status.media_attachments.any?
remove_from_direct if status.direct_visibility?
- remove_from_spam_check
remove_media
end
@@ -85,13 +81,10 @@ class RemoveStatusService < BaseService
end
def remove_from_remote_reach
- return if @status.reblog?
-
- # People who got mentioned in the status, or who
- # reblogged it from someone else might not follow
- # the author and wouldn't normally receive the
- # delete notification - so here, we explicitly
- # send it to them
+ # Followers, relays, people who got mentioned in the status,
+ # or who reblogged it from someone else might not follow
+ # the author and wouldn't normally receive the delete
+ # notification - so here, we explicitly send it to them
status_reach_finder = StatusReachFinder.new(@status)
@@ -100,24 +93,6 @@ class RemoveStatusService < BaseService
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
@signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
end
@@ -171,10 +146,6 @@ class RemoveStatusService < BaseService
@status.media_attachments.destroy_all
end
- def remove_from_spam_check
- redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id)
- end
-
def lock_options
{ redis: Redis.current, key: "distribute:#{@status.id}" }
end
diff --git a/app/services/report_service.rb b/app/services/report_service.rb
index 9d9c7d6c9f..bc0a8b4642 100644
--- a/app/services/report_service.rb
+++ b/app/services/report_service.rb
@@ -10,6 +10,8 @@ class ReportService < BaseService
@comment = options.delete(:comment) || ''
@options = options
+ raise ActiveRecord::RecordNotFound if @target_account.suspended?
+
create_report!
notify_staff!
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 9f4da91d4c..b8dc8d5e09 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -42,7 +42,13 @@ class SuspendAccountService < BaseService
end
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
def unmerge_from_home_timelines!
@@ -90,4 +96,8 @@ class SuspendAccountService < BaseService
end
end
end
+
+ def signed_activity_json
+ @signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
+ end
end
diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb
index ce9ee48ed1..949c670aac 100644
--- a/app/services/unsuspend_account_service.rb
+++ b/app/services/unsuspend_account_service.rb
@@ -12,6 +12,7 @@ class UnsuspendAccountService < BaseService
merge_into_home_timelines!
merge_into_list_timelines!
publish_media_attachments!
+ distribute_update_actor!
end
private
@@ -36,6 +37,16 @@ class UnsuspendAccountService < BaseService
# @account would now be nil.
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!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.merge_into_home(@account, follower)
@@ -81,4 +92,8 @@ class UnsuspendAccountService < BaseService
end
end
end
+
+ def signed_activity_json
+ @signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
+ end
end
diff --git a/app/validators/blacklisted_email_validator.rb b/app/validators/blacklisted_email_validator.rb
index 1ca73fdcc1..eb66ad93d7 100644
--- a/app/validators/blacklisted_email_validator.rb
+++ b/app/validators/blacklisted_email_validator.rb
@@ -6,26 +6,25 @@ class BlacklistedEmailValidator < ActiveModel::Validator
@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
private
- def blocked_email?
- on_blacklist? || not_on_whitelist?
+ def blocked_email_provider?
+ disallowed_through_email_domain_block? || disallowed_through_configuration? || not_allowed_through_configuration?
end
- def on_blacklist?
- return true if EmailDomainBlock.block?(@email)
- return false if Rails.configuration.x.email_domains_blacklist.blank?
-
- domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
- regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
-
- regexp.match?(@email)
+ def blocked_canonical_email?
+ CanonicalEmailBlock.block?(@email)
end
- def not_on_whitelist?
+ def disallowed_through_email_domain_block?
+ EmailDomainBlock.block?(@email)
+ end
+
+ def not_allowed_through_configuration?
return false if Rails.configuration.x.email_domains_whitelist.blank?
domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.')
@@ -33,4 +32,13 @@ class BlacklistedEmailValidator < ActiveModel::Validator
@email !~ regexp
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
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index f2f0c813db..ae5ee270e2 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -79,8 +79,6 @@
= feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled)
%li
= 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
%div
diff --git a/app/views/admin/follow_recommendations/_account.html.haml b/app/views/admin/follow_recommendations/_account.html.haml
new file mode 100644
index 0000000000..af5a4aaf7d
--- /dev/null
+++ b/app/views/admin/follow_recommendations/_account.html.haml
@@ -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')
diff --git a/app/views/admin/follow_recommendations/show.html.haml b/app/views/admin/follow_recommendations/show.html.haml
new file mode 100644
index 0000000000..2878f07d76
--- /dev/null
+++ b/app/views/admin/follow_recommendations/show.html.haml
@@ -0,0 +1,38 @@
+- content_for :page_title do
+ = t('admin.follow_recommendations.title')
+
+%p= t('admin.follow_recommendations.description_html')
+
+%hr.spacer/
+
+= form_tag admin_follow_recommendations_path, method: 'GET', class: 'simple_form' do
+ .filters
+ .filter-subset.filter-subset--with-select
+ %strong= t('admin.follow_recommendations.language')
+ .input.select.optional
+ = select_tag :language, options_for_select(I18n.available_locales.map { |key| [human_locale(key), key]}, @language)
+
+ .filter-subset
+ %strong= t('admin.follow_recommendations.status')
+ %ul
+ %li= filter_link_to t('admin.accounts.moderation.active'), status: nil
+ %li= filter_link_to t('admin.follow_recommendations.suppressed'), status: 'suppressed'
+
+= form_for(@form, url: admin_follow_recommendations_path, method: :patch) do |f|
+ - RelationshipFilter::KEYS.each do |key|
+ = hidden_field_tag key, params[key] if params[key].present?
+
+ .batch-table
+ .batch-table__toolbar
+ %label.batch-table__toolbar__select.batch-checkbox-all
+ = check_box_tag :batch_checkbox_all, nil, false
+ .batch-table__toolbar__actions
+ - if params[:status].blank? && can?(:suppress, :follow_recommendation)
+ = f.button safe_join([fa_icon('times'), t('admin.follow_recommendations.suppress')]), name: :suppress, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+ - if params[:status] == 'suppressed' && can?(:unsuppress, :follow_recommendation)
+ = f.button safe_join([fa_icon('plus'), t('admin.follow_recommendations.unsuppress')]), name: :unsuppress, class: 'table-action-link', type: :submit
+ .batch-table__body
+ - if @accounts.empty?
+ = nothing_here 'nothing-here--under-tabs'
+ - else
+ = render partial: 'account', collection: @accounts, locals: { f: f }
diff --git a/app/views/admin/rules/index.html.haml b/app/views/admin/rules/index.html.haml
index 3b069d083f..4fb993ad07 100644
--- a/app/views/admin/rules/index.html.haml
+++ b/app/views/admin/rules/index.html.haml
@@ -1,8 +1,9 @@
- content_for :page_title do
= t('admin.rules.title')
-.simple_form
- %p.hint= t('admin.rules.description')
+%p= t('admin.rules.description_html')
+
+%hr.spacer/
- if can? :create, :rule
= simple_form_for @rule, url: admin_rules_path do |f|
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index fa8d8441ef..1fab9dd06f 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -101,9 +101,6 @@
.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')
- .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/
.fields-group
diff --git a/app/views/user_mailer/webauthn_enabled.text.erb b/app/views/user_mailer/webauthn_enabled.text.erb
index 4c233fefbd..d4482a69bb 100644
--- a/app/views/user_mailer/webauthn_enabled.text.erb
+++ b/app/views/user_mailer/webauthn_enabled.text.erb
@@ -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 %>
diff --git a/app/workers/scheduler/follow_recommendations_scheduler.rb b/app/workers/scheduler/follow_recommendations_scheduler.rb
new file mode 100644
index 0000000000..0a02864960
--- /dev/null
+++ b/app/workers/scheduler/follow_recommendations_scheduler.rb
@@ -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
diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb
index 46aeaa30b1..57f5b5c228 100644
--- a/app/workers/web/push_notification_worker.rb
+++ b/app/workers/web/push_notification_worker.rb
@@ -3,22 +3,67 @@
class Web::PushNotificationWorker
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)
- subscription = ::Web::PushSubscription.find(subscription_id)
- notification = Notification.find(notification_id)
+ @subscription = Web::PushSubscription.find(subscription_id)
+ @notification = Notification.find(notification_id)
- subscription.push(notification) unless notification.activity.nil?
- rescue Webpush::ResponseError => e
- code = e.response.code.to_i
+ # 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)
- if (400..499).cover?(code) && ![408, 429].include?(code)
- subscription.destroy!
- else
- raise e
+ payload = @subscription.encrypt(push_notification_json)
+
+ request_pool.with(@subscription.audience) do |http_client|
+ request = Request.new(:post, @subscription.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
+
+ request.add_headers(
+ 'Content-Type' => 'application/octet-stream',
+ 'Ttl' => TTL,
+ 'Urgency' => URGENCY,
+ 'Content-Encoding' => 'aesgcm',
+ 'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
+ 'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{@subscription.crypto_key_header}",
+ 'Authorization' => @subscription.authorization_header
+ )
+
+ request.perform do |response|
+ # If the server responds with an error in the 4xx range
+ # that isn't about rate-limiting or timeouts, we can
+ # assume that the subscription is invalid or expired
+ # and must be removed
+
+ if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
+ @subscription.destroy!
+ elsif !(200...300).cover?(response.code)
+ raise Mastodon::UnexpectedResponseError, response
+ end
+ end
end
rescue ActiveRecord::RecordNotFound
true
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
diff --git a/config/application.rb b/config/application.rb
index c911e76dcf..eb2c91677b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -29,6 +29,7 @@ require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/action_dispatch/cookie_jar_extensions'
require_relative '../lib/rails/engine_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
+require_relative '../lib/active_record/batches'
Dotenv::Railtie.load
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 8d811451cb..bf6b5d88e3 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -90,9 +90,12 @@ Rails.application.configure do
config.action_mailer.perform_caching = false
# 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 = {
- from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost'),
- reply_to: ENV['SMTP_REPLY_TO']
+ from: outgoing_email_address,
+ reply_to: ENV['SMTP_REPLY_TO'],
+ 'Message-ID': -> { "<#{Mail.random_tag}@#{outgoing_mail_domain}>" },
}
config.action_mailer.smtp_settings = {
@@ -116,10 +119,10 @@ Rails.application.configure do
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
+ 'Permissions-Policy' => 'interest-cohort=()',
'Referrer-Policy' => 'same-origin',
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
-
}
config.x.otp_secret = ENV.fetch('OTP_SECRET')
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 3a1438201e..549ac3568f 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -53,11 +53,13 @@ Rails.application.config.content_security_policy_nonce_generator = -> request {
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
-PgHero::HomeController.content_security_policy do |p|
- p.script_src :self, :unsafe_inline, assets_host
- p.style_src :self, :unsafe_inline, assets_host
-end
+Rails.application.reloader.to_prepare do
+ PgHero::HomeController.content_security_policy do |p|
+ p.script_src :self, :unsafe_inline, assets_host
+ p.style_src :self, :unsafe_inline, assets_host
+ end
-PgHero::HomeController.after_action do
- request.content_security_policy_nonce_generator = nil
+ PgHero::HomeController.after_action do
+ request.content_security_policy_nonce_generator = nil
+ end
end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 63cff7c598..f78db86534 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -52,6 +52,11 @@ Doorkeeper.configure do
# Issue access tokens with refresh token (disabled by default)
# 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)
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
# a registered application
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 9ad7fd814c..e2a0456475 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -112,7 +112,9 @@ else
)
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
# load the S3 library, so it may be an undefined constant:
diff --git a/config/initializers/suppress_csrf_warnings.rb b/config/initializers/suppress_csrf_warnings.rb
index 410ab585b4..b86adc6f11 100644
--- a/config/initializers/suppress_csrf_warnings.rb
+++ b/config/initializers/suppress_csrf_warnings.rb
@@ -1,3 +1,5 @@
# 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
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 182a8e9850..88acf31645 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -315,10 +315,12 @@ en:
new:
create: Create announcement
title: New announcement
+ publish: Publish
published_msg: Announcement successfully published!
scheduled_for: Scheduled for %{time}
scheduled_msg: Announcement scheduled for publication!
title: Announcements
+ unpublish: Unpublish
unpublished_msg: Announcement successfully unpublished!
updated_msg: Announcement successfully updated!
custom_emojis:
@@ -363,7 +365,6 @@ en:
feature_profile_directory: Profile directory
feature_registrations: Registrations
feature_relay: Federation relay
- feature_spam_check: Anti-spam
feature_timeline_preview: Timeline preview
features: Features
hidden_service: Federation with hidden services
@@ -441,6 +442,14 @@ en:
create: Add domain
title: Block new e-mail domain
title: Blocked e-mail domains
+ follow_recommendations:
+ description_html: "Follow recommendations help new users quickly find interesting content. 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:
by_domain: Domain
delivery_available: Delivery is available
@@ -545,8 +554,10 @@ en:
updated_at: Updated
rules:
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. 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.
edit: Edit rule
+ empty: No server rules have been defined yet.
title: Server rules
settings:
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
title: Custom terms of service
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:
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
title: Server thumbnail
@@ -691,6 +699,7 @@ en:
add_new: Add new
delete: Delete
edit_preset: Edit warning preset
+ empty: You haven't defined any warning presets yet.
title: Manage warning presets
admin_mailer:
new_pending_account:
@@ -1209,8 +1218,6 @@ en:
relationships: Follows and followers
two_factor_authentication: Two-factor Auth
webauthn_authentication: Security keys
- spam_check:
- spam_detected: This is an automated report. Spam has been detected.
statuses:
attached:
audio:
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 97d5b31225..7146adcedd 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -30,19 +30,19 @@ en:
defaults:
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
- 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
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
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
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
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
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
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.
@@ -51,7 +51,7 @@ en:
setting_display_media_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide 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_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
@@ -128,7 +128,7 @@ en:
context: Filter contexts
current_password: Current password
data: Data
- discoverable: List this account on the directory
+ discoverable: Suggest account to others
display_name: Display name
email: E-mail address
expires_in: Expire after
@@ -138,7 +138,7 @@ en:
inbox_url: URL of the relay inbox
irreversible: Drop instead of hide
locale: Interface language
- locked: Lock account
+ locked: Require follow requests
max_uses: Max number of uses
new_password: New password
note: Bio
@@ -160,7 +160,7 @@ en:
setting_display_media_hide_all: Hide all
setting_display_media_show_all: Show all
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_reduce_motion: Reduce motion in animations
setting_show_application: Disclose application used to send toots
diff --git a/config/navigation.rb b/config/navigation.rb
index 0bb3189c12..c626b09ee4 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -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 :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 :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 :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? }
diff --git a/config/routes.rb b/config/routes.rb
index 8ec67113b9..73d6c66183 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,8 +3,6 @@
require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web'
-Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
-
Rails.application.routes.draw do
root 'home#index'
@@ -296,6 +294,7 @@ Rails.application.routes.draw do
end
resources :account_moderation_notes, only: [:create, :destroy]
+ resource :follow_recommendations, only: [:show, :update]
resources :tags, only: [:index, :show, :update] do
collection do
@@ -513,6 +512,7 @@ Rails.application.routes.draw do
namespace :v2 do
resources :media, only: [:create]
get '/search', to: 'search#index', as: :search
+ resources :suggestions, only: [:index]
end
namespace :web do
diff --git a/config/settings.yml b/config/settings.yml
index 1d94880529..0af1a61a2a 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -75,7 +75,6 @@ defaults: &defaults
show_reblogs_in_public_timelines: false
show_replies_in_public_timelines: false
default_content_type: 'text/plain'
- spam_check_enabled: true
show_domain_blocks: 'disabled'
show_domain_blocks_rationale: 'disabled'
outgoing_spoilers: ''
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 010923717e..a8e4c7feb4 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -25,6 +25,10 @@
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
class: Scheduler::FeedCleanupScheduler
queue: scheduler
+ follow_recommendations_scheduler:
+ cron: '<%= Random.rand(0..59) %> <%= Random.rand(6..9) %> * * *'
+ class: Scheduler::FollowRecommendationsScheduler
+ queue: scheduler
doorkeeper_cleanup_scheduler:
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
class: Scheduler::DoorkeeperCleanupScheduler
diff --git a/db/migrate/20210306164523_account_ids_to_timestamp_ids.rb b/db/migrate/20210306164523_account_ids_to_timestamp_ids.rb
new file mode 100644
index 0000000000..39cd4cdea4
--- /dev/null
+++ b/db/migrate/20210306164523_account_ids_to_timestamp_ids.rb
@@ -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
diff --git a/db/migrate/20210322164601_create_account_summaries.rb b/db/migrate/20210322164601_create_account_summaries.rb
new file mode 100644
index 0000000000..b9faf180d8
--- /dev/null
+++ b/db/migrate/20210322164601_create_account_summaries.rb
@@ -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
diff --git a/db/migrate/20210323114347_create_follow_recommendations.rb b/db/migrate/20210323114347_create_follow_recommendations.rb
new file mode 100644
index 0000000000..77e7290320
--- /dev/null
+++ b/db/migrate/20210323114347_create_follow_recommendations.rb
@@ -0,0 +1,5 @@
+class CreateFollowRecommendations < ActiveRecord::Migration[5.2]
+ def change
+ create_view :follow_recommendations
+ end
+end
diff --git a/db/migrate/20210324171613_create_follow_recommendation_suppressions.rb b/db/migrate/20210324171613_create_follow_recommendation_suppressions.rb
new file mode 100644
index 0000000000..c17a0be635
--- /dev/null
+++ b/db/migrate/20210324171613_create_follow_recommendation_suppressions.rb
@@ -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
diff --git a/db/migrate/20210416200740_create_canonical_email_blocks.rb b/db/migrate/20210416200740_create_canonical_email_blocks.rb
new file mode 100644
index 0000000000..a1f1660bff
--- /dev/null
+++ b/db/migrate/20210416200740_create_canonical_email_blocks.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 4c67353fbc..fba4a57583 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2,15 +2,15 @@
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# 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
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"
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 "domain"
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"
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|
t.bigint "conversation_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"
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|
t.datetime "created_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 "bookmarks", "accounts", 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", "conversations", 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 "featured_tags", "accounts", 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", name: "fk_76d644b0e7", 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
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
diff --git a/db/views/account_summaries_v01.sql b/db/views/account_summaries_v01.sql
new file mode 100644
index 0000000000..5a632b6227
--- /dev/null
+++ b/db/views/account_summaries_v01.sql
@@ -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
diff --git a/db/views/follow_recommendations_v01.sql b/db/views/follow_recommendations_v01.sql
new file mode 100644
index 0000000000..799abeaee5
--- /dev/null
+++ b/db/views/follow_recommendations_v01.sql
@@ -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
diff --git a/lib/active_record/batches.rb b/lib/active_record/batches.rb
new file mode 100644
index 0000000000..55d29e52ef
--- /dev/null
+++ b/lib/active_record/batches.rb
@@ -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
diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake
index c8655cc478..a373e7652e 100644
--- a/lib/tasks/emojis.rake
+++ b/lib/tasks/emojis.rake
@@ -91,7 +91,7 @@ namespace :emojis do
desc 'Generate emoji variants with white borders'
task :generate_borders do
src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json')
- emojis = '๐ฑ๐โซ๐คโฌโผ๏ธโพโผ๏ธโ๏ธโช๏ธ๐ฃ๐ณ๐ท๐ธโฃ๏ธ๐ถ๏ธโด๏ธ๐๐โโ๏ธ๐ฝ๏ธ๐ณ๐ฆ๐๐ช๐ณ๏ธ๐น๏ธ๐๐๏ธ๐๏ธ๐โโ๏ธ๐ค๐๐ฅ๐ผโ ๏ธ๐ฉ๐ฆ๐ผ๐น๐ฎ๐๐ด๐๐บ๐ฑ๐ฒ๐ฝโพ๐โ๏ธ๐จ๐๏ธ๐๐ฅ๐ป๐โโโธ๏ธ๐ฉ๏ธ๐๐๐๐ง๏ธ๐๐๐๐๐๐โ ๏ธ๐จ๏ธ๐๐๐ฌ๐ญ๐๐ณ๏ธโชโฌโฝโป๏ธโซ๏ธ'
+ emojis = '๐ฑ๐โซ๐คโฌโผ๏ธโพโผ๏ธโ๏ธโช๏ธ๐ฃ๐ณ๐ท๐ธโฃ๏ธ๐ถ๏ธโด๏ธ๐๐โโ๏ธ๐ฝ๏ธ๐ณ๐ฆ๐๐ช๐ณ๏ธ๐น๏ธ๐๐๏ธ๐๏ธ๐โโ๏ธ๐ค๐๐ฅ๐ผโ ๏ธ๐ฉ๐ฆ๐ผ๐น๐ฎ๐๐ด๐๐บ๐ฑ๐ฒ๐ฒ๐ฝโพ๐โ๏ธ๐จ๐๏ธ๐๐ฅ๐ป๐โโโธ๏ธ๐ฉ๏ธ๐๐๐๐ง๏ธ๐๐๐๐๐๐โ ๏ธ๐จ๏ธ๐๐๐ฌ๐ญ๐๐ณ๏ธโชโฌโฝโป๏ธโซ๏ธ'
map = Oj.load(File.read(src))
diff --git a/package.json b/package.json
index c5855f6af1..426adbc1d2 100644
--- a/package.json
+++ b/package.json
@@ -60,12 +60,12 @@
},
"private": true,
"dependencies": {
- "@babel/core": "^7.13.14",
+ "@babel/core": "^7.13.15",
"@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-runtime": "^7.13.10",
- "@babel/preset-env": "^7.13.12",
+ "@babel/plugin-transform-runtime": "^7.13.15",
+ "@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"@babel/runtime": "^7.13.10",
"@gamestdio/websocket": "^0.3.2",
@@ -83,12 +83,12 @@
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-runtime": "^6.26.0",
"blurhash": "^1.1.3",
- "classnames": "^2.2.5",
+ "classnames": "^2.3.1",
"color-blend": "^3.0.1",
"compression-webpack-plugin": "^6.1.1",
"cross-env": "^7.0.3",
- "css-loader": "^5.2.0",
- "cssnano": "^4.1.10",
+ "css-loader": "^5.2.2",
+ "cssnano": "^4.1.11",
"detect-passive-events": "^2.0.3",
"dotenv": "^8.2.0",
"emoji-mart": "Gargron/emoji-mart#build",
@@ -109,11 +109,11 @@
"intl-messageformat": "^2.2.0",
"intl-relativeformat": "^6.4.3",
"is-nan": "^1.3.2",
- "js-yaml": "^4.0.0",
+ "js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mark-loader": "^0.1.6",
"marky": "^1.2.1",
- "mini-css-extract-plugin": "^1.4.0",
+ "mini-css-extract-plugin": "^1.5.0",
"mkdirp": "^1.0.4",
"npmlog": "^4.1.2",
"object-assign": "^4.1.1",
@@ -146,7 +146,7 @@
"react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^8.3.2",
"react-toggle": "^4.1.2",
- "redis": "^3.0.2",
+ "redis": "^3.1.1",
"redux": "^4.0.5",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.2.0",
@@ -155,7 +155,7 @@
"requestidlecallback": "^0.3.0",
"reselect": "^4.0.0",
"rimraf": "^3.0.2",
- "sass": "^1.32.8",
+ "sass": "^1.32.10",
"sass-loader": "^10.1.1",
"stacktrace-js": "^2.0.2",
"stringz": "^2.1.0",
@@ -167,23 +167,23 @@
"twitter-text": "3.1.0",
"uuid": "^8.3.1",
"webpack": "^4.46.0",
- "webpack-assets-manifest": "^4.0.2",
- "webpack-bundle-analyzer": "^4.4.0",
+ "webpack-assets-manifest": "^4.0.5",
+ "webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.7.3",
"wicg-inert": "^3.1.1",
- "ws": "^7.4.4"
+ "ws": "^7.4.5"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
- "eslint": "^7.23.0",
+ "eslint": "^7.24.0",
"eslint-plugin-import": "~2.22.1",
"eslint-plugin-jsx-a11y": "~6.4.1",
- "eslint-plugin-promise": "~4.3.1",
- "eslint-plugin-react": "~7.23.1",
+ "eslint-plugin-promise": "~5.1.0",
+ "eslint-plugin-react": "~7.23.2",
"jest": "^26.6.3",
"raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3",
diff --git a/public/emoji/1f6b2_border.svg b/public/emoji/1f6b2_border.svg
new file mode 100644
index 0000000000..0219841a1f
--- /dev/null
+++ b/public/emoji/1f6b2_border.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/spec/controllers/api/v1/apps_controller_spec.rb b/spec/controllers/api/v1/apps_controller_spec.rb
index 60a4c3b41c..70cd62d482 100644
--- a/spec/controllers/api/v1/apps_controller_spec.rb
+++ b/spec/controllers/api/v1/apps_controller_spec.rb
@@ -4,23 +4,83 @@ RSpec.describe Api::V1::AppsController, type: :controller do
render_views
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
- post :create, params: { client_name: 'Test app', redirect_uris: 'urn:ietf:wg:oauth:2.0:oob' }
+ post :create, params: app_params
end
- it 'returns http success' do
- expect(response).to have_http_status(200)
+ context 'with valid params' do
+ 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
- it 'creates an OAuth app' do
- expect(Doorkeeper::Application.find_by(name: 'Test app')).to_not be nil
+ context 'with an unsupported scope' do
+ let(:scopes) { 'hoge' }
+
+ it 'returns http unprocessable entity' do
+ expect(response).to have_http_status(422)
+ end
end
- it 'returns client ID and client secret' do
- json = body_as_json
+ context 'with many duplicate scopes' do
+ let(:scopes) { (%w(read) * 40).join(' ') }
- expect(json[:client_id]).to_not be_blank
- expect(json[:client_secret]).to_not be_blank
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'only saves the scope once' do
+ expect(Doorkeeper::Application.find_by(name: client_name).scopes.to_s).to eq 'read'
+ end
+ end
+
+ context 'with a too-long name' do
+ let(:client_name) { 'hoge' * 20 }
+
+ it 'returns http unprocessable entity' do
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with a too-long website' do
+ let(:website) { 'https://foo.bar/' + ('hoge' * 2_000) }
+
+ it 'returns http unprocessable entity' do
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with a too-long redirect_uris' do
+ let(:redirect_uris) { 'https://foo.bar/' + ('hoge' * 2_000) }
+
+ it 'returns http unprocessable entity' do
+ expect(response).to have_http_status(422)
+ end
end
end
end
diff --git a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb
index 01146294f8..534d028790 100644
--- a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb
+++ b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb
@@ -27,20 +27,27 @@ describe Api::V1::Push::SubscriptionsController do
let(:alerts_payload) do
{
data: {
+ policy: 'all',
+
alerts: {
follow: true,
+ follow_request: true,
favourite: false,
reblog: true,
mention: false,
+ poll: true,
+ status: false,
}
}
}.with_indifferent_access
end
describe 'POST #create' do
- it 'saves push subscriptions' do
+ before do
post :create, params: create_payload
+ end
+ it 'saves push subscriptions' do
push_subscription = Web::PushSubscription.find_by(endpoint: 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
post :create, params: create_payload
- post :create, params: create_payload
-
expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1
end
end
describe 'PUT #update' do
- it 'changes alert settings' do
+ before do
post :create, params: create_payload
put :update, params: alerts_payload
+ end
+ it 'changes alert settings' do
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.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)
- expect(push_subscription.data.dig('alerts', 'mention')).to eq(alerts_payload[:data][:alerts][:mention].to_s)
+ expect(push_subscription.data['policy']).to eq(alerts_payload[:data][:policy])
+
+ %w(follow follow_request favourite reblog mention poll status).each do |type|
+ expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+ end
end
end
describe 'DELETE #destroy' do
- it 'removes the subscription' do
+ before do
post :create, params: create_payload
delete :destroy
+ end
+ it 'removes the subscription' do
expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil
end
end
diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
index 381cdeab94..bda4a76614 100644
--- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb
+++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
@@ -22,11 +22,16 @@ describe Api::Web::PushSubscriptionsController do
let(:alerts_payload) do
{
data: {
+ policy: 'all',
+
alerts: {
follow: true,
+ follow_request: false,
favourite: false,
reblog: true,
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])
- expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s)
- 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)
- expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s)
+ expect(push_subscription.data['policy']).to eq 'all'
+
+ %w(follow follow_request favourite reblog mention poll status).each do |type|
+ expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+ end
end
end
end
@@ -81,10 +87,11 @@ describe Api::Web::PushSubscriptionsController do
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['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)
- expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s)
+ expect(push_subscription.data['policy']).to eq 'all'
+
+ %w(follow follow_request favourite reblog mention poll status).each do |type|
+ expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+ end
end
end
end
diff --git a/spec/fabricators/canonical_email_block_fabricator.rb b/spec/fabricators/canonical_email_block_fabricator.rb
new file mode 100644
index 0000000000..a0b6e0d221
--- /dev/null
+++ b/spec/fabricators/canonical_email_block_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:canonical_email_block) do
+ email "test@example.com"
+ reference_account { Fabricate(:account) }
+end
diff --git a/spec/fabricators/follow_recommendation_suppression_fabricator.rb b/spec/fabricators/follow_recommendation_suppression_fabricator.rb
new file mode 100644
index 0000000000..4a6a07a66c
--- /dev/null
+++ b/spec/fabricators/follow_recommendation_suppression_fabricator.rb
@@ -0,0 +1,3 @@
+Fabricator(:follow_recommendation_suppression) do
+ account
+end
diff --git a/spec/lib/spam_check_spec.rb b/spec/lib/spam_check_spec.rb
deleted file mode 100644
index 159d832573..0000000000
--- a/spec/lib/spam_check_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index e9a7aa9344..2230f97103 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -83,40 +83,4 @@ RSpec.describe TagManager do
expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
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
diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb
new file mode 100644
index 0000000000..8e0050d65a
--- /dev/null
+++ b/spec/models/canonical_email_block_spec.rb
@@ -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
diff --git a/spec/models/follow_recommendation_suppression_spec.rb b/spec/models/follow_recommendation_suppression_spec.rb
new file mode 100644
index 0000000000..39107a2b04
--- /dev/null
+++ b/spec/models/follow_recommendation_suppression_spec.rb
@@ -0,0 +1,4 @@
+require 'rails_helper'
+
+RSpec.describe FollowRecommendationSuppression, type: :model do
+end
diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb
index c6665611c6..b44904369b 100644
--- a/spec/models/web/push_subscription_spec.rb
+++ b/spec/models/web/push_subscription_spec.rb
@@ -1,16 +1,94 @@
require 'rails_helper'
RSpec.describe Web::PushSubscription, type: :model do
- let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } }
- let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) }
+ let(:account) { Fabricate(:account) }
+
+ 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
- it 'obeys alert settings' do
- expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true
- 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
- expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'FollowRequest'))).to eq false
- expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Favourite'))).to eq true
+ let(:notification_type) { :mention }
+ let(:notification) { Fabricate(:notification, account: account, type: notification_type) }
+
+ %i(mention reblog follow follow_request favourite).each do |type|
+ context "when notification is a #{type}" do
+ 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
diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb
index 53b355a579..f7d5e01bc8 100644
--- a/spec/validators/blacklisted_email_validator_spec.rb
+++ b/spec/validators/blacklisted_email_validator_spec.rb
@@ -9,23 +9,36 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
before do
allow(user).to receive(:valid_invitation?) { false }
- allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
- described_class.new.validate(user)
+ allow_any_instance_of(described_class).to receive(:blocked_email_provider?) { blocked_email }
end
- context 'blocked_email?' do
+ subject { described_class.new.validate(user); errors }
+
+ context 'when e-mail provider is blocked' do
let(:blocked_email) { true }
- it 'calls errors.add' do
- expect(errors).to have_received(:add).with(:email, :blocked)
+ it 'adds error' do
+ expect(subject).to have_received(:add).with(:email, :blocked)
end
end
- context '!blocked_email?' do
+ context 'when e-mail provider is not blocked' do
let(:blocked_email) { false }
- it 'not calls errors.add' do
- expect(errors).not_to have_received(:add).with(:email, :blocked)
+ it 'does not add errors' do
+ 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
diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb
new file mode 100644
index 0000000000..5bc24f8886
--- /dev/null
+++ b/spec/workers/web/push_notification_worker_spec.rb
@@ -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
diff --git a/yarn.lock b/yarn.lock
index d0aa623076..41251fc6fe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16,24 +16,24 @@
dependencies:
"@babel/highlight" "^7.12.13"
-"@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8":
- version "7.13.12"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1"
- integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==
+"@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.15"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4"
+ 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":
- version "7.13.14"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06"
- integrity sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==
+"@babel/core@^7.1.0", "@babel/core@^7.13.15", "@babel/core@^7.7.2", "@babel/core@^7.7.5":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.15.tgz#a6d40917df027487b54312202a06812c4f7792d0"
+ integrity sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==
dependencies:
"@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.9"
"@babel/helper-compilation-targets" "^7.13.13"
"@babel/helper-module-transforms" "^7.13.14"
"@babel/helpers" "^7.13.10"
- "@babel/parser" "^7.13.13"
+ "@babel/parser" "^7.13.15"
"@babel/template" "^7.12.13"
- "@babel/traverse" "^7.13.13"
+ "@babel/traverse" "^7.13.15"
"@babel/types" "^7.13.14"
convert-source-map "^1.7.0"
debug "^4.1.0"
@@ -81,7 +81,7 @@
"@babel/helper-annotate-as-pure" "^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"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5"
integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==
@@ -91,10 +91,10 @@
browserslist "^4.14.5"
semver "^6.3.0"
-"@babel/helper-create-class-features-plugin@^7.13.0":
- version "7.13.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.0.tgz#28d04ad9cfbd1ed1d8b988c9ea7b945263365846"
- integrity sha512-twwzhthM4/+6o9766AW2ZBHpIHPSGrPGk1+WfHiu13u/lBnggXGNYCpeAyVfNwGDKfkhEDp+WOD/xafoJ2iLjA==
+"@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.13.11":
+ version "7.13.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6"
+ integrity sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==
dependencies:
"@babel/helper-function-name" "^7.12.13"
"@babel/helper-member-expression-to-functions" "^7.13.0"
@@ -110,10 +110,10 @@
"@babel/helper-annotate-as-pure" "^7.12.13"
regexpu-core "^4.7.1"
-"@babel/helper-define-polyfill-provider@^0.1.2":
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.2.tgz#619f01afe1deda460676c25c463b42eaefdb71a2"
- integrity sha512-hWeolZJivTNGHXHzJjQz/NwDaG4mGXf22ZroOP8bQYgvHNzaQ5tylsVbAcAS2oDjXBwpu8qH2I/654QFS2rDpw==
+"@babel/helper-define-polyfill-provider@^0.2.0":
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz#a640051772045fedaaecc6f0c6c69f02bdd34bf1"
+ integrity sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==
dependencies:
"@babel/helper-compilation-targets" "^7.13.0"
"@babel/helper-module-imports" "^7.12.13"
@@ -176,21 +176,7 @@
dependencies:
"@babel/types" "^7.13.12"
-"@babel/helper-module-imports@^7.0.0-beta.49":
- 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":
+"@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.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
@@ -328,10 +314,10 @@
chalk "^2.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":
- version "7.13.13"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df"
- integrity sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==
+"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.15", "@babel/parser@^7.7.0":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8"
+ integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12":
version "7.13.12"
@@ -342,10 +328,10 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
"@babel/plugin-proposal-optional-chaining" "^7.13.12"
-"@babel/plugin-proposal-async-generator-functions@^7.13.8":
- version "7.13.8"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1"
- integrity sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==
+"@babel/plugin-proposal-async-generator-functions@^7.13.15":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz#80e549df273a3b3050431b148c892491df1bcc5b"
+ integrity sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==
dependencies:
"@babel/helper-plugin-utils" "^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-plugin-utils" "^7.13.0"
-"@babel/plugin-proposal-decorators@^7.13.5":
- version "7.13.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.13.5.tgz#d28071457a5ba8ee1394b23e38d5dcf32ea20ef7"
- integrity sha512-i0GDfVNuoapwiheevUOuSW67mInqJ8qw7uWfpjNVeHMn143kXblEy/bmL9AdZ/0yf/4BMQeWXezK0tQIvNPqag==
+"@babel/plugin-proposal-decorators@^7.13.15":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.13.15.tgz#e91ccfef2dc24dd5bd5dcc9fc9e2557c684ecfb8"
+ integrity sha512-ibAMAqUm97yzi+LPgdr5Nqb9CMkeieGHvwPg1ywSGjZrZHQEGqE01HmOio8kxRpA/+VtOHouIVy2FMpBbtltjA==
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/plugin-syntax-decorators" "^7.12.13"
@@ -796,10 +782,10 @@
"@babel/helper-annotate-as-pure" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
-"@babel/plugin-transform-regenerator@^7.12.13":
- version "7.12.13"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz#b628bcc9c85260ac1aeb05b45bde25210194a2f5"
- integrity sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==
+"@babel/plugin-transform-regenerator@^7.13.15":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz#e5eb28945bf8b6563e7f818945f966a8d2997f39"
+ integrity sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==
dependencies:
regenerator-transform "^0.14.2"
@@ -810,16 +796,16 @@
dependencies:
"@babel/helper-plugin-utils" "^7.12.13"
-"@babel/plugin-transform-runtime@^7.13.10":
- version "7.13.10"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1"
- integrity sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==
+"@babel/plugin-transform-runtime@^7.13.15":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz#2eddf585dd066b84102517e10a577f24f76a9cd7"
+ integrity sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==
dependencies:
- "@babel/helper-module-imports" "^7.12.13"
+ "@babel/helper-module-imports" "^7.13.12"
"@babel/helper-plugin-utils" "^7.13.0"
- babel-plugin-polyfill-corejs2 "^0.1.4"
- babel-plugin-polyfill-corejs3 "^0.1.3"
- babel-plugin-polyfill-regenerator "^0.1.2"
+ babel-plugin-polyfill-corejs2 "^0.2.0"
+ babel-plugin-polyfill-corejs3 "^0.2.0"
+ babel-plugin-polyfill-regenerator "^0.2.0"
semver "^6.3.0"
"@babel/plugin-transform-shorthand-properties@^7.12.13":
@@ -873,17 +859,17 @@
"@babel/helper-create-regexp-features-plugin" "^7.12.13"
"@babel/helper-plugin-utils" "^7.12.13"
-"@babel/preset-env@^7.13.12":
- version "7.13.12"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.12.tgz#6dff470478290582ac282fb77780eadf32480237"
- integrity sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==
+"@babel/preset-env@^7.13.15":
+ version "7.13.15"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.15.tgz#c8a6eb584f96ecba183d3d414a83553a599f478f"
+ integrity sha512-D4JAPMXcxk69PKe81jRJ21/fP/uYdcTZ3hJDF5QX2HSI9bBxxYw/dumdR6dGumhjxlprHPE4XWoPaqzZUVy2MA==
dependencies:
- "@babel/compat-data" "^7.13.12"
- "@babel/helper-compilation-targets" "^7.13.10"
+ "@babel/compat-data" "^7.13.15"
+ "@babel/helper-compilation-targets" "^7.13.13"
"@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-validator-option" "^7.12.17"
"@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-dynamic-import" "^7.13.8"
"@babel/plugin-proposal-export-namespace-from" "^7.12.13"
@@ -931,7 +917,7 @@
"@babel/plugin-transform-object-super" "^7.12.13"
"@babel/plugin-transform-parameters" "^7.13.0"
"@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-shorthand-properties" "^7.12.13"
"@babel/plugin-transform-spread" "^7.13.0"
@@ -941,10 +927,10 @@
"@babel/plugin-transform-unicode-escapes" "^7.12.13"
"@babel/plugin-transform-unicode-regex" "^7.12.13"
"@babel/preset-modules" "^0.1.4"
- "@babel/types" "^7.13.12"
- babel-plugin-polyfill-corejs2 "^0.1.4"
- babel-plugin-polyfill-corejs3 "^0.1.3"
- babel-plugin-polyfill-regenerator "^0.1.2"
+ "@babel/types" "^7.13.14"
+ babel-plugin-polyfill-corejs2 "^0.2.0"
+ babel-plugin-polyfill-corejs3 "^0.2.0"
+ babel-plugin-polyfill-regenerator "^0.2.0"
core-js-compat "^3.9.0"
semver "^6.3.0"
@@ -1002,21 +988,21 @@
"@babel/parser" "^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":
- version "7.13.13"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.13.tgz#39aa9c21aab69f74d948a486dd28a2dbdbf5114d"
- integrity sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==
+"@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.15"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.15.tgz#c38bf7679334ddd4028e8e1f7b3aa5019f0dada7"
+ integrity sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==
dependencies:
"@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.9"
"@babel/helper-function-name" "^7.12.13"
"@babel/helper-split-export-declaration" "^7.12.13"
- "@babel/parser" "^7.13.13"
- "@babel/types" "^7.13.13"
+ "@babel/parser" "^7.13.15"
+ "@babel/types" "^7.13.14"
debug "^4.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"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
@@ -1025,15 +1011,6 @@
lodash "^4.17.19"
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":
version "0.2.3"
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"
resolve "^1.12.0"
-babel-plugin-polyfill-corejs2@^0.1.4:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.5.tgz#8fc4779965311393594a1b9ad3adefab3860c8fe"
- integrity sha512-5IzdFIjYWqlOFVr/hMYUpc+5fbfuvJTAISwIY58jhH++ZtawtNlcJnxAixlk8ahVwHCz1ipW/kpXYliEBp66wg==
+babel-plugin-polyfill-corejs2@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz#686775bf9a5aa757e10520903675e3889caeedc4"
+ integrity sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==
dependencies:
- "@babel/compat-data" "^7.13.0"
- "@babel/helper-define-polyfill-provider" "^0.1.2"
+ "@babel/compat-data" "^7.13.11"
+ "@babel/helper-define-polyfill-provider" "^0.2.0"
semver "^6.1.1"
-babel-plugin-polyfill-corejs3@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.4.tgz#2ae290200e953bade30907b7a3bebcb696e6c59d"
- integrity sha512-ysSzFn/qM8bvcDAn4mC7pKk85Y5dVaoa9h4u0mHxOEpDzabsseONhUpR7kHxpUinfj1bjU7mUZqD23rMZBoeSg==
+babel-plugin-polyfill-corejs3@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz#f4b4bb7b19329827df36ff56f6e6d367026cb7a2"
+ integrity sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==
dependencies:
- "@babel/helper-define-polyfill-provider" "^0.1.2"
- core-js-compat "^3.8.1"
+ "@babel/helper-define-polyfill-provider" "^0.2.0"
+ core-js-compat "^3.9.1"
-babel-plugin-polyfill-regenerator@^0.1.2:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.1.3.tgz#350f857225fc640ae1ec78d1536afcbb457db841"
- integrity sha512-hRjTJQiOYt/wBKEc+8V8p9OJ9799blAJcuKzn1JXh3pApHoWl1Emxh2BHc6MC7Qt6bbr3uDpNxaYQnATLIudEg==
+babel-plugin-polyfill-regenerator@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz#853f5f5716f4691d98c84f8069c7636ea8da7ab8"
+ integrity sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==
dependencies:
- "@babel/helper-define-polyfill-provider" "^0.1.2"
+ "@babel/helper-define-polyfill-provider" "^0.2.0"
babel-plugin-preval@^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"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
-"chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.1:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1"
- integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
+ integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
dependencies:
anymatch "~3.1.1"
braces "~3.0.2"
@@ -2893,9 +2870,9 @@ char-regex@^1.0.2:
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
- readdirp "~3.4.0"
+ readdirp "~3.5.0"
optionalDependencies:
- fsevents "~2.1.2"
+ fsevents "~2.3.1"
chokidar@^2.1.8:
version "2.1.8"
@@ -2966,10 +2943,10 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
-classnames@^2.2.5:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
- integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
+classnames@^2.2.5, classnames@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+ integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-stack@^2.0.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"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-core-js-compat@^3.8.1, core-js-compat@^3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.0.tgz#29da39385f16b71e1915565aa0385c4e0963ad56"
- integrity sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ==
+core-js-compat@^3.9.0, core-js-compat@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.10.1.tgz#62183a3a77ceeffcc420d907a3e6fc67d9b27f1c"
+ integrity sha512-ZHQTdTPkqvw2CeHiZC970NNJcnwzT6YIueDMASKt+p3WbZsLXOcoD392SkcWhkC0wBBHhlfhqGKKsNCQUozYtg==
dependencies:
browserslist "^4.16.3"
semver "7.0.0"
@@ -3424,23 +3401,22 @@ css-list-helpers@^1.0.1:
dependencies:
tcomb "^2.5.0"
-css-loader@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.0.tgz#a9ecda190500863673ce4434033710404efbff00"
- integrity sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==
+css-loader@^5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.2.tgz#65f2c1482255f15847ecad6cbc515cae8a5b234e"
+ integrity sha512-IS722y7Lh2Yq+acMR74tdf3faMOLRP2RfLwS0VzSS7T98IHtacMWJLku3A0OBTFHB07zAa4nWBhA8gfxwQVWGQ==
dependencies:
camelcase "^6.2.0"
- cssesc "^3.0.0"
icss-utils "^5.1.0"
loader-utils "^2.0.0"
- postcss "^8.2.8"
+ postcss "^8.2.10"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.1.0"
schema-utils "^3.0.0"
- semver "^7.3.4"
+ semver "^7.3.5"
css-select-base-adapter@^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"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
-cssnano-preset-default@^4.0.7:
- version "4.0.7"
- resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
- integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+cssnano-preset-default@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
+ integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==
dependencies:
css-declaration-sorter "^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-reduce-initial "^4.0.3"
postcss-reduce-transforms "^4.0.2"
- postcss-svgo "^4.0.2"
+ postcss-svgo "^4.0.3"
postcss-unique-selectors "^4.0.1"
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"
integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
-cssnano@^4.1.10:
- version "4.1.10"
- resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
- integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+cssnano@^4.1.11:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.11.tgz#c7b5f5b81da269cb1fd982cb960c1200910c9a99"
+ integrity sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==
dependencies:
cosmiconfig "^5.0.0"
- cssnano-preset-default "^4.0.7"
+ cssnano-preset-default "^4.0.8"
is-resolvable "^1.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"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
-denque@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
- integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
+denque@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
+ integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
depd@~1.1.2:
version "1.1.2"
@@ -4298,15 +4274,15 @@ eslint-plugin-jsx-a11y@~6.4.1:
jsx-ast-utils "^3.1.0"
language-tags "^1.0.5"
-eslint-plugin-promise@~4.3.1:
- version "4.3.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz#61485df2a359e03149fdafc0a68b0e030ad2ac45"
- integrity sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==
+eslint-plugin-promise@~5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz#fb2188fb734e4557993733b41aa1a688f46c6f24"
+ integrity sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==
-eslint-plugin-react@~7.23.1:
- version "7.23.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.23.1.tgz#f1a2e844c0d1967c822388204a8bc4dee8415b11"
- integrity sha512-MvFGhZjI8Z4HusajmSw0ougGrq3Gs4vT/0WgwksZgf5RrLrRa2oYAw56okU4tZJl8+j7IYNuTM+2RnFEuTSdRQ==
+eslint-plugin-react@~7.23.2:
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz#2d2291b0f95c03728b55869f01102290e792d494"
+ integrity sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==
dependencies:
array-includes "^3.1.3"
array.prototype.flatmap "^1.2.4"
@@ -4393,10 +4369,10 @@ eslint@^2.7.0:
text-table "~0.2.0"
user-home "^2.0.0"
-eslint@^7.23.0:
- version "7.23.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.23.0.tgz#8d029d252f6e8cf45894b4bee08f5493f8e94325"
- integrity sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==
+eslint@^7.24.0:
+ version "7.24.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.24.0.tgz#2e44fa62d93892bfdb100521f17345ba54b8513a"
+ integrity sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.0"
@@ -4992,11 +4968,16 @@ fsevents@^1.2.7:
bindings "^1.5.0"
nan "^2.12.1"
-fsevents@^2.1.2, fsevents@~2.1.2:
+fsevents@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
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:
version "1.1.1"
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"
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:
version "2.0.1"
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"
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:
version "1.0.3"
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"
esprima "^4.0.0"
-js-yaml@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
- integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
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"
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:
version "4.4.2"
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"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-mini-css-extract-plugin@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.4.0.tgz#c8e571c4b6d63afa56c47260343adf623349c473"
- integrity sha512-DyQr5DhXXARKZoc4kwvCvD95kh69dUupfuKOmBUqZ4kBTmRaRZcU32lYu3cLd6nEGXhQ1l7LzZ3F/CjItaY6VQ==
+mini-css-extract-plugin@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.5.0.tgz#69bee3b273d2d4ee8649a2eb409514b7df744a27"
+ integrity sha512-SIbuLMv6jsk1FnLIU5OUG/+VMGUprEjM1+o2trOAx8i5KOKMrhyezb1dJ4Ugsykb8Jgq8/w5NEopy6escV9G7g==
dependencies:
loader-utils "^2.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"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
-nanoid@^3.1.20:
- version "3.1.20"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
- integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+nanoid@^3.1.22:
+ version "3.1.22"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
+ integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
nanomatch@^1.2.9:
version "1.2.13"
@@ -8485,12 +8449,11 @@ postcss-selector-parser@^6.0.4:
uniq "^1.0.1"
util-deprecate "^1.0.2"
-postcss-svgo@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
- integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+postcss-svgo@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e"
+ integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==
dependencies:
- is-svg "^3.0.0"
postcss "^7.0.0"
postcss-value-parser "^3.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"
supports-color "^6.1.0"
-postcss@^8.2.8:
- version "8.2.8"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
- integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
+postcss@^8.2.10:
+ version "8.2.10"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b"
+ integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==
dependencies:
colorette "^1.2.2"
- nanoid "^3.1.20"
+ nanoid "^3.1.22"
source-map "^0.6.1"
postgres-array@~2.0.0:
@@ -9150,10 +9113,10 @@ readdirp@^2.2.1:
micromatch "^3.1.10"
readable-stream "^2.0.2"
-readdirp@~3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
- integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
+readdirp@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
+ integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
dependencies:
picomatch "^2.2.1"
@@ -9174,10 +9137,10 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
-redis-commands@^1.5.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23"
- integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==
+redis-commands@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
+ integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
@@ -9191,13 +9154,13 @@ redis-parser@^3.0.0:
dependencies:
redis-errors "^1.0.0"
-redis@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a"
- integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==
+redis@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.1.tgz#a44bee7c072dcf685e139048d6a1a4d3b00f5d01"
+ integrity sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg==
dependencies:
- denque "^1.4.1"
- redis-commands "^1.5.0"
+ denque "^1.5.0"
+ redis-commands "^1.7.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
@@ -9633,12 +9596,12 @@ sass-loader@^10.1.1:
schema-utils "^3.0.0"
semver "^7.3.2"
-sass@^1.32.8:
- version "1.32.8"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.8.tgz#f16a9abd8dc530add8834e506878a2808c037bdc"
- integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==
+sass@^1.32.10:
+ version "1.32.10"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.10.tgz#d40da4e20031b450359ee1c7e69bc8cc89569241"
+ integrity sha512-Nx0pcWoonAkn7CRp0aE/hket1UP97GiR1IFw3kcjV3pnenhWgZEWUf0ZcfPOV2fK52fnOcK3JdC/YYZ9E47DTQ==
dependencies:
- chokidar ">=2.0.0 <4.0.0"
+ chokidar ">=3.0.0 <4.0.0"
sax@~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"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
- version "7.3.4"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
- integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
+semver@^7.2.1, semver@^7.3.2, semver@^7.3.5:
+ version "7.3.5"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+ integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
@@ -10118,9 +10081,9 @@ sshpk@^1.7.0:
tweetnacl "~0.14.0"
ssri@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
- integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
+ integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
dependencies:
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"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
-webpack-assets-manifest@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-4.0.2.tgz#ead6e6dbdcd1c2af45d11a382246fcc79a286372"
- integrity sha512-bBb9PvEGDOCFvW5/t6Yp9MEE0fymNJ0OvEud9nPvQegDbQEUZ/2WTeHnNoALwWMu1x3JHPyqHVYh8SwtYZ/dww==
+webpack-assets-manifest@^4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-4.0.5.tgz#802d45fd58203fc7a70ac557636a93605a218d3f"
+ integrity sha512-cvvr0AtTHyi7D9otmLkv0Bv8j5KKwwD5Wwt6MNxLxgc3U3XmIZnNykw2PMChzUvPr9Ibiv9ceROIc0mS1C7MeA==
dependencies:
chalk "^4.0"
deepmerge "^4.0"
lockfile "^1.0"
- lodash.escaperegexp "^4.0"
lodash.get "^4.0"
lodash.has "^4.0"
mkdirp "^1.0"
@@ -11229,10 +11191,10 @@ webpack-assets-manifest@^4.0.2:
tapable "^1.0"
webpack-sources "^1.0"
-webpack-bundle-analyzer@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7"
- integrity sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==
+webpack-bundle-analyzer@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz#c71fb2eaffc10a4754d7303b224adb2342069da1"
+ integrity sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw==
dependencies:
acorn "^8.0.4"
acorn-walk "^8.0.0"
@@ -11512,15 +11474,10 @@ ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
-ws@^7.2.3, ws@^7.3.1:
- version "7.4.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7"
- integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==
-
-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==
+ws@^7.2.3, ws@^7.3.1, ws@^7.4.5:
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1"
+ integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==
xml-name-validator@^3.0.0:
version "3.0.0"