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 controllergh/stable
parent
be0a01145b
commit
88725d6ce8
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) {
|
let image = '';
|
||||||
return null;
|
let provider = card.get('provider_name');
|
||||||
}
|
|
||||||
|
|
||||||
let image = '';
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
StreamEntry.find(params[:id])
|
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])
|
||||||
|
else
|
||||||
|
Status.find(params[:id]).stream_entry
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../../app/lib/provider_discovery'
|
||||||
|
OEmbed::Providers.register_fallback(ProviderDiscovery)
|
|
@ -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
|
12
db/schema.rb
12
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
|
||||||
|
|
||||||
|
@ -333,4 +341,4 @@ ActiveRecord::Schema.define(version: 20170425131920) do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade
|
add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade
|
||||||
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