From f34185e3ded4c0d4e43232a495b860d88e71780c Mon Sep 17 00:00:00 2001 From: Emerick Rogul Date: Wed, 17 Jul 2019 15:57:20 -0400 Subject: [PATCH 1/5] Update Twitter tipping to support new Twitter site --- .../extension/brave_rewards/background.ts | 34 ++++++ .../brave_rewards/content_scripts/twitter.ts | 108 ++++++++++++++++-- .../extension/brave_rewards/manifest.json | 4 +- 3 files changed, 135 insertions(+), 11 deletions(-) diff --git a/components/brave_rewards/resources/extension/brave_rewards/background.ts b/components/brave_rewards/resources/extension/brave_rewards/background.ts index 0fa70af6374e..92610e99d40a 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) @@ -116,6 +124,10 @@ const tipRedditMedia = (mediaMetaData: RewardsTip.MediaMetaData) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { const action = typeof msg === 'string' ? msg : msg.type switch (action) { + case 'getTwitterAPICredentials': { + sendResponse(twitterAuthHeaders) + return false + } case 'tipInlineMedia': { switch (msg.mediaMetaData.mediaType) { case 'twitter': @@ -147,3 +159,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 855a73f7ef4f..2532833f9ded 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,65 @@ import { getMessage } from '../background/api/locale_api' let timeout: any = null -const getTweetMetaData = (tweet: Element): RewardsTip.MediaMetaData | 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): Promise => { + 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 => { + const mediaMetadata: RewardsTip.MediaMetaData = { + mediaType: 'twitter', + twitterName: 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 + } + return mediaMetadata + }) + .catch(error => { + console.log(`Failed to fetch tweet details for ${tweetId}: ${error.message}`) + return Promise.reject(error) + }) +} + +const getTweetMetaDataForOldTwitter = (tweet: Element): RewardsTip.MediaMetaData | null => { if (!tweet) { return null } @@ -37,7 +95,12 @@ const getTweetMetaData = (tweet: Element): RewardsTip.MediaMetaData | null => { } } -const createBraveTipAction = (tweet: Element) => { +const tipTwitterUser = (mediaMetaData: RewardsTip.MediaMetaData) => { + const msg = { type: 'tipInlineMedia', mediaMetaData } + 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' @@ -60,14 +123,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: 'tipInlineMedia', mediaMetaData: tweetMetaData } - chrome.runtime.sendMessage(msg) + if (newTwitter) { + getTweetMetaData(tweet) + .then(tweetMetaData => { + if (tweetMetaData) { + tipTwitterUser(tweetMetaData) + } + }) + .catch(error => { + console.error(`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' @@ -133,14 +211,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", From 2ca55792bdf18b75eaa3de7b20494669bcba253f Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Fri, 19 Jul 2019 12:18:07 -0700 Subject: [PATCH 2/5] Rewards Extension: reset cached twitter auth when twitter session changes --- .../extension/brave_rewards/background.ts | 35 +--------- .../brave_rewards/background/twitterAuth.ts | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 components/brave_rewards/resources/extension/brave_rewards/background/twitterAuth.ts diff --git a/components/brave_rewards/resources/extension/brave_rewards/background.ts b/components/brave_rewards/resources/extension/brave_rewards/background.ts index 92610e99d40a..1c8fd6bee9cb 100644 --- a/components/brave_rewards/resources/extension/brave_rewards/background.ts +++ b/components/brave_rewards/resources/extension/brave_rewards/background.ts @@ -5,6 +5,7 @@ import rewardsPanelActions from './background/actions/rewardsPanelActions' import './background/store' +import './background/twitterAuth' import './background/events/rewardsEvents' import './background/events/tabEvents' import batIconOn18Url from './img/rewards-on.png' @@ -19,14 +20,6 @@ const iconOn = { } } -const twitterAuthHeaders = {} - -const twitterAuthHeaderNames = [ - 'authorization', - 'x-csrf-token', - 'x-guest-token' -] - chrome.browserAction.setBadgeBackgroundColor({ color: '#FB542B' }) chrome.browserAction.setIcon(iconOn) @@ -124,10 +117,6 @@ const tipRedditMedia = (mediaMetaData: RewardsTip.MediaMetaData) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { const action = typeof msg === 'string' ? msg : msg.type switch (action) { - case 'getTwitterAPICredentials': { - sendResponse(twitterAuthHeaders) - return false - } case 'tipInlineMedia': { switch (msg.mediaMetaData.mediaType) { case 'twitter': @@ -159,25 +148,3 @@ 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/background/twitterAuth.ts b/components/brave_rewards/resources/extension/brave_rewards/background/twitterAuth.ts new file mode 100644 index 000000000000..a60648e64bd0 --- /dev/null +++ b/components/brave_rewards/resources/extension/brave_rewards/background/twitterAuth.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2019 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +type SessionId = string | null +type Header = { name: string, value: string } + +const twitterAuthHeaderNames = [ + 'authorization', + 'x-csrf-token', + 'x-guest-token' +] +const authTokenCookieRegex = /[; ]_twitter_sess=([^\s;]*)/ + +let twitterAuthHeaders = {} +let lastSessionId: SessionId = null + +function readTwitterSessionCookie (cookiesString: string): SessionId { + const match = cookiesString.match(authTokenCookieRegex) + if (match) { + return unescape(match[1]) + } + return null +} + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + const action = typeof msg === 'string' ? msg : msg.type + switch (action) { + case 'getTwitterAPICredentials': { + sendResponse(twitterAuthHeaders) + break + } + default: + break + } +}) + +// Grab auth headers from twitter's normal requests +chrome.webRequest.onSendHeaders.addListener( + // Listener + function ({ requestHeaders }) { + if (requestHeaders) { + for (const header of requestHeaders) { + // Parse cookies for session id + if (header.name === 'Cookie') { + let currentSessionId = readTwitterSessionCookie(header.value as string) + const hasAuthChanged = (currentSessionId !== lastSessionId) + if (hasAuthChanged) { + // clear cached auth data when session changes + lastSessionId = currentSessionId + twitterAuthHeaders = { } + } + } else 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', + 'extraHeaders' // need cookies + ]) From ee6a7b2640e64b576c58e0d43d5f3eb249363c4b Mon Sep 17 00:00:00 2001 From: Emerick Rogul Date: Fri, 19 Jul 2019 16:41:27 -0400 Subject: [PATCH 3/5] Update IsExcludedPath with new Twitter login/logout patterns --- .../bat-native-ledger/src/bat/ledger/internal/media/twitter.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vendor/bat-native-ledger/src/bat/ledger/internal/media/twitter.cc b/vendor/bat-native-ledger/src/bat/ledger/internal/media/twitter.cc index 20ecde84e9a0..751c673704bb 100644 --- a/vendor/bat-native-ledger/src/bat/ledger/internal/media/twitter.cc +++ b/vendor/bat-native-ledger/src/bat/ledger/internal/media/twitter.cc @@ -113,6 +113,8 @@ bool Twitter::IsExcludedPath(const std::string& path) { "/i/", "/account/", "/compose/", + "/?login", + "/?logout", "/who_to_follow/", "/hashtag/", "/settings/" From a0598fa59b559b2236305db51ccfcbde63a0916e Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Mon, 22 Jul 2019 13:08:15 -0700 Subject: [PATCH 4/5] Twitter Tips: Don't show tip button for tweets we won't be able to extract enough information from --- .../brave_rewards/content_scripts/twitter.ts | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) 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 2532833f9ded..8069b23ec3ae 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 @@ -6,6 +6,7 @@ import { getMessage } from '../background/api/locale_api' let timeout: any = null +let newTwitter = true const getTwitterAPICredentials = () => { const msg = { type: 'getTwitterAPICredentials' } @@ -29,22 +30,25 @@ const getTweetDetails = async (tweetId: string) => { return response.json() } -const getTweetMetaData = (tweet: Element): Promise => { - if (!tweet) { - return Promise.reject(null) +function getTweetId (tweet: Element) { + if (!newTwitter) { + return tweet.getAttribute('data-tweet-id') } - const status = tweet.querySelector("a[href*='/status/']") as HTMLAnchorElement if (!status || !status.href) { - return Promise.reject(null) + return null } - const tweetIdMatches = status.href.match(/status\/(\d+)/) if (!tweetIdMatches || tweetIdMatches.length < 2) { - return Promise.reject(null) + return null } + return tweetIdMatches[1] +} - const tweetId = tweetIdMatches[1] +const getTweetMetaData = (tweet: Element, tweetId: string): Promise => { + if (!tweet) { + return Promise.reject(null) + } return getTweetDetails(tweetId) .then(tweetDetails => { @@ -53,7 +57,7 @@ const getTweetMetaData = (tweet: Element): Promise => twitterName: tweetDetails.user.name, screenName: tweetDetails.user.screen_name, userId: tweetDetails.user.id_str, - tweetId: tweetId, + tweetId, tweetTimestamp: Date.parse(tweetDetails.created_at) / 1000, tweetText: tweetDetails.text } @@ -65,7 +69,7 @@ const getTweetMetaData = (tweet: Element): Promise => }) } -const getTweetMetaDataForOldTwitter = (tweet: Element): RewardsTip.MediaMetaData | null => { +const getTweetMetaDataForOldTwitter = (tweet: Element, tweetId: string): RewardsTip.MediaMetaData | null => { if (!tweet) { return null } @@ -89,7 +93,7 @@ const getTweetMetaDataForOldTwitter = (tweet: Element): RewardsTip.MediaMetaData twitterName: tweet.getAttribute('data-name') || '', screenName: tweet.getAttribute('data-screen-name') || '', userId: tweet.getAttribute('data-user-id') || '', - tweetId: tweet.getAttribute('data-tweet-id') || '', + tweetId, tweetTimestamp: parseInt(tweetTimestamp, 10) || 0, tweetText: tweetText.innerText || '' } @@ -100,7 +104,7 @@ const tipTwitterUser = (mediaMetaData: RewardsTip.MediaMetaData) => { chrome.runtime.sendMessage(msg) } -const createBraveTipAction = (tweet: Element, newTwitter: boolean) => { +const createBraveTipAction = (tweet: Element, tweetId: string) => { // Create the tip action const braveTipAction = document.createElement('div') braveTipAction.className = 'ProfileTweet-action js-tooltip action-brave-tip' @@ -124,7 +128,7 @@ const createBraveTipAction = (tweet: Element, newTwitter: boolean) => { braveTipButton.type = 'button' braveTipButton.onclick = function (event) { if (newTwitter) { - getTweetMetaData(tweet) + getTweetMetaData(tweet, tweetId) .then(tweetMetaData => { if (tweetMetaData) { tipTwitterUser(tweetMetaData) @@ -134,7 +138,7 @@ const createBraveTipAction = (tweet: Element, newTwitter: boolean) => { console.error(`Failed to fetch tweet metadata for ${tweet}:`, error) }) } else { - const tweetMetaData = getTweetMetaDataForOldTwitter(tweet) + const tweetMetaData = getTweetMetaDataForOldTwitter(tweet, tweetId) if (tweetMetaData) { tipTwitterUser(tweetMetaData) } @@ -212,26 +216,32 @@ const configureBraveTipAction = () => { } chrome.runtime.sendMessage(msg, function (inlineTip) { const tippingEnabled = rewards.enabled && inlineTip.enabled - let newTwitter = true let tweets = document.querySelectorAll('[data-testid="tweet"], [data-testid="tweetDetail"]') + // Reset page state since first run of this function may have been pre-content + newTwitter = true if (tweets.length === 0) { tweets = document.querySelectorAll('.tweet') newTwitter = false } for (let i = 0; i < tweets.length; ++i) { let actions + const tweetId = getTweetId(tweets[i]) + if (!tweetId) { + continue + } if (newTwitter) { actions = tweets[i].querySelector('[role="group"]') } else { actions = tweets[i].querySelector('.js-actions') } - if (actions) { - const braveTipActions = actions.getElementsByClassName('action-brave-tip') - if (tippingEnabled && braveTipActions.length === 0) { - actions.appendChild(createBraveTipAction(tweets[i], newTwitter)) - } else if (!tippingEnabled && braveTipActions.length === 1) { - actions.removeChild(braveTipActions[0]) - } + if (!actions) { + continue + } + const braveTipActions = actions.getElementsByClassName('action-brave-tip') + if (tippingEnabled && braveTipActions.length === 0) { + actions.appendChild(createBraveTipAction(tweets[i], tweetId)) + } else if (!tippingEnabled && braveTipActions.length === 1) { + actions.removeChild(braveTipActions[0]) } } }) From 6e61ed84754b0750723aa9548ab5bf0871640975 Mon Sep 17 00:00:00 2001 From: Emerick Rogul Date: Tue, 23 Jul 2019 04:20:21 -0400 Subject: [PATCH 5/5] Fix tests for old Twitter and add tests for new Twitter --- .../browser/rewards_service_browsertest.cc | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/components/brave_rewards/browser/rewards_service_browsertest.cc b/components/brave_rewards/browser/rewards_service_browsertest.cc index a820345da839..5803772bf9fd 100644 --- a/components/brave_rewards/browser/rewards_service_browsertest.cc +++ b/components/brave_rewards/browser/rewards_service_browsertest.cc @@ -55,7 +55,18 @@ std::unique_ptr HandleRequest( "" " " " " - "
" + "
" + " " + "
Hello, Twitter!
" + "
" + " " + ""); + } else if (request.relative_url == "/oldtwitter") { + http_response->set_content( + "" + " " + " " + "
" "
Hello, Twitter!
" "
" " " @@ -1716,6 +1727,36 @@ IN_PROC_BROWSER_TEST_F(BraveRewardsBrowserTest, EXPECT_FALSE(IsMediaTipsInjected()); } +// Brave tip icon is injected when visiting old Twitter +IN_PROC_BROWSER_TEST_F(BraveRewardsBrowserTest, + TwitterTipsInjectedOnOldTwitter) { + // Enable Rewards + EnableRewards(); + + // Navigate to Twitter in a new tab + GURL url = https_server()->GetURL("twitter.com", "/oldtwitter"); + ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); + + // Ensure that Media tips injection is active + EXPECT_TRUE(IsMediaTipsInjected()); +} + +// Brave tip icon is not injected when visiting old Twitter while +// Brave Rewards is disabled +IN_PROC_BROWSER_TEST_F(BraveRewardsBrowserTest, + TwitterTipsNotInjectedWhenRewardsDisabledOldTwitter) { + // Navigate to Twitter in a new tab + GURL url = https_server()->GetURL("twitter.com", "/oldtwitter"); + ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); + + // Ensure that Media tips injection is not active + EXPECT_FALSE(IsMediaTipsInjected()); +} + // Brave tip icon is not injected into non-Twitter sites IN_PROC_BROWSER_TEST_F(BraveRewardsBrowserTest, TwitterTipsNotInjectedOnNonTwitter) {