1
0
mirror of https://github.com/funamitech/mastodon synced 2024-12-12 05:39:02 +09:00

Merge pull request #1504 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
This commit is contained in:
Claire 2021-03-02 16:33:06 +01:00 committed by GitHub
commit f4abf8e782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 983 additions and 948 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 'flavours/glitch/components/short_number'; import ShortNumber from 'flavours/glitch/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>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.みんな/' }

View File

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

View File

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

View File

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

View File

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

View File

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

1024
yarn.lock

File diff suppressed because it is too large Load Diff