# frozen_string_literal: true module Onebox module Engine class TwitterStatusOnebox include Engine include LayoutSupport include HTML include ActionView::Helpers::NumberHelper matches_regexp( %r{^https?://(mobile\.|www\.)?(twitter\.com|x\.com)/.+?/status(es)?/\d+(/(video|photo)/\d?+)?+(/?\?.*)?/?$}, ) always_https def http_params { "User-Agent" => "DiscourseBot/1.0" } end def to_html raw.present? ? super : "" end private def get_twitter_data response = begin # 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, ) 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 def match @match ||= @url.match(%r{(twitter\.com|x\.com)/.+?/status(es)?/(?\d+)}) end 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 def client Onebox.options.twitter_client end def twitter_api_credentials_present? client && !client.twitter_credentials_missing? end 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 end def raw if twitter_api_credentials_present? @raw ||= symbolize_keys(client.status(match[:id])) else super end end def tweet if twitter_api_credentials_present? client.prettify_tweet(raw)&.strip else twitter_data[:description].gsub(/“(.+?)”/im) { $1 } if twitter_data[:description] end end def timestamp 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 end def title if twitter_api_credentials_present? raw.dig(:includes, :users)&.first&.dig(:name) else twitter_data[:title] end end def screen_name if twitter_api_credentials_present? raw.dig(:includes, :users)&.first&.dig(:username) else twitter_data[:title][/\(@([^\)\(]*)\) on X/, 1] if twitter_data[:title].present? end end def avatar if twitter_api_credentials_present? raw.dig(:includes, :users)&.first&.dig(:profile_image_url) else twitter_data[:image] if twitter_data[:image]&.include?("profile_images") end end def likes if twitter_api_credentials_present? prettify_number(raw.dig(:data, :public_metrics, :like_count).to_i) end end def retweets if twitter_api_credentials_present? prettify_number(raw.dig(:data, :public_metrics, :retweet_count).to_i) end end def is_reply if twitter_api_credentials_present? raw.dig(:data, :referenced_tweets)&.any? { |tweet| tweet.dig(:type) == "replied_to" } end end def quoted_full_name if twitter_api_credentials_present? && quoted_tweet_author.present? quoted_tweet_author[:name] end end def quoted_screen_name if twitter_api_credentials_present? && quoted_tweet_author.present? quoted_tweet_author[:username] end end def quoted_text quoted_tweet[:text] if twitter_api_credentials_present? && quoted_tweet.present? end def quoted_link 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) } end def prettify_number(count) 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 end def data @data ||= { link: link, tweet: tweet, timestamp: timestamp, title: title, screen_name: screen_name, avatar: avatar, likes: likes, retweets: retweets, is_reply: is_reply, quoted_text: quoted_text, quoted_full_name: quoted_full_name, quoted_screen_name: quoted_screen_name, quoted_link: quoted_link, } end end end end