From ffefd38d2d85271e7493b04ecfa17650993b9333 Mon Sep 17 00:00:00 2001 From: WebGL3D Date: Sun, 25 Jun 2023 17:50:23 -0700 Subject: [PATCH] 2.4.234 https://github.com/roblox-plus/extension/pull/111 --- background.html | 2 - dist/service-worker.js | 93 +++++- dist/service-worker.js.map | 2 +- js/background/background.js | 78 ----- js/background/notifications.js | 78 ----- js/service-worker/notifiers/index.ts | 1 + js/service-worker/notifiers/startup/index.ts | 86 ++++++ js/vanilla/extension/notificationService.js | 289 ------------------- manifest.json | 5 +- 9 files changed, 180 insertions(+), 454 deletions(-) delete mode 100644 js/background/notifications.js create mode 100644 js/service-worker/notifiers/startup/index.ts delete mode 100644 js/vanilla/extension/notificationService.js diff --git a/background.html b/background.html index 5683744..59190a1 100644 --- a/background.html +++ b/background.html @@ -17,7 +17,6 @@ - @@ -40,7 +39,6 @@ - diff --git a/dist/service-worker.js b/dist/service-worker.js index 61500f0..50701cf 100644 --- a/dist/service-worker.js +++ b/dist/service-worker.js @@ -1349,7 +1349,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _catalog__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./catalog */ "./src/js/service-worker/notifiers/catalog/index.ts"); /* harmony import */ var _friend_presence__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./friend-presence */ "./src/js/service-worker/notifiers/friend-presence/index.ts"); /* harmony import */ var _group_shout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./group-shout */ "./src/js/service-worker/notifiers/group-shout/index.ts"); -/* harmony import */ var _trades__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./trades */ "./src/js/service-worker/notifiers/trades/index.ts"); +/* harmony import */ var _startup__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./startup */ "./src/js/service-worker/notifiers/startup/index.ts"); +/* harmony import */ var _trades__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./trades */ "./src/js/service-worker/notifiers/trades/index.ts"); + @@ -1359,7 +1361,7 @@ const notifiers = {}; notifiers['notifiers/catalog'] = _catalog__WEBPACK_IMPORTED_MODULE_0__["default"]; notifiers['notifiers/group-shouts'] = _group_shout__WEBPACK_IMPORTED_MODULE_2__["default"]; notifiers['notifiers/friend-presence'] = _friend_presence__WEBPACK_IMPORTED_MODULE_1__["default"]; -notifiers['notifiers/trade'] = _trades__WEBPACK_IMPORTED_MODULE_3__["default"]; +notifiers['notifiers/trade'] = _trades__WEBPACK_IMPORTED_MODULE_4__["default"]; // TODO: Update to use chrome.storage.session for manifest V3 const notifierStates = {}; // Execute a notifier by name. @@ -1401,6 +1403,93 @@ globalThis.executeNotifier = executeNotifier; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (notifiers); +/***/ }), + +/***/ "./src/js/service-worker/notifiers/startup/index.ts": +/*!**********************************************************!*\ + !*** ./src/js/service-worker/notifiers/startup/index.ts ***! + \**********************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _tix_factory_extension_utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @tix-factory/extension-utils */ "./libs/extension-utils/dist/index.js"); +/* harmony import */ var _services_settings__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../../services/settings */ "./src/js/services/settings/index.ts"); +/* harmony import */ var _services_users__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../services/users */ "./src/js/services/users/index.ts"); + + + +const notificationId = 'startup-notification'; +const displayStartupNotification = async () => { + if (!_tix_factory_extension_utils__WEBPACK_IMPORTED_MODULE_0__.manifest.icons) { + console.warn('Missing manifest icons'); + return; + } + const authenticatedUser = await (0,_services_users__WEBPACK_IMPORTED_MODULE_2__.getAuthenticatedUser)(); + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: chrome.extension.getURL(_tix_factory_extension_utils__WEBPACK_IMPORTED_MODULE_0__.manifest.icons[128]), + title: 'Roblox+ Started', + message: authenticatedUser + ? `Hello, ${authenticatedUser.displayName}` + : 'You are currently signed out', + contextMessage: `${_tix_factory_extension_utils__WEBPACK_IMPORTED_MODULE_0__.manifest.name} ${_tix_factory_extension_utils__WEBPACK_IMPORTED_MODULE_0__.manifest.version}, by WebGL3D`, + }); +}; +(0,_services_settings__WEBPACK_IMPORTED_MODULE_1__.getSettingValue)('startupNotification') + .then(async (setting) => { + if (typeof setting !== 'object') { + setting = { + on: !chrome.extension.inIncognitoContext, + visit: false, + }; + } + if (!setting.on) { + return; + } + if (setting.visit) { + // Only show the startup notification after Roblox has been visited. + const updatedListener = (_tabId, _changes, tab) => { + return takeAction(tab); + }; + const takeAction = async (tab) => { + if (!tab.url) { + return; + } + try { + const tabURL = new URL(tab.url); + if (!tabURL.hostname.endsWith('.roblox.com')) { + return; + } + chrome.tabs.onCreated.removeListener(takeAction); + chrome.tabs.onUpdated.removeListener(updatedListener); + await displayStartupNotification(); + } + catch { + // don't care for now + } + }; + chrome.tabs.onUpdated.addListener(updatedListener); + chrome.tabs.onCreated.addListener(takeAction); + } + else { + await displayStartupNotification(); + } +}) + .catch((err) => { + console.warn('Failed to render startup notification', err); +}); +chrome.notifications.onClicked.addListener((id) => { + if (id !== notificationId) { + return; + } + chrome.tabs.create({ + url: `https://roblox.plus/about/changes?version=${_tix_factory_extension_utils__WEBPACK_IMPORTED_MODULE_0__.manifest.version}`, + active: true, + }); +}); + + /***/ }), /***/ "./src/js/service-worker/notifiers/trades/index.ts": diff --git a/dist/service-worker.js.map b/dist/service-worker.js.map index e2dad85..6123c4c 100644 --- a/dist/service-worker.js.map +++ b/dist/service-worker.js.map @@ -1 +1 @@ -{"version":3,"file":"./service-worker.js","mappings":";;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC7SA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACxCA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AC7EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;ACrCA;AACA;;;;;;;;;;;;;;;;;;;ACDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;ACzKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC/LA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;AC9FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;AChDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;ACxMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACjEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC9CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC7BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC7BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACrDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACtDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;ACxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC1GA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC/BA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACnBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACnCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AClCA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACHA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACxBA;AACA;AACA;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACnCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC1BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AChCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACjCA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;AC5BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC5DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACnFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC9CA;AACA;AACA;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC7EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AClBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACnCA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACvFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;ACpDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACrCA;AACA;AACA;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;AC5DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC5DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;ACrCA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC7CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC9BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AC/EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC7BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACtMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACdA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACpEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;ACPA;;;;;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://roblox-plus/./libs/extension-messaging/dist/constants.js","webpack://roblox-plus/./libs/extension-messaging/dist/index.js","webpack://roblox-plus/./libs/extension-messaging/dist/tabs.js","webpack://roblox-plus/./libs/extension-utils/dist/constants/index.js","webpack://roblox-plus/./libs/extension-utils/dist/enums/loading-state.js","webpack://roblox-plus/./libs/extension-utils/dist/index.js","webpack://roblox-plus/./libs/extension-utils/dist/utils/wait.js","webpack://roblox-plus/./libs/roblox/dist/enums/asset-type.js","webpack://roblox-plus/./libs/roblox/dist/enums/presence-type.js","webpack://roblox-plus/./libs/roblox/dist/enums/thumbnail-state.js","webpack://roblox-plus/./libs/roblox/dist/enums/thumbnail-type.js","webpack://roblox-plus/./libs/roblox/dist/enums/trade-status-type.js","webpack://roblox-plus/./libs/roblox/dist/index.js","webpack://roblox-plus/./libs/roblox/dist/utils/linkify.js","webpack://roblox-plus/./node_modules/db.js/dist/db.min.js","webpack://roblox-plus/./src/js/service-worker/notifiers/catalog/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/friend-presence/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/group-shout/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/trades/index.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-contents-url.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-dependencies.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-details.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-sales-count.ts","webpack://roblox-plus/./src/js/services/assets/index.ts","webpack://roblox-plus/./src/js/services/avatar/get-avatar-asset-rules.ts","webpack://roblox-plus/./src/js/services/avatar/index.ts","webpack://roblox-plus/./src/js/services/badges/batchProcessor.ts","webpack://roblox-plus/./src/js/services/badges/index.ts","webpack://roblox-plus/./src/js/services/currency/getRobuxBalance.ts","webpack://roblox-plus/./src/js/services/currency/history.ts","webpack://roblox-plus/./src/js/services/currency/index.ts","webpack://roblox-plus/./src/js/services/followings/authenticatedUserFollowingProcessor.ts","webpack://roblox-plus/./src/js/services/followings/index.ts","webpack://roblox-plus/./src/js/services/followings/isAuthenticatedUserFollowing.ts","webpack://roblox-plus/./src/js/services/friends/getFriendRequestCount.ts","webpack://roblox-plus/./src/js/services/friends/getUserFriends.ts","webpack://roblox-plus/./src/js/services/friends/index.ts","webpack://roblox-plus/./src/js/services/game-launch/index.ts","webpack://roblox-plus/./src/js/services/game-passes/get-game-pass-sale-count.ts","webpack://roblox-plus/./src/js/services/game-passes/index.ts","webpack://roblox-plus/./src/js/services/groups/get-creator-groups.ts","webpack://roblox-plus/./src/js/services/groups/get-group-shout.ts","webpack://roblox-plus/./src/js/services/groups/get-user-groups.ts","webpack://roblox-plus/./src/js/services/groups/get-user-primary-group.ts","webpack://roblox-plus/./src/js/services/groups/index.ts","webpack://roblox-plus/./src/js/services/inventory/get-asset-owners.ts","webpack://roblox-plus/./src/js/services/inventory/index.ts","webpack://roblox-plus/./src/js/services/inventory/limitedInventory.ts","webpack://roblox-plus/./src/js/services/localization/index.ts","webpack://roblox-plus/./src/js/services/premium-payouts/getPremiumPayoutsSummary.ts","webpack://roblox-plus/./src/js/services/premium-payouts/index.ts","webpack://roblox-plus/./src/js/services/premium/getPremiumExpirationDate.ts","webpack://roblox-plus/./src/js/services/premium/index.ts","webpack://roblox-plus/./src/js/services/presence/batchProcessor.ts","webpack://roblox-plus/./src/js/services/presence/index.ts","webpack://roblox-plus/./src/js/services/private-messages/getUnreadMessageCount.ts","webpack://roblox-plus/./src/js/services/private-messages/index.ts","webpack://roblox-plus/./src/js/services/settings/index.ts","webpack://roblox-plus/./src/js/services/thumbnails/batchProcessor.ts","webpack://roblox-plus/./src/js/services/thumbnails/index.ts","webpack://roblox-plus/./src/js/services/trades/getTradeCount.ts","webpack://roblox-plus/./src/js/services/trades/index.ts","webpack://roblox-plus/./src/js/services/transactions/email-transactions.ts","webpack://roblox-plus/./src/js/services/transactions/index.ts","webpack://roblox-plus/./src/js/services/users/get-user-by-id.ts","webpack://roblox-plus/./src/js/services/users/get-user-by-name.ts","webpack://roblox-plus/./src/js/services/users/getAuthenticatedUser.ts","webpack://roblox-plus/./src/js/services/users/index.ts","webpack://roblox-plus/./src/js/utils/expireableDictionary.ts","webpack://roblox-plus/./src/js/utils/fetchDataUri.ts","webpack://roblox-plus/./src/js/utils/launchProtocolUrl.ts","webpack://roblox-plus/./src/js/utils/xsrfFetch.ts","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/batch/index.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/events/errorEvent.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/events/itemErrorEvent.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/index.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/promise-queue/index.js","webpack://roblox-plus/webpack/bootstrap","webpack://roblox-plus/webpack/runtime/compat get default export","webpack://roblox-plus/webpack/runtime/define property getters","webpack://roblox-plus/webpack/runtime/hasOwnProperty shorthand","webpack://roblox-plus/webpack/runtime/make namespace object","webpack://roblox-plus/./src/js/service-worker/index.ts"],"sourcesContent":["// An identifier that tells us which version of the messaging service we're using,\n// to ensure we don't try to process a message not intended for us.\nconst version = 2.5;\nexport { version };\n","var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar _a;\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nimport { version } from './constants';\n// All the listeners, set in the background page.\nconst listeners = {};\n// Keep track of all the listeners that accept external calls.\nconst externalListeners = {};\nconst externalResponseHandlers = {};\n// Send a message to a destination, and get back the result.\nconst sendMessage = (destination, message, external) => __awaiter(void 0, void 0, void 0, function* () {\n return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {\n var _b;\n const serializedMessage = JSON.stringify(message);\n if (isBackgroundPage) {\n // Message is from the background page, to the background page.\n try {\n if (listeners[destination]) {\n const message = JSON.parse(serializedMessage);\n const result = yield listeners[destination](message);\n console.debug(`Local listener response for '${destination}':`, result, message);\n const data = result.data === undefined ? undefined : JSON.parse(result.data);\n if (result.success) {\n resolve(data);\n }\n else {\n reject(data);\n }\n }\n else {\n reject(`No message listener: ${destination}`);\n }\n }\n catch (e) {\n reject(e);\n }\n }\n else if (chrome === null || chrome === void 0 ? void 0 : chrome.runtime) {\n // Message is being sent from the content script\n const outboundMessage = JSON.stringify({\n version,\n destination,\n external,\n message: serializedMessage,\n });\n console.debug(`Sending message to '${destination}'`, serializedMessage);\n chrome.runtime.sendMessage(outboundMessage, (result) => {\n if (result === undefined) {\n reject(`Unexpected message result (undefined), suggests no listener in background page.\\n\\tDestination: ${destination}`);\n return;\n }\n const data = result.data === undefined ? undefined : JSON.parse(result.data);\n if (result.success) {\n resolve(data);\n }\n else {\n reject(data);\n }\n });\n }\n else if ((_b = document.body) === null || _b === void 0 ? void 0 : _b.dataset.extensionId) {\n // Message is being sent by the native browser tab.\n const messageId = crypto.randomUUID();\n const timeout = setTimeout(() => {\n if (externalResponseHandlers[messageId]) {\n delete externalResponseHandlers[messageId];\n reject(`Message timed out trying to contact extension`);\n }\n }, 15 * 1000);\n externalResponseHandlers[messageId] = {\n resolve: (result) => {\n clearTimeout(timeout);\n delete externalResponseHandlers[messageId];\n resolve(result);\n },\n reject: (error) => {\n clearTimeout(timeout);\n delete externalResponseHandlers[messageId];\n reject(error);\n },\n };\n window.postMessage({\n version,\n extensionId: document.body.dataset.extensionId,\n destination,\n message,\n messageId,\n });\n }\n else {\n reject(`Could not find a way to transport the message to the extension.`);\n }\n }));\n});\n// Listen for messages at a specific destination.\nconst addListener = (destination, listener, options = {\n levelOfParallelism: -1,\n}) => {\n if (listeners[destination]) {\n throw new Error(`${destination} already has message listener attached`);\n }\n const processMessage = (message) => __awaiter(void 0, void 0, void 0, function* () {\n try {\n console.debug(`Processing message for '${destination}'`, message);\n const result = yield listener(message);\n const response = {\n success: true,\n data: JSON.stringify(result),\n };\n console.debug(`Successful message result from '${destination}':`, response, message);\n return response;\n }\n catch (err) {\n const response = {\n success: false,\n data: JSON.stringify(err),\n };\n console.debug(`Failed message result from '${destination}':`, response, message, err);\n return response;\n }\n });\n listeners[destination] = (message) => {\n if (options.levelOfParallelism !== 1) {\n return processMessage(message);\n }\n return new Promise((resolve, reject) => {\n // https://stackoverflow.com/a/73482349/1663648\n navigator.locks\n .request(`messageService:${destination}`, () => __awaiter(void 0, void 0, void 0, function* () {\n try {\n const result = yield processMessage(message);\n resolve(result);\n }\n catch (e) {\n reject(e);\n }\n }))\n .catch(reject);\n });\n };\n if (options.allowExternalConnections) {\n externalListeners[destination] = true;\n }\n};\n// If we're currently in the background page, listen for messages.\nif (isBackgroundPage) {\n chrome.runtime.onMessage.addListener((rawMessage, sender, sendResponse) => {\n if (typeof rawMessage !== 'string') {\n // Not for us.\n return;\n }\n const fullMessage = JSON.parse(rawMessage);\n if (fullMessage.version !== version ||\n !fullMessage.destination ||\n !fullMessage.message) {\n // Not for us.\n return;\n }\n if (fullMessage.external && !externalListeners[fullMessage.destination]) {\n sendResponse({\n success: false,\n data: JSON.stringify('Listener does not accept external callers.'),\n });\n return;\n }\n const listener = listeners[fullMessage.destination];\n if (!listener) {\n sendResponse({\n success: false,\n data: JSON.stringify(`Could not route message to destination: ${fullMessage.destination}`),\n });\n return;\n }\n const message = JSON.parse(fullMessage.message);\n listener(message)\n .then(sendResponse)\n .catch((err) => {\n console.error('Listener is never expected to throw.', err, rawMessage, fullMessage);\n sendResponse({\n success: false,\n data: JSON.stringify('Listener threw unhandled exception (see background page for error).'),\n });\n });\n // Required for asynchronous callbacks\n // https://stackoverflow.com/a/20077854/1663648\n return true;\n });\n}\nelse if ((_a = globalThis.chrome) === null || _a === void 0 ? void 0 : _a.runtime) {\n console.debug(`Not attaching listener for messages, because we're not in the background.`);\n if (!window.messageServiceConnection) {\n const port = (window.messageServiceConnection = chrome.runtime.connect(chrome.runtime.id, {\n name: 'messageService',\n }));\n port.onMessage.addListener((rawMessage) => {\n if (typeof rawMessage !== 'string') {\n // Not for us.\n return;\n }\n const fullMessage = JSON.parse(rawMessage);\n if (fullMessage.version !== version ||\n !fullMessage.destination ||\n !fullMessage.message) {\n // Not for us.\n return;\n }\n const listener = listeners[fullMessage.destination];\n if (!listener) {\n // No listener in this tab for this message.\n return;\n }\n // We don't really have a way to communicate the response back to the service worker.\n // So we just... do nothing with it.\n const message = JSON.parse(fullMessage.message);\n listener(message).catch((err) => {\n console.error('Unhandled error processing message in tab', fullMessage, err);\n });\n });\n }\n // chrome.runtime is available, and we got a message from the window\n // this could be a tab trying to get information from the extension\n window.addEventListener('message', (messageEvent) => __awaiter(void 0, void 0, void 0, function* () {\n const { extensionId, messageId, destination, message } = messageEvent.data;\n if (extensionId !== chrome.runtime.id ||\n !messageId ||\n !destination ||\n !message) {\n // They didn't want to contact us.\n // Or if they did, they didn't have the required fields.\n return;\n }\n if (messageEvent.data.version !== version) {\n // They did want to contact us, but there was a version mismatch.\n // We can't handle this message.\n window.postMessage({\n extensionId,\n messageId,\n success: false,\n data: `Extension message receiver is incompatible with message sender`,\n });\n return;\n }\n console.debug('Received message for', destination, message);\n try {\n const response = yield sendMessage(destination, message, true);\n // Success! Now go tell the client they got everything they wanted.\n window.postMessage({\n extensionId,\n messageId,\n success: true,\n data: response,\n });\n }\n catch (e) {\n console.debug('Failed to send message to', destination, e);\n // :coffin:\n window.postMessage({\n extensionId,\n messageId,\n success: false,\n data: e,\n });\n }\n }));\n}\nelse {\n // Not a background page, and not a content script.\n // This could be a page where we want to listen for calls from the tab.\n window.addEventListener('message', (messageEvent) => {\n const { extensionId, messageId, success, data } = messageEvent.data;\n if (extensionId !== document.body.dataset.extensionId ||\n !messageId ||\n typeof success !== 'boolean') {\n // Not for us.\n return;\n }\n // Check to see if we have a handler waiting for this message response...\n const responseHandler = externalResponseHandlers[messageId];\n if (!responseHandler) {\n console.warn('We got a response back for a message we no longer have a handler for.', extensionId, messageId, success, data);\n return;\n }\n // Yay! Tell the krustomer we have their data, from the extension.\n console.debug('We received a response for', messageId, success, data);\n if (success) {\n responseHandler.resolve(data);\n }\n else {\n responseHandler.reject(data);\n }\n });\n}\nexport { getWorkerTab, sendMessageToTab } from './tabs';\nexport { addListener, sendMessage };\n","var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nimport { version } from './constants';\n// All the tabs actively connected to the message service.\nconst tabs = {};\n// Sends a message to a tab.\nconst sendMessageToTab = (destination, message, tab) => __awaiter(void 0, void 0, void 0, function* () {\n const serializedMessage = JSON.stringify(message);\n const outboundMessage = JSON.stringify({\n version,\n destination,\n message: serializedMessage,\n });\n console.debug(`Sending message to '${destination}' in tab`, serializedMessage, tab);\n tab.postMessage(outboundMessage);\n});\n// Fetches a tab that we can send a message to, for work processing.\nconst getWorkerTab = () => {\n const keys = Object.keys(tabs);\n return keys.length > 0 ? tabs[keys[0]] : undefined;\n};\nif (isBackgroundPage) {\n chrome.runtime.onConnect.addListener((port) => {\n const id = crypto.randomUUID();\n console.debug('Tab connected', id, port);\n tabs[id] = port;\n port.onDisconnect.addListener(() => {\n console.debug('Disconnecting tab', id, port);\n delete tabs[id];\n });\n });\n}\nexport { getWorkerTab, sendMessageToTab };\n","var _a, _b, _c, _d, _e;\nconst manifest = (_b = (_a = globalThis.chrome) === null || _a === void 0 ? void 0 : _a.runtime) === null || _b === void 0 ? void 0 : _b.getManifest();\nconst isBackgroundPage = ((_d = (_c = globalThis.chrome) === null || _c === void 0 ? void 0 : _c.runtime) === null || _d === void 0 ? void 0 : _d.getURL(((_e = manifest === null || manifest === void 0 ? void 0 : manifest.background) === null || _e === void 0 ? void 0 : _e.page) || '')) ===\n location.href;\nexport { isBackgroundPage, manifest };\n","// A generic loading state enum.\nvar LoadingState;\n(function (LoadingState) {\n LoadingState[\"Loading\"] = \"Loading\";\n LoadingState[\"Success\"] = \"Success\";\n LoadingState[\"Error\"] = \"Error\";\n})(LoadingState || (LoadingState = {}));\nexport default LoadingState;\n","// Export constants\nexport * from './constants';\n// Export enums\nexport { default as LoadingState } from './enums/loading-state';\n// Export utils\nexport { default as wait } from './utils/wait';\n","const wait = (time) => {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, time);\n });\n};\nexport default wait;\n","var AssetType;\n(function (AssetType) {\n AssetType[AssetType[\"Image\"] = 1] = \"Image\";\n AssetType[AssetType[\"TShirt\"] = 2] = \"TShirt\";\n AssetType[AssetType[\"Audio\"] = 3] = \"Audio\";\n AssetType[AssetType[\"Mesh\"] = 4] = \"Mesh\";\n AssetType[AssetType[\"Lua\"] = 5] = \"Lua\";\n AssetType[AssetType[\"Html\"] = 6] = \"Html\";\n AssetType[AssetType[\"Text\"] = 7] = \"Text\";\n AssetType[AssetType[\"Hat\"] = 8] = \"Hat\";\n AssetType[AssetType[\"Place\"] = 9] = \"Place\";\n AssetType[AssetType[\"Model\"] = 10] = \"Model\";\n AssetType[AssetType[\"Shirt\"] = 11] = \"Shirt\";\n AssetType[AssetType[\"Pants\"] = 12] = \"Pants\";\n AssetType[AssetType[\"Decal\"] = 13] = \"Decal\";\n AssetType[AssetType[\"Avatar\"] = 16] = \"Avatar\";\n AssetType[AssetType[\"Head\"] = 17] = \"Head\";\n AssetType[AssetType[\"Face\"] = 18] = \"Face\";\n AssetType[AssetType[\"Gear\"] = 19] = \"Gear\";\n AssetType[AssetType[\"Badge\"] = 21] = \"Badge\";\n AssetType[AssetType[\"GroupEmblem\"] = 22] = \"GroupEmblem\";\n AssetType[AssetType[\"Animation\"] = 24] = \"Animation\";\n AssetType[AssetType[\"Arms\"] = 25] = \"Arms\";\n AssetType[AssetType[\"Legs\"] = 26] = \"Legs\";\n AssetType[AssetType[\"Torso\"] = 27] = \"Torso\";\n AssetType[AssetType[\"RightArm\"] = 28] = \"RightArm\";\n AssetType[AssetType[\"LeftArm\"] = 29] = \"LeftArm\";\n AssetType[AssetType[\"LeftLeg\"] = 30] = \"LeftLeg\";\n AssetType[AssetType[\"RightLeg\"] = 31] = \"RightLeg\";\n AssetType[AssetType[\"Package\"] = 32] = \"Package\";\n AssetType[AssetType[\"YouTubeVideo\"] = 33] = \"YouTubeVideo\";\n AssetType[AssetType[\"GamePass\"] = 34] = \"GamePass\";\n AssetType[AssetType[\"App\"] = 35] = \"App\";\n AssetType[AssetType[\"Code\"] = 37] = \"Code\";\n AssetType[AssetType[\"Plugin\"] = 38] = \"Plugin\";\n AssetType[AssetType[\"SolidModel\"] = 39] = \"SolidModel\";\n AssetType[AssetType[\"MeshPart\"] = 40] = \"MeshPart\";\n AssetType[AssetType[\"HairAccessory\"] = 41] = \"HairAccessory\";\n AssetType[AssetType[\"FaceAccessory\"] = 42] = \"FaceAccessory\";\n AssetType[AssetType[\"NeckAccessory\"] = 43] = \"NeckAccessory\";\n AssetType[AssetType[\"ShoulderAccessory\"] = 44] = \"ShoulderAccessory\";\n AssetType[AssetType[\"FrontAccessory\"] = 45] = \"FrontAccessory\";\n AssetType[AssetType[\"BackAccessory\"] = 46] = \"BackAccessory\";\n AssetType[AssetType[\"WaistAccessory\"] = 47] = \"WaistAccessory\";\n AssetType[AssetType[\"ClimbAnimation\"] = 48] = \"ClimbAnimation\";\n AssetType[AssetType[\"DeathAnimation\"] = 49] = \"DeathAnimation\";\n AssetType[AssetType[\"FallAnimation\"] = 50] = \"FallAnimation\";\n AssetType[AssetType[\"IdleAnimation\"] = 51] = \"IdleAnimation\";\n AssetType[AssetType[\"JumpAnimation\"] = 52] = \"JumpAnimation\";\n AssetType[AssetType[\"RunAnimation\"] = 53] = \"RunAnimation\";\n AssetType[AssetType[\"SwimAnimation\"] = 54] = \"SwimAnimation\";\n AssetType[AssetType[\"WalkAnimation\"] = 55] = \"WalkAnimation\";\n AssetType[AssetType[\"PoseAnimation\"] = 56] = \"PoseAnimation\";\n AssetType[AssetType[\"EarAccessory\"] = 57] = \"EarAccessory\";\n AssetType[AssetType[\"EyeAccessory\"] = 58] = \"EyeAccessory\";\n AssetType[AssetType[\"LocalizationTableManifest\"] = 59] = \"LocalizationTableManifest\";\n AssetType[AssetType[\"LocalizationTableTranslation\"] = 60] = \"LocalizationTableTranslation\";\n AssetType[AssetType[\"Emote\"] = 61] = \"Emote\";\n AssetType[AssetType[\"Video\"] = 62] = \"Video\";\n AssetType[AssetType[\"TexturePack\"] = 63] = \"TexturePack\";\n AssetType[AssetType[\"TShirtAccessory\"] = 64] = \"TShirtAccessory\";\n AssetType[AssetType[\"ShirtAccessory\"] = 65] = \"ShirtAccessory\";\n AssetType[AssetType[\"PantsAccessory\"] = 66] = \"PantsAccessory\";\n AssetType[AssetType[\"JacketAccessory\"] = 67] = \"JacketAccessory\";\n AssetType[AssetType[\"SweaterAccessory\"] = 68] = \"SweaterAccessory\";\n AssetType[AssetType[\"ShortsAccessory\"] = 69] = \"ShortsAccessory\";\n AssetType[AssetType[\"LeftShoeAccessory\"] = 70] = \"LeftShoeAccessory\";\n AssetType[AssetType[\"RightShoeAccessory\"] = 71] = \"RightShoeAccessory\";\n AssetType[AssetType[\"DressSkirtAccessory\"] = 72] = \"DressSkirtAccessory\";\n AssetType[AssetType[\"FontFamily\"] = 73] = \"FontFamily\";\n AssetType[AssetType[\"FontFace\"] = 74] = \"FontFace\";\n AssetType[AssetType[\"MeshHiddenSurfaceRemoval\"] = 75] = \"MeshHiddenSurfaceRemoval\";\n AssetType[AssetType[\"EyebrowAccessory\"] = 76] = \"EyebrowAccessory\";\n AssetType[AssetType[\"EyelashAccessory\"] = 77] = \"EyelashAccessory\";\n AssetType[AssetType[\"MoodAnimation\"] = 78] = \"MoodAnimation\";\n AssetType[AssetType[\"DynamicHead\"] = 79] = \"DynamicHead\";\n})(AssetType || (AssetType = {}));\nexport default AssetType;\n","// The types of user presence.\nvar PresenceType;\n(function (PresenceType) {\n // The user is offline.\n PresenceType[\"Offline\"] = \"Offline\";\n // The user is online.\n PresenceType[\"Online\"] = \"Online\";\n // The user is currently in an experience.\n PresenceType[\"Experience\"] = \"Experience\";\n // The user is currently in Roblox Studio.\n PresenceType[\"Studio\"] = \"Studio\";\n})(PresenceType || (PresenceType = {}));\nexport default PresenceType;\n","// Possible states for a thumbnail to be in.\nvar ThumbnailState;\n(function (ThumbnailState) {\n // The thumbnail had an unexpected error trying to load.\n ThumbnailState[\"Error\"] = \"Error\";\n // The thumbnailed loaded successfully.\n ThumbnailState[\"Completed\"] = \"Completed\";\n // The thumbnail is currently in review.\n ThumbnailState[\"InReview\"] = \"InReview\";\n // The thumbnail is pending, and should be retried.\n ThumbnailState[\"Pending\"] = \"Pending\";\n // The thumbnail is blocked.\n ThumbnailState[\"Blocked\"] = \"Blocked\";\n // The thumbnail is temporarily unavailable.\n ThumbnailState[\"TemporarilyUnavailable\"] = \"TemporarilyUnavailable\";\n})(ThumbnailState || (ThumbnailState = {}));\nexport default ThumbnailState;\n","// The types of thumbnails that can be requested.\nvar ThumbnailType;\n(function (ThumbnailType) {\n // An avatar head shot thumbnail.\n ThumbnailType[\"AvatarHeadShot\"] = \"AvatarHeadShot\";\n // The thumbnail for an asset.\n ThumbnailType[\"Asset\"] = \"Asset\";\n // The icon for a group.\n ThumbnailType[\"GroupIcon\"] = \"GroupIcon\";\n // The icon for a game pass.\n ThumbnailType[\"GamePass\"] = \"GamePass\";\n // The icon for a developer product.\n ThumbnailType[\"DeveloperProduct\"] = \"DeveloperProduct\";\n // The icon for a game.\n ThumbnailType[\"GameIcon\"] = \"GameIcon\";\n})(ThumbnailType || (ThumbnailType = {}));\nexport default ThumbnailType;\n","var TradeStatusType;\n(function (TradeStatusType) {\n TradeStatusType[\"Inbound\"] = \"Inbound\";\n TradeStatusType[\"Outbound\"] = \"Outbound\";\n TradeStatusType[\"Completed\"] = \"Completed\";\n TradeStatusType[\"Inactive\"] = \"Inactive\";\n})(TradeStatusType || (TradeStatusType = {}));\nexport default TradeStatusType;\n","// Export enums\nexport { default as AssetType } from './enums/asset-type';\nexport { default as PresenceType } from './enums/presence-type';\nexport { default as ThumbnailState } from './enums/thumbnail-state';\nexport { default as ThumbnailType } from './enums/thumbnail-type';\nexport { default as TradeStatusType } from './enums/trade-status-type';\n// Export utils\nexport * from './utils/linkify';\n","const getSEOLink = (id, name, path) => {\n if (!name) {\n name = 'redirect';\n }\n else {\n name =\n name\n .replace(/'/g, '')\n .replace(/\\W+/g, '-')\n .replace(/^-+/, '')\n .replace(/-+$/, '') || 'redirect';\n }\n return new URL(`https://www.roblox.com/${path}/${id}/${name}`);\n};\nconst getGroupLink = (groupId, groupName) => {\n return getSEOLink(groupId, groupName, 'groups');\n};\nconst getGamePassLink = (gamePassId, gamePassName) => {\n return getSEOLink(gamePassId, gamePassName, 'game-pass');\n};\nconst getCatalogLink = (assetId, assetName) => {\n return getSEOLink(assetId, assetName, 'catalog');\n};\nconst getLibraryLink = (assetId, assetName) => {\n return getSEOLink(assetId, assetName, 'library');\n};\nconst getPlaceLink = (placeId, placeName) => {\n return getSEOLink(placeId, placeName, 'games');\n};\nconst getUserProfileLink = (userId) => {\n return getSEOLink(userId, 'profile', 'users');\n};\nconst getIdFromUrl = (url) => {\n const match = url.pathname.match(/^\\/(badges|games|game-pass|groups|catalog|library|users)\\/(\\d+)\\/?/i) || [];\n // Returns NaN if the URL doesn't match.\n return Number(match[2]);\n};\nexport { getCatalogLink, getGamePassLink, getGroupLink, getIdFromUrl, getLibraryLink, getPlaceLink, getUserProfileLink, };\n","!function(a){if(\"object\"==typeof exports&&\"undefined\"!=typeof module)module.exports=a();else if(\"function\"==typeof define&&define.amd)define([],a);else{var b;b=\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:this,b.db=a()}}(function(){var a;return function b(a,c,d){function e(g,h){if(!c[g]){if(!a[g]){var i=\"function\"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error(\"Cannot find module '\"+g+\"'\");throw j.code=\"MODULE_NOT_FOUND\",j}var k=c[g]={exports:{}};a[g][0].call(k.exports,function(b){var c=a[g][1][b];return e(c?c:b)},k,k.exports,b,a,c,d)}return c[g].exports}for(var f=\"function\"==typeof require&&require,g=0;gu)u=m[0],b.advance(m[0]);else if(null!==m&&u>=m[0]+m[1]);else{var c=function(){var a=!0,c=\"value\"in b?b.value:b.key;try{n.forEach(function(b){a=\"function\"==typeof b[0]?a&&b[0](c):a&&c[b[0]]===b[1]})}catch(d){return q(d),{v:void 0}}if(a){if(u++,i)try{c=A(c),b.update(c)}catch(d){return q(d),{v:void 0}}try{t.push(o(c))}catch(d){return q(d),{v:void 0}}}b[\"continue\"]()}();if(\"object\"===(\"undefined\"==typeof c?\"undefined\":g(c)))return c.v}}})},n=function(a,b,c){var e=[],f=\"next\",h=\"openCursor\",j=null,k=m,n=!1,o=d||c,p=function(){return o?Promise.reject(o):l(a,b,h,n?f+\"unique\":f,j,e,k)},q=function(){return f=null,h=\"count\",{execute:p}},r=function(){return h=\"openKeyCursor\",{desc:u,distinct:v,execute:p,filter:t,limit:s,map:x}},s=function(a,b){return j=b?[a,b]:[0,a],o=j.some(function(a){return\"number\"!=typeof a})?new Error(\"limit() arguments must be numeric\"):o,{desc:u,distinct:v,filter:t,keys:r,execute:p,map:x,modify:w}},t=function y(a,b){return e.push([a,b]),{desc:u,distinct:v,execute:p,filter:y,keys:r,limit:s,map:x,modify:w}},u=function(){return f=\"prev\",{distinct:v,execute:p,filter:t,keys:r,limit:s,map:x,modify:w}},v=function(){return n=!0,{count:q,desc:u,execute:p,filter:t,keys:r,limit:s,map:x,modify:w}},w=function(a){return i=a&&\"object\"===(\"undefined\"==typeof a?\"undefined\":g(a))?a:null,{execute:p}},x=function(a){return k=a,{count:q,desc:u,distinct:v,execute:p,filter:t,keys:r,limit:s,modify:w}};return{count:q,desc:u,distinct:v,execute:p,filter:t,keys:r,limit:s,map:x,modify:w}};[\"only\",\"bound\",\"upperBound\",\"lowerBound\"].forEach(function(a){f[a]=function(){return n(a,arguments)}}),this.range=function(a){var b=void 0,c=[null,null];try{c=h(a)}catch(d){b=d}return n.apply(void 0,e(c).concat([b]))},this.filter=function(){var a=n(null,null);return a.filter.apply(a,arguments)},this.all=function(){return this.filter()}},r=function(a,b,c,e){var f=this,g=!1;if(this.getIndexedDB=function(){return a},this.isClosed=function(){return g},this.query=function(b,c){var d=g?new Error(\"Database has been closed\"):null;return new q(b,a,c,d)},this.add=function(b){for(var c=arguments.length,e=Array(c>1?c-1:0),f=1;c>f;f++)e[f-1]=arguments[f];return new Promise(function(c,f){if(g)return void f(new Error(\"Database has been closed\"));var h=e.reduce(function(a,b){return a.concat(b)},[]),j=a.transaction(b,k.readwrite);j.onerror=function(a){a.preventDefault(),f(a)},j.onabort=function(a){return f(a)},j.oncomplete=function(){return c(h)};var m=j.objectStore(b);h.some(function(a){var b=void 0,c=void 0;if(d(a)&&l.call(a,\"item\")&&(c=a.key,a=a.item,null!=c))try{c=i(c)}catch(e){return f(e),!0}try{b=null!=c?m.add(a,c):m.add(a)}catch(e){return f(e),!0}b.onsuccess=function(b){if(d(a)){var c=b.target,e=c.source.keyPath;null===e&&(e=\"__id__\"),l.call(a,e)||Object.defineProperty(a,e,{value:c.result,enumerable:!0})}}})})},this.update=function(b){for(var c=arguments.length,e=Array(c>1?c-1:0),f=1;c>f;f++)e[f-1]=arguments[f];return new Promise(function(c,f){if(g)return void f(new Error(\"Database has been closed\"));var h=e.reduce(function(a,b){return a.concat(b)},[]),j=a.transaction(b,k.readwrite);j.onerror=function(a){a.preventDefault(),f(a)},j.onabort=function(a){return f(a)},j.oncomplete=function(){return c(h)};var m=j.objectStore(b);h.some(function(a){var b=void 0,c=void 0;if(d(a)&&l.call(a,\"item\")&&(c=a.key,a=a.item,null!=c))try{c=i(c)}catch(e){return f(e),!0}try{b=null!=c?m.put(a,c):m.put(a)}catch(g){return f(g),!0}b.onsuccess=function(b){if(d(a)){var c=b.target,e=c.source.keyPath;null===e&&(e=\"__id__\"),l.call(a,e)||Object.defineProperty(a,e,{value:c.result,enumerable:!0})}}})})},this.put=function(){return this.update.apply(this,arguments)},this.remove=function(b,c){return new Promise(function(d,e){if(g)return void e(new Error(\"Database has been closed\"));try{c=i(c)}catch(f){return void e(f)}var h=a.transaction(b,k.readwrite);h.onerror=function(a){a.preventDefault(),e(a)},h.onabort=function(a){return e(a)},h.oncomplete=function(){return d(c)};var j=h.objectStore(b);try{j[\"delete\"](c)}catch(l){e(l)}})},this[\"delete\"]=function(){return this.remove.apply(this,arguments)},this.clear=function(b){return new Promise(function(c,d){if(g)return void d(new Error(\"Database has been closed\"));var e=a.transaction(b,k.readwrite);e.onerror=function(a){return d(a)},e.onabort=function(a){return d(a)},e.oncomplete=function(){return c()};var f=e.objectStore(b);f.clear()})},this.close=function(){return new Promise(function(d,e){return g?void e(new Error(\"Database has been closed\")):(a.close(),g=!0,delete o[b][c],void d())})},this.get=function(b,c){return new Promise(function(d,e){if(g)return void e(new Error(\"Database has been closed\"));try{c=i(c)}catch(f){return void e(f)}var h=a.transaction(b);h.onerror=function(a){a.preventDefault(),e(a)},h.onabort=function(a){return e(a)};var j=h.objectStore(b),k=void 0;try{k=j.get(c)}catch(l){e(l)}k.onsuccess=function(a){return d(a.target.result)}})},this.count=function(b,c){return new Promise(function(d,e){if(g)return void e(new Error(\"Database has been closed\"));try{c=i(c)}catch(f){return void e(f)}var h=a.transaction(b);h.onerror=function(a){a.preventDefault(),e(a)},h.onabort=function(a){return e(a)};var j=h.objectStore(b),k=void 0;try{k=null==c?j.count():j.count(c)}catch(l){e(l)}k.onsuccess=function(a){return d(a.target.result)}})},this.addEventListener=function(b,c){if(!p.includes(b))throw new Error(\"Unrecognized event type \"+b);return\"error\"===b?void a.addEventListener(b,function(a){a.preventDefault(),c(a)}):void a.addEventListener(b,c)},this.removeEventListener=function(b,c){if(!p.includes(b))throw new Error(\"Unrecognized event type \"+b);a.removeEventListener(b,c)},p.forEach(function(a){this[a]=function(b){return this.addEventListener(a,b),this}},this),!e){var h=void 0;return[].some.call(a.objectStoreNames,function(a){if(f[a])return h=new Error('The store name, \"'+a+'\", which you have attempted to load, conflicts with db.js method names.\"'),f.close(),!0;f[a]={};var b=Object.keys(f);b.filter(function(a){return![].concat(p,[\"close\",\"addEventListener\",\"removeEventListener\"]).includes(a)}).map(function(b){return f[a][b]=function(){for(var c=arguments.length,d=Array(c),e=0;c>e;e++)d[e]=arguments[e];return f[b].apply(f,[a].concat(d))}})}),h}},s=function(a,b,c,d,e,f){if(c&&0!==c.length){for(var h=0;h {\n return getToggleSettingValue('itemNotifier');\n};\nconst updateToken = () => {\n return new Promise(async (resolve, reject) => {\n try {\n const enabled = await isEnabled();\n if (!enabled) {\n // Do nothing if the notifier is not enabled.\n resolve();\n return;\n }\n const authenticatedUser = await getAuthenticatedUser();\n // @ts-ignore:next-line: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65809\n chrome.instanceID.getToken({ authorizedEntity: '303497097698', scope: 'FCM' }, (token) => {\n fetch('https://api.roblox.plus/v2/itemnotifier/registertoken', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: `robloxUserId=${authenticatedUser?.id}&token=${encodeURIComponent(token)}`,\n })\n .then((response) => {\n if (response.ok) {\n resolve();\n }\n else {\n reject();\n }\n })\n .catch(reject);\n });\n }\n catch (err) {\n reject(err);\n }\n });\n};\nconst shouldShowNotification = async (creatorName) => {\n // This logic is no longer valid, but still in use. It doesn't support group creators, it assumes all creators are users that can be followed.\n // As a result: No notifications for group-created items will be shown.\n if (!creatorName) {\n // If there's no creator on the notification, it is assumed to be created by the Roblox account.\n // And of course everyone wants these notifications.. right?\n return true;\n }\n const authenticatedUser = await getAuthenticatedUser();\n if (!authenticatedUser) {\n // Not logged in, no notification.\n return false;\n }\n if (authenticatedUser.name === creatorName) {\n // Of course you always want to see your own notifications.\n return true;\n }\n const creator = await getUserByName(creatorName);\n if (!creator) {\n // Couldn't determine who the user is, so no notification will be visible. Cool.\n return false;\n }\n // And the final kicker... you can only see notifications if you follow the creator.\n const isFollowing = await isAuthenticatedUserFollowing(creator.id);\n return isFollowing;\n};\nconst processNotification = async (notification) => {\n const showNotification = await shouldShowNotification(notification.items?.Creator);\n if (!showNotification) {\n console.log('Skipping notification, likely because the authenticated user does not follow the creator', notification);\n return;\n }\n const requireProperties = ['icon', 'url', 'title', 'message'];\n for (let i = 0; i < requireProperties.length; i++) {\n if (!notification[requireProperties[i]]) {\n console.warn(`Skipping notification because there is no ${requireProperties[i]}`, notification);\n return;\n }\n }\n //console.log('Building notification', notification);\n const iconUrl = await fetchDataUri(new URL(notification.icon));\n const notificationOptions = {\n type: 'basic',\n iconUrl,\n title: notification.title,\n message: notification.message,\n };\n if (notification.items && Object.keys(notification.items).length > 0) {\n notificationOptions.type = 'list';\n notificationOptions.items = [];\n notificationOptions.contextMessage = notification.message;\n for (let title in notification.items) {\n notificationOptions.items.push({\n title,\n message: notification.items[title],\n });\n }\n }\n console.log('Displaying notification', notificationOptions, notification);\n chrome.notifications.create(`${notificationIdPrefix}${notification.url}`, notificationOptions, () => { });\n};\nconst processMessage = async (message) => {\n try {\n const enabled = await isEnabled();\n if (!enabled) {\n return;\n }\n console.log('Processing gcm message', message);\n switch (message.from) {\n case '/topics/catalog-notifier':\n case '/topics/catalog-notifier-premium':\n if (!message.data?.notification) {\n console.warn('Failed to parse gcm message notification', message);\n return;\n }\n await processNotification(JSON.parse(message.data.notification));\n return;\n default:\n console.warn('Unknown gcm message sender', message);\n return;\n }\n }\n catch (err) {\n console.error('Failed to process gcm message', err, message);\n }\n};\n// @ts-ignore:next-line: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65809\nchrome.instanceID.onTokenRefresh.addListener(updateToken);\nchrome.gcm.onMessage.addListener(processMessage);\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n const url = notificationId.substring(notificationIdPrefix.length);\n if (!url.startsWith('https://www.roblox.com/')) {\n console.warn('Skipped opening URL for notification because it was not for roblox.com', notificationId);\n return;\n }\n chrome.tabs.create({\n url,\n active: true,\n });\n});\n/*\n// Exists for debugging\ndeclare global {\n var processMessage: any;\n}\n\nwindow.processMessage = processMessage;\n//*/\nexport default async (nextTokenUpdate) => {\n const enabled = await isEnabled();\n if (!enabled) {\n return 0;\n }\n // Check to see if it's time to refresh the token\n const now = +new Date();\n if (nextTokenUpdate && nextTokenUpdate > now) {\n return nextTokenUpdate;\n }\n // Send the token to the server\n await updateToken();\n // Update the token again later\n return now + tokenRefreshInterval;\n};\n","import { PresenceType, getUserProfileLink } from 'roblox';\nimport { isAuthenticatedUserFollowing } from '../../../services/followings';\nimport { getUserFriends } from '../../../services/friends';\nimport { followUser } from '../../../services/game-launch';\nimport { getTranslationResource } from '../../../services/localization';\nimport { getUserPresence } from '../../../services/presence';\nimport { getSettingValue } from '../../../services/settings';\nimport { getAvatarHeadshotThumbnail } from '../../../services/thumbnails';\nimport { getAuthenticatedUser } from '../../../services/users';\nimport fetchDataUri from '../../../utils/fetchDataUri';\n// The prefix for the ID of the notification to display.\nconst notificationIdPrefix = 'friend-notifier-';\n// A method to check if two presences match.\nconst presenceMatches = (a, b) => {\n if (a.type !== b.type) {\n // Not the same presence type, definitely not a match.\n return false;\n }\n if (a.location?.universeId !== b.location?.universeId) {\n // Not the same experience, definitely not a match.\n return false;\n }\n // The type, and location are the same. Must be the same presence.\n return true;\n};\nconst isEnabled = async () => {\n const setting = await getSettingValue('friendNotifier');\n return setting?.on === true;\n};\nconst isPresenceTypeEnabled = async (presenceType) => {\n const setting = await getSettingValue('friendNotifier');\n switch (presenceType) {\n case PresenceType.Online:\n return setting?.online || false;\n case PresenceType.Offline:\n return setting?.offline || false;\n case PresenceType.Experience:\n // If the setting is somehow null, assume we want to know about this one by default.\n if (setting?.game === false) {\n return false;\n }\n return true;\n case PresenceType.Studio:\n default:\n // We don't care about these presence types.\n return false;\n }\n};\n// Gets the icon URL to display on the notification.\nconst getNotificationIconUrl = async (userId) => {\n const thumbnail = await getAvatarHeadshotThumbnail(userId);\n if (!thumbnail.imageUrl) {\n return '';\n }\n try {\n return await fetchDataUri(new URL(thumbnail.imageUrl));\n }\n catch (err) {\n console.error('Failed to fetch icon URL from thumbnail', userId, thumbnail, err);\n return '';\n }\n};\n// Fetches the title for the notification to display to the user, based on current and previous known presence.\nconst getNotificationTitle = (user, presence, previousState) => {\n switch (presence.type) {\n case PresenceType.Offline:\n return `${user.displayName} went offline`;\n case PresenceType.Online:\n if (previousState.type !== PresenceType.Offline) {\n // If they were already online, don't notify them of this again.\n return '';\n }\n return `${user.displayName} is now online`;\n case PresenceType.Experience:\n if (!presence.location?.name) {\n // They joined an experience, but we don't know what they're playing.\n // Don't tell the human what we don't know.\n return '';\n }\n return `${user.displayName} is now playing`;\n case PresenceType.Studio:\n if (!presence.location?.name) {\n // They launched Roblox studio, but we don't know what they're creating.\n // Don't tell the human what we don't know.\n return '';\n }\n if (previousState.type !== PresenceType.Online) {\n // If they went from in-experience -> in-studio, it's possible they just had Roblox studio open\n // while playing a game, and then closed it.\n // Occassionally I have also observed offline <-> Studio swapping back and forth..\n // This creates noise, and we don't like noise.\n return '';\n }\n return `${user.displayName} is now creating`;\n }\n};\n// Gets the buttons that should be displayed on a notification, based on the presence.\nconst getNotificationButtons = async (presence) => {\n if (presence.type === PresenceType.Experience && presence.location?.placeId) {\n const joinText = await getTranslationResource('Feature.PeopleList', 'Action.Join');\n return [\n {\n title: joinText,\n },\n ];\n }\n return [];\n};\n// Handle what happens when a notification is clicked.\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n chrome.tabs.create({\n url: getUserProfileLink(Number(notificationId.substring(notificationIdPrefix.length))).href,\n active: true,\n });\n});\nchrome.notifications.onButtonClicked.addListener(async (notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n const userId = Number(notificationId.substring(notificationIdPrefix.length));\n try {\n await followUser(userId);\n }\n catch (err) {\n console.error('Failed to launch the experience', err);\n }\n});\n// Processes the presences, and send the notifications, when appropriate.\nexport default async (previousStates) => {\n // Check if the notifier is enabled.\n const enabled = await isEnabled();\n if (!enabled) {\n // The feature is not enabled, clear the state, and do nothing.\n return null;\n }\n // Check who is logged in right now.\n const authenticatedUser = await getAuthenticatedUser();\n if (!authenticatedUser) {\n // User is not logged in, no state to return.\n return null;\n }\n // Fetch the friends\n const friends = await getUserFriends(authenticatedUser.id);\n // Check the presence for each of the friends\n const currentState = {};\n await Promise.all(friends.map(async (friend) => {\n const presence = (currentState[friend.id] = await getUserPresence(friend.id));\n const previousState = previousStates && previousStates[friend.id];\n if (previousState && !presenceMatches(previousState, presence)) {\n // The presence for this friend changed, do something!\n const notificationId = notificationIdPrefix + friend.id;\n const buttons = await getNotificationButtons(presence);\n const title = getNotificationTitle(friend, presence, previousState);\n if (!title) {\n // We don't have a title for the notification, so don't show one.\n chrome.notifications.clear(notificationId);\n return;\n }\n const isEnabled = await isPresenceTypeEnabled(presence.type);\n if (!isEnabled) {\n // The authenticated user does not want to know about these types of presence changes.\n chrome.notifications.clear(notificationId);\n return;\n }\n const isFollowing = await isAuthenticatedUserFollowing(friend.id);\n if (!isFollowing) {\n // We're not following this friend, don't show notifications about them.\n chrome.notifications.clear(notificationId);\n return;\n }\n const iconUrl = await getNotificationIconUrl(friend.id);\n if (!iconUrl) {\n // We don't have an icon we can use, so we can't display a notification.\n chrome.notifications.clear(notificationId);\n return;\n }\n chrome.notifications.create(notificationId, {\n type: 'basic',\n iconUrl,\n title,\n message: presence.location?.name ?? '',\n contextMessage: 'Roblox+ Friend Notifier',\n isClickable: true,\n buttons,\n });\n }\n }));\n return currentState;\n};\n","import { ThumbnailState, getGroupLink } from 'roblox';\nimport { getGroupShout, getUserGroups } from '../../../services/groups';\nimport { getSettingValue, getToggleSettingValue, } from '../../../services/settings';\nimport { getGroupIcon } from '../../../services/thumbnails';\nimport { getAuthenticatedUser } from '../../../services/users';\nimport fetchDataUri from '../../../utils/fetchDataUri';\n// The prefix for the ID of the notification to display.\nconst notificationIdPrefix = 'group-shout-notifier-';\n// Returns all the groups that we want to load the group shouts for.\nconst getGroups = async () => {\n const groupMap = [];\n const enabled = await getToggleSettingValue('groupShoutNotifier');\n if (!enabled) {\n // Not enabled, skip.\n return groupMap;\n }\n const authenticatedUser = await getAuthenticatedUser();\n if (!authenticatedUser) {\n // Not logged in, no notifier.\n return groupMap;\n }\n const mode = await getSettingValue('groupShoutNotifier_mode');\n if (mode === 'whitelist') {\n // Only specific groups should be notified on.\n const list = await getSettingValue('groupShoutNotifierList');\n if (typeof list !== 'object') {\n return groupMap;\n }\n for (let rawId in list) {\n const id = Number(rawId);\n if (id && typeof list[rawId] === 'string') {\n groupMap.push({\n id,\n name: list[rawId],\n });\n }\n }\n }\n else {\n // All groups the user is in should be notified on.\n const groups = await getUserGroups(authenticatedUser.id);\n groups.forEach((group) => {\n groupMap.push({\n id: group.id,\n name: group.name,\n });\n });\n }\n return groupMap;\n};\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n chrome.tabs.create({\n url: getGroupLink(Number(notificationId.substring(notificationIdPrefix.length)), 'redirect').href,\n active: true,\n });\n});\nexport default async (previousState) => {\n const newState = {};\n const groups = await getGroups();\n const promises = groups.map(async (group) => {\n try {\n const groupShout = await getGroupShout(group.id);\n newState[group.id] = groupShout;\n if (previousState &&\n previousState.hasOwnProperty(group.id) &&\n previousState[group.id] !== groupShout &&\n groupShout) {\n // Send notification, the shout has changed.\n const groupIcon = await getGroupIcon(group.id);\n if (groupIcon.state !== ThumbnailState.Completed) {\n return;\n }\n const notificationIcon = await fetchDataUri(new URL(groupIcon.imageUrl));\n chrome.notifications.create(`${notificationIdPrefix}${group.id}`, {\n type: 'basic',\n title: group.name,\n message: groupShout,\n contextMessage: 'Roblox+ Group Shout Notifier',\n iconUrl: notificationIcon,\n });\n }\n }\n catch (err) {\n console.error('Failed to check group for group shout notifier', err, group);\n if (previousState && previousState.hasOwnProperty(group.id)) {\n newState[group.id] = previousState[group.id];\n }\n }\n });\n await Promise.all(promises);\n return newState;\n};\n","import CatalogNotifier from './catalog';\nimport FriendPresenceNotifier from './friend-presence';\nimport GroupShoutNotifier from './group-shout';\nimport TradeNotifier from './trades';\n// Registry of all the notifiers\nconst notifiers = {};\nnotifiers['notifiers/catalog'] = CatalogNotifier;\nnotifiers['notifiers/group-shouts'] = GroupShoutNotifier;\nnotifiers['notifiers/friend-presence'] = FriendPresenceNotifier;\nnotifiers['notifiers/trade'] = TradeNotifier;\n// TODO: Update to use chrome.storage.session for manifest V3\nconst notifierStates = {};\n// Execute a notifier by name.\nconst executeNotifier = async (name) => {\n const notifier = notifiers[name];\n if (!notifier) {\n return;\n }\n try {\n // Fetch the state from the last time the notifier ran.\n // ...\n // Run the notifier.\n const newState = await notifier(notifierStates[name]);\n // Save the state for the next time the notifier runs.\n if (newState) {\n notifierStates[name] = newState;\n }\n else {\n delete notifierStates[name];\n }\n }\n catch (err) {\n console.error(name, 'failed to run', err);\n }\n};\n// Listener for the chrome.alarms API, to process the notification checks\nchrome.alarms.onAlarm.addListener(async ({ name }) => {\n await executeNotifier(name);\n});\nfor (let name in notifiers) {\n chrome.alarms.create(name, {\n periodInMinutes: 1,\n });\n}\nglobalThis.notifiers = notifiers;\nglobalThis.notifierStates = notifierStates;\nglobalThis.executeNotifier = executeNotifier;\nexport { executeNotifier };\nexport default notifiers;\n","import { TradeStatusType } from 'roblox';\nimport { getToggleSettingValue } from '../../../services/settings';\nimport { getAvatarHeadshotThumbnail } from '../../../services/thumbnails';\nimport fetchDataUri from '../../../utils/fetchDataUri';\n// The prefix for the ID of the notification to display.\nconst notificationIdPrefix = 'trade-notifier-';\n// Gets the trade status types that should be notified on.\nconst getEnabledTradeStatusTypes = async () => {\n const enabled = await getToggleSettingValue('tradeNotifier');\n if (enabled) {\n return [\n TradeStatusType.Inbound,\n TradeStatusType.Outbound,\n TradeStatusType.Completed,\n TradeStatusType.Inactive,\n ];\n }\n return [];\n /*\n const values = await getSettingValue('notifiers/trade/status-types');\n if (!Array.isArray(values)) {\n return [];\n }\n \n return values.filter((v) => Object.keys(v).includes(v));\n */\n};\n// Load the trade IDs for a status type.\nconst getTrades = async (tradeStatusType) => {\n const response = await fetch(`https://trades.roblox.com/v1/trades/${tradeStatusType}?limit=10&sortOrder=Desc`);\n const result = await response.json();\n return result.data.map((t) => t.id);\n};\n// Gets an individual trade by its ID.\nconst getTrade = async (id, tradeStatusType) => {\n const response = await fetch(`https://trades.roblox.com/v1/trades/${id}`);\n const result = await response.json();\n const tradePartner = result.user;\n const tradePartnerOffer = result.offers.find((o) => o.user.id === tradePartner.id);\n const authenticatedUserOffer = result.offers.find((o) => o.user.id !== tradePartner.id);\n return {\n id,\n tradePartner,\n authenticatedUserOffer: {\n robux: authenticatedUserOffer.robux,\n assets: authenticatedUserOffer.userAssets.map((a) => {\n return {\n id: a.assetId,\n userAssetId: a.id,\n name: a.name,\n recentAveragePrice: a.recentAveragePrice,\n };\n }),\n },\n partnerOffer: {\n robux: tradePartnerOffer.robux,\n assets: tradePartnerOffer.userAssets.map((a) => {\n return {\n id: a.assetId,\n userAssetId: a.id,\n name: a.name,\n recentAveragePrice: a.recentAveragePrice,\n };\n }),\n },\n status: result.status,\n type: tradeStatusType,\n };\n};\n// Gets the icon URL to display on the notification.\nconst getNotificationIconUrl = async (trade) => {\n const thumbnail = await getAvatarHeadshotThumbnail(trade.tradePartner.id);\n if (!thumbnail.imageUrl) {\n return '';\n }\n try {\n return await fetchDataUri(new URL(thumbnail.imageUrl));\n }\n catch (err) {\n console.error('Failed to fetch icon URL from thumbnail', trade, thumbnail, err);\n return '';\n }\n};\n// Fetches the title for the notification to display to the user, based on current and previous known presence.\nconst getNotificationTitle = (trade) => {\n switch (trade.type) {\n case TradeStatusType.Inbound:\n return 'Trade inbound';\n case TradeStatusType.Outbound:\n return 'Trade sent';\n case TradeStatusType.Completed:\n return 'Trade completed';\n default:\n return 'Trade ' + trade.status.toLowerCase();\n }\n};\nconst getOfferValue = (tradeOffer) => {\n let value = 0;\n tradeOffer.assets.forEach((asset) => {\n value += asset.recentAveragePrice;\n });\n return (`${value.toLocaleString()}` +\n (tradeOffer.robux > 0 ? ` + R\\$${tradeOffer.robux.toLocaleString()}` : ''));\n};\n// Handle what happens when a notification is clicked.\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n // If only we could link to specific trades..\n const tradeId = Number(notificationId.substring(notificationIdPrefix.length));\n chrome.tabs.create({\n url: 'https://www.roblox.com/trades',\n active: true,\n });\n});\n// Processes the presences, and send the notifications, when appropriate.\nexport default async (previousState) => {\n const previousEnabledStatusTypes = previousState?.enabledStatusTypes || [];\n const previousTradeStatusTypes = previousState?.tradeStatusMap || {};\n const newState = {\n // Preserve the trade statuses for the future\n // This is definitely how memory leaks come to be, but... how many trades could someone possibly be going through.\n tradeStatusMap: Object.assign({}, previousTradeStatusTypes),\n enabledStatusTypes: await getEnabledTradeStatusTypes(),\n };\n await Promise.all(newState.enabledStatusTypes.map(async (tradeStatusType) => {\n try {\n const trades = await getTrades(tradeStatusType);\n const tradePromises = [];\n // No matter what: Keep track of this trade we have seen, for future reference.\n trades.forEach((tradeId) => {\n newState.tradeStatusMap[tradeId] = tradeStatusType;\n });\n // now check each of them, to see if we want to send a notification.\n for (let i = 0; i < trades.length; i++) {\n const tradeId = trades[i];\n // Previously, the notifier type wasn't enabled.\n // Do nothing with the information we now know.\n if (!previousEnabledStatusTypes.includes(tradeStatusType)) {\n continue;\n }\n // We have seen this trade before, in this same status type\n // Because the trades are ordered in descending order, we know there are\n // no other changes further down in this list. We can break.\n if (previousTradeStatusTypes[tradeId] === tradeStatusType) {\n // And in fact, we have to break.\n // Because if we don't, \"new\" trades could come in at the bottom of the list.\n break;\n }\n // In all cases, we clear the current notification, to make room for a potential new one.\n const notificationId = notificationIdPrefix + tradeId;\n chrome.notifications.clear(notificationId);\n tradePromises.push(getTrade(tradeId, tradeStatusType)\n .then(async (trade) => {\n try {\n const iconUrl = await getNotificationIconUrl(trade);\n if (!iconUrl) {\n // No icon.. no new notification.\n return;\n }\n const title = getNotificationTitle(trade);\n chrome.notifications.create(notificationId, {\n type: 'list',\n iconUrl,\n title,\n message: '@' + trade.tradePartner.name,\n items: [\n {\n title: 'Partner',\n message: trade.tradePartner.displayName,\n },\n {\n title: 'Your Value',\n message: getOfferValue(trade.authenticatedUserOffer),\n },\n {\n title: 'Partner Value',\n message: getOfferValue(trade.partnerOffer),\n },\n ],\n contextMessage: 'Roblox+ Trade Notifier',\n isClickable: true,\n });\n }\n catch (e) {\n console.error('Failed to send notification about trade', trade);\n }\n })\n .catch((err) => {\n console.error('Failed to load trade information', tradeId, tradeStatusType, err);\n }));\n }\n await Promise.all(tradePromises);\n }\n catch (e) {\n console.error(`Failed to check ${tradeStatusType} trade notifier`, e);\n }\n }));\n return newState;\n};\n","import { Batch } from '@tix-factory/batch';\nimport { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { manifest } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'assetsService.getAssetContentsUrl';\nclass AssetContentsBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const requestHeaders = new Headers();\n requestHeaders.append('Roblox-Place-Id', '258257446');\n requestHeaders.append('Roblox-Browser-Asset-Request', manifest.name);\n const response = await xsrfFetch(new URL(`https://assetdelivery.roblox.com/v2/assets/batch`), {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(items.map((batchItem) => {\n return {\n assetId: batchItem.value,\n requestId: batchItem.key,\n };\n })),\n });\n if (!response.ok) {\n throw new Error('Failed to load asset contents URL');\n }\n const result = await response.json();\n items.forEach((item) => {\n const asset = result.find((a) => a.requestId === item.key);\n const location = asset?.locations[0];\n if (location?.location) {\n item.resolve(location.location);\n }\n else {\n item.resolve('');\n }\n });\n }\n getKey(item) {\n return item.toString();\n }\n}\nconst assetContentsProcessor = new AssetContentsBatchProcessor();\nconst assetContentsCache = new ExpirableDictionary(messageDestination, 10 * 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getAssetContentsUrl = async (assetId) => {\n const url = await sendMessage(messageDestination, {\n assetId,\n });\n return url ? new URL(url) : undefined;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return assetContentsCache.getOrAdd(assetContentsProcessor.getKey(message.assetId), () => {\n // Queue up the fetch request, when not in the cache\n return assetContentsProcessor.enqueue(message.assetId);\n });\n});\nexport default getAssetContentsUrl;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport getAssetContentsUrl from './get-asset-contents-url';\nconst messageDestination = 'assetsService.getAssetDependencies';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst contentRegexes = [\n /\"TextureI?d?\".*=\\s*(\\d+)/gi,\n /\"TextureI?d?\".*rbxassetid:\\/\\/(\\d+)/gi,\n /\"MeshId\".*=\\s*(\\d+)/gi,\n /MeshId.*rbxassetid:\\/\\/(\\d+)/gi,\n /asset\\/?\\?\\s*id\\s*=\\s*(\\d+)/gi,\n /rbxassetid:\\/\\/(\\d+)/gi,\n /:LoadAsset\\((\\d+)\\)/gi,\n /require\\((\\d+)\\)/gi,\n];\nconst getAssetDependencies = async (assetId) => {\n return sendMessage(messageDestination, { assetId });\n};\nconst loadAssetDependencies = async (assetId) => {\n const assetIds = [];\n const assetContentsUrl = await getAssetContentsUrl(assetId);\n if (!assetContentsUrl) {\n return [];\n }\n const assetContentsResponse = await fetch(assetContentsUrl);\n const assetContents = await assetContentsResponse.text();\n contentRegexes.forEach((regex) => {\n let match = assetContents.match(regex) || [];\n match.forEach((m) => {\n let id = Number((m.match(/(\\d+)/) || [])[1]);\n if (id && !isNaN(id) && !assetIds.includes(id)) {\n assetIds.push(id);\n }\n });\n });\n return assetIds;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.assetId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAssetDependencies(message.assetId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getAssetDependencies;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'assetsService.getAssetDetails';\nconst cache = new ExpirableDictionary(messageDestination, 5 * 60 * 1000);\nconst getAssetDetails = async (assetId) => {\n return sendMessage(messageDestination, { assetId });\n};\nconst loadAssetDetails = async (assetId) => {\n const response = await fetch(`https://economy.roblox.com/v2/assets/${assetId}/details`);\n if (!response.ok) {\n throw new Error('Failed to load asset product info');\n }\n const result = await response.json();\n return {\n id: assetId,\n name: result.Name,\n type: result.AssetTypeId,\n sales: result.Sales,\n };\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.assetId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAssetDetails(message.assetId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getAssetDetails;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'assetsService.getAssetSalesCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst getAssetSalesCount = async (assetId) => {\n return sendMessage(messageDestination, { assetId });\n};\nconst loadAssetSalesCount = async (assetId) => {\n const response = await fetch(`https://economy.roblox.com/v2/assets/${assetId}/details`);\n if (!response.ok) {\n throw new Error('Failed to load asset product info');\n }\n const result = await response.json();\n return result.Sales || NaN;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.assetId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAssetSalesCount(message.assetId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getAssetSalesCount;\n","import getAssetContentsUrl from './get-asset-contents-url';\nimport getAssetSalesCount from './get-asset-sales-count';\nimport getAssetDependencies from './get-asset-dependencies';\nimport getAssetDetails from './get-asset-details';\nglobalThis.assetsService = {\n getAssetContentsUrl,\n getAssetSalesCount,\n getAssetDependencies,\n getAssetDetails,\n};\nexport { getAssetContentsUrl, getAssetSalesCount, getAssetDependencies, getAssetDetails, };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nconst messageDestination = 'avatarService.getAvatarRules';\nlet avatarAssetRules = [];\nconst getAvatarAssetRules = async () => {\n return sendMessage(messageDestination, {});\n};\nconst loadAvatarAssetRules = async () => {\n const response = await fetch(`https://avatar.roblox.com/v1/avatar-rules`);\n if (!response.ok) {\n throw new Error(`Failed to load avatar rules (${response.status})`);\n }\n const result = await response.json();\n return result.wearableAssetTypes.map((rule) => {\n return {\n maxNumber: rule.maxNumber,\n assetType: rule.id,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, async () => {\n if (avatarAssetRules.length > 0) {\n return avatarAssetRules;\n }\n avatarAssetRules = await loadAvatarAssetRules();\n return avatarAssetRules;\n}, {\n levelOfParallelism: 1,\n});\nexport default getAvatarAssetRules;\n","import { AssetType } from 'roblox';\nimport xsrfFetch from '../../utils/xsrfFetch';\nimport getAvatarAssetRules from './get-avatar-asset-rules';\nconst getAvatarAssets = async (userId) => {\n const response = await fetch(`https://avatar.roblox.com/v1/users/${userId}/avatar`);\n if (!response.ok) {\n throw new Error(`Failed to load avatar (${response.status})`);\n }\n const result = await response.json();\n const assets = result.assets.map((asset) => {\n return {\n id: asset.id,\n name: asset.name,\n assetType: asset.assetType.id,\n };\n });\n result.emotes.forEach((emote) => {\n assets.push({\n id: emote.assetId,\n name: emote.assetName,\n assetType: AssetType.Emote,\n });\n });\n return assets;\n};\nconst wearItem = async (assetId, authenticatedUserId) => {\n // Use set-wearing-assets instead of wear because it will allow more than the limit\n const currentAssets = await getAvatarAssets(authenticatedUserId);\n const response = await xsrfFetch(new URL(`https://avatar.roblox.com/v1/avatar/set-wearing-assets`), {\n method: 'POST',\n body: JSON.stringify({\n assetIds: [assetId].concat(currentAssets\n .filter((a) => a.assetType !== AssetType.Emote)\n .map((a) => a.id)),\n }),\n });\n if (!response.ok) {\n throw new Error(`Failed to wear asset (${assetId})`);\n }\n const result = await response.json();\n if (result.invalidAssetIds.length > 0) {\n throw new Error(`Failed to wear assets (${result.invalidAssetIds.join(', ')})`);\n }\n};\nconst removeItem = async (assetId) => {\n const response = await xsrfFetch(new URL(`https://avatar.roblox.com/v1/avatar/assets/${assetId}/remove`), {\n method: 'POST',\n });\n if (!response.ok) {\n throw new Error(`Failed to remove asset (${assetId})`);\n }\n};\nglobalThis.avatarService = { getAvatarAssetRules, getAvatarAssets, wearItem, removeItem };\nexport { getAvatarAssetRules, getAvatarAssets, wearItem, removeItem };\n","import { Batch } from '@tix-factory/batch';\nclass BadgeAwardBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await fetch(`https://badges.roblox.com/v1/users/${items[0].value.userId}/badges/awarded-dates?badgeIds=${items\n .map((i) => i.value.badgeId)\n .join(',')}`);\n if (!response.ok) {\n throw new Error('Failed to load badge award statuses');\n }\n const result = await response.json();\n items.forEach((item) => {\n const badgeAward = result.data.find((b) => b.badgeId === item.value.badgeId);\n if (badgeAward?.awardedDate) {\n item.resolve(new Date(badgeAward.awardedDate));\n }\n else {\n item.resolve(undefined);\n }\n });\n }\n getBatch() {\n const now = performance.now();\n const batch = [];\n for (let i = 0; i < this.queueArray.length; i++) {\n const batchItem = this.queueArray[i];\n if (batchItem.retryAfter > now) {\n // retryAfter is set at Infinity while the item is being processed\n // so we should always check it, even if we're not retrying items\n continue;\n }\n if (batch.length < 1 ||\n batch[0].value.userId === batchItem.value.userId) {\n // We group all the requests for badge award dates together by user ID.\n batch.push(batchItem);\n }\n if (batch.length >= this.config.maxSize) {\n // We have all the items we need, break.\n break;\n }\n }\n return batch;\n }\n getKey(item) {\n return `${item.userId}:${item.badgeId}`;\n }\n}\nexport default BadgeAwardBatchProcessor;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport BadgeAwardBatchProcessor from './batchProcessor';\nconst messageDestination = 'badgesService.getBadgeAwardDate';\nconst badgeAwardProcessor = new BadgeAwardBatchProcessor();\nconst badgeAwardCache = new ExpirableDictionary('badgesService', 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getBadgeAwardDate = async (userId, badgeId) => {\n const date = await sendMessage(messageDestination, {\n userId,\n badgeId,\n });\n return date ? new Date(date) : undefined;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return badgeAwardCache.getOrAdd(badgeAwardProcessor.getKey(message), async () => {\n // Queue up the fetch request, when not in the cache\n const date = await badgeAwardProcessor.enqueue(message);\n return date?.getTime();\n });\n});\nglobalThis.badgesService = { getBadgeAwardDate };\nexport { getBadgeAwardDate };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport { recordUserRobux } from './history';\nconst messageDestination = 'currencyService.getRobuxBalance';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the Robux balance of the currently authenticated user.\nconst getRobuxBalance = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the Robux balance of the currently authenticated user.\nconst loadRobuxBalance = async (userId) => {\n const response = await fetch(`https://economy.roblox.com/v1/users/${userId}/currency`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw 'Failed to load Robux balance';\n }\n const result = await response.json();\n try {\n await recordUserRobux(userId, result.robux);\n }\n catch (err) {\n console.warn('Failed to record Robux history');\n }\n return result.robux;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadRobuxBalance(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getRobuxBalance;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nimport { open } from 'db.js';\nimport { getToggleSettingValue } from '../settings';\nconst messageDestination = 'currencyService.history.';\nif (isBackgroundPage) {\n open({\n server: 'currencyBalances',\n version: 1,\n schema: {\n robuxHistory: {\n key: {\n keyPath: ['currencyHolderType', 'currencyHolderId', 'robuxDate'],\n },\n indexes: {\n currencyHolderType: {},\n currencyHolderId: {},\n robuxDate: {},\n },\n },\n },\n })\n .then((database) => {\n console.log('Database connection (for robuxHistory) opened.');\n window.robuxHistoryDatabase = database;\n // Ensure the amount of stored data doesn't get too out of hand.\n // Only store one year of data.\n setInterval(async () => {\n try {\n const now = +new Date();\n const purgeDate = new Date(now - 32 * 12 * 24 * 60 * 60 * 1000);\n const robuxHistory = await database.robuxHistory\n .query('robuxDate')\n .range({ lte: purgeDate.getTime() })\n .execute();\n if (robuxHistory.length <= 0) {\n return;\n }\n await Promise.all(robuxHistory.map((robuxHistoryRecord) => {\n return database.robuxHistory.remove({\n eq: [\n robuxHistoryRecord.currencyHolderType,\n robuxHistoryRecord.currencyHolderId,\n robuxHistoryRecord.robuxDate,\n ],\n });\n }));\n }\n catch (e) {\n console.warn('Failed to purge Robux history database', e);\n }\n }, 60 * 60 * 1000);\n })\n .catch((err) => {\n console.error('Failed to connect to robuxHistory database.', err);\n });\n}\nconst recordUserRobux = async (userId, robux) => {\n const enabled = await getToggleSettingValue('robuxHistoryEnabled');\n if (!enabled) {\n return;\n }\n return sendMessage(messageDestination + 'recordUserRobux', {\n userId,\n robux,\n });\n};\nconst getUserRobuxHistory = async (userId, startDateTime, endDateTime) => {\n const robuxHistory = await sendMessage(messageDestination + 'getUserRobuxHistory', {\n userId,\n startDateTime: startDateTime.getTime(),\n endDateTime: endDateTime.getTime(),\n });\n return robuxHistory.map((h) => {\n return {\n value: h.robux,\n date: new Date(h.robuxDate),\n };\n });\n};\naddListener(messageDestination + 'recordUserRobux', async (message) => {\n const now = +new Date();\n const robuxDateTime = new Date(now - (now % 60000));\n await robuxHistoryDatabase.robuxHistory.update({\n currencyHolderType: 'User',\n currencyHolderId: message.userId,\n robux: message.robux,\n robuxDate: robuxDateTime.getTime(),\n });\n}, {\n levelOfParallelism: 1,\n});\naddListener(messageDestination + 'getUserRobuxHistory', async (message) => {\n const history = await robuxHistoryDatabase.robuxHistory\n .query('robuxDate')\n .range({\n gte: message.startDateTime,\n lte: message.endDateTime,\n })\n .filter((row) => row.currencyHolderType === 'User' &&\n row.currencyHolderId === message.userId)\n .execute();\n return history;\n}, {\n levelOfParallelism: 1,\n});\nexport { recordUserRobux, getUserRobuxHistory };\n","import { default as getRobuxBalance } from './getRobuxBalance';\nimport { getUserRobuxHistory } from './history';\nglobalThis.currencyService = { getRobuxBalance, getUserRobuxHistory };\nexport { getRobuxBalance, getUserRobuxHistory };\n","import { Batch } from '@tix-factory/batch';\nimport xsrfFetch from '../../utils/xsrfFetch';\nclass AuthenticatedUserFollowingProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await xsrfFetch(new URL('https://friends.roblox.com/v1/user/following-exists'), {\n method: 'POST',\n body: JSON.stringify({\n targetUserIds: items.map((i) => i.value),\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to load authenticated user following statuses');\n }\n const result = await response.json();\n items.forEach((item) => {\n const following = result.followings.find((f) => f.userId === item.value);\n item.resolve(following?.isFollowing === true);\n });\n }\n getKey(userId) {\n return `${userId}`;\n }\n}\nexport default AuthenticatedUserFollowingProcessor;\n","import { default as isAuthenticatedUserFollowing } from './isAuthenticatedUserFollowing';\nglobalThis.followingsService = { isAuthenticatedUserFollowing };\nexport { isAuthenticatedUserFollowing };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport AuthenticatedUserFollowingProcessor from './authenticatedUserFollowingProcessor';\nconst messageDestination = 'followingsService.isAuthenticatedUserFollowing';\nconst batchProcessor = new AuthenticatedUserFollowingProcessor();\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\n// Checks if the authenticated user is following another user.\nconst isAuthenticatedUserFollowing = (userId) => {\n return sendMessage(messageDestination, {\n userId,\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n batchProcessor.enqueue(message.userId));\n});\nexport default isAuthenticatedUserFollowing;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'friendsService.getFriendRequestCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the inbound friend request count for the currently authenticated user.\nconst getFriendRequestCount = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the inbound friend request count for the currently authenticated user.\nconst loadFriendRequestCount = async (userId) => {\n // User ID is used as a cache buster.\n const response = await fetch(`https://friends.roblox.com/v1/user/friend-requests/count`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw 'Failed to load friend request count';\n }\n const result = await response.json();\n return result.count;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadFriendRequestCount(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getFriendRequestCount;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'friendsService.getUserFriends';\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\n// Fetches the list of friends for the user.\nconst getUserFriends = (userId) => {\n return sendMessage(messageDestination, {\n userId,\n });\n};\n// Loads the actual friend list for the user.\nconst loadUserFriends = async (userId) => {\n const response = await fetch(`https://friends.roblox.com/v1/users/${userId}/friends`);\n if (!response.ok) {\n throw new Error(`Failed to load friends for user (${userId})`);\n }\n const result = await response.json();\n return result.data.map((r) => {\n return {\n id: r.id,\n name: r.name,\n displayName: r.displayName,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUserFriends(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getUserFriends;\n","import { default as getUserFriends } from './getUserFriends';\nimport { default as getFriendRequestCount } from './getFriendRequestCount';\nglobalThis.friendsService = { getUserFriends, getFriendRequestCount };\nexport { getUserFriends, getFriendRequestCount };\n","import launchProtocolUrl from '../../utils/launchProtocolUrl';\n// Launches into the experience that the specified user is playing.\nconst followUser = async (userId) => {\n await launchProtocolUrl(`roblox://userId=${userId}`);\n};\nglobalThis.gameLaunchService = { followUser };\nexport { followUser };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'gamePassesService.getGamePassSaleCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst getGamePassSaleCount = async (gamePassId) => {\n return sendMessage(messageDestination, { gamePassId });\n};\nconst loadGamePassSales = async (gamePassId) => {\n const response = await fetch(`https://economy.roblox.com/v1/game-pass/${gamePassId}/game-pass-product-info`);\n if (!response.ok) {\n throw new Error('Failed to load game pass product info');\n }\n const result = await response.json();\n return result.Sales || NaN;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.gamePassId}`, () => \n // Queue up the fetch request, when not in the cache\n loadGamePassSales(message.gamePassId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getGamePassSaleCount;\n","import getGamePassSaleCount from './get-game-pass-sale-count';\nglobalThis.gamePassesService = { getGamePassSaleCount };\nexport { getGamePassSaleCount };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getCreatorGroups';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\n// Fetches the groups the user has access privileged roles in.\nconst getCreatorGroups = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the groups the user has access privileged roles in.\nconst loadAuthenticatedUserCreatorGroups = async () => {\n const response = await fetch(`https://develop.roblox.com/v1/user/groups/canmanage`);\n if (response.status === 401) {\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n throw 'Failed to load creation groups for the authenticated user';\n }\n const result = await response.json();\n return result.data.map((g) => {\n return {\n id: g.id,\n name: g.name,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAuthenticatedUserCreatorGroups());\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getCreatorGroups;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getGroupShout';\nconst cache = new ExpirableDictionary(messageDestination, 90 * 1000);\n// Fetches the group shout.\nconst getGroupShout = (groupId) => {\n return sendMessage(messageDestination, { groupId });\n};\n// Loads the groups the user is a member of.\nconst loadGroupShout = async (groupId) => {\n const response = await fetch(`https://groups.roblox.com/v1/groups/${groupId}`);\n if (!response.ok) {\n throw `Failed to load group shout for group ${groupId}`;\n }\n const result = await response.json();\n return result.shout?.body || '';\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.groupId}`, () => \n // Queue up the fetch request, when not in the cache\n loadGroupShout(message.groupId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getGroupShout;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getUserGroups';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\n// Fetches the groups the user is a member of.\nconst getUserGroups = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the groups the user is a member of.\nconst loadUserGroups = async (userId) => {\n const response = await fetch(`https://groups.roblox.com/v1/users/${userId}/groups/roles`);\n if (!response.ok) {\n throw 'Failed to load groups the user is a member of';\n }\n const result = await response.json();\n return result.data.map((g) => {\n return {\n id: g.group.id,\n name: g.group.name,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUserGroups(message.userId));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getUserGroups;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getUserPrimaryGroup';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\n// Fetches the groups the user is a member of.\nconst getUserPrimaryGroup = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the groups the user is a member of.\nconst loadUserPrimaryGroup = async (userId) => {\n const response = await fetch(`https://groups.roblox.com/v1/users/${userId}/groups/primary/role`);\n if (!response.ok) {\n throw 'Failed to load primary group for the user';\n }\n const result = await response.json();\n if (!result || !result.group) {\n return null;\n }\n return {\n id: result.group.id,\n name: result.group.name,\n };\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUserPrimaryGroup(message.userId));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getUserPrimaryGroup;\n","import getCreatorGroups from './get-creator-groups';\nimport getGroupShout from './get-group-shout';\nimport getUserGroups from './get-user-groups';\nimport getUserPrimaryGroup from './get-user-primary-group';\nglobalThis.groupsService = { getCreatorGroups, getGroupShout, getUserGroups, getUserPrimaryGroup };\nexport { getCreatorGroups, getGroupShout, getUserGroups, getUserPrimaryGroup };\n","import { getUserById } from '../users';\nconst getAssetOwners = async (assetId, cursor, isAscending) => {\n const response = await fetch(`https://inventory.roblox.com/v2/assets/${assetId}/owners?limit=100&cursor=${cursor}&sortOrder=${isAscending ? 'Asc' : 'Desc'}`, {\n credentials: 'include',\n });\n if (!response.ok) {\n throw new Error(`Failed to load ownership records (${assetId}, ${cursor}, ${isAscending})`);\n }\n const result = await response.json();\n const ownershipRecords = [];\n await Promise.all(result.data.map(async (i) => {\n const record = {\n id: i.id,\n user: null,\n serialNumber: i.serialNumber || NaN,\n created: new Date(i.created),\n updated: new Date(i.updated),\n };\n ownershipRecords.push(record);\n if (i.owner) {\n record.user = await getUserById(i.owner.id);\n }\n }));\n return {\n nextPageCursor: result.nextPageCursor || '',\n data: ownershipRecords,\n };\n};\nexport default getAssetOwners;\n","import xsrfFetch from '../../utils/xsrfFetch';\nimport getLimitedInventory from './limitedInventory';\nimport getAssetOwners from './get-asset-owners';\n// Removes an asset from the authenticated user's inventory.\nconst deleteAsset = async (assetId) => {\n const response = await xsrfFetch(new URL(`https://assetgame.roblox.com/asset/delete-from-inventory`), {\n method: 'POST',\n body: JSON.stringify({\n assetId: assetId,\n }),\n });\n if (!response.ok) {\n throw new Error(`Failed to remove asset (${assetId})`);\n }\n};\nglobalThis.inventoryService = { deleteAsset, getLimitedInventory, getAssetOwners };\nexport { deleteAsset, getLimitedInventory, getAssetOwners };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'inventoryService.getLimitedInventory';\nconst cache = new ExpirableDictionary(messageDestination, 5 * 60 * 1000);\n// Fetches the limited inventory for the specified user.\nconst getLimitedInventory = (userId) => {\n return sendMessage(messageDestination, {\n userId,\n });\n};\n// Actually loads the inventory.\nconst loadLimitedInventory = async (userId) => {\n const foundUserAssetIds = new Set();\n const limitedAssets = [];\n let nextPageCursor = '';\n do {\n const response = await fetch(`https://inventory.roblox.com/v1/users/${userId}/assets/collectibles?limit=100&cursor=${nextPageCursor}`);\n if (response.status === 429) {\n // Throttled. Wait a few seconds, and try again.\n await wait(5000);\n continue;\n }\n else if (response.status === 403) {\n throw new Error('Inventory hidden');\n }\n else if (!response.ok) {\n throw new Error('Inventory failed to load');\n }\n const result = await response.json();\n nextPageCursor = result.nextPageCursor;\n result.data.forEach((item) => {\n const userAssetId = Number(item.userAssetId);\n if (foundUserAssetIds.has(userAssetId)) {\n return;\n }\n foundUserAssetIds.add(userAssetId);\n limitedAssets.push({\n userAssetId,\n id: item.assetId,\n name: item.name,\n recentAveragePrice: item.recentAveragePrice\n ? Number(item.recentAveragePrice)\n : NaN,\n serialNumber: item.serialNumber ? Number(item.serialNumber) : NaN,\n stock: item.assetStock === 0 ? 0 : item.assetStock || undefined,\n });\n });\n } while (nextPageCursor);\n return limitedAssets;\n};\n// Listen for background messages\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadLimitedInventory(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getLimitedInventory;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nconst englishLocale = 'en_us';\nconst messageDestination = 'localizationService.getTranslationResources';\nlet translationResourceCache = [];\nlet localeCache = '';\n// Gets the locale for the authenticated user.\nconst getAuthenticatedUserLocale = async () => {\n if (localeCache) {\n return localeCache;\n }\n try {\n const response = await fetch(`https://locale.roblox.com/v1/locales/user-locale`);\n if (!response.ok) {\n console.warn('Failed to fetch user locale - defaulting to English.', response.status);\n return (localeCache = englishLocale);\n }\n const result = await response.json();\n return (localeCache = result.supportedLocale.locale);\n }\n catch (e) {\n console.warn('Unhandled error loading user locale - defaulting to English.', e);\n return (localeCache = englishLocale);\n }\n};\n// Fetches all the translation resources for the authenticated user.\nconst getTranslationResources = async () => {\n if (translationResourceCache.length > 0) {\n return translationResourceCache;\n }\n return (translationResourceCache = await sendMessage(messageDestination, {}));\n};\n// Fetches an individual translation resource.\nconst getTranslationResource = async (namespace, key) => {\n const translationResources = await getTranslationResources();\n const resource = translationResources.find((r) => r.namespace === namespace && r.key === key);\n if (!resource) {\n console.warn(`No translation resource available.\\n\\tNamespace: ${namespace}\\n\\tKey: ${key}`);\n }\n return resource?.value || '';\n};\nconst getTranslationResourceWithFallback = async (namespace, key, defaultValue) => {\n try {\n const value = await getTranslationResource(namespace, key);\n if (!value) {\n return defaultValue;\n }\n return value;\n }\n catch (e) {\n console.warn('Failed to load translation resource', namespace, key, e);\n return defaultValue;\n }\n};\n// Listener to ensure these always happen in the background, for strongest caching potential.\naddListener(messageDestination, async () => {\n if (translationResourceCache.length > 0) {\n return translationResourceCache;\n }\n const locale = await getAuthenticatedUserLocale();\n const response = await fetch(`https://translations.roblox.com/v1/translations?consumerType=Web`);\n if (!response.ok) {\n throw new Error(`Failed to load translation resources (${response.status})`);\n }\n const result = await response.json();\n const resourcesUrl = result.data.find((r) => r.locale === locale) ||\n result.data.find((r) => r.locale === englishLocale);\n if (!resourcesUrl) {\n throw new Error(`Failed to find translation resources for locale (${locale})`);\n }\n const resources = await fetch(resourcesUrl.url);\n const resourcesJson = await resources.json();\n return (translationResourceCache = resourcesJson.contents.map((r) => {\n return {\n namespace: r.namespace,\n key: r.key,\n value: r.translation || r.english,\n };\n }));\n}, {\n // Ensure that multiple requests for this information can't be processed at once.\n levelOfParallelism: 1,\n});\nglobalThis.localizationService = { getTranslationResource, getTranslationResourceWithFallback };\nexport { getTranslationResource, getTranslationResourceWithFallback };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'premiumPayoutsService.getPremiumPayoutsSummary';\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\nconst serializeDate = (date) => {\n return `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, '0')}-${`${date.getDate()}`.padStart(2, '0')}`;\n};\n// Fetches the Robux balance of the currently authenticated user.\nconst getPremiumPayoutsSummary = (universeId, startDate, endDate) => {\n return sendMessage(messageDestination, {\n universeId,\n startDate: serializeDate(startDate),\n endDate: serializeDate(endDate),\n });\n};\n// Loads the Robux balance of the currently authenticated user.\nconst loadPremiumPayoutsSummary = async (universeId, startDate, endDate) => {\n const response = await fetch(`https://engagementpayouts.roblox.com/v1/universe-payout-history?universeId=${universeId}&startDate=${startDate}&endDate=${endDate}`);\n if (!response.ok) {\n throw 'Failed to load premium payouts';\n }\n const result = await response.json();\n const payouts = [];\n for (let date in result) {\n const payout = result[date];\n if (payout.eligibilityType !== 'Eligible') {\n continue;\n }\n payouts.push({\n date,\n engagementScore: payout.engagementScore,\n payoutInRobux: payout.payoutInRobux,\n payoutType: payout.payoutType,\n });\n }\n return payouts;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.universeId}_${message.startDate}_${message.endDate}`, () => \n // Queue up the fetch request, when not in the cache\n loadPremiumPayoutsSummary(message.universeId, message.startDate, message.endDate));\n}, {\n levelOfParallelism: 1,\n});\nexport default getPremiumPayoutsSummary;\n","import { default as getPremiumPayoutsSummary } from './getPremiumPayoutsSummary';\nglobalThis.premiumPayoutsService = { getPremiumPayoutsSummary };\nexport { getPremiumPayoutsSummary };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'premiumService.getPremiumExpirationDate';\nconst definitelyPremium = {};\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\n// Check whether or not a user has a Roblox+ Premium subscription.\nconst getPremiumExpirationDate = async (userId) => {\n const expiration = await sendMessage(messageDestination, {\n userId,\n });\n if (!expiration) {\n return expiration;\n }\n return new Date(expiration);\n};\nconst getPrivateServerExpiration = async (id) => {\n const response = await fetch(`https://games.roblox.com/v1/vip-servers/${id}`);\n if (!response.ok) {\n console.warn('Failed to load private server details', id, response);\n return null;\n }\n const result = await response.json();\n if (result.subscription?.expired === false) {\n // If it's not expired, return the expiration date.\n return result.subscription.expirationDate;\n }\n return null;\n};\n// Check if the user has a private server with the Roblox+ hub.\nconst checkPrivateServerExpirations = async (userId) => {\n try {\n const response = await fetch(`https://games.roblox.com/v1/games/258257446/private-servers`);\n if (!response.ok) {\n console.warn('Failed to load private servers', userId, response);\n return null;\n }\n const result = await response.json();\n for (let i = 0; i < result.data.length; i++) {\n const privateServer = result.data[i];\n if (privateServer.owner?.id !== userId) {\n continue;\n }\n try {\n const expirationDate = await getPrivateServerExpiration(privateServer.vipServerId);\n if (expirationDate) {\n // We found a private server we paid for, we're done!\n return expirationDate;\n }\n }\n catch (err) {\n console.warn('Failed to check if private server was active', privateServer, err);\n }\n }\n return null;\n }\n catch (err) {\n console.warn('Failed to check private servers', userId, err);\n return null;\n }\n};\n// Fetch whether or not a user has a Roblox+ Premium subscription.\nconst loadPremiumMembership = async (userId) => {\n if (definitelyPremium[userId]) {\n return definitelyPremium[userId];\n }\n const expirationDate = await checkPrivateServerExpirations(userId);\n if (expirationDate) {\n return (definitelyPremium[userId] = expirationDate);\n }\n const response = await fetch(`https://api.roblox.plus/v1/rpluspremium/${userId}`);\n if (!response.ok) {\n throw new Error(`Failed to check premium membership for user (${userId})`);\n }\n const result = await response.json();\n if (result.data) {\n return (definitelyPremium[userId] = result.data.expiration);\n }\n return '';\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadPremiumMembership(message.userId));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getPremiumExpirationDate;\n","import { default as getPremiumExpirationDate } from './getPremiumExpirationDate';\nconst isPremiumUser = async (userId) => {\n const expiration = await getPremiumExpirationDate(userId);\n if (expiration || expiration === null) {\n // We have an expiration date, or it's a lifetime subscription.\n // They are definitely premium.\n return true;\n }\n // No expiration date, no premium.\n return false;\n};\nglobalThis.premiumService = { isPremiumUser, getPremiumExpirationDate };\nexport { isPremiumUser, getPremiumExpirationDate };\n","import { Batch } from '@tix-factory/batch';\nimport { PresenceType } from 'roblox';\nconst getPresenceType = (presenceType) => {\n switch (presenceType) {\n case 1:\n return PresenceType.Online;\n case 2:\n return PresenceType.Experience;\n case 3:\n return PresenceType.Studio;\n default:\n return PresenceType.Offline;\n }\n};\nconst getLocationName = (presenceType, name) => {\n if (!name) {\n return '';\n }\n if (presenceType === PresenceType.Studio) {\n return name.replace(/^Studio\\s+-\\s*/, '');\n }\n return name;\n};\nclass PresenceBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 3 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await fetch('https://presence.roblox.com/v1/presence/users', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n userIds: items.map((i) => i.value),\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to load user presence');\n }\n const result = await response.json();\n items.forEach((item) => {\n const presence = result.userPresences.find((p) => p.userId === item.value);\n if (presence) {\n const presenceType = getPresenceType(presence.userPresenceType);\n if (presence.placeId &&\n (presenceType === PresenceType.Experience ||\n presenceType === PresenceType.Studio)) {\n item.resolve({\n type: presenceType,\n location: {\n placeId: presence.placeId || undefined,\n universeId: presence.universeId || undefined,\n name: getLocationName(presenceType, presence.lastLocation),\n serverId: presence.gameId,\n },\n });\n }\n else {\n item.resolve({\n type: presenceType,\n });\n }\n }\n else {\n item.resolve({\n type: PresenceType.Offline,\n });\n }\n });\n }\n}\nexport default PresenceBatchProcessor;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport PresenceBatchProcessor from './batchProcessor';\nconst messageDestination = 'presenceService.getUserPresence';\nconst presenceProcessor = new PresenceBatchProcessor();\nconst presenceCache = new ExpirableDictionary('presenceService', 15 * 1000);\n// Fetches the presence for a user.\nconst getUserPresence = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return presenceCache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n presenceProcessor.enqueue(message.userId));\n});\nglobalThis.presenceService = { getUserPresence };\nexport { getUserPresence };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'privateMessagesService.getUnreadMessageCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the unread private message count for the currently authenticated user.\nconst getUnreadMessageCount = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the unread private message count for the authenticated user.\nconst loadUnreadMessageCount = async (userId) => {\n // User ID is used as a cache buster.\n const response = await fetch(`https://privatemessages.roblox.com/v1/messages/unread/count`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw 'Failed to load unread private message count';\n }\n const result = await response.json();\n return result.count;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUnreadMessageCount(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getUnreadMessageCount;\n","import { default as getUnreadMessageCount } from './getUnreadMessageCount';\nglobalThis.privateMessagesService = { getUnreadMessageCount };\nexport { getUnreadMessageCount };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\n// Destination to be used with messaging.\nconst messageDestinationPrefix = 'settingsService';\n// Fetches a locally stored setting value by its key.\nconst getSettingValue = (key) => {\n return sendMessage(`${messageDestinationPrefix}.getSettingValue`, {\n key,\n });\n};\n// Gets a boolean setting value, toggled to false by default.\nconst getToggleSettingValue = async (key) => {\n const value = await getSettingValue(key);\n return !!value;\n};\n// Locally stores a setting value.\nconst setSettingValue = (key, value) => {\n return sendMessage(`${messageDestinationPrefix}.setSettingValue`, {\n key,\n value,\n });\n};\nconst getValueFromLocalStorage = (key) => {\n if (!localStorage.hasOwnProperty(key)) {\n return undefined;\n }\n try {\n const valueArray = JSON.parse(localStorage[key]);\n if (Array.isArray(valueArray) && valueArray.length > 0) {\n return valueArray[0];\n }\n console.warn(`Setting value in localStorage invalid: ${localStorage[key]} - removing it.`);\n localStorage.removeItem(key);\n return undefined;\n }\n catch (err) {\n console.warn(`Failed to parse '${key}' value from localStorage - removing it.`, err);\n localStorage.removeItem(key);\n return undefined;\n }\n};\naddListener(`${messageDestinationPrefix}.getSettingValue`, ({ key }) => {\n return new Promise((resolve, reject) => {\n // chrome.storage APIs are callback-based until manifest V3.\n // Currently in migration phase, to migrate settings from localStorage -> chrome.storage.local\n const value = getValueFromLocalStorage(key);\n if (value !== undefined) {\n chrome.storage.local.set({\n [key]: value,\n }, () => {\n localStorage.removeItem(key);\n resolve(value);\n });\n }\n else {\n chrome.storage.local.get(key, (values) => {\n resolve(values[key]);\n });\n }\n });\n}, {\n levelOfParallelism: -1,\n allowExternalConnections: true,\n});\naddListener(`${messageDestinationPrefix}.setSettingValue`, ({ key, value }) => {\n return new Promise((resolve, reject) => {\n // chrome.storage APIs are callback-based until manifest V3.\n // Currently in migration phase, to migrate settings from localStorage -> chrome.storage.local\n if (value === undefined) {\n chrome.storage.local.remove(key, () => {\n localStorage.removeItem(key);\n resolve(undefined);\n });\n }\n else {\n chrome.storage.local.set({\n [key]: value,\n }, () => {\n localStorage.removeItem(key);\n resolve(undefined);\n });\n }\n });\n}, {\n levelOfParallelism: -1,\n allowExternalConnections: true,\n});\nglobalThis.settingsService = { getSettingValue, getToggleSettingValue, setSettingValue };\nexport { getSettingValue, getToggleSettingValue, setSettingValue };\n","import { Batch } from '@tix-factory/batch';\nimport { ThumbnailState } from 'roblox';\nclass ThumbnailBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await fetch('https://thumbnails.roblox.com/v1/batch', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(items.map(({ value }) => {\n return {\n requestId: `${value.type}_${value.targetId}_${value.size}`,\n type: value.type,\n targetId: value.targetId,\n size: value.size,\n };\n })),\n });\n if (!response.ok) {\n throw new Error('Failed to load thumbnails');\n }\n const result = await response.json();\n items.forEach((item) => {\n const thumbnail = result.data.find((t) => t.requestId ===\n `${item.value.type}_${item.value.targetId}_${item.value.size}`);\n if (thumbnail) {\n const thumbnailState = thumbnail.state;\n item.resolve({\n state: thumbnailState,\n imageUrl: thumbnailState === ThumbnailState.Completed\n ? thumbnail.imageUrl\n : '',\n });\n }\n else {\n item.resolve({\n state: ThumbnailState.Error,\n imageUrl: '',\n });\n }\n });\n }\n}\nconst thumbnailBatchProcessor = new ThumbnailBatchProcessor();\nexport default thumbnailBatchProcessor;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { ThumbnailState, ThumbnailType } from 'roblox';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport batchProcessor from './batchProcessor';\nconst messageDestination = 'thumbnailsService.getThumbnail';\nconst cache = new ExpirableDictionary(messageDestination, 5 * 60 * 1000);\n// Fetches an avatar headshot thumbnail, for the given user ID.\nconst getAvatarHeadshotThumbnail = (userId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.AvatarHeadShot,\n targetId: userId,\n });\n};\n// Fetches an asset thumbnail, for the given asset ID.\nconst getAssetThumbnail = (assetId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.Asset,\n targetId: assetId,\n });\n};\n// Fetches a group icon.\nconst getGroupIcon = (groupId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.GroupIcon,\n targetId: groupId,\n });\n};\n// Fetches a game pass icon.\nconst getGamePassIcon = (gamePassId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.GamePass,\n targetId: gamePassId,\n });\n};\n// Fetches a developer product icon.\nconst getDeveloperProductIcon = (gamePassId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.DeveloperProduct,\n targetId: gamePassId,\n });\n};\n// Fetches a game icon.\nconst getGameIcon = (gamePassId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.GameIcon,\n targetId: gamePassId,\n });\n};\n// Gets the default size for the thumbnail, by type.\nconst getThumbnailSize = (thumbnailType) => {\n switch (thumbnailType) {\n case ThumbnailType.GamePass:\n return '150x150';\n case ThumbnailType.GameIcon:\n return '256x256';\n default:\n return '420x420';\n }\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, async (message) => {\n const cacheKey = `${message.type}:${message.targetId}`;\n // Check the cache\n const thumbnail = await cache.getOrAdd(cacheKey, () => \n // Queue up the fetch request, when not in the cache\n batchProcessor.enqueue({\n type: message.type,\n targetId: message.targetId,\n size: getThumbnailSize(message.type),\n }));\n if (thumbnail.state !== ThumbnailState.Completed) {\n setTimeout(() => {\n // If the thumbnail isn't complete, evict it from the cache early.\n cache.evict(cacheKey);\n }, 30 * 1000);\n }\n return thumbnail;\n}, {\n levelOfParallelism: -1,\n allowExternalConnections: true,\n});\nglobalThis.thumbnailsService = {\n getAvatarHeadshotThumbnail,\n getAssetThumbnail,\n getGroupIcon,\n getGamePassIcon,\n getDeveloperProductIcon,\n getGameIcon,\n};\nexport { getAvatarHeadshotThumbnail, getAssetThumbnail, getGroupIcon, getGamePassIcon, getDeveloperProductIcon, getGameIcon, };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'tradesService.getTradeCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the unread private message count for the currently authenticated user.\nconst getTradeCount = (tradeStatusType) => {\n return sendMessage(messageDestination, {\n tradeStatusType,\n });\n};\n// Loads the unread private message count for the authenticated user.\nconst loadTradeCount = async (tradeStatusType) => {\n // User ID is used as a cache buster.\n const response = await fetch(`https://trades.roblox.com/v1/trades/${tradeStatusType}/count`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw `Failed to load ${tradeStatusType} trade count`;\n }\n const result = await response.json();\n return result.count;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.tradeStatusType}`, () => \n // Queue up the fetch request, when not in the cache\n loadTradeCount(message.tradeStatusType));\n}, {\n levelOfParallelism: 1,\n});\nexport default getTradeCount;\n","import { default as getTradeCount } from './getTradeCount';\nglobalThis.tradesService = { getTradeCount };\nexport { getTradeCount };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'transactionsService.emailTransactions';\n// Fetches the groups the user has access privileged roles in.\nconst emailTransactions = (targetType, targetId, transactionType, startDate, endDate) => {\n return sendMessage(messageDestination, {\n targetType,\n targetId,\n transactionType,\n startDate: startDate.getTime(),\n endDate: endDate.getTime(),\n });\n};\n// Loads the groups the user has access privileged roles in.\nconst doEmailTransactions = async (targetType, targetId, transactionType, startDate, endDate) => {\n const response = await xsrfFetch(new URL(`https://economy.roblox.com/v2/sales/sales-report-download`), {\n method: 'POST',\n body: JSON.stringify({\n targetType,\n targetId,\n transactionType,\n startDate: startDate.toISOString(),\n endDate: endDate.toISOString(),\n }),\n });\n if (!response.ok) {\n throw 'Failed to send transactions email';\n }\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return doEmailTransactions(message.targetType, message.targetId, message.transactionType, new Date(message.startDate), new Date(message.endDate));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default emailTransactions;\n","import emailTransactions from './email-transactions';\n// Sends an email to the authenticated user with the group's transactions (sales).\nconst emailGroupTransactionSales = (groupId, startDate, endDate) => emailTransactions('Group', groupId, 'Sale', startDate, endDate);\n// Sends an email to the authenticated user with their personally transactions (sales).\nconst emailUserTransactionSales = (userId, startDate, endDate) => emailTransactions('User', userId, 'Sale', startDate, endDate);\nglobalThis.transactionsService = { emailGroupTransactionSales, emailUserTransactionSales };\nexport { emailGroupTransactionSales, emailUserTransactionSales };\n","import { Batch } from '@tix-factory/batch';\nimport { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'usersService.getUserById';\nclass UsersBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await xsrfFetch(new URL(`https://users.roblox.com/v1/users`), {\n method: 'POST',\n body: JSON.stringify({\n userIds: items.map((i) => i.key),\n excludeBannedUsers: false,\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to users by ids');\n }\n const result = await response.json();\n items.forEach((item) => {\n const user = result.data.find((a) => a.id === item.value);\n if (user) {\n item.resolve({\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n });\n }\n else {\n item.resolve(null);\n }\n });\n }\n getKey(item) {\n return item.toString();\n }\n}\nconst batchProcessor = new UsersBatchProcessor();\nconst cache = new ExpirableDictionary(messageDestination, 2 * 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getUserById = async (id) => {\n return sendMessage(messageDestination, {\n id,\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(batchProcessor.getKey(message.id), () => {\n // Queue up the fetch request, when not in the cache\n return batchProcessor.enqueue(message.id);\n });\n});\nexport default getUserById;\n","import { Batch } from '@tix-factory/batch';\nimport { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'usersService.getUserByName';\nclass UserNamesBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await xsrfFetch(new URL(`https://users.roblox.com/v1/usernames/users`), {\n method: 'POST',\n body: JSON.stringify({\n usernames: items.map((i) => i.key),\n excludeBannedUsers: false,\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to users by names');\n }\n const result = await response.json();\n items.forEach((item) => {\n const user = result.data.find((a) => a.requestedUsername === item.key);\n if (user) {\n item.resolve({\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n });\n }\n else {\n item.resolve(null);\n }\n });\n }\n getKey(item) {\n return item;\n }\n}\nconst batchProcessor = new UserNamesBatchProcessor();\nconst cache = new ExpirableDictionary(messageDestination, 2 * 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getUserByName = async (name) => {\n return sendMessage(messageDestination, {\n name: name.toLowerCase(),\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(batchProcessor.getKey(message.name), () => {\n // Queue up the fetch request, when not in the cache\n return batchProcessor.enqueue(message.name);\n });\n});\nexport default getUserByName;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nconst messageDestination = 'usersService.getAuthenticatedUser';\nconst cacheDuration = 60 * 1000;\nlet authenticatedUser = undefined;\n// Fetches the currently authenticated user.\nconst getAuthenticatedUser = () => {\n return sendMessage(messageDestination, {});\n};\n// Loads the currently authenticated user.\nconst loadAuthenticatedUser = async () => {\n if (authenticatedUser !== undefined) {\n return authenticatedUser;\n }\n try {\n const response = await fetch('https://users.roblox.com/v1/users/authenticated');\n if (response.status === 401) {\n return (authenticatedUser = null);\n }\n else if (!response.ok) {\n throw new Error('Failed to load authenticated user');\n }\n const result = await response.json();\n return (authenticatedUser = {\n id: result.id,\n name: result.name,\n displayName: result.displayName,\n });\n }\n finally {\n setTimeout(() => {\n authenticatedUser = undefined;\n }, cacheDuration);\n }\n};\naddListener(messageDestination, () => loadAuthenticatedUser(), {\n levelOfParallelism: 1,\n});\nexport default getAuthenticatedUser;\n","import getAuthenticatedUser from './getAuthenticatedUser';\nimport getUserByName from './get-user-by-name';\nimport getUserById from './get-user-by-id';\nglobalThis.usersService = { getAuthenticatedUser, getUserByName, getUserById };\nexport { getAuthenticatedUser, getUserByName, getUserById };\n","// This class can be used to concurrently cache items, or fetch their values.\nclass ExpirableDictionary {\n lockKey;\n expirationInMilliseconds;\n // The items that are in the dictionary.\n items = {};\n constructor(\n // A name for the dictionary, used for locking.\n name, \n // How long the item will remain in the dictionary, in milliseconds.\n expirationInMilliseconds) {\n this.lockKey = `ExpirableDictionary:${name}`;\n this.expirationInMilliseconds = expirationInMilliseconds;\n }\n // Tries to fetch an item by its key from the dictionary, or it will call the value factory to add it in.\n getOrAdd(key, valueFactory) {\n const item = this.items[key];\n if (item !== undefined) {\n return Promise.resolve(item);\n }\n return new Promise((resolve, reject) => {\n navigator.locks\n .request(`${this.lockKey}:${key}`, async () => {\n // It's possible the item was added since we requested the lock, check again.\n const item = this.items[key];\n if (item !== undefined) {\n resolve(item);\n return;\n }\n try {\n const value = (this.items[key] = await valueFactory());\n setTimeout(() => this.evict(key), this.expirationInMilliseconds);\n resolve(value);\n }\n catch (e) {\n reject(e);\n }\n })\n .catch(reject);\n });\n }\n evict(key) {\n delete this.items[key];\n }\n}\nexport default ExpirableDictionary;\n","import ExpirableDictionary from './expireableDictionary';\nconst cache = new ExpirableDictionary('fetchDataUri', 5 * 60 * 1000);\n// Converts a URL to a data URI of its loaded contents.\nexport default (url) => {\n return cache.getOrAdd(url.href, () => {\n return new Promise((resolve, reject) => {\n fetch(url.href)\n .then((result) => {\n const reader = new FileReader();\n reader.onerror = (err) => {\n reject(err);\n };\n reader.onloadend = () => {\n if (typeof reader.result === 'string') {\n resolve(reader.result);\n }\n else {\n reject(new Error(`fetchDataUri: Unexpected result type (${typeof reader.result})`));\n }\n };\n result\n .blob()\n .then((blob) => {\n reader.readAsDataURL(blob);\n })\n .catch(reject);\n })\n .catch(reject);\n });\n });\n};\n","import { addListener, getWorkerTab, sendMessage, sendMessageToTab, } from '@tix-factory/extension-messaging';\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nconst messageDestination = 'launchProtocolUrl';\n// Keep track of the tabs, so we can put the user back where they were.b\nlet previousTab = undefined;\nlet protocolLauncherTab = undefined;\n// Attempt to launch the protocol URL in the current tab.\nconst tryDirectLaunch = (protocolUrl) => {\n if (!isBackgroundPage && location) {\n location.href = protocolUrl;\n return true;\n }\n return false;\n};\n// Launch the protocol URL from a service worker.\nconst launchProtocolUrl = (protocolUrl) => {\n if (tryDirectLaunch(protocolUrl)) {\n // We were able to directly launch the protocol URL.\n // Nothing more to do.\n return Promise.resolve();\n }\n const workerTab = getWorkerTab();\n if (workerTab) {\n // If we're in the background, and we have a tab that can process the protocol URL, use that instead.\n // This will ensure that when we use the protocol launcher to launch Roblox, that they have the highest\n // likihood of already having accepted the protocol launcher permission.\n sendMessageToTab(messageDestination, {\n protocolUrl,\n }, workerTab);\n return Promise.resolve();\n }\n // TODO: Convert to promise signatures when moving to manifest V3.\n chrome.tabs.query({\n active: true,\n currentWindow: true,\n }, (currentTab) => {\n previousTab = currentTab[0];\n if (previousTab) {\n // Try to open the protocol launcher tab right next to the current tab, so that when it\n // closes, it will put the user back on the tab they are on now.\n chrome.tabs.create({\n url: protocolUrl,\n index: previousTab.index + 1,\n windowId: previousTab.windowId,\n }, (tab) => {\n protocolLauncherTab = tab;\n });\n }\n else {\n chrome.tabs.create({ url: protocolUrl });\n // If we don't know where they were before, then don't try to keep track of anything.\n previousTab = undefined;\n protocolLauncherTab = undefined;\n }\n });\n return Promise.resolve();\n};\nif (isBackgroundPage) {\n chrome.tabs.onRemoved.addListener((tabId) => {\n // Return the user to the tab they were on before, when we're done launching the protocol URL.\n // chrome self-closes the protocol URL tab when opened.\n if (tabId === protocolLauncherTab?.id && previousTab?.id) {\n chrome.tabs.update(previousTab.id, {\n active: true,\n });\n }\n previousTab = undefined;\n protocolLauncherTab = undefined;\n });\n}\naddListener(messageDestination, (message) => launchProtocolUrl(message.protocolUrl));\n// Launches a protocol URL, using the most user-friendly method.\nexport default async (protocolUrl) => {\n if (tryDirectLaunch(protocolUrl)) {\n // If we can directly launch the protocol URL, there's nothing left to do.\n return;\n }\n // Otherwise, we have to send a message out and try some nonsense.\n await sendMessage(messageDestination, { protocolUrl });\n};\n","const headerName = 'X-CSRF-Token';\nlet xsrfToken = '';\n// A fetch request which will attach an X-CSRF-Token in all outbound requests.\nconst xsrfFetch = async (url, requestDetails) => {\n if (url.hostname.endsWith('.roblox.com')) {\n if (!requestDetails) {\n requestDetails = {};\n }\n requestDetails.credentials = 'include';\n if (!requestDetails.headers) {\n requestDetails.headers = new Headers();\n }\n if (requestDetails.headers instanceof Headers) {\n if (xsrfToken) {\n requestDetails.headers.set(headerName, xsrfToken);\n }\n if (requestDetails.body && !requestDetails.headers.has('Content-Type')) {\n requestDetails.headers.set('Content-Type', 'application/json');\n }\n }\n }\n const response = await fetch(url, requestDetails);\n const token = response.headers.get(headerName);\n if (response.ok || !token) {\n return response;\n }\n xsrfToken = token;\n return xsrfFetch(url, requestDetails);\n};\nexport default xsrfFetch;\n","import PromiseQueue from '../promise-queue';\nimport ErrorEvent from '../events/errorEvent';\nimport ItemErrorEvent from '../events/itemErrorEvent';\n// A class for batching and processing multiple single items into a single call.\nclass Batch extends EventTarget {\n queueMap = {};\n promiseMap = {};\n limiter;\n concurrencyHandler;\n // All the batch items waiting to be processed.\n queueArray = [];\n // The configuration for this batch processor.\n config;\n constructor(configuration) {\n super();\n this.config = configuration;\n this.limiter = new PromiseQueue({\n levelOfParallelism: 1,\n delayInMilliseconds: configuration.minimumDelay || 0,\n });\n this.concurrencyHandler = new PromiseQueue({\n levelOfParallelism: configuration.levelOfParallelism || Infinity,\n });\n }\n // Enqueues an item into a batch, to be processed.\n enqueue(item) {\n return new Promise((resolve, reject) => {\n const key = this.getKey(item);\n const promiseMap = this.promiseMap;\n const queueArray = this.queueArray;\n const queueMap = this.queueMap;\n const retryCount = this.config.retryCount || 0;\n const getRetryDelay = this.getRetryDelay.bind(this);\n const dispatchEvent = this.dispatchEvent.bind(this);\n const check = this.check.bind(this);\n // Step 1: Ensure we have a way to resolve/reject the promise for this item.\n const mergedPromise = promiseMap[key] || [];\n if (mergedPromise.length < 0) {\n this.promiseMap[key] = mergedPromise;\n }\n mergedPromise.push({ resolve, reject });\n // Step 2: Check if we have the batched item created.\n if (!queueMap[key]) {\n const remove = (item) => {\n // Mark the item as completed, so we know we either resolved or rejected it.\n item.completed = true;\n for (let i = 0; i < queueArray.length; i++) {\n if (queueArray[i].key === key) {\n queueArray.splice(i, 1);\n break;\n }\n }\n delete promiseMap[key];\n delete queueMap[key];\n };\n const batchItem = {\n key,\n value: item,\n attempt: 0,\n retryAfter: 0,\n completed: false,\n resolve(result) {\n // We're not accepting any new items for this resolution.\n remove(this);\n // Defer the resolution until after the thread resolves.\n setTimeout(() => {\n // Process anyone who applied.\n while (mergedPromise.length > 0) {\n const promise = mergedPromise.shift();\n promise?.resolve(result);\n }\n }, 0);\n },\n reject(error) {\n // Defer the resolution until after the thread resolves.\n const retryDelay = this.attempt <= retryCount ? getRetryDelay(this) : undefined;\n const retryAfter = retryDelay !== undefined\n ? performance.now() + retryDelay\n : undefined;\n // Emit an event to notify that the item failed to process.\n dispatchEvent(new ItemErrorEvent(error, this, retryAfter));\n if (retryAfter !== undefined) {\n // The item can be retried, we haven't hit the maximum number of attempts yet.\n this.retryAfter = retryAfter;\n // Ensure the check runs after the retry delay.\n setTimeout(check, retryDelay);\n }\n else {\n // Remove the item, and reject anyone waiting on it.\n remove(this);\n // Defer the resolution until after the thread resolves.\n setTimeout(() => {\n // Process anyone who applied.\n while (mergedPromise.length > 0) {\n const promise = mergedPromise.shift();\n promise?.reject(error);\n }\n }, 0);\n }\n },\n };\n queueMap[key] = batchItem;\n queueArray.push(batchItem);\n }\n // Attempt to process the queue on the next event loop.\n setTimeout(check, this.config.enqueueDeferDelay);\n });\n }\n // Batches together queued items, calls the process method.\n // Will do nothing if the config requirements aren't met.\n check() {\n if (this.limiter.size > 0) {\n // Already being checked.\n return;\n }\n // We're using p-limit to ensure that multiple process calls can't be called at once.\n this.limiter.enqueue(this._check.bind(this)).catch((err) => {\n // This should be \"impossible\".. right?\n this.dispatchEvent(new ErrorEvent(err));\n });\n }\n // The actual implementation of the check method.\n _check() {\n const retry = this.check.bind(this);\n // Get a batch of items to process.\n const batch = this.getBatch();\n // Nothing in the queue ready to be processed.\n if (batch.length < 1) {\n return Promise.resolve();\n }\n // Update the items that we're about to process, so they don't get double processed.\n batch.forEach((item) => {\n item.attempt += 1;\n item.retryAfter = Infinity;\n });\n setTimeout(async () => {\n try {\n await this.concurrencyHandler.enqueue(this.process.bind(this, batch));\n }\n catch (err) {\n this.dispatchEvent(new ErrorEvent(err));\n }\n finally {\n batch.forEach((item) => {\n if (item.completed) {\n // Item completed its processing, nothing more to do.\n return;\n }\n else if (item.retryAfter > 0 && item.retryAfter !== Infinity) {\n // The item failed to process, but it is going to be retried.\n return;\n }\n else {\n // Item neither rejected, or completed its processing status.\n // This is a requirement, so we reject the item.\n item.reject(new Error('Item was not marked as resolved or rejected after batch processing completed.'));\n }\n });\n // Now that we've finished processing the batch, run the process again, just in case there's anything left.\n setTimeout(retry, 0);\n }\n }, 0);\n if (batch.length >= this.config.maxSize) {\n // We have the maximum number of items in the batch, let's make sure we kick off the process call again.\n setTimeout(retry, this.config.minimumDelay);\n }\n return Promise.resolve();\n }\n getBatch() {\n const now = performance.now();\n const batch = [];\n for (let i = 0; i < this.queueArray.length; i++) {\n const batchItem = this.queueArray[i];\n if (batchItem.retryAfter > now) {\n // Item is not ready to be retried, or it is currently being processed.\n continue;\n }\n batch.push(batchItem);\n if (batch.length >= this.config.maxSize) {\n break;\n }\n }\n return batch;\n }\n // Obtains a unique key to identify the item.\n // This is used to deduplicate the batched items.\n getKey(item) {\n return item === undefined ? 'undefined' : JSON.stringify(item);\n }\n // Returns how long to wait before retrying the item.\n getRetryDelay(item) {\n return 0;\n }\n // Called when it is time to process a batch of items.\n process(items) {\n return Promise.reject(new Error('Inherit this class, and implement the processBatch method.'));\n }\n}\nexport default Batch;\n","// An event class which can be used to emit an error.\nclass ErrorEvent extends Event {\n // The error associated with the event.\n error;\n // Constructs the event from the error.\n constructor(error) {\n super('error');\n this.error = error;\n }\n}\nexport default ErrorEvent;\n","import ErrorEvent from './errorEvent';\n// An event class which can be used to emit an error event for an item that failed to process.\nclass ItemErrorEvent extends ErrorEvent {\n // The item that failed to process.\n batchItem;\n // The amount of time when the item will be retried.\n retryAfter;\n // Constructs the event from the error.\n constructor(error, batchItem, retryAfter) {\n super(error);\n this.batchItem = batchItem;\n this.retryAfter = retryAfter;\n }\n}\nexport default ItemErrorEvent;\n","// Export all the things from this module.\nexport { default as Batch } from './batch';\nexport { default as ErrorEvent } from './events/errorEvent';\nexport { default as ItemErrorEvent } from './events/itemErrorEvent';\nexport { default as PromiseQueue } from './promise-queue';\n","// A limiter for running promises in parallel.\n// Queue ensures order is maintained.\nclass PromiseQueue {\n // All the promises that have been enqueued, and are waiting to be processed.\n queue = [];\n // The PromiseQueue configuration.\n config;\n // How many promises are actively being processed.\n activeCount = 0;\n // The next time a promise can be processed.\n nextProcessTime = 0;\n // Constructs a promise queue, defining the number of promises that may run in parallel.\n constructor(config) {\n this.config = config;\n }\n // The number of promises waiting to be processed.\n get size() {\n return this.queue.length;\n }\n // Puts a function that will create the promise to run on the queue, and returns a promise\n // that will return the result of the enqueued promise.\n enqueue(createPromise) {\n return new Promise(async (resolve, reject) => {\n this.queue.push({\n deferredPromise: { resolve, reject },\n createPromise,\n });\n await this.process();\n });\n }\n async process() {\n if (this.activeCount >= this.config.levelOfParallelism) {\n // Already running max number of promises in parallel.\n return;\n }\n const reprocess = this.process.bind(this);\n const delayInMilliseconds = this.config.delayInMilliseconds;\n if (delayInMilliseconds !== undefined && delayInMilliseconds > 0) {\n const now = performance.now();\n const remainingTime = this.nextProcessTime - now;\n if (remainingTime > 0) {\n // We're not allowed to process the next promise yet.\n setTimeout(reprocess, remainingTime);\n return;\n }\n this.nextProcessTime = now + delayInMilliseconds;\n }\n const promise = this.queue.shift();\n if (!promise) {\n // No promise to process.\n return;\n }\n this.activeCount++;\n try {\n const result = await promise.createPromise();\n promise.deferredPromise.resolve(result);\n }\n catch (err) {\n promise.deferredPromise.reject(err);\n }\n finally {\n // Ensure we subtract from how many promises are active\n this.activeCount--;\n // And then run the process function again, in case there are any promises left to run.\n setTimeout(reprocess, 0);\n }\n }\n}\nexport default PromiseQueue;\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export * as assets from '../services/assets';\nexport * as avatar from '../services/avatar';\nexport * as badges from '../services/badges';\nexport * as currency from '../services/currency';\nexport * as followings from '../services/followings';\nexport * as friends from '../services/friends';\nexport * as gameLaunch from '../services/game-launch';\nexport * as gamePasses from '../services/game-passes';\nexport * as groups from '../services/groups';\nexport * as inventory from '../services/inventory';\nexport * as localization from '../services/localization';\nexport * as premium from '../services/premium';\nexport * as premiumPayouts from '../services/premium-payouts';\nexport * as presence from '../services/presence';\nexport * as privateMessages from '../services/private-messages';\nexport * as settings from '../services/settings';\nexport * as thumbnails from '../services/thumbnails';\nexport * as trades from '../services/trades';\nexport * as transactions from '../services/transactions';\nexport * as users from '../services/users';\nimport { addListener } from '@tix-factory/extension-messaging';\nimport { manifest } from '@tix-factory/extension-utils';\nexport * from './notifiers';\nchrome.browserAction.setTitle({\n title: `${manifest.name} ${manifest.version}`,\n});\nchrome.browserAction.onClicked.addListener(() => {\n chrome.tabs.create({\n url: manifest.homepage_url,\n active: true,\n });\n});\naddListener('extension.reload', async () => {\n setTimeout(() => {\n chrome.runtime.reload();\n }, 250);\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\n"],"names":[],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"./service-worker.js","mappings":";;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC7SA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACxCA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AC7EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;ACrCA;AACA;;;;;;;;;;;;;;;;;;;ACDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;ACzKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC/LA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC9FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACjDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;ACvEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;ACxMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACjEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC9CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC7BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC7BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACrDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACtDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;ACxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;ACzCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC1GA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACHA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC/BA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACnBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACnCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AClCA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACHA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACxBA;AACA;AACA;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACnCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC1BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AChCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACjCA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;AC5BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AChBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC5DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACnFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC9CA;AACA;AACA;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC7EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AClBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACnCA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACvFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;ACpDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACrCA;AACA;AACA;;;;;;;;;;;;;;;;;;ACFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACrCA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;AC5DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC5DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;ACrCA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC7CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC9BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AC/EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC7BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACtMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACdA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;ACJA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACpEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;ACPA;;;;;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://roblox-plus/./libs/extension-messaging/dist/constants.js","webpack://roblox-plus/./libs/extension-messaging/dist/index.js","webpack://roblox-plus/./libs/extension-messaging/dist/tabs.js","webpack://roblox-plus/./libs/extension-utils/dist/constants/index.js","webpack://roblox-plus/./libs/extension-utils/dist/enums/loading-state.js","webpack://roblox-plus/./libs/extension-utils/dist/index.js","webpack://roblox-plus/./libs/extension-utils/dist/utils/wait.js","webpack://roblox-plus/./libs/roblox/dist/enums/asset-type.js","webpack://roblox-plus/./libs/roblox/dist/enums/presence-type.js","webpack://roblox-plus/./libs/roblox/dist/enums/thumbnail-state.js","webpack://roblox-plus/./libs/roblox/dist/enums/thumbnail-type.js","webpack://roblox-plus/./libs/roblox/dist/enums/trade-status-type.js","webpack://roblox-plus/./libs/roblox/dist/index.js","webpack://roblox-plus/./libs/roblox/dist/utils/linkify.js","webpack://roblox-plus/./node_modules/db.js/dist/db.min.js","webpack://roblox-plus/./src/js/service-worker/notifiers/catalog/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/friend-presence/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/group-shout/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/startup/index.ts","webpack://roblox-plus/./src/js/service-worker/notifiers/trades/index.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-contents-url.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-dependencies.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-details.ts","webpack://roblox-plus/./src/js/services/assets/get-asset-sales-count.ts","webpack://roblox-plus/./src/js/services/assets/index.ts","webpack://roblox-plus/./src/js/services/avatar/get-avatar-asset-rules.ts","webpack://roblox-plus/./src/js/services/avatar/index.ts","webpack://roblox-plus/./src/js/services/badges/batchProcessor.ts","webpack://roblox-plus/./src/js/services/badges/index.ts","webpack://roblox-plus/./src/js/services/currency/getRobuxBalance.ts","webpack://roblox-plus/./src/js/services/currency/history.ts","webpack://roblox-plus/./src/js/services/currency/index.ts","webpack://roblox-plus/./src/js/services/followings/authenticatedUserFollowingProcessor.ts","webpack://roblox-plus/./src/js/services/followings/index.ts","webpack://roblox-plus/./src/js/services/followings/isAuthenticatedUserFollowing.ts","webpack://roblox-plus/./src/js/services/friends/getFriendRequestCount.ts","webpack://roblox-plus/./src/js/services/friends/getUserFriends.ts","webpack://roblox-plus/./src/js/services/friends/index.ts","webpack://roblox-plus/./src/js/services/game-launch/index.ts","webpack://roblox-plus/./src/js/services/game-passes/get-game-pass-sale-count.ts","webpack://roblox-plus/./src/js/services/game-passes/index.ts","webpack://roblox-plus/./src/js/services/groups/get-creator-groups.ts","webpack://roblox-plus/./src/js/services/groups/get-group-shout.ts","webpack://roblox-plus/./src/js/services/groups/get-user-groups.ts","webpack://roblox-plus/./src/js/services/groups/get-user-primary-group.ts","webpack://roblox-plus/./src/js/services/groups/index.ts","webpack://roblox-plus/./src/js/services/inventory/get-asset-owners.ts","webpack://roblox-plus/./src/js/services/inventory/index.ts","webpack://roblox-plus/./src/js/services/inventory/limitedInventory.ts","webpack://roblox-plus/./src/js/services/localization/index.ts","webpack://roblox-plus/./src/js/services/premium-payouts/getPremiumPayoutsSummary.ts","webpack://roblox-plus/./src/js/services/premium-payouts/index.ts","webpack://roblox-plus/./src/js/services/premium/getPremiumExpirationDate.ts","webpack://roblox-plus/./src/js/services/premium/index.ts","webpack://roblox-plus/./src/js/services/presence/batchProcessor.ts","webpack://roblox-plus/./src/js/services/presence/index.ts","webpack://roblox-plus/./src/js/services/private-messages/getUnreadMessageCount.ts","webpack://roblox-plus/./src/js/services/private-messages/index.ts","webpack://roblox-plus/./src/js/services/settings/index.ts","webpack://roblox-plus/./src/js/services/thumbnails/batchProcessor.ts","webpack://roblox-plus/./src/js/services/thumbnails/index.ts","webpack://roblox-plus/./src/js/services/trades/getTradeCount.ts","webpack://roblox-plus/./src/js/services/trades/index.ts","webpack://roblox-plus/./src/js/services/transactions/email-transactions.ts","webpack://roblox-plus/./src/js/services/transactions/index.ts","webpack://roblox-plus/./src/js/services/users/get-user-by-id.ts","webpack://roblox-plus/./src/js/services/users/get-user-by-name.ts","webpack://roblox-plus/./src/js/services/users/getAuthenticatedUser.ts","webpack://roblox-plus/./src/js/services/users/index.ts","webpack://roblox-plus/./src/js/utils/expireableDictionary.ts","webpack://roblox-plus/./src/js/utils/fetchDataUri.ts","webpack://roblox-plus/./src/js/utils/launchProtocolUrl.ts","webpack://roblox-plus/./src/js/utils/xsrfFetch.ts","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/batch/index.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/events/errorEvent.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/events/itemErrorEvent.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/index.js","webpack://roblox-plus/./node_modules/@tix-factory/batch/dist/promise-queue/index.js","webpack://roblox-plus/webpack/bootstrap","webpack://roblox-plus/webpack/runtime/compat get default export","webpack://roblox-plus/webpack/runtime/define property getters","webpack://roblox-plus/webpack/runtime/hasOwnProperty shorthand","webpack://roblox-plus/webpack/runtime/make namespace object","webpack://roblox-plus/./src/js/service-worker/index.ts"],"sourcesContent":["// An identifier that tells us which version of the messaging service we're using,\n// to ensure we don't try to process a message not intended for us.\nconst version = 2.5;\nexport { version };\n","var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar _a;\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nimport { version } from './constants';\n// All the listeners, set in the background page.\nconst listeners = {};\n// Keep track of all the listeners that accept external calls.\nconst externalListeners = {};\nconst externalResponseHandlers = {};\n// Send a message to a destination, and get back the result.\nconst sendMessage = (destination, message, external) => __awaiter(void 0, void 0, void 0, function* () {\n return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {\n var _b;\n const serializedMessage = JSON.stringify(message);\n if (isBackgroundPage) {\n // Message is from the background page, to the background page.\n try {\n if (listeners[destination]) {\n const message = JSON.parse(serializedMessage);\n const result = yield listeners[destination](message);\n console.debug(`Local listener response for '${destination}':`, result, message);\n const data = result.data === undefined ? undefined : JSON.parse(result.data);\n if (result.success) {\n resolve(data);\n }\n else {\n reject(data);\n }\n }\n else {\n reject(`No message listener: ${destination}`);\n }\n }\n catch (e) {\n reject(e);\n }\n }\n else if (chrome === null || chrome === void 0 ? void 0 : chrome.runtime) {\n // Message is being sent from the content script\n const outboundMessage = JSON.stringify({\n version,\n destination,\n external,\n message: serializedMessage,\n });\n console.debug(`Sending message to '${destination}'`, serializedMessage);\n chrome.runtime.sendMessage(outboundMessage, (result) => {\n if (result === undefined) {\n reject(`Unexpected message result (undefined), suggests no listener in background page.\\n\\tDestination: ${destination}`);\n return;\n }\n const data = result.data === undefined ? undefined : JSON.parse(result.data);\n if (result.success) {\n resolve(data);\n }\n else {\n reject(data);\n }\n });\n }\n else if ((_b = document.body) === null || _b === void 0 ? void 0 : _b.dataset.extensionId) {\n // Message is being sent by the native browser tab.\n const messageId = crypto.randomUUID();\n const timeout = setTimeout(() => {\n if (externalResponseHandlers[messageId]) {\n delete externalResponseHandlers[messageId];\n reject(`Message timed out trying to contact extension`);\n }\n }, 15 * 1000);\n externalResponseHandlers[messageId] = {\n resolve: (result) => {\n clearTimeout(timeout);\n delete externalResponseHandlers[messageId];\n resolve(result);\n },\n reject: (error) => {\n clearTimeout(timeout);\n delete externalResponseHandlers[messageId];\n reject(error);\n },\n };\n window.postMessage({\n version,\n extensionId: document.body.dataset.extensionId,\n destination,\n message,\n messageId,\n });\n }\n else {\n reject(`Could not find a way to transport the message to the extension.`);\n }\n }));\n});\n// Listen for messages at a specific destination.\nconst addListener = (destination, listener, options = {\n levelOfParallelism: -1,\n}) => {\n if (listeners[destination]) {\n throw new Error(`${destination} already has message listener attached`);\n }\n const processMessage = (message) => __awaiter(void 0, void 0, void 0, function* () {\n try {\n console.debug(`Processing message for '${destination}'`, message);\n const result = yield listener(message);\n const response = {\n success: true,\n data: JSON.stringify(result),\n };\n console.debug(`Successful message result from '${destination}':`, response, message);\n return response;\n }\n catch (err) {\n const response = {\n success: false,\n data: JSON.stringify(err),\n };\n console.debug(`Failed message result from '${destination}':`, response, message, err);\n return response;\n }\n });\n listeners[destination] = (message) => {\n if (options.levelOfParallelism !== 1) {\n return processMessage(message);\n }\n return new Promise((resolve, reject) => {\n // https://stackoverflow.com/a/73482349/1663648\n navigator.locks\n .request(`messageService:${destination}`, () => __awaiter(void 0, void 0, void 0, function* () {\n try {\n const result = yield processMessage(message);\n resolve(result);\n }\n catch (e) {\n reject(e);\n }\n }))\n .catch(reject);\n });\n };\n if (options.allowExternalConnections) {\n externalListeners[destination] = true;\n }\n};\n// If we're currently in the background page, listen for messages.\nif (isBackgroundPage) {\n chrome.runtime.onMessage.addListener((rawMessage, sender, sendResponse) => {\n if (typeof rawMessage !== 'string') {\n // Not for us.\n return;\n }\n const fullMessage = JSON.parse(rawMessage);\n if (fullMessage.version !== version ||\n !fullMessage.destination ||\n !fullMessage.message) {\n // Not for us.\n return;\n }\n if (fullMessage.external && !externalListeners[fullMessage.destination]) {\n sendResponse({\n success: false,\n data: JSON.stringify('Listener does not accept external callers.'),\n });\n return;\n }\n const listener = listeners[fullMessage.destination];\n if (!listener) {\n sendResponse({\n success: false,\n data: JSON.stringify(`Could not route message to destination: ${fullMessage.destination}`),\n });\n return;\n }\n const message = JSON.parse(fullMessage.message);\n listener(message)\n .then(sendResponse)\n .catch((err) => {\n console.error('Listener is never expected to throw.', err, rawMessage, fullMessage);\n sendResponse({\n success: false,\n data: JSON.stringify('Listener threw unhandled exception (see background page for error).'),\n });\n });\n // Required for asynchronous callbacks\n // https://stackoverflow.com/a/20077854/1663648\n return true;\n });\n}\nelse if ((_a = globalThis.chrome) === null || _a === void 0 ? void 0 : _a.runtime) {\n console.debug(`Not attaching listener for messages, because we're not in the background.`);\n if (!window.messageServiceConnection) {\n const port = (window.messageServiceConnection = chrome.runtime.connect(chrome.runtime.id, {\n name: 'messageService',\n }));\n port.onMessage.addListener((rawMessage) => {\n if (typeof rawMessage !== 'string') {\n // Not for us.\n return;\n }\n const fullMessage = JSON.parse(rawMessage);\n if (fullMessage.version !== version ||\n !fullMessage.destination ||\n !fullMessage.message) {\n // Not for us.\n return;\n }\n const listener = listeners[fullMessage.destination];\n if (!listener) {\n // No listener in this tab for this message.\n return;\n }\n // We don't really have a way to communicate the response back to the service worker.\n // So we just... do nothing with it.\n const message = JSON.parse(fullMessage.message);\n listener(message).catch((err) => {\n console.error('Unhandled error processing message in tab', fullMessage, err);\n });\n });\n }\n // chrome.runtime is available, and we got a message from the window\n // this could be a tab trying to get information from the extension\n window.addEventListener('message', (messageEvent) => __awaiter(void 0, void 0, void 0, function* () {\n const { extensionId, messageId, destination, message } = messageEvent.data;\n if (extensionId !== chrome.runtime.id ||\n !messageId ||\n !destination ||\n !message) {\n // They didn't want to contact us.\n // Or if they did, they didn't have the required fields.\n return;\n }\n if (messageEvent.data.version !== version) {\n // They did want to contact us, but there was a version mismatch.\n // We can't handle this message.\n window.postMessage({\n extensionId,\n messageId,\n success: false,\n data: `Extension message receiver is incompatible with message sender`,\n });\n return;\n }\n console.debug('Received message for', destination, message);\n try {\n const response = yield sendMessage(destination, message, true);\n // Success! Now go tell the client they got everything they wanted.\n window.postMessage({\n extensionId,\n messageId,\n success: true,\n data: response,\n });\n }\n catch (e) {\n console.debug('Failed to send message to', destination, e);\n // :coffin:\n window.postMessage({\n extensionId,\n messageId,\n success: false,\n data: e,\n });\n }\n }));\n}\nelse {\n // Not a background page, and not a content script.\n // This could be a page where we want to listen for calls from the tab.\n window.addEventListener('message', (messageEvent) => {\n const { extensionId, messageId, success, data } = messageEvent.data;\n if (extensionId !== document.body.dataset.extensionId ||\n !messageId ||\n typeof success !== 'boolean') {\n // Not for us.\n return;\n }\n // Check to see if we have a handler waiting for this message response...\n const responseHandler = externalResponseHandlers[messageId];\n if (!responseHandler) {\n console.warn('We got a response back for a message we no longer have a handler for.', extensionId, messageId, success, data);\n return;\n }\n // Yay! Tell the krustomer we have their data, from the extension.\n console.debug('We received a response for', messageId, success, data);\n if (success) {\n responseHandler.resolve(data);\n }\n else {\n responseHandler.reject(data);\n }\n });\n}\nexport { getWorkerTab, sendMessageToTab } from './tabs';\nexport { addListener, sendMessage };\n","var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nimport { version } from './constants';\n// All the tabs actively connected to the message service.\nconst tabs = {};\n// Sends a message to a tab.\nconst sendMessageToTab = (destination, message, tab) => __awaiter(void 0, void 0, void 0, function* () {\n const serializedMessage = JSON.stringify(message);\n const outboundMessage = JSON.stringify({\n version,\n destination,\n message: serializedMessage,\n });\n console.debug(`Sending message to '${destination}' in tab`, serializedMessage, tab);\n tab.postMessage(outboundMessage);\n});\n// Fetches a tab that we can send a message to, for work processing.\nconst getWorkerTab = () => {\n const keys = Object.keys(tabs);\n return keys.length > 0 ? tabs[keys[0]] : undefined;\n};\nif (isBackgroundPage) {\n chrome.runtime.onConnect.addListener((port) => {\n const id = crypto.randomUUID();\n console.debug('Tab connected', id, port);\n tabs[id] = port;\n port.onDisconnect.addListener(() => {\n console.debug('Disconnecting tab', id, port);\n delete tabs[id];\n });\n });\n}\nexport { getWorkerTab, sendMessageToTab };\n","var _a, _b, _c, _d, _e;\nconst manifest = (_b = (_a = globalThis.chrome) === null || _a === void 0 ? void 0 : _a.runtime) === null || _b === void 0 ? void 0 : _b.getManifest();\nconst isBackgroundPage = ((_d = (_c = globalThis.chrome) === null || _c === void 0 ? void 0 : _c.runtime) === null || _d === void 0 ? void 0 : _d.getURL(((_e = manifest === null || manifest === void 0 ? void 0 : manifest.background) === null || _e === void 0 ? void 0 : _e.page) || '')) ===\n location.href;\nexport { isBackgroundPage, manifest };\n","// A generic loading state enum.\nvar LoadingState;\n(function (LoadingState) {\n LoadingState[\"Loading\"] = \"Loading\";\n LoadingState[\"Success\"] = \"Success\";\n LoadingState[\"Error\"] = \"Error\";\n})(LoadingState || (LoadingState = {}));\nexport default LoadingState;\n","// Export constants\nexport * from './constants';\n// Export enums\nexport { default as LoadingState } from './enums/loading-state';\n// Export utils\nexport { default as wait } from './utils/wait';\n","const wait = (time) => {\n return new Promise((resolve, reject) => {\n setTimeout(resolve, time);\n });\n};\nexport default wait;\n","var AssetType;\n(function (AssetType) {\n AssetType[AssetType[\"Image\"] = 1] = \"Image\";\n AssetType[AssetType[\"TShirt\"] = 2] = \"TShirt\";\n AssetType[AssetType[\"Audio\"] = 3] = \"Audio\";\n AssetType[AssetType[\"Mesh\"] = 4] = \"Mesh\";\n AssetType[AssetType[\"Lua\"] = 5] = \"Lua\";\n AssetType[AssetType[\"Html\"] = 6] = \"Html\";\n AssetType[AssetType[\"Text\"] = 7] = \"Text\";\n AssetType[AssetType[\"Hat\"] = 8] = \"Hat\";\n AssetType[AssetType[\"Place\"] = 9] = \"Place\";\n AssetType[AssetType[\"Model\"] = 10] = \"Model\";\n AssetType[AssetType[\"Shirt\"] = 11] = \"Shirt\";\n AssetType[AssetType[\"Pants\"] = 12] = \"Pants\";\n AssetType[AssetType[\"Decal\"] = 13] = \"Decal\";\n AssetType[AssetType[\"Avatar\"] = 16] = \"Avatar\";\n AssetType[AssetType[\"Head\"] = 17] = \"Head\";\n AssetType[AssetType[\"Face\"] = 18] = \"Face\";\n AssetType[AssetType[\"Gear\"] = 19] = \"Gear\";\n AssetType[AssetType[\"Badge\"] = 21] = \"Badge\";\n AssetType[AssetType[\"GroupEmblem\"] = 22] = \"GroupEmblem\";\n AssetType[AssetType[\"Animation\"] = 24] = \"Animation\";\n AssetType[AssetType[\"Arms\"] = 25] = \"Arms\";\n AssetType[AssetType[\"Legs\"] = 26] = \"Legs\";\n AssetType[AssetType[\"Torso\"] = 27] = \"Torso\";\n AssetType[AssetType[\"RightArm\"] = 28] = \"RightArm\";\n AssetType[AssetType[\"LeftArm\"] = 29] = \"LeftArm\";\n AssetType[AssetType[\"LeftLeg\"] = 30] = \"LeftLeg\";\n AssetType[AssetType[\"RightLeg\"] = 31] = \"RightLeg\";\n AssetType[AssetType[\"Package\"] = 32] = \"Package\";\n AssetType[AssetType[\"YouTubeVideo\"] = 33] = \"YouTubeVideo\";\n AssetType[AssetType[\"GamePass\"] = 34] = \"GamePass\";\n AssetType[AssetType[\"App\"] = 35] = \"App\";\n AssetType[AssetType[\"Code\"] = 37] = \"Code\";\n AssetType[AssetType[\"Plugin\"] = 38] = \"Plugin\";\n AssetType[AssetType[\"SolidModel\"] = 39] = \"SolidModel\";\n AssetType[AssetType[\"MeshPart\"] = 40] = \"MeshPart\";\n AssetType[AssetType[\"HairAccessory\"] = 41] = \"HairAccessory\";\n AssetType[AssetType[\"FaceAccessory\"] = 42] = \"FaceAccessory\";\n AssetType[AssetType[\"NeckAccessory\"] = 43] = \"NeckAccessory\";\n AssetType[AssetType[\"ShoulderAccessory\"] = 44] = \"ShoulderAccessory\";\n AssetType[AssetType[\"FrontAccessory\"] = 45] = \"FrontAccessory\";\n AssetType[AssetType[\"BackAccessory\"] = 46] = \"BackAccessory\";\n AssetType[AssetType[\"WaistAccessory\"] = 47] = \"WaistAccessory\";\n AssetType[AssetType[\"ClimbAnimation\"] = 48] = \"ClimbAnimation\";\n AssetType[AssetType[\"DeathAnimation\"] = 49] = \"DeathAnimation\";\n AssetType[AssetType[\"FallAnimation\"] = 50] = \"FallAnimation\";\n AssetType[AssetType[\"IdleAnimation\"] = 51] = \"IdleAnimation\";\n AssetType[AssetType[\"JumpAnimation\"] = 52] = \"JumpAnimation\";\n AssetType[AssetType[\"RunAnimation\"] = 53] = \"RunAnimation\";\n AssetType[AssetType[\"SwimAnimation\"] = 54] = \"SwimAnimation\";\n AssetType[AssetType[\"WalkAnimation\"] = 55] = \"WalkAnimation\";\n AssetType[AssetType[\"PoseAnimation\"] = 56] = \"PoseAnimation\";\n AssetType[AssetType[\"EarAccessory\"] = 57] = \"EarAccessory\";\n AssetType[AssetType[\"EyeAccessory\"] = 58] = \"EyeAccessory\";\n AssetType[AssetType[\"LocalizationTableManifest\"] = 59] = \"LocalizationTableManifest\";\n AssetType[AssetType[\"LocalizationTableTranslation\"] = 60] = \"LocalizationTableTranslation\";\n AssetType[AssetType[\"Emote\"] = 61] = \"Emote\";\n AssetType[AssetType[\"Video\"] = 62] = \"Video\";\n AssetType[AssetType[\"TexturePack\"] = 63] = \"TexturePack\";\n AssetType[AssetType[\"TShirtAccessory\"] = 64] = \"TShirtAccessory\";\n AssetType[AssetType[\"ShirtAccessory\"] = 65] = \"ShirtAccessory\";\n AssetType[AssetType[\"PantsAccessory\"] = 66] = \"PantsAccessory\";\n AssetType[AssetType[\"JacketAccessory\"] = 67] = \"JacketAccessory\";\n AssetType[AssetType[\"SweaterAccessory\"] = 68] = \"SweaterAccessory\";\n AssetType[AssetType[\"ShortsAccessory\"] = 69] = \"ShortsAccessory\";\n AssetType[AssetType[\"LeftShoeAccessory\"] = 70] = \"LeftShoeAccessory\";\n AssetType[AssetType[\"RightShoeAccessory\"] = 71] = \"RightShoeAccessory\";\n AssetType[AssetType[\"DressSkirtAccessory\"] = 72] = \"DressSkirtAccessory\";\n AssetType[AssetType[\"FontFamily\"] = 73] = \"FontFamily\";\n AssetType[AssetType[\"FontFace\"] = 74] = \"FontFace\";\n AssetType[AssetType[\"MeshHiddenSurfaceRemoval\"] = 75] = \"MeshHiddenSurfaceRemoval\";\n AssetType[AssetType[\"EyebrowAccessory\"] = 76] = \"EyebrowAccessory\";\n AssetType[AssetType[\"EyelashAccessory\"] = 77] = \"EyelashAccessory\";\n AssetType[AssetType[\"MoodAnimation\"] = 78] = \"MoodAnimation\";\n AssetType[AssetType[\"DynamicHead\"] = 79] = \"DynamicHead\";\n})(AssetType || (AssetType = {}));\nexport default AssetType;\n","// The types of user presence.\nvar PresenceType;\n(function (PresenceType) {\n // The user is offline.\n PresenceType[\"Offline\"] = \"Offline\";\n // The user is online.\n PresenceType[\"Online\"] = \"Online\";\n // The user is currently in an experience.\n PresenceType[\"Experience\"] = \"Experience\";\n // The user is currently in Roblox Studio.\n PresenceType[\"Studio\"] = \"Studio\";\n})(PresenceType || (PresenceType = {}));\nexport default PresenceType;\n","// Possible states for a thumbnail to be in.\nvar ThumbnailState;\n(function (ThumbnailState) {\n // The thumbnail had an unexpected error trying to load.\n ThumbnailState[\"Error\"] = \"Error\";\n // The thumbnailed loaded successfully.\n ThumbnailState[\"Completed\"] = \"Completed\";\n // The thumbnail is currently in review.\n ThumbnailState[\"InReview\"] = \"InReview\";\n // The thumbnail is pending, and should be retried.\n ThumbnailState[\"Pending\"] = \"Pending\";\n // The thumbnail is blocked.\n ThumbnailState[\"Blocked\"] = \"Blocked\";\n // The thumbnail is temporarily unavailable.\n ThumbnailState[\"TemporarilyUnavailable\"] = \"TemporarilyUnavailable\";\n})(ThumbnailState || (ThumbnailState = {}));\nexport default ThumbnailState;\n","// The types of thumbnails that can be requested.\nvar ThumbnailType;\n(function (ThumbnailType) {\n // An avatar head shot thumbnail.\n ThumbnailType[\"AvatarHeadShot\"] = \"AvatarHeadShot\";\n // The thumbnail for an asset.\n ThumbnailType[\"Asset\"] = \"Asset\";\n // The icon for a group.\n ThumbnailType[\"GroupIcon\"] = \"GroupIcon\";\n // The icon for a game pass.\n ThumbnailType[\"GamePass\"] = \"GamePass\";\n // The icon for a developer product.\n ThumbnailType[\"DeveloperProduct\"] = \"DeveloperProduct\";\n // The icon for a game.\n ThumbnailType[\"GameIcon\"] = \"GameIcon\";\n})(ThumbnailType || (ThumbnailType = {}));\nexport default ThumbnailType;\n","var TradeStatusType;\n(function (TradeStatusType) {\n TradeStatusType[\"Inbound\"] = \"Inbound\";\n TradeStatusType[\"Outbound\"] = \"Outbound\";\n TradeStatusType[\"Completed\"] = \"Completed\";\n TradeStatusType[\"Inactive\"] = \"Inactive\";\n})(TradeStatusType || (TradeStatusType = {}));\nexport default TradeStatusType;\n","// Export enums\nexport { default as AssetType } from './enums/asset-type';\nexport { default as PresenceType } from './enums/presence-type';\nexport { default as ThumbnailState } from './enums/thumbnail-state';\nexport { default as ThumbnailType } from './enums/thumbnail-type';\nexport { default as TradeStatusType } from './enums/trade-status-type';\n// Export utils\nexport * from './utils/linkify';\n","const getSEOLink = (id, name, path) => {\n if (!name) {\n name = 'redirect';\n }\n else {\n name =\n name\n .replace(/'/g, '')\n .replace(/\\W+/g, '-')\n .replace(/^-+/, '')\n .replace(/-+$/, '') || 'redirect';\n }\n return new URL(`https://www.roblox.com/${path}/${id}/${name}`);\n};\nconst getGroupLink = (groupId, groupName) => {\n return getSEOLink(groupId, groupName, 'groups');\n};\nconst getGamePassLink = (gamePassId, gamePassName) => {\n return getSEOLink(gamePassId, gamePassName, 'game-pass');\n};\nconst getCatalogLink = (assetId, assetName) => {\n return getSEOLink(assetId, assetName, 'catalog');\n};\nconst getLibraryLink = (assetId, assetName) => {\n return getSEOLink(assetId, assetName, 'library');\n};\nconst getPlaceLink = (placeId, placeName) => {\n return getSEOLink(placeId, placeName, 'games');\n};\nconst getUserProfileLink = (userId) => {\n return getSEOLink(userId, 'profile', 'users');\n};\nconst getIdFromUrl = (url) => {\n const match = url.pathname.match(/^\\/(badges|games|game-pass|groups|catalog|library|users)\\/(\\d+)\\/?/i) || [];\n // Returns NaN if the URL doesn't match.\n return Number(match[2]);\n};\nexport { getCatalogLink, getGamePassLink, getGroupLink, getIdFromUrl, getLibraryLink, getPlaceLink, getUserProfileLink, };\n","!function(a){if(\"object\"==typeof exports&&\"undefined\"!=typeof module)module.exports=a();else if(\"function\"==typeof define&&define.amd)define([],a);else{var b;b=\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:this,b.db=a()}}(function(){var a;return function b(a,c,d){function e(g,h){if(!c[g]){if(!a[g]){var i=\"function\"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error(\"Cannot find module '\"+g+\"'\");throw j.code=\"MODULE_NOT_FOUND\",j}var k=c[g]={exports:{}};a[g][0].call(k.exports,function(b){var c=a[g][1][b];return e(c?c:b)},k,k.exports,b,a,c,d)}return c[g].exports}for(var f=\"function\"==typeof require&&require,g=0;gu)u=m[0],b.advance(m[0]);else if(null!==m&&u>=m[0]+m[1]);else{var c=function(){var a=!0,c=\"value\"in b?b.value:b.key;try{n.forEach(function(b){a=\"function\"==typeof b[0]?a&&b[0](c):a&&c[b[0]]===b[1]})}catch(d){return q(d),{v:void 0}}if(a){if(u++,i)try{c=A(c),b.update(c)}catch(d){return q(d),{v:void 0}}try{t.push(o(c))}catch(d){return q(d),{v:void 0}}}b[\"continue\"]()}();if(\"object\"===(\"undefined\"==typeof c?\"undefined\":g(c)))return c.v}}})},n=function(a,b,c){var e=[],f=\"next\",h=\"openCursor\",j=null,k=m,n=!1,o=d||c,p=function(){return o?Promise.reject(o):l(a,b,h,n?f+\"unique\":f,j,e,k)},q=function(){return f=null,h=\"count\",{execute:p}},r=function(){return h=\"openKeyCursor\",{desc:u,distinct:v,execute:p,filter:t,limit:s,map:x}},s=function(a,b){return j=b?[a,b]:[0,a],o=j.some(function(a){return\"number\"!=typeof a})?new Error(\"limit() arguments must be numeric\"):o,{desc:u,distinct:v,filter:t,keys:r,execute:p,map:x,modify:w}},t=function y(a,b){return e.push([a,b]),{desc:u,distinct:v,execute:p,filter:y,keys:r,limit:s,map:x,modify:w}},u=function(){return f=\"prev\",{distinct:v,execute:p,filter:t,keys:r,limit:s,map:x,modify:w}},v=function(){return n=!0,{count:q,desc:u,execute:p,filter:t,keys:r,limit:s,map:x,modify:w}},w=function(a){return i=a&&\"object\"===(\"undefined\"==typeof a?\"undefined\":g(a))?a:null,{execute:p}},x=function(a){return k=a,{count:q,desc:u,distinct:v,execute:p,filter:t,keys:r,limit:s,modify:w}};return{count:q,desc:u,distinct:v,execute:p,filter:t,keys:r,limit:s,map:x,modify:w}};[\"only\",\"bound\",\"upperBound\",\"lowerBound\"].forEach(function(a){f[a]=function(){return n(a,arguments)}}),this.range=function(a){var b=void 0,c=[null,null];try{c=h(a)}catch(d){b=d}return n.apply(void 0,e(c).concat([b]))},this.filter=function(){var a=n(null,null);return a.filter.apply(a,arguments)},this.all=function(){return this.filter()}},r=function(a,b,c,e){var f=this,g=!1;if(this.getIndexedDB=function(){return a},this.isClosed=function(){return g},this.query=function(b,c){var d=g?new Error(\"Database has been closed\"):null;return new q(b,a,c,d)},this.add=function(b){for(var c=arguments.length,e=Array(c>1?c-1:0),f=1;c>f;f++)e[f-1]=arguments[f];return new Promise(function(c,f){if(g)return void f(new Error(\"Database has been closed\"));var h=e.reduce(function(a,b){return a.concat(b)},[]),j=a.transaction(b,k.readwrite);j.onerror=function(a){a.preventDefault(),f(a)},j.onabort=function(a){return f(a)},j.oncomplete=function(){return c(h)};var m=j.objectStore(b);h.some(function(a){var b=void 0,c=void 0;if(d(a)&&l.call(a,\"item\")&&(c=a.key,a=a.item,null!=c))try{c=i(c)}catch(e){return f(e),!0}try{b=null!=c?m.add(a,c):m.add(a)}catch(e){return f(e),!0}b.onsuccess=function(b){if(d(a)){var c=b.target,e=c.source.keyPath;null===e&&(e=\"__id__\"),l.call(a,e)||Object.defineProperty(a,e,{value:c.result,enumerable:!0})}}})})},this.update=function(b){for(var c=arguments.length,e=Array(c>1?c-1:0),f=1;c>f;f++)e[f-1]=arguments[f];return new Promise(function(c,f){if(g)return void f(new Error(\"Database has been closed\"));var h=e.reduce(function(a,b){return a.concat(b)},[]),j=a.transaction(b,k.readwrite);j.onerror=function(a){a.preventDefault(),f(a)},j.onabort=function(a){return f(a)},j.oncomplete=function(){return c(h)};var m=j.objectStore(b);h.some(function(a){var b=void 0,c=void 0;if(d(a)&&l.call(a,\"item\")&&(c=a.key,a=a.item,null!=c))try{c=i(c)}catch(e){return f(e),!0}try{b=null!=c?m.put(a,c):m.put(a)}catch(g){return f(g),!0}b.onsuccess=function(b){if(d(a)){var c=b.target,e=c.source.keyPath;null===e&&(e=\"__id__\"),l.call(a,e)||Object.defineProperty(a,e,{value:c.result,enumerable:!0})}}})})},this.put=function(){return this.update.apply(this,arguments)},this.remove=function(b,c){return new Promise(function(d,e){if(g)return void e(new Error(\"Database has been closed\"));try{c=i(c)}catch(f){return void e(f)}var h=a.transaction(b,k.readwrite);h.onerror=function(a){a.preventDefault(),e(a)},h.onabort=function(a){return e(a)},h.oncomplete=function(){return d(c)};var j=h.objectStore(b);try{j[\"delete\"](c)}catch(l){e(l)}})},this[\"delete\"]=function(){return this.remove.apply(this,arguments)},this.clear=function(b){return new Promise(function(c,d){if(g)return void d(new Error(\"Database has been closed\"));var e=a.transaction(b,k.readwrite);e.onerror=function(a){return d(a)},e.onabort=function(a){return d(a)},e.oncomplete=function(){return c()};var f=e.objectStore(b);f.clear()})},this.close=function(){return new Promise(function(d,e){return g?void e(new Error(\"Database has been closed\")):(a.close(),g=!0,delete o[b][c],void d())})},this.get=function(b,c){return new Promise(function(d,e){if(g)return void e(new Error(\"Database has been closed\"));try{c=i(c)}catch(f){return void e(f)}var h=a.transaction(b);h.onerror=function(a){a.preventDefault(),e(a)},h.onabort=function(a){return e(a)};var j=h.objectStore(b),k=void 0;try{k=j.get(c)}catch(l){e(l)}k.onsuccess=function(a){return d(a.target.result)}})},this.count=function(b,c){return new Promise(function(d,e){if(g)return void e(new Error(\"Database has been closed\"));try{c=i(c)}catch(f){return void e(f)}var h=a.transaction(b);h.onerror=function(a){a.preventDefault(),e(a)},h.onabort=function(a){return e(a)};var j=h.objectStore(b),k=void 0;try{k=null==c?j.count():j.count(c)}catch(l){e(l)}k.onsuccess=function(a){return d(a.target.result)}})},this.addEventListener=function(b,c){if(!p.includes(b))throw new Error(\"Unrecognized event type \"+b);return\"error\"===b?void a.addEventListener(b,function(a){a.preventDefault(),c(a)}):void a.addEventListener(b,c)},this.removeEventListener=function(b,c){if(!p.includes(b))throw new Error(\"Unrecognized event type \"+b);a.removeEventListener(b,c)},p.forEach(function(a){this[a]=function(b){return this.addEventListener(a,b),this}},this),!e){var h=void 0;return[].some.call(a.objectStoreNames,function(a){if(f[a])return h=new Error('The store name, \"'+a+'\", which you have attempted to load, conflicts with db.js method names.\"'),f.close(),!0;f[a]={};var b=Object.keys(f);b.filter(function(a){return![].concat(p,[\"close\",\"addEventListener\",\"removeEventListener\"]).includes(a)}).map(function(b){return f[a][b]=function(){for(var c=arguments.length,d=Array(c),e=0;c>e;e++)d[e]=arguments[e];return f[b].apply(f,[a].concat(d))}})}),h}},s=function(a,b,c,d,e,f){if(c&&0!==c.length){for(var h=0;h {\n return getToggleSettingValue('itemNotifier');\n};\nconst updateToken = () => {\n return new Promise(async (resolve, reject) => {\n try {\n const enabled = await isEnabled();\n if (!enabled) {\n // Do nothing if the notifier is not enabled.\n resolve();\n return;\n }\n const authenticatedUser = await getAuthenticatedUser();\n // @ts-ignore:next-line: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65809\n chrome.instanceID.getToken({ authorizedEntity: '303497097698', scope: 'FCM' }, (token) => {\n fetch('https://api.roblox.plus/v2/itemnotifier/registertoken', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: `robloxUserId=${authenticatedUser?.id}&token=${encodeURIComponent(token)}`,\n })\n .then((response) => {\n if (response.ok) {\n resolve();\n }\n else {\n reject();\n }\n })\n .catch(reject);\n });\n }\n catch (err) {\n reject(err);\n }\n });\n};\nconst shouldShowNotification = async (creatorName) => {\n // This logic is no longer valid, but still in use. It doesn't support group creators, it assumes all creators are users that can be followed.\n // As a result: No notifications for group-created items will be shown.\n if (!creatorName) {\n // If there's no creator on the notification, it is assumed to be created by the Roblox account.\n // And of course everyone wants these notifications.. right?\n return true;\n }\n const authenticatedUser = await getAuthenticatedUser();\n if (!authenticatedUser) {\n // Not logged in, no notification.\n return false;\n }\n if (authenticatedUser.name === creatorName) {\n // Of course you always want to see your own notifications.\n return true;\n }\n const creator = await getUserByName(creatorName);\n if (!creator) {\n // Couldn't determine who the user is, so no notification will be visible. Cool.\n return false;\n }\n // And the final kicker... you can only see notifications if you follow the creator.\n const isFollowing = await isAuthenticatedUserFollowing(creator.id);\n return isFollowing;\n};\nconst processNotification = async (notification) => {\n const showNotification = await shouldShowNotification(notification.items?.Creator);\n if (!showNotification) {\n console.log('Skipping notification, likely because the authenticated user does not follow the creator', notification);\n return;\n }\n const requireProperties = ['icon', 'url', 'title', 'message'];\n for (let i = 0; i < requireProperties.length; i++) {\n if (!notification[requireProperties[i]]) {\n console.warn(`Skipping notification because there is no ${requireProperties[i]}`, notification);\n return;\n }\n }\n //console.log('Building notification', notification);\n const iconUrl = await fetchDataUri(new URL(notification.icon));\n const notificationOptions = {\n type: 'basic',\n iconUrl,\n title: notification.title,\n message: notification.message,\n };\n if (notification.items && Object.keys(notification.items).length > 0) {\n notificationOptions.type = 'list';\n notificationOptions.items = [];\n notificationOptions.contextMessage = notification.message;\n for (let title in notification.items) {\n notificationOptions.items.push({\n title,\n message: notification.items[title],\n });\n }\n }\n console.log('Displaying notification', notificationOptions, notification);\n chrome.notifications.create(`${notificationIdPrefix}${notification.url}`, notificationOptions, () => { });\n};\nconst processMessage = async (message) => {\n try {\n const enabled = await isEnabled();\n if (!enabled) {\n return;\n }\n console.log('Processing gcm message', message);\n switch (message.from) {\n case '/topics/catalog-notifier':\n case '/topics/catalog-notifier-premium':\n if (!message.data?.notification) {\n console.warn('Failed to parse gcm message notification', message);\n return;\n }\n await processNotification(JSON.parse(message.data.notification));\n return;\n default:\n console.warn('Unknown gcm message sender', message);\n return;\n }\n }\n catch (err) {\n console.error('Failed to process gcm message', err, message);\n }\n};\n// @ts-ignore:next-line: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65809\nchrome.instanceID.onTokenRefresh.addListener(updateToken);\nchrome.gcm.onMessage.addListener(processMessage);\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n const url = notificationId.substring(notificationIdPrefix.length);\n if (!url.startsWith('https://www.roblox.com/')) {\n console.warn('Skipped opening URL for notification because it was not for roblox.com', notificationId);\n return;\n }\n chrome.tabs.create({\n url,\n active: true,\n });\n});\n/*\n// Exists for debugging\ndeclare global {\n var processMessage: any;\n}\n\nwindow.processMessage = processMessage;\n//*/\nexport default async (nextTokenUpdate) => {\n const enabled = await isEnabled();\n if (!enabled) {\n return 0;\n }\n // Check to see if it's time to refresh the token\n const now = +new Date();\n if (nextTokenUpdate && nextTokenUpdate > now) {\n return nextTokenUpdate;\n }\n // Send the token to the server\n await updateToken();\n // Update the token again later\n return now + tokenRefreshInterval;\n};\n","import { PresenceType, getUserProfileLink } from 'roblox';\nimport { isAuthenticatedUserFollowing } from '../../../services/followings';\nimport { getUserFriends } from '../../../services/friends';\nimport { followUser } from '../../../services/game-launch';\nimport { getTranslationResource } from '../../../services/localization';\nimport { getUserPresence } from '../../../services/presence';\nimport { getSettingValue } from '../../../services/settings';\nimport { getAvatarHeadshotThumbnail } from '../../../services/thumbnails';\nimport { getAuthenticatedUser } from '../../../services/users';\nimport fetchDataUri from '../../../utils/fetchDataUri';\n// The prefix for the ID of the notification to display.\nconst notificationIdPrefix = 'friend-notifier-';\n// A method to check if two presences match.\nconst presenceMatches = (a, b) => {\n if (a.type !== b.type) {\n // Not the same presence type, definitely not a match.\n return false;\n }\n if (a.location?.universeId !== b.location?.universeId) {\n // Not the same experience, definitely not a match.\n return false;\n }\n // The type, and location are the same. Must be the same presence.\n return true;\n};\nconst isEnabled = async () => {\n const setting = await getSettingValue('friendNotifier');\n return setting?.on === true;\n};\nconst isPresenceTypeEnabled = async (presenceType) => {\n const setting = await getSettingValue('friendNotifier');\n switch (presenceType) {\n case PresenceType.Online:\n return setting?.online || false;\n case PresenceType.Offline:\n return setting?.offline || false;\n case PresenceType.Experience:\n // If the setting is somehow null, assume we want to know about this one by default.\n if (setting?.game === false) {\n return false;\n }\n return true;\n case PresenceType.Studio:\n default:\n // We don't care about these presence types.\n return false;\n }\n};\n// Gets the icon URL to display on the notification.\nconst getNotificationIconUrl = async (userId) => {\n const thumbnail = await getAvatarHeadshotThumbnail(userId);\n if (!thumbnail.imageUrl) {\n return '';\n }\n try {\n return await fetchDataUri(new URL(thumbnail.imageUrl));\n }\n catch (err) {\n console.error('Failed to fetch icon URL from thumbnail', userId, thumbnail, err);\n return '';\n }\n};\n// Fetches the title for the notification to display to the user, based on current and previous known presence.\nconst getNotificationTitle = (user, presence, previousState) => {\n switch (presence.type) {\n case PresenceType.Offline:\n return `${user.displayName} went offline`;\n case PresenceType.Online:\n if (previousState.type !== PresenceType.Offline) {\n // If they were already online, don't notify them of this again.\n return '';\n }\n return `${user.displayName} is now online`;\n case PresenceType.Experience:\n if (!presence.location?.name) {\n // They joined an experience, but we don't know what they're playing.\n // Don't tell the human what we don't know.\n return '';\n }\n return `${user.displayName} is now playing`;\n case PresenceType.Studio:\n if (!presence.location?.name) {\n // They launched Roblox studio, but we don't know what they're creating.\n // Don't tell the human what we don't know.\n return '';\n }\n if (previousState.type !== PresenceType.Online) {\n // If they went from in-experience -> in-studio, it's possible they just had Roblox studio open\n // while playing a game, and then closed it.\n // Occassionally I have also observed offline <-> Studio swapping back and forth..\n // This creates noise, and we don't like noise.\n return '';\n }\n return `${user.displayName} is now creating`;\n }\n};\n// Gets the buttons that should be displayed on a notification, based on the presence.\nconst getNotificationButtons = async (presence) => {\n if (presence.type === PresenceType.Experience && presence.location?.placeId) {\n const joinText = await getTranslationResource('Feature.PeopleList', 'Action.Join');\n return [\n {\n title: joinText,\n },\n ];\n }\n return [];\n};\n// Handle what happens when a notification is clicked.\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n chrome.tabs.create({\n url: getUserProfileLink(Number(notificationId.substring(notificationIdPrefix.length))).href,\n active: true,\n });\n});\nchrome.notifications.onButtonClicked.addListener(async (notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n const userId = Number(notificationId.substring(notificationIdPrefix.length));\n try {\n await followUser(userId);\n }\n catch (err) {\n console.error('Failed to launch the experience', err);\n }\n});\n// Processes the presences, and send the notifications, when appropriate.\nexport default async (previousStates) => {\n // Check if the notifier is enabled.\n const enabled = await isEnabled();\n if (!enabled) {\n // The feature is not enabled, clear the state, and do nothing.\n return null;\n }\n // Check who is logged in right now.\n const authenticatedUser = await getAuthenticatedUser();\n if (!authenticatedUser) {\n // User is not logged in, no state to return.\n return null;\n }\n // Fetch the friends\n const friends = await getUserFriends(authenticatedUser.id);\n // Check the presence for each of the friends\n const currentState = {};\n await Promise.all(friends.map(async (friend) => {\n const presence = (currentState[friend.id] = await getUserPresence(friend.id));\n const previousState = previousStates && previousStates[friend.id];\n if (previousState && !presenceMatches(previousState, presence)) {\n // The presence for this friend changed, do something!\n const notificationId = notificationIdPrefix + friend.id;\n const buttons = await getNotificationButtons(presence);\n const title = getNotificationTitle(friend, presence, previousState);\n if (!title) {\n // We don't have a title for the notification, so don't show one.\n chrome.notifications.clear(notificationId);\n return;\n }\n const isEnabled = await isPresenceTypeEnabled(presence.type);\n if (!isEnabled) {\n // The authenticated user does not want to know about these types of presence changes.\n chrome.notifications.clear(notificationId);\n return;\n }\n const isFollowing = await isAuthenticatedUserFollowing(friend.id);\n if (!isFollowing) {\n // We're not following this friend, don't show notifications about them.\n chrome.notifications.clear(notificationId);\n return;\n }\n const iconUrl = await getNotificationIconUrl(friend.id);\n if (!iconUrl) {\n // We don't have an icon we can use, so we can't display a notification.\n chrome.notifications.clear(notificationId);\n return;\n }\n chrome.notifications.create(notificationId, {\n type: 'basic',\n iconUrl,\n title,\n message: presence.location?.name ?? '',\n contextMessage: 'Roblox+ Friend Notifier',\n isClickable: true,\n buttons,\n });\n }\n }));\n return currentState;\n};\n","import { ThumbnailState, getGroupLink } from 'roblox';\nimport { getGroupShout, getUserGroups } from '../../../services/groups';\nimport { getSettingValue, getToggleSettingValue, } from '../../../services/settings';\nimport { getGroupIcon } from '../../../services/thumbnails';\nimport { getAuthenticatedUser } from '../../../services/users';\nimport fetchDataUri from '../../../utils/fetchDataUri';\n// The prefix for the ID of the notification to display.\nconst notificationIdPrefix = 'group-shout-notifier-';\n// Returns all the groups that we want to load the group shouts for.\nconst getGroups = async () => {\n const groupMap = [];\n const enabled = await getToggleSettingValue('groupShoutNotifier');\n if (!enabled) {\n // Not enabled, skip.\n return groupMap;\n }\n const authenticatedUser = await getAuthenticatedUser();\n if (!authenticatedUser) {\n // Not logged in, no notifier.\n return groupMap;\n }\n const mode = await getSettingValue('groupShoutNotifier_mode');\n if (mode === 'whitelist') {\n // Only specific groups should be notified on.\n const list = await getSettingValue('groupShoutNotifierList');\n if (typeof list !== 'object') {\n return groupMap;\n }\n for (let rawId in list) {\n const id = Number(rawId);\n if (id && typeof list[rawId] === 'string') {\n groupMap.push({\n id,\n name: list[rawId],\n });\n }\n }\n }\n else {\n // All groups the user is in should be notified on.\n const groups = await getUserGroups(authenticatedUser.id);\n groups.forEach((group) => {\n groupMap.push({\n id: group.id,\n name: group.name,\n });\n });\n }\n return groupMap;\n};\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n chrome.tabs.create({\n url: getGroupLink(Number(notificationId.substring(notificationIdPrefix.length)), 'redirect').href,\n active: true,\n });\n});\nexport default async (previousState) => {\n const newState = {};\n const groups = await getGroups();\n const promises = groups.map(async (group) => {\n try {\n const groupShout = await getGroupShout(group.id);\n newState[group.id] = groupShout;\n if (previousState &&\n previousState.hasOwnProperty(group.id) &&\n previousState[group.id] !== groupShout &&\n groupShout) {\n // Send notification, the shout has changed.\n const groupIcon = await getGroupIcon(group.id);\n if (groupIcon.state !== ThumbnailState.Completed) {\n return;\n }\n const notificationIcon = await fetchDataUri(new URL(groupIcon.imageUrl));\n chrome.notifications.create(`${notificationIdPrefix}${group.id}`, {\n type: 'basic',\n title: group.name,\n message: groupShout,\n contextMessage: 'Roblox+ Group Shout Notifier',\n iconUrl: notificationIcon,\n });\n }\n }\n catch (err) {\n console.error('Failed to check group for group shout notifier', err, group);\n if (previousState && previousState.hasOwnProperty(group.id)) {\n newState[group.id] = previousState[group.id];\n }\n }\n });\n await Promise.all(promises);\n return newState;\n};\n","import CatalogNotifier from './catalog';\nimport FriendPresenceNotifier from './friend-presence';\nimport GroupShoutNotifier from './group-shout';\nimport './startup';\nimport TradeNotifier from './trades';\n// Registry of all the notifiers\nconst notifiers = {};\nnotifiers['notifiers/catalog'] = CatalogNotifier;\nnotifiers['notifiers/group-shouts'] = GroupShoutNotifier;\nnotifiers['notifiers/friend-presence'] = FriendPresenceNotifier;\nnotifiers['notifiers/trade'] = TradeNotifier;\n// TODO: Update to use chrome.storage.session for manifest V3\nconst notifierStates = {};\n// Execute a notifier by name.\nconst executeNotifier = async (name) => {\n const notifier = notifiers[name];\n if (!notifier) {\n return;\n }\n try {\n // Fetch the state from the last time the notifier ran.\n // ...\n // Run the notifier.\n const newState = await notifier(notifierStates[name]);\n // Save the state for the next time the notifier runs.\n if (newState) {\n notifierStates[name] = newState;\n }\n else {\n delete notifierStates[name];\n }\n }\n catch (err) {\n console.error(name, 'failed to run', err);\n }\n};\n// Listener for the chrome.alarms API, to process the notification checks\nchrome.alarms.onAlarm.addListener(async ({ name }) => {\n await executeNotifier(name);\n});\nfor (let name in notifiers) {\n chrome.alarms.create(name, {\n periodInMinutes: 1,\n });\n}\nglobalThis.notifiers = notifiers;\nglobalThis.notifierStates = notifierStates;\nglobalThis.executeNotifier = executeNotifier;\nexport { executeNotifier };\nexport default notifiers;\n","import { manifest } from '@tix-factory/extension-utils';\nimport { getSettingValue } from '../../../services/settings';\nimport { getAuthenticatedUser } from '../../../services/users';\nconst notificationId = 'startup-notification';\nconst displayStartupNotification = async () => {\n if (!manifest.icons) {\n console.warn('Missing manifest icons');\n return;\n }\n const authenticatedUser = await getAuthenticatedUser();\n chrome.notifications.create(notificationId, {\n type: 'basic',\n iconUrl: chrome.extension.getURL(manifest.icons['128']),\n title: 'Roblox+ Started',\n message: authenticatedUser\n ? `Hello, ${authenticatedUser.displayName}`\n : 'You are currently signed out',\n contextMessage: `${manifest.name} ${manifest.version}, by WebGL3D`,\n });\n};\ngetSettingValue('startupNotification')\n .then(async (setting) => {\n if (typeof setting !== 'object') {\n setting = {\n on: !chrome.extension.inIncognitoContext,\n visit: false,\n };\n }\n if (!setting.on) {\n return;\n }\n if (setting.visit) {\n // Only show the startup notification after Roblox has been visited.\n const updatedListener = (_tabId, _changes, tab) => {\n return takeAction(tab);\n };\n const takeAction = async (tab) => {\n if (!tab.url) {\n return;\n }\n try {\n const tabURL = new URL(tab.url);\n if (!tabURL.hostname.endsWith('.roblox.com')) {\n return;\n }\n chrome.tabs.onCreated.removeListener(takeAction);\n chrome.tabs.onUpdated.removeListener(updatedListener);\n await displayStartupNotification();\n }\n catch {\n // don't care for now\n }\n };\n chrome.tabs.onUpdated.addListener(updatedListener);\n chrome.tabs.onCreated.addListener(takeAction);\n }\n else {\n await displayStartupNotification();\n }\n})\n .catch((err) => {\n console.warn('Failed to render startup notification', err);\n});\nchrome.notifications.onClicked.addListener((id) => {\n if (id !== notificationId) {\n return;\n }\n chrome.tabs.create({\n url: `https://roblox.plus/about/changes?version=${manifest.version}`,\n active: true,\n });\n});\n","import { TradeStatusType } from 'roblox';\nimport { getToggleSettingValue } from '../../../services/settings';\nimport { getAvatarHeadshotThumbnail } from '../../../services/thumbnails';\nimport fetchDataUri from '../../../utils/fetchDataUri';\n// The prefix for the ID of the notification to display.\nconst notificationIdPrefix = 'trade-notifier-';\n// Gets the trade status types that should be notified on.\nconst getEnabledTradeStatusTypes = async () => {\n const enabled = await getToggleSettingValue('tradeNotifier');\n if (enabled) {\n return [\n TradeStatusType.Inbound,\n TradeStatusType.Outbound,\n TradeStatusType.Completed,\n TradeStatusType.Inactive,\n ];\n }\n return [];\n /*\n const values = await getSettingValue('notifiers/trade/status-types');\n if (!Array.isArray(values)) {\n return [];\n }\n \n return values.filter((v) => Object.keys(v).includes(v));\n */\n};\n// Load the trade IDs for a status type.\nconst getTrades = async (tradeStatusType) => {\n const response = await fetch(`https://trades.roblox.com/v1/trades/${tradeStatusType}?limit=10&sortOrder=Desc`);\n const result = await response.json();\n return result.data.map((t) => t.id);\n};\n// Gets an individual trade by its ID.\nconst getTrade = async (id, tradeStatusType) => {\n const response = await fetch(`https://trades.roblox.com/v1/trades/${id}`);\n const result = await response.json();\n const tradePartner = result.user;\n const tradePartnerOffer = result.offers.find((o) => o.user.id === tradePartner.id);\n const authenticatedUserOffer = result.offers.find((o) => o.user.id !== tradePartner.id);\n return {\n id,\n tradePartner,\n authenticatedUserOffer: {\n robux: authenticatedUserOffer.robux,\n assets: authenticatedUserOffer.userAssets.map((a) => {\n return {\n id: a.assetId,\n userAssetId: a.id,\n name: a.name,\n recentAveragePrice: a.recentAveragePrice,\n };\n }),\n },\n partnerOffer: {\n robux: tradePartnerOffer.robux,\n assets: tradePartnerOffer.userAssets.map((a) => {\n return {\n id: a.assetId,\n userAssetId: a.id,\n name: a.name,\n recentAveragePrice: a.recentAveragePrice,\n };\n }),\n },\n status: result.status,\n type: tradeStatusType,\n };\n};\n// Gets the icon URL to display on the notification.\nconst getNotificationIconUrl = async (trade) => {\n const thumbnail = await getAvatarHeadshotThumbnail(trade.tradePartner.id);\n if (!thumbnail.imageUrl) {\n return '';\n }\n try {\n return await fetchDataUri(new URL(thumbnail.imageUrl));\n }\n catch (err) {\n console.error('Failed to fetch icon URL from thumbnail', trade, thumbnail, err);\n return '';\n }\n};\n// Fetches the title for the notification to display to the user, based on current and previous known presence.\nconst getNotificationTitle = (trade) => {\n switch (trade.type) {\n case TradeStatusType.Inbound:\n return 'Trade inbound';\n case TradeStatusType.Outbound:\n return 'Trade sent';\n case TradeStatusType.Completed:\n return 'Trade completed';\n default:\n return 'Trade ' + trade.status.toLowerCase();\n }\n};\nconst getOfferValue = (tradeOffer) => {\n let value = 0;\n tradeOffer.assets.forEach((asset) => {\n value += asset.recentAveragePrice;\n });\n return (`${value.toLocaleString()}` +\n (tradeOffer.robux > 0 ? ` + R\\$${tradeOffer.robux.toLocaleString()}` : ''));\n};\n// Handle what happens when a notification is clicked.\nchrome.notifications.onClicked.addListener((notificationId) => {\n if (!notificationId.startsWith(notificationIdPrefix)) {\n return;\n }\n // If only we could link to specific trades..\n const tradeId = Number(notificationId.substring(notificationIdPrefix.length));\n chrome.tabs.create({\n url: 'https://www.roblox.com/trades',\n active: true,\n });\n});\n// Processes the presences, and send the notifications, when appropriate.\nexport default async (previousState) => {\n const previousEnabledStatusTypes = previousState?.enabledStatusTypes || [];\n const previousTradeStatusTypes = previousState?.tradeStatusMap || {};\n const newState = {\n // Preserve the trade statuses for the future\n // This is definitely how memory leaks come to be, but... how many trades could someone possibly be going through.\n tradeStatusMap: Object.assign({}, previousTradeStatusTypes),\n enabledStatusTypes: await getEnabledTradeStatusTypes(),\n };\n await Promise.all(newState.enabledStatusTypes.map(async (tradeStatusType) => {\n try {\n const trades = await getTrades(tradeStatusType);\n const tradePromises = [];\n // No matter what: Keep track of this trade we have seen, for future reference.\n trades.forEach((tradeId) => {\n newState.tradeStatusMap[tradeId] = tradeStatusType;\n });\n // now check each of them, to see if we want to send a notification.\n for (let i = 0; i < trades.length; i++) {\n const tradeId = trades[i];\n // Previously, the notifier type wasn't enabled.\n // Do nothing with the information we now know.\n if (!previousEnabledStatusTypes.includes(tradeStatusType)) {\n continue;\n }\n // We have seen this trade before, in this same status type\n // Because the trades are ordered in descending order, we know there are\n // no other changes further down in this list. We can break.\n if (previousTradeStatusTypes[tradeId] === tradeStatusType) {\n // And in fact, we have to break.\n // Because if we don't, \"new\" trades could come in at the bottom of the list.\n break;\n }\n // In all cases, we clear the current notification, to make room for a potential new one.\n const notificationId = notificationIdPrefix + tradeId;\n chrome.notifications.clear(notificationId);\n tradePromises.push(getTrade(tradeId, tradeStatusType)\n .then(async (trade) => {\n try {\n const iconUrl = await getNotificationIconUrl(trade);\n if (!iconUrl) {\n // No icon.. no new notification.\n return;\n }\n const title = getNotificationTitle(trade);\n chrome.notifications.create(notificationId, {\n type: 'list',\n iconUrl,\n title,\n message: '@' + trade.tradePartner.name,\n items: [\n {\n title: 'Partner',\n message: trade.tradePartner.displayName,\n },\n {\n title: 'Your Value',\n message: getOfferValue(trade.authenticatedUserOffer),\n },\n {\n title: 'Partner Value',\n message: getOfferValue(trade.partnerOffer),\n },\n ],\n contextMessage: 'Roblox+ Trade Notifier',\n isClickable: true,\n });\n }\n catch (e) {\n console.error('Failed to send notification about trade', trade);\n }\n })\n .catch((err) => {\n console.error('Failed to load trade information', tradeId, tradeStatusType, err);\n }));\n }\n await Promise.all(tradePromises);\n }\n catch (e) {\n console.error(`Failed to check ${tradeStatusType} trade notifier`, e);\n }\n }));\n return newState;\n};\n","import { Batch } from '@tix-factory/batch';\nimport { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { manifest } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'assetsService.getAssetContentsUrl';\nclass AssetContentsBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const requestHeaders = new Headers();\n requestHeaders.append('Roblox-Place-Id', '258257446');\n requestHeaders.append('Roblox-Browser-Asset-Request', manifest.name);\n const response = await xsrfFetch(new URL(`https://assetdelivery.roblox.com/v2/assets/batch`), {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(items.map((batchItem) => {\n return {\n assetId: batchItem.value,\n requestId: batchItem.key,\n };\n })),\n });\n if (!response.ok) {\n throw new Error('Failed to load asset contents URL');\n }\n const result = await response.json();\n items.forEach((item) => {\n const asset = result.find((a) => a.requestId === item.key);\n const location = asset?.locations[0];\n if (location?.location) {\n item.resolve(location.location);\n }\n else {\n item.resolve('');\n }\n });\n }\n getKey(item) {\n return item.toString();\n }\n}\nconst assetContentsProcessor = new AssetContentsBatchProcessor();\nconst assetContentsCache = new ExpirableDictionary(messageDestination, 10 * 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getAssetContentsUrl = async (assetId) => {\n const url = await sendMessage(messageDestination, {\n assetId,\n });\n return url ? new URL(url) : undefined;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return assetContentsCache.getOrAdd(assetContentsProcessor.getKey(message.assetId), () => {\n // Queue up the fetch request, when not in the cache\n return assetContentsProcessor.enqueue(message.assetId);\n });\n});\nexport default getAssetContentsUrl;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport getAssetContentsUrl from './get-asset-contents-url';\nconst messageDestination = 'assetsService.getAssetDependencies';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst contentRegexes = [\n /\"TextureI?d?\".*=\\s*(\\d+)/gi,\n /\"TextureI?d?\".*rbxassetid:\\/\\/(\\d+)/gi,\n /\"MeshId\".*=\\s*(\\d+)/gi,\n /MeshId.*rbxassetid:\\/\\/(\\d+)/gi,\n /asset\\/?\\?\\s*id\\s*=\\s*(\\d+)/gi,\n /rbxassetid:\\/\\/(\\d+)/gi,\n /:LoadAsset\\((\\d+)\\)/gi,\n /require\\((\\d+)\\)/gi,\n];\nconst getAssetDependencies = async (assetId) => {\n return sendMessage(messageDestination, { assetId });\n};\nconst loadAssetDependencies = async (assetId) => {\n const assetIds = [];\n const assetContentsUrl = await getAssetContentsUrl(assetId);\n if (!assetContentsUrl) {\n return [];\n }\n const assetContentsResponse = await fetch(assetContentsUrl);\n const assetContents = await assetContentsResponse.text();\n contentRegexes.forEach((regex) => {\n let match = assetContents.match(regex) || [];\n match.forEach((m) => {\n let id = Number((m.match(/(\\d+)/) || [])[1]);\n if (id && !isNaN(id) && !assetIds.includes(id)) {\n assetIds.push(id);\n }\n });\n });\n return assetIds;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.assetId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAssetDependencies(message.assetId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getAssetDependencies;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'assetsService.getAssetDetails';\nconst cache = new ExpirableDictionary(messageDestination, 5 * 60 * 1000);\nconst getAssetDetails = async (assetId) => {\n return sendMessage(messageDestination, { assetId });\n};\nconst loadAssetDetails = async (assetId) => {\n const response = await fetch(`https://economy.roblox.com/v2/assets/${assetId}/details`);\n if (!response.ok) {\n throw new Error('Failed to load asset product info');\n }\n const result = await response.json();\n return {\n id: assetId,\n name: result.Name,\n type: result.AssetTypeId,\n sales: result.Sales,\n };\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.assetId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAssetDetails(message.assetId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getAssetDetails;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'assetsService.getAssetSalesCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst getAssetSalesCount = async (assetId) => {\n return sendMessage(messageDestination, { assetId });\n};\nconst loadAssetSalesCount = async (assetId) => {\n const response = await fetch(`https://economy.roblox.com/v2/assets/${assetId}/details`);\n if (!response.ok) {\n throw new Error('Failed to load asset product info');\n }\n const result = await response.json();\n return result.Sales || NaN;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.assetId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAssetSalesCount(message.assetId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getAssetSalesCount;\n","import getAssetContentsUrl from './get-asset-contents-url';\nimport getAssetSalesCount from './get-asset-sales-count';\nimport getAssetDependencies from './get-asset-dependencies';\nimport getAssetDetails from './get-asset-details';\nglobalThis.assetsService = {\n getAssetContentsUrl,\n getAssetSalesCount,\n getAssetDependencies,\n getAssetDetails,\n};\nexport { getAssetContentsUrl, getAssetSalesCount, getAssetDependencies, getAssetDetails, };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nconst messageDestination = 'avatarService.getAvatarRules';\nlet avatarAssetRules = [];\nconst getAvatarAssetRules = async () => {\n return sendMessage(messageDestination, {});\n};\nconst loadAvatarAssetRules = async () => {\n const response = await fetch(`https://avatar.roblox.com/v1/avatar-rules`);\n if (!response.ok) {\n throw new Error(`Failed to load avatar rules (${response.status})`);\n }\n const result = await response.json();\n return result.wearableAssetTypes.map((rule) => {\n return {\n maxNumber: rule.maxNumber,\n assetType: rule.id,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, async () => {\n if (avatarAssetRules.length > 0) {\n return avatarAssetRules;\n }\n avatarAssetRules = await loadAvatarAssetRules();\n return avatarAssetRules;\n}, {\n levelOfParallelism: 1,\n});\nexport default getAvatarAssetRules;\n","import { AssetType } from 'roblox';\nimport xsrfFetch from '../../utils/xsrfFetch';\nimport getAvatarAssetRules from './get-avatar-asset-rules';\nconst getAvatarAssets = async (userId) => {\n const response = await fetch(`https://avatar.roblox.com/v1/users/${userId}/avatar`);\n if (!response.ok) {\n throw new Error(`Failed to load avatar (${response.status})`);\n }\n const result = await response.json();\n const assets = result.assets.map((asset) => {\n return {\n id: asset.id,\n name: asset.name,\n assetType: asset.assetType.id,\n };\n });\n result.emotes.forEach((emote) => {\n assets.push({\n id: emote.assetId,\n name: emote.assetName,\n assetType: AssetType.Emote,\n });\n });\n return assets;\n};\nconst wearItem = async (assetId, authenticatedUserId) => {\n // Use set-wearing-assets instead of wear because it will allow more than the limit\n const currentAssets = await getAvatarAssets(authenticatedUserId);\n const response = await xsrfFetch(new URL(`https://avatar.roblox.com/v1/avatar/set-wearing-assets`), {\n method: 'POST',\n body: JSON.stringify({\n assetIds: [assetId].concat(currentAssets\n .filter((a) => a.assetType !== AssetType.Emote)\n .map((a) => a.id)),\n }),\n });\n if (!response.ok) {\n throw new Error(`Failed to wear asset (${assetId})`);\n }\n const result = await response.json();\n if (result.invalidAssetIds.length > 0) {\n throw new Error(`Failed to wear assets (${result.invalidAssetIds.join(', ')})`);\n }\n};\nconst removeItem = async (assetId) => {\n const response = await xsrfFetch(new URL(`https://avatar.roblox.com/v1/avatar/assets/${assetId}/remove`), {\n method: 'POST',\n });\n if (!response.ok) {\n throw new Error(`Failed to remove asset (${assetId})`);\n }\n};\nglobalThis.avatarService = { getAvatarAssetRules, getAvatarAssets, wearItem, removeItem };\nexport { getAvatarAssetRules, getAvatarAssets, wearItem, removeItem };\n","import { Batch } from '@tix-factory/batch';\nclass BadgeAwardBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await fetch(`https://badges.roblox.com/v1/users/${items[0].value.userId}/badges/awarded-dates?badgeIds=${items\n .map((i) => i.value.badgeId)\n .join(',')}`);\n if (!response.ok) {\n throw new Error('Failed to load badge award statuses');\n }\n const result = await response.json();\n items.forEach((item) => {\n const badgeAward = result.data.find((b) => b.badgeId === item.value.badgeId);\n if (badgeAward?.awardedDate) {\n item.resolve(new Date(badgeAward.awardedDate));\n }\n else {\n item.resolve(undefined);\n }\n });\n }\n getBatch() {\n const now = performance.now();\n const batch = [];\n for (let i = 0; i < this.queueArray.length; i++) {\n const batchItem = this.queueArray[i];\n if (batchItem.retryAfter > now) {\n // retryAfter is set at Infinity while the item is being processed\n // so we should always check it, even if we're not retrying items\n continue;\n }\n if (batch.length < 1 ||\n batch[0].value.userId === batchItem.value.userId) {\n // We group all the requests for badge award dates together by user ID.\n batch.push(batchItem);\n }\n if (batch.length >= this.config.maxSize) {\n // We have all the items we need, break.\n break;\n }\n }\n return batch;\n }\n getKey(item) {\n return `${item.userId}:${item.badgeId}`;\n }\n}\nexport default BadgeAwardBatchProcessor;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport BadgeAwardBatchProcessor from './batchProcessor';\nconst messageDestination = 'badgesService.getBadgeAwardDate';\nconst badgeAwardProcessor = new BadgeAwardBatchProcessor();\nconst badgeAwardCache = new ExpirableDictionary('badgesService', 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getBadgeAwardDate = async (userId, badgeId) => {\n const date = await sendMessage(messageDestination, {\n userId,\n badgeId,\n });\n return date ? new Date(date) : undefined;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return badgeAwardCache.getOrAdd(badgeAwardProcessor.getKey(message), async () => {\n // Queue up the fetch request, when not in the cache\n const date = await badgeAwardProcessor.enqueue(message);\n return date?.getTime();\n });\n});\nglobalThis.badgesService = { getBadgeAwardDate };\nexport { getBadgeAwardDate };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport { recordUserRobux } from './history';\nconst messageDestination = 'currencyService.getRobuxBalance';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the Robux balance of the currently authenticated user.\nconst getRobuxBalance = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the Robux balance of the currently authenticated user.\nconst loadRobuxBalance = async (userId) => {\n const response = await fetch(`https://economy.roblox.com/v1/users/${userId}/currency`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw 'Failed to load Robux balance';\n }\n const result = await response.json();\n try {\n await recordUserRobux(userId, result.robux);\n }\n catch (err) {\n console.warn('Failed to record Robux history');\n }\n return result.robux;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadRobuxBalance(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getRobuxBalance;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nimport { open } from 'db.js';\nimport { getToggleSettingValue } from '../settings';\nconst messageDestination = 'currencyService.history.';\nif (isBackgroundPage) {\n open({\n server: 'currencyBalances',\n version: 1,\n schema: {\n robuxHistory: {\n key: {\n keyPath: ['currencyHolderType', 'currencyHolderId', 'robuxDate'],\n },\n indexes: {\n currencyHolderType: {},\n currencyHolderId: {},\n robuxDate: {},\n },\n },\n },\n })\n .then((database) => {\n console.log('Database connection (for robuxHistory) opened.');\n window.robuxHistoryDatabase = database;\n // Ensure the amount of stored data doesn't get too out of hand.\n // Only store one year of data.\n setInterval(async () => {\n try {\n const now = +new Date();\n const purgeDate = new Date(now - 32 * 12 * 24 * 60 * 60 * 1000);\n const robuxHistory = await database.robuxHistory\n .query('robuxDate')\n .range({ lte: purgeDate.getTime() })\n .execute();\n if (robuxHistory.length <= 0) {\n return;\n }\n await Promise.all(robuxHistory.map((robuxHistoryRecord) => {\n return database.robuxHistory.remove({\n eq: [\n robuxHistoryRecord.currencyHolderType,\n robuxHistoryRecord.currencyHolderId,\n robuxHistoryRecord.robuxDate,\n ],\n });\n }));\n }\n catch (e) {\n console.warn('Failed to purge Robux history database', e);\n }\n }, 60 * 60 * 1000);\n })\n .catch((err) => {\n console.error('Failed to connect to robuxHistory database.', err);\n });\n}\nconst recordUserRobux = async (userId, robux) => {\n const enabled = await getToggleSettingValue('robuxHistoryEnabled');\n if (!enabled) {\n return;\n }\n return sendMessage(messageDestination + 'recordUserRobux', {\n userId,\n robux,\n });\n};\nconst getUserRobuxHistory = async (userId, startDateTime, endDateTime) => {\n const robuxHistory = await sendMessage(messageDestination + 'getUserRobuxHistory', {\n userId,\n startDateTime: startDateTime.getTime(),\n endDateTime: endDateTime.getTime(),\n });\n return robuxHistory.map((h) => {\n return {\n value: h.robux,\n date: new Date(h.robuxDate),\n };\n });\n};\naddListener(messageDestination + 'recordUserRobux', async (message) => {\n const now = +new Date();\n const robuxDateTime = new Date(now - (now % 60000));\n await robuxHistoryDatabase.robuxHistory.update({\n currencyHolderType: 'User',\n currencyHolderId: message.userId,\n robux: message.robux,\n robuxDate: robuxDateTime.getTime(),\n });\n}, {\n levelOfParallelism: 1,\n});\naddListener(messageDestination + 'getUserRobuxHistory', async (message) => {\n const history = await robuxHistoryDatabase.robuxHistory\n .query('robuxDate')\n .range({\n gte: message.startDateTime,\n lte: message.endDateTime,\n })\n .filter((row) => row.currencyHolderType === 'User' &&\n row.currencyHolderId === message.userId)\n .execute();\n return history;\n}, {\n levelOfParallelism: 1,\n});\nexport { recordUserRobux, getUserRobuxHistory };\n","import { default as getRobuxBalance } from './getRobuxBalance';\nimport { getUserRobuxHistory } from './history';\nglobalThis.currencyService = { getRobuxBalance, getUserRobuxHistory };\nexport { getRobuxBalance, getUserRobuxHistory };\n","import { Batch } from '@tix-factory/batch';\nimport xsrfFetch from '../../utils/xsrfFetch';\nclass AuthenticatedUserFollowingProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await xsrfFetch(new URL('https://friends.roblox.com/v1/user/following-exists'), {\n method: 'POST',\n body: JSON.stringify({\n targetUserIds: items.map((i) => i.value),\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to load authenticated user following statuses');\n }\n const result = await response.json();\n items.forEach((item) => {\n const following = result.followings.find((f) => f.userId === item.value);\n item.resolve(following?.isFollowing === true);\n });\n }\n getKey(userId) {\n return `${userId}`;\n }\n}\nexport default AuthenticatedUserFollowingProcessor;\n","import { default as isAuthenticatedUserFollowing } from './isAuthenticatedUserFollowing';\nglobalThis.followingsService = { isAuthenticatedUserFollowing };\nexport { isAuthenticatedUserFollowing };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport AuthenticatedUserFollowingProcessor from './authenticatedUserFollowingProcessor';\nconst messageDestination = 'followingsService.isAuthenticatedUserFollowing';\nconst batchProcessor = new AuthenticatedUserFollowingProcessor();\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\n// Checks if the authenticated user is following another user.\nconst isAuthenticatedUserFollowing = (userId) => {\n return sendMessage(messageDestination, {\n userId,\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n batchProcessor.enqueue(message.userId));\n});\nexport default isAuthenticatedUserFollowing;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'friendsService.getFriendRequestCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the inbound friend request count for the currently authenticated user.\nconst getFriendRequestCount = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the inbound friend request count for the currently authenticated user.\nconst loadFriendRequestCount = async (userId) => {\n // User ID is used as a cache buster.\n const response = await fetch(`https://friends.roblox.com/v1/user/friend-requests/count`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw 'Failed to load friend request count';\n }\n const result = await response.json();\n return result.count;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadFriendRequestCount(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getFriendRequestCount;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'friendsService.getUserFriends';\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\n// Fetches the list of friends for the user.\nconst getUserFriends = (userId) => {\n return sendMessage(messageDestination, {\n userId,\n });\n};\n// Loads the actual friend list for the user.\nconst loadUserFriends = async (userId) => {\n const response = await fetch(`https://friends.roblox.com/v1/users/${userId}/friends`);\n if (!response.ok) {\n throw new Error(`Failed to load friends for user (${userId})`);\n }\n const result = await response.json();\n return result.data.map((r) => {\n return {\n id: r.id,\n name: r.name,\n displayName: r.displayName,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUserFriends(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getUserFriends;\n","import { default as getUserFriends } from './getUserFriends';\nimport { default as getFriendRequestCount } from './getFriendRequestCount';\nglobalThis.friendsService = { getUserFriends, getFriendRequestCount };\nexport { getUserFriends, getFriendRequestCount };\n","import launchProtocolUrl from '../../utils/launchProtocolUrl';\n// Launches into the experience that the specified user is playing.\nconst followUser = async (userId) => {\n await launchProtocolUrl(`roblox://userId=${userId}`);\n};\nglobalThis.gameLaunchService = { followUser };\nexport { followUser };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'gamePassesService.getGamePassSaleCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst getGamePassSaleCount = async (gamePassId) => {\n return sendMessage(messageDestination, { gamePassId });\n};\nconst loadGamePassSales = async (gamePassId) => {\n const response = await fetch(`https://economy.roblox.com/v1/game-pass/${gamePassId}/game-pass-product-info`);\n if (!response.ok) {\n throw new Error('Failed to load game pass product info');\n }\n const result = await response.json();\n return result.Sales || NaN;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.gamePassId}`, () => \n // Queue up the fetch request, when not in the cache\n loadGamePassSales(message.gamePassId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getGamePassSaleCount;\n","import getGamePassSaleCount from './get-game-pass-sale-count';\nglobalThis.gamePassesService = { getGamePassSaleCount };\nexport { getGamePassSaleCount };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getCreatorGroups';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\n// Fetches the groups the user has access privileged roles in.\nconst getCreatorGroups = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the groups the user has access privileged roles in.\nconst loadAuthenticatedUserCreatorGroups = async () => {\n const response = await fetch(`https://develop.roblox.com/v1/user/groups/canmanage`);\n if (response.status === 401) {\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n throw 'Failed to load creation groups for the authenticated user';\n }\n const result = await response.json();\n return result.data.map((g) => {\n return {\n id: g.id,\n name: g.name,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadAuthenticatedUserCreatorGroups());\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getCreatorGroups;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getGroupShout';\nconst cache = new ExpirableDictionary(messageDestination, 90 * 1000);\n// Fetches the group shout.\nconst getGroupShout = (groupId) => {\n return sendMessage(messageDestination, { groupId });\n};\n// Loads the groups the user is a member of.\nconst loadGroupShout = async (groupId) => {\n const response = await fetch(`https://groups.roblox.com/v1/groups/${groupId}`);\n if (!response.ok) {\n throw `Failed to load group shout for group ${groupId}`;\n }\n const result = await response.json();\n return result.shout?.body || '';\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.groupId}`, () => \n // Queue up the fetch request, when not in the cache\n loadGroupShout(message.groupId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getGroupShout;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getUserGroups';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\n// Fetches the groups the user is a member of.\nconst getUserGroups = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the groups the user is a member of.\nconst loadUserGroups = async (userId) => {\n const response = await fetch(`https://groups.roblox.com/v1/users/${userId}/groups/roles`);\n if (!response.ok) {\n throw 'Failed to load groups the user is a member of';\n }\n const result = await response.json();\n return result.data.map((g) => {\n return {\n id: g.group.id,\n name: g.group.name,\n };\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUserGroups(message.userId));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getUserGroups;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'groupsService.getUserPrimaryGroup';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\n// Fetches the groups the user is a member of.\nconst getUserPrimaryGroup = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the groups the user is a member of.\nconst loadUserPrimaryGroup = async (userId) => {\n const response = await fetch(`https://groups.roblox.com/v1/users/${userId}/groups/primary/role`);\n if (!response.ok) {\n throw 'Failed to load primary group for the user';\n }\n const result = await response.json();\n if (!result || !result.group) {\n return null;\n }\n return {\n id: result.group.id,\n name: result.group.name,\n };\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUserPrimaryGroup(message.userId));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getUserPrimaryGroup;\n","import getCreatorGroups from './get-creator-groups';\nimport getGroupShout from './get-group-shout';\nimport getUserGroups from './get-user-groups';\nimport getUserPrimaryGroup from './get-user-primary-group';\nglobalThis.groupsService = { getCreatorGroups, getGroupShout, getUserGroups, getUserPrimaryGroup };\nexport { getCreatorGroups, getGroupShout, getUserGroups, getUserPrimaryGroup };\n","import { getUserById } from '../users';\nconst getAssetOwners = async (assetId, cursor, isAscending) => {\n const response = await fetch(`https://inventory.roblox.com/v2/assets/${assetId}/owners?limit=100&cursor=${cursor}&sortOrder=${isAscending ? 'Asc' : 'Desc'}`, {\n credentials: 'include',\n });\n if (!response.ok) {\n throw new Error(`Failed to load ownership records (${assetId}, ${cursor}, ${isAscending})`);\n }\n const result = await response.json();\n const ownershipRecords = [];\n await Promise.all(result.data.map(async (i) => {\n const record = {\n id: i.id,\n user: null,\n serialNumber: i.serialNumber || NaN,\n created: new Date(i.created),\n updated: new Date(i.updated),\n };\n ownershipRecords.push(record);\n if (i.owner) {\n record.user = await getUserById(i.owner.id);\n }\n }));\n return {\n nextPageCursor: result.nextPageCursor || '',\n data: ownershipRecords,\n };\n};\nexport default getAssetOwners;\n","import xsrfFetch from '../../utils/xsrfFetch';\nimport getLimitedInventory from './limitedInventory';\nimport getAssetOwners from './get-asset-owners';\n// Removes an asset from the authenticated user's inventory.\nconst deleteAsset = async (assetId) => {\n const response = await xsrfFetch(new URL(`https://assetgame.roblox.com/asset/delete-from-inventory`), {\n method: 'POST',\n body: JSON.stringify({\n assetId: assetId,\n }),\n });\n if (!response.ok) {\n throw new Error(`Failed to remove asset (${assetId})`);\n }\n};\nglobalThis.inventoryService = { deleteAsset, getLimitedInventory, getAssetOwners };\nexport { deleteAsset, getLimitedInventory, getAssetOwners };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'inventoryService.getLimitedInventory';\nconst cache = new ExpirableDictionary(messageDestination, 5 * 60 * 1000);\n// Fetches the limited inventory for the specified user.\nconst getLimitedInventory = (userId) => {\n return sendMessage(messageDestination, {\n userId,\n });\n};\n// Actually loads the inventory.\nconst loadLimitedInventory = async (userId) => {\n const foundUserAssetIds = new Set();\n const limitedAssets = [];\n let nextPageCursor = '';\n do {\n const response = await fetch(`https://inventory.roblox.com/v1/users/${userId}/assets/collectibles?limit=100&cursor=${nextPageCursor}`);\n if (response.status === 429) {\n // Throttled. Wait a few seconds, and try again.\n await wait(5000);\n continue;\n }\n else if (response.status === 403) {\n throw new Error('Inventory hidden');\n }\n else if (!response.ok) {\n throw new Error('Inventory failed to load');\n }\n const result = await response.json();\n nextPageCursor = result.nextPageCursor;\n result.data.forEach((item) => {\n const userAssetId = Number(item.userAssetId);\n if (foundUserAssetIds.has(userAssetId)) {\n return;\n }\n foundUserAssetIds.add(userAssetId);\n limitedAssets.push({\n userAssetId,\n id: item.assetId,\n name: item.name,\n recentAveragePrice: item.recentAveragePrice\n ? Number(item.recentAveragePrice)\n : NaN,\n serialNumber: item.serialNumber ? Number(item.serialNumber) : NaN,\n stock: item.assetStock === 0 ? 0 : item.assetStock || undefined,\n });\n });\n } while (nextPageCursor);\n return limitedAssets;\n};\n// Listen for background messages\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadLimitedInventory(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getLimitedInventory;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nconst englishLocale = 'en_us';\nconst messageDestination = 'localizationService.getTranslationResources';\nlet translationResourceCache = [];\nlet localeCache = '';\n// Gets the locale for the authenticated user.\nconst getAuthenticatedUserLocale = async () => {\n if (localeCache) {\n return localeCache;\n }\n try {\n const response = await fetch(`https://locale.roblox.com/v1/locales/user-locale`);\n if (!response.ok) {\n console.warn('Failed to fetch user locale - defaulting to English.', response.status);\n return (localeCache = englishLocale);\n }\n const result = await response.json();\n return (localeCache = result.supportedLocale.locale);\n }\n catch (e) {\n console.warn('Unhandled error loading user locale - defaulting to English.', e);\n return (localeCache = englishLocale);\n }\n};\n// Fetches all the translation resources for the authenticated user.\nconst getTranslationResources = async () => {\n if (translationResourceCache.length > 0) {\n return translationResourceCache;\n }\n return (translationResourceCache = await sendMessage(messageDestination, {}));\n};\n// Fetches an individual translation resource.\nconst getTranslationResource = async (namespace, key) => {\n const translationResources = await getTranslationResources();\n const resource = translationResources.find((r) => r.namespace === namespace && r.key === key);\n if (!resource) {\n console.warn(`No translation resource available.\\n\\tNamespace: ${namespace}\\n\\tKey: ${key}`);\n }\n return resource?.value || '';\n};\nconst getTranslationResourceWithFallback = async (namespace, key, defaultValue) => {\n try {\n const value = await getTranslationResource(namespace, key);\n if (!value) {\n return defaultValue;\n }\n return value;\n }\n catch (e) {\n console.warn('Failed to load translation resource', namespace, key, e);\n return defaultValue;\n }\n};\n// Listener to ensure these always happen in the background, for strongest caching potential.\naddListener(messageDestination, async () => {\n if (translationResourceCache.length > 0) {\n return translationResourceCache;\n }\n const locale = await getAuthenticatedUserLocale();\n const response = await fetch(`https://translations.roblox.com/v1/translations?consumerType=Web`);\n if (!response.ok) {\n throw new Error(`Failed to load translation resources (${response.status})`);\n }\n const result = await response.json();\n const resourcesUrl = result.data.find((r) => r.locale === locale) ||\n result.data.find((r) => r.locale === englishLocale);\n if (!resourcesUrl) {\n throw new Error(`Failed to find translation resources for locale (${locale})`);\n }\n const resources = await fetch(resourcesUrl.url);\n const resourcesJson = await resources.json();\n return (translationResourceCache = resourcesJson.contents.map((r) => {\n return {\n namespace: r.namespace,\n key: r.key,\n value: r.translation || r.english,\n };\n }));\n}, {\n // Ensure that multiple requests for this information can't be processed at once.\n levelOfParallelism: 1,\n});\nglobalThis.localizationService = { getTranslationResource, getTranslationResourceWithFallback };\nexport { getTranslationResource, getTranslationResourceWithFallback };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'premiumPayoutsService.getPremiumPayoutsSummary';\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\nconst serializeDate = (date) => {\n return `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, '0')}-${`${date.getDate()}`.padStart(2, '0')}`;\n};\n// Fetches the Robux balance of the currently authenticated user.\nconst getPremiumPayoutsSummary = (universeId, startDate, endDate) => {\n return sendMessage(messageDestination, {\n universeId,\n startDate: serializeDate(startDate),\n endDate: serializeDate(endDate),\n });\n};\n// Loads the Robux balance of the currently authenticated user.\nconst loadPremiumPayoutsSummary = async (universeId, startDate, endDate) => {\n const response = await fetch(`https://engagementpayouts.roblox.com/v1/universe-payout-history?universeId=${universeId}&startDate=${startDate}&endDate=${endDate}`);\n if (!response.ok) {\n throw 'Failed to load premium payouts';\n }\n const result = await response.json();\n const payouts = [];\n for (let date in result) {\n const payout = result[date];\n if (payout.eligibilityType !== 'Eligible') {\n continue;\n }\n payouts.push({\n date,\n engagementScore: payout.engagementScore,\n payoutInRobux: payout.payoutInRobux,\n payoutType: payout.payoutType,\n });\n }\n return payouts;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.universeId}_${message.startDate}_${message.endDate}`, () => \n // Queue up the fetch request, when not in the cache\n loadPremiumPayoutsSummary(message.universeId, message.startDate, message.endDate));\n}, {\n levelOfParallelism: 1,\n});\nexport default getPremiumPayoutsSummary;\n","import { default as getPremiumPayoutsSummary } from './getPremiumPayoutsSummary';\nglobalThis.premiumPayoutsService = { getPremiumPayoutsSummary };\nexport { getPremiumPayoutsSummary };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'premiumService.getPremiumExpirationDate';\nconst definitelyPremium = {};\nconst cache = new ExpirableDictionary(messageDestination, 60 * 1000);\n// Check whether or not a user has a Roblox+ Premium subscription.\nconst getPremiumExpirationDate = async (userId) => {\n const expiration = await sendMessage(messageDestination, {\n userId,\n });\n if (!expiration) {\n return expiration;\n }\n return new Date(expiration);\n};\nconst getPrivateServerExpiration = async (id) => {\n const response = await fetch(`https://games.roblox.com/v1/vip-servers/${id}`);\n if (!response.ok) {\n console.warn('Failed to load private server details', id, response);\n return null;\n }\n const result = await response.json();\n if (result.subscription?.expired === false) {\n // If it's not expired, return the expiration date.\n return result.subscription.expirationDate;\n }\n return null;\n};\n// Check if the user has a private server with the Roblox+ hub.\nconst checkPrivateServerExpirations = async (userId) => {\n try {\n const response = await fetch(`https://games.roblox.com/v1/games/258257446/private-servers`);\n if (!response.ok) {\n console.warn('Failed to load private servers', userId, response);\n return null;\n }\n const result = await response.json();\n for (let i = 0; i < result.data.length; i++) {\n const privateServer = result.data[i];\n if (privateServer.owner?.id !== userId) {\n continue;\n }\n try {\n const expirationDate = await getPrivateServerExpiration(privateServer.vipServerId);\n if (expirationDate) {\n // We found a private server we paid for, we're done!\n return expirationDate;\n }\n }\n catch (err) {\n console.warn('Failed to check if private server was active', privateServer, err);\n }\n }\n return null;\n }\n catch (err) {\n console.warn('Failed to check private servers', userId, err);\n return null;\n }\n};\n// Fetch whether or not a user has a Roblox+ Premium subscription.\nconst loadPremiumMembership = async (userId) => {\n if (definitelyPremium[userId]) {\n return definitelyPremium[userId];\n }\n const expirationDate = await checkPrivateServerExpirations(userId);\n if (expirationDate) {\n return (definitelyPremium[userId] = expirationDate);\n }\n const response = await fetch(`https://api.roblox.plus/v1/rpluspremium/${userId}`);\n if (!response.ok) {\n throw new Error(`Failed to check premium membership for user (${userId})`);\n }\n const result = await response.json();\n if (result.data) {\n return (definitelyPremium[userId] = result.data.expiration);\n }\n return '';\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadPremiumMembership(message.userId));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default getPremiumExpirationDate;\n","import { default as getPremiumExpirationDate } from './getPremiumExpirationDate';\nconst isPremiumUser = async (userId) => {\n const expiration = await getPremiumExpirationDate(userId);\n if (expiration || expiration === null) {\n // We have an expiration date, or it's a lifetime subscription.\n // They are definitely premium.\n return true;\n }\n // No expiration date, no premium.\n return false;\n};\nglobalThis.premiumService = { isPremiumUser, getPremiumExpirationDate };\nexport { isPremiumUser, getPremiumExpirationDate };\n","import { Batch } from '@tix-factory/batch';\nimport { PresenceType } from 'roblox';\nconst getPresenceType = (presenceType) => {\n switch (presenceType) {\n case 1:\n return PresenceType.Online;\n case 2:\n return PresenceType.Experience;\n case 3:\n return PresenceType.Studio;\n default:\n return PresenceType.Offline;\n }\n};\nconst getLocationName = (presenceType, name) => {\n if (!name) {\n return '';\n }\n if (presenceType === PresenceType.Studio) {\n return name.replace(/^Studio\\s+-\\s*/, '');\n }\n return name;\n};\nclass PresenceBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 3 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await fetch('https://presence.roblox.com/v1/presence/users', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n userIds: items.map((i) => i.value),\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to load user presence');\n }\n const result = await response.json();\n items.forEach((item) => {\n const presence = result.userPresences.find((p) => p.userId === item.value);\n if (presence) {\n const presenceType = getPresenceType(presence.userPresenceType);\n if (presence.placeId &&\n (presenceType === PresenceType.Experience ||\n presenceType === PresenceType.Studio)) {\n item.resolve({\n type: presenceType,\n location: {\n placeId: presence.placeId || undefined,\n universeId: presence.universeId || undefined,\n name: getLocationName(presenceType, presence.lastLocation),\n serverId: presence.gameId,\n },\n });\n }\n else {\n item.resolve({\n type: presenceType,\n });\n }\n }\n else {\n item.resolve({\n type: PresenceType.Offline,\n });\n }\n });\n }\n}\nexport default PresenceBatchProcessor;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport PresenceBatchProcessor from './batchProcessor';\nconst messageDestination = 'presenceService.getUserPresence';\nconst presenceProcessor = new PresenceBatchProcessor();\nconst presenceCache = new ExpirableDictionary('presenceService', 15 * 1000);\n// Fetches the presence for a user.\nconst getUserPresence = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return presenceCache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n presenceProcessor.enqueue(message.userId));\n});\nglobalThis.presenceService = { getUserPresence };\nexport { getUserPresence };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'privateMessagesService.getUnreadMessageCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the unread private message count for the currently authenticated user.\nconst getUnreadMessageCount = (userId) => {\n return sendMessage(messageDestination, { userId });\n};\n// Loads the unread private message count for the authenticated user.\nconst loadUnreadMessageCount = async (userId) => {\n // User ID is used as a cache buster.\n const response = await fetch(`https://privatemessages.roblox.com/v1/messages/unread/count`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw 'Failed to load unread private message count';\n }\n const result = await response.json();\n return result.count;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.userId}`, () => \n // Queue up the fetch request, when not in the cache\n loadUnreadMessageCount(message.userId));\n}, {\n levelOfParallelism: 1,\n});\nexport default getUnreadMessageCount;\n","import { default as getUnreadMessageCount } from './getUnreadMessageCount';\nglobalThis.privateMessagesService = { getUnreadMessageCount };\nexport { getUnreadMessageCount };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\n// Destination to be used with messaging.\nconst messageDestinationPrefix = 'settingsService';\n// Fetches a locally stored setting value by its key.\nconst getSettingValue = (key) => {\n return sendMessage(`${messageDestinationPrefix}.getSettingValue`, {\n key,\n });\n};\n// Gets a boolean setting value, toggled to false by default.\nconst getToggleSettingValue = async (key) => {\n const value = await getSettingValue(key);\n return !!value;\n};\n// Locally stores a setting value.\nconst setSettingValue = (key, value) => {\n return sendMessage(`${messageDestinationPrefix}.setSettingValue`, {\n key,\n value,\n });\n};\nconst getValueFromLocalStorage = (key) => {\n if (!localStorage.hasOwnProperty(key)) {\n return undefined;\n }\n try {\n const valueArray = JSON.parse(localStorage[key]);\n if (Array.isArray(valueArray) && valueArray.length > 0) {\n return valueArray[0];\n }\n console.warn(`Setting value in localStorage invalid: ${localStorage[key]} - removing it.`);\n localStorage.removeItem(key);\n return undefined;\n }\n catch (err) {\n console.warn(`Failed to parse '${key}' value from localStorage - removing it.`, err);\n localStorage.removeItem(key);\n return undefined;\n }\n};\naddListener(`${messageDestinationPrefix}.getSettingValue`, ({ key }) => {\n return new Promise((resolve, reject) => {\n // chrome.storage APIs are callback-based until manifest V3.\n // Currently in migration phase, to migrate settings from localStorage -> chrome.storage.local\n const value = getValueFromLocalStorage(key);\n if (value !== undefined) {\n chrome.storage.local.set({\n [key]: value,\n }, () => {\n localStorage.removeItem(key);\n resolve(value);\n });\n }\n else {\n chrome.storage.local.get(key, (values) => {\n resolve(values[key]);\n });\n }\n });\n}, {\n levelOfParallelism: -1,\n allowExternalConnections: true,\n});\naddListener(`${messageDestinationPrefix}.setSettingValue`, ({ key, value }) => {\n return new Promise((resolve, reject) => {\n // chrome.storage APIs are callback-based until manifest V3.\n // Currently in migration phase, to migrate settings from localStorage -> chrome.storage.local\n if (value === undefined) {\n chrome.storage.local.remove(key, () => {\n localStorage.removeItem(key);\n resolve(undefined);\n });\n }\n else {\n chrome.storage.local.set({\n [key]: value,\n }, () => {\n localStorage.removeItem(key);\n resolve(undefined);\n });\n }\n });\n}, {\n levelOfParallelism: -1,\n allowExternalConnections: true,\n});\nglobalThis.settingsService = { getSettingValue, getToggleSettingValue, setSettingValue };\nexport { getSettingValue, getToggleSettingValue, setSettingValue };\n","import { Batch } from '@tix-factory/batch';\nimport { ThumbnailState } from 'roblox';\nclass ThumbnailBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1 * 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await fetch('https://thumbnails.roblox.com/v1/batch', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(items.map(({ value }) => {\n return {\n requestId: `${value.type}_${value.targetId}_${value.size}`,\n type: value.type,\n targetId: value.targetId,\n size: value.size,\n };\n })),\n });\n if (!response.ok) {\n throw new Error('Failed to load thumbnails');\n }\n const result = await response.json();\n items.forEach((item) => {\n const thumbnail = result.data.find((t) => t.requestId ===\n `${item.value.type}_${item.value.targetId}_${item.value.size}`);\n if (thumbnail) {\n const thumbnailState = thumbnail.state;\n item.resolve({\n state: thumbnailState,\n imageUrl: thumbnailState === ThumbnailState.Completed\n ? thumbnail.imageUrl\n : '',\n });\n }\n else {\n item.resolve({\n state: ThumbnailState.Error,\n imageUrl: '',\n });\n }\n });\n }\n}\nconst thumbnailBatchProcessor = new ThumbnailBatchProcessor();\nexport default thumbnailBatchProcessor;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { ThumbnailState, ThumbnailType } from 'roblox';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport batchProcessor from './batchProcessor';\nconst messageDestination = 'thumbnailsService.getThumbnail';\nconst cache = new ExpirableDictionary(messageDestination, 5 * 60 * 1000);\n// Fetches an avatar headshot thumbnail, for the given user ID.\nconst getAvatarHeadshotThumbnail = (userId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.AvatarHeadShot,\n targetId: userId,\n });\n};\n// Fetches an asset thumbnail, for the given asset ID.\nconst getAssetThumbnail = (assetId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.Asset,\n targetId: assetId,\n });\n};\n// Fetches a group icon.\nconst getGroupIcon = (groupId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.GroupIcon,\n targetId: groupId,\n });\n};\n// Fetches a game pass icon.\nconst getGamePassIcon = (gamePassId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.GamePass,\n targetId: gamePassId,\n });\n};\n// Fetches a developer product icon.\nconst getDeveloperProductIcon = (gamePassId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.DeveloperProduct,\n targetId: gamePassId,\n });\n};\n// Fetches a game icon.\nconst getGameIcon = (gamePassId) => {\n return sendMessage(messageDestination, {\n type: ThumbnailType.GameIcon,\n targetId: gamePassId,\n });\n};\n// Gets the default size for the thumbnail, by type.\nconst getThumbnailSize = (thumbnailType) => {\n switch (thumbnailType) {\n case ThumbnailType.GamePass:\n return '150x150';\n case ThumbnailType.GameIcon:\n return '256x256';\n default:\n return '420x420';\n }\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, async (message) => {\n const cacheKey = `${message.type}:${message.targetId}`;\n // Check the cache\n const thumbnail = await cache.getOrAdd(cacheKey, () => \n // Queue up the fetch request, when not in the cache\n batchProcessor.enqueue({\n type: message.type,\n targetId: message.targetId,\n size: getThumbnailSize(message.type),\n }));\n if (thumbnail.state !== ThumbnailState.Completed) {\n setTimeout(() => {\n // If the thumbnail isn't complete, evict it from the cache early.\n cache.evict(cacheKey);\n }, 30 * 1000);\n }\n return thumbnail;\n}, {\n levelOfParallelism: -1,\n allowExternalConnections: true,\n});\nglobalThis.thumbnailsService = {\n getAvatarHeadshotThumbnail,\n getAssetThumbnail,\n getGroupIcon,\n getGamePassIcon,\n getDeveloperProductIcon,\n getGameIcon,\n};\nexport { getAvatarHeadshotThumbnail, getAssetThumbnail, getGroupIcon, getGamePassIcon, getDeveloperProductIcon, getGameIcon, };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport { wait } from '@tix-factory/extension-utils';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nconst messageDestination = 'tradesService.getTradeCount';\nconst cache = new ExpirableDictionary(messageDestination, 30 * 1000);\nconst failureDelay = 5 * 1000;\n// Fetches the unread private message count for the currently authenticated user.\nconst getTradeCount = (tradeStatusType) => {\n return sendMessage(messageDestination, {\n tradeStatusType,\n });\n};\n// Loads the unread private message count for the authenticated user.\nconst loadTradeCount = async (tradeStatusType) => {\n // User ID is used as a cache buster.\n const response = await fetch(`https://trades.roblox.com/v1/trades/${tradeStatusType}/count`);\n // If we fail to send the request, delay the response to ensure we don't spam the API.\n if (response.status === 401) {\n await wait(failureDelay);\n throw 'User is unauthenticated';\n }\n else if (!response.ok) {\n await wait(failureDelay);\n throw `Failed to load ${tradeStatusType} trade count`;\n }\n const result = await response.json();\n return result.count;\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(`${message.tradeStatusType}`, () => \n // Queue up the fetch request, when not in the cache\n loadTradeCount(message.tradeStatusType));\n}, {\n levelOfParallelism: 1,\n});\nexport default getTradeCount;\n","import { default as getTradeCount } from './getTradeCount';\nglobalThis.tradesService = { getTradeCount };\nexport { getTradeCount };\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'transactionsService.emailTransactions';\n// Fetches the groups the user has access privileged roles in.\nconst emailTransactions = (targetType, targetId, transactionType, startDate, endDate) => {\n return sendMessage(messageDestination, {\n targetType,\n targetId,\n transactionType,\n startDate: startDate.getTime(),\n endDate: endDate.getTime(),\n });\n};\n// Loads the groups the user has access privileged roles in.\nconst doEmailTransactions = async (targetType, targetId, transactionType, startDate, endDate) => {\n const response = await xsrfFetch(new URL(`https://economy.roblox.com/v2/sales/sales-report-download`), {\n method: 'POST',\n body: JSON.stringify({\n targetType,\n targetId,\n transactionType,\n startDate: startDate.toISOString(),\n endDate: endDate.toISOString(),\n }),\n });\n if (!response.ok) {\n throw 'Failed to send transactions email';\n }\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return doEmailTransactions(message.targetType, message.targetId, message.transactionType, new Date(message.startDate), new Date(message.endDate));\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\nexport default emailTransactions;\n","import emailTransactions from './email-transactions';\n// Sends an email to the authenticated user with the group's transactions (sales).\nconst emailGroupTransactionSales = (groupId, startDate, endDate) => emailTransactions('Group', groupId, 'Sale', startDate, endDate);\n// Sends an email to the authenticated user with their personally transactions (sales).\nconst emailUserTransactionSales = (userId, startDate, endDate) => emailTransactions('User', userId, 'Sale', startDate, endDate);\nglobalThis.transactionsService = { emailGroupTransactionSales, emailUserTransactionSales };\nexport { emailGroupTransactionSales, emailUserTransactionSales };\n","import { Batch } from '@tix-factory/batch';\nimport { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'usersService.getUserById';\nclass UsersBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await xsrfFetch(new URL(`https://users.roblox.com/v1/users`), {\n method: 'POST',\n body: JSON.stringify({\n userIds: items.map((i) => i.key),\n excludeBannedUsers: false,\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to users by ids');\n }\n const result = await response.json();\n items.forEach((item) => {\n const user = result.data.find((a) => a.id === item.value);\n if (user) {\n item.resolve({\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n });\n }\n else {\n item.resolve(null);\n }\n });\n }\n getKey(item) {\n return item.toString();\n }\n}\nconst batchProcessor = new UsersBatchProcessor();\nconst cache = new ExpirableDictionary(messageDestination, 2 * 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getUserById = async (id) => {\n return sendMessage(messageDestination, {\n id,\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(batchProcessor.getKey(message.id), () => {\n // Queue up the fetch request, when not in the cache\n return batchProcessor.enqueue(message.id);\n });\n});\nexport default getUserById;\n","import { Batch } from '@tix-factory/batch';\nimport { addListener, sendMessage } from '@tix-factory/extension-messaging';\nimport ExpirableDictionary from '../../utils/expireableDictionary';\nimport xsrfFetch from '../../utils/xsrfFetch';\nconst messageDestination = 'usersService.getUserByName';\nclass UserNamesBatchProcessor extends Batch {\n constructor() {\n super({\n levelOfParallelism: 1,\n maxSize: 100,\n minimumDelay: 1000,\n enqueueDeferDelay: 10,\n });\n }\n async process(items) {\n const response = await xsrfFetch(new URL(`https://users.roblox.com/v1/usernames/users`), {\n method: 'POST',\n body: JSON.stringify({\n usernames: items.map((i) => i.key),\n excludeBannedUsers: false,\n }),\n });\n if (!response.ok) {\n throw new Error('Failed to users by names');\n }\n const result = await response.json();\n items.forEach((item) => {\n const user = result.data.find((a) => a.requestedUsername === item.key);\n if (user) {\n item.resolve({\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n });\n }\n else {\n item.resolve(null);\n }\n });\n }\n getKey(item) {\n return item;\n }\n}\nconst batchProcessor = new UserNamesBatchProcessor();\nconst cache = new ExpirableDictionary(messageDestination, 2 * 60 * 1000);\n// Fetches the date when a badge was awarded to the specified user.\nconst getUserByName = async (name) => {\n return sendMessage(messageDestination, {\n name: name.toLowerCase(),\n });\n};\n// Listen for messages sent to the service worker.\naddListener(messageDestination, (message) => {\n // Check the cache\n return cache.getOrAdd(batchProcessor.getKey(message.name), () => {\n // Queue up the fetch request, when not in the cache\n return batchProcessor.enqueue(message.name);\n });\n});\nexport default getUserByName;\n","import { addListener, sendMessage } from '@tix-factory/extension-messaging';\nconst messageDestination = 'usersService.getAuthenticatedUser';\nconst cacheDuration = 60 * 1000;\nlet authenticatedUser = undefined;\n// Fetches the currently authenticated user.\nconst getAuthenticatedUser = () => {\n return sendMessage(messageDestination, {});\n};\n// Loads the currently authenticated user.\nconst loadAuthenticatedUser = async () => {\n if (authenticatedUser !== undefined) {\n return authenticatedUser;\n }\n try {\n const response = await fetch('https://users.roblox.com/v1/users/authenticated');\n if (response.status === 401) {\n return (authenticatedUser = null);\n }\n else if (!response.ok) {\n throw new Error('Failed to load authenticated user');\n }\n const result = await response.json();\n return (authenticatedUser = {\n id: result.id,\n name: result.name,\n displayName: result.displayName,\n });\n }\n finally {\n setTimeout(() => {\n authenticatedUser = undefined;\n }, cacheDuration);\n }\n};\naddListener(messageDestination, () => loadAuthenticatedUser(), {\n levelOfParallelism: 1,\n});\nexport default getAuthenticatedUser;\n","import getAuthenticatedUser from './getAuthenticatedUser';\nimport getUserByName from './get-user-by-name';\nimport getUserById from './get-user-by-id';\nglobalThis.usersService = { getAuthenticatedUser, getUserByName, getUserById };\nexport { getAuthenticatedUser, getUserByName, getUserById };\n","// This class can be used to concurrently cache items, or fetch their values.\nclass ExpirableDictionary {\n lockKey;\n expirationInMilliseconds;\n // The items that are in the dictionary.\n items = {};\n constructor(\n // A name for the dictionary, used for locking.\n name, \n // How long the item will remain in the dictionary, in milliseconds.\n expirationInMilliseconds) {\n this.lockKey = `ExpirableDictionary:${name}`;\n this.expirationInMilliseconds = expirationInMilliseconds;\n }\n // Tries to fetch an item by its key from the dictionary, or it will call the value factory to add it in.\n getOrAdd(key, valueFactory) {\n const item = this.items[key];\n if (item !== undefined) {\n return Promise.resolve(item);\n }\n return new Promise((resolve, reject) => {\n navigator.locks\n .request(`${this.lockKey}:${key}`, async () => {\n // It's possible the item was added since we requested the lock, check again.\n const item = this.items[key];\n if (item !== undefined) {\n resolve(item);\n return;\n }\n try {\n const value = (this.items[key] = await valueFactory());\n setTimeout(() => this.evict(key), this.expirationInMilliseconds);\n resolve(value);\n }\n catch (e) {\n reject(e);\n }\n })\n .catch(reject);\n });\n }\n evict(key) {\n delete this.items[key];\n }\n}\nexport default ExpirableDictionary;\n","import ExpirableDictionary from './expireableDictionary';\nconst cache = new ExpirableDictionary('fetchDataUri', 5 * 60 * 1000);\n// Converts a URL to a data URI of its loaded contents.\nexport default (url) => {\n return cache.getOrAdd(url.href, () => {\n return new Promise((resolve, reject) => {\n fetch(url.href)\n .then((result) => {\n const reader = new FileReader();\n reader.onerror = (err) => {\n reject(err);\n };\n reader.onloadend = () => {\n if (typeof reader.result === 'string') {\n resolve(reader.result);\n }\n else {\n reject(new Error(`fetchDataUri: Unexpected result type (${typeof reader.result})`));\n }\n };\n result\n .blob()\n .then((blob) => {\n reader.readAsDataURL(blob);\n })\n .catch(reject);\n })\n .catch(reject);\n });\n });\n};\n","import { addListener, getWorkerTab, sendMessage, sendMessageToTab, } from '@tix-factory/extension-messaging';\nimport { isBackgroundPage } from '@tix-factory/extension-utils';\nconst messageDestination = 'launchProtocolUrl';\n// Keep track of the tabs, so we can put the user back where they were.b\nlet previousTab = undefined;\nlet protocolLauncherTab = undefined;\n// Attempt to launch the protocol URL in the current tab.\nconst tryDirectLaunch = (protocolUrl) => {\n if (!isBackgroundPage && location) {\n location.href = protocolUrl;\n return true;\n }\n return false;\n};\n// Launch the protocol URL from a service worker.\nconst launchProtocolUrl = (protocolUrl) => {\n if (tryDirectLaunch(protocolUrl)) {\n // We were able to directly launch the protocol URL.\n // Nothing more to do.\n return Promise.resolve();\n }\n const workerTab = getWorkerTab();\n if (workerTab) {\n // If we're in the background, and we have a tab that can process the protocol URL, use that instead.\n // This will ensure that when we use the protocol launcher to launch Roblox, that they have the highest\n // likihood of already having accepted the protocol launcher permission.\n sendMessageToTab(messageDestination, {\n protocolUrl,\n }, workerTab);\n return Promise.resolve();\n }\n // TODO: Convert to promise signatures when moving to manifest V3.\n chrome.tabs.query({\n active: true,\n currentWindow: true,\n }, (currentTab) => {\n previousTab = currentTab[0];\n if (previousTab) {\n // Try to open the protocol launcher tab right next to the current tab, so that when it\n // closes, it will put the user back on the tab they are on now.\n chrome.tabs.create({\n url: protocolUrl,\n index: previousTab.index + 1,\n windowId: previousTab.windowId,\n }, (tab) => {\n protocolLauncherTab = tab;\n });\n }\n else {\n chrome.tabs.create({ url: protocolUrl });\n // If we don't know where they were before, then don't try to keep track of anything.\n previousTab = undefined;\n protocolLauncherTab = undefined;\n }\n });\n return Promise.resolve();\n};\nif (isBackgroundPage) {\n chrome.tabs.onRemoved.addListener((tabId) => {\n // Return the user to the tab they were on before, when we're done launching the protocol URL.\n // chrome self-closes the protocol URL tab when opened.\n if (tabId === protocolLauncherTab?.id && previousTab?.id) {\n chrome.tabs.update(previousTab.id, {\n active: true,\n });\n }\n previousTab = undefined;\n protocolLauncherTab = undefined;\n });\n}\naddListener(messageDestination, (message) => launchProtocolUrl(message.protocolUrl));\n// Launches a protocol URL, using the most user-friendly method.\nexport default async (protocolUrl) => {\n if (tryDirectLaunch(protocolUrl)) {\n // If we can directly launch the protocol URL, there's nothing left to do.\n return;\n }\n // Otherwise, we have to send a message out and try some nonsense.\n await sendMessage(messageDestination, { protocolUrl });\n};\n","const headerName = 'X-CSRF-Token';\nlet xsrfToken = '';\n// A fetch request which will attach an X-CSRF-Token in all outbound requests.\nconst xsrfFetch = async (url, requestDetails) => {\n if (url.hostname.endsWith('.roblox.com')) {\n if (!requestDetails) {\n requestDetails = {};\n }\n requestDetails.credentials = 'include';\n if (!requestDetails.headers) {\n requestDetails.headers = new Headers();\n }\n if (requestDetails.headers instanceof Headers) {\n if (xsrfToken) {\n requestDetails.headers.set(headerName, xsrfToken);\n }\n if (requestDetails.body && !requestDetails.headers.has('Content-Type')) {\n requestDetails.headers.set('Content-Type', 'application/json');\n }\n }\n }\n const response = await fetch(url, requestDetails);\n const token = response.headers.get(headerName);\n if (response.ok || !token) {\n return response;\n }\n xsrfToken = token;\n return xsrfFetch(url, requestDetails);\n};\nexport default xsrfFetch;\n","import PromiseQueue from '../promise-queue';\nimport ErrorEvent from '../events/errorEvent';\nimport ItemErrorEvent from '../events/itemErrorEvent';\n// A class for batching and processing multiple single items into a single call.\nclass Batch extends EventTarget {\n queueMap = {};\n promiseMap = {};\n limiter;\n concurrencyHandler;\n // All the batch items waiting to be processed.\n queueArray = [];\n // The configuration for this batch processor.\n config;\n constructor(configuration) {\n super();\n this.config = configuration;\n this.limiter = new PromiseQueue({\n levelOfParallelism: 1,\n delayInMilliseconds: configuration.minimumDelay || 0,\n });\n this.concurrencyHandler = new PromiseQueue({\n levelOfParallelism: configuration.levelOfParallelism || Infinity,\n });\n }\n // Enqueues an item into a batch, to be processed.\n enqueue(item) {\n return new Promise((resolve, reject) => {\n const key = this.getKey(item);\n const promiseMap = this.promiseMap;\n const queueArray = this.queueArray;\n const queueMap = this.queueMap;\n const retryCount = this.config.retryCount || 0;\n const getRetryDelay = this.getRetryDelay.bind(this);\n const dispatchEvent = this.dispatchEvent.bind(this);\n const check = this.check.bind(this);\n // Step 1: Ensure we have a way to resolve/reject the promise for this item.\n const mergedPromise = promiseMap[key] || [];\n if (mergedPromise.length < 0) {\n this.promiseMap[key] = mergedPromise;\n }\n mergedPromise.push({ resolve, reject });\n // Step 2: Check if we have the batched item created.\n if (!queueMap[key]) {\n const remove = (item) => {\n // Mark the item as completed, so we know we either resolved or rejected it.\n item.completed = true;\n for (let i = 0; i < queueArray.length; i++) {\n if (queueArray[i].key === key) {\n queueArray.splice(i, 1);\n break;\n }\n }\n delete promiseMap[key];\n delete queueMap[key];\n };\n const batchItem = {\n key,\n value: item,\n attempt: 0,\n retryAfter: 0,\n completed: false,\n resolve(result) {\n // We're not accepting any new items for this resolution.\n remove(this);\n // Defer the resolution until after the thread resolves.\n setTimeout(() => {\n // Process anyone who applied.\n while (mergedPromise.length > 0) {\n const promise = mergedPromise.shift();\n promise?.resolve(result);\n }\n }, 0);\n },\n reject(error) {\n // Defer the resolution until after the thread resolves.\n const retryDelay = this.attempt <= retryCount ? getRetryDelay(this) : undefined;\n const retryAfter = retryDelay !== undefined\n ? performance.now() + retryDelay\n : undefined;\n // Emit an event to notify that the item failed to process.\n dispatchEvent(new ItemErrorEvent(error, this, retryAfter));\n if (retryAfter !== undefined) {\n // The item can be retried, we haven't hit the maximum number of attempts yet.\n this.retryAfter = retryAfter;\n // Ensure the check runs after the retry delay.\n setTimeout(check, retryDelay);\n }\n else {\n // Remove the item, and reject anyone waiting on it.\n remove(this);\n // Defer the resolution until after the thread resolves.\n setTimeout(() => {\n // Process anyone who applied.\n while (mergedPromise.length > 0) {\n const promise = mergedPromise.shift();\n promise?.reject(error);\n }\n }, 0);\n }\n },\n };\n queueMap[key] = batchItem;\n queueArray.push(batchItem);\n }\n // Attempt to process the queue on the next event loop.\n setTimeout(check, this.config.enqueueDeferDelay);\n });\n }\n // Batches together queued items, calls the process method.\n // Will do nothing if the config requirements aren't met.\n check() {\n if (this.limiter.size > 0) {\n // Already being checked.\n return;\n }\n // We're using p-limit to ensure that multiple process calls can't be called at once.\n this.limiter.enqueue(this._check.bind(this)).catch((err) => {\n // This should be \"impossible\".. right?\n this.dispatchEvent(new ErrorEvent(err));\n });\n }\n // The actual implementation of the check method.\n _check() {\n const retry = this.check.bind(this);\n // Get a batch of items to process.\n const batch = this.getBatch();\n // Nothing in the queue ready to be processed.\n if (batch.length < 1) {\n return Promise.resolve();\n }\n // Update the items that we're about to process, so they don't get double processed.\n batch.forEach((item) => {\n item.attempt += 1;\n item.retryAfter = Infinity;\n });\n setTimeout(async () => {\n try {\n await this.concurrencyHandler.enqueue(this.process.bind(this, batch));\n }\n catch (err) {\n this.dispatchEvent(new ErrorEvent(err));\n }\n finally {\n batch.forEach((item) => {\n if (item.completed) {\n // Item completed its processing, nothing more to do.\n return;\n }\n else if (item.retryAfter > 0 && item.retryAfter !== Infinity) {\n // The item failed to process, but it is going to be retried.\n return;\n }\n else {\n // Item neither rejected, or completed its processing status.\n // This is a requirement, so we reject the item.\n item.reject(new Error('Item was not marked as resolved or rejected after batch processing completed.'));\n }\n });\n // Now that we've finished processing the batch, run the process again, just in case there's anything left.\n setTimeout(retry, 0);\n }\n }, 0);\n if (batch.length >= this.config.maxSize) {\n // We have the maximum number of items in the batch, let's make sure we kick off the process call again.\n setTimeout(retry, this.config.minimumDelay);\n }\n return Promise.resolve();\n }\n getBatch() {\n const now = performance.now();\n const batch = [];\n for (let i = 0; i < this.queueArray.length; i++) {\n const batchItem = this.queueArray[i];\n if (batchItem.retryAfter > now) {\n // Item is not ready to be retried, or it is currently being processed.\n continue;\n }\n batch.push(batchItem);\n if (batch.length >= this.config.maxSize) {\n break;\n }\n }\n return batch;\n }\n // Obtains a unique key to identify the item.\n // This is used to deduplicate the batched items.\n getKey(item) {\n return item === undefined ? 'undefined' : JSON.stringify(item);\n }\n // Returns how long to wait before retrying the item.\n getRetryDelay(item) {\n return 0;\n }\n // Called when it is time to process a batch of items.\n process(items) {\n return Promise.reject(new Error('Inherit this class, and implement the processBatch method.'));\n }\n}\nexport default Batch;\n","// An event class which can be used to emit an error.\nclass ErrorEvent extends Event {\n // The error associated with the event.\n error;\n // Constructs the event from the error.\n constructor(error) {\n super('error');\n this.error = error;\n }\n}\nexport default ErrorEvent;\n","import ErrorEvent from './errorEvent';\n// An event class which can be used to emit an error event for an item that failed to process.\nclass ItemErrorEvent extends ErrorEvent {\n // The item that failed to process.\n batchItem;\n // The amount of time when the item will be retried.\n retryAfter;\n // Constructs the event from the error.\n constructor(error, batchItem, retryAfter) {\n super(error);\n this.batchItem = batchItem;\n this.retryAfter = retryAfter;\n }\n}\nexport default ItemErrorEvent;\n","// Export all the things from this module.\nexport { default as Batch } from './batch';\nexport { default as ErrorEvent } from './events/errorEvent';\nexport { default as ItemErrorEvent } from './events/itemErrorEvent';\nexport { default as PromiseQueue } from './promise-queue';\n","// A limiter for running promises in parallel.\n// Queue ensures order is maintained.\nclass PromiseQueue {\n // All the promises that have been enqueued, and are waiting to be processed.\n queue = [];\n // The PromiseQueue configuration.\n config;\n // How many promises are actively being processed.\n activeCount = 0;\n // The next time a promise can be processed.\n nextProcessTime = 0;\n // Constructs a promise queue, defining the number of promises that may run in parallel.\n constructor(config) {\n this.config = config;\n }\n // The number of promises waiting to be processed.\n get size() {\n return this.queue.length;\n }\n // Puts a function that will create the promise to run on the queue, and returns a promise\n // that will return the result of the enqueued promise.\n enqueue(createPromise) {\n return new Promise(async (resolve, reject) => {\n this.queue.push({\n deferredPromise: { resolve, reject },\n createPromise,\n });\n await this.process();\n });\n }\n async process() {\n if (this.activeCount >= this.config.levelOfParallelism) {\n // Already running max number of promises in parallel.\n return;\n }\n const reprocess = this.process.bind(this);\n const delayInMilliseconds = this.config.delayInMilliseconds;\n if (delayInMilliseconds !== undefined && delayInMilliseconds > 0) {\n const now = performance.now();\n const remainingTime = this.nextProcessTime - now;\n if (remainingTime > 0) {\n // We're not allowed to process the next promise yet.\n setTimeout(reprocess, remainingTime);\n return;\n }\n this.nextProcessTime = now + delayInMilliseconds;\n }\n const promise = this.queue.shift();\n if (!promise) {\n // No promise to process.\n return;\n }\n this.activeCount++;\n try {\n const result = await promise.createPromise();\n promise.deferredPromise.resolve(result);\n }\n catch (err) {\n promise.deferredPromise.reject(err);\n }\n finally {\n // Ensure we subtract from how many promises are active\n this.activeCount--;\n // And then run the process function again, in case there are any promises left to run.\n setTimeout(reprocess, 0);\n }\n }\n}\nexport default PromiseQueue;\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export * as assets from '../services/assets';\nexport * as avatar from '../services/avatar';\nexport * as badges from '../services/badges';\nexport * as currency from '../services/currency';\nexport * as followings from '../services/followings';\nexport * as friends from '../services/friends';\nexport * as gameLaunch from '../services/game-launch';\nexport * as gamePasses from '../services/game-passes';\nexport * as groups from '../services/groups';\nexport * as inventory from '../services/inventory';\nexport * as localization from '../services/localization';\nexport * as premium from '../services/premium';\nexport * as premiumPayouts from '../services/premium-payouts';\nexport * as presence from '../services/presence';\nexport * as privateMessages from '../services/private-messages';\nexport * as settings from '../services/settings';\nexport * as thumbnails from '../services/thumbnails';\nexport * as trades from '../services/trades';\nexport * as transactions from '../services/transactions';\nexport * as users from '../services/users';\nimport { addListener } from '@tix-factory/extension-messaging';\nimport { manifest } from '@tix-factory/extension-utils';\nexport * from './notifiers';\nchrome.browserAction.setTitle({\n title: `${manifest.name} ${manifest.version}`,\n});\nchrome.browserAction.onClicked.addListener(() => {\n chrome.tabs.create({\n url: manifest.homepage_url,\n active: true,\n });\n});\naddListener('extension.reload', async () => {\n setTimeout(() => {\n chrome.runtime.reload();\n }, 250);\n}, {\n levelOfParallelism: 1,\n allowExternalConnections: true,\n});\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/js/background/background.js b/js/background/background.js index 58089c3..554e54a 100644 --- a/js/background/background.js +++ b/js/background/background.js @@ -46,82 +46,4 @@ foreach( } ); -/* Startup Notification */ -Extension.Storage.Singleton.get('startupNotification') - .then((startnote) => { - if (!startnote || typeof startnote !== 'object') { - startnote = { - on: !Extension.Singleton.isIncognito, - visit: false, - names: {}, - }; - - Extension.Storage.Singleton.blindSet('startupNotification', startnote); - } - - const makenote = function () { - Roblox.users.getAuthenticatedUser().then(function (user) { - var username = user ? user.username : ''; - for (var n in startnote.names) { - if (n.toLowerCase() === username.toLowerCase()) { - username = startnote.names[n]; - break; - } - } - - Extension.NotificationService.Singleton.createNotification({ - id: `${Extension.Singleton.id}.startNotification`, - title: user - ? `Hello, ${user.username}!` - : "You're currently signed out", - message: 'Made by WebGL3D', - context: `${Extension.Singleton.manifest.name} ${Extension.Singleton.manifest.version} started`, - expiration: 15 * 1000, - buttons: [ - { - text: 'Problems? Suggestions? Post here!', - url: 'https://www.roblox.com/groups/2518656/ROBLOX-Fan-Group?rbxp=48103520', - }, - ], - metadata: { - url: `https://roblox.plus/about/changes?version=${Extension.Singleton.manifest.version}`, - }, - }); - }); - }; - - startnote.names = type(startnote.names) == 'object' ? startnote.names : {}; - if (startnote.on && !startnote.visit) { - makenote(); - } else if (startnote.on) { - let createdListener; - let updatedListener; - const takeAction = (tab) => { - try { - const tabURL = new URL(tab.url); - if (!tabURL.hostname.endsWith('.roblox.com')) { - return; - } - - chrome.tabs.onCreated.removeListener(createdListener); - chrome.tabs.onUpdated.removeListener(updatedListener); - makenote(); - } catch { - // don't care for now - } - }; - createdListener = (tab) => { - takeAction(tab); - }; - updatedListener = (tabId, changes, tab) => { - takeAction(tab); - }; - chrome.tabs.onCreated.addListener(createdListener); - chrome.tabs.onUpdated.addListener(updatedListener); - } - }) - .catch((e) => { - console.warn('could not read startupNotification', e); - }); - // WebGL3D diff --git a/js/background/notifications.js b/js/background/notifications.js deleted file mode 100644 index b9644c4..0000000 --- a/js/background/notifications.js +++ /dev/null @@ -1,78 +0,0 @@ -(function () { - let audioPlayers = {}; - let speaking = ""; - - Extension.NotificationService.Singleton.onNotificationCreated.addEventListener(notification => { - Extension.Storage.Singleton.get("notificationVolume").then(storedVolume => { - var volume = 0.5; - if (notification.metadata.hasOwnProperty("volume")) { - volume = notification.metadata.volume; - } else { - volume = isNaN(storedVolume) ? 0.5 : storedVolume; - } - - if (notification.metadata.robloxSound) { - // This is no longer supported - } else if (notification.metadata.speak) { - if (chrome.tts.isSpeaking) { - chrome.tts.stop(); - } - - chrome.tts.speak(notification.metadata.speak, { - lang: "en-GB", - volume: volume, - onEvent: function (e) { - if (e.type == "start") { - speaking = notification.id; - } else { - if (speaking == notification.id) { - speaking = ""; - } - } - } - }); - } - }).catch(err => { - console.warn(notification, err); - }); - }); - - Extension.NotificationService.Singleton.onNotificationClosed.addEventListener(notification => { - if (audioPlayers[notification.id]) { - audioPlayers[notification.id].stop(); - } - - if (speaking == notification.id) { - chrome.tts.stop(); - } - - delete audioPlayers[notification.id]; - }); - - Extension.NotificationService.Singleton.onNotificationClicked.addEventListener(notification => { - if (notification.metadata.url) { - window.open(notification.metadata.url); - } - }); - - Extension.NotificationService.Singleton.onNotificationButtonClicked.addEventListener(data => { - let button = data.notification.buttons[data.buttonIndex]; - if (button && button.url) { - window.open(button.url); - } - }); - - chrome.contextMenus.create({ - id: "clearNotifications", - title: "Clear Notifications", - contexts: ["browser_action"], - onclick: function () { - Extension.NotificationService.Singleton.clearNotifications().then(notifications => { - console.log("Notifications cleared", notifications); - }).catch(console.error); - } - }); -})(); - - -// WebGL3D diff --git a/js/service-worker/notifiers/index.ts b/js/service-worker/notifiers/index.ts index 2ce2b7e..e24cb6a 100644 --- a/js/service-worker/notifiers/index.ts +++ b/js/service-worker/notifiers/index.ts @@ -1,6 +1,7 @@ import CatalogNotifier from './catalog'; import FriendPresenceNotifier from './friend-presence'; import GroupShoutNotifier from './group-shout'; +import './startup'; import TradeNotifier from './trades'; // Registry of all the notifiers diff --git a/js/service-worker/notifiers/startup/index.ts b/js/service-worker/notifiers/startup/index.ts new file mode 100644 index 0000000..17fb8fa --- /dev/null +++ b/js/service-worker/notifiers/startup/index.ts @@ -0,0 +1,86 @@ +import { manifest } from '@tix-factory/extension-utils'; +import { getSettingValue } from '../../../services/settings'; +import { getAuthenticatedUser } from '../../../services/users'; + +const notificationId = 'startup-notification'; + +const displayStartupNotification = async (): Promise => { + if (!manifest.icons) { + console.warn('Missing manifest icons'); + return; + } + + const authenticatedUser = await getAuthenticatedUser(); + chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: chrome.extension.getURL(manifest.icons['128']), + title: 'Roblox+ Started', + message: authenticatedUser + ? `Hello, ${authenticatedUser.displayName}` + : 'You are currently signed out', + contextMessage: `${manifest.name} ${manifest.version}, by WebGL3D`, + }); +}; + +getSettingValue('startupNotification') + .then(async (setting) => { + if (typeof setting !== 'object') { + setting = { + on: !chrome.extension.inIncognitoContext, + visit: false, + }; + } + + if (!setting.on) { + return; + } + + if (setting.visit) { + // Only show the startup notification after Roblox has been visited. + const updatedListener = ( + _tabId: number, + _changes: chrome.tabs.TabChangeInfo, + tab: chrome.tabs.Tab + ): Promise => { + return takeAction(tab); + }; + + const takeAction = async (tab: chrome.tabs.Tab): Promise => { + if (!tab.url) { + return; + } + + try { + const tabURL = new URL(tab.url); + if (!tabURL.hostname.endsWith('.roblox.com')) { + return; + } + + chrome.tabs.onCreated.removeListener(takeAction); + chrome.tabs.onUpdated.removeListener(updatedListener); + await displayStartupNotification(); + } catch { + // don't care for now + } + }; + + chrome.tabs.onUpdated.addListener(updatedListener); + chrome.tabs.onCreated.addListener(takeAction); + } else { + await displayStartupNotification(); + } + }) + .catch((err) => { + console.warn('Failed to render startup notification', err); + }); + +chrome.notifications.onClicked.addListener((id) => { + if (id !== notificationId) { + return; + } + + chrome.tabs.create({ + url: `https://roblox.plus/about/changes?version=${manifest.version}`, + active: true, + }); +}); diff --git a/js/vanilla/extension/notificationService.js b/js/vanilla/extension/notificationService.js deleted file mode 100644 index 6397186..0000000 --- a/js/vanilla/extension/notificationService.js +++ /dev/null @@ -1,289 +0,0 @@ -Extension.NotificationService = class extends Extension.BackgroundService { - constructor(extension) { - super("Extension.NotificationService"); - - this._idBase = 0; - this.notifications = {}; - this.imageCache = {}; - this.extension = extension; - this.onNotificationClosed = new Extension.Event("Extension.NotificationService.onNotificationClosed", extension); - this.onNotificationCreated = new Extension.Event("Extension.NotificationService.onNotificationCreated", extension); - this.onNotificationClicked = new Extension.Event("Extension.NotificationService.onNotificationClicked", extension); - this.onNotificationButtonClicked = new Extension.Event("Extension.NotificationService.onNotificationButtonClicked", extension); - - this.register([ - this.createNotification, - this.closeNotification, - this.showNotification, - this.hideNotification, - this.getNotifications, - - this.clickNotification, - this.clickNotificationButton, - - this.getNotificationImageUrl - ]); - } - - createNotification(notificationData) { - return new Promise((resolve, reject) => { - let notification = { - // A unique identifier for the notification that can be used to ensure no two notifications exist at the same time for the same thing. - id: typeof (notificationData.id) === "string" && notificationData.id.length > 0 ? notificationData.id : `Extension.NotificationService.Notification.${this.extension.id}.${++this._idBase}`, - - // Title of the notification - title: typeof (notificationData.title) === "string" && notificationData.title.length > 0 ? notificationData.title : this.extension.name, - - // Main notification content. - message: typeof (notificationData.message) === "string" && notificationData.message.length > 0 ? notificationData.message : "", - - // Alternate notification content with a lower-weight font. - context: typeof (notificationData.context) === "string" && notificationData.context.length > 0 ? notificationData.context : "", - - // A URL to the sender's avatar, app icon, or a thumbnail for image notifications. - // URLs can be a data URL, a blob URL, or a URL relative to a resource within this extension's .crx file. - icon: typeof (notificationData.icon) === "string" && notificationData.icon.length > 0 ? notificationData.icon : this.extension.icon.imageUrl, - - // Items for multi-item notifications. Users on Mac OS X only see the first item. - items: typeof(notificationData.items) === "object" ? notificationData.items : {}, - - // Text and icons for up to two notification action buttons. - buttons: Array.isArray(notificationData.buttons) ? notificationData.buttons.filter(b => { - return b && b.text; - }) : [], - - // Metadata about the notification that can be dumped into. Not used for any display purposes. - metadata: typeof(notificationData.metadata) === "object" ? notificationData.metadata : {}, - - created: +new Date - }; - - const createNotification = () => { - this.getNotificationImageUrl(notification.icon).then(icon => { - notification.icon = icon; - - this.notifications[notification.id] = notification; - this.onNotificationCreated.blindDispatchEvent(notification); - resolve(notification); - - if (!notificationData.hidden) { - this.showNotification(notification.id, notificationData.displayExpiration).then(() => { - // Notification shown successfully - }).catch(err => { - console.warn(`Extension.NotificationService.showNotification("${notification.id}", ${notificationData.displayExpiration})`, err); - }); - } - - if (notificationData.expiration && notificationData.expiration > 0) { - setTimeout(() => { - this.closeNotification(notification.id).then(() => { - // Notification closed successfully - }).catch(err => { - console.warn(`Extension.NotificationService.closeNotification("${notification.id}")`, err); - }); - }, notificationData.expiration); - } - }).catch(reject); - }; - - let existingNotification = this.notifications[notification.id]; - if (existingNotification) { - this.closeNotification(existingNotification.id).then(createNotification).catch(reject); - } else { - createNotification(); - } - }); - } - - closeNotification(id) { - return new Promise((resolve, reject) => { - const removeNotification = () => { - let notification = this.notifications[id]; - delete this.notifications[id]; - - if (notification) { - this.onNotificationClosed.blindDispatchEvent(notification); - } - - resolve({}); - }; - - this.hideNotification(id).then(removeNotification).catch((err) => { - console.warn(`Extension.NotificationService.closeNotification("${id}")`, err); - removeNotification(); - }); - }); - } - - showNotification(id, expiration) { - let notification = this.notifications[id]; - if (notification) { - return new Promise((resolve, reject) => { - let items = []; - let buttons = notification.buttons.map(button => { - return { - title: button.text - }; - }).slice(0, 2); - - for (let key in notification.items) { - items.push({ - title: key, - message: notification.items[key] - }); - } - - let chromeNotification = { - type: "basic", - iconUrl: notification.icon, - title: notification.title, - message: notification.message, - requireInteraction: true - }; - - if (notification.context.length > 0) { - chromeNotification.contextMessage = notification.context; - } - - if (buttons.length > 0) { - chromeNotification.buttons = buttons; - } - - if (items.length > 0) { - chromeNotification.type = "list"; - chromeNotification.items = items; - } - - if (expiration && expiration > 0) { - setTimeout(() => { - // TODO: This will give an unexpected result if the user clicks close on the notification and then another notification is shown with the same id after - this.hideNotification(notification.id); - }, expiration); - } - - chrome.notifications.create(notification.id, chromeNotification, function() { - resolve({}); - }); - }); - } else { - return Promise.reject("Notification does not exist"); - } - } - - hideNotification(id) { - return new Promise((resolve, reject) => { - chrome.notifications.clear(id, wasCleared => { - resolve(wasCleared); - }); - }); - } - - clickNotification(id) { - let notification = this.notifications[id]; - if (notification) { - return this.onNotificationClicked.dispatchEvent(notification); - } - } - - clickNotificationButton(id, buttonIndex) { - let notification = this.notifications[id]; - if (notification) { - return this.onNotificationButtonClicked.dispatchEvent({ - notification: notification, - buttonIndex: buttonIndex - }); - } - } - - getNotifications() { - let notifications = []; - for (var id in this.notifications) { - notifications.push(this.notifications[id]); - } - - return Promise.resolve(notifications.sort((a, b) => { - return b.created - a.created; - })); - } - - clearNotifications() { - return new Promise((resolve, reject) => { - this.getNotifications().then(notifications => { - let promises = []; - notifications.forEach(notification => { - promises.push(this.closeNotification(notification.id)); - }); - - Promise.all(promises).then(() => { - resolve(notifications); - }).catch(reject); - }).catch(reject); - }); - } - - getNotificationImageUrl(imageUrl) { - // This method exists because chrome notifications won't display at all if the image download fails or has incorrect response headers - // This method attempts to turn an HTTP image url into a data url - // https://stackoverflow.com/a/42508185/1663648 - // https://stackoverflow.com/a/30407959/1663648 - if (imageUrl.startsWith("chrome-extension")) { - return Promise.resolve(imageUrl); - } - - return new Promise((resolve, reject) => { - if (this.imageCache[imageUrl]) { - resolve(this.imageCache[imageUrl]); - return; - } - - const onError = function(e) { - console.warn("getNotificationImageUrl", imageUrl, e); - - // hope for the best... - resolve(imageUrl); - }; - - fetch(imageUrl).then((response) => { - return response.blob() - }).then((blob) => { - var blobReader = new FileReader(); - - blobReader.onerror = onError; - blobReader.onload = (e) => { - this.imageCache[imageUrl] = e.target.result; - resolve(e.target.result); - - // Clean up the memory later.. - setTimeout(() => { - delete this.imageCache[imageUrl]; - }, 5 * 60 * 1000); - }; - - blobReader.readAsDataURL(blob); - }).catch(onError); - }); - } -}; - -Extension.NotificationService.Singleton = new Extension.NotificationService(Extension.Singleton); - -if (Extension.Singleton.executionContextType == Extension.ExecutionContextTypes.background) { - //Extension.NotificationService.Singleton.onNotificationCreated.addEventListener(console.log.bind(console, "onNotificationCreated")); - //Extension.NotificationService.Singleton.onNotificationClosed.addEventListener(console.log.bind(console, "onNotificationClosed")); - //Extension.NotificationService.Singleton.onNotificationClicked.addEventListener(console.log.bind(console, "onNotificationClicked")); - //Extension.NotificationService.Singleton.onNotificationButtonClicked.addEventListener(console.log.bind(console, "onNotificationButtonClicked")); - - chrome.notifications.onClosed.addListener(function (notificationId, byUser) { - if (byUser) { - Extension.NotificationService.Singleton.closeNotification(notificationId); - } - }); - - chrome.notifications.onClicked.addListener(function (notificationId) { - Extension.NotificationService.Singleton.clickNotification(notificationId); - }); - - chrome.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) { - Extension.NotificationService.Singleton.clickNotificationButton(notificationId, buttonIndex); - }); -} diff --git a/manifest.json b/manifest.json index fc0526d..308110a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "2.4.228", + "version": "2.4.234", "name": "Roblox+", "short_name": "Roblox+", "description": "Adds features and notifiers made by WebGL3D to the Roblox website", @@ -47,7 +47,6 @@ "/js/vanilla/extension/event.js", "/js/vanilla/extension/backgroundService.js", "/js/vanilla/extension/reload.js", - "/js/vanilla/extension/notificationService.js", "/js/vanilla/extension/storage.js", "/js/vanilla/queuedPromise.js", "/js/vanilla/cachedPromise.js", @@ -240,9 +239,7 @@ "permissions": [ "alarms", "gcm", - "contextMenus", "declarativeNetRequest", - "tts", "notifications", "storage", "https://*.roblox.com/*",