diff --git a/.gitignore b/.gitignore index 461003c..ee8ff21 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .vscode src.zip node_modules +developer.json \ No newline at end of file diff --git a/src/background.js b/src/background.js index b0c27a9..136155b 100644 --- a/src/background.js +++ b/src/background.js @@ -3,6 +3,7 @@ function onExtensionLoad() { setBadge(new GreyBadge()); loadSavedSettings(); + getDeveloperSettings(); setDelayedInitialisation(5000); } @@ -14,6 +15,22 @@ function loadSavedSettings() { }); } +async function getDeveloperSettings() { + const devJson = chrome.runtime.getURL('developer.json'); + const fetchProm = await fetch(devJson, {method: 'GET'}).then((response) => { + return response.json(); + }).then((json) => { + developer = json; + console.log('Developer mode enabled.'); + console.log(developer); + }).catch((ex) => { + if (ex.name == 'TypeError') { + return; + } + throw ex; + }); +} + // ----------------------------- // Work // ----------------------------- @@ -47,7 +64,6 @@ async function doBackgroundWork() { setBadge(new BusyBadge()); - await updateUA(); checkNewDay(); await checkDailyRewardStatus(); @@ -104,6 +120,7 @@ const WAIT_FOR_ONLINE_TIMEOUT = 60000; const googleTrend = new GoogleTrend(); const userDailyStatus = new DailyRewardStatus(); const searchQuest = new SearchQuest(googleTrend); +let developer = false; let userAgents; let _compatibilityMode; diff --git a/src/exception.js b/src/exception.js index 51c1b68..8a4af89 100644 --- a/src/exception.js +++ b/src/exception.js @@ -38,16 +38,6 @@ class FetchFailedException extends ErrorWithSourceInnerException { } } -class FetchRedirectedException extends ErrorWithSourceInnerException { - constructor(source, innerException, message) { - if (!message) { - message = 'Fetch failed because redirection occurred.'; - } - super(source, innerException, message); - this.name = 'FetchRedirected'; - } -} - class ResponseUnexpectedStatusException extends ErrorWithSourceInnerException { constructor(source, response, message) { if (!message) { @@ -87,3 +77,17 @@ class FetchTimeoutException extends ErrorWithSourceInnerException { this.name = 'FetchTimeout'; } } + +class UserAgentInvalidException extends Error { + constructor(message) { + super(message); + this.name = 'UserAgentInvalid'; + } +} + +class NotRewardUserException extends Error { + constructor(message) { + super(message); + this.name = 'UserNotLoggedIn'; + } +} diff --git a/src/manifest.json b/src/manifest.json index 2942527..6b2c980 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -34,10 +34,12 @@ "permissions": ["https://www.bing.com/search?q=*", "https://trends.google.com/trends/*", - "https://account.microsoft.com/rewards/pointsbreakdown*", - "https://rewards.microsoft.com/pointsbreakdown*", + "https://account.microsoft.com/rewards/*", + "https://rewards.microsoft.com/*", + "https://rewards.bing.com/*", "webRequest", "webRequestBlocking", - "storage" + "storage", + "notifications" ] } \ No newline at end of file diff --git a/src/quest/searchQuest.js b/src/quest/searchQuest.js index 08da248..29fb4c9 100644 --- a/src/quest/searchQuest.js +++ b/src/quest/searchQuest.js @@ -24,28 +24,36 @@ class SearchQuest { this._status_ = status; this._jobStatus_ = STATUS_BUSY; try { + await getStableUA(); await this._googleTrend_.getGoogleTrendWords(); - await this._startSearchQuests(); - await this._doWorkClosedLoop(status); + await this._doWorkLoop(); } catch (ex) { this._jobStatus_ = STATUS_ERROR; + if (ex instanceof UserAgentInvalidException) { + notifyUpdatedUAOutdated(); + } throw ex; } } - async _doWorkClosedLoop(status) { - await status.update(); - if (status.isSearchCompleted) { - return; - } + async _doWorkLoop() { + while (true) { + if (this._status_.isSearchCompleted) { + return; + } - if (status.jobStatus==STATUS_ERROR || !status.summary.isValid) { - this._jobStatus_ = STATUS_ERROR; - return; - } + if (this._status_.jobStatus == STATUS_ERROR || !this._status_.summary.isValid) { + this._jobStatus_ = STATUS_ERROR; + return; + } + + await this._startSearchQuests(); - await this._startSearchQuests(); - await this._doWorkClosedLoop(status); + const flag = await this.isSearchSuccessful(); + if (flag > 0) { + await this._getAlternativeUA(flag); + } + } } async _startSearchQuests() { @@ -54,6 +62,36 @@ class SearchQuest { this._quitSearchCleanUp(); } + async isSearchSuccessful() { + // Return: + // 0 - successful; 1 - pc search failed; 2 - mb search failed; 3 - both failed + const pcSearchProgBefore = this._status_.pcSearchStatus.progress; + const mbSearchProgBefore = this._status_.mbSearchStatus.progress; + await this._status_.update(); + const flag = (!this._status_.pcSearchStatus.isValidAndCompleted && (pcSearchProgBefore == this._status_.pcSearchStatus.progress)); + return flag + 2 * (!this._status_.mbSearchStatus.isValidAndCompleted && (mbSearchProgBefore == this._status_.mbSearchStatus.progress)); + } + + async _getAlternativeUA(flag) { + if (flag == 3) { + if (userAgents.pcSource == 'updated' && userAgents.mbSource == 'updated') { + throw new UserAgentInvalidException('Cannot find working UAs for pc and mobile.'); + } + await getUpdatedUA('both'); + } else if (flag == 1) { + if (userAgents.pcSource == 'updated') { + throw new UserAgentInvalidException('Cannot find a working UA for pc.'); + } + await getUpdatedUA('pc'); + } else if (flag == 2) { + if (userAgents.mbSource == 'updated') { + throw new UserAgentInvalidException('Cannot find a working UA for mobile.'); + } + await getUpdatedUA('mb'); + } + notifyStableUAOutdated(flag); + } + async _doPcSearch() { this._initiateSearch(); if (this._currentSearchType_ != SEARCH_TYPE_PC_SEARCH) { @@ -133,13 +171,12 @@ class SearchQuest { } function removeUA() { - // remove user-agent try { chrome.webRequest.onBeforeSendHeaders.removeListener(toMobileUA); - } catch (ex) {} + } catch (ex) { } try { chrome.webRequest.onBeforeSendHeaders.removeListener(toMsEdgeUA); - } catch (ex) {} + } catch (ex) { } } function setMsEdgeUA() { @@ -182,6 +219,36 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +function notifyStableUAOutdated(flag) { + if (developer && developer.notification_ua_stable_outdated) { + const message = 'Stable UA is outdated! Flag: ' + (flag == 3 ? 'pc and mobile' : flag == 1 ? 'pc' : 'mobile'); + console.log(message); + chrome.notifications.clear('stable_ua_outdated'); + chrome.notifications.create('stable_ua_outdated', { + type: 'basic', + iconUrl: 'img/warn@8x.png', + title: 'Developer notification', + message: message, + priority: 2, + }); + } +} + +function notifyUpdatedUAOutdated() { + if (developer && developer.notification_ua_updated_outdated) { + const message = 'Critical!! Updated UA is outdated!'; + console.log(message); + chrome.notifications.clear('updated_ua_outdated'); + chrome.notifications.create('updated_ua_outdated', { + type: 'basic', + iconUrl: 'img/err@8x.png', + title: 'Developer notification', + message: message, + priority: 2, + }); + } +} + const SEARCH_TYPE_PC_SEARCH = 0; const SEARCH_TYPE_MB_SEARCH = 1; const STATUS_NONE = 0; diff --git a/src/status/DailyRewardStatus.js b/src/status/DailyRewardStatus.js index 194ba76..9489a80 100644 --- a/src/status/DailyRewardStatus.js +++ b/src/status/DailyRewardStatus.js @@ -52,7 +52,11 @@ class DailyRewardStatus { this._jobStatus_ = STATUS_BUSY; try { const statusJson = await this.getUserStatusJson(); - this._parsePointBreakdownDocument(statusJson); + this._parseUserStatus(statusJson); + const detailedStatusJson = await this.getDetailedUserStatusJson(); + if (detailedStatusJson) { + this._parseDetailedUserStatus(detailedStatusJson); + } } catch (ex) { this._jobStatus_ = STATUS_ERROR; throw ex; @@ -65,31 +69,29 @@ class DailyRewardStatus { async getUserStatusJson() { const controller = new AbortController(); const signal = controller.signal; - const fetchPromise = fetch(POINT_BREAKDOWN_URL_NEW, this._getFetchOptions(signal)); + const fetchPromise = fetch(USER_STATUS_BING_URL, this._getFetchOptions(signal)); setTimeout(() => controller.abort(), 3000); const text = await this._awaitFetchPromise(fetchPromise).catch(async (ex) => { - if (ex.name == 'FetchFailed::TypeError') { - console.log('An error occurred in the first status update attempt:'); - logException(ex); - return await this._getPointBreakdownTextOld(); - } - throw new ResponseUnexpectedStatusException('DailyRewardStatus::getUserStatusJson', ex, errorMessage); + throw new ResponseUnexpectedStatusException('DailyRewardStatus::getUserStatusJsonFromBing', ex, ex.message); }); - const doc = getDomFromText(text); - return DailyRewardStatus.getUserStatusJSON(doc); + return DailyRewardStatus.getUserStatusJSON(text); } - async _getPointBreakdownTextOld() { + async getDetailedUserStatusJson() { const controller = new AbortController(); const signal = controller.signal; - const fetchPromise = fetch(POINT_BREAKDOWN_URL_OLD, this._getFetchOptions(signal)); + const fetchPromise = fetch(USER_STATUS_DETAILED_URL, this._getFetchOptions(signal)); setTimeout(() => controller.abort(), 3000); - return await this._awaitFetchPromise(fetchPromise).catch((ex) => { + const text = await this._awaitFetchPromise(fetchPromise).catch(async (ex) => { if (ex.name == 'FetchFailed::TypeError') { - throw new FetchFailedException('DailyRewardStatus::_getPointBreakdownTextOld', ex, 'Are we redirected by the old URL too? Report to the author now!'); - }; - throw ex; + console.log('An error occurred in the first status update attempt:'); + logException(ex); + return null; + } + throw new ResponseUnexpectedStatusException('DailyRewardStatus::getUserStatusJson', ex, ex.message); }); + const doc = getDomFromText(text); + return DailyRewardStatus.getDetailedUserStatusJSON(doc); } async _awaitFetchPromise(fetchPromise) { @@ -98,7 +100,7 @@ class DailyRewardStatus { response = await fetchPromise; } catch (ex) { if (ex.name == 'TypeError') { - throw new FetchFailedException('DailyRewardStatus::_awaitFetchPromise', ex, 'Are we redirected?'); + throw new FetchFailedException('DailyRewardStatus::_awaitFetchPromise', ex, 'Are we redirected? You probably haven\'t logged in yet.'); } if (error.name == 'AbortError') { throw new FetchFailedException('DailyRewardStatus::_awaitFetchPromise', ex, 'Fetch timed out. Do you have internet connection? Otherwise, perhaps MSR server is down.'); @@ -123,65 +125,82 @@ class DailyRewardStatus { //* ************* // PARSE METHODS //* ************* - _parsePointBreakdownDocument(statusJson) { + _parseUserStatus(statusJson) { if (statusJson == null) { - throw new ParseJSONFailedException('DailyRewardStatus::_getPointBreakdownDocumentOld', null, 'Empty json received.'); + throw new ParseJSONFailedException('DailyRewardStatus::_parseDetailedUserStatus', null, 'Empty json received.'); } try { - if (_compatibilityMode) { - this._parseStatusJsonCompatibilityMode(statusJson); - } else { - this._parseStatusJson(statusJson); + this._parseRewardUser(statusJson); + if (this._userIsError || !this._isRewardsUser) { + throw new NotRewardUserException(`Have you logged into Microsoft Rewards? Query returns {IsError:${this._userIsError},IsRewardsUser:${this._isRewardsUser}}`); } + this._parsePcSearch(statusJson.FlyoutResult); + this._parseMbSearch(statusJson.FlyoutResult); + this._parseActivityAndQuiz(statusJson.FlyoutResult); + this._parseDaily(statusJson.FlyoutResult); } catch (ex) { if (ex.name == 'TypeError' || ex.name == 'ReferenceError') { - throw new ParseJSONFailedException('DailyRewardStatus::_getPointBreakdownDocumentOld', ex, 'Fail to parse the received json document. Has MSR updated its json structure?'); + throw new ParseJSONFailedException('DailyRewardStatus::_parseDetailedUserStatus', ex, 'Fail to parse the received json document. Has MSR updated its json structure?'); } + throw ex; } } - _parseStatusJsonCompatibilityMode(statusJson) { - this._parsePcSearch(statusJson); - this._parseMbSearch(statusJson); - this._parseQuiz(statusJson); - this._parsePunchCards(statusJson, true); - this._parseDaily(statusJson); - } - - _parseStatusJson(statusJson) { - this._parsePcSearch(statusJson); - this._parseMbSearch(statusJson); - this._parseMorePromo(statusJson); - this._parsePunchCards(statusJson, false); - this._parseDaily(statusJson); + _parseRewardUser(statusJson) { + this._userIsError = statusJson.hasOwnProperty('IsError') && statusJson.IsError; + this._isRewardsUser = statusJson.hasOwnProperty('IsRewardsUser') && statusJson.IsRewardsUser; } _parsePcSearch(statusJson) { - console.assert(statusJson.userStatus.counters.pcSearch.length > 0 && statusJson.userStatus.counters.pcSearch.length <= 2); - statusJson.userStatus.counters.pcSearch.forEach((obj) => { - this._pcSearch_.progress += obj.pointProgress; - this._pcSearch_.max += obj.pointProgressMax; + statusJson.UserStatus.Counters.PCSearch.forEach((obj) => { + this._pcSearch_.progress += obj.PointProgress; + this._pcSearch_.max += obj.PointProgressMax; }); } _parseMbSearch(statusJson) { - if (!statusJson.userStatus.counters.hasOwnProperty('mobileSearch')) { + if (!statusJson.UserStatus.Counters.hasOwnProperty('MobileSearch')) { this._mbSearch_.progress = 1; this._mbSearch_.max = 1; return; } - console.assert(statusJson.userStatus.counters.mobileSearch.length == 1); - this._mbSearch_.progress = statusJson.userStatus.counters.mobileSearch[0].pointProgress; - this._mbSearch_.max = statusJson.userStatus.counters.mobileSearch[0].pointProgressMax; + this._mbSearch_.progress = statusJson.UserStatus.Counters.MobileSearch[0].PointProgress; + this._mbSearch_.max = statusJson.UserStatus.Counters.MobileSearch[0].PointProgressMax; } - _parseQuiz(statusJson) { - console.assert(statusJson.userStatus.counters.activityAndQuiz.length == 1); - this._quizAndDaily_.progress += statusJson.userStatus.counters.activityAndQuiz[0].pointProgress; - this._quizAndDaily_.max += statusJson.userStatus.counters.activityAndQuiz[0].pointProgressMax; + _parseActivityAndQuiz(statusJson) { + this._quizAndDaily_.progress += statusJson.UserStatus.Counters.ActivityAndQuiz[0].PointProgress; + this._quizAndDaily_.max += statusJson.UserStatus.Counters.ActivityAndQuiz[0].PointProgressMax; + } + + _parseDaily(statusJson) { + const dailySet = statusJson.DailySetPromotions[getTodayDate()]; + if (!dailySet) return; + dailySet.forEach((obj) => { + if (obj.Complete) { + this._quizAndDaily_.progress += obj.PointProgressMax; + } else { + this._quizAndDaily_.progress += obj.PointProgress; + } + this._quizAndDaily_.max += obj.PointProgressMax; + }); + } + + _parseDetailedUserStatus(statusJson) { + if (statusJson == null) { + throw new ParseJSONFailedException('DailyRewardStatus::_parseDetailedUserStatus', null, 'Empty json received.'); + } + try { + this._parsePunchCards(statusJson, _compatibilityMode); + } catch (ex) { + if (ex.name == 'TypeError' || ex.name == 'ReferenceError') { + throw new ParseJSONFailedException('DailyRewardStatus::_parseDetailedUserStatus', ex, 'Fail to parse the received json document. Has MSR updated its json structure?'); + } + } } _parsePunchCards(statusJson, flagDeduct) { + // flagDeduct: set true to deduct the point progress from the total point progress, only accurate in rare cases, reserved for compatibility mode for (let i = 0; i < statusJson.punchCards.length; i++) { const parentPromo = statusJson.punchCards[i].parentPromotion; if (!parentPromo) continue; @@ -197,47 +216,27 @@ class DailyRewardStatus { } } - _parseDaily(statusJson) { - const dailyset = statusJson.dailySetPromotions[getTodayDate()]; - if (!dailyset) return; - dailyset.forEach((obj) => { - if (obj.complete) { - this._quizAndDaily_.progress += obj.pointProgressMax; - } else { - this._quizAndDaily_.progress += obj.pointProgress; - } - this._quizAndDaily_.max += obj.pointProgressMax; - }); - } - - _parseMorePromo(statusJson) { - const morePromo = statusJson.morePromotions; - if (!morePromo) return; - morePromo.forEach((obj) => { - if (obj.complete) { - this._quizAndDaily_.progress += obj.pointProgressMax; - } else { - this._quizAndDaily_.progress += obj.pointProgress; - } - this._quizAndDaily_.max += obj.pointProgressMax; - }); - } - //* ************** // STATIC METHODS //* ************** - // parses a document object from text/string. - static getUserStatusJSON(doc) { + static getUserStatusJSON(text) { + const m = /(=?\{"FlyoutConfig":).*(=?\}\);;)/.exec(text); + if (m) { + return JSON.parse(m[0].slice(0, m[0].length - 3)); + } + } + + static getDetailedUserStatusJSON(doc) { const jsList = doc.querySelectorAll('body script[type=\'text/javascript\']:not([id])'); for (let i = 0; i < jsList.length; i++) { const m = /(?=\{"userStatus":).*(=?\}\};)/.exec(jsList[i].text); if (m) { - return JSON.parse(m[0].substr(0, m[0].length - 1)); + return JSON.parse(m[0].slice(0, m[0].length - 1)); } } return null; } } -const POINT_BREAKDOWN_URL_OLD = 'https://account.microsoft.com/rewards/pointsbreakdown'; -const POINT_BREAKDOWN_URL_NEW = 'https://rewards.microsoft.com/pointsbreakdown'; +const USER_STATUS_BING_URL = 'https://www.bing.com/rewardsapp/flyout?channel=0&partnerId=EdgeNTP&pageType=ntp&isDarkMode=0'; +const USER_STATUS_DETAILED_URL = 'https://rewards.bing.com/'; diff --git a/src/utility.js b/src/utility.js index 7074f18..73a7f09 100644 --- a/src/utility.js +++ b/src/utility.js @@ -100,10 +100,10 @@ async function getDebugInfo() { copyTextToClipboard(text); } -async function updateUA() { +async function getStableUA() { const controller = new AbortController(); const signal = controller.signal; - const fetchProm = fetch('https://raw.githubusercontent.com/tmxkn1/UpdatedUserAgents/master/useragents.json', {method: 'GET', signal: signal}); + const fetchProm = fetch('https://raw.githubusercontent.com/tmxkn1/Microsoft-Reward-Chrome-Ext/master/useragents.json', {method: 'GET', signal: signal}); setTimeout(() => controller.abort(), 3000); @@ -118,14 +118,54 @@ async function updateUA() { (text) => { const ua = JSON.parse(text); userAgents = { - 'pc': ua.edge.windows, - 'mb': ua.chrome.ios, + 'pc': ua.stable.edge_win, + 'mb': ua.stable.chrome_ios, + 'pcSource': 'stable', + 'mbSource': 'stable', + }; + }, + ).catch((ex) => { + if (ex.name == 'AbortError') { + throw new FetchFailedException('updateUA::_awaitFetchPromise', ex, 'Fetch timed out. Failed to update user agents. Do you have internet connection? Otherwise, perhaps Github server is down.'); + } + throw new ResponseUnexpectedStatusException('updateUA::_awaitFetchPromise', ex, ex.message); + }); +} + +async function getUpdatedUA(type='both') { + const controller = new AbortController(); + const signal = controller.signal; + const fetchProm = fetch('https://raw.githubusercontent.com/tmxkn1/UpdatedUserAgents/master/useragents.json', {method: 'GET', signal: signal}); + + setTimeout(() => controller.abort(), 3000); + + await fetchProm.then( + async (response) => { + if (!response.ok) { + throw await response.text(); + } + return response.text(); + }, + ).then( + (text) => { + const ua = JSON.parse(text); + if (type == 'both') { + userAgents.pc= ua.edge.windows; + userAgents.mb = ua.chrome.chrome_ios; + userAgents.pcSource = 'updated'; + userAgents.mbSource = 'updated'; + } else if (type == 'pc') { + userAgents.pc = ua.edge.windows; + userAgents.pcSource = 'updated'; + } else if (type == 'mb') { + userAgents.mb = ua.chrome.ios; + userAgents.mbSource = 'updated'; }; }, ).catch((ex) => { if (ex.name == 'AbortError') { throw new FetchFailedException('updateUA::_awaitFetchPromise', ex, 'Fetch timed out. Failed to update user agents. Do you have internet connection? Otherwise, perhaps Github server is down.'); } - throw new ResponseUnexpectedStatusException('updateUA::_awaitFetchPromise', ex, errorMessage); + throw new ResponseUnexpectedStatusException('updateUA::_awaitFetchPromise', ex, ex.message); }); }