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

Conflicts:
- `README.md`:
  Upstream updated copyright year, we don't mention it so kept our version.
- `app/controllers/admin/dashboard_controller.rb`:
  Not really a conflict, upstream change (removing the spam checker) too close
  to glitch-soc changes. Ported upstream changes.
- `app/models/form/admin_settings.rb`:
  Same.
- `app/services/remove_status_service.rb`:
  Same.
- `app/views/admin/settings/edit.html.haml`:
  Same.
- `config/settings.yml`:
  Same.
- `config/environments/production.rb`:
  Not a real conflict, upstream added a default HTTP header, but we have
  extra headers in glitch-soc.
  Added the header.
main
Claire 4 years ago
commit e2a2bc9021

@ -1 +1 @@
2.7.2
2.7.3

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

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

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

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

@ -0,0 +1,53 @@
# frozen_string_literal: true
module Admin
class FollowRecommendationsController < BaseController
before_action :set_language
def show
authorize :follow_recommendation, :show?
@form = Form::AccountBatch.new
@accounts = filtered_follow_recommendations
end
def update
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
# Do nothing
ensure
redirect_to admin_follow_recommendations_path(filter_params)
end
private
def set_language
@language = follow_recommendation_filter.language
end
def filtered_follow_recommendations
follow_recommendation_filter.results
end
def follow_recommendation_filter
@follow_recommendation_filter ||= FollowRecommendationFilter.new(filter_params)
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def filter_params
params.slice(*FollowRecommendationFilter::KEYS).permit(*FollowRecommendationFilter::KEYS)
end
def action_from_button
if params[:suppress]
'suppress_follow_recommendation'
elsif params[:unsuppress]
'unsuppress_follow_recommendation'
end
end
end
end

@ -3,13 +3,13 @@
class Api::V1::Push::SubscriptionsController < Api::BaseController
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

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

@ -0,0 +1,19 @@
# frozen_string_literal: true
class Api::V2::SuggestionsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
before_action :set_suggestions
def index
render json: @suggestions, each_serializer: REST::SuggestionSerializer
end
private
def set_suggestions
@suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT))
end
end

@ -2,6 +2,7 @@
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
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

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

@ -0,0 +1,18 @@
# frozen_string_literal: true
module EmailHelper
def self.included(base)
base.extend(self)
end
def email_to_canonical_email(str)
username, domain = str.downcase.split('@', 2)
username, = username.gsub('.', '').split('+', 2)
"#{username}@#{domain}"
end
def email_to_canonical_email_hash(str)
Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str))
end
end

@ -24,6 +24,7 @@ export function normalizeAccount(account) {
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
account.note_plain = unescapeHTML(account.note);
if (account.fields) {
account.fields = account.fields.map(pair => ({

@ -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());
}
}));
};

@ -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(() => {});
};

@ -78,8 +78,10 @@ class Account extends ImmutablePureComponent {
let buttons;
if (onActionClick && actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
if (actionIcon) {
if (onActionClick) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
}
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);

@ -0,0 +1,9 @@
import React from 'react';
const Logo = () => (
<svg viewBox='0 0 216.4144 232.00976' className='logo'>
<use xlinkHref='#mastodon-svg-logo' />
</svg>
);
export default Logo;

@ -1,12 +1,10 @@
import React from 'react';
import { 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 <Introduction />;
}
return (
<BrowserRouter basename='/web'>
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
<Route path='/' component={UI} />
</ScrollContext>
</BrowserRouter>
);
}
}
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 {
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<ErrorBoundary>
<MastodonMount />
<BrowserRouter basename='/web'>
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
<Route path='/' component={UI} />
</ScrollContext>
</BrowserRouter>
</ErrorBoundary>
</Provider>
</IntlProvider>

@ -51,12 +51,12 @@ class SearchResults extends ImmutablePureComponent {
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
</div>
{suggestions && suggestions.map(accountId => (
{suggestions && suggestions.map(suggestion => (
<AccountContainer
key={accountId}
id={accountId}
actionIcon='times'
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
key={suggestion.get('account')}
id={suggestion.get('account')}
actionIcon={suggestion.get('source') === 'past_interaction' ? 'times' : null}
actionTitle={suggestion.get('source') === 'past_interaction' ? intl.formatMessage(messages.dismissSuggestion) : null}
onActionClick={dismissSuggestion}
/>
))}

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

@ -0,0 +1,85 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink';
import IconButton from 'mastodon/components/icon_button';
import { injectIntl, defineMessages } from 'react-intl';
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const getFirstSentence = str => {
const arr = str.split(/(([\.\?!]+\s)|[.。?!\n•])/);
return arr[0];
};
export default @connect(makeMapStateToProps)
@injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
};
handleFollow = () => {
const { account, dispatch } = this.props;
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(unfollowAccount(account.get('id')));
} else {
dispatch(followAccount(account.get('id')));
}
}
render () {
const { account, intl } = this.props;
let button;
if (account.getIn(['relationship', 'following'])) {
button = <IconButton icon='check' title={intl.formatMessage(messages.unfollow)} active onClick={this.handleFollow} />;
} else {
button = <IconButton icon='plus' title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} />;
}
return (
<div className='account follow-recommendations-account'>
<div className='account__wrapper'>
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
<div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
</Permalink>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
}
}

@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { changeSetting, saveSettings } from 'mastodon/actions/settings';
import { requestBrowserPermission } from 'mastodon/actions/notifications';
import Column from 'mastodon/features/ui/components/column';
import Account from './components/account';
import Logo from 'mastodon/components/logo';
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
import Button from 'mastodon/components/button';
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
});
export default @connect(mapStateToProps)
class FollowRecommendations extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
};
componentDidMount () {
const { dispatch, suggestions } = this.props;
// Don't re-fetch if we're e.g. navigating backwards to this page,
// since we don't want followed accounts to disappear from the list
if (suggestions.size === 0) {
dispatch(fetchSuggestions(true));
}
}
handleDone = () => {
const { dispatch } = this.props;
const { router } = this.context;
dispatch(requestBrowserPermission((permission) => {
if (permission === 'granted') {
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
dispatch(changeSetting(['notifications', 'alerts', 'status'], true));
dispatch(saveSettings());
}
}));
router.history.push('/timelines/home');
}
render () {
const { suggestions, isLoading } = this.props;
return (
<Column>
<div className='scrollable'>
<div className='column-title'>
<Logo />
<h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3>
<p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p>
</div>
{!isLoading && (
<React.Fragment>
<div>
{suggestions.map(suggestion => (
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
))}
</div>
<div className='column-actions'>
<img src={imageGreeting} alt='' className='column-actions__background' />
<Button onClick={this.handleDone}><FormattedMessage id='follow_recommendations.done' defaultMessage='Done' /></Button>
</div>
</React.Fragment>
)}
</div>
</Column>
);
}
}

