Merge pull request #1504 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						f4abf8e782
					
				
							
								
								
									
										8
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								Gemfile
									
									
									
									
									
								
							| @ -17,7 +17,7 @@ gem 'makara', '~> 0.5' | |||||||
| gem 'pghero', '~> 2.7' | gem 'pghero', '~> 2.7' | ||||||
| gem 'dotenv-rails', '~> 2.7' | gem 'dotenv-rails', '~> 2.7' | ||||||
| 
 | 
 | ||||||
| gem 'aws-sdk-s3', '~> 1.88', require: false | gem 'aws-sdk-s3', '~> 1.89', require: false | ||||||
| gem 'fog-core', '<= 2.1.0' | gem 'fog-core', '<= 2.1.0' | ||||||
| gem 'fog-openstack', '~> 0.3', require: false | gem 'fog-openstack', '~> 0.3', require: false | ||||||
| gem 'paperclip', '~> 6.0' | gem 'paperclip', '~> 6.0' | ||||||
| @ -48,7 +48,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1' | |||||||
| 
 | 
 | ||||||
| gem 'color_diff', '~> 0.1' | gem 'color_diff', '~> 0.1' | ||||||
| gem 'discard', '~> 1.2' | gem 'discard', '~> 1.2' | ||||||
| gem 'doorkeeper', '~> 5.4' | gem 'doorkeeper', '~> 5.5' | ||||||
| gem 'ed25519', '~> 1.2' | gem 'ed25519', '~> 1.2' | ||||||
| gem 'fast_blank', '~> 1.0' | gem 'fast_blank', '~> 1.0' | ||||||
| gem 'fastimage' | gem 'fastimage' | ||||||
| @ -93,7 +93,7 @@ gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' | |||||||
| gem 'stoplight', '~> 2.2.1' | gem 'stoplight', '~> 2.2.1' | ||||||
| gem 'strong_migrations', '~> 0.7' | gem 'strong_migrations', '~> 0.7' | ||||||
| gem 'tty-prompt', '~> 0.23', require: false | gem 'tty-prompt', '~> 0.23', require: false | ||||||
| gem 'twitter-text', '~> 1.14' | gem 'twitter-text', '~> 3.1.0' | ||||||
| gem 'tzinfo-data', '~> 1.2021' | gem 'tzinfo-data', '~> 1.2021' | ||||||
| gem 'webpacker', '~> 5.2' | gem 'webpacker', '~> 5.2' | ||||||
| gem 'webpush' | gem 'webpush' | ||||||
| @ -126,7 +126,7 @@ group :test do | |||||||
|   gem 'rails-controller-testing', '~> 1.0' |   gem 'rails-controller-testing', '~> 1.0' | ||||||
|   gem 'rspec-sidekiq', '~> 3.1' |   gem 'rspec-sidekiq', '~> 3.1' | ||||||
|   gem 'simplecov', '~> 0.21', require: false |   gem 'simplecov', '~> 0.21', require: false | ||||||
|   gem 'webmock', '~> 3.11' |   gem 'webmock', '~> 3.12' | ||||||
|   gem 'parallel_tests', '~> 3.4' |   gem 'parallel_tests', '~> 3.4' | ||||||
|   gem 'rspec_junit_formatter', '~> 0.4' |   gem 'rspec_junit_formatter', '~> 0.4' | ||||||
| end | end | ||||||
|  | |||||||
							
								
								
									
										33
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @ -79,7 +79,7 @@ GEM | |||||||
|       cocaine (~> 0.5.3) |       cocaine (~> 0.5.3) | ||||||
|     awrence (1.1.1) |     awrence (1.1.1) | ||||||
|     aws-eventstream (1.1.0) |     aws-eventstream (1.1.0) | ||||||
|     aws-partitions (1.427.0) |     aws-partitions (1.429.0) | ||||||
|     aws-sdk-core (3.112.0) |     aws-sdk-core (3.112.0) | ||||||
|       aws-eventstream (~> 1, >= 1.0.2) |       aws-eventstream (~> 1, >= 1.0.2) | ||||||
|       aws-partitions (~> 1, >= 1.239.0) |       aws-partitions (~> 1, >= 1.239.0) | ||||||
| @ -88,7 +88,7 @@ GEM | |||||||
|     aws-sdk-kms (1.42.0) |     aws-sdk-kms (1.42.0) | ||||||
|       aws-sdk-core (~> 3, >= 3.112.0) |       aws-sdk-core (~> 3, >= 3.112.0) | ||||||
|       aws-sigv4 (~> 1.1) |       aws-sigv4 (~> 1.1) | ||||||
|     aws-sdk-s3 (1.88.1) |     aws-sdk-s3 (1.89.0) | ||||||
|       aws-sdk-core (~> 3, >= 3.112.0) |       aws-sdk-core (~> 3, >= 3.112.0) | ||||||
|       aws-sdk-kms (~> 1) |       aws-sdk-kms (~> 1) | ||||||
|       aws-sigv4 (~> 1.1) |       aws-sigv4 (~> 1.1) | ||||||
| @ -109,7 +109,7 @@ GEM | |||||||
|     brakeman (4.10.1) |     brakeman (4.10.1) | ||||||
|     browser (4.2.0) |     browser (4.2.0) | ||||||
|     builder (3.2.4) |     builder (3.2.4) | ||||||
|     bullet (6.1.3) |     bullet (6.1.4) | ||||||
|       activesupport (>= 3.0.0) |       activesupport (>= 3.0.0) | ||||||
|       uniform_notifier (~> 1.11) |       uniform_notifier (~> 1.11) | ||||||
|     bundler-audit (0.7.0.1) |     bundler-audit (0.7.0.1) | ||||||
| @ -187,7 +187,7 @@ GEM | |||||||
|     docile (1.3.4) |     docile (1.3.4) | ||||||
|     domain_name (0.5.20190701) |     domain_name (0.5.20190701) | ||||||
|       unf (>= 0.0.5, < 1.0.0) |       unf (>= 0.0.5, < 1.0.0) | ||||||
|     doorkeeper (5.4.0) |     doorkeeper (5.5.0) | ||||||
|       railties (>= 5) |       railties (>= 5) | ||||||
|     dotenv (2.7.6) |     dotenv (2.7.6) | ||||||
|     dotenv-rails (2.7.6) |     dotenv-rails (2.7.6) | ||||||
| @ -218,7 +218,7 @@ GEM | |||||||
|       ruby2_keywords |       ruby2_keywords | ||||||
|     faraday-net_http (1.0.1) |     faraday-net_http (1.0.1) | ||||||
|     fast_blank (1.0.0) |     fast_blank (1.0.0) | ||||||
|     fastimage (2.2.2) |     fastimage (2.2.3) | ||||||
|     ffi (1.10.0) |     ffi (1.10.0) | ||||||
|     ffi-compiler (1.0.1) |     ffi-compiler (1.0.1) | ||||||
|       ffi (>= 1.0.0) |       ffi (>= 1.0.0) | ||||||
| @ -292,8 +292,8 @@ GEM | |||||||
|     iso-639 (0.3.5) |     iso-639 (0.3.5) | ||||||
|     jmespath (1.4.0) |     jmespath (1.4.0) | ||||||
|     json (2.3.1) |     json (2.3.1) | ||||||
|     json-canonicalization (0.2.0) |     json-canonicalization (0.2.1) | ||||||
|     json-ld (3.1.8) |     json-ld (3.1.9) | ||||||
|       htmlentities (~> 4.3) |       htmlentities (~> 4.3) | ||||||
|       json-canonicalization (~> 0.2) |       json-canonicalization (~> 0.2) | ||||||
|       link_header (~> 0.0, >= 0.0.8) |       link_header (~> 0.0, >= 0.0.8) | ||||||
| @ -353,7 +353,7 @@ GEM | |||||||
|     mimemagic (0.3.5) |     mimemagic (0.3.5) | ||||||
|     mini_mime (1.0.2) |     mini_mime (1.0.2) | ||||||
|     mini_portile2 (2.5.0) |     mini_portile2 (2.5.0) | ||||||
|     minitest (5.14.3) |     minitest (5.14.4) | ||||||
|     msgpack (1.4.2) |     msgpack (1.4.2) | ||||||
|     multi_json (1.15.0) |     multi_json (1.15.0) | ||||||
|     multipart-post (2.1.1) |     multipart-post (2.1.1) | ||||||
| @ -482,7 +482,7 @@ GEM | |||||||
|       thor (>= 0.19.0, < 2.0) |       thor (>= 0.19.0, < 2.0) | ||||||
|     rainbow (3.0.0) |     rainbow (3.0.0) | ||||||
|     rake (13.0.3) |     rake (13.0.3) | ||||||
|     rdf (3.1.10) |     rdf (3.1.12) | ||||||
|       hamster (~> 3.0) |       hamster (~> 3.0) | ||||||
|       link_header (~> 0.0, >= 0.0.8) |       link_header (~> 0.0, >= 0.0.8) | ||||||
|     rdf-normalize (0.4.0) |     rdf-normalize (0.4.0) | ||||||
| @ -642,7 +642,8 @@ GEM | |||||||
|       tty-screen (~> 0.8) |       tty-screen (~> 0.8) | ||||||
|       wisper (~> 2.0) |       wisper (~> 2.0) | ||||||
|     tty-screen (0.8.1) |     tty-screen (0.8.1) | ||||||
|     twitter-text (1.14.7) |     twitter-text (3.1.0) | ||||||
|  |       idn-ruby | ||||||
|       unf (~> 0.1.0) |       unf (~> 0.1.0) | ||||||
|     tzinfo (1.2.9) |     tzinfo (1.2.9) | ||||||
|       thread_safe (~> 0.1) |       thread_safe (~> 0.1) | ||||||
| @ -652,7 +653,7 @@ GEM | |||||||
|       unf_ext |       unf_ext | ||||||
|     unf_ext (0.0.7.7) |     unf_ext (0.0.7.7) | ||||||
|     unicode-display_width (1.7.0) |     unicode-display_width (1.7.0) | ||||||
|     uniform_notifier (1.13.2) |     uniform_notifier (1.14.1) | ||||||
|     warden (1.2.9) |     warden (1.2.9) | ||||||
|       rack (>= 2.0.9) |       rack (>= 2.0.9) | ||||||
|     webauthn (3.0.0.alpha1) |     webauthn (3.0.0.alpha1) | ||||||
| @ -665,7 +666,7 @@ GEM | |||||||
|       safety_net_attestation (~> 0.4.0) |       safety_net_attestation (~> 0.4.0) | ||||||
|       securecompare (~> 1.0) |       securecompare (~> 1.0) | ||||||
|       tpm-key_attestation (~> 0.9.0) |       tpm-key_attestation (~> 0.9.0) | ||||||
|     webmock (3.11.2) |     webmock (3.12.0) | ||||||
|       addressable (>= 2.3.6) |       addressable (>= 2.3.6) | ||||||
|       crack (>= 0.3.2) |       crack (>= 0.3.2) | ||||||
|       hashdiff (>= 0.4.0, < 2.0.0) |       hashdiff (>= 0.4.0, < 2.0.0) | ||||||
| @ -693,7 +694,7 @@ DEPENDENCIES | |||||||
|   active_record_query_trace (~> 1.8) |   active_record_query_trace (~> 1.8) | ||||||
|   addressable (~> 2.7) |   addressable (~> 2.7) | ||||||
|   annotate (~> 3.1) |   annotate (~> 3.1) | ||||||
|   aws-sdk-s3 (~> 1.88) |   aws-sdk-s3 (~> 1.89) | ||||||
|   better_errors (~> 2.9) |   better_errors (~> 2.9) | ||||||
|   binding_of_caller (~> 1.0) |   binding_of_caller (~> 1.0) | ||||||
|   blurhash (~> 0.1) |   blurhash (~> 0.1) | ||||||
| @ -718,7 +719,7 @@ DEPENDENCIES | |||||||
|   devise-two-factor (~> 3.1) |   devise-two-factor (~> 3.1) | ||||||
|   devise_pam_authenticatable2 (~> 9.2) |   devise_pam_authenticatable2 (~> 9.2) | ||||||
|   discard (~> 1.2) |   discard (~> 1.2) | ||||||
|   doorkeeper (~> 5.4) |   doorkeeper (~> 5.5) | ||||||
|   dotenv-rails (~> 2.7) |   dotenv-rails (~> 2.7) | ||||||
|   ed25519 (~> 1.2) |   ed25519 (~> 1.2) | ||||||
|   fabrication (~> 2.21) |   fabrication (~> 2.21) | ||||||
| @ -812,10 +813,10 @@ DEPENDENCIES | |||||||
|   strong_migrations (~> 0.7) |   strong_migrations (~> 0.7) | ||||||
|   thor (~> 1.1) |   thor (~> 1.1) | ||||||
|   tty-prompt (~> 0.23) |   tty-prompt (~> 0.23) | ||||||
|   twitter-text (~> 1.14) |   twitter-text (~> 3.1.0) | ||||||
|   tzinfo-data (~> 1.2021) |   tzinfo-data (~> 1.2021) | ||||||
|   webauthn (~> 3.0.0.alpha1) |   webauthn (~> 3.0.0.alpha1) | ||||||
|   webmock (~> 3.11) |   webmock (~> 3.12) | ||||||
|   webpacker (~> 5.2) |   webpacker (~> 5.2) | ||||||
|   webpush |   webpush | ||||||
|   xorcist (~> 1.1) |   xorcist (~> 1.1) | ||||||
|  | |||||||
| @ -27,6 +27,8 @@ class Api::V1::AccountsController < Api::BaseController | |||||||
| 
 | 
 | ||||||
