Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/validators/status_length_validator.rb`: Upstream changes too close to glitch-soc MAX_CHARS changes, but not a real conflict. Applied upstream changes. - `package.json`: glitch-soc-only dependency textually too close to a dependency updated upstream, not a real conflict. Applied upstream changes.
This commit is contained in:
commit
d8fdbb054e
8
Gemfile
8
Gemfile
|
@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.7'
|
gem 'pghero', '~> 2.7'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.88', require: false
|
gem 'aws-sdk-s3', '~> 1.89', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
|
@ -48,7 +48,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
||||||
|
|
||||||
gem 'color_diff', '~> 0.1'
|
gem 'color_diff', '~> 0.1'
|
||||||
gem 'discard', '~> 1.2'
|
gem 'discard', '~> 1.2'
|
||||||
gem 'doorkeeper', '~> 5.4'
|
gem 'doorkeeper', '~> 5.5'
|
||||||
gem 'ed25519', '~> 1.2'
|
gem 'ed25519', '~> 1.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
|
@ -93,7 +93,7 @@ gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.2.1'
|
gem 'stoplight', '~> 2.2.1'
|
||||||
gem 'strong_migrations', '~> 0.7'
|
gem 'strong_migrations', '~> 0.7'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 1.14'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2021'
|
gem 'tzinfo-data', '~> 1.2021'
|
||||||
gem 'webpacker', '~> 5.2'
|
gem 'webpacker', '~> 5.2'
|
||||||
gem 'webpush'
|
gem 'webpush'
|
||||||
|
@ -126,7 +126,7 @@ group :test do
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.21', require: false
|
gem 'simplecov', '~> 0.21', require: false
|
||||||
gem 'webmock', '~> 3.11'
|
gem 'webmock', '~> 3.12'
|
||||||
gem 'parallel_tests', '~> 3.4'
|
gem 'parallel_tests', '~> 3.4'
|
||||||
gem 'rspec_junit_formatter', '~> 0.4'
|
gem 'rspec_junit_formatter', '~> 0.4'
|
||||||
end
|
end
|
||||||
|
|
33
Gemfile.lock
33
Gemfile.lock
|
@ -79,7 +79,7 @@ GEM
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.427.0)
|
aws-partitions (1.429.0)
|
||||||
aws-sdk-core (3.112.0)
|
aws-sdk-core (3.112.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
|
@ -88,7 +88,7 @@ GEM
|
||||||
aws-sdk-kms (1.42.0)
|
aws-sdk-kms (1.42.0)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.88.1)
|
aws-sdk-s3 (1.89.0)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
|
@ -109,7 +109,7 @@ GEM
|
||||||
brakeman (4.10.1)
|
brakeman (4.10.1)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.3)
|
bullet (6.1.4)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundler-audit (0.7.0.1)
|
bundler-audit (0.7.0.1)
|
||||||
|
@ -187,7 +187,7 @@ GEM
|
||||||
docile (1.3.4)
|
docile (1.3.4)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.4.0)
|
doorkeeper (5.5.0)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
|
@ -218,7 +218,7 @@ GEM
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
fastimage (2.2.2)
|
fastimage (2.2.3)
|
||||||
ffi (1.10.0)
|
ffi (1.10.0)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
|
@ -292,8 +292,8 @@ GEM
|
||||||
iso-639 (0.3.5)
|
iso-639 (0.3.5)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
json (2.3.1)
|
json (2.3.1)
|
||||||
json-canonicalization (0.2.0)
|
json-canonicalization (0.2.1)
|
||||||
json-ld (3.1.8)
|
json-ld (3.1.9)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 0.2)
|
json-canonicalization (~> 0.2)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
|
@ -353,7 +353,7 @@ GEM
|
||||||
mimemagic (0.3.5)
|
mimemagic (0.3.5)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.5.0)
|
mini_portile2 (2.5.0)
|
||||||
minitest (5.14.3)
|
minitest (5.14.4)
|
||||||
msgpack (1.4.2)
|
msgpack (1.4.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
|
@ -482,7 +482,7 @@ GEM
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.3)
|
rake (13.0.3)
|
||||||
rdf (3.1.10)
|
rdf (3.1.12)
|
||||||
hamster (~> 3.0)
|
hamster (~> 3.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.4.0)
|
||||||
|
@ -642,7 +642,8 @@ GEM
|
||||||
tty-screen (~> 0.8)
|
tty-screen (~> 0.8)
|
||||||
wisper (~> 2.0)
|
wisper (~> 2.0)
|
||||||
tty-screen (0.8.1)
|
tty-screen (0.8.1)
|
||||||
twitter-text (1.14.7)
|
twitter-text (3.1.0)
|
||||||
|
idn-ruby
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.9)
|
tzinfo (1.2.9)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
|
@ -652,7 +653,7 @@ GEM
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.7.7)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.7.0)
|
||||||
uniform_notifier (1.13.2)
|
uniform_notifier (1.14.1)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
webauthn (3.0.0.alpha1)
|
webauthn (3.0.0.alpha1)
|
||||||
|
@ -665,7 +666,7 @@ GEM
|
||||||
safety_net_attestation (~> 0.4.0)
|
safety_net_attestation (~> 0.4.0)
|
||||||
securecompare (~> 1.0)
|
securecompare (~> 1.0)
|
||||||
tpm-key_attestation (~> 0.9.0)
|
tpm-key_attestation (~> 0.9.0)
|
||||||
webmock (3.11.2)
|
webmock (3.12.0)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
@ -693,7 +694,7 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.88)
|
aws-sdk-s3 (~> 1.89)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
|
@ -718,7 +719,7 @@ DEPENDENCIES
|
||||||
devise-two-factor (~> 3.1)
|
devise-two-factor (~> 3.1)
|
||||||
devise_pam_authenticatable2 (~> 9.2)
|
devise_pam_authenticatable2 (~> 9.2)
|
||||||
discard (~> 1.2)
|
discard (~> 1.2)
|
||||||
doorkeeper (~> 5.4)
|
doorkeeper (~> 5.5)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
fabrication (~> 2.21)
|
fabrication (~> 2.21)
|
||||||
|
@ -812,10 +813,10 @@ DEPENDENCIES
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 1.1)
|
thor (~> 1.1)
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 3.1.0)
|
||||||
tzinfo-data (~> 1.2021)
|
tzinfo-data (~> 1.2021)
|
||||||
webauthn (~> 3.0.0.alpha1)
|
webauthn (~> 3.0.0.alpha1)
|
||||||
webmock (~> 3.11)
|
webmock (~> 3.12)
|
||||||
webpacker (~> 5.2)
|
webpacker (~> 5.2)
|
||||||
webpush
|
webpush
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
|
@ -27,6 +27,8 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
|
|
||||||
self.response_body = Oj.dump(response.body)
|
self.response_body = Oj.dump(response.body)
|
||||||
self.status = response.status
|
self.status = response.status
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Emails::ConfirmationsController < Api::BaseController
|
||||||
|
before_action :doorkeeper_authorize!
|
||||||
|
before_action :require_user_owned_by_application!
|
||||||
|
|
||||||
|
def create
|
||||||
|
current_user.resend_confirmation_instructions if current_user.unconfirmed_email.present?
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_user_owned_by_application!
|
||||||
|
render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id
|
||||||
|
end
|
||||||
|
end
|
|
@ -133,6 +133,7 @@ module SignatureVerification
|
||||||
|
|
||||||
def verify_body_digest!
|
def verify_body_digest!
|
||||||
return unless signed_headers.include?('digest')
|
return unless signed_headers.include?('digest')
|
||||||
|
raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
|
||||||
|
|
||||||
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
||||||
sha256 = digests.assoc('sha-256')
|
sha256 = digests.assoc('sha-256')
|
||||||
|
|
|
@ -2,10 +2,35 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
|
||||||
|
class SilentErrorBoundary extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidCatch () {
|
||||||
|
this.setState({ error: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
if (this.state.error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to render counter of how much people are talking about hashtag
|
* Used to render counter of how much people are talking about hashtag
|
||||||
*
|
*
|
||||||
|
@ -51,17 +76,19 @@ const Hashtag = ({ hashtag }) => (
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
<div className='trends__item__sparkline'>
|
||||||
<Sparklines
|
<SilentErrorBoundary>
|
||||||
width={50}
|
<Sparklines
|
||||||
height={28}
|
width={50}
|
||||||
data={hashtag
|
height={28}
|
||||||
.get('history')
|
data={hashtag
|
||||||
.reverse()
|
.get('history')
|
||||||
.map((day) => day.get('uses'))
|
.reverse()
|
||||||
.toArray()}
|
.map((day) => day.get('uses'))
|
||||||
>
|
.toArray()}
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
>
|
||||||
</Sparklines>
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
|
</Sparklines>
|
||||||
|
</SilentErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { urlRegex } from './url_regex';
|
import { urlRegex } from './url_regex';
|
||||||
|
|
||||||
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
|
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
|
||||||
|
|
||||||
export function countableText(inputText) {
|
export function countableText(inputText) {
|
||||||
return inputText
|
return inputText
|
||||||
|
|
|
@ -1,196 +1,30 @@
|
||||||
const regexen = {};
|
import regexSupplant from 'twitter-text/dist/lib/regexSupplant';
|
||||||
|
import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars';
|
||||||
|
import validDomain from 'twitter-text/dist/regexp/validDomain';
|
||||||
|
import validPortNumber from 'twitter-text/dist/regexp/validPortNumber';
|
||||||
|
import validUrlPath from 'twitter-text/dist/regexp/validUrlPath';
|
||||||
|
import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars';
|
||||||
|
import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars';
|
||||||
|
|
||||||
const regexSupplant = function(regex, flags) {
|
// The difference with twitter-text's extractURL is that the protocol isn't
|
||||||
flags = flags || '';
|
// optional.
|
||||||
if (typeof regex !== 'string') {
|
|
||||||
if (regex.global && flags.indexOf('g') < 0) {
|
|
||||||
flags += 'g';
|
|
||||||
}
|
|
||||||
if (regex.ignoreCase && flags.indexOf('i') < 0) {
|
|
||||||
flags += 'i';
|
|
||||||
}
|
|
||||||
if (regex.multiline && flags.indexOf('m') < 0) {
|
|
||||||
flags += 'm';
|
|
||||||
}
|
|
||||||
|
|
||||||
regex = regex.source;
|
export const urlRegex = regexSupplant(
|
||||||
}
|
'(' + // $1 URL
|
||||||
return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
|
'(#{validUrlPrecedingChars})' + // $2
|
||||||
var newRegex = regexen[name] || '';
|
'(https?:\\/\\/)' + // $3 Protocol
|
||||||
if (typeof newRegex !== 'string') {
|
'(#{validDomain})' + // $4 Domain(s)
|
||||||
newRegex = newRegex.source;
|
'(?::(#{validPortNumber}))?' + // $5 Port number (optional)
|
||||||
}
|
'(\\/#{validUrlPath}*)?' + // $6 URL Path
|
||||||
return newRegex;
|
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String
|
||||||
}), flags);
|
')',
|
||||||
};
|
{
|
||||||
|
validUrlPrecedingChars,
|
||||||
const stringSupplant = function(str, values) {
|
validDomain,
|
||||||
return str.replace(/#\{(\w+)\}/g, function(match, name) {
|
validPortNumber,
|
||||||
return values[name] || '';
|
validUrlPath,
|
||||||
});
|
validUrlQueryChars,
|
||||||
};
|
validUrlQueryEndingChars,
|
||||||
|
},
|
||||||
export const urlRegex = (function() {
|
'gi',
|
||||||
regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
|
);
|
||||||
regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
|
|
||||||
regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
|
|
||||||
regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/);
|
|
||||||
regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen);
|
|
||||||
regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
|
|
||||||
regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
|
|
||||||
regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
|
|
||||||
regexen.validGTLD = regexSupplant(RegExp(
|
|
||||||
'(?:(?:' +
|
|
||||||
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
|
|
||||||
'政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
|
|
||||||
'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
|
|
||||||
'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
|
|
||||||
'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
|
|
||||||
'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
|
|
||||||
'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
|
|
||||||
'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
|
|
||||||
'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
|
|
||||||
'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
|
|
||||||
'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
|
|
||||||
'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
|
|
||||||
'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
|
|
||||||
'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
|
|
||||||
'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
|
|
||||||
'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
|
|
||||||
'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
|
|
||||||
'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
|
|
||||||
'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
|
|
||||||
'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
|
|
||||||
'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
|
|
||||||
'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
|
|
||||||
'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
|
|
||||||
'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
|
|
||||||
'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
|
|
||||||
'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
|
|
||||||
'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
|
|
||||||
'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
|
|
||||||
'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
|
|
||||||
'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
|
|
||||||
'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
|
|
||||||
'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
|
|
||||||
'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
|
|
||||||
'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
|
|
||||||
'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
|
|
||||||
'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
|
|
||||||
'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
|
|
||||||
'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
|
|
||||||
'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
|
|
||||||
'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
|
|
||||||
'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
|
|
||||||
'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
|
|
||||||
'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
|
|
||||||
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
|
|
||||||
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
|
|
||||||
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
|
|
||||||
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
|
|
||||||
'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
|
|
||||||
'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
|
|
||||||
'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
|
|
||||||
'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
|
|
||||||
'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
|
|
||||||
'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
|
|
||||||
'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
|
|
||||||
'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
|
|
||||||
'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
|
|
||||||
'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
|
|
||||||
'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
|
|
||||||
'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
|
|
||||||
'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
|
|
||||||
'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
|
|
||||||
'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
|
|
||||||
'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
|
|
||||||
'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
|
|
||||||
'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
|
|
||||||
'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
|
|
||||||
'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
|
|
||||||
'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
|
|
||||||
'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
|
|
||||||
'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
|
|
||||||
'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
|
|
||||||
'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
|
|
||||||
'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
|
|
||||||
'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
|
|
||||||
'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
|
|
||||||
'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
|
|
||||||
'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
|
|
||||||
'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
|
|
||||||
'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
|
|
||||||
'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
|
|
||||||
'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
|
|
||||||
'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
|
|
||||||
'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
|
|
||||||
'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
|
|
||||||
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
|
|
||||||
'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
|
|
||||||
'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
|
|
||||||
'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
|
|
||||||
')(?=[^0-9a-zA-Z@]|$))'));
|
|
||||||
regexen.validCCTLD = regexSupplant(RegExp(
|
|
||||||
'(?:(?:' +
|
|
||||||
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
|
|
||||||
'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
|
|
||||||
'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
|
|
||||||
'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
|
|
||||||
'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
|
|
||||||
're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
|
|
||||||
'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
|
|
||||||
'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
|
|
||||||
'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
|
|
||||||
'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
|
|
||||||
'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
|
|
||||||
')(?=[^0-9a-zA-Z@]|$))'));
|
|
||||||
regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
|
|
||||||
regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
|
|
||||||
regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
|
|
||||||
regexen.validPortNumber = /[0-9]+/;
|
|
||||||
regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/;
|
|
||||||
regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i);
|
|
||||||
// Allow URL paths to contain up to two nested levels of balanced parens
|
|
||||||
// 1. Used in Wikipedia URLs like /Primer_(film)
|
|
||||||
// 2. Used in IIS sessions like /S(dfd346)/
|
|
||||||
// 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
|
|
||||||
regexen.validUrlBalancedParens = regexSupplant(
|
|
||||||
'\\(' +
|
|
||||||
'(?:' +
|
|
||||||
'#{validGeneralUrlPathChars}+' +
|
|
||||||
'|' +
|
|
||||||
// allow one nested level of balanced parentheses
|
|
||||||
'(?:' +
|
|
||||||
'#{validGeneralUrlPathChars}*' +
|
|
||||||
'\\(' +
|
|
||||||
'#{validGeneralUrlPathChars}+' +
|
|
||||||
'\\)' +
|
|
||||||
'#{validGeneralUrlPathChars}*' +
|
|
||||||
')' +
|
|
||||||
')' +
|
|
||||||
'\\)',
|
|
||||||
'i');
|
|
||||||
// Valid end-of-path characters (so /foo. does not gobble the period).
|
|
||||||
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
|
|
||||||
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
|
|
||||||
// Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
|
|
||||||
regexen.validUrlPath = regexSupplant('(?:' +
|
|
||||||
'(?:' +
|
|
||||||
'#{validGeneralUrlPathChars}*' +
|
|
||||||
'(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
|
|
||||||
'#{validUrlPathEndingChars}'+
|
|
||||||
')|(?:@#{validGeneralUrlPathChars}+\/)'+
|
|
||||||
')', 'i');
|
|
||||||
regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
|
|
||||||
regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
|
|
||||||
regexen.validUrl = regexSupplant(
|
|
||||||
'(' + // $1 URL
|
|
||||||
'(https?:\\/\\/)' + // $2 Protocol
|
|
||||||
'(#{validDomain})' + // $3 Domain(s)
|
|
||||||
'(?::(#{validPortNumber}))?' + // $4 Port number (optional)
|
|
||||||
'(\\/#{validUrlPath}*)?' + // $5 URL Path
|
|
||||||
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String
|
|
||||||
')',
|
|
||||||
'gi');
|
|
||||||
return regexen.validUrl;
|
|
||||||
}());
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ const emojiFilenames = (emojis) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emoji requiring extra borders depending on theme
|
// Emoji requiring extra borders depending on theme
|
||||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']);
|
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲']);
|
||||||
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
||||||
|
|
||||||
const emojiFilename = (filename) => {
|
const emojiFilename = (filename) => {
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Extractor
|
module Extractor
|
||||||
extend Twitter::Extractor
|
extend Twitter::TwitterText::Extractor
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
# :yields: username, list_slug, start, end
|
# :yields: username, list_slug, start, end
|
||||||
def extract_mentions_or_lists_with_indices(text)
|
def extract_mentions_or_lists_with_indices(text)
|
||||||
return [] unless Twitter::Regex[:at_signs].match?(text)
|
return [] unless Twitter::TwitterText::Regex[:at_signs].match?(text)
|
||||||
|
|
||||||
possible_entries = []
|
possible_entries = []
|
||||||
|
|
||||||
text.to_s.scan(Account::MENTION_RE) do |screen_name, _|
|
text.to_s.scan(Account::MENTION_RE) do |screen_name, _|
|
||||||
match_data = $LAST_MATCH_INFO
|
match_data = $LAST_MATCH_INFO
|
||||||
after = $'
|
after = $'
|
||||||
unless Twitter::Regex[:end_mention_match].match?(after)
|
unless Twitter::TwitterText::Regex[:end_mention_match].match?(after)
|
||||||
start_position = match_data.char_begin(1) - 1
|
start_position = match_data.char_begin(1) - 1
|
||||||
end_position = match_data.char_end(1)
|
end_position = match_data.char_end(1)
|
||||||
possible_entries << {
|
possible_entries << {
|
||||||
|
@ -44,7 +44,7 @@ module Extractor
|
||||||
if %r{\A://}.match?(after)
|
if %r{\A://}.match?(after)
|
||||||
hash_text.match(/(.+)(https?\Z)/) do |matched|
|
hash_text.match(/(.+)(https?\Z)/) do |matched|
|
||||||
hash_text = matched[1]
|
hash_text = matched[1]
|
||||||
end_position -= matched[2].char_length
|
end_position -= matched[2].codepoint_length
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -348,7 +348,7 @@ class Formatter
|
||||||
|
|
||||||
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
||||||
|
|
||||||
Twitter::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
|
Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
|
||||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||||
encode(entity[:url])
|
encode(entity[:url])
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ValidationErrorFormatter
|
||||||
|
def initialize(error, aliases = {})
|
||||||
|
@error = error
|
||||||
|
@aliases = aliases
|
||||||
|
end
|
||||||
|
|
||||||
|
def as_json
|
||||||
|
{ error: @error.to_s, details: details }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def details
|
||||||
|
h = {}
|
||||||
|
|
||||||
|
errors.details.each_pair do |attribute_name, attribute_errors|
|
||||||
|
messages = errors.messages[attribute_name]
|
||||||
|
|
||||||
|
h[@aliases[attribute_name] || attribute_name] = attribute_errors.map.with_index do |error, index|
|
||||||
|
{ error: 'ERR_' + error[:error].to_s.upcase, description: messages[index] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
h
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors
|
||||||
|
@errors ||= @error.record.errors
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
class FetchLinkCardService < BaseService
|
class FetchLinkCardService < BaseService
|
||||||
URL_PATTERN = %r{
|
URL_PATTERN = %r{
|
||||||
( # $1 URL
|
( # $1 URL
|
||||||
(https?:\/\/) # $2 Protocol (required)
|
(https?:\/\/) # $2 Protocol (required)
|
||||||
(#{Twitter::Regex[:valid_domain]}) # $3 Domain(s)
|
(#{Twitter::TwitterText::Regex[:valid_domain]}) # $3 Domain(s)
|
||||||
(?::(#{Twitter::Regex[:valid_port_number]}))? # $4 Port number (optional)
|
(?::(#{Twitter::TwitterText::Regex[:valid_port_number]}))? # $4 Port number (optional)
|
||||||
(/#{Twitter::Regex[:valid_url_path]}*)? # $5 URL Path and anchor
|
(/#{Twitter::TwitterText::Regex[:valid_url_path]}*)? # $5 URL Path and anchor
|
||||||
(\?#{Twitter::Regex[:valid_url_query_chars]}*#{Twitter::Regex[:valid_url_query_ending_chars]})? # $6 Query String
|
(\?#{Twitter::TwitterText::Regex[:valid_url_query_chars]}*#{Twitter::TwitterText::Regex[:valid_url_query_ending_chars]})? # $6 Query String
|
||||||
)
|
)
|
||||||
}iox
|
}iox
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
class FollowService < BaseService
|
class FollowService < BaseService
|
||||||
include Redisable
|
include Redisable
|
||||||
include Payloadable
|
include Payloadable
|
||||||
|
include DomainControlHelper
|
||||||
|
|
||||||
# Follow a remote user, notify remote user about the follow
|
# Follow a remote user, notify remote user about the follow
|
||||||
# @param [Account] source_account From which to follow
|
# @param [Account] source_account From which to follow
|
||||||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
# @param [Account] target_account Account to follow
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
|
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
|
||||||
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
|
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
|
||||||
|
@ -15,7 +16,7 @@ class FollowService < BaseService
|
||||||
# @option [Boolean] :with_rate_limit
|
# @option [Boolean] :with_rate_limit
|
||||||
def call(source_account, target_account, options = {})
|
def call(source_account, target_account, options = {})
|
||||||
@source_account = source_account
|
@source_account = source_account
|
||||||
@target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
@target_account = target_account
|
||||||
@options = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)
|
@options = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if following_not_possible?
|
raise ActiveRecord::RecordNotFound if following_not_possible?
|
||||||
|
@ -43,7 +44,7 @@ class FollowService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def following_not_allowed?
|
def following_not_allowed?
|
||||||
@target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain)
|
domain_not_allowed?(@target_account.domain) || @target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_follow_options!
|
def change_follow_options!
|
||||||
|
|
|
@ -10,7 +10,7 @@ class ResolveAccountService < BaseService
|
||||||
# @param [String, Account] uri URI in the username@domain format or account record
|
# @param [String, Account] uri URI in the username@domain format or account record
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option options [Boolean] :redirected Do not follow further Webfinger redirects
|
# @option options [Boolean] :redirected Do not follow further Webfinger redirects
|
||||||
# @option options [Boolean] :skip_webfinger Do not attempt to refresh account data
|
# @option options [Boolean] :skip_webfinger Do not attempt any webfinger query or refreshing account data
|
||||||
# @return [Account]
|
# @return [Account]
|
||||||
def call(uri, options = {})
|
def call(uri, options = {})
|
||||||
return if uri.blank?
|
return if uri.blank?
|
||||||
|
@ -120,8 +120,9 @@ class ResolveAccountService < BaseService
|
||||||
|
|
||||||
def webfinger_update_due?
|
def webfinger_update_due?
|
||||||
return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
|
return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
|
||||||
|
return false if @options[:skip_webfinger]
|
||||||
|
|
||||||
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
@account.nil? || (@account.ostatus? && @account.possibly_stale?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def activitypub_ready?
|
def activitypub_ready?
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
class BlacklistedEmailValidator < ActiveModel::Validator
|
class BlacklistedEmailValidator < ActiveModel::Validator
|
||||||
def validate(user)
|
def validate(user)
|
||||||
return if user.valid_invitation?
|
return if user.valid_invitation? || user.email.blank?
|
||||||
|
|
||||||
@email = user.email
|
@email = user.email
|
||||||
|
|
||||||
user.errors.add(:email, I18n.t('users.blocked_email_provider')) if blocked_email?
|
user.errors.add(:email, :blocked) if blocked_email?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -4,16 +4,19 @@ require 'resolv'
|
||||||
|
|
||||||
class EmailMxValidator < ActiveModel::Validator
|
class EmailMxValidator < ActiveModel::Validator
|
||||||
def validate(user)
|
def validate(user)
|
||||||
|
return if user.email.blank?
|
||||||
|
|
||||||
domain = get_domain(user.email)
|
domain = get_domain(user.email)
|
||||||
|
|
||||||
if domain.nil?
|
if domain.blank?
|
||||||
user.errors.add(:email, I18n.t('users.invalid_email'))
|
user.errors.add(:email, :invalid)
|
||||||
else
|
else
|
||||||
ips, hostnames = resolve_mx(domain)
|
ips, hostnames = resolve_mx(domain)
|
||||||
|
|
||||||
if ips.empty?
|
if ips.empty?
|
||||||
user.errors.add(:email, I18n.t('users.invalid_email_mx'))
|
user.errors.add(:email, :unreachable)
|
||||||
elsif on_blacklist?(hostnames + ips)
|
elsif on_blacklist?(hostnames + ips)
|
||||||
user.errors.add(:email, I18n.t('users.blocked_email_provider'))
|
user.errors.add(:email, :blocked)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class NoteLengthValidator < ActiveModel::EachValidator
|
class NoteLengthValidator < ActiveModel::EachValidator
|
||||||
def validate_each(record, attribute, value)
|
def validate_each(record, attribute, value)
|
||||||
record.errors.add(attribute, I18n.t('statuses.over_character_limit', max: options[:maximum])) if too_long?(value)
|
record.errors.add(attribute, :too_long, message: I18n.t('statuses.over_character_limit', max: options[:maximum]), count: options[:maximum]) if too_long?(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
class StatusLengthValidator < ActiveModel::Validator
|
class StatusLengthValidator < ActiveModel::Validator
|
||||||
MAX_CHARS = (ENV['MAX_TOOT_CHARS'] || 500).to_i
|
MAX_CHARS = (ENV['MAX_TOOT_CHARS'] || 500).to_i
|
||||||
|
URL_PATTERN = %r{
|
||||||
|
(?:
|
||||||
|
(#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]})
|
||||||
|
(#{FetchLinkCardService::URL_PATTERN})
|
||||||
|
)
|
||||||
|
}iox
|
||||||
|
URL_PLACEHOLDER = "\1#{'x' * 23}"
|
||||||
|
|
||||||
def validate(status)
|
def validate(status)
|
||||||
return unless status.local? && !status.reblog?
|
return unless status.local? && !status.reblog?
|
||||||
|
@ -28,7 +35,7 @@ class StatusLengthValidator < ActiveModel::Validator
|
||||||
return '' if @status.text.nil?
|
return '' if @status.text.nil?
|
||||||
|
|
||||||
@status.text.dup.tap do |new_text|
|
@status.text.dup.tap do |new_text|
|
||||||
new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23)
|
new_text.gsub!(URL_PATTERN, URL_PLACEHOLDER)
|
||||||
new_text.gsub!(Account::MENTION_RE, '@\2')
|
new_text.gsub!(Account::MENTION_RE, '@\2')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
class UniqueUsernameValidator < ActiveModel::Validator
|
class UniqueUsernameValidator < ActiveModel::Validator
|
||||||
def validate(account)
|
def validate(account)
|
||||||
return if account.username.nil?
|
return if account.username.blank?
|
||||||
|
|
||||||
normalized_username = account.username.downcase
|
normalized_username = account.username.downcase
|
||||||
normalized_domain = account.domain&.downcase
|
normalized_domain = account.domain&.downcase
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
class UnreservedUsernameValidator < ActiveModel::Validator
|
class UnreservedUsernameValidator < ActiveModel::Validator
|
||||||
def validate(account)
|
def validate(account)
|
||||||
@username = account.username
|
@username = account.username
|
||||||
return if @username.nil?
|
|
||||||
|
|
||||||
account.errors.add(:username, I18n.t('accounts.reserved_username')) if reserved_username?
|
return if @username.blank?
|
||||||
|
|
||||||
|
account.errors.add(:username, :reserved) if reserved_username?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
.column-3
|
.column-3
|
||||||
= render 'application/flashes'
|
= render 'application/flashes'
|
||||||
|
|
||||||
- if @contents.blank? && (!display_blocks? || @blocks&.empty?)
|
- if @contents.blank? && @rules.empty? && (!display_blocks? || @blocks&.empty?)
|
||||||
= nothing_here
|
= nothing_here
|
||||||
- else
|
- else
|
||||||
.box-widget
|
.box-widget
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
= f.input :report_id, as: :hidden
|
= f.input :report_id, as: :hidden
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :type, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { I18n.t("simple_form.labels.admin_account_action.types.#{type}")}, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.acct)
|
= f.input :type, as: :radio_buttons, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { safe_join([I18n.t("simple_form.labels.admin_account_action.types.#{type}"), content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint')])}, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.acct)
|
||||||
|
|
||||||
- if @account.local?
|
- if @account.local?
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
|
@ -94,11 +94,15 @@ class Rack::Attack
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req|
|
throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req|
|
||||||
req.remote_ip if req.post? && req.path == '/auth/confirmation'
|
req.remote_ip if req.post? && %w(/auth/confirmation /api/v1/emails/confirmations).include?(req.path)
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req|
|
throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req|
|
||||||
req.params.dig('user', 'email').presence if req.post? && req.path == '/auth/password'
|
if req.post? && req.path == '/auth/password'
|
||||||
|
req.params.dig('user', 'email').presence
|
||||||
|
elsif req.post? && req.path == '/api/v1/emails/confirmations'
|
||||||
|
req.authenticated_user_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req|
|
throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req|
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
module Twitter
|
module Twitter::TwitterText
|
||||||
|
class Configuration
|
||||||
|
def emoji_parsing_enabled
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class Regex
|
class Regex
|
||||||
REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou
|
REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou
|
||||||
REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
|
REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
|
||||||
|
@ -79,7 +85,7 @@ module Twitter
|
||||||
return [] unless text && text.index(":")
|
return [] unless text && text.index(":")
|
||||||
urls = []
|
urls = []
|
||||||
|
|
||||||
text.to_s.scan(Twitter::Regex[:valid_extended_uri]) do
|
text.to_s.scan(Twitter::TwitterText::Regex[:valid_extended_uri]) do
|
||||||
valid_uri_match_data = $~
|
valid_uri_match_data = $~
|
||||||
|
|
||||||
start_position = valid_uri_match_data.char_begin(3)
|
start_position = valid_uri_match_data.char_begin(3)
|
||||||
|
|
|
@ -5,13 +5,28 @@ en:
|
||||||
poll:
|
poll:
|
||||||
expires_at: Deadline
|
expires_at: Deadline
|
||||||
options: Choices
|
options: Choices
|
||||||
|
user:
|
||||||
|
agreement: Service agreement
|
||||||
|
email: E-mail address
|
||||||
|
locale: Locale
|
||||||
|
password: Password
|
||||||
|
user/account:
|
||||||
|
username: Username
|
||||||
|
user/invite_request:
|
||||||
|
text: Reason
|
||||||
errors:
|
errors:
|
||||||
models:
|
models:
|
||||||
account:
|
account:
|
||||||
attributes:
|
attributes:
|
||||||
username:
|
username:
|
||||||
invalid: only letters, numbers and underscores
|
invalid: must contain only letters, numbers and underscores
|
||||||
|
reserved: is reserved
|
||||||
status:
|
status:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
taken: of status already exists
|
taken: of status already exists
|
||||||
|
user:
|
||||||
|
attributes:
|
||||||
|
email:
|
||||||
|
blocked: uses a disallowed e-mail provider
|
||||||
|
unreachable: does not seem to exist
|
||||||
|
|
|
@ -80,7 +80,6 @@ en:
|
||||||
other: Toots
|
other: Toots
|
||||||
posts_tab_heading: Toots
|
posts_tab_heading: Toots
|
||||||
posts_with_replies: Toots and replies
|
posts_with_replies: Toots and replies
|
||||||
reserved_username: The username is reserved
|
|
||||||
roles:
|
roles:
|
||||||
admin: Admin
|
admin: Admin
|
||||||
bot: Bot
|
bot: Bot
|
||||||
|
@ -1410,11 +1409,8 @@ en:
|
||||||
tips: Tips
|
tips: Tips
|
||||||
title: Welcome aboard, %{name}!
|
title: Welcome aboard, %{name}!
|
||||||
users:
|
users:
|
||||||
blocked_email_provider: This e-mail provider isn't allowed
|
|
||||||
follow_limit_reached: You cannot follow more than %{limit} people
|
follow_limit_reached: You cannot follow more than %{limit} people
|
||||||
generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance
|
generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance
|
||||||
invalid_email: The e-mail address is invalid
|
|
||||||
invalid_email_mx: The e-mail address does not seem to exist
|
|
||||||
invalid_otp_token: Invalid two-factor code
|
invalid_otp_token: Invalid two-factor code
|
||||||
invalid_sign_in_token: Invalid security code
|
invalid_sign_in_token: Invalid security code
|
||||||
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
||||||
|
|
|
@ -14,6 +14,12 @@ en:
|
||||||
send_email_notification: The user will receive an explanation of what happened with their account
|
send_email_notification: The user will receive an explanation of what happened with their account
|
||||||
text_html: Optional. You can use toot syntax. You can <a href="%{path}">add warning presets</a> to save time
|
text_html: Optional. You can use toot syntax. You can <a href="%{path}">add warning presets</a> to save time
|
||||||
type_html: Choose what to do with <strong>%{acct}</strong>
|
type_html: Choose what to do with <strong>%{acct}</strong>
|
||||||
|
types:
|
||||||
|
disable: Prevent the user from using their account, but do not delete or hide their contents.
|
||||||
|
none: Use this to send a warning to the user, without triggering any other action.
|
||||||
|
sensitive: Force all this user's media attachments to be flagged as sensitive.
|
||||||
|
silence: Prevent the user from being able to post with public visibility, hide their posts and notifications from people not following them.
|
||||||
|
suspend: Prevent any interaction from or to this account and delete its contents. Revertible within 30 days.
|
||||||
warning_preset_id: Optional. You can still add custom text to end of the preset
|
warning_preset_id: Optional. You can still add custom text to end of the preset
|
||||||
announcement:
|
announcement:
|
||||||
all_day: When checked, only the dates of the time range will be displayed
|
all_day: When checked, only the dates of the time range will be displayed
|
||||||
|
|
|
@ -406,6 +406,10 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :apps, only: [:create]
|
resources :apps, only: [:create]
|
||||||
|
|
||||||
|
namespace :emails do
|
||||||
|
resources :confirmations, only: [:create]
|
||||||
|
end
|
||||||
|
|
||||||
resource :instance, only: [:show] do
|
resource :instance, only: [:show] do
|
||||||
resources :peers, only: [:index], controller: 'instances/peers'
|
resources :peers, only: [:index], controller: 'instances/peers'
|
||||||
resource :activity, only: [:show], controller: 'instances/activity'
|
resource :activity, only: [:show], controller: 'instances/activity'
|
||||||
|
|
|
@ -91,7 +91,7 @@ namespace :emojis do
|
||||||
desc 'Generate emoji variants with white borders'
|
desc 'Generate emoji variants with white borders'
|
||||||
task :generate_borders do
|
task :generate_borders do
|
||||||
src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json')
|
src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json')
|
||||||
emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'
|
emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'
|
||||||
|
|
||||||
map = Oj.load(File.read(src))
|
map = Oj.load(File.read(src))
|
||||||
|
|
||||||
|
|
25
package.json
25
package.json
|
@ -60,19 +60,19 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.12.17",
|
"@babel/core": "^7.13.8",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-decorators": "^7.12.13",
|
"@babel/plugin-proposal-decorators": "^7.13.5",
|
||||||
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
||||||
"@babel/plugin-transform-runtime": "^7.12.17",
|
"@babel/plugin-transform-runtime": "^7.13.8",
|
||||||
"@babel/preset-env": "^7.12.17",
|
"@babel/preset-env": "^7.13.8",
|
||||||
"@babel/preset-react": "^7.12.13",
|
"@babel/preset-react": "^7.12.13",
|
||||||
"@babel/runtime": "^7.12.18",
|
"@babel/runtime": "^7.13.8",
|
||||||
"@clusterws/cws": "^3.0.0",
|
"@clusterws/cws": "^3.0.0",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^0.5.7",
|
"@github/webauthn-json": "^0.5.7",
|
||||||
"@rails/ujs": "^6.1.3",
|
"@rails/ujs": "^6.1.3",
|
||||||
"array-includes": "^3.1.2",
|
"array-includes": "^3.1.3",
|
||||||
"atrament": "0.2.4",
|
"atrament": "0.2.4",
|
||||||
"arrow-key-navigation": "^1.2.0",
|
"arrow-key-navigation": "^1.2.0",
|
||||||
"autoprefixer": "^9.8.6",
|
"autoprefixer": "^9.8.6",
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
"color-blend": "^3.0.1",
|
"color-blend": "^3.0.1",
|
||||||
"compression-webpack-plugin": "^6.1.1",
|
"compression-webpack-plugin": "^6.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^5.0.2",
|
"css-loader": "^5.1.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"detect-passive-events": "^2.0.3",
|
"detect-passive-events": "^2.0.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
@ -111,18 +111,18 @@
|
||||||
"intl-relativeformat": "^6.4.3",
|
"intl-relativeformat": "^6.4.3",
|
||||||
"is-nan": "^1.3.2",
|
"is-nan": "^1.3.2",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.21",
|
||||||
"mark-loader": "^0.1.6",
|
"mark-loader": "^0.1.6",
|
||||||
"marky": "^1.2.1",
|
"marky": "^1.2.1",
|
||||||
"mini-css-extract-plugin": "^1.3.8",
|
"mini-css-extract-plugin": "^1.3.9",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"npmlog": "^4.1.2",
|
"npmlog": "^4.1.2",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"object-fit-images": "^3.2.3",
|
"object-fit-images": "^3.2.3",
|
||||||
"object.values": "^1.1.2",
|
"object.values": "^1.1.3",
|
||||||
"offline-plugin": "^5.0.7",
|
"offline-plugin": "^5.0.7",
|
||||||
"path-complete-extname": "^1.0.0",
|
"path-complete-extname": "^1.0.0",
|
||||||
"pg": "^6.4.0",
|
"pg": "^8.5.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-object-fit-images": "^1.1.2",
|
"postcss-object-fit-images": "^1.1.2",
|
||||||
"promise.prototype.finally": "^3.1.2",
|
"promise.prototype.finally": "^3.1.2",
|
||||||
|
@ -165,6 +165,7 @@
|
||||||
"tesseract.js": "^2.1.1",
|
"tesseract.js": "^2.1.1",
|
||||||
"throng": "^4.0.0",
|
"throng": "^4.0.0",
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
|
"twitter-text": "3.1.0",
|
||||||
"uuid": "^8.3.1",
|
"uuid": "^8.3.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-assets-manifest": "^4.0.1",
|
"webpack-assets-manifest": "^4.0.1",
|
||||||
|
@ -178,7 +179,7 @@
|
||||||
"@testing-library/react": "^11.2.5",
|
"@testing-library/react": "^11.2.5",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"eslint": "^7.20.0",
|
"eslint": "^7.21.0",
|
||||||
"eslint-plugin-import": "~2.22.1",
|
"eslint-plugin-import": "~2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "~6.4.1",
|
"eslint-plugin-jsx-a11y": "~6.4.1",
|
||||||
"eslint-plugin-promise": "~4.3.1",
|
"eslint-plugin-promise": "~4.3.1",
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 40 40">
|
||||||
|
<g>
|
||||||
|
<path d="M11 36s-4 0-4-4V4s0-4 4-4h14s4 0 4 4v28s0 4-4 4H11z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M9 5h18v26H9z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#31373D" d="M11 36s-4 0-4-4V4s0-4 4-4h14s4 0 4 4v28s0 4-4 4H11z"/>
|
||||||
|
<path fill="#55ACEE" d="M9 5h18v26H9z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 443 B |
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 40 40">
|
||||||
|
<g>
|
||||||
|
<path d="M18 36s-4 0-4-4V4s0-4 4-4h14s4 0 4 4v28s0 4-4 4H18z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
<path d="M16 5h18v26H16zm-3 11s1 1 1 2-1 2-1 2l-5 5c-1 1-3 1-3-1v-3H2s-2 0-2-2v-2c0-2 2-2 2-2h3v-3c0-2 2-2 3-1l5 5z" stroke="white" stroke-linejoin="round" stroke-width="4px"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#31373D" d="M18 36s-4 0-4-4V4s0-4 4-4h14s4 0 4 4v28s0 4-4 4H18z"/>
|
||||||
|
<path fill="#55ACEE" d="M16 5h18v26H16zm-3 11s1 1 1 2-1 2-1 2l-5 5c-1 1-3 1-3-1v-3H2s-2 0-2-2v-2c0-2 2-2 2-2h3v-3c0-2 2-2 3-1l5 5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 629 B |
|
@ -8,7 +8,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
|
||||||
let(:follower) { Fabricate(:account, username: 'bob') }
|
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
FollowService.new.call(follower, user.account.acct)
|
FollowService.new.call(follower, user.account)
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
|
||||||
@mention_from_status = mentioning_status.mentions.first
|
@mention_from_status = mentioning_status.mentions.first
|
||||||
@favourite = FavouriteService.new.call(other.account, first_status)
|
@favourite = FavouriteService.new.call(other.account, first_status)
|
||||||
@second_favourite = FavouriteService.new.call(third.account, first_status)
|
@second_favourite = FavouriteService.new.call(third.account, first_status)
|
||||||
@follow = FollowService.new.call(other.account, 'alice')
|
@follow = FollowService.new.call(other.account, user.account)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'with no options' do
|
describe 'with no options' do
|
||||||
|
|
|
@ -69,7 +69,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows a login error' do
|
it 'shows a login error' do
|
||||||
expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: 'Email')
|
expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't log the user in" do
|
it "doesn't log the user in" do
|
||||||
|
@ -136,7 +136,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows a login error' do
|
it 'shows a login error' do
|
||||||
expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: 'Email')
|
expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't log the user in" do
|
it "doesn't log the user in" do
|
||||||
|
|
|
@ -99,12 +99,10 @@ describe AuthorizeInteractionsController do
|
||||||
|
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
||||||
allow(service).to receive(:call).with(target_account, skip_webfinger: true).and_return(target_account)
|
|
||||||
|
|
||||||
|
|
||||||
post :create, params: { acct: 'acct:user@hostname' }
|
post :create, params: { acct: 'acct:user@hostname' }
|
||||||
|
|
||||||
expect(service).to have_received(:call).with(target_account, skip_webfinger: true)
|
|
||||||
expect(account.following?(target_account)).to be true
|
expect(account.following?(target_account)).to be true
|
||||||
expect(response).to render_template(:success)
|
expect(response).to render_template(:success)
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,14 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'given a stand-alone URL with a newer TLD' do
|
||||||
|
let(:text) { 'http://example.gay' }
|
||||||
|
|
||||||
|
it 'matches the full URL' do
|
||||||
|
is_expected.to include 'href="http://example.gay"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'given a stand-alone IDN URL' do
|
context 'given a stand-alone IDN URL' do
|
||||||
let(:text) { 'https://nic.みんな/' }
|
let(:text) { 'https://nic.みんな/' }
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
|
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.call(sender, bob.acct)
|
subject.call(sender, bob)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a follow request with reblogs' do
|
it 'creates a follow request with reblogs' do
|
||||||
|
@ -22,7 +22,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
|
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.call(sender, bob.acct, reblogs: false)
|
subject.call(sender, bob, reblogs: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a follow request without reblogs' do
|
it 'creates a follow request without reblogs' do
|
||||||
|
@ -35,7 +35,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sender.touch(:silenced_at)
|
sender.touch(:silenced_at)
|
||||||
subject.call(sender, bob.acct)
|
subject.call(sender, bob)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a follow request with reblogs' do
|
it 'creates a follow request with reblogs' do
|
||||||
|
@ -48,7 +48,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
bob.mute!(sender)
|
bob.mute!(sender)
|
||||||
subject.call(sender, bob.acct)
|
subject.call(sender, bob)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a following relation with reblogs' do
|
it 'creates a following relation with reblogs' do
|
||||||
|
@ -61,7 +61,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.call(sender, bob.acct)
|
subject.call(sender, bob)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a following relation with reblogs' do
|
it 'creates a following relation with reblogs' do
|
||||||
|
@ -74,7 +74,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.call(sender, bob.acct, reblogs: false)
|
subject.call(sender, bob, reblogs: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a following relation without reblogs' do
|
it 'creates a following relation without reblogs' do
|
||||||
|
@ -88,7 +88,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sender.follow!(bob)
|
sender.follow!(bob)
|
||||||
subject.call(sender, bob.acct)
|
subject.call(sender, bob)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps a following relation' do
|
it 'keeps a following relation' do
|
||||||
|
@ -101,7 +101,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sender.follow!(bob, reblogs: true)
|
sender.follow!(bob, reblogs: true)
|
||||||
subject.call(sender, bob.acct, reblogs: false)
|
subject.call(sender, bob, reblogs: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'disables reblogs' do
|
it 'disables reblogs' do
|
||||||
|
@ -114,7 +114,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sender.follow!(bob, reblogs: false)
|
sender.follow!(bob, reblogs: false)
|
||||||
subject.call(sender, bob.acct, reblogs: true)
|
subject.call(sender, bob, reblogs: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'disables reblogs' do
|
it 'disables reblogs' do
|
||||||
|
@ -128,7 +128,7 @@ RSpec.describe FollowService, type: :service do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
|
stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
|
||||||
subject.call(sender, bob.acct)
|
subject.call(sender, bob)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates follow request' do
|
it 'creates follow request' do
|
||||||
|
|
|
@ -13,6 +13,47 @@ RSpec.describe ResolveAccountService, type: :service do
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410)
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'using skip_webfinger' do
|
||||||
|
context 'when account is known' do
|
||||||
|
let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') }
|
||||||
|
|
||||||
|
context 'when domain is banned' do
|
||||||
|
let!(:domain_block) { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) }
|
||||||
|
|
||||||
|
it 'does not return an account' do
|
||||||
|
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not make a webfinger query' do
|
||||||
|
subject.call('foo@ap.example.com', skip_webfinger: true)
|
||||||
|
expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when domain is not banned' do
|
||||||
|
it 'returns the expected account' do
|
||||||
|
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to eq remote_account
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not make a webfinger query' do
|
||||||
|
subject.call('foo@ap.example.com', skip_webfinger: true)
|
||||||
|
expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account is not known' do
|
||||||
|
it 'does not return an account' do
|
||||||
|
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not make a webfinger query' do
|
||||||
|
subject.call('foo@ap.example.com', skip_webfinger: true)
|
||||||
|
expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when there is an LRDD endpoint but no resolvable account' do
|
context 'when there is an LRDD endpoint but no resolvable account' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
|
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
|
||||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
||||||
let(:blocked_email) { true }
|
let(:blocked_email) { true }
|
||||||
|
|
||||||
it 'calls errors.add' do
|
it 'calls errors.add' do
|
||||||
expect(errors).to have_received(:add).with(:email, I18n.t('users.blocked_email_provider'))
|
expect(errors).to have_received(:add).with(:email, :blocked)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
||||||
let(:blocked_email) { false }
|
let(:blocked_email) { false }
|
||||||
|
|
||||||
it 'not calls errors.add' do
|
it 'not calls errors.add' do
|
||||||
expect(errors).not_to have_received(:add).with(:email, I18n.t('users.blocked_email_provider'))
|
expect(errors).not_to have_received(:add).with(:email, :blocked)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,6 +47,14 @@ describe StatusLengthValidator do
|
||||||
expect(status.errors).to_not have_received(:add)
|
expect(status.errors).to_not have_received(:add)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not count non-autolinkable URLs as 23 characters flat' do
|
||||||
|
text = ('a' * 476) + "http://#{'b' * 30}.com/example"
|
||||||
|
status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false)
|
||||||
|
|
||||||
|
subject.validate(status)
|
||||||
|
expect(status.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
it 'counts only the front part of remote usernames' do
|
it 'counts only the front part of remote usernames' do
|
||||||
username = '@alice'
|
username = '@alice'
|
||||||
chars = StatusLengthValidator::MAX_CHARS - 1 - username.length
|
chars = StatusLengthValidator::MAX_CHARS - 1 - username.length
|
||||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
|
||||||
let(:account) { double(username: username, errors: errors) }
|
let(:account) { double(username: username, errors: errors) }
|
||||||
let(:errors ) { double(add: nil) }
|
let(:errors ) { double(add: nil) }
|
||||||
|
|
||||||
context '@username.nil?' do
|
context '@username.blank?' do
|
||||||
let(:username) { nil }
|
let(:username) { nil }
|
||||||
|
|
||||||
it 'not calls errors.add' do
|
it 'not calls errors.add' do
|
||||||
|
@ -21,14 +21,14 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context '!@username.nil?' do
|
context '!@username.blank?' do
|
||||||
let(:username) { '' }
|
let(:username) { 'f' }
|
||||||
|
|
||||||
context 'reserved_username?' do
|
context 'reserved_username?' do
|
||||||
let(:reserved_username) { true }
|
let(:reserved_username) { true }
|
||||||
|
|
||||||
it 'calls erros.add' do
|
it 'calls erros.add' do
|
||||||
expect(errors).to have_received(:add).with(:username, I18n.t('accounts.reserved_username'))
|
expect(errors).to have_received(:add).with(:username, :reserved)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Reference in New Issue