2021-05-26 17:41:35 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Onebox
|
|
|
|
module Engine
|
|
|
|
class TwitterStatusOnebox
|
|
|
|
include Engine
|
|
|
|
include LayoutSupport
|
|
|
|
include HTML
|
2023-06-23 01:39:02 +08:00
|
|
|
include ActionView::Helpers::NumberHelper
|
2021-05-26 17:41:35 +08:00
|
|
|
|
|
|
|
matches_regexp(
|
2024-05-23 21:25:42 +08:00
|
|
|
%r{^https?://(mobile\.|www\.)?(twitter\.com|x\.com)/.+?/status(es)?/\d+(/(video|photo)/\d?+)?+(/?\?.*)?/?$},
|
2021-05-26 17:41:35 +08:00
|
|
|
)
|
|
|
|
always_https
|
|
|
|
|
|
|
|
def http_params
|
|
|
|
{ "User-Agent" => "DiscourseBot/1.0" }
|
|
|
|
end
|
|
|
|
|
2022-08-17 23:32:48 +08:00
|
|
|
def to_html
|
|
|
|
raw.present? ? super : ""
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
2022-08-17 23:32:48 +08:00
|
|
|
private
|
|
|
|
|
2023-06-23 01:39:02 +08:00
|
|
|
def get_twitter_data
|
|
|
|
response =
|
|
|
|
begin
|
2024-05-23 21:25:42 +08:00
|
|
|
# We need to allow cross domain cookies to prevent an
|
|
|
|
# infinite redirect loop between twitter.com and x.com
|
|
|
|
Onebox::Helpers.fetch_response(
|
|
|
|
url,
|
|
|
|
headers: http_params,
|
|
|
|
allow_cross_domain_cookies: true,
|
|
|
|
)
|
2023-06-23 01:39:02 +08:00
|
|
|
rescue StandardError
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
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
|
|
|
|
|
2021-05-26 17:41:35 +08:00
|
|
|
def match
|
2024-05-23 21:25:42 +08:00
|
|
|
@match ||= @url.match(%r{(twitter\.com|x\.com)/.+?/status(es)?/(?<id>\d+)})
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
2023-06-23 01:39:02 +08:00
|
|
|
def twitter_data
|
|
|
|
@twitter_data ||= get_twitter_data
|
|
|
|
end
|
|
|
|
|
|
|
|
def guess_tweet_index
|
|
|
|
usernames = meta_tags_data("additionalName").compact
|
|
|
|
usernames.each_with_index do |username, index|
|
|
|
|
return index if twitter_data[:url].to_s.include?(username)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def tweet_index
|
|
|
|
@tweet_index ||= guess_tweet_index
|
|
|
|
end
|
|
|
|
|
2021-05-26 17:41:35 +08:00
|
|
|
def client
|
|
|
|
Onebox.options.twitter_client
|
|
|
|
end
|
|
|
|
|
|
|
|
def twitter_api_credentials_present?
|
|
|
|
client && !client.twitter_credentials_missing?
|
|
|
|
end
|
|
|
|
|
2023-06-23 01:39:02 +08:00
|
|
|
def symbolize_keys(obj)
|
|
|
|
case obj
|
|
|
|
when Array
|
|
|
|
obj.map { |item| symbolize_keys(item) }
|
|
|
|
when Hash
|
|
|
|
obj.each_with_object({}) do |(key, value), result|
|
|
|
|
result[key.to_sym] = symbolize_keys(value)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
obj
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
2023-06-23 01:39:02 +08:00
|
|
|
def raw
|
|
|
|
if twitter_api_credentials_present?
|
|
|
|
@raw ||= symbolize_keys(client.status(match[:id]))
|
|
|
|
else
|
|
|
|
super
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def tweet
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
client.prettify_tweet(raw)&.strip
|
|
|
|
else
|
|
|
|
twitter_data[:description].gsub(/“(.+?)”/im) { $1 } if twitter_data[:description]
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def timestamp
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present? && (created_at = raw.dig(:data, :created_at))
|
|
|
|
date = DateTime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%L%z")
|
|
|
|
date.strftime("%-l:%M %p - %-d %b %Y")
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def title
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
raw.dig(:includes, :users)&.first&.dig(:name)
|
|
|
|
else
|
2024-05-14 23:41:53 +08:00
|
|
|
twitter_data[:title]
|
2023-06-23 01:39:02 +08:00
|
|
|
end
|
2021-05-27 13:35:32 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def screen_name
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
raw.dig(:includes, :users)&.first&.dig(:username)
|
|
|
|
else
|
2024-05-14 23:41:53 +08:00
|
|
|
twitter_data[:title][/\(@([^\)\(]*)\) on X/, 1] if twitter_data[:title].present?
|
2023-06-23 01:39:02 +08:00
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def avatar
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
raw.dig(:includes, :users)&.first&.dig(:profile_image_url)
|
2024-05-14 23:41:53 +08:00
|
|
|
else
|
|
|
|
twitter_data[:image] if twitter_data[:image]&.include?("profile_images")
|
2023-06-23 01:39:02 +08:00
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def likes
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
prettify_number(raw.dig(:data, :public_metrics, :like_count).to_i)
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def retweets
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
prettify_number(raw.dig(:data, :public_metrics, :retweet_count).to_i)
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
2023-07-04 06:53:12 +08:00
|
|
|
def is_reply
|
|
|
|
if twitter_api_credentials_present?
|
|
|
|
raw.dig(:data, :referenced_tweets)&.any? { |tweet| tweet.dig(:type) == "replied_to" }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-26 17:41:35 +08:00
|
|
|
def quoted_full_name
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present? && quoted_tweet_author.present?
|
|
|
|
quoted_tweet_author[:name]
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_screen_name
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present? && quoted_tweet_author.present?
|
|
|
|
quoted_tweet_author[:username]
|
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
2023-06-23 01:39:02 +08:00
|
|
|
def quoted_text
|
|
|
|
quoted_tweet[:text] if twitter_api_credentials_present? && quoted_tweet.present?
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_link
|
2023-06-23 01:39:02 +08:00
|
|
|
if twitter_api_credentials_present?
|
|
|
|
"https://twitter.com/#{quoted_screen_name}/status/#{quoted_status_id}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_status_id
|
|
|
|
raw.dig(:data, :referenced_tweets)&.find { |ref| ref[:type] == "quoted" }&.dig(:id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_tweet
|
|
|
|
raw.dig(:includes, :tweets)&.find { |tweet| tweet[:id] == quoted_status_id }
|
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_tweet_author
|
|
|
|
raw.dig(:includes, :users)&.find { |user| user[:id] == quoted_tweet&.dig(:author_id) }
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def prettify_number(count)
|
2023-06-23 01:39:02 +08:00
|
|
|
if count > 0
|
|
|
|
number_to_human(
|
|
|
|
count,
|
|
|
|
format: "%n%u",
|
|
|
|
precision: 2,
|
|
|
|
units: {
|
|
|
|
thousand: "K",
|
|
|
|
million: "M",
|
|
|
|
billion: "B",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def attr_at_css(css_property, attribute_name)
|
|
|
|
raw.at_css(css_property)&.attr(attribute_name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def meta_tags_data(attribute_name)
|
|
|
|
data = []
|
|
|
|
raw
|
|
|
|
.css("meta")
|
|
|
|
.each do |m|
|
|
|
|
if m.attribute("itemprop") && m.attribute("itemprop").to_s.strip == attribute_name
|
|
|
|
data.push(m.attribute("content").to_s.strip)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
data
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def data
|
|
|
|
@data ||= {
|
|
|
|
link: link,
|
|
|
|
tweet: tweet,
|
|
|
|
timestamp: timestamp,
|
|
|
|
title: title,
|
2021-05-27 13:35:32 +08:00
|
|
|
screen_name: screen_name,
|
2021-05-26 17:41:35 +08:00
|
|
|
avatar: avatar,
|
|
|
|
likes: likes,
|
|
|
|
retweets: retweets,
|
2023-07-04 06:53:12 +08:00
|
|
|
is_reply: is_reply,
|
2023-06-23 01:39:02 +08:00
|
|
|
quoted_text: quoted_text,
|
2021-05-26 17:41:35 +08:00
|
|
|
quoted_full_name: quoted_full_name,
|
|
|
|
quoted_screen_name: quoted_screen_name,
|
|
|
|
quoted_link: quoted_link,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|