|     self.response_body = Oj.dump(response.body) |     self.response_body = Oj.dump(response.body) | ||||||
|     self.status        = response.status |     self.status        = response.status | ||||||
|  |   rescue ActiveRecord::RecordInvalid => e | ||||||
|  |     render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def follow |   def follow | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								app/controllers/api/v1/emails/confirmations_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/controllers/api/v1/emails/confirmations_controller.rb
									
									
									
									
									
										Normal 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 | ||||||
| @ -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 '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> | ||||||
| ); | ); | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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,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; |  | ||||||
| }()); |  | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								app/lib/validation_error_formatter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/lib/validation_error_formatter.rb
									
									
									
									
									
										Normal 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 | ||||||
| @ -2,12 +2,12 @@ | |||||||
| 
 | 
 | ||||||
| class FetchLinkCardService < BaseService | class FetchLinkCardService < BaseService | ||||||
|   URL_PATTERN = %r{ |   URL_PATTERN = %r{ | ||||||
|     (                                                                                                 #   $1 URL |     (                                                                                                                           #   $1 URL | ||||||
|       (https?:\/\/)                                                                                   #   $2 Protocol (required) |       (https?:\/\/)                                                                                                             #   $2 Protocol (required) | ||||||
|       (#{Twitter::Regex[:valid_domain]})                                                              #   $3 Domain(s) |       (#{Twitter::TwitterText::Regex[:valid_domain]})                                                                           #   $3 Domain(s) | ||||||
|       (?::(#{Twitter::Regex[:valid_port_number]}))?                                                   #   $4 Port number (optional) |       (?::(#{Twitter::TwitterText::Regex[:valid_port_number]}))?                                                                #   $4 Port number (optional) | ||||||
|       (/#{Twitter::Regex[:valid_url_path]}*)?                                                         #   $5 URL Path and anchor |       (/#{Twitter::TwitterText::Regex[:valid_url_path]}*)?                                                                      #   $5 URL Path and anchor | ||||||
|       (\?#{Twitter::Regex[:valid_url_query_chars]}*#{Twitter::Regex[:valid_url_query_ending_chars]})? #   $6 Query String |       (\?#{Twitter::TwitterText::Regex[:valid_url_query_chars]}*#{Twitter::TwitterText::Regex[:valid_url_query_ending_chars]})? #   $6 Query String | ||||||
|     ) |     ) | ||||||
|   }iox |   }iox | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,10 +3,11 @@ | |||||||
| class FollowService < BaseService | class FollowService < BaseService | ||||||
|   include Redisable |   include Redisable | ||||||
|   include Payloadable |   include Payloadable | ||||||
|  |   include DomainControlHelper | ||||||
| 
 | 
 | ||||||
|   # Follow a remote user, notify remote user about the follow |   # Follow a remote user, notify remote user about the follow | ||||||
|   # @param [Account] source_account From which to follow |   # @param [Account] source_account From which to follow | ||||||
|   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) |   # @param [Account] target_account Account to follow | ||||||
|   # @param [Hash] options |   # @param [Hash] options | ||||||
|   # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true |   # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true | ||||||
|   # @option [Boolean] :notify Whether to create notifications about new posts, defaults to false |   # @option [Boolean] :notify Whether to create notifications about new posts, defaults to false | ||||||
| @ -15,7 +16,7 @@ class FollowService < BaseService | |||||||
|   # @option [Boolean] :with_rate_limit |   # @option [Boolean] :with_rate_limit | ||||||
|   def call(source_account, target_account, options = {}) |   def call(source_account, target_account, options = {}) | ||||||
|     @source_account = source_account |     @source_account = source_account | ||||||
|     @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) |     @target_account = target_account | ||||||
|     @options        = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options) |     @options        = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options) | ||||||
| 
 | 
 | ||||||
