Skip to content

Commit

Permalink
Merge pull request #2943 from brave/twitter-tips-new-twitter
Browse files Browse the repository at this point in the history
Support Tips for new Twitter site
  • Loading branch information
emerick authored Jul 23, 2019
2 parents 24c51cd + 6e61ed8 commit 9258a23
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 19 deletions.
43 changes: 42 additions & 1 deletion components/brave_rewards/browser/rewards_service_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
"<html>"
" <head></head>"
" <body>"
" <div class='tweet'>"
" <div data-testid='tweet' data-tweet-id='123'>"
" <a href='/status/123'></a>"
" <div role='group'>Hello, Twitter!</div>"
" </div>"
" </body>"
"</html>");
} else if (request.relative_url == "/oldtwitter") {
http_response->set_content(
"<html>"
" <head></head>"
" <body>"
" <div class='tweet' data-tweet-id='123'>"
" <div class='js-actions'>Hello, Twitter!</div>"
" </div>"
" </body>"
Expand Down Expand Up @@ -1819,6 +1830,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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
])
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,70 @@
import { getMessage } from '../background/api/locale_api'

let timeout: any = null
let newTwitter = true

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()
}

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 null
}
const tweetIdMatches = status.href.match(/status\/(\d+)/)
if (!tweetIdMatches || tweetIdMatches.length < 2) {
return null
}
return tweetIdMatches[1]
}

const getTweetMetaData = (tweet: Element, tweetId: string): Promise<RewardsTip.MediaMetaData> => {
if (!tweet) {
return Promise.reject(null)
}

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,
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, tweetId: string): RewardsTip.MediaMetaData | null => {
if (!tweet) {
return null
}
Expand All @@ -31,13 +93,18 @@ const getTweetMetaData = (tweet: Element): RewardsTip.MediaMetaData | null => {
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 || ''
}
}

const createBraveTipAction = (tweet: Element) => {
const tipTwitterUser = (mediaMetaData: RewardsTip.MediaMetaData) => {
const msg = { type: 'tipInlineMedia', mediaMetaData }
chrome.runtime.sendMessage(msg)
}

const createBraveTipAction = (tweet: Element, tweetId: string) => {
// Create the tip action
const braveTipAction = document.createElement('div')
braveTipAction.className = 'ProfileTweet-action js-tooltip action-brave-tip'
Expand All @@ -60,14 +127,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, tweetId)
.then(tweetMetaData => {
if (tweetMetaData) {
tipTwitterUser(tweetMetaData)
}
})
.catch(error => {
console.error(`Failed to fetch tweet metadata for ${tweet}:`, error)
})
} else {
const tweetMetaData = getTweetMetaDataForOldTwitter(tweet, tweetId)
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'
Expand Down Expand Up @@ -133,17 +215,33 @@ const configureBraveTipAction = () => {
key: 'twitter'
}
chrome.runtime.sendMessage(msg, function (inlineTip) {
const tweets = document.getElementsByClassName('tweet')
const tippingEnabled = rewards.enabled && inlineTip.enabled
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) {
const actions = tweets[i].getElementsByClassName('js-actions')[0]
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]))
} else if (!tippingEnabled && braveTipActions.length === 1) {
actions.removeChild(braveTipActions[0])
}
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) {
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])
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ bool Twitter::IsExcludedPath(const std::string& path) {
"/i/",
"/account/",
"/compose/",
"/?login",
"/?logout",
"/who_to_follow/",
"/hashtag/",
"/settings/"
Expand Down

0 comments on commit 9258a23

Please sign in to comment.