diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index b770e693948..c813b5f3964 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -8,6 +8,7 @@ const acorn = require('acorn') const moment = require('moment') const Immutable = require('immutable') const electron = require('electron') +const ipc = electron.ipcMain const path = require('path') const os = require('os') const qr = require('qr-image') @@ -29,6 +30,7 @@ const migrationState = require('../../common/state/migrationState') // Constants const settings = require('../../../js/constants/settings') +const messages = require('../../../js/constants/messages') // Utils const tabs = require('../../browser/tabs') @@ -42,6 +44,7 @@ const ledgerUtil = require('../../common/lib/ledgerUtil') const tabState = require('../../common/state/tabState') const pageDataUtil = require('../../common/lib/pageDataUtil') const ledgerNotifications = require('./ledgerNotifications') +const ledgerVideoCache = require('../../common/cache/ledgerVideoCache') // Caching let locationDefault = 'NOOP' @@ -107,6 +110,38 @@ underscore.keys(fileTypes).forEach((fileType) => { }) signatureMax = Math.ceil(signatureMax * 1.5) +if (ipc) { + ipc.on(messages.LEDGER_PUBLISHER, (event, location) => { + if (!synopsis || event.sender.session === electron.session.fromPartition('default') || !tldjs.isValid(location)) { + event.returnValue = {} + return + } + + let ctx = urlParse(location, true) + ctx.TLD = tldjs.getPublicSuffix(ctx.host) + if (!ctx.TLD) { + if (_internal.verboseP) console.log('\nno TLD for:' + ctx.host) + event.returnValue = {} + return + } + + ctx = underscore.mapObject(ctx, function (value) { + if (!underscore.isFunction(value)) return value + }) + ctx.URL = location + ctx.SLD = tldjs.getDomain(ctx.host) + ctx.RLD = tldjs.getSubdomain(ctx.host) + ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' + + if (!event.sender.isDestroyed()) { + event.sender.send(messages.LEDGER_PUBLISHER_RESPONSE + '-' + location, { + context: ctx, + rules: _internal.ruleset.cooked + }) + } + }) +} + let ledgerPaymentsPresent = {} const paymentPresent = (state, tabId, present) => { if (present) { @@ -247,7 +282,8 @@ const getPublisherData = (result, scorekeeper) => { let data = { verified: result.options.verified || false, exclude: result.options.exclude || false, - site: result.publisherKey, + publisherKey: result.publisherKey, + siteName: result.publisherKey, views: result.visits, duration: duration, daysSpent: 0, @@ -259,9 +295,16 @@ const getPublisherData = (result, scorekeeper) => { pinPercentage: result.pinPercentage, weight: result.pinPercentage } - // HACK: Protocol is sometimes blank here, so default to http:// so we can - // still generate publisherURL. - data.publisherURL = (result.protocol || 'http:') + '//' + result.publisherKey + + data.publisherURL = result.publisherURL || ((result.protocol || 'https:') + '//' + result.publisherKey) + + // media publisher + if (result.faviconName) { + data.siteName = locale.translation('publisherMediaName', { + publisherName: result.faviconName, + provider: result.providerName + }) + } if (duration >= miliseconds.day) { data.daysSpent = Math.max(Math.round(duration / miliseconds.day), 1) @@ -543,7 +586,7 @@ const excludeP = (publisherKey, callback) => { return done(err, result) } - let props = ledgerPublisher.getPublisherProps('https://' + publisherKey) + let props = ledgerPublisher.getPublisherProps(publisherKey) if (!props) return done() v2RulesetDB.createReadStream({lt: 'domain:'}).on('data', (data) => { @@ -584,30 +627,26 @@ const excludeP = (publisherKey, callback) => { }) } -const addVisit = (state, startTimestamp, location, tabId) => { +const addSiteVisit = (state, timestamp, location, tabId) => { if (!synopsis) { return state } location = pageDataUtil.getInfoKey(location) const locationData = ledgerState.getLocation(state, location) - const timestamp = new Date().getTime() + const duration = new Date().getTime() - timestamp if (_internal.verboseP) { console.log( `locations[${location}]=${JSON.stringify(locationData, null, 2)} ` + - `duration=${(timestamp - startTimestamp)} msec tabId= ${tabId}` + `duration=${(duration)} msec tabId= ${tabId}` ) } - if (locationData.isEmpty() || !tabId) { - return state - } - let publisherKey = locationData.get('publisher') - if (!publisherKey) { + if (locationData.isEmpty()) { return state } - let duration = timestamp - startTimestamp + let publisherKey = locationData.get('publisher') let revisitP = false if (duration >= getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME)) { @@ -627,13 +666,19 @@ const addVisit = (state, startTimestamp, location, tabId) => { } } + return saveVisit(state, publisherKey, duration, revisitP) +} + +const saveVisit = (state, publisherKey, duration, revisited) => { + if (!synopsis || !publisherKey) { + return state + } + if (_internal.verboseP) { - console.log('\nadd publisher ' + publisherKey + ': ' + (duration / 1000) + ' sec' + ' revisitP=' + revisitP + ' state=' + - JSON.stringify(underscore.extend({location: location}, visitsByPublisher[publisherKey][location]), - null, 2)) + console.log('\nadd publisher ' + publisherKey + ': ' + (duration / 1000) + ' sec' + ' revisitP=' + revisited) } - synopsis.addPublisher(publisherKey, {duration: duration, revisitP: revisitP}) + synopsis.addPublisher(publisherKey, {duration: duration, revisitP: revisited}) state = ledgerState.setPublisher(state, publisherKey, synopsis.publishers[publisherKey]) state = updatePublisherInfo(state) state = checkVerifiedStatus(state, publisherKey) @@ -684,7 +729,7 @@ const addNewLocation = (state, location, tabId = tabState.TAB_ID_NONE, keepInfo // Add visit to the ledger when we are not in a private tab if (!isPrivate && !tabFromState.isEmpty() && ledgerUtil.shouldTrackView(tabFromState)) { - state = addVisit(state, currentTimestamp, currentUrl, currentTabId) + state = addSiteVisit(state, currentTimestamp, currentUrl, currentTabId) } } @@ -1130,21 +1175,12 @@ const cacheRuleSet = (state, ruleset) => { for (let item of publishers) { const publisherKey = item[0] const publisher = item[1] - const location = (publisher.get('protocol') || 'http:') + '//' + publisherKey - let ctx = urlParse(location) + const ctx = ledgerPublisher.getPublisherProps(publisherKey) - ctx.TLD = tldjs.getPublicSuffix(ctx.host) - if (!ctx.TLD) { - return state - } + if (!ctx.TLD) continue - ctx = underscore.mapObject(ctx, function (value) { - if (!underscore.isFunction(value)) return value - }) - ctx.URL = location - ctx.SLD = tldjs.getDomain(ctx.host) - ctx.RLD = tldjs.getSubdomain(ctx.host) - ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' + if (publisher.publisherURL) ctx.URL = publisher.publisherURL + if (!ctx.URL) ctx.URL = (publisher.get('protocol') || 'https:') + '//' + publisherKey stewed.forEach((rule) => { if (rule.consequent !== null || rule.dom) return @@ -1178,7 +1214,8 @@ const roundtrip = (params, options, callback) => { let parts = typeof params.server === 'string' ? urlParse(params.server) : typeof params.server !== 'undefined' ? params.server : typeof options.server === 'string' ? urlParse(options.server) : options.server - const rawP = options.rawP + const binaryP = options.binaryP + const rawP = binaryP || options.rawP if (!params.method) params.method = 'GET' parts = underscore.extend(underscore.pick(parts, ['protocol', 'hostname', 'port']), @@ -1205,7 +1242,7 @@ const roundtrip = (params, options, callback) => { url: urlFormat(parts), method: params.method, payload: params.payload, - responseType: 'text', + responseType: binaryP ? 'binary' : 'text', headers: underscore.defaults(params.headers || {}, {'content-type': 'application/json; charset=utf-8'}), verboseP: options.verboseP } @@ -1220,7 +1257,7 @@ const roundtrip = (params, options, callback) => { console.log('>>> ' + header + ': ' + response.headers[header]) }) console.log('>>>') - console.log('>>> ' + (body || '').split('\n').join('\n>>> ')) + console.log('>>> ' + (rawP ? '...' : (body || '').split('\n').join('\n>>> '))) } if (err) return callback(err, response) @@ -2155,6 +2192,117 @@ const transitionWalletToBat = () => { } } +let currentMediaKey = null +const onMediaRequest = (state, xhr, type, tabId) => { + if (!xhr || type == null) { + return state + } + + const parsed = ledgerUtil.getMediaData(xhr, type) + const mediaId = ledgerUtil.getMediaId(parsed, type) + const mediaKey = ledgerUtil.getMediaKey(mediaId, type) + let duration = ledgerUtil.getMediaDuration(parsed, type) + + if (mediaId == null || duration == null || mediaKey == null) { + return state + } + + const minDuration = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) + duration = parseInt(duration) + if (duration > 0 && duration < minDuration) { + duration = minDuration + } + + if (!ledgerPublisher) { + ledgerPublisher = require('bat-publisher') + } + + let revisited = true + const activeTabId = tabState.getActiveTabId(state) + if (activeTabId === tabId && mediaKey !== currentMediaKey) { + revisited = false + currentMediaKey = mediaKey + } + + const cache = ledgerVideoCache.getDataByVideoId(state, mediaKey) + + if (!cache.isEmpty()) { + return module.exports.saveVisit(state, cache.get('publisher'), duration, revisited) + } + + const options = underscore.extend({roundtrip: roundtrip}, clientOptions) + const mediaProps = { + mediaId, + providerName: type + } + + ledgerPublisher.getMedia.getPublisherFromMediaProps(mediaProps, options, (error, response) => { + if (error) { + console.error('Error while getting publisher from media', error.toString()) + return + } + + // publisher not found + if (!response) { + return + } + + appActions.onLedgerMediaPublisher(mediaKey, response, duration, revisited) + }) + + return state +} + +const onMediaPublisher = (state, mediaKey, response, duration, revisited) => { + const publisherKey = response ? response.get('publisher') : null + if (publisherKey == null) { + return state + } + + let publisher = ledgerState.getPublisher(state, publisherKey) + const faviconName = response.get('faviconName') + const faviconURL = response.get('faviconURL') + const publisherURL = response.get('publisherURL') + + if (publisher.isEmpty()) { + revisited = false + synopsis.initPublisher(publisherKey) + + synopsis.publishers[publisherKey].faviconName = faviconName + synopsis.publishers[publisherKey].faviconURL = faviconURL + synopsis.publishers[publisherKey].publisherURL = publisherURL + synopsis.publishers[publisherKey].providerName = response.get('providerName') + + if (synopsis.publishers[publisherKey]) { + state = ledgerState.setPublisher(state, publisherKey, synopsis.publishers[publisherKey]) + } + + if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', true) + savePublisherOption(publisherKey, 'exclude', true) + } else { + excludeP(publisherKey, (unused, exclude) => { + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + savePublisherOption(publisherKey, 'exclude', exclude) + }) + } + } else { + synopsis.publishers[publisherKey].faviconName = faviconName + synopsis.publishers[publisherKey].faviconURL = faviconURL + synopsis.publishers[publisherKey].publisherURL = publisherURL + state = ledgerState.setPublishersProp(state, publisherKey, 'faviconName', faviconName) + state = ledgerState.setPublishersProp(state, publisherKey, 'faviconURL', faviconURL) + state = ledgerState.setPublishersProp(state, publisherKey, 'publisherURL', publisherURL) + } + + // Add to cache + state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, response) + + state = module.exports.saveVisit(state, publisherKey, duration, revisited) + + return state +} + const getMethods = () => { const publicMethods = { backupKeys, @@ -2189,7 +2337,10 @@ const getMethods = () => { getNewClient, savePublisherData, pruneSynopsis, - checkBtcBatMigrated + checkBtcBatMigrated, + onMediaRequest, + onMediaPublisher, + saveVisit } let privateMethods = {} @@ -2197,7 +2348,8 @@ const getMethods = () => { if (process.env.NODE_ENV === 'test') { privateMethods = { enable, - addVisit, + addSiteVisit, + checkBtcBatMigrated, clearVisitsByPublisher: function () { visitsByPublisher = {} }, @@ -2217,6 +2369,10 @@ const getMethods = () => { setClient: (data) => { client = data }, + setCurrentMediaKey: (key) => { + currentMediaKey = key + }, + getCurrentMediaKey: (key) => currentMediaKey, synopsisNormalizer, checkVerifiedStatus } diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index fd6a066607d..f2c5e777d2e 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -404,6 +404,22 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerState.setLedgerValue(state, 'publisherTimestamp', action.get('timestamp')) break } + case appConstants.APP_ON_LEDGER_MEDIA_DATA: + { + state = ledgerApi.onMediaRequest(state, action.get('url'), action.get('type'), action.get('tabId')) + break + } + case appConstants.APP_ON_LEDGER_MEDIA_PUBLISHER: + { + state = ledgerApi.onMediaPublisher( + state, + action.get('mediaKey'), + action.get('response'), + action.get('duration'), + action.get('revisited') + ) + break + } } return state } diff --git a/app/common/cache/ledgerVideoCache.js b/app/common/cache/ledgerVideoCache.js new file mode 100644 index 00000000000..c7f0ae3fcb1 --- /dev/null +++ b/app/common/cache/ledgerVideoCache.js @@ -0,0 +1,39 @@ +/* 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/. */ + +const Immutable = require('immutable') +const assert = require('assert') + +const { makeImmutable, isMap } = require('../state/immutableUtil') + +const validateState = function (state) { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.getIn(['cache', 'ledgerVideos'])), 'state must contain ledgerVideos as Immutable.Map') + return state +} + +const getDataByVideoId = (state, key) => { + state = validateState(state) + if (key == null) { + return Immutable.Map() + } + + return state.getIn(['cache', 'ledgerVideos', key]) || Immutable.Map() +} + +const setCacheByVideoId = (state, key, data) => { + state = validateState(state) + if (key == null) { + return state + } + + data = makeImmutable(data) + + return state.setIn(['cache', 'ledgerVideos', key], data) +} + +module.exports = { + getDataByVideoId, + setCacheByVideoId +} diff --git a/app/common/constants/ledgerMediaProviders.js b/app/common/constants/ledgerMediaProviders.js new file mode 100644 index 00000000000..84ef2d1af80 --- /dev/null +++ b/app/common/constants/ledgerMediaProviders.js @@ -0,0 +1,9 @@ +/* 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/. */ + +const providers = { + YOUTUBE: 'youtube' +} + +module.exports = providers diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index 207141aee72..c7b473c541b 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -7,6 +7,7 @@ const Immutable = require('immutable') const moment = require('moment') const BigNumber = require('bignumber.js') +const queryString = require('querystring') // State const siteSettingsState = require('../state/siteSettingsState') @@ -14,11 +15,13 @@ const ledgerState = require('../state/ledgerState') // Constants const settings = require('../../../js/constants/settings') +const ledgerMediaProviders = require('../constants/ledgerMediaProviders') // Utils const {responseHasContent} = require('./httpUtil') const urlUtil = require('../../../js/lib/urlutil') const getSetting = require('../../../js/settings').getSetting +const urlParse = require('../urlParse') /** * Is page an actual page being viewed by the user? (not an error page, etc) @@ -188,16 +191,136 @@ const stickyP = (state, publisherKey) => { return (result === undefined || result) } -module.exports = { - shouldTrackView, - batToCurrencyString, - formattedTimeFromNow, - formattedDateFromTimestamp, - walletStatus, - blockedP, - contributeP, - visibleP, - eligibleP, - stickyP, - formatCurrentBalance +const getMediaId = (data, type) => { + let id = null + + if (type == null || data == null) { + return id + } + + switch (type) { + case ledgerMediaProviders.YOUTUBE: + { + id = data.docid + break + } + } + + return id +} + +const getMediaKey = (id, type) => { + if (id == null || type == null) { + return null + } + + return `${type.toLowerCase()}_${id}` +} + +const getMediaData = (xhr, type) => { + let result = null + + if (xhr == null || type == null) { + return result + } + + switch (type) { + case ledgerMediaProviders.YOUTUBE: + { + const parsedUrl = urlParse(xhr) + let query = null + + if (parsedUrl && parsedUrl.query) { + query = queryString.parse(parsedUrl.query) + } + result = query + break + } + } + + return result +} + +const getMediaDuration = (data, type) => { + let duration = 0 + switch (type) { + case ledgerMediaProviders.YOUTUBE: { + duration = getYouTubeDuration(data) + break + } + } + + return duration +} + +const getYouTubeDuration = (data) => { + let time = 0 + + if (data == null || data.st == null || data.et == null) { + return time + } + + const startTime = data.st.split(',') + const endTime = data.et.split(',') + + if (startTime.length !== endTime.length) { + return time + } + + for (let i = 0; i < startTime.length; i++) { + time += parseFloat(endTime[i]) - parseFloat(startTime[i]) + } + + // we get seconds back, so we need to convert it into ms + time = time * 1000 + + return parseInt(time) +} + +const getMediaProvider = (url) => { + let provider = null + + if (url == null) { + return provider + } + + // Youtube + if (url.startsWith('https://www.youtube.com/api/stats/watchtime?')) { + provider = ledgerMediaProviders.YOUTUBE + } + + return provider +} + +const getMethods = () => { + const publicMethods = { + shouldTrackView, + batToCurrencyString, + formattedTimeFromNow, + formattedDateFromTimestamp, + walletStatus, + blockedP, + contributeP, + visibleP, + eligibleP, + stickyP, + formatCurrentBalance, + getMediaId, + getMediaDuration, + getMediaProvider, + getMediaData, + getMediaKey + } + + let privateMethods = {} + + if (process.env.NODE_ENV === 'test') { + privateMethods = { + getYouTubeDuration + } + } + + return Object.assign({}, publicMethods, privateMethods) } + +module.exports = getMethods() diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index d651df23a69..c8bfe82910d 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -4,6 +4,7 @@ accountBalanceConnectionError=Please check your Internet connection. actions=Actions add=Fund with debit/credit addFundsHeader=Add funds to your Brave Wallet +allowMediaPublishers=Allow contributions to videos upholdFooterText1=All transactions are processed by Uphold. upholdFooterText2=You can also use Uphold to easily fund your Brave wallet. learnMore=Learn more... @@ -248,6 +249,7 @@ privateData=Private Data privateDataMessage=Clear the following data types when I close Brave protocolRegistrationPermission=Protocol registration publisher=Site +publisherMediaName={{publisherName}} on {{provider}} publishers=Publishers rank=Rank receiptLink=Receipt Link diff --git a/app/filtering.js b/app/filtering.js index eb8a75d80c4..4fd57ae5bb5 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -31,6 +31,7 @@ const {updateElectronDownloadItem} = require('./browser/electronDownloadItem') const {fullscreenOption} = require('./common/constants/settingsEnums') const isThirdPartyHost = require('./browser/isThirdPartyHost') const extensionState = require('./common/state/extensionState') +const ledgerUtil = require('./common/lib/ledgerUtil') const {cookieExceptions, refererExceptions} = require('../js/data/siteHacks') const {getBraverySettingsCache, updateBraverySettingsCache} = require('./common/cache/braverySettingsCache') @@ -200,15 +201,22 @@ function registerForBeforeRequest (session, partition) { } } // Redirect to non-script version of DDG when it's blocked - let url = details.url + const url = details.url if (details.resourceType === 'mainFrame' && url.startsWith('https://duckduckgo.com/?q') && module.exports.isResourceEnabled('noScript', url, isPrivate)) { - url = url.replace('?q=', 'html?q=') - muonCb({redirectURL: url}) + muonCb({redirectURL: url.replace('?q=', 'html?q=')}) } else { muonCb({}) } + + if (module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { + // Ledger media + const provider = ledgerUtil.getMediaProvider(url) + if (provider) { + appActions.onLedgerMediaData(url, provider, details.tabId) + } + } }) } @@ -761,6 +769,14 @@ module.exports.isResourceEnabled = (resourceName, url, isPrivate) => { return extension !== undefined ? extension.get('enabled') : false } + if (resourceName === 'ledger') { + return getSetting(settings.PAYMENTS_ENABLED, settingsState) + } + + if (resourceName === 'ledgerMedia') { + return getSetting(settings.PAYMENTS_ALLOW_MEDIA_PUBLISHERS, settingsState) + } + const braverySettings = getBraverySettingsForUrl(url, appState, isPrivate) // If full shields are down never enable extra protection diff --git a/app/locale.js b/app/locale.js index a1b574c0de9..003ecc034a4 100644 --- a/app/locale.js +++ b/app/locale.js @@ -182,6 +182,7 @@ var rendererIdentifiers = function () { 'learnSpelling', 'forgetLearnedSpelling', 'lookupSelection', + 'publisherMediaName', // Other identifiers 'aboutBlankTitle', 'urlCopied', diff --git a/app/renderer/components/preferences/payment/advancedSettings.js b/app/renderer/components/preferences/payment/advancedSettings.js index e5f508d2a70..dad7d591f88 100644 --- a/app/renderer/components/preferences/payment/advancedSettings.js +++ b/app/renderer/components/preferences/payment/advancedSettings.js @@ -73,6 +73,15 @@ class AdvancedSettingsContent extends ImmutableComponent { prefKey={settings.PAYMENTS_ALLOW_NON_VERIFIED} settings={this.props.settings} onChangeSetting={this.props.onChangeSetting} + className={css(styles.advancedSettings__switches__listItem_first)} + switchClassName={css(styles.advancedSettings__switches__listItem__checkboxSwitch)} + /> + diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index 9f8b95f4806..72905e88bde 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -52,7 +52,7 @@ class LedgerTable extends ImmutableComponent { } getHostPattern (synopsis) { - return urlUtil.getHostPattern(synopsis.get('site')) + return urlUtil.getHostPattern(synopsis.get('publisherKey')) } getVerifiedIcon (synopsis) { @@ -151,10 +151,11 @@ class LedgerTable extends ImmutableComponent { const duration = synopsis.get('duration') const publisherURL = synopsis.get('publisherURL') const percentage = pinned ? this.pinPercentageValue(synopsis) : synopsis.get('percentage') - const site = synopsis.get('site') + const publisherKey = synopsis.get('publisherKey') + const siteName = synopsis.get('siteName') const defaultAutoInclude = this.enabledForSite(synopsis) - const rowRefName = 'rowPercentage_' + site + const rowRefName = 'rowPercentage_' + publisherKey if (this.refs[rowRefName]) { this.refs[rowRefName].value = percentage } @@ -169,13 +170,13 @@ class LedgerTable extends ImmutableComponent { { faviconURL - ? {site} + ? {siteName} : } - {site} + {siteName} , - value: site + value: publisherKey }, { html: pinned diff --git a/app/sessionStore.js b/app/sessionStore.js index 1f0afa9a156..04030735f81 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -926,7 +926,8 @@ module.exports.defaultAppState = () => { }, cache: { bookmarkLocation: undefined, - bookmarkOrder: {} + bookmarkOrder: {}, + ledgerVideos: {} }, pinnedSites: {}, bookmarks: {}, diff --git a/docs/state.md b/docs/state.md index 307b747cbd9..c072f747a48 100644 --- a/docs/state.md +++ b/docs/state.md @@ -89,6 +89,9 @@ AppStore order: number, type: string // siteTags.BOOKMARK or siteTags.BOOKMARK_FOLDER }] + }, + ledgerVideos: { + [mediaKey]: string // publisher key } } clearBrowsingDataDefaults: { @@ -356,6 +359,7 @@ AppStore 'general.startup-mode': string, // one of: lastTime, homePage, newTabPage 'notification-add-funds-timestamp': number, // timestamp on which we decide if we will show notification Add founds 'notification-reconcile-soon-timestamp': number, // timestamp + 'payments.allow-media-publishers': boolean, 'payments.allow-non-verified-publishers': boolean, 'payments.contribution-amount': number, // in USD 'payments.enabled': boolean, // true if the Payments pane is active diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 714c44e7c54..402eeaf61fd 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -1812,6 +1812,25 @@ const appActions = { actionType: appConstants.APP_ON_PUBLISHER_TIMESTAMP, timestamp }) + }, + + onLedgerMediaData: function (url, type, tabId) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_MEDIA_DATA, + url, + type, + tabId + }) + }, + + onLedgerMediaPublisher: function (mediaKey, response, duration, revisited) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_MEDIA_PUBLISHER, + mediaKey, + response, + duration, + revisited + }) } } diff --git a/js/constants/appConfig.js b/js/constants/appConfig.js index 8f34e336cc6..27c3636f350 100644 --- a/js/constants/appConfig.js +++ b/js/constants/appConfig.js @@ -183,6 +183,7 @@ module.exports = { // Tracking issue for that and to re-enable title mode on Windows is at #9900. 'general.disable-title-mode': process.platform === 'linux' || process.platform === 'win32', // payments + 'payments.allow-media-publishers': true, 'payments.allow-non-verified-publishers': true, 'payments.contribution-amount': 25, // BAT 'payments.enabled': false, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index ac5c63c3cfd..94f11ee08cd 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -179,7 +179,9 @@ const appConstants = { APP_ON_BTC_TO_BAT_TRANSITIONED: _, APP_ON_LEDGER_QR_GENERATED: _, APP_ON_BTC_TO_BAT_BEGIN_TRANSITION: _, - APP_ON_PUBLISHER_TIMESTAMP: _ + APP_ON_PUBLISHER_TIMESTAMP: _, + APP_ON_LEDGER_MEDIA_DATA: _, + APP_ON_LEDGER_MEDIA_PUBLISHER: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/js/constants/settings.js b/js/constants/settings.js index 518511b85ce..d327bcecbac 100644 --- a/js/constants/settings.js +++ b/js/constants/settings.js @@ -53,6 +53,7 @@ const settings = { // Autofill AUTOFILL_ENABLED: 'privacy.autofill-enabled', // Payments Tab + PAYMENTS_ALLOW_MEDIA_PUBLISHERS: 'payments.allow-media-publishers', PAYMENTS_ALLOW_NON_VERIFIED: 'payments.allow-non-verified-publishers', PAYMENTS_CONTRIBUTION_AMOUNT: 'payments.contribution-amount', PAYMENTS_ENABLED: 'payments.enabled', diff --git a/package-lock.json b/package-lock.json index 742735c453d..a01fe8e94a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "brave", - "version": "0.21.0", + "version": "0.22.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1431,13 +1431,13 @@ } }, "bat-client": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/bat-client/-/bat-client-1.0.15.tgz", - "integrity": "sha1-ayPKg6QsaSzl0M/hietJ2J5xnCA=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bat-client/-/bat-client-1.2.0.tgz", + "integrity": "sha1-OXAH7txSSSBq0ZMg6HEZsK3CRdM=", "requires": { "@ambassify/backoff-strategies": "1.0.0", "bat-balance": "1.0.3", - "bat-publisher": "1.1.5", + "bat-publisher": "1.2.0", "bitgo": "4.10.0", "brave-crypto": "0.0.1", "http-request-signature": "0.0.2", @@ -1450,27 +1450,6 @@ "uuid": "3.1.0" }, "dependencies": { - "bat-publisher": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bat-publisher/-/bat-publisher-1.1.5.tgz", - "integrity": "sha1-xadplXDIsIWYf+whwhiQv+w5axw=", - "requires": { - "@ambassify/backoff-strategies": "1.0.0", - "async": "2.5.0", - "data-expression": "1.0.0", - "glob": "7.1.2", - "jimp": "0.2.28", - "joi": "11.4.0", - "jsdom": "11.2.0", - "metascraper": "1.0.7", - "node-cache": "4.1.1", - "parse-cache-control": "1.0.1", - "random-lib": "2.1.0", - "tldjs": "2.2.0", - "underscore": "1.8.3", - "underscore.string": "3.3.4" - } - }, "joi": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/joi/-/joi-11.4.0.tgz", @@ -1480,41 +1459,24 @@ "isemail": "3.0.0", "topo": "2.0.2" } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "tldjs": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.2.0.tgz", - "integrity": "sha512-5b5t+HKprfccAFRAsH/fzDR4O+UgO6vStvbaJo10jvMcUavlwxR3Jrn2WmXfjG3k22T7b4pqqfput38nr1RpJQ==", - "requires": { - "punycode": "1.4.1" - } - }, - "underscore.string": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz", - "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=", - "requires": { - "sprintf-js": "1.1.1", - "util-deprecate": "1.0.2" - } } } }, "bat-publisher": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bat-publisher/-/bat-publisher-1.0.3.tgz", - "integrity": "sha512-Y4YfTrADz8LltdJw8oaOr6fTgFfkWQLftj2N/2WZ6dwvJWhzrzxcZzEy1fOWNvxdc5/c9/nw5C9ayZ6Xct8rsQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bat-publisher/-/bat-publisher-1.2.0.tgz", + "integrity": "sha1-rf4wRIHvspjQci615UdTI9+C5Vc=", "requires": { + "@ambassify/backoff-strategies": "1.0.0", "async": "2.5.0", "data-expression": "1.0.0", "glob": "7.1.2", + "jimp": "0.2.28", "joi": "11.4.0", "jsdom": "11.2.0", + "metascraper": "1.0.7", + "node-cache": "4.1.1", + "parse-cache-control": "1.0.1", "random-lib": "2.1.0", "tldjs": "2.2.0", "underscore": "1.8.3", @@ -5240,6 +5202,61 @@ } } }, + "electron-download": { + "version": "github:brave/electron-download#409b65caff14edeef1daa36a7445ba6334658d7c", + "dev": true, + "requires": { + "debug": "2.6.9", + "home-path": "1.0.5", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "mv": "2.1.1", + "nugget": "1.6.2", + "path-exists": "1.0.0", + "rc": "1.2.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "nugget": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", + "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", + "dev": true, + "requires": { + "debug": "2.6.9", + "minimist": "1.2.0", + "pretty-bytes": "1.0.4", + "progress-stream": "1.2.0", + "request": "2.82.0", + "single-line-log": "0.4.1", + "throttleit": "0.0.2" + } + }, + "path-exists": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", + "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", + "dev": true + }, + "single-line-log": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", + "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", + "dev": true + }, + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "dev": true + } + } + }, "electron-download-tf": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/electron-download-tf/-/electron-download-tf-4.3.1.tgz", @@ -5425,20 +5442,6 @@ "integrity": "sha1-HUixB9ghJqLz4hHC6iX4A7pVGyE=", "dev": true }, - "electron-download": { - "version": "github:brave/electron-download#409b65caff14edeef1daa36a7445ba6334658d7c", - "dev": true, - "requires": { - "debug": "2.6.9", - "home-path": "1.0.5", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "mv": "2.1.1", - "nugget": "1.6.2", - "path-exists": "1.0.0", - "rc": "1.2.1" - } - }, "electron-osx-sign": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.3.2.tgz", @@ -5488,27 +5491,6 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, - "nugget": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", - "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", - "dev": true, - "requires": { - "debug": "2.6.9", - "minimist": "1.2.0", - "pretty-bytes": "1.0.4", - "progress-stream": "1.2.0", - "request": "2.82.0", - "single-line-log": "0.4.1", - "throttleit": "0.0.2" - } - }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true - }, "plist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", @@ -5521,18 +5503,6 @@ "xmldom": "0.1.27" } }, - "single-line-log": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", - "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", - "dev": true - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - }, "xmlbuilder": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz", @@ -5550,61 +5520,6 @@ "requires": { "electron-download": "github:brave/electron-download#409b65caff14edeef1daa36a7445ba6334658d7c", "extract-zip": "1.6.5" - }, - "dependencies": { - "electron-download": { - "version": "github:brave/electron-download#409b65caff14edeef1daa36a7445ba6334658d7c", - "dev": true, - "requires": { - "debug": "2.6.9", - "home-path": "1.0.5", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "mv": "2.1.1", - "nugget": "1.6.2", - "path-exists": "1.0.0", - "rc": "1.2.1" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "nugget": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", - "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", - "dev": true, - "requires": { - "debug": "2.6.9", - "minimist": "1.2.0", - "pretty-bytes": "1.0.4", - "progress-stream": "1.2.0", - "request": "2.82.0", - "single-line-log": "0.4.1", - "throttleit": "0.0.2" - } - }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true - }, - "single-line-log": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", - "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", - "dev": true - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - } } }, "electron-publish": { @@ -5630,7 +5545,7 @@ } }, "electron-squirrel-startup": { - "version": "github:brave/electron-squirrel-startup#5575c01deca9aac963b6888a5f02bd9f3b7bd5c1", + "version": "github:brave/electron-squirrel-startup#88d78fee0079d7bfce7e5238658e54e2e75550ef", "requires": { "debug": "2.6.9" } diff --git a/package.json b/package.json index e336769838b..60759a3f47a 100644 --- a/package.json +++ b/package.json @@ -88,8 +88,8 @@ "aphrodite": "1.1.0", "async": "^2.0.1", "bat-balance": "^1.0.3", - "bat-client": "^1.0.15", - "bat-publisher": "^1.0.3", + "bat-client": "^1.2.0", + "bat-publisher": "^1.2.0", "bignumber.js": "^4.0.4", "bloodhound-js": "brave/bloodhound", "clipboard-copy": "^1.0.0", diff --git a/test/unit/app/browser/api/ledgerTest.js b/test/unit/app/browser/api/ledgerTest.js index 1cba09bd6a6..0c36a48f930 100644 --- a/test/unit/app/browser/api/ledgerTest.js +++ b/test/unit/app/browser/api/ledgerTest.js @@ -11,12 +11,19 @@ const settings = require('../../../../../js/constants/settings') const appActions = require('../../../../../js/actions/appActions') const migrationState = require('../../../../../app/common/state/migrationState') const batPublisher = require('bat-publisher') +const ledgerMediaProviders = require('../../../../../app/common/constants/ledgerMediaProviders') describe('ledger api unit tests', function () { let ledgerApi let ledgerNotificationsApi let isBusy = false let ledgerClient + let ledgerPublisher + + // constants + const xhr = 'https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339' + const videoId = 'youtube_kLiLOkzLetE' + const publisherKey = 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' // settings let contributionAmount = 25 @@ -31,6 +38,9 @@ describe('ledger api unit tests', function () { let onChangeSettingSpy const defaultAppState = Immutable.fromJS({ + cache: { + ledgerVideos: {} + }, ledger: {}, migrations: {} }) @@ -128,14 +138,21 @@ describe('ledger api unit tests', function () { mockery.registerMock('bat-client', ledgerClient) // ledger publisher stubbing - const lp = { + ledgerPublisher = { ruleset: [], - getPublisherProps: function (publisher) { + getPublisherProps: function () { return null }, - Synopsis: batPublisher.Synopsis + Synopsis: batPublisher.Synopsis, + getMedia: { + getPublisherFromMediaProps: () => {} + } } - mockery.registerMock('bat-publisher', lp) + mockery.registerMock('bat-publisher', ledgerPublisher) + mockery.registerMock('../../common/state/tabState', { + TAB_ID_NONE: -1, + getActiveTabId: () => 1 + }) ledgerNotificationsApi = require('../../../../../app/browser/api/ledgerNotifications') @@ -156,9 +173,15 @@ describe('ledger api unit tests', function () { beforeEach(function () { notificationsInitStub = sinon.stub(ledgerNotificationsApi, 'init') }) + afterEach(function () { notificationsInitStub.restore() }) + + after(function () { + ledgerApi.setSynopsis(undefined) + }) + it('calls notifications.init', function () { ledgerApi.initialize(defaultAppState, true) assert(notificationsInitStub.calledOnce) @@ -264,7 +287,7 @@ describe('ledger api unit tests', function () { }) }) - describe('addVisit', function () { + describe('addSiteVisit', function () { const fakeTabId = 7 let stateWithLocation let fakeClock @@ -289,7 +312,7 @@ describe('ledger api unit tests', function () { fakeClock.tick(6000) - const result = ledgerApi.addVisit(state, 0, 'https://clifton.io', fakeTabId) + const result = ledgerApi.addSiteVisit(state, 0, 'https://clifton.io', fakeTabId) const visitsByPublisher = ledgerApi.getVisitsByPublisher() // Assert state WAS modified AND publisher was recorded @@ -301,7 +324,7 @@ describe('ledger api unit tests', function () { fakeClock.tick(0) - const result = ledgerApi.addVisit(state, 0, 'https://clifton.io', fakeTabId) + const result = ledgerApi.addSiteVisit(state, 0, 'https://clifton.io', fakeTabId) const visitsByPublisher = ledgerApi.getVisitsByPublisher() // Assert state WAS modified but publisher wasn NOT recorded @@ -312,10 +335,10 @@ describe('ledger api unit tests', function () { const state = ledgerApi.initialize(stateWithLocation, true) fakeClock.tick(2000) - const result1 = ledgerApi.addVisit(state, 0, 'https://clifton.io', fakeTabId) + const result1 = ledgerApi.addSiteVisit(state, 0, 'https://clifton.io', fakeTabId) fakeClock.tick(15000) - const result2 = ledgerApi.addVisit(result1, 0, 'https://clifton.io', fakeTabId) + const result2 = ledgerApi.addSiteVisit(result1, 0, 'https://clifton.io', fakeTabId) const visitsByPublisher = ledgerApi.getVisitsByPublisher() @@ -398,6 +421,10 @@ describe('ledger api unit tests', function () { }) describe('transitionWalletToBat', function () { + after(function () { + ledgerApi.setSynopsis(undefined) + }) + describe('when client is not busy', function () { before(function () { ledgerApi.onBootStateFile(defaultAppState) @@ -487,6 +514,10 @@ describe('ledger api unit tests', function () { }) describe('synopsisNormalizer', function () { + after(function () { + ledgerApi.setSynopsis(undefined) + }) + describe('prune synopsis', function () { let pruneSynopsisSpy @@ -511,6 +542,10 @@ describe('ledger api unit tests', function () { }) describe('pruneSynopsis', function () { + after(function () { + ledgerApi.setSynopsis(undefined) + }) + it('null case', function () { const result = ledgerApi.pruneSynopsis(defaultAppState) assert.deepEqual(result.toJS(), defaultAppState.toJS()) @@ -538,6 +573,9 @@ describe('ledger api unit tests', function () { }) const expectedResult = { + cache: { + ledgerVideos: {} + }, ledger: { synopsis: { publishers: { @@ -598,4 +636,210 @@ describe('ledger api unit tests', function () { assert(verifiedPSpy.calledOnce) }) }) + + describe('onMediaRequest', function () { + let publisherFromMediaPropsSpy, saveVisitSpy + + beforeEach(function () { + publisherFromMediaPropsSpy = sinon.spy(ledgerPublisher.getMedia, 'getPublisherFromMediaProps') + saveVisitSpy = sinon.spy(ledgerApi, 'saveVisit') + }) + + afterEach(function () { + publisherFromMediaPropsSpy.restore() + saveVisitSpy.restore() + ledgerApi.setCurrentMediaKey(null) + }) + + after(function () { + ledgerApi.setSynopsis(undefined) + }) + + it('null case', function () { + const result = ledgerApi.onMediaRequest(defaultAppState) + assert.deepEqual(result.toJS(), defaultAppState.toJS()) + assert(publisherFromMediaPropsSpy.notCalled) + assert(saveVisitSpy.notCalled) + }) + + it('set currentMediaKey when it is different than saved', function () { + ledgerApi.onMediaRequest(defaultAppState, xhr, ledgerMediaProviders.YOUTUBE, 1) + assert.equal(ledgerApi.getCurrentMediaKey(), videoId) + assert(publisherFromMediaPropsSpy.calledOnce) + assert(saveVisitSpy.notCalled) + }) + + it('get data from cache', function () { + const state = defaultAppState.setIn(['cache', 'ledgerVideos', videoId], Immutable.fromJS({ + publisher: publisherKey + })) + ledgerApi.onMediaRequest(state, xhr, ledgerMediaProviders.YOUTUBE, 1) + assert(publisherFromMediaPropsSpy.notCalled) + assert(saveVisitSpy.withArgs(state, publisherKey, 10001, false).calledOnce) + }) + + it('min duration is set to minimum visit time if below that threshold', function () { + const state = defaultAppState.setIn(['cache', 'ledgerVideos', videoId], Immutable.fromJS({ + publisher: publisherKey + })) + const xhr2 = 'https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=20.338&et=21.339' + ledgerApi.onMediaRequest(state, xhr2, ledgerMediaProviders.YOUTUBE, 1) + assert(publisherFromMediaPropsSpy.notCalled) + assert(saveVisitSpy.withArgs(state, publisherKey, paymentsMinVisitTime, false).calledOnce) + }) + + it('revisited if visiting the same media in the same tab', function () { + const state = defaultAppState.setIn(['cache', 'ledgerVideos', videoId], Immutable.fromJS({ + publisher: publisherKey + })) + // first call, revisit false + ledgerApi.onMediaRequest(state, xhr, ledgerMediaProviders.YOUTUBE, 1) + assert.equal(ledgerApi.getCurrentMediaKey(), videoId) + assert(saveVisitSpy.withArgs(state, publisherKey, 10001, false).calledOnce) + + // second call, revisit true + ledgerApi.onMediaRequest(state, xhr, ledgerMediaProviders.YOUTUBE, 1) + assert(publisherFromMediaPropsSpy.notCalled) + assert(saveVisitSpy.withArgs(state, publisherKey, 10001, true).calledOnce) + }) + + it('revisited if visiting media in the background tab', function () { + const state = defaultAppState.setIn(['cache', 'ledgerVideos', videoId], Immutable.fromJS({ + publisher: publisherKey + })) + // first call, revisit false + ledgerApi.setCurrentMediaKey('11') + ledgerApi.onMediaRequest(state, xhr, ledgerMediaProviders.YOUTUBE, 10) + assert.equal(ledgerApi.getCurrentMediaKey(), '11') + assert(saveVisitSpy.withArgs(state, publisherKey, 10001, true).calledOnce) + }) + }) + + describe('onMediaPublisher', function () { + let saveVisitSpy, verifiedPStub + + const expectedState = Immutable.fromJS({ + cache: { + ledgerVideos: { + 'youtube_kLiLOkzLetE': { + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg', + faviconName: 'Brave', + providerName: 'Youtube', + faviconURL: 'data:image/jpeg;base64,...', + publisherURL: 'https://brave.com' + } + } + }, + ledger: { + synopsis: { + publishers: { + 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg': { + exclude: false, + options: { + exclude: true + }, + providerName: 'Youtube', + faviconName: 'Brave', + faviconURL: 'data:image/jpeg;base64,...', + publisherURL: 'https://brave.com' + } + } + } + }, + migrations: {} + }) + + before(function () { + verifiedPStub = sinon.stub(ledgerApi, 'verifiedP', (state, publisherKey, fn) => state) + }) + + after(function () { + verifiedPStub.restore() + }) + + beforeEach(function () { + ledgerApi.setSynopsis({ + initPublisher: () => {}, + addPublisher: () => {}, + publishers: { + [publisherKey]: { + exclude: false, + options: { + exclude: true + }, + providerName: 'Youtube' + } + } + }) + saveVisitSpy = sinon.spy(ledgerApi, 'saveVisit') + }) + + afterEach(function () { + ledgerApi.setSynopsis(undefined) + saveVisitSpy.restore() + }) + + it('null case', function () { + const result = ledgerApi.onMediaPublisher(defaultAppState) + assert.deepEqual(result.toJS(), defaultAppState.toJS()) + }) + + it('create publisher if new and add cache', function () { + const response = Immutable.fromJS({ + publisher: publisherKey, + faviconName: 'Brave', + faviconURL: 'data:image/jpeg;base64,...', + publisherURL: 'https://brave.com', + providerName: 'Youtube' + }) + + const state = ledgerApi.onMediaPublisher(defaultAppState, videoId, response, 1000, false) + assert(saveVisitSpy.calledOnce) + assert.deepEqual(state.toJS(), expectedState.toJS()) + }) + + it('update publisher if exists', function () { + const newState = Immutable.fromJS({ + cache: { + ledgerVideos: { + 'youtube_kLiLOkzLetE': { + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg', + faviconName: 'Brave', + providerName: 'Youtube', + faviconURL: 'data:image/jpeg;base64,...', + publisherURL: 'https://brave.com' + } + } + }, + ledger: { + synopsis: { + publishers: { + 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg': { + options: { + exclude: true + }, + faviconName: 'old Brave', + faviconURL: 'data:image/jpeg;base64,...', + publisherURL: 'https://brave.io', + providerName: 'Youtube' + } + } + } + }, + migrations: {} + }) + + const response = Immutable.fromJS({ + publisher: publisherKey, + faviconName: 'Brave', + faviconURL: 'data:image/jpeg;base64,...', + publisherURL: 'https://brave.com', + providerName: 'Youtube' + }) + + const state = ledgerApi.onMediaPublisher(newState, videoId, response, 1000, false) + assert(saveVisitSpy.calledOnce) + assert.deepEqual(state.toJS(), expectedState.toJS()) + }) + }) }) diff --git a/test/unit/app/common/cache/ledgerVideoCacheTest.js b/test/unit/app/common/cache/ledgerVideoCacheTest.js new file mode 100644 index 00000000000..feadd27153c --- /dev/null +++ b/test/unit/app/common/cache/ledgerVideoCacheTest.js @@ -0,0 +1,61 @@ +/* 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/. */ +/* global describe, it */ + +const Immutable = require('immutable') +const assert = require('assert') +const ledgerVideoCache = require('../../../../../app/common/cache/ledgerVideoCache') + +const baseState = Immutable.fromJS({ + cache: { + ledgerVideos: {} + } +}) +const stateWithData = Immutable.fromJS({ + cache: { + ledgerVideos: { + 'youtube_kLiLOkzLetE': { + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' + } + } + } +}) + +describe('ledgerVideoCache unit test', function () { + describe('getDataByVideoId', function () { + it('key is not provided', function () { + const result = ledgerVideoCache.getDataByVideoId(baseState) + assert.deepEqual(result.toJS(), {}) + }) + + it('key does not exist in the cache', function () { + const result = ledgerVideoCache.getDataByVideoId(baseState, 'key') + assert.deepEqual(result.toJS(), {}) + }) + + it('data is ok', function () { + const result = ledgerVideoCache.getDataByVideoId(stateWithData, 'youtube_kLiLOkzLetE') + assert.deepEqual(result.toJS(), { + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' + }) + }) + }) + + describe('setCacheByVideoId', function () { + it('key is not provided', function () { + const state = ledgerVideoCache.setCacheByVideoId(baseState) + assert.deepEqual(state.toJS(), baseState.toJS()) + }) + + it('data is ok', function () { + const state = ledgerVideoCache.setCacheByVideoId(baseState, 'youtube_kLiLOkzLetE', Immutable.fromJS({ + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' + })) + const expectedState = state.setIn(['cache', 'ledgerVideos', 'youtube_kLiLOkzLetE'], Immutable.fromJS({ + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' + })) + assert.deepEqual(state.toJS(), expectedState.toJS()) + }) + }) +}) diff --git a/test/unit/app/common/lib/ledgerUtilTest.js b/test/unit/app/common/lib/ledgerUtilTest.js index e54df4a155a..96939a6fa39 100644 --- a/test/unit/app/common/lib/ledgerUtilTest.js +++ b/test/unit/app/common/lib/ledgerUtilTest.js @@ -3,6 +3,7 @@ const mockery = require('mockery') const assert = require('assert') const Immutable = require('immutable') require('../../../braveUnit') +const ledgerMediaProviders = require('../../../../../app/common/constants/ledgerMediaProviders') describe('ledgerUtil test', function () { let ledgerUtil @@ -211,4 +212,123 @@ describe('ledgerUtil test', function () { describe('walletStatus', function () { }) + + describe('getMediaId', function () { + it('null case', function () { + const result = ledgerUtil.getMediaId() + assert.equal(result, null) + }) + + it('unknown type', function () { + const result = ledgerUtil.getMediaData({}, 'test') + assert.equal(result, null) + }) + + describe('Youtube', function () { + it('null case', function () { + const result = ledgerUtil.getMediaId(null, ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) + + it('id is provided', function () { + const result = ledgerUtil.getMediaId({docid: 'kLiLOkzLetE'}, ledgerMediaProviders.YOUTUBE) + assert.equal(result, 'kLiLOkzLetE') + }) + }) + }) + + describe('getMediaKey', function () { + it('null case', function () { + const result = ledgerUtil.getMediaKey() + assert.equal(result, null) + }) + + it('type is missing', function () { + const result = ledgerUtil.getMediaKey('kLiLOkzLetE') + assert.equal(result, null) + }) + + it('id is null', function () { + const result = ledgerUtil.getMediaKey(null, ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) + + it('data is ok', function () { + const result = ledgerUtil.getMediaKey('kLiLOkzLetE', ledgerMediaProviders.YOUTUBE) + assert.equal(result, 'youtube_kLiLOkzLetE') + }) + }) + + describe('getMediaData', function () { + it('null case', function () { + const result = ledgerUtil.getMediaData() + assert.equal(result, null) + }) + + it('unknown type', function () { + const result = ledgerUtil.getMediaData('https://youtube.com', 'test') + assert.equal(result, null) + }) + + describe('Youtube', function () { + it('null case', function () { + const result = ledgerUtil.getMediaData(null, ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) + + it('query is not present', function () { + const result = ledgerUtil.getMediaData('https://youtube.com', ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) + + it('query is present', function () { + const result = ledgerUtil.getMediaData('https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339', ledgerMediaProviders.YOUTUBE) + assert.deepEqual(result, { + docid: 'kLiLOkzLetE', + st: '11.338', + et: '21.339' + }) + }) + }) + }) + + describe('getYouTubeDuration', function () { + it('null case', function () { + const result = ledgerUtil.getYouTubeDuration() + assert.equal(result, 0) + }) + + it('multiple times', function () { + const result = ledgerUtil.getYouTubeDuration({ + st: '11.338,21.339,25.000', + et: '21.339,25.000,26.100' + }) + assert.equal(result, 14762) + }) + + it('single time', function () { + const result = ledgerUtil.getYouTubeDuration({ + st: '11.338', + et: '21.339' + }) + assert.equal(result, 10001) + }) + }) + + describe('getMediaProvider', function () { + it('null case', function () { + const result = ledgerUtil.getMediaProvider() + assert.equal(result, null) + }) + + it('unknown provider', function () { + const result = ledgerUtil.getMediaProvider('https://www.brave.com') + assert.equal(result, null) + }) + + it('youtube', function () { + const result = ledgerUtil.getMediaProvider('https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339') + assert.equal(result, ledgerMediaProviders.YOUTUBE) + }) + }) }) diff --git a/test/unit/app/renderer/components/preferences/payments/ledgerTableTest.js b/test/unit/app/renderer/components/preferences/payments/ledgerTableTest.js index 65bead1131a..167a651f07f 100644 --- a/test/unit/app/renderer/components/preferences/payments/ledgerTableTest.js +++ b/test/unit/app/renderer/components/preferences/payments/ledgerTableTest.js @@ -51,7 +51,8 @@ const fivePublishers = { ]), synopsis: Immutable.List([ Immutable.Map({ - site: 'times.com', + publisherKey: 'times.com', + siteName: 'times.com', verified: false, views: 2, pinPercentage: 10, @@ -66,7 +67,8 @@ const fivePublishers = { faviconURL: '' }), Immutable.Map({ - site: 'cnn.com', + publisherKey: 'cnn.com', + siteName: 'cnn.com', verified: false, views: 1, pinPercentage: 15, @@ -81,7 +83,8 @@ const fivePublishers = { faviconURL: '' }), Immutable.Map({ - site: 'brianbondy.com', + publisherKey: 'brianbondy.com', + siteName: 'brianbondy.com', verified: true, views: 1, pinPercentage: 0, @@ -96,7 +99,8 @@ const fivePublishers = { faviconURL: '' }), Immutable.Map({ - site: 'github.com', + publisherKey: 'github.com', + siteName: 'github.com', verified: false, views: 1, pinPercentage: 0, @@ -111,7 +115,8 @@ const fivePublishers = { faviconURL: '' }), Immutable.Map({ - site: 'clifton.io', + publisherKey: 'clifton.io', + siteName: 'clifton.io', verified: false, views: 1, pinPercentage: 0, @@ -164,7 +169,8 @@ describe('LedgerTable component', function () { const synopsis = Immutable.List([ Immutable.Map({ - site: 'times.com', + publisherKey: 'times.com', + siteName: 'times.com', verified: false, views: 2, pinPercentage: 0, @@ -218,7 +224,8 @@ describe('LedgerTable component', function () { const synopsis = Immutable.List([ Immutable.Map({ - site: 'times.com', + publisherKey: 'times.com', + siteName: 'times.com', verified: false, views: 2, pinPercentage: 10, @@ -233,7 +240,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'cnn.com', + publisherKey: 'cnn.com', + siteName: 'cnn.com', verified: false, views: 1, pinPercentage: 15, @@ -248,7 +256,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'brianbondy.com', + publisherKey: 'brianbondy.com', + siteName: 'brianbondy.com', verified: true, views: 1, pinPercentage: 0, @@ -297,7 +306,8 @@ describe('LedgerTable component', function () { const synopsis = Immutable.List([ Immutable.Map({ - site: 'times.com', + publisherKey: 'times.com', + siteName: 'times.com', verified: false, views: 2, pinPercentage: 10, @@ -312,7 +322,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'cnn.com', + publisherKey: 'cnn.com', + siteName: 'cnn.com', verified: false, views: 1, pinPercentage: 15, @@ -354,7 +365,8 @@ describe('LedgerTable component', function () { const synopsis = Immutable.List([ Immutable.Map({ - site: 'times.com', + publisherKey: 'times.com', + siteName: 'times.com', verified: false, views: 2, pinPercentage: 42, @@ -424,7 +436,8 @@ describe('LedgerTable component', function () { const synopsis = Immutable.List([ Immutable.Map({ - site: 'times.com', + siteName: 'times.com', + publisherKey: 'times.com', verified: false, views: 2, pinPercentage: 10, @@ -439,7 +452,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'cnn.com', + siteName: 'cnn.com', + publisherKey: 'cnn.com', verified: false, views: 1, pinPercentage: 15, @@ -454,7 +468,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'brianbondy.com', + siteName: 'brianbondy.com', + publisherKey: 'brianbondy.com', verified: true, views: 1, pinPercentage: 0, @@ -469,7 +484,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'github.com', + siteName: 'github.com', + publisherKey: 'github.com', verified: false, views: 1, pinPercentage: 0, @@ -484,7 +500,8 @@ describe('LedgerTable component', function () { faviconURL: '' }), Immutable.Map({ - site: 'clifton.io', + siteName: 'clifton.io', + publisherKey: 'clifton.io', verified: false, views: 1, pinPercentage: 0,