|     raise ActiveRecord::RecordNotFound if following_not_possible? |     raise ActiveRecord::RecordNotFound if following_not_possible? | ||||||
| @ -43,7 +44,7 @@ class FollowService < BaseService | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def following_not_allowed? |   def following_not_allowed? | ||||||
|     @target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain) |     domain_not_allowed?(@target_account.domain) || @target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def change_follow_options! |   def change_follow_options! | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ class ResolveAccountService < BaseService | |||||||
|   # @param [String, Account] uri URI in the username@domain format or account record |   # @param [String, Account] uri URI in the username@domain format or account record | ||||||
|   # @param [Hash] options |   # @param [Hash] options | ||||||
|   # @option options [Boolean] :redirected Do not follow further Webfinger redirects |   # @option options [Boolean] :redirected Do not follow further Webfinger redirects | ||||||
|   # @option options [Boolean] :skip_webfinger Do not attempt to refresh account data |   # @option options [Boolean] :skip_webfinger Do not attempt any webfinger query or refreshing account data | ||||||
|   # @return [Account] |   # @return [Account] | ||||||
|   def call(uri, options = {}) |   def call(uri, options = {}) | ||||||
|     return if uri.blank? |     return if uri.blank? | ||||||
| @ -120,8 +120,9 @@ class ResolveAccountService < BaseService | |||||||
| 
 | 
 | ||||||
