FIX: Disable Twitter onebox without API support (#17519)

Twitter removed OpenGraph tags from their pages. We can no longer
extract all the information (for example, the quoted tweet) we need
to render Oneboxes without using their API.
This commit is contained in:
Bianca Nenciu 2022-08-17 18:32:48 +03:00 committed by GitHub
parent 2b43238973
commit 626d50c15c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 68 additions and 111 deletions

View File

@ -10,34 +10,24 @@ module Onebox
matches_regexp(/^https?:\/\/(mobile\.|www\.)?twitter\.com\/.+?\/status(es)?\/\d+(\/(video|photo)\/\d?+)?+(\/?\?.*)?\/?$/) matches_regexp(/^https?:\/\/(mobile\.|www\.)?twitter\.com\/.+?\/status(es)?\/\d+(\/(video|photo)\/\d?+)?+(\/?\?.*)?\/?$/)
always_https always_https
def self.===(other)
!Onebox.options.twitter_client.twitter_credentials_missing? && super
end
def http_params def http_params
{ 'User-Agent' => 'DiscourseBot/1.0' } { 'User-Agent' => 'DiscourseBot/1.0' }
end end
private def to_html
raw.present? ? super : ''
end
def get_twitter_data private
response = Onebox::Helpers.fetch_response(url, headers: http_params) rescue nil
html = Nokogiri::HTML(response)
twitter_data = {}
html.css('meta').each do |m|
if m.attribute('property') && m.attribute('property').to_s.match(/^og:/i)
m_content = m.attribute('content').to_s.strip
m_property = m.attribute('property').to_s.gsub('og:', '').gsub(':', '_')
twitter_data[m_property.to_sym] = m_content
end
end
twitter_data
end
def match def match
@match ||= @url.match(%r{twitter\.com/.+?/status(es)?/(?<id>\d+)}) @match ||= @url.match(%r{twitter\.com/.+?/status(es)?/(?<id>\d+)})
end end
def twitter_data
@twitter_data ||= get_twitter_data
end
def client def client
Onebox.options.twitter_client Onebox.options.twitter_client
end end
@ -48,9 +38,7 @@ module Onebox
def raw def raw
if twitter_api_credentials_present? if twitter_api_credentials_present?
@raw ||= OpenStruct.new(client.status(match[:id]).to_hash) @raw ||= client.status(match[:id]).to_hash
else
super
end end
end end
@ -62,104 +50,56 @@ module Onebox
end end
def tweet def tweet
if twitter_api_credentials_present?
client.prettify_tweet(raw)&.strip client.prettify_tweet(raw)&.strip
else
twitter_data[:description].gsub(/“(.+?)”/im) { $1 } if twitter_data[:description]
end
end end
def timestamp def timestamp
if twitter_api_credentials_present?
date = DateTime.strptime(access(:created_at), "%a %b %d %H:%M:%S %z %Y") date = DateTime.strptime(access(:created_at), "%a %b %d %H:%M:%S %z %Y")
user_offset = access(:user, :utc_offset).to_i user_offset = access(:user, :utc_offset).to_i
offset = (user_offset >= 0 ? "+" : "-") + Time.at(user_offset.abs).gmtime.strftime("%H%M") offset = (user_offset >= 0 ? "+" : "-") + Time.at(user_offset.abs).gmtime.strftime("%H%M")
date.new_offset(offset).strftime("%-l:%M %p - %-d %b %Y") date.new_offset(offset).strftime("%-l:%M %p - %-d %b %Y")
else
attr_at_css(".tweet-timestamp", 'title')
end
end end
def title def title
if twitter_api_credentials_present?
access(:user, :name) access(:user, :name)
else
attr_at_css('.tweet.permalink-tweet', 'data-name')
end
end end
def screen_name def screen_name
if twitter_api_credentials_present?
access(:user, :screen_name) access(:user, :screen_name)
else
attr_at_css('.tweet.permalink-tweet', 'data-screen-name')
end
end end
def avatar def avatar
if twitter_api_credentials_present?
access(:user, :profile_image_url_https).sub('normal', '400x400') access(:user, :profile_image_url_https).sub('normal', '400x400')
elsif twitter_data[:image]
twitter_data[:image] unless twitter_data[:image_user_generated]
end
end end
def likes def likes
if twitter_api_credentials_present?
prettify_number(access(:favorite_count).to_i) prettify_number(access(:favorite_count).to_i)
else
attr_at_css(".request-favorited-popup", 'data-compact-localized-count')
end
end end
def retweets def retweets
if twitter_api_credentials_present?
prettify_number(access(:retweet_count).to_i) prettify_number(access(:retweet_count).to_i)
else
attr_at_css(".request-retweeted-popup", 'data-compact-localized-count')
end
end end
def quoted_full_name def quoted_full_name
if twitter_api_credentials_present?
access(:quoted_status, :user, :name) access(:quoted_status, :user, :name)
else
raw.css('.QuoteTweet-fullname')[0]&.text
end
end end
def quoted_screen_name def quoted_screen_name
if twitter_api_credentials_present?
access(:quoted_status, :user, :screen_name) access(:quoted_status, :user, :screen_name)
else
attr_at_css(".QuoteTweet-innerContainer", "data-screen-name")
end
end end
def quoted_tweet def quoted_tweet
if twitter_api_credentials_present?
access(:quoted_status, :full_text) access(:quoted_status, :full_text)
else
raw.css('.QuoteTweet-text')[0]&.text
end
end end
def quoted_link def quoted_link
if twitter_api_credentials_present?
"https://twitter.com/#{quoted_screen_name}/status/#{access(:quoted_status, :id)}" "https://twitter.com/#{quoted_screen_name}/status/#{access(:quoted_status, :id)}"
else
"https://twitter.com#{attr_at_css(".QuoteTweet-innerContainer", "href")}"
end
end end
def prettify_number(count) def prettify_number(count)
count > 0 ? client.prettify_number(count) : nil count > 0 ? client.prettify_number(count) : nil
end end
def attr_at_css(css_property, attribute_name)
raw.at_css(css_property)&.attr(attribute_name)
end
def data def data
@data ||= { @data ||= {
link: link, link: link,

View File

@ -125,7 +125,13 @@ class TwitterApi
def twitter_get(uri) def twitter_get(uri)
request = Net::HTTP::Get.new(uri) request = Net::HTTP::Get.new(uri)
request.add_field 'Authorization', "Bearer #{bearer_token}" request.add_field 'Authorization', "Bearer #{bearer_token}"
http(uri).request(request).body response = http(uri).request(request)
if response.kind_of?(Net::HTTPTooManyRequests)
Rails.logger.warn("Twitter API rate limit has been reached")
end
response.body
end end
def authorization def authorization

View File

@ -107,40 +107,28 @@ RSpec.describe Onebox::Engine::TwitterStatusOnebox do
end end
end end
context "with html" do context "without twitter client" do
context "with a standard tweet" do let(:link) { "https://twitter.com/discourse/status/1428031057186627589" }
let(:tweet_content) { "I'm a sucker for pledges." } let(:html) { described_class.new(link).to_html }
include_context "with standard tweet info" it "does not match the url" do
include_context "with engines" onebox = Onebox::Matcher.new(link, { allowed_iframe_regexes: [/.*/] }).oneboxed
expect(onebox).not_to be(described_class)
it_behaves_like "an engine"
it_behaves_like "#to_html"
end end
context "with a quoted tweet" do it "logs a warn message if rate limited" do
let(:tweet_content) do SiteSetting.twitter_consumer_key = 'twitter_consumer_key'
"Thank you to everyone who came out for #MetInParis last night for helping us support @EMMAUSolidarite &amp;" SiteSetting.twitter_consumer_secret = 'twitter_consumer_secret'
end
include_context "with quoted tweet info" stub_request(:post, "https://api.twitter.com/oauth2/token")
include_context "with engines" .to_return(status: 200, body: "{\"access_token\":\"token\"}", headers: {})
it_behaves_like "an engine" stub_request(:get, "https://api.twitter.com/1.1/statuses/show.json?id=1428031057186627589&tweet_mode=extended")
it_behaves_like '#to_html' .to_return(status: 429, body: "{}", headers: {})
it_behaves_like "includes quoted tweet data"
end
context "with a featured image tweet" do Rails.logger.expects(:warn).with(regexp_matches(/rate limit/)).at_least_once
let(:tweet_content) do
"My first text message from my child! A moment that shall live on in infamy!"
end
include_context "with featured image info" expect(html).to eq('')
include_context "with engines"
it_behaves_like "an engine"
it_behaves_like '#to_html'
end end
end end

View File

@ -508,6 +508,29 @@ RSpec.describe Oneboxer do
end end
end end
describe 'Twitter' do
let(:url) { 'https://twitter.com/discourse/status/1428031057186627589' }
before do
SiteSetting.twitter_consumer_key = 'twitter_consumer_key'
SiteSetting.twitter_consumer_secret = 'twitter_consumer_secret'
end
it 'works with rate limit' do
stub_request(:head, "https://twitter.com/discourse/status/1428031057186627589")
.to_return(status: 200, body: "", headers: {})
stub_request(:get, "https://twitter.com/discourse/status/1428031057186627589")
.to_return(status: 200, body: "", headers: {})
stub_request(:post, "https://api.twitter.com/oauth2/token")
.to_return(status: 200, body: "{access_token: 'token'}", headers: {})
expect(Oneboxer.preview(url, invalidate_oneboxes: true)).to eq('')
expect(Oneboxer.onebox(url, invalidate_oneboxes: true)).to eq('')
end
end
describe '#apply' do describe '#apply' do
it 'generates valid HTML' do it 'generates valid HTML' do
raw = "Before Onebox\nhttps://example.com\nAfter Onebox" raw = "Before Onebox\nhttps://example.com\nAfter Onebox"