Merge commit '52ab8a59c6e77b6409a7d4d81b15751732b3af91' into glitch-soc/merge-upstream
This commit is contained in:
commit
0fb469e2f3
18 changed files with 125 additions and 42 deletions
2
Gemfile
2
Gemfile
|
@ -207,3 +207,5 @@ gem 'net-http', '~> 0.4.0'
|
||||||
gem 'rubyzip', '~> 2.3'
|
gem 'rubyzip', '~> 2.3'
|
||||||
|
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
||||||
|
gem 'mail', '~> 2.8'
|
||||||
|
|
|
@ -356,7 +356,7 @@ GEM
|
||||||
rdoc
|
rdoc
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.1)
|
json (2.7.2)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.15.3.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
|
@ -607,7 +607,7 @@ GEM
|
||||||
redlock (1.3.2)
|
redlock (1.3.2)
|
||||||
redis (>= 3.0.0, < 6.0)
|
redis (>= 3.0.0, < 6.0)
|
||||||
regexp_parser (2.9.0)
|
regexp_parser (2.9.0)
|
||||||
reline (0.4.3)
|
reline (0.5.0)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
|
@ -671,7 +671,7 @@ GEM
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (2.28.0)
|
rubocop-rspec (2.29.1)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
|
@ -692,7 +692,7 @@ GEM
|
||||||
sanitize (6.1.0)
|
sanitize (6.1.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.7.0)
|
scenic (1.8.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
selenium-webdriver (4.19.0)
|
selenium-webdriver (4.19.0)
|
||||||
|
@ -880,6 +880,7 @@ DEPENDENCIES
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
|
mail (~> 2.8)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
md-paperclip-azure (~> 2.2)
|
md-paperclip-azure (~> 2.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
|
|
1
Vagrantfile
vendored
1
Vagrantfile
vendored
|
@ -173,6 +173,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||||
|
|
||||||
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080
|
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080
|
||||||
config.vm.network :forwarded_port, guest: 3000, host: 3000
|
config.vm.network :forwarded_port, guest: 3000, host: 3000
|
||||||
|
config.vm.network :forwarded_port, guest: 3035, host: 3035
|
||||||
config.vm.network :forwarded_port, guest: 4000, host: 4000
|
config.vm.network :forwarded_port, guest: 4000, host: 4000
|
||||||
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
||||||
config.vm.network :forwarded_port, guest: 9200, host: 9200
|
config.vm.network :forwarded_port, guest: 9200, host: 9200
|
||||||
|
|
|
@ -3,7 +3,9 @@ import * as html from '../html';
|
||||||
describe('html', () => {
|
describe('html', () => {
|
||||||
describe('unescapeHTML', () => {
|
describe('unescapeHTML', () => {
|
||||||
it('returns unescaped HTML', () => {
|
it('returns unescaped HTML', () => {
|
||||||
const output = html.unescapeHTML('<p>lorem</p><p>ipsum</p><br><br>');
|
const output = html.unescapeHTML(
|
||||||
|
'<p>lorem</p><p>ipsum</p><br><br>',
|
||||||
|
);
|
||||||
expect(output).toEqual('lorem\n\nipsum\n<br>');
|
expect(output).toEqual('lorem\n\nipsum\n<br>');
|
||||||
});
|
});
|
||||||
});
|
});
|
24
app/javascript/mastodon/utils/__tests__/numbers.ts
Normal file
24
app/javascript/mastodon/utils/__tests__/numbers.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { DECIMAL_UNITS, toShortNumber } from '../numbers';
|
||||||
|
|
||||||
|
interface TableRow {
|
||||||
|
input: number;
|
||||||
|
base: number;
|
||||||
|
unit: number;
|
||||||
|
digits: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each`
|
||||||
|
input | base | unit | digits
|
||||||
|
${10_000_000} | ${10} | ${DECIMAL_UNITS.MILLION} | ${0}
|
||||||
|
${2_789_123} | ${2.789123} | ${DECIMAL_UNITS.MILLION} | ${1}
|
||||||
|
${12_345_789} | ${12.345789} | ${DECIMAL_UNITS.MILLION} | ${0}
|
||||||
|
${10_000_000_000} | ${10} | ${DECIMAL_UNITS.BILLION} | ${0}
|
||||||
|
${12} | ${12} | ${DECIMAL_UNITS.ONE} | ${0}
|
||||||
|
${123} | ${123} | ${DECIMAL_UNITS.ONE} | ${0}
|
||||||
|
${1234} | ${1.234} | ${DECIMAL_UNITS.THOUSAND} | ${1}
|
||||||
|
${6666} | ${6.666} | ${DECIMAL_UNITS.THOUSAND} | ${1}
|
||||||
|
`('toShortNumber', ({ input, base, unit, digits }: TableRow) => {
|
||||||
|
test(`correctly formats ${input}`, () => {
|
||||||
|
expect(toShortNumber(input)).toEqual([base, unit, digits]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -112,9 +112,11 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus,
|
&:active,
|
||||||
&:active {
|
&:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
border-width: 1px !important;
|
||||||
|
border-color: $ui-button-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-search-cancel-button {
|
&::-webkit-search-cancel-button {
|
||||||
|
|
|
@ -46,11 +46,11 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def earliest_status_id
|
def earliest_status_id
|
||||||
Mastodon::Snowflake.id_at(@start_at, with_random: false)
|
Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def latest_status_id
|
def latest_status_id
|
||||||
Mastodon::Snowflake.id_at(@end_at, with_random: false)
|
Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag
|
def tag
|
||||||
|
|
17
app/models/concerns/custom_filter_cache.rb
Normal file
17
app/models/concerns/custom_filter_cache.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module CustomFilterCache
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
after_commit :invalidate_cache!
|
||||||
|
before_destroy :prepare_cache_invalidation!
|
||||||
|
before_save :prepare_cache_invalidation!
|
||||||
|
|
||||||
|
delegate(
|
||||||
|
:invalidate_cache!,
|
||||||
|
:prepare_cache_invalidation!,
|
||||||
|
to: :custom_filter
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,16 +13,14 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomFilterKeyword < ApplicationRecord
|
class CustomFilterKeyword < ApplicationRecord
|
||||||
|
include CustomFilterCache
|
||||||
|
|
||||||
belongs_to :custom_filter
|
belongs_to :custom_filter
|
||||||
|
|
||||||
validates :keyword, presence: true
|
validates :keyword, presence: true
|
||||||
|
|
||||||
alias_attribute :phrase, :keyword
|
alias_attribute :phrase, :keyword
|
||||||
|
|
||||||
before_save :prepare_cache_invalidation!
|
|
||||||
before_destroy :prepare_cache_invalidation!
|
|
||||||
after_commit :invalidate_cache!
|
|
||||||
|
|
||||||
def to_regex
|
def to_regex
|
||||||
if whole_word?
|
if whole_word?
|
||||||
/(?mix:#{to_regex_sb}#{Regexp.escape(keyword)}#{to_regex_eb})/
|
/(?mix:#{to_regex_sb}#{Regexp.escape(keyword)}#{to_regex_eb})/
|
||||||
|
@ -40,12 +38,4 @@ class CustomFilterKeyword < ApplicationRecord
|
||||||
def to_regex_eb
|
def to_regex_eb
|
||||||
/[[:word:]]\z/.match?(keyword) ? '\b' : ''
|
/[[:word:]]\z/.match?(keyword) ? '\b' : ''
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_cache_invalidation!
|
|
||||||
custom_filter.prepare_cache_invalidation!
|
|
||||||
end
|
|
||||||
|
|
||||||
def invalidate_cache!
|
|
||||||
custom_filter.invalidate_cache!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,27 +12,17 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomFilterStatus < ApplicationRecord
|
class CustomFilterStatus < ApplicationRecord
|
||||||
|
include CustomFilterCache
|
||||||
|
|
||||||
belongs_to :custom_filter
|
belongs_to :custom_filter
|
||||||
belongs_to :status
|
belongs_to :status
|
||||||
|
|
||||||
validates :status, uniqueness: { scope: :custom_filter }
|
validates :status, uniqueness: { scope: :custom_filter }
|
||||||
validate :validate_status_access
|
validate :validate_status_access
|
||||||
|
|
||||||
before_save :prepare_cache_invalidation!
|
|
||||||
before_destroy :prepare_cache_invalidation!
|
|
||||||
after_commit :invalidate_cache!
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_status_access
|
def validate_status_access
|
||||||
errors.add(:status_id, :invalid) unless StatusPolicy.new(custom_filter.account, status).show?
|
errors.add(:status_id, :invalid) unless StatusPolicy.new(custom_filter.account, status).show?
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_cache_invalidation!
|
|
||||||
custom_filter.prepare_cache_invalidation!
|
|
||||||
end
|
|
||||||
|
|
||||||
def invalidate_cache!
|
|
||||||
custom_filter.invalidate_cache!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -95,6 +95,8 @@ class User < ApplicationRecord
|
||||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
|
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
|
||||||
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
||||||
|
|
||||||
|
validates :email, presence: true, email_address: true
|
||||||
|
|
||||||
validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
||||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||||
|
|
18
app/validators/email_address_validator.rb
Normal file
18
app/validators/email_address_validator.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# NOTE: I initially wrote this as `EmailValidator` but it ended up clashing
|
||||||
|
# with an indirect dependency of ours, `validate_email`, which, turns out,
|
||||||
|
# has the same approach as we do, but with an extra check disallowing
|
||||||
|
# single-label domains. Decided to not switch to `validate_email` because
|
||||||
|
# we do want to allow at least `localhost`.
|
||||||
|
|
||||||
|
class EmailAddressValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
value = value.strip
|
||||||
|
|
||||||
|
address = Mail::Address.new(value)
|
||||||
|
record.errors.add(attribute, :invalid) if address.address != value
|
||||||
|
rescue Mail::Field::FieldError
|
||||||
|
record.errors.add(attribute, :invalid)
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,7 @@
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
%tr
|
%tr
|
||||||
%td.email-feature-td
|
%td.email-feature-td
|
||||||
.email-desktop-flex{ class: ('email-dir-rtl' if defined?(text_first_on_desktop) && !text_first_on_desktop) }
|
.email-desktop-flex{ class: ('email-dir-rtl' if feature_iteration.index.odd?) }
|
||||||
/[if mso]
|
/[if mso]
|
||||||
<table border="0" cellpadding="0" cellspacing="0" align="center" style="width:100%;" role="presentation"><tr><td style="width:50%; vertical-align:top;">
|
<table border="0" cellpadding="0" cellspacing="0" align="center" style="width:100%;" role="presentation"><tr><td style="width:50%; vertical-align:top;">
|
||||||
.email-desktop-column
|
.email-desktop-column
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
%tr
|
%tr
|
||||||
%td.email-column-td
|
%td.email-column-td
|
||||||
- if defined?(feature)
|
- if defined?(feature)
|
||||||
%p{ class: ('email-desktop-text-right' if defined?(text_first_on_desktop) && text_first_on_desktop) }
|
%p{ class: ('email-desktop-text-right' if feature_iteration.index.even?) }
|
||||||
= image_tag frontend_asset_url("images/mailer-new/welcome/feature_#{feature}.png"), alt: '', width: 240, height: 230
|
= image_tag frontend_asset_url("images/mailer-new/welcome/feature_#{feature}.png"), alt: '', width: 240, height: 230
|
||||||
/[if mso]
|
/[if mso]
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
|
|
|
@ -68,7 +68,4 @@
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
%tr
|
%tr
|
||||||
%td.email-extra-td
|
%td.email-extra-td
|
||||||
= render 'application/mailer/feature', feature: 'control', text_first_on_desktop: true
|
= render partial: 'application/mailer/feature', collection: %w(control audience moderation creativity)
|
||||||
= render 'application/mailer/feature', feature: 'audience', text_first_on_desktop: false
|
|
||||||
= render 'application/mailer/feature', feature: 'moderation', text_first_on_desktop: true
|
|
||||||
= render 'application/mailer/feature', feature: 'creativity', text_first_on_desktop: false
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Admin::Metrics::Dimension::LanguagesDimension do
|
describe Admin::Metrics::Dimension::LanguagesDimension do
|
||||||
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
|
subject { described_class.new(start_at, end_at, limit, params) }
|
||||||
|
|
||||||
let(:start_at) { 2.days.ago }
|
let(:start_at) { 2.days.ago }
|
||||||
let(:end_at) { Time.now.utc }
|
let(:end_at) { Time.now.utc }
|
||||||
|
@ -11,8 +11,21 @@ describe Admin::Metrics::Dimension::LanguagesDimension do
|
||||||
let(:params) { ActionController::Parameters.new }
|
let(:params) { ActionController::Parameters.new }
|
||||||
|
|
||||||
describe '#data' do
|
describe '#data' do
|
||||||
it 'runs data query without error' do
|
let(:alice) { Fabricate(:user, locale: 'en', current_sign_in_at: 1.day.ago) }
|
||||||
expect { dimension.data }.to_not raise_error
|
let(:bob) { Fabricate(:user, locale: 'en', current_sign_in_at: 30.days.ago) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
alice.update(current_sign_in_at: 1.day.ago)
|
||||||
|
bob.update(current_sign_in_at: 30.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns locales with sign in counts' do
|
||||||
|
expect(subject.data.size)
|
||||||
|
.to eq(1)
|
||||||
|
expect(subject.data.map(&:symbolize_keys))
|
||||||
|
.to contain_exactly(
|
||||||
|
include(key: 'en', value: '1')
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -247,6 +247,12 @@ describe UserMailer do
|
||||||
describe '#welcome' do
|
describe '#welcome' do
|
||||||
let(:mail) { described_class.welcome(receiver) }
|
let(:mail) { described_class.welcome(receiver) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# This is a bit hacky and low-level but this allows stubbing trending tags
|
||||||
|
tag_ids = Fabricate.times(5, :tag).pluck(:id)
|
||||||
|
allow(Trends.tags).to receive(:query).and_return(instance_double(Trends::Query, allowed: Tag.where(id: tag_ids)))
|
||||||
|
end
|
||||||
|
|
||||||
it 'renders welcome mail' do
|
it 'renders welcome mail' do
|
||||||
expect(mail)
|
expect(mail)
|
||||||
.to be_present
|
.to be_present
|
||||||
|
|
|
@ -38,6 +38,12 @@ RSpec.describe User do
|
||||||
user.save(validate: false)
|
user.save(validate: false)
|
||||||
expect(user.valid?).to be true
|
expect(user.valid?).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'is valid with a localhost e-mail address' do
|
||||||
|
user = Fabricate.build(:user, email: 'admin@localhost')
|
||||||
|
user.valid?
|
||||||
|
expect(user.valid?).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Normalizations' do
|
describe 'Normalizations' do
|
||||||
|
|
|
@ -137,6 +137,18 @@ RSpec.describe '/api/web/embed' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when sanitizing the fragment fails' do
|
||||||
|
let(:call_result) { { html: 'ok' } }
|
||||||
|
|
||||||
|
before { allow(Sanitize).to receive(:fragment).and_raise(ArgumentError) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when failing to fetch OEmbed' do
|
context 'when failing to fetch OEmbed' do
|
||||||
let(:call_result) { nil }
|
let(:call_result) { nil }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue