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.
main
Claire 4 years ago
commit d8fdbb054e

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

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

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

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save