@ -51,10 +51,12 @@ import {
Lists,
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 <Status /> 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 {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
@ -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());

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

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

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

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AccountReachFinder
def initialize(account)
@account = account
end
def inboxes
(followers_inboxes + reporters_inboxes + relay_inboxes).uniq
end
private
def followers_inboxes
@account.followers.inboxes
end
def reporters_inboxes
Account.where(id: @account.targeted_reports.select(:account_id)).inboxes
end
def relay_inboxes
Relay.enabled.pluck(:inbox_url)
end
end

@ -88,7 +88,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
resolve_thread(@status)
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?

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

@ -7,7 +7,6 @@ class Admin::SystemCheck::SidekiqProcessCheck < Admin::SystemCheck::BaseCheck
mailers
pull
scheduler
ingress
).freeze
def pass?

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

@ -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
"<a href=\"#{encode(tag_url(tag))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
end
def mention_html(account)
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"
def mention_html(account, with_domain: false)
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(with_domain ? account.pretty_acct : account.username)}</span></a></span>"
end
end

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

@ -1,198 +0,0 @@
# frozen_string_literal: true
class SpamCheck
include Redisable
include ActionView::Helpers::TextHelper
# Threshold over which two Nilsimsa values are considered
# to refer to the same text
NILSIMSA_COMPARE_THRESHOLD = 95
# Nilsimsa doesn't work well on small inputs, so below
# this size, we check only for exact matches with MD5
NILSIMSA_MIN_SIZE = 10
# How long to keep the trail of digests between updates,
# there is no reason to store it forever
EXPIRE_SET_AFTER = 1.week.seconds
# How many digests to keep in an account's trail. If it's
# too small, spam could rotate around different message templates
MAX_TRAIL_SIZE = 10
# How many detected duplicates to allow through before
# considering the message as spam
THRESHOLD = 5
def initialize(status)
@account = status.account
@status = status
end
def skip?
disabled? || already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply?
end
def spam?
if insufficient_data?
false
elsif nilsimsa?
digests_over_threshold?('nilsimsa') { |_, other_digest| nilsimsa_compare_value(digest, other_digest) >= NILSIMSA_COMPARE_THRESHOLD }
else
digests_over_threshold?('md5') { |_, other_digest| other_digest == digest }
end
end
def flag!
auto_report_status!
end
def remember!
# The scores in sorted sets don't actually have enough bits to hold an exact
# value of our snowflake IDs, so we use it only for its ordering property. To
# get the correct status ID back, we have to save it in the string value
redis.zadd(redis_key, @status.id, digest_with_algorithm)
redis.zremrangebyrank(redis_key, 0, -(MAX_TRAIL_SIZE + 1))
redis.expire(redis_key, EXPIRE_SET_AFTER)
end
def reset!
redis.del(redis_key)
end
def hashable_text
return @hashable_text if defined?(@hashable_text)
@hashable_text = @status.text
@hashable_text = remove_mentions(@hashable_text)
@hashable_text = strip_tags(@hashable_text) unless @status.local?
@hashable_text = normalize_unicode(@status.spoiler_text + ' ' + @hashable_text)
@hashable_text = remove_whitespace(@hashable_text)
end
def insufficient_data?
hashable_text.blank?
end
def digest
@digest ||= begin
if nilsimsa?
Nilsimsa.new(hashable_text).hexdigest
else
Digest::MD5.hexdigest(hashable_text)
end
end
end
def digest_with_algorithm
if nilsimsa?
['nilsimsa', digest, @status.id].join(':')
else
['md5', digest, @status.id].join(':')
end
end
class << self
def perform(status)
spam_check = new(status)
return if spam_check.skip?
if spam_check.spam?
spam_check.flag!
else
spam_check.remember!
end
end
end
private
def disabled?
!Setting.spam_check_enabled
end
def remove_mentions(text)
return text.gsub(Account::MENTION_RE, '') if @status.local?
Nokogiri::HTML.fragment(text).tap do |html|
mentions = @status.mentions.map { |mention| ActivityPub::TagManager.instance.url_for(mention.account) }
html.traverse do |element|
element.unlink if element.name == 'a' && mentions.include?(element['href'])
end
end.to_s
end
def normalize_unicode(text)
text.unicode_normalize(:nfkc).downcase
end
def remove_whitespace(text)
text.gsub(/\s+/, ' ').strip
end
def auto_report_status!
status_ids = Status.where(visibility: %i(public unlisted)).where(id: matching_status_ids).pluck(:id) + [@status.id] if @status.distributable?
ReportService.new.call(Account.representative, @account, status_ids: status_ids, comment: I18n.t('spam_check.spam_detected'))
end
def already_flagged?
@account.silenced? || @account.targeted_reports.unresolved.where(account_id: -99).exists?
end
def trusted?
@account.trust_level > Account::TRUST_LEVELS[:untrusted] || (@account.local? && @account.user_staff?)
end
def no_unsolicited_mentions?
@status.mentions.all? { |mention| mention.silent? || (!@account.local? && !mention.account.local?) || mention.account.following?(@account) }
end
def solicited_reply?
!@status.thread.nil? && @status.thread.mentions.where(account: @account).exists?
end
def nilsimsa_compare_value(first, second)
first = [first].pack('H*')
second = [second].pack('H*')
bits = 0
0.upto(31) do |i|
bits += Nilsimsa::POPC[255 & (first[i].ord ^ second[i].ord)].ord
end
128 - bits # -128 <= Nilsimsa Compare Value <= 128
end
def nilsimsa?
hashable_text.size > NILSIMSA_MIN_SIZE
end
def other_digests
redis.zrange(redis_key, 0, -1)
end
def digests_over_threshold?(filter_algorithm)
other_digests.select do |record|
algorithm, other_digest, status_id = record.split(':')
next unless algorithm == filter_algorithm
yield algorithm, other_digest, status_id
end.size >= THRESHOLD
end
def matching_status_ids
if nilsimsa?
other_digests.filter_map { |record| record.split(':')[2] if record.start_with?('nilsimsa') && nilsimsa_compare_value(digest, record.split(':')[1]) >= NILSIMSA_COMPARE_THRESHOLD }
else
other_digests.filter_map { |record| record.split(':')[2] if record.start_with?('md5') && record.split(':')[1] == digest }
end
end
def redis_key
@redis_key ||= "spam_check:#{@account.id}"
end
end