|   def webfinger_update_due? |   def webfinger_update_due? | ||||||
|     return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain) |     return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain) | ||||||
|  |     return false if @options[:skip_webfinger] | ||||||
| 
 | 
 | ||||||
|     @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) |     @account.nil? || (@account.ostatus? && @account.possibly_stale?) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def activitypub_ready? |   def activitypub_ready? | ||||||
|  | |||||||
| @ -2,11 +2,11 @@ | |||||||
| 
 | 
 | ||||||
| class BlacklistedEmailValidator < ActiveModel::Validator | class BlacklistedEmailValidator < ActiveModel::Validator | ||||||
|   def validate(user) |   def validate(user) | ||||||
|     return if user.valid_invitation? |     return if user.valid_invitation? || user.email.blank? | ||||||
| 
 | 
 | ||||||
|     @email = user.email |     @email = user.email | ||||||
| 
 | 
 | ||||||
|     user.errors.add(:email, I18n.t('users.blocked_email_provider')) if blocked_email? |     user.errors.add(:email, :blocked) if blocked_email? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  | |||||||
| @ -4,16 +4,19 @@ require 'resolv' | |||||||
| 
 | 
 | ||||||
| class EmailMxValidator < ActiveModel::Validator | class EmailMxValidator < ActiveModel::Validator | ||||||
|   def validate(user) |   def validate(user) | ||||||
|  |     return if user.email.blank? | ||||||
|  | 
 | ||||||
|     domain = get_domain(user.email) |     domain = get_domain(user.email) | ||||||
| 
 | 
 | ||||||
|     if domain.nil? |     if domain.blank? | ||||||
|       user.errors.add(:email, I18n.t('users.invalid_email')) |       user.errors.add(:email, :invalid) | ||||||
|     else |     else | ||||||
|       ips, hostnames = resolve_mx(domain) |       ips, hostnames = resolve_mx(domain) | ||||||
|  | 
 | ||||||
|       if ips.empty? |       if ips.empty? | ||||||
|         user.errors.add(:email, I18n.t('users.invalid_email_mx')) |         user.errors.add(:email, :unreachable) | ||||||
|       elsif on_blacklist?(hostnames + ips) |       elsif on_blacklist?(hostnames + ips) | ||||||
|         user.errors.add(:email, I18n.t('users.blocked_email_provider')) |         user.errors.add(:email, :blocked) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| class NoteLengthValidator < ActiveModel::EachValidator | class NoteLengthValidator < ActiveModel::EachValidator | ||||||
|   def validate_each(record, attribute, value) |   def validate_each(record, attribute, value) | ||||||
|     record.errors.add(attribute, I18n.t('statuses.over_character_limit', max: options[:maximum])) if too_long?(value) |     record.errors.add(attribute, :too_long, message: I18n.t('statuses.over_character_limit', max: options[:maximum]), count: options[:maximum]) if too_long?(value) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  | |||||||
| @ -2,6 +2,13 @@ | |||||||
| 
 | 
 | ||||||
| class StatusLengthValidator < ActiveModel::Validator | class StatusLengthValidator < ActiveModel::Validator | ||||||
|   MAX_CHARS = (ENV['MAX_TOOT_CHARS'] || 500).to_i |   MAX_CHARS = (ENV['MAX_TOOT_CHARS'] || 500).to_i | ||||||
|  |   URL_PATTERN = %r{ | ||||||
|  |     (?: | ||||||
|  |       (#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]}) | ||||||
|  |       (#{FetchLinkCardService::URL_PATTERN}) | ||||||
|  |     ) | ||||||
|  |   }iox | ||||||
|  |   URL_PLACEHOLDER = "\1#{'x' * 23}" | ||||||
| 
 | 
 | ||||||
|   def validate(status) |   def validate(status) | ||||||
|     return unless status.local? && !status.reblog? |     return unless status.local? && !status.reblog? | ||||||
| @ -28,7 +35,7 @@ class StatusLengthValidator < ActiveModel::Validator | |||||||
|     return '' if @status.text.nil? |     return '' if @status.text.nil? | ||||||
| 
 | 
 | ||||||
|     @status.text.dup.tap do |new_text| |     @status.text.dup.tap do |new_text| | ||||||
|       new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23) |       new_text.gsub!(URL_PATTERN, URL_PLACEHOLDER) | ||||||
|       new_text.gsub!(Account::MENTION_RE, '@\2') |       new_text.gsub!(Account::MENTION_RE, '@\2') | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ | |||||||
| 
 | 
 | ||||||
| class UniqueUsernameValidator < ActiveModel::Validator | class UniqueUsernameValidator < ActiveModel::Validator | ||||||
|   def validate(account) |   def validate(account) | ||||||
|     return if account.username.nil? |     return if account.username.blank? | ||||||
| 
 | 
 | ||||||
|     normalized_username = account.username.downcase |     normalized_username = account.username.downcase | ||||||
|     normalized_domain = account.domain&.downcase |     normalized_domain = account.domain&.downcase | ||||||
|  | |||||||
| @ -3,9 +3,10 @@ | |||||||
| class UnreservedUsernameValidator < ActiveModel::Validator | class UnreservedUsernameValidator < ActiveModel::Validator | ||||||
|   def validate(account) |   def validate(account) | ||||||
|     @username = account.username |     @username = account.username | ||||||
|     return if @username.nil? |  | ||||||
| 
 | 
 | ||||||
|     account.errors.add(:username, I18n.t('accounts.reserved_username')) if reserved_username? |     return if @username.blank? | ||||||
|  | 
 | ||||||
|  |     account.errors.add(:username, :reserved) if reserved_username? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  | |||||||
| @ -42,7 +42,7 @@ | |||||||
|   .column-3 |   .column-3 | ||||||
|     = render 'application/flashes' |     = render 'application/flashes' | ||||||
| 
 | 
 | ||||||
|     - if @contents.blank? && (!display_blocks? || @blocks&.empty?) |     - if @contents.blank? && @rules.empty? && (!display_blocks? || @blocks&.empty?) | ||||||
|       = nothing_here |       = nothing_here | ||||||
|     - else |     - else | ||||||
|       .box-widget |       .box-widget | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ | |||||||
|   = f.input :report_id, as: :hidden |   = f.input :report_id, as: :hidden | ||||||
| 
 | 
 | ||||||
|   .fields-group |   .fields-group | ||||||
|     = f.input :type, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { I18n.t("simple_form.labels.admin_account_action.types.#{type}")}, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.acct) |     = f.input :type, as: :radio_buttons, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { safe_join([I18n.t("simple_form.labels.admin_account_action.types.#{type}"), content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint')])}, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.acct) | ||||||
| 
 | 
 | ||||||
|   - if @account.local? |   - if @account.local? | ||||||
|     %hr.spacer/ |     %hr.spacer/ | ||||||
|  | |||||||
| @ -94,11 +94,15 @@ class Rack::Attack | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req| |   throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req| | ||||||
|     req.remote_ip if req.post? && req.path == '/auth/confirmation' |     req.remote_ip if req.post? && %w(/auth/confirmation /api/v1/emails/confirmations).include?(req.path) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req| |   throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req| | ||||||
|     req.params.dig('user', 'email').presence if req.post? && req.path == '/auth/password' |     if req.post? && req.path == '/auth/password' | ||||||
|  |       req.params.dig('user', 'email').presence | ||||||
|  |     elsif req.post? && req.path == '/api/v1/emails/confirmations' | ||||||
|  |       req.authenticated_user_id | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req| |   throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req| | ||||||
|  | |||||||
| @ -1,4 +1,10 @@ | |||||||
| module Twitter | module Twitter::TwitterText | ||||||
|  |   class Configuration | ||||||
|  |     def emoji_parsing_enabled | ||||||
|  |       false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   class Regex |   class Regex | ||||||
|     REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou |     REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou | ||||||
|     REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou |     REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou | ||||||
| @ -79,7 +85,7 @@ module Twitter | |||||||
|       return [] unless text && text.index(":") |       return [] unless text && text.index(":") | ||||||
|       urls = [] |       urls = [] | ||||||
| 
 | 
 | ||||||
|       text.to_s.scan(Twitter::Regex[:valid_extended_uri]) do |       text.to_s.scan(Twitter::TwitterText::Regex[:valid_extended_uri]) do | ||||||
|         valid_uri_match_data = $~ |         valid_uri_match_data = $~ | ||||||
| 
 | 
 | ||||||
|         start_position = valid_uri_match_data.char_begin(3) |         start_position = valid_uri_match_data.char_begin(3) | ||||||
|  | |||||||
| @ -5,13 +5,28 @@ en: | |||||||
|       poll: |       poll: | ||||||
|         expires_at: Deadline |         expires_at: Deadline | ||||||
|         options: Choices |         options: Choices | ||||||
|  |       user: | ||||||
|  |         agreement: Service agreement | ||||||
|  |         email: E-mail address | ||||||
|  |         locale: Locale | ||||||
|  |         password: Password | ||||||
|  |       user/account: | ||||||
|  |         username: Username | ||||||
|  |       user/invite_request: | ||||||
|  |         text: Reason | ||||||
|     errors: |     errors: | ||||||
|       models: |       models: | ||||||
|         account: |         account: | ||||||
|           attributes: |           attributes: | ||||||
|             username: |             username: | ||||||
|               invalid: only letters, numbers and underscores |               invalid: must contain only letters, numbers and underscores | ||||||
|  |               reserved: is reserved | ||||||
|         status: |         status: | ||||||
|           attributes: |           attributes: | ||||||
|             reblog: |             reblog: | ||||||
|               taken: of status already exists |               taken: of status already exists | ||||||
|  |         user: | ||||||
|  |           attributes: | ||||||
|  |             email: | ||||||
|  |               blocked: uses a disallowed e-mail provider | ||||||
|  |               unreachable: does not seem to exist | ||||||
|  | |||||||
| @ -80,7 +80,6 @@ en: | |||||||
|       other: Toots |       other: Toots | ||||||
|     posts_tab_heading: Toots |     posts_tab_heading: Toots | ||||||
|     posts_with_replies: Toots and replies |     posts_with_replies: Toots and replies | ||||||
|     reserved_username: The username is reserved |  | ||||||
|     roles: |     roles: | ||||||
|       admin: Admin |       admin: Admin | ||||||
|       bot: Bot |       bot: Bot | ||||||
| @ -1410,11 +1409,8 @@ en: | |||||||
|       tips: Tips |       tips: Tips | ||||||
|       title: Welcome aboard, %{name}! |       title: Welcome aboard, %{name}! | ||||||
|   users: |   users: | ||||||
|     blocked_email_provider: This e-mail provider isn't allowed |  | ||||||
|     follow_limit_reached: You cannot follow more than %{limit} people |     follow_limit_reached: You cannot follow more than %{limit} people | ||||||
|     generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance |     generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance | ||||||
|     invalid_email: The e-mail address is invalid |  | ||||||
|     invalid_email_mx: The e-mail address does not seem to exist |  | ||||||
|     invalid_otp_token: Invalid two-factor code |     invalid_otp_token: Invalid two-factor code | ||||||
|     invalid_sign_in_token: Invalid security code |     invalid_sign_in_token: Invalid security code | ||||||
|     otp_lost_help_html: If you lost access to both, you may get in touch with %{email} |     otp_lost_help_html: If you lost access to both, you may get in touch with %{email} | ||||||
|  | |||||||
| @ -14,6 +14,12 @@ en: | |||||||
|         send_email_notification: The user will receive an explanation of what happened with their account |         send_email_notification: The user will receive an explanation of what happened with their account | ||||||
|         text_html: Optional. You can use toot syntax. You can <a href="%{path}">add warning presets</a> to save time |         text_html: Optional. You can use toot syntax. You can <a href="%{path}">add warning presets</a> to save time | ||||||
|         type_html: Choose what to do with <strong>%{acct}</strong> |         type_html: Choose what to do with <strong>%{acct}</strong> | ||||||
|  |         types: | ||||||
|  |           disable: Prevent the user from using their account, but do not delete or hide their contents. | ||||||
|  |           none: Use this to send a warning to the user, without triggering any other action. | ||||||
|  |           sensitive: Force all this user's media attachments to be flagged as sensitive. | ||||||
|  |           silence: Prevent the user from being able to post with public visibility, hide their posts and notifications from people not following them. | ||||||
|  |           suspend: Prevent any interaction from or to this account and delete its contents. Revertible within 30 days. | ||||||
|         warning_preset_id: Optional. You can still add custom text to end of the preset |         warning_preset_id: Optional. You can still add custom text to end of the preset | ||||||
|       announcement: |       announcement: | ||||||
|         all_day: When checked, only the dates of the time range will be displayed |         all_day: When checked, only the dates of the time range will be displayed | ||||||
|  | |||||||
| @ -406,6 +406,10 @@ Rails.application.routes.draw do | |||||||
| 
 | 
 | ||||||
|       resources :apps, only: [:create] |       resources :apps, only: [:create] | ||||||
| 
 | 
 | ||||||
|  |       namespace :emails do | ||||||
|  |         resources :confirmations, only: [:create] | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       resource :instance, only: [:show] do |       resource :instance, only: [:show] do | ||||||
|         resources :peers, only: [:index], controller: 'instances/peers' |         resources :peers, only: [:index], controller: 'instances/peers' | ||||||
|         resource :activity, only: [:show], controller: 'instances/activity' |         resource :activity, only: [:show], controller: 'instances/activity' | ||||||
|  | |||||||
| @ -91,7 +91,7 @@ namespace :emojis do | |||||||
|   desc 'Generate emoji variants with white borders' |   desc 'Generate emoji variants with white borders' | ||||||
|   task :generate_borders do |   task :generate_borders do | ||||||
|     src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json') |     src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json') | ||||||
|     emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️' |     emojis = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴🐞🕺📱📲👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️' | ||||||
| 
 | 
 | ||||||
|     map = Oj.load(File.read(src)) |     map = Oj.load(File.read(src)) | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								package.json
									
									
									
									
									
								
							| @ -60,19 +60,19 @@ | |||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@babel/core": "^7.12.17", |     "@babel/core": "^7.13.8", | ||||||
|     "@babel/plugin-proposal-class-properties": "^7.8.3", |     "@babel/plugin-proposal-class-properties": "^7.8.3", | ||||||
|     "@babel/plugin-proposal-decorators": "^7.12.13", |     "@babel/plugin-proposal-decorators": "^7.13.5", | ||||||
|     "@babel/plugin-transform-react-inline-elements": "^7.12.13", |     "@babel/plugin-transform-react-inline-elements": "^7.12.13", | ||||||
|     "@babel/plugin-transform-runtime": "^7.12.17", |     "@babel/plugin-transform-runtime": "^7.13.8", | ||||||
|     "@babel/preset-env": "^7.12.17", |     "@babel/preset-env": "^7.13.8", | ||||||
|     "@babel/preset-react": "^7.12.13", |     "@babel/preset-react": "^7.12.13", | ||||||
|     "@babel/runtime": "^7.12.18", |     "@babel/runtime": "^7.13.8", | ||||||
|     "@clusterws/cws": "^3.0.0", |     "@clusterws/cws": "^3.0.0", | ||||||
|     "@gamestdio/websocket": "^0.3.2", |     "@gamestdio/websocket": "^0.3.2", | ||||||
|     "@github/webauthn-json": "^0.5.7", |     "@github/webauthn-json": "^0.5.7", | ||||||
|     "@rails/ujs": "^6.1.3", |     "@rails/ujs": "^6.1.3", | ||||||
|     "array-includes": "^3.1.2", |     "array-includes": "^3.1.3", | ||||||
|     "atrament": "0.2.4", |     "atrament": "0.2.4", | ||||||
|     "arrow-key-navigation": "^1.2.0", |     "arrow-key-navigation": "^1.2.0", | ||||||
|     "autoprefixer": "^9.8.6", |     "autoprefixer": "^9.8.6", | ||||||
| @ -88,7 +88,7 @@ | |||||||
|     "color-blend": "^3.0.1", |     "color-blend": "^3.0.1", | ||||||
|     "compression-webpack-plugin": "^6.1.1", |     "compression-webpack-plugin": "^6.1.1", | ||||||
|     "cross-env": "^7.0.3", |     "cross-env": "^7.0.3", | ||||||
|     "css-loader": "^5.0.2", |     "css-loader": "^5.1.0", | ||||||
|     "cssnano": "^4.1.10", |     "cssnano": "^4.1.10", | ||||||
|     "detect-passive-events": "^2.0.3", |     "detect-passive-events": "^2.0.3", | ||||||
|     "dotenv": "^8.2.0", |     "dotenv": "^8.2.0", | ||||||
| @ -111,18 +111,18 @@ | |||||||
|     "intl-relativeformat": "^6.4.3", |     "intl-relativeformat": "^6.4.3", | ||||||
|     "is-nan": "^1.3.2", |     "is-nan": "^1.3.2", | ||||||
|     "js-yaml": "^4.0.0", |     "js-yaml": "^4.0.0", | ||||||
|     "lodash": "^4.17.19", |     "lodash": "^4.17.21", | ||||||
|     "mark-loader": "^0.1.6", |     "mark-loader": "^0.1.6", | ||||||
|     "marky": "^1.2.1", |     "marky": "^1.2.1", | ||||||
|     "mini-css-extract-plugin": "^1.3.8", |     "mini-css-extract-plugin": "^1.3.9", | ||||||
|     "mkdirp": "^1.0.4", |     "mkdirp": "^1.0.4", | ||||||
|     "npmlog": "^4.1.2", |     "npmlog": "^4.1.2", | ||||||
|     "object-assign": "^4.1.1", |     "object-assign": "^4.1.1", | ||||||
|     "object-fit-images": "^3.2.3", |     "object-fit-images": "^3.2.3", | ||||||
|     "object.values": "^1.1.2", |     "object.values": "^1.1.3", | ||||||
|     "offline-plugin": "^5.0.7", |     "offline-plugin": "^5.0.7", | ||||||
|     "path-complete-extname": "^1.0.0", |     "path-complete-extname": "^1.0.0", | ||||||
|     "pg": "^6.4.0", |     "pg": "^8.5.0", | ||||||
|     "postcss-loader": "^3.0.0", |     "postcss-loader": "^3.0.0", | ||||||
|     "postcss-object-fit-images": "^1.1.2", |     "postcss-object-fit-images": "^1.1.2", | ||||||
|     "promise.prototype.finally": "^3.1.2", |     "promise.prototype.finally": "^3.1.2", | ||||||
| @ -165,6 +165,7 @@ | |||||||
|     "tesseract.js": "^2.1.1", |     "tesseract.js": "^2.1.1", | ||||||
|     "throng": "^4.0.0", |     "throng": "^4.0.0", | ||||||
|     "tiny-queue": "^0.2.1", |     "tiny-queue": "^0.2.1", | ||||||
|  |     "twitter-text": "3.1.0", | ||||||
|     "uuid": "^8.3.1", |     "uuid": "^8.3.1", | ||||||
|     "webpack": "^4.46.0", |     "webpack": "^4.46.0", | ||||||
|     "webpack-assets-manifest": "^4.0.1", |     "webpack-assets-manifest": "^4.0.1", | ||||||
| @ -178,7 +179,7 @@ | |||||||
|     "@testing-library/react": "^11.2.5", |     "@testing-library/react": "^11.2.5", | ||||||
|     "babel-eslint": "^10.1.0", |     "babel-eslint": "^10.1.0", | ||||||
|     "babel-jest": "^26.6.3", |     "babel-jest": "^26.6.3", | ||||||
|     "eslint": "^7.20.0", |     "eslint": "^7.21.0", | ||||||
|     "eslint-plugin-import": "~2.22.1", |     "eslint-plugin-import": "~2.22.1", | ||||||
|     "eslint-plugin-jsx-a11y": "~6.4.1", |     "eslint-plugin-jsx-a11y": "~6.4.1", | ||||||
|     "eslint-plugin-promise": "~4.3.1", |     "eslint-plugin-promise": "~4.3.1", | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								public/emoji/1f4f1_border.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								public/emoji/1f4f1_border.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										9
									
								
								public/emoji/1f4f2_border.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								public/emoji/1f4f2_border.svg
									
									
									
									
									
										Normal 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 | 
| @ -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 | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user