OEmbed support for PreviewCard (#2337)
* OEmbed support for PreviewCard * Improve ProviderDiscovery code failure treatment * Do not crawl links if there is a content warning, since those don't display a link card anyway * Reset db schema * Fresh migrate * Fix rubocop style issues Fix #1681 - return existing access token when applicable instead of creating new * Fix test * Extract http client to helper * Improve oembed controller
This commit is contained in:
		
							
								
								
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							| @ -49,6 +49,7 @@ gem 'rails-settings-cached' | |||||||
| gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] | gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] | ||||||
| gem 'rqrcode' | gem 'rqrcode' | ||||||
| gem 'ruby-oembed', require: 'oembed' | gem 'ruby-oembed', require: 'oembed' | ||||||
|  | gem 'sanitize' | ||||||
| gem 'sidekiq' | gem 'sidekiq' | ||||||
| gem 'sidekiq-unique-jobs' | gem 'sidekiq-unique-jobs' | ||||||
| gem 'simple-navigation' | gem 'simple-navigation' | ||||||
|  | |||||||
| @ -123,6 +123,7 @@ GEM | |||||||
|     connection_pool (2.2.1) |     connection_pool (2.2.1) | ||||||
|     crack (0.4.3) |     crack (0.4.3) | ||||||
|       safe_yaml (~> 1.0.0) |       safe_yaml (~> 1.0.0) | ||||||
|  |     crass (1.0.2) | ||||||
|     debug_inspector (0.0.2) |     debug_inspector (0.0.2) | ||||||
|     devise (4.2.1) |     devise (4.2.1) | ||||||
|       bcrypt (~> 3.0) |       bcrypt (~> 3.0) | ||||||
| @ -258,6 +259,8 @@ GEM | |||||||
|     nio4r (2.0.0) |     nio4r (2.0.0) | ||||||
|     nokogiri (1.7.1) |     nokogiri (1.7.1) | ||||||
|       mini_portile2 (~> 2.1.0) |       mini_portile2 (~> 2.1.0) | ||||||
|  |     nokogumbo (1.4.10) | ||||||
|  |       nokogiri | ||||||
|     oj (2.18.5) |     oj (2.18.5) | ||||||
|     openssl (2.0.3) |     openssl (2.0.3) | ||||||
|     orm_adapter (0.5.0) |     orm_adapter (0.5.0) | ||||||
| @ -398,6 +401,10 @@ GEM | |||||||
|     ruby-oembed (0.12.0) |     ruby-oembed (0.12.0) | ||||||
|     ruby-progressbar (1.8.1) |     ruby-progressbar (1.8.1) | ||||||
|     safe_yaml (1.0.4) |     safe_yaml (1.0.4) | ||||||
|  |     sanitize (4.4.0) | ||||||
|  |       crass (~> 1.0.2) | ||||||
|  |       nokogiri (>= 1.4.4) | ||||||
|  |       nokogumbo (~> 1.4.1) | ||||||
|     sass (3.4.23) |     sass (3.4.23) | ||||||
|     sass-rails (5.0.6) |     sass-rails (5.0.6) | ||||||
|       railties (>= 4.0.0, < 6) |       railties (>= 4.0.0, < 6) | ||||||
| @ -540,6 +547,7 @@ DEPENDENCIES | |||||||
|   rspec-sidekiq |   rspec-sidekiq | ||||||
|   rubocop |   rubocop | ||||||
|   ruby-oembed |   ruby-oembed | ||||||
|  |   sanitize | ||||||
|   sass-rails (~> 5.0) |   sass-rails (~> 5.0) | ||||||
|   sidekiq |   sidekiq | ||||||
|   sidekiq-unique-jobs |   sidekiq-unique-jobs | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ export function fetchStatusCard(id) { | |||||||
|     dispatch(fetchStatusCardRequest(id)); |     dispatch(fetchStatusCardRequest(id)); | ||||||
|  |  | ||||||
|     api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { |     api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { | ||||||
|       if (!response.data.url || !response.data.title || !response.data.description) { |       if (!response.data.url) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | |||||||
| @ -14,14 +14,11 @@ const getHostname = url => { | |||||||
|  |  | ||||||
| class Card extends React.PureComponent { | class Card extends React.PureComponent { | ||||||
|  |  | ||||||
|   render () { |   renderLink () { | ||||||
|     const { card } = this.props; |     const { card } = this.props; | ||||||
|  |  | ||||||
|     if (card === null) { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let image    = ''; |     let image    = ''; | ||||||
|  |     let provider = card.get('provider_name'); | ||||||
|  |  | ||||||
|     if (card.get('image')) { |     if (card.get('image')) { | ||||||
|       image = ( |       image = ( | ||||||
| @ -31,18 +28,64 @@ class Card extends React.PureComponent { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (provider.length < 1) { | ||||||
|  |       provider = getHostname(card.get('url')) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <a href={card.get('url')} className='status-card' target='_blank' rel='noopener'> |       <a href={card.get('url')} className='status-card' target='_blank' rel='noopener'> | ||||||
|         {image} |         {image} | ||||||
|  |  | ||||||
|         <div className='status-card__content'> |         <div className='status-card__content'> | ||||||
|           <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> |           <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> | ||||||
|           <p className='status-card__description'>{card.get('description').substring(0, 50)}</p> |           <p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p> | ||||||
|           <span className='status-card__host' style={hostStyle}>{getHostname(card.get('url'))}</span> |           <span className='status-card__host' style={hostStyle}>{provider}</span> | ||||||
|         </div> |         </div> | ||||||
|       </a> |       </a> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   renderPhoto () { | ||||||
|  |     const { card } = this.props; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'> | ||||||
|  |         <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} /> | ||||||
|  |       </a> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   renderVideo () { | ||||||
|  |     const { card } = this.props; | ||||||
|  |     const content  = { __html: card.get('html') }; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       <div | ||||||
|  |         className='status-card-video' | ||||||
|  |         dangerouslySetInnerHTML={content} | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render () { | ||||||
|  |     const { card } = this.props; | ||||||
|  |  | ||||||
|  |     if (card === null) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     switch(card.get('type')) { | ||||||
|  |     case 'link': | ||||||
|  |       return this.renderLink(); | ||||||
|  |     case 'photo': | ||||||
|  |       return this.renderPhoto(); | ||||||
|  |     case 'video': | ||||||
|  |       return this.renderVideo(); | ||||||
|  |     case 'rich': | ||||||
|  |     default: | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| Card.propTypes = { | Card.propTypes = { | ||||||
|  | |||||||
| @ -1734,6 +1734,28 @@ button.icon-button.active i.fa-retweet { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .status-card-video, .status-card-rich, .status-card-photo { | ||||||
|  |   margin-top: 14px; | ||||||
|  |   overflow: hidden; | ||||||
|  |  | ||||||
|  |   iframe { | ||||||
|  |     width: 100%; | ||||||
|  |     height: auto; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .status-card-photo { | ||||||
|  |   display: block; | ||||||
|  |   text-decoration: none; | ||||||
|  |  | ||||||
|  |   img { | ||||||
|  |     display: block; | ||||||
|  |     width: 100%; | ||||||
|  |     height: auto; | ||||||
|  |     margin: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .status-card__title { | .status-card__title { | ||||||
|   display: block; |   display: block; | ||||||
|   font-weight: 500; |   font-weight: 500; | ||||||
|  | |||||||
| @ -14,8 +14,20 @@ class Api::OEmbedController < ApiController | |||||||
|   def stream_entry_from_url(url) |   def stream_entry_from_url(url) | ||||||
|     params = Rails.application.routes.recognize_path(url) |     params = Rails.application.routes.recognize_path(url) | ||||||
|  |  | ||||||
|     raise ActiveRecord::RecordNotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show' |     raise ActiveRecord::RecordNotFound unless recognized_stream_entry_url?(params) | ||||||
|  |  | ||||||
|  |     stream_entry(params) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def recognized_stream_entry_url?(params) | ||||||
|  |     %w(stream_entries statuses).include?(params[:controller]) && params[:action] == 'show' | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def stream_entry(params) | ||||||
|  |     if params[:controller] == 'stream_entries' | ||||||
|       StreamEntry.find(params[:id]) |       StreamEntry.find(params[:id]) | ||||||
|  |     else | ||||||
|  |       Status.find(params[:id]).stream_entry | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								app/helpers/http_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/helpers/http_helper.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | module HttpHelper | ||||||
|  |   USER_AGENT = "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::VERSION}; +http://#{Rails.configuration.x.local_domain}/)" | ||||||
|  |  | ||||||
|  |   def http_client(options = {}) | ||||||
|  |     timeout = { write: 10, connect: 10, read: 10 }.merge(options) | ||||||
|  |  | ||||||
|  |     HTTP.headers(user_agent: USER_AGENT) | ||||||
|  |         .timeout(:per_operation, timeout) | ||||||
|  |         .follow | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -1,13 +1,13 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
|  |  | ||||||
| require 'singleton' | require 'singleton' | ||||||
|  | require_relative './sanitize_config' | ||||||
|  |  | ||||||
| class Formatter | class Formatter | ||||||
|   include Singleton |   include Singleton | ||||||
|   include RoutingHelper |   include RoutingHelper | ||||||
|  |  | ||||||
|   include ActionView::Helpers::TextHelper |   include ActionView::Helpers::TextHelper | ||||||
|   include ActionView::Helpers::SanitizeHelper |  | ||||||
|  |  | ||||||
|   def format(status) |   def format(status) | ||||||
|     return reformat(status.content) unless status.local? |     return reformat(status.content) unless status.local? | ||||||
| @ -23,7 +23,7 @@ class Formatter | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def reformat(html) |   def reformat(html) | ||||||
|     sanitize(html, tags: %w(a br p span), attributes: %w(href rel class)) |     sanitize(html, Sanitize::Config::MASTODON_STRICT) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def plaintext(status) |   def plaintext(status) | ||||||
| @ -43,6 +43,10 @@ class Formatter | |||||||
|     html.html_safe # rubocop:disable Rails/OutputSafety |     html.html_safe # rubocop:disable Rails/OutputSafety | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def sanitize(html, config) | ||||||
|  |     Sanitize.fragment(html, config) | ||||||
|  |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def encode(html) |   def encode(html) | ||||||
|  | |||||||
							
								
								
									
										36
									
								
								app/lib/provider_discovery.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/lib/provider_discovery.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | class ProviderDiscovery < OEmbed::ProviderDiscovery | ||||||
|  |   include HttpHelper | ||||||
|  |  | ||||||
|  |   class << self | ||||||
|  |     def discover_provider(url, options = {}) | ||||||
|  |       res    = http_client.get(url) | ||||||
|  |       format = options[:format] | ||||||
|  |  | ||||||
|  |       raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html' | ||||||
|  |  | ||||||
|  |       html = Nokogiri::HTML(res.to_s) | ||||||
|  |  | ||||||
|  |       if format.nil? || format == :json | ||||||
|  |         provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value | ||||||
|  |         format ||= :json if provider_endpoint | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       if format.nil? || format == :xml | ||||||
|  |         provider_endpoint ||= html.at_xpath('//link[@type="application/xml+oembed"]')&.attribute('href')&.value | ||||||
|  |         format ||= :xml if provider_endpoint | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       begin | ||||||
|  |         provider_endpoint = Addressable::URI.parse(provider_endpoint) | ||||||
|  |         provider_endpoint.query = nil | ||||||
|  |         provider_endpoint = provider_endpoint.to_s | ||||||
|  |       rescue Addressable::URI::InvalidURIError | ||||||
|  |         raise OEmbed::NotFound, url | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       OEmbed::Provider.new(provider_endpoint, format || OEmbed::Formatter.default) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										42
									
								
								app/lib/sanitize_config.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/lib/sanitize_config.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | class Sanitize | ||||||
|  |   module Config | ||||||
|  |     HTTP_PROTOCOLS ||= ['http', 'https', :relative].freeze | ||||||
|  |  | ||||||
|  |     MASTODON_STRICT ||= freeze_config( | ||||||
|  |       elements: %w(p br span a), | ||||||
|  |  | ||||||
|  |       attributes: { | ||||||
|  |         'a'    => %w(href), | ||||||
|  |         'span' => %w(class), | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       protocols: { | ||||||
|  |         'a' => { 'href' => HTTP_PROTOCOLS }, | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     MASTODON_OEMBED ||= freeze_config merge( | ||||||
|  |       RELAXED, | ||||||
|  |       elements: RELAXED[:elements] + %w(audio embed iframe source video), | ||||||
|  |  | ||||||
|  |       attributes: merge( | ||||||
|  |         RELAXED[:attributes], | ||||||
|  |         'audio'  => %w(controls), | ||||||
|  |         'embed'  => %w(height src type width), | ||||||
|  |         'iframe' => %w(allowfullscreen frameborder height scrolling src width), | ||||||
|  |         'source' => %w(src type), | ||||||
|  |         'video'  => %w(controls height loop width), | ||||||
|  |         'div'    => [:data] | ||||||
|  |       ), | ||||||
|  |  | ||||||
|  |       protocols: merge( | ||||||
|  |         RELAXED[:protocols], | ||||||
|  |         'embed'  => { 'src' => HTTP_PROTOCOLS }, | ||||||
|  |         'iframe' => { 'src' => HTTP_PROTOCOLS }, | ||||||
|  |         'source' => { 'src' => HTTP_PROTOCOLS } | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -3,6 +3,10 @@ | |||||||
| class PreviewCard < ApplicationRecord | class PreviewCard < ApplicationRecord | ||||||
|   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze |   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze | ||||||
|  |  | ||||||
|  |   self.inheritance_column = false | ||||||
|  |  | ||||||
|  |   enum type: [:link, :photo, :video, :rich] | ||||||
|  |  | ||||||
|   belongs_to :status |   belongs_to :status | ||||||
|  |  | ||||||
|   has_attached_file :image, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } |   has_attached_file :image, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
|  |  | ||||||
| class FetchAtomService < BaseService | class FetchAtomService < BaseService | ||||||
|  |   include HttpHelper | ||||||
|  |  | ||||||
|   def call(url) |   def call(url) | ||||||
|     return if url.blank? |     return if url.blank? | ||||||
|  |  | ||||||
| @ -45,8 +47,4 @@ class FetchAtomService < BaseService | |||||||
|   def fetch(url) |   def fetch(url) | ||||||
|     http_client.get(url).to_s |     http_client.get(url).to_s | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def http_client |  | ||||||
|     HTTP.timeout(:per_operation, write: 10, connect: 10, read: 10).follow |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,8 +1,9 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
|  |  | ||||||
| class FetchLinkCardService < BaseService | class FetchLinkCardService < BaseService | ||||||
|  |   include HttpHelper | ||||||
|  |  | ||||||
|   URL_PATTERN = %r{https?://\S+} |   URL_PATTERN = %r{https?://\S+} | ||||||
|   USER_AGENT = "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::VERSION}; +http://#{Rails.configuration.x.local_domain}/)" |  | ||||||
|  |  | ||||||
|   def call(status) |   def call(status) | ||||||
|     # Get first http/https URL that isn't local |     # Get first http/https URL that isn't local | ||||||
| @ -10,13 +11,53 @@ class FetchLinkCardService < BaseService | |||||||
|  |  | ||||||
|     return if url.nil? |     return if url.nil? | ||||||
|  |  | ||||||
|  |     card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url) | ||||||
|  |     attempt_opengraph(card, url) unless attempt_oembed(card, url) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def attempt_oembed(card, url) | ||||||
|  |     response = OEmbed::Providers.get(url) | ||||||
|  |  | ||||||
|  |     card.type          = response.type | ||||||
|  |     card.title         = response.respond_to?(:title)         ? response.title         : '' | ||||||
|  |     card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : '' | ||||||
|  |     card.author_url    = response.respond_to?(:author_url)    ? response.author_url    : '' | ||||||
|  |     card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : '' | ||||||
|  |     card.provider_url  = response.respond_to?(:provider_url)  ? response.provider_url  : '' | ||||||
|  |     card.width         = 0 | ||||||
|  |     card.height        = 0 | ||||||
|  |  | ||||||
|  |     case card.type | ||||||
|  |     when 'link' | ||||||
|  |       card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url) | ||||||
|  |     when 'photo' | ||||||
|  |       card.url    = response.url | ||||||
|  |       card.width  = response.width.presence  || 0 | ||||||
|  |       card.height = response.height.presence || 0 | ||||||
|  |     when 'video' | ||||||
|  |       card.width  = response.width.presence  || 0 | ||||||
|  |       card.height = response.height.presence || 0 | ||||||
|  |       card.html   = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED) | ||||||
|  |     when 'rich' | ||||||
|  |       # Most providers rely on <script> tags, which is a no-no | ||||||
|  |       return false | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     card.save_with_optional_image! | ||||||
|  |   rescue OEmbed::NotFound | ||||||
|  |     false | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def attempt_opengraph(card, url) | ||||||
|     response = http_client.get(url) |     response = http_client.get(url) | ||||||
|  |  | ||||||
|     return if response.code != 200 || response.mime_type != 'text/html' |     return if response.code != 200 || response.mime_type != 'text/html' | ||||||
|  |  | ||||||
|     page = Nokogiri::HTML(response.to_s) |     page = Nokogiri::HTML(response.to_s) | ||||||
|     card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url) |  | ||||||
|  |  | ||||||
|  |     card.type        = :link | ||||||
|     card.title       = meta_property(page, 'og:title') || page.at_xpath('//title')&.content |     card.title       = meta_property(page, 'og:title') || page.at_xpath('//title')&.content | ||||||
|     card.description = meta_property(page, 'og:description') || meta_property(page, 'description') |     card.description = meta_property(page, 'og:description') || meta_property(page, 'description') | ||||||
|     card.image       = URI.parse(Addressable::URI.parse(meta_property(page, 'og:image')).normalize.to_s) if meta_property(page, 'og:image') |     card.image       = URI.parse(Addressable::URI.parse(meta_property(page, 'og:image')).normalize.to_s) if meta_property(page, 'og:image') | ||||||
| @ -26,12 +67,6 @@ class FetchLinkCardService < BaseService | |||||||
|     card.save_with_optional_image! |     card.save_with_optional_image! | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   private |  | ||||||
|  |  | ||||||
|   def http_client |  | ||||||
|     HTTP.headers(user_agent: USER_AGENT).timeout(:per_operation, write: 10, connect: 10, read: 10).follow |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def meta_property(html, property) |   def meta_property(html, property) | ||||||
|     html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value |     html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| class FollowRemoteAccountService < BaseService | class FollowRemoteAccountService < BaseService | ||||||
|   include OStatus2::MagicKey |   include OStatus2::MagicKey | ||||||
|  |   include HttpHelper | ||||||
|  |  | ||||||
|   DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' |   DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' | ||||||
|  |  | ||||||
| @ -73,7 +74,7 @@ class FollowRemoteAccountService < BaseService | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def get_feed(url) |   def get_feed(url) | ||||||
|     response = http_client.get(Addressable::URI.parse(url).normalize) |     response = http_client(write: 20, connect: 20, read: 50).get(Addressable::URI.parse(url).normalize) | ||||||
|     [response.to_s, Nokogiri::XML(response)] |     [response.to_s, Nokogiri::XML(response)] | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @ -98,8 +99,4 @@ class FollowRemoteAccountService < BaseService | |||||||
|   def get_profile(body, account) |   def get_profile(body, account) | ||||||
|     RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), false) |     RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), false) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def http_client |  | ||||||
|     HTTP.timeout(:per_operation, write: 20, connect: 20, read: 50) |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ class PostStatusService < BaseService | |||||||
|     process_mentions_service.call(status) |     process_mentions_service.call(status) | ||||||
|     process_hashtags_service.call(status) |     process_hashtags_service.call(status) | ||||||
|  |  | ||||||
|     LinkCrawlWorker.perform_async(status.id) |     LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text.present? | ||||||
|     DistributionWorker.perform_async(status.id) |     DistributionWorker.perform_async(status.id) | ||||||
|     Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) |     Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| object @card | object @card | ||||||
|  |  | ||||||
| attributes :url, :title, :description | attributes :url, :title, :description, :type, | ||||||
|  |            :author_name, :author_url, :provider_name, | ||||||
|  |            :provider_url, :html, :width, :height | ||||||
|  |  | ||||||
| node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } | node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ Doorkeeper.configure do | |||||||
|  |  | ||||||
|   # Reuse access token for the same resource owner within an application (disabled by default) |   # Reuse access token for the same resource owner within an application (disabled by default) | ||||||
|   # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 |   # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 | ||||||
|   # reuse_access_token |   reuse_access_token | ||||||
|  |  | ||||||
|   # Issue access tokens with refresh token (disabled by default) |   # Issue access tokens with refresh token (disabled by default) | ||||||
|   # use_refresh_token |   # use_refresh_token | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
|  |  | ||||||
| Kaminari.configure do |config| | Kaminari.configure do |config| | ||||||
|   config.default_per_page = 40 |   config.default_per_page = 40 | ||||||
|   config.window = 1 |   config.window = 1 | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								config/initializers/oembed.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								config/initializers/oembed.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | require_relative '../../app/lib/provider_discovery' | ||||||
|  | OEmbed::Providers.register_fallback(ProviderDiscovery) | ||||||
							
								
								
									
										12
									
								
								db/migrate/20170425202925_add_oembed_to_preview_cards.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20170425202925_add_oembed_to_preview_cards.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | class AddOEmbedToPreviewCards < ActiveRecord::Migration[5.0] | ||||||
|  |   def change | ||||||
|  |     add_column :preview_cards, :type, :integer, default: 0, null: false | ||||||
|  |     add_column :preview_cards, :html, :text, null: false, default: '' | ||||||
|  |     add_column :preview_cards, :author_name, :string, null: false, default: '' | ||||||
|  |     add_column :preview_cards, :author_url, :string, null: false, default: '' | ||||||
|  |     add_column :preview_cards, :provider_name, :string, null: false, default: '' | ||||||
|  |     add_column :preview_cards, :provider_url, :string, null: false, default: '' | ||||||
|  |     add_column :preview_cards, :width, :integer, default: 0, null: false | ||||||
|  |     add_column :preview_cards, :height, :integer, default: 0, null: false | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										10
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								db/schema.rb
									
									
									
									
									
								
							| @ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
|  |  | ||||||
| ActiveRecord::Schema.define(version: 20170425131920) do | ActiveRecord::Schema.define(version: 20170425202925) do | ||||||
|  |  | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
| @ -203,6 +203,14 @@ ActiveRecord::Schema.define(version: 20170425131920) do | |||||||
|     t.datetime "image_updated_at" |     t.datetime "image_updated_at" | ||||||
|     t.datetime "created_at",                      null: false |     t.datetime "created_at",                      null: false | ||||||
|     t.datetime "updated_at",                      null: false |     t.datetime "updated_at",                      null: false | ||||||
|  |     t.integer  "type",               default: 0,  null: false | ||||||
|  |     t.text     "html",               default: "", null: false | ||||||
|  |     t.string   "author_name",        default: "", null: false | ||||||
|  |     t.string   "author_url",         default: "", null: false | ||||||
|  |     t.string   "provider_name",      default: "", null: false | ||||||
|  |     t.string   "provider_url",       default: "", null: false | ||||||
|  |     t.integer  "width",              default: 0,  null: false | ||||||
|  |     t.integer  "height",             default: 0,  null: false | ||||||
|     t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree |     t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | |||||||
| @ -9,6 +9,6 @@ RSpec.describe FetchLinkCardService do | |||||||
|     status = Fabricate(:status, text: 'Check out http://example.中国') |     status = Fabricate(:status, text: 'Check out http://example.中国') | ||||||
|  |  | ||||||
|     FetchLinkCardService.new.call(status) |     FetchLinkCardService.new.call(status) | ||||||
|     expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made |     expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user