@ -6,11 +6,22 @@ class StatusReachFinder
end
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

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

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

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AccountSuggestions
class Suggestion < ActiveModelSerializers::Model
attributes :account, :source
end
def self.get(account, limit)
suggestions = PotentialFriendshipTracker.get(account, limit).map { |target_account| Suggestion.new(account: target_account, source: :past_interaction) }
suggestions.concat(FollowRecommendation.get(account, limit - suggestions.size, suggestions.map { |suggestion| suggestion.account.id }).map { |target_account| Suggestion.new(account: target_account, source: :global) }) if suggestions.size < limit
suggestions
end
def self.remove(account, target_account_id)
PotentialFriendshipTracker.remove(account.id, target_account_id)
end
end

@ -0,0 +1,25 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_summaries
#
# account_id :bigint(8) primary key
# language :string
# sensitive :boolean
#
class AccountSummary < ApplicationRecord
self.primary_key = :account_id
scope :safe, -> { where(sensitive: false) }
scope :localized, ->(locale) { where(language: locale) }
scope :filtered, -> { joins(arel_table.join(FollowRecommendationSuppression.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:account_id].eq(FollowRecommendationSuppression.arel_table[:account_id])).join_sources).where(FollowRecommendationSuppression.arel_table[:id].eq(nil)) }
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
end
def readonly?
true
end
end

@ -0,0 +1,27 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: canonical_email_blocks
#
# id :bigint(8) not null, primary key
# canonical_email_hash :string default(""), not null
# reference_account_id :bigint(8) not null
# created_at :datetime not null
# updated_at :datetime not null
#
class CanonicalEmailBlock < ApplicationRecord
include EmailHelper
belongs_to :reference_account, class_name: 'Account'
validates :canonical_email_hash, presence: true
def email=(email)
self.canonical_email_hash = email_to_canonical_email_hash(email)
end
def self.block?(email)
where(canonical_email_hash: email_to_canonical_email_hash(email)).exists?
end
end

@ -63,5 +63,8 @@ module AccountAssociations
# Account deletion requests
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

@ -0,0 +1,39 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: follow_recommendations
#
# account_id :bigint(8) primary key
# rank :decimal(, )
# reason :text is an Array
#
class FollowRecommendation < ApplicationRecord
self.primary_key = :account_id
belongs_to :account_summary, foreign_key: :account_id
belongs_to :account, foreign_key: :account_id
scope :safe, -> { joins(:account_summary).merge(AccountSummary.safe) }
scope :localized, ->(locale) { joins(:account_summary).merge(AccountSummary.localized(locale)) }
scope :filtered, -> { joins(:account_summary).merge(AccountSummary.filtered) }
def readonly?
true
end
def self.get(account, limit, exclude_account_ids = [])
account_ids = Redis.current.zrevrange("follow_recommendations:#{account.user_locale}", 0, -1).map(&:to_i) - exclude_account_ids - [account.id]
return [] if account_ids.empty? || limit < 1
accounts = Account.followable_by(account)
.not_excluded_by_account(account)
.not_domain_blocked_by_account(account)
.where(id: account_ids)
.limit(limit)
.index_by(&:id)
account_ids.map { |id| accounts[id] }.compact
end
end

@ -0,0 +1,26 @@
# frozen_string_literal: true
class FollowRecommendationFilter
KEYS = %i(
language
status
).freeze
attr_reader :params, :language
def initialize(params)
@language = params.delete('language') || I18n.locale
@params = params
end
def results
if params['status'] == 'suppressed'
Account.joins(:follow_recommendation_suppression).order(FollowRecommendationSuppression.arel_table[:id].desc).to_a
else
account_ids = Redis.current.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i)
accounts = Account.where(id: account_ids).index_by(&:id)
account_ids.map { |id| accounts[id] }.compact
end
end
end

@ -0,0 +1,28 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: follow_recommendation_suppressions
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# created_at :datetime not null
# updated_at :datetime not null
#
class FollowRecommendationSuppression < ApplicationRecord
include Redisable
belongs_to :account
after_commit :remove_follow_recommendations, on: :create
private
def remove_follow_recommendations
redis.pipelined do
I18n.available_locales.each do |locale|
redis.zrem("follow_recommendations:#{locale}", account_id)
end
end
end
end

@ -21,6 +21,10 @@ class Form::AccountBatch
approve!
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

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

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

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

@ -0,0 +1,7 @@
# frozen_string_literal: true
class REST::SuggestionSerializer < ActiveModel::Serializer
attributes :source
has_one :account, serializer: REST::AccountSerializer
end

@ -43,7 +43,6 @@ class ProcessMentionsService < BaseService
end
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

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

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

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

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

@ -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)
def blocked_canonical_email?
CanonicalEmailBlock.block?(@email)
end
regexp.match?(@email)
def disallowed_through_email_domain_block?
EmailDomainBlock.block?(@email)
end
def not_on_whitelist?
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

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

@ -0,0 +1,20 @@
.batch-table__row
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
.batch-table__row__content.batch-table__row__content--unpadded
%table.accounts-table
%tbody
%tr
%td= account_link_to account
%td.accounts-table__count.optional
= number_to_human account.statuses_count, strip_insignificant_zeros: true
%small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional
= number_to_human account.followers_count, strip_insignificant_zeros: true
%small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count
- if account.last_status_at.present?
%time.time-ago{ datetime: account.last_status_at.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at
- else
\-
%small= t('accounts.last_active')

@ -0,0 +1,41 @@
- content_for :page_title do
= t('admin.follow_recommendations.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
%p= t('admin.follow_recommendations.description_html')
%hr.spacer/
= form_tag admin_follow_recommendations_path, method: 'GET', class: 'simple_form' do
.filters
.filter-subset.filter-subset--with-select
%strong= t('admin.follow_recommendations.language')
.input.select.optional
= select_tag :language, options_for_select(I18n.available_locales.map { |key| [human_locale(key), key]}, @language)
.filter-subset
%strong= t('admin.follow_recommendations.status')
%ul
%li= filter_link_to t('admin.accounts.moderation.active'), status: nil
%li= filter_link_to t('admin.follow_recommendations.suppressed'), status: 'suppressed'
= form_for(@form, url: admin_follow_recommendations_path, method: :patch) do |f|
- RelationshipFilter::KEYS.each do |key|
= hidden_field_tag key, params[key] if params[key].present?
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
- if params[:status].blank? && can?(:suppress, :follow_recommendation)
= f.button safe_join([fa_icon('times'), t('admin.follow_recommendations.suppress')]), name: :suppress, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- if params[:status] == 'suppressed' && can?(:unsuppress, :follow_recommendation)
= f.button safe_join([fa_icon('plus'), t('admin.follow_recommendations.unsuppress')]), name: :unsuppress, class: 'table-action-link', type: :submit
.batch-table__body
- if @accounts.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'account', collection: @accounts, locals: { f: f }

@ -1,8 +1,9 @@
- content_for :page_title do
= 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|

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

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

@ -0,0 +1,61 @@
# frozen_string_literal: true
class Scheduler::FollowRecommendationsScheduler
include Sidekiq::Worker
include Redisable
sidekiq_options retry: 0
# The maximum number of accounts that can be requested in one page from the
# API is 80, and the suggestions API does not allow pagination. This number
# leaves some room for accounts being filtered during live access
SET_SIZE = 100
def perform
# Maintaining a materialized view speeds-up subsequent queries significantly
AccountSummary.refresh
fallback_recommendations = FollowRecommendation.safe.filtered.limit(SET_SIZE).index_by(&:account_id)
I18n.available_locales.each do |locale|
recommendations = begin
if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist
FollowRecommendation.safe.filtered.localized(locale).limit(SET_SIZE).index_by(&:account_id)
else
{}
end
end
# Use language-agnostic results if there are not enough language-specific ones
missing = SET_SIZE - recommendations.keys.size
if missing.positive?
added = 0
# Avoid duplicate results
fallback_recommendations.each_value do |recommendation|
next if recommendations.key?(recommendation.account_id)
recommendations[recommendation.account_id] = recommendation
added += 1
break if added >= missing
end
end
redis.pipelined do
redis.del(key(locale))
recommendations.each_value do |recommendation|
redis.zadd(key(locale), recommendation.rank, recommendation.account_id)
end
end
end
end
private
def key(locale)
"follow_recommendations:#{locale}"
end
end

@ -3,22 +3,67 @@
class Web::PushNotificationWorker
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)
# Polymorphically associated activity could have been deleted
# in the meantime, so we have to double-check before proceeding
return unless @notification.activity.present? && @subscription.pushable?(@notification)
payload = @subscription.encrypt(push_notification_json)
subscription.push(notification) unless notification.activity.nil?
rescue Webpush::ResponseError => e
code = e.response.code.to_i
request_pool.with(@subscription.audience) do |http_client|
request = Request.new(:post, @subscription.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
if (400..499).cover?(code) && ![408, 429].include?(code)
subscription.destroy!
else
raise e
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

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

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

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

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

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

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

@ -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: "<strong>Follow recommendations help new users quickly find interesting content</strong>. When a user has not interacted with others enough to form personalized follow recommendations, these accounts are recommended instead. They are re-calculated on a daily basis from a mix of accounts with the highest recent engagements and highest local follower counts for a given language."
language: For language
status: Status
suppress: Suppress follow recommendation
suppressed: Suppressed
title: Follow recommendations
unsuppress: Restore follow recommendation
instances:
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. <strong>Make it easier to see your server's rules at a glance by providing them in a flat bullet point list.</strong> Try to keep individual rules short and simple, but try not to split them up into many separate items either.
edit: Edit rule
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:

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

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

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

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

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

@ -0,0 +1,17 @@
class AccountIdsToTimestampIds < ActiveRecord::Migration[5.1]
def up
# Set up the accounts.id column to use our timestamp-based IDs.
safety_assured do
execute("ALTER TABLE accounts ALTER COLUMN id SET DEFAULT timestamp_id('accounts')")
end
# Make sure we have a sequence to use.
Mastodon::Snowflake.ensure_id_sequences_exist
end
def down
execute("LOCK accounts")
execute("SELECT setval('accounts_id_seq', (SELECT MAX(id) FROM accounts))")
execute("ALTER TABLE accounts ALTER COLUMN id SET DEFAULT nextval('accounts_id_seq')")
end
end

@ -0,0 +1,9 @@
class CreateAccountSummaries < ActiveRecord::Migration[5.2]
def change
create_view :account_summaries, materialized: true
# To be able to refresh the view concurrently,
# at least one unique index is required
safety_assured { add_index :account_summaries, :account_id, unique: true }
end
end

@ -0,0 +1,5 @@
class CreateFollowRecommendations < ActiveRecord::Migration[5.2]
def change
create_view :follow_recommendations
end
end

@ -0,0 +1,9 @@
class CreateFollowRecommendationSuppressions < ActiveRecord::Migration[6.1]
def change
create_table :follow_recommendation_suppressions do |t|
t.references :account, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
t.timestamps
end
end
end

@ -0,0 +1,10 @@
class CreateCanonicalEmailBlocks < ActiveRecord::Migration[6.1]
def change
create_table :canonical_email_blocks do |t|
t.string :canonical_email_hash, null: false, default: '', index: { unique: true }
t.belongs_to :reference_account, null: false, foreign_key: { on_cascade: :delete, to_table: 'accounts' }
t.timestamps
end
end
end

@ -2,15 +2,15 @@
# of editing this file, please use the migrations feature of Active Record to
# 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

@ -0,0 +1,22 @@
SELECT
accounts.id AS account_id,
mode() WITHIN GROUP (ORDER BY language ASC) AS language,
mode() WITHIN GROUP (ORDER BY sensitive ASC) AS sensitive
FROM accounts
CROSS JOIN LATERAL (
SELECT
statuses.account_id,
statuses.language,
statuses.sensitive
FROM statuses
WHERE statuses.account_id = accounts.id
AND statuses.deleted_at IS NULL
ORDER BY statuses.id DESC
LIMIT 20
) t0
WHERE accounts.suspended_at IS NULL
AND accounts.silenced_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND accounts.discoverable = 't'
AND accounts.locked = 'f'
GROUP BY accounts.id

@ -0,0 +1,38 @@
SELECT
account_id,
sum(rank) AS rank,
array_agg(reason) AS reason
FROM (
SELECT
accounts.id AS account_id,
count(follows.id) / (1.0 + count(follows.id)) AS rank,
'most_followed' AS reason
FROM follows
INNER JOIN accounts ON accounts.id = follows.target_account_id
INNER JOIN users ON users.account_id = follows.account_id
WHERE users.current_sign_in_at >= (now() - interval '30 days')
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND accounts.silenced_at IS NULL
AND accounts.locked = 'f'
AND accounts.discoverable = 't'
GROUP BY accounts.id
HAVING count(follows.id) >= 5
UNION ALL
SELECT accounts.id AS account_id,
sum(reblogs_count + favourites_count) / (1.0 + sum(reblogs_count + favourites_count)) AS rank,
'most_interactions' AS reason
FROM status_stats
INNER JOIN statuses ON statuses.id = status_stats.status_id
INNER JOIN accounts ON accounts.id = statuses.account_id
WHERE statuses.id >= ((date_part('epoch', now() - interval '30 days') * 1000)::bigint << 16)
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND accounts.silenced_at IS NULL
AND accounts.locked = 'f'
AND accounts.discoverable = 't'
GROUP BY accounts.id
HAVING sum(reblogs_count + favourites_count) >= 5
) t0
GROUP BY account_id
ORDER BY rank DESC

@ -0,0 +1,44 @@
# frozen_string_literal: true
module ActiveRecord
module Batches
def pluck_each(*column_names)
relation = self
options = column_names.extract_options!
flatten = column_names.size == 1
batch_limit = options[:batch_limit] || 1_000
order = options[:order] || :asc
column_names.unshift(primary_key)
relation = relation.reorder(batch_order(order)).limit(batch_limit)
relation.skip_query_cache!
batch_relation = relation
loop do
batch = batch_relation.pluck(*column_names)
break if batch.empty?
primary_key_offset = batch.last[0]
batch.each do |record|
if flatten
yield record[1]
else
yield record[1..-1]
end
end
break if batch.size < batch_limit
batch_relation = relation.where(
predicate_builder[primary_key, primary_key_offset, order == :desc ? :lt : :gt]
)
end
end
end
end

@ -91,7 +91,7 @@ namespace :emojis do
desc 'Generate emoji variants with white borders'
task :generate_borders do
src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json')
emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂‍♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂‍♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'
emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂‍♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂‍♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲🚲👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'
map = Oj.load(File.read(src))

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

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 40 40">
<g>
<path d="M7 24c1.957 0 3.633 1.135 4.455 2.772l3.477-1.739C13.488 22.058 10.446 20 6.916 20c-1.301 0-2.534.285-3.649.787l1.668 3.67C5.566 24.17 6.262 24 7 24zm22 0c1.467 0 2.772.643 3.688 1.648l2.897-2.635C33.952 21.169 31.573 20 28.916 20c-3.576 0-6.652 2.111-8.073 5.15l3.648 1.722C25.293 25.18 27.003 24 29 24z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
<path d="M7 22c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5 5 2.238 5 5-2.238 5-5 5zm22-12c-3.865 0-7 3.134-7 7s3.135 7 7 7c3.867 0 7-3.134 7-7s-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5c2.762 0 5 2.238 5 5s-2.238 5-5 5z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
<path d="M29.984 28.922c-.005-.067-.021-.132-.04-.198-.019-.065-.04-.126-.071-.186-.013-.024-.015-.052-.029-.075l-7-11c-.297-.466-.914-.604-1.381-.307-.299.19-.444.513-.445.843H12c-.552 0-1 .447-1 1 0 .553.448 1 1 1h10c.027 0 .05-.014.077-.016L27.178 28H18c-.552 0-1 .447-1 1s.448 1 1 1h11.001c.116 0 .23-.028.343-.069.034-.013.064-.027.097-.043.031-.017.066-.024.097-.044.03-.02.048-.051.075-.072.055-.044.103-.089.147-.143.041-.049.074-.099.104-.154.03-.056.055-.11.075-.172.021-.066.033-.132.04-.201.004-.036.021-.066.021-.102 0-.027-.014-.051-.016-.078z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
<path d="M21.581 16l-2.899 8.117-5.929-6.775c-.364-.415-.996-.459-1.411-.094-.415.364-.457.995-.094 1.411l6.664 7.615-.854 2.39c-.185.519.086 1.092.606 1.277.111.04.224.059.336.059.411 0 .796-.255.942-.664L23.705 16h-2.124z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
<path d="M7 30c-.15 0-.303-.034-.446-.105-.494-.247-.694-.848-.447-1.342l3.062-6.106C9.186 22.419 11 19.651 11 17c0-3.242-2.293-4.043-2.316-4.051-.524-.175-.807-.741-.632-1.265.174-.524.739-.81 1.265-.632C9.467 11.102 13 12.333 13 17c0 3.068-1.836 6.042-2.131 6.497l-2.974 5.949C7.72 29.798 7.367 30 7 30z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
<path d="M14.612 13.663c-.054 0-.11-.004-.165-.014l-6-1c-.544-.091-.913-.606-.822-1.151.091-.544.601-.913 1.151-.822l6 1c.544.091.913.606.822 1.151-.082.489-.506.836-.986.836zM26.383 17c-.03 0-.059-.002-.089-.006l-5.672-.708c-.372-.046-.644-.374-.62-.748.023-.374.333-.665.707-.665.041 0 4.067-.018 5.989-1.299.25-.167.582-.157.824.026.239.185.337.501.241.788l-.709 2.127c-.096.293-.369.485-.671.485z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
<path d="M20 29c0 1.104-.895 2-2 2-1.104 0-2-.896-2-2s.896-2 2-2c1.105 0 2 .896 2 2z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
</g>
<path fill="#EA596E" d="M7 24c1.957 0 3.633 1.135 4.455 2.772l3.477-1.739C13.488 22.058 10.446 20 6.916 20c-1.301 0-2.534.285-3.649.787l1.668 3.67C5.566 24.17 6.262 24 7 24zm22 0c1.467 0 2.772.643 3.688 1.648l2.897-2.635C33.952 21.169 31.573 20 28.916 20c-3.576 0-6.652 2.111-8.073 5.15l3.648 1.722C25.293 25.18 27.003 24 29 24z"/>
<path fill="#292F33" d="M7 22c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5 5 2.238 5 5-2.238 5-5 5zm22-12c-3.865 0-7 3.134-7 7s3.135 7 7 7c3.867 0 7-3.134 7-7s-3.133-7-7-7zm0 12c-2.761 0-5-2.238-5-5s2.239-5 5-5c2.762 0 5 2.238 5 5s-2.238 5-5 5z"/>
<path fill="#DD2E44" d="M29.984 28.922c-.005-.067-.021-.132-.04-.198-.019-.065-.04-.126-.071-.186-.013-.024-.015-.052-.029-.075l-7-11c-.297-.466-.914-.604-1.381-.307-.299.19-.444.513-.445.843H12c-.552 0-1 .447-1 1 0 .553.448 1 1 1h10c.027 0 .05-.014.077-.016L27.178 28H18c-.552 0-1 .447-1 1s.448 1 1 1h11.001c.116 0 .23-.028.343-.069.034-.013.064-.027.097-.043.031-.017.066-.024.097-.044.03-.02.048-.051.075-.072.055-.044.103-.089.147-.143.041-.049.074-.099.104-.154.03-.056.055-.11.075-.172.021-.066.033-.132.04-.201.004-.036.021-.066.021-.102 0-.027-.014-.051-.016-.078z"/>
<path fill="#DD2E44" d="M21.581 16l-2.899 8.117-5.929-6.775c-.364-.415-.996-.459-1.411-.094-.415.364-.457.995-.094 1.411l6.664 7.615-.854 2.39c-.185.519.086 1.092.606 1.277.111.04.224.059.336.059.411 0 .796-.255.942-.664L23.705 16h-2.124z"/>
<path fill="#DD2E44" d="M7 30c-.15 0-.303-.034-.446-.105-.494-.247-.694-.848-.447-1.342l3.062-6.106C9.186 22.419 11 19.651 11 17c0-3.242-2.293-4.043-2.316-4.051-.524-.175-.807-.741-.632-1.265.174-.524.739-.81 1.265-.632C9.467 11.102 13 12.333 13 17c0 3.068-1.836 6.042-2.131 6.497l-2.974 5.949C7.72 29.798 7.367 30 7 30z"/>
<path fill="#292F33" d="M14.612 13.663c-.054 0-.11-.004-.165-.014l-6-1c-.544-.091-.913-.606-.822-1.151.091-.544.601-.913 1.151-.822l6 1c.544.091.913.606.822 1.151-.082.489-.506.836-.986.836zM26.383 17c-.03 0-.059-.002-.089-.006l-5.672-.708c-.372-.046-.644-.374-.62-.748.023-.374.333-.665.707-.665.041 0 4.067-.018 5.989-1.299.25-.167.582-.157.824.026.239.185.337.501.241.788l-.709 2.127c-.096.293-.369.485-.671.485z"/>
<path fill="#66757F" d="M20 29c0 1.104-.895 2-2 2-1.104 0-2-.896-2-2s.896-2 2-2c1.105 0 2 .896 2 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

@ -4,23 +4,83 @@ RSpec.describe Api::V1::AppsController, type: :controller do
render_views
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
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 'creates an OAuth app' do
expect(Doorkeeper::Application.find_by(name: 'Test app')).to_not be nil
context 'with many duplicate scopes' do
let(:scopes) { (%w(read) * 40).join(' ') }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'only saves the scope once' do
expect(Doorkeeper::Application.find_by(name: client_name).scopes.to_s).to eq 'read'
end
end
context 'with a too-long name' do
let(:client_name) { 'hoge' * 20 }
it 'returns http unprocessable entity' do
expect(response).to have_http_status(422)
end
end
context 'with a too-long website' do
let(:website) { 'https://foo.bar/' + ('hoge' * 2_000) }
it 'returns http unprocessable entity' do
expect(response).to have_http_status(422)
end
end
it 'returns client ID and client secret' do
json = body_as_json
context 'with a too-long redirect_uris' do
let(:redirect_uris) { 'https://foo.bar/' + ('hoge' * 2_000) }
expect(json[:client_id]).to_not be_blank
expect(json[:client_secret]).to_not be_blank
it 'returns http unprocessable entity' do
expect(response).to have_http_status(422)
end
end
end
end

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

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

@ -0,0 +1,4 @@
Fabricator(:canonical_email_block) do
email "test@example.com"
reference_account { Fabricate(:account) }
end

@ -0,0 +1,3 @@
Fabricator(:follow_recommendation_suppression) do
account
end

@ -1,192 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe SpamCheck do
let!(:sender) { Fabricate(:account) }
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob') }
def status_with_html(text, options = {})
status = PostStatusService.new.call(sender, { text: text }.merge(options))
status.update_columns(text: Formatter.instance.format(status), local: false)
status
end
describe '#hashable_text' do
it 'removes mentions from HTML for remote statuses' do
status = status_with_html('@alice Hello')
expect(described_class.new(status).hashable_text).to eq 'hello'
end
it 'removes mentions from text for local statuses' do
status = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
expect(described_class.new(status).hashable_text).to eq 'hey , how are you?'
end
end
describe '#insufficient_data?' do
it 'returns true when there is no text' do
status = status_with_html('@alice')
expect(described_class.new(status).insufficient_data?).to be true
end
it 'returns false when there is text' do
status = status_with_html('@alice h')
expect(described_class.new(status).insufficient_data?).to be false
end
end
describe '#digest' do
it 'returns a string' do
status = status_with_html('@alice Hello world')
expect(described_class.new(status).digest).to be_a String
end
end
describe '#spam?' do
it 'returns false for a unique status' do
status = status_with_html('@alice Hello')
expect(described_class.new(status).spam?).to be false
end
it 'returns false for different statuses to the same recipient' do
status1 = status_with_html('@alice Hello')
described_class.new(status1).remember!
status2 = status_with_html('@alice Are you available to talk?')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for statuses with different content warnings' do
status1 = status_with_html('@alice Are you available to talk?')
described_class.new(status1).remember!
status2 = status_with_html('@alice Are you available to talk?', spoiler_text: 'This is a completely different matter than what I was talking about previously, I swear!')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for different statuses to different recipients' do
status1 = status_with_html('@alice How is it going?')
described_class.new(status1).remember!
status2 = status_with_html('@bob Are you okay?')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for very short different statuses to different recipients' do
status1 = status_with_html('@alice 🙄')
described_class.new(status1).remember!
status2 = status_with_html('@bob Huh?')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for statuses with no text' do
status1 = status_with_html('@alice')
described_class.new(status1).remember!
status2 = status_with_html('@bob')
expect(described_class.new(status2).spam?).to be false
end
it 'returns true for duplicate statuses to the same recipient' do
described_class::THRESHOLD.times do
status1 = status_with_html('@alice Hello')
described_class.new(status1).remember!
end
status2 = status_with_html('@alice Hello')
expect(described_class.new(status2).spam?).to be true
end
it 'returns true for duplicate statuses to different recipients' do
described_class::THRESHOLD.times do
status1 = status_with_html('@alice Hello')
described_class.new(status1).remember!
end
status2 = status_with_html('@bob Hello')
expect(described_class.new(status2).spam?).to be true
end
it 'returns true for nearly identical statuses with random numbers' do
source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.'
described_class::THRESHOLD.times do
status1 = status_with_html('@alice ' + source_text + ' 1234')
described_class.new(status1).remember!
end
status2 = status_with_html('@bob ' + source_text + ' 9568')
expect(described_class.new(status2).spam?).to be true
end
end
describe '#skip?' do
it 'returns true when the sender is already silenced' do
status = status_with_html('@alice Hello')
sender.silence!
expect(described_class.new(status).skip?).to be true
end
it 'returns true when the mentioned person follows the sender' do
status = status_with_html('@alice Hello')
alice.follow!(sender)
expect(described_class.new(status).skip?).to be true
end
it 'returns false when even one mentioned person doesn\'t follow the sender' do
status = status_with_html('@alice @bob Hello')
alice.follow!(sender)
expect(described_class.new(status).skip?).to be false
end
it 'returns true when the sender is replying to a status that mentions the sender' do
parent = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
status = status_with_html('@alice @bob Hello', thread: parent)
expect(described_class.new(status).skip?).to be true
end
end
describe '#remember!' do
let(:status) { status_with_html('@alice') }
let(:spam_check) { described_class.new(status) }
let(:redis_key) { spam_check.send(:redis_key) }
it 'remembers' do
expect(Redis.current.exists?(redis_key)).to be true
spam_check.remember!
expect(Redis.current.exists?(redis_key)).to be true
end
end
describe '#reset!' do
let(:status) { status_with_html('@alice') }
let(:spam_check) { described_class.new(status) }
let(:redis_key) { spam_check.send(:redis_key) }
before do
spam_check.remember!
end
it 'resets' do
expect(Redis.current.exists?(redis_key)).to be true
spam_check.reset!
expect(Redis.current.exists?(redis_key)).to be false
end
end
describe '#flag!' do
let!(:status1) { status_with_html('@alice General Kenobi you are a bold one') }
let!(:status2) { status_with_html('@alice @bob General Kenobi, you are a bold one') }
before do
described_class.new(status1).remember!
described_class.new(status2).flag!
end
it 'creates a report about the account' do
expect(sender.targeted_reports.unresolved.count).to eq 1
end
it 'attaches both matching statuses to the report' do
expect(sender.targeted_reports.first.status_ids).to include(status1.id, status2.id)
end
end
end

@ -83,40 +83,4 @@ RSpec.describe TagManager do
expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
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

@ -0,0 +1,47 @@
require 'rails_helper'
RSpec.describe CanonicalEmailBlock, type: :model do
describe '#email=' do
let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' }
it 'sets canonical_email_hash' do
subject.email = 'test@example.com'
expect(subject.canonical_email_hash).to eq target_hash
end
it 'sets the same hash even with dot permutations' do
subject.email = 't.e.s.t@example.com'
expect(subject.canonical_email_hash).to eq target_hash
end
it 'sets the same hash even with extensions' do
subject.email = 'test+mastodon1@example.com'
expect(subject.canonical_email_hash).to eq target_hash
end
it 'sets the same hash with different casing' do
subject.email = 'Test@EXAMPLE.com'
expect(subject.canonical_email_hash).to eq target_hash
end
end
describe '.block?' do
let!(:canonical_email_block) { Fabricate(:canonical_email_block, email: 'foo@bar.com') }
it 'returns true for the same email' do
expect(described_class.block?('foo@bar.com')).to be true
end
it 'returns true for the same email with dots' do
expect(described_class.block?('f.oo@bar.com')).to be true
end
it 'returns true for the same email with extensions' do
expect(described_class.block?('foo+spam@bar.com')).to be true
end
it 'returns false for different email' do
expect(described_class.block?('hoge@bar.com')).to be false
end
end
end

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe FollowRecommendationSuppression, type: :model do
end

@ -1,16 +1,94 @@
require 'rails_helper'
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

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

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rails_helper'
describe Web::PushNotificationWorker do
subject { described_class.new }
let(:p256dh) { 'BN4GvZtEZiZuqFxSKVZfSfluwKBD7UxHNBmWkfiZfCtgDE8Bwh-_MtLXbBxTBAWH9r7IPKL0lhdcaqtL1dfxU5E=' }
let(:auth) { 'Q2BoAjC09xH3ywDLNJr-dA==' }
let(:endpoint) { 'https://updates.push.services.mozilla.com/push/v1/subscription-id' }
let(:user) { Fabricate(:user) }
let(:notification) { Fabricate(:notification) }
let(:subscription) { Fabricate(:web_push_subscription, user_id: user.id, key_p256dh: p256dh, key_auth: auth, endpoint: endpoint, data: { alerts: { notification.type => true } }) }
let(:vapid_public_key) { 'BB37UCyc8LLX4PNQSe-04vSFvpUWGrENubUaslVFM_l5TxcGVMY0C3RXPeUJAQHKYlcOM2P4vTYmkoo0VZGZTM4=' }
let(:vapid_private_key) { 'OPrw1Sum3gRoL4-DXfSCC266r-qfFSRZrnj8MgIhRHg=' }
let(:vapid_key) { Webpush::VapidKey.from_keys(vapid_public_key, vapid_private_key) }
let(:contact_email) { 'sender@example.com' }
let(:ciphertext) { "+\xB8\xDBT}\x13\xB6\xDD.\xF9\xB0\xA7\xC8\xD2\x80\xFD\x99#\xF7\xAC\x83\xA4\xDB,\x1F\xB5\xB9w\x85>\xF7\xADr" }
let(:salt) { "X\x97\x953\xE4X\xF8_w\xE7T\x95\xC51q\xFE" }
let(:server_public_key) { "\x04\b-RK9w\xDD$\x16lFz\xF9=\xB4~\xC6\x12k\xF3\xF40t\xA9\xC1\fR\xC3\x81\x80\xAC\f\x7F\xE4\xCC\x8E\xC2\x88 n\x8BB\xF1\x9C\x14\a\xFA\x8D\xC9\x80\xA1\xDDyU\\&c\x01\x88#\x118Ua" }
let(:shared_secret) { "\t\xA7&\x85\t\xC5m\b\xA8\xA7\xF8B{1\xADk\xE1y'm\xEDE\xEC\xDD\xEDj\xB3$s\xA9\xDA\xF0" }
let(:payload) { { ciphertext: ciphertext, salt: salt, server_public_key: server_public_key, shared_secret: shared_secret } }
describe 'perform' do
before do
allow_any_instance_of(subscription.class).to receive(:contact_email).and_return(contact_email)
allow_any_instance_of(subscription.class).to receive(:vapid_key).and_return(vapid_key)
allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
stub_request(:post, endpoint).to_return(status: 201, body: '')
subject.perform(subscription.id, notification.id)
end
it 'calls the relevant service with the correct headers' do
expect(a_request(:post, endpoint).with(headers: {
'Content-Encoding' => 'aesgcm',
'Content-Type' => 'application/octet-stream',
'Crypto-Key' => 'dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=' + vapid_public_key.delete('='),
'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g',
'Ttl' => '172800',
'Urgency' => 'normal',
'Authorization' => 'WebPush jwt.encoded.payload',
}, body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr")).to have_been_made
end
end
end

@ -16,24 +16,24 @@
dependencies:
"@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"

Loading…
Cancel
Save