diff --git a/components/brave_rewards/resources/extension/brave_rewards/background.ts b/components/brave_rewards/resources/extension/brave_rewards/background.ts index 3b9ab8d187f1..a69410d1d518 100644 --- a/components/brave_rewards/resources/extension/brave_rewards/background.ts +++ b/components/brave_rewards/resources/extension/brave_rewards/background.ts @@ -19,6 +19,14 @@ const iconOn = { } } +const twitterAuthHeaders = {} + +const twitterAuthHeaderNames = [ + 'authorization', + 'x-csrf-token', + 'x-guest-token' +] + chrome.browserAction.setBadgeBackgroundColor({ color: '#FB542B' }) chrome.browserAction.setIcon(iconOn) @@ -114,6 +122,10 @@ const tipRedditUser = (redditMetaData: RewardsTip.RedditMetaData) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { const action = typeof msg === 'string' ? msg : msg.type switch (action) { + case 'getTwitterAPICredentials': { + sendResponse(twitterAuthHeaders) + return false + } case 'tipTwitterUser': { tipTwitterUser(msg.tweetMetaData) return false @@ -142,3 +154,25 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { return false } }) + +chrome.webRequest.onSendHeaders.addListener( + // Listener + function ({ requestHeaders }) { + if (requestHeaders) { + for (const header of requestHeaders) { + if (twitterAuthHeaderNames.includes(header.name) || header.name.startsWith('x-twitter-')) { + twitterAuthHeaders[header.name] = header.value + } + } + } + }, + // Filters + { + urls: [ + 'https://api.twitter.com/1.1/*' + ] + }, + // Extra + [ + 'requestHeaders' + ]) diff --git a/components/brave_rewards/resources/extension/brave_rewards/content_scripts/twitter.ts b/components/brave_rewards/resources/extension/brave_rewards/content_scripts/twitter.ts index c29c370cc81f..f8ee9d0add3c 100644 --- a/components/brave_rewards/resources/extension/brave_rewards/content_scripts/twitter.ts +++ b/components/brave_rewards/resources/extension/brave_rewards/content_scripts/twitter.ts @@ -7,7 +7,62 @@ import { getMessage } from '../background/api/locale_api' let timeout: any = null -const getTweetMetaData = (tweet: Element): RewardsTip.TweetMetaData | null => { +const getTwitterAPICredentials = () => { + const msg = { type: 'getTwitterAPICredentials' } + return new Promise(resolve => chrome.runtime.sendMessage(msg, resolve)) +} + +const getTweetDetails = async (tweetId: string) => { + const credentialHeaders = await getTwitterAPICredentials() + const url = new URL('https://api.twitter.com/1.1/statuses/show.json') + url.searchParams.append('id', tweetId) + const response = await fetch(url.toString(), { + credentials: 'include', + headers: { + ...credentialHeaders + }, + referrerPolicy: 'no-referrer-when-downgrade', + method: 'GET', + mode: 'cors', + redirect: 'follow' + }) + return response.json() +} + +const getTweetMetaData = (tweet: Element) => { + if (!tweet) { + return Promise.reject(null) + } + + const status = tweet.querySelector("a[href*='/status/']") as HTMLAnchorElement + if (!status || !status.href) { + return Promise.reject(null) + } + + const tweetIdMatches = status.href.match(/status\/(\d+)/) + if (!tweetIdMatches || tweetIdMatches.length < 2) { + return Promise.reject(null) + } + + const tweetId = tweetIdMatches[1] + + return getTweetDetails(tweetId) + .then(tweetDetails => { + return { + name: tweetDetails.user.name, + screenName: tweetDetails.user.screen_name, + userId: tweetDetails.user.id_str, + tweetId: tweetId, + tweetTimestamp: Date.parse(tweetDetails.created_at) / 1000, + tweetText: tweetDetails.text + } + }) + .catch(error => { + console.log(`Failed to fetch tweet details for ${tweetId}: ${error}`) + }) +} + +const getTweetMetaDataForOldTwitter = (tweet: Element): RewardsTip.TweetMetaData | null => { if (!tweet) { return null } @@ -36,7 +91,12 @@ const getTweetMetaData = (tweet: Element): RewardsTip.TweetMetaData | null => { } } -const createBraveTipAction = (tweet: Element) => { +const tipTwitterUser = (tweetMetaData: RewardsTip.TweetMetaData) => { + const msg = { type: 'tipTwitterUser', tweetMetaData: tweetMetaData } + chrome.runtime.sendMessage(msg) +} + +const createBraveTipAction = (tweet: Element, newTwitter: boolean) => { // Create the tip action const braveTipAction = document.createElement('div') braveTipAction.className = 'ProfileTweet-action js-tooltip action-brave-tip' @@ -59,14 +119,29 @@ const createBraveTipAction = (tweet: Element) => { braveTipButton.style.position = 'relative' braveTipButton.type = 'button' braveTipButton.onclick = function (event) { - const tweetMetaData = getTweetMetaData(tweet) - if (tweetMetaData) { - const msg = { type: 'tipTwitterUser', tweetMetaData: tweetMetaData } - chrome.runtime.sendMessage(msg) + if (newTwitter) { + getTweetMetaData(tweet) + .then(tweetMetaData => { + if (tweetMetaData) { + tipTwitterUser(tweetMetaData) + } + }) + .catch(error => { + console.log(`Failed to fetch tweet metadata for ${tweet}: ${error}`) + }) + } else { + const tweetMetaData = getTweetMetaDataForOldTwitter(tweet) + if (tweetMetaData) { + tipTwitterUser(tweetMetaData) + } } event.stopPropagation() } + if (newTwitter && tweet && tweet.getAttribute('data-testid') === 'tweetDetail') { + braveTipButton.style.marginTop = '12px' + } + // Create the tip icon container const braveTipIconContainer = document.createElement('div') braveTipIconContainer.className = 'IconContainer js-tooltip' @@ -132,14 +207,24 @@ const configureBraveTipAction = () => { key: 'twitter' } chrome.runtime.sendMessage(msg, function (inlineTip) { - const tweets = document.getElementsByClassName('tweet') + const tippingEnabled = rewards.enabled && inlineTip.enabled + let newTwitter = true + let tweets = document.querySelectorAll('[data-testid="tweet"], [data-testid="tweetDetail"]') + if (tweets.length === 0) { + tweets = document.querySelectorAll('.tweet') + newTwitter = false + } for (let i = 0; i < tweets.length; ++i) { - const actions = tweets[i].getElementsByClassName('js-actions')[0] + let actions + if (newTwitter) { + actions = tweets[i].querySelector('[role="group"]') + } else { + actions = tweets[i].querySelector('.js-actions') + } if (actions) { const braveTipActions = actions.getElementsByClassName('action-brave-tip') - const tippingEnabled = rewards.enabled && inlineTip.enabled if (tippingEnabled && braveTipActions.length === 0) { - actions.appendChild(createBraveTipAction(tweets[i])) + actions.appendChild(createBraveTipAction(tweets[i], newTwitter)) } else if (!tippingEnabled && braveTipActions.length === 1) { actions.removeChild(braveTipActions[0]) } diff --git a/components/brave_rewards/resources/extension/brave_rewards/manifest.json b/components/brave_rewards/resources/extension/brave_rewards/manifest.json index d45ac5b72e88..48cc316c9e81 100644 --- a/components/brave_rewards/resources/extension/brave_rewards/manifest.json +++ b/components/brave_rewards/resources/extension/brave_rewards/manifest.json @@ -12,8 +12,10 @@ "permissions": [ "storage", "tabs", + "webRequest", "chrome://favicon/*", - "https://www.twitch.tv/*" + "https://www.twitch.tv/*", + "https://*.twitter.com/*" ], "browser_action": { "default_popup": "brave_rewards_panel.html",