diff --git a/app/index.js b/app/index.js index 819b367..17a6c07 100644 --- a/app/index.js +++ b/app/index.js @@ -1,26 +1,24 @@ -'use strict'; +"use strict"; // Core modules -const fs = require('fs'); +const fs = require("fs"); // Public modules from npm -const urlExist = require('url-exist'); +const urlExist = require("url-exist"); // Modules from file -const shared = require('./scripts/shared.js'); -const constURLs = require('./scripts/costants/urls.js'); -const constSelectors = require('./scripts/costants/css-selectors.js'); +const shared = require("./scripts/shared.js"); +const constURLs = require("./scripts/costants/urls.js"); +const constSelectors = require("./scripts/costants/css-selectors.js"); +const { isStringAValidURL } = require("./scripts/urls-helper.js"); +const gameScraper = require("./scripts/game-scraper.js"); const { - isStringAValidURL -} = require('./scripts/urls-helper.js'); -const gameScraper = require('./scripts/game-scraper.js'); -const { - prepareBrowser, - preparePage -} = require('./scripts/puppeteer-helper.js'); -const GameInfo = require('./scripts/classes/game-info.js'); -const LoginResult = require('./scripts/classes/login-result.js'); -const UserData = require('./scripts/classes/user-data.js'); + prepareBrowser, + preparePage, +} = require("./scripts/puppeteer-helper.js"); +const GameInfo = require("./scripts/classes/game-info.js"); +const LoginResult = require("./scripts/classes/login-result.js"); +const UserData = require("./scripts/classes/user-data.js"); //#region Expose classes module.exports.GameInfo = GameInfo; @@ -31,47 +29,47 @@ module.exports.UserData = UserData; //#region Exposed properties /** * Shows log messages and other useful functions for module debugging. - * @param {Boolean} value + * @param {Boolean} value */ module.exports.debug = function (value) { - shared.debug = value; -} + shared.debug = value; +}; /** * @public * Indicates whether a user is logged in to the F95Zone platform or not. * @returns {String} */ module.exports.isLogged = function () { - return shared.isLogged; + return shared.isLogged; }; /** * @public - * If true, it opens a new browser for each request + * If true, it opens a new browser for each request * to the F95Zone platform, otherwise it reuses the same. * @returns {String} */ -module.exports.setIsolation = function(value) { - shared.isolation = value; -} +module.exports.setIsolation = function (value) { + shared.isolation = value; +}; /** * @public * Path to the cache directory * @returns {String} */ -module.exports.getCacheDir = function() { - return shared.cacheDir; -} +module.exports.getCacheDir = function () { + return shared.cacheDir; +}; /** * @public * Set path to the cache directory * @returns {String} */ -module.exports.setCacheDir = function(value) { - shared.cacheDir = value; +module.exports.setCacheDir = function (value) { + shared.cacheDir = value; - // Create directory if it doesn't exist - if (!fs.existsSync(shared.cacheDir)) fs.mkdirSync(shared.cacheDir); -} + // Create directory if it doesn't exist + if (!fs.existsSync(shared.cacheDir)) fs.mkdirSync(shared.cacheDir); +}; //#endregion Exposed properties //#region Global variables @@ -88,158 +86,163 @@ var _browser = null; * @returns {Promise} Result of the operation */ module.exports.login = async function (username, password) { - if (shared.isLogged) { - if (shared.debug) console.log("Already logged in"); - let result = new LoginResult(); - result.success = true; - result.message = 'Already logged in'; - return result; - } - - // If cookies are loaded, use them to authenticate - shared.cookies = loadCookies(); - if (shared.cookies !== null) { - if (shared.debug) console.log('Valid session, no need to re-authenticate'); - shared.isLogged = true; - let result = new LoginResult(); - result.success = true; - result.message = 'Logged with cookies'; - return result; - } - - // Else, log in throught browser - if (shared.debug) console.log('No saved sessions or expired session, login on the platform'); - - let browser = null; - if (shared.isolation) browser = await prepareBrowser(); - else { - if (_browser === null) _browser = await prepareBrowser(); - browser = _browser; - } - - let result = await loginF95(browser, username, password); - shared.isLogged = result.success; - - if (result.success) { - // Reload cookies - shared.cookies = loadCookies(); - if (shared.debug) console.log('User logged in through the platform'); - } else { - console.warn('Error during authentication: ' + result.message); - } - if (shared.isolation) await browser.close(); + if (shared.isLogged) { + if (shared.debug) console.log("Already logged in"); + let result = new LoginResult(); + result.success = true; + result.message = "Already logged in"; return result; -} + } + + // If cookies are loaded, use them to authenticate + shared.cookies = loadCookies(); + if (shared.cookies !== null) { + if (shared.debug) console.log("Valid session, no need to re-authenticate"); + shared.isLogged = true; + let result = new LoginResult(); + result.success = true; + result.message = "Logged with cookies"; + return result; + } + + // Else, log in throught browser + if (shared.debug) + console.log("No saved sessions or expired session, login on the platform"); + + let browser = null; + if (shared.isolation) browser = await prepareBrowser(); + else { + if (_browser === null) _browser = await prepareBrowser(); + browser = _browser; + } + + let result = await loginF95(browser, username, password); + shared.isLogged = result.success; + + if (result.success) { + // Reload cookies + shared.cookies = loadCookies(); + if (shared.debug) console.log("User logged in through the platform"); + } else { + console.warn("Error during authentication: " + result.message); + } + if (shared.isolation) await browser.close(); + return result; +}; /** * @public - * This method loads the main data from the F95 portal - * used to provide game information. You **must** be logged + * This method loads the main data from the F95 portal + * used to provide game information. You **must** be logged * in to the portal before calling this method. * @returns {Promise} Result of the operation */ module.exports.loadF95BaseData = async function () { - if (!shared.isLogged) { - console.warn('User not authenticated, unable to continue'); - return false; - } + if (!shared.isLogged) { + console.warn("User not authenticated, unable to continue"); + return false; + } - if (shared.debug) console.log('Loading base data...'); + if (shared.debug) console.log("Loading base data..."); - // Prepare a new web page - let browser = null; - if (shared.isolation) browser = await prepareBrowser(); - else { - if (_browser === null) _browser = await prepareBrowser(); - browser = _browser; - } + // Prepare a new web page + let browser = null; + if (shared.isolation) browser = await prepareBrowser(); + else { + if (_browser === null) _browser = await prepareBrowser(); + browser = _browser; + } - let page = await preparePage(browser); // Set new isolated page - await page.setCookie(...shared.cookies); // Set cookies to avoid login - - // Go to latest update page and wait for it to load - await page.goto(constURLs.F95_LATEST_UPDATES, { - waitUntil: shared.WAIT_STATEMENT - }); + let page = await preparePage(browser); // Set new isolated page + await page.setCookie(...shared.cookies); // Set cookies to avoid login - // Obtain engines (disc/online) - await page.waitForSelector(constSelectors.ENGINE_ID_SELECTOR); - shared.engines = await loadValuesFromLatestPage(page, - shared.enginesCachePath, - constSelectors.ENGINE_ID_SELECTOR, - 'engines'); + // Go to latest update page and wait for it to load + await page.goto(constURLs.F95_LATEST_UPDATES, { + waitUntil: shared.WAIT_STATEMENT, + }); - // Obtain statuses (disc/online) - await page.waitForSelector(constSelectors.STATUS_ID_SELECTOR); - shared.statuses = await loadValuesFromLatestPage(page, - shared.statusesCachePath, - constSelectors.STATUS_ID_SELECTOR, - 'statuses'); + // Obtain engines (disc/online) + await page.waitForSelector(constSelectors.ENGINE_ID_SELECTOR); + shared.engines = await loadValuesFromLatestPage( + page, + shared.enginesCachePath, + constSelectors.ENGINE_ID_SELECTOR, + "engines" + ); - if (shared.isolation) await browser.close(); - if (shared.debug) console.log('Base data loaded'); - return true; -} + // Obtain statuses (disc/online) + await page.waitForSelector(constSelectors.STATUS_ID_SELECTOR); + shared.statuses = await loadValuesFromLatestPage( + page, + shared.statusesCachePath, + constSelectors.STATUS_ID_SELECTOR, + "statuses" + ); + + if (shared.isolation) await browser.close(); + if (shared.debug) console.log("Base data loaded"); + return true; +}; /** * @public - * Returns the currently online version of the specified game. + * Returns the currently online version of the specified game. * You **must** be logged in to the portal before calling this method. * @param {GameInfo} info Information about the game to get the version for * @returns {Promise} Currently online version of the specified game */ module.exports.getGameVersion = async function (info) { - if (!shared.isLogged) { - console.warn('user not authenticated, unable to continue'); - return info.version; - } + if (!shared.isLogged) { + console.warn("user not authenticated, unable to continue"); + return info.version; + } - let urlExists = await urlExist(info.f95url.toString()); + let urlExists = await urlExist(info.f95url.toString()); - // F95 change URL at every game update, so if the URL is the same no update is available - if (urlExists) return info.version; - else return await module.exports.getGameData(info.name, info.isMod).version; -} + // F95 change URL at every game update, so if the URL is the same no update is available + if (urlExists) return info.version; + else return await module.exports.getGameData(info.name, info.isMod).version; +}; /** * @public * Starting from the name, it gets all the information about the game you are looking for. * You **must** be logged in to the portal before calling this method. * @param {String} name Name of the game searched * @param {Boolean} includeMods Indicates whether to also take mods into account when searching - * @returns {Promise} List of information obtained where each item corresponds to + * @returns {Promise} List of information obtained where each item corresponds to * an identified game (in the case of homonymy). If no games were found, null is returned */ module.exports.getGameData = async function (name, includeMods) { - if (!shared.isLogged) { - console.warn('user not authenticated, unable to continue'); - return null; - } + if (!shared.isLogged) { + console.warn("user not authenticated, unable to continue"); + return null; + } - // Gets the search results of the game being searched for - let browser = null; - if (shared.isolation) browser = await prepareBrowser(); - else { - if (_browser === null) _browser = await prepareBrowser(); - browser = _browser; - } - let urlList = await getSearchGameResults(browser, name); + // Gets the search results of the game being searched for + let browser = null; + if (shared.isolation) browser = await prepareBrowser(); + else { + if (_browser === null) _browser = await prepareBrowser(); + browser = _browser; + } + let urlList = await getSearchGameResults(browser, name); - // Process previous partial results - let promiseList = []; - for (let url of urlList) { - // Start looking for information - promiseList.push(gameScraper.getGameInfo(browser, url)); - } + // Process previous partial results + let promiseList = []; + for (let url of urlList) { + // Start looking for information + promiseList.push(gameScraper.getGameInfo(browser, url)); + } - // Filter for mods - let result = []; - for (let info of await Promise.all(promiseList)) { - // Skip mods if not required - if (info.isMod && !includeMods) continue; - else result.push(info); - } + // Filter for mods + let result = []; + for (let info of await Promise.all(promiseList)) { + // Skip mods if not required + if (info.isMod && !includeMods) continue; + else result.push(info); + } - if (shared.isolation) await browser.close(); - return result; -} + if (shared.isolation) await browser.close(); + return result; +}; /** * @public * Gets the data of the currently logged in user. @@ -247,58 +250,62 @@ module.exports.getGameData = async function (name, includeMods) { * @returns {Promise} Data of the user currently logged in or null if an error arise */ module.exports.getUserData = async function () { - if (!shared.isLogged) { - console.warn('user not authenticated, unable to continue'); - return null; - } + if (!shared.isLogged) { + console.warn("user not authenticated, unable to continue"); + return null; + } - // Prepare a new web page - let browser = null; - if (shared.isolation) browser = await prepareBrowser(); - else { - if (_browser === null) _browser = await prepareBrowser(); - browser = _browser; - } - let page = await preparePage(browser); // Set new isolated page - await page.setCookie(...shared.cookies); // Set cookies to avoid login - await page.goto(constURLs.F95_BASE_URL); // Go to base page + // Prepare a new web page + let browser = null; + if (shared.isolation) browser = await prepareBrowser(); + else { + if (_browser === null) _browser = await prepareBrowser(); + browser = _browser; + } + let page = await preparePage(browser); // Set new isolated page + await page.setCookie(...shared.cookies); // Set cookies to avoid login + await page.goto(constURLs.F95_BASE_URL); // Go to base page - // Explicitly wait for the required items to load - await page.waitForSelector(constSelectors.USERNAME_ELEMENT); - await page.waitForSelector(constSelectors.AVATAR_PIC); + // Explicitly wait for the required items to load + await page.waitForSelector(constSelectors.USERNAME_ELEMENT); + await page.waitForSelector(constSelectors.AVATAR_PIC); - let threads = getUserWatchedGameThreads(browser); + let threads = getUserWatchedGameThreads(browser); - let username = await page.evaluate( /* istanbul ignore next */ (selector) => - document.querySelector(selector).innerText, - constSelectors.USERNAME_ELEMENT); + let username = await page.evaluate( + /* istanbul ignore next */ (selector) => + document.querySelector(selector).innerText, + constSelectors.USERNAME_ELEMENT + ); - let avatarSrc = await page.evaluate( /* istanbul ignore next */ (selector) => - document.querySelector(selector).getAttribute('src'), - constSelectors.AVATAR_PIC); + let avatarSrc = await page.evaluate( + /* istanbul ignore next */ (selector) => + document.querySelector(selector).getAttribute("src"), + constSelectors.AVATAR_PIC + ); - let ud = new UserData(); - ud.username = username; - ud.avatarSrc = isStringAValidURL(avatarSrc) ? new URL(avatarSrc) : null; - ud.watchedThreads = await threads; + let ud = new UserData(); + ud.username = username; + ud.avatarSrc = isStringAValidURL(avatarSrc) ? new URL(avatarSrc) : null; + ud.watchedThreads = await threads; - await page.close(); - if (shared.isolation) await browser.close(); + await page.close(); + if (shared.isolation) await browser.close(); - return ud; -} + return ud; +}; /** * @public * Logout from the current user. * You **must** be logged in to the portal before calling this method. */ -module.exports.logout = function() { - if (!shared.isLogged) { - console.warn('user not authenticated, unable to continue'); - return; - } - shared.isLogged = false; -} +module.exports.logout = function () { + if (!shared.isLogged) { + console.warn("user not authenticated, unable to continue"); + return; + } + shared.isLogged = false; +}; //#endregion //#region Private methods @@ -311,21 +318,20 @@ module.exports.logout = function() { * @return {object[]} List of dictionaries or null if cookies don't exist */ function loadCookies() { - // Check the existence of the cookie file - if (fs.existsSync(shared.cookiesCachePath)) { - // Read cookies - let cookiesJSON = fs.readFileSync(shared.cookiesCachePath); - let cookies = JSON.parse(cookiesJSON); + // Check the existence of the cookie file + if (fs.existsSync(shared.cookiesCachePath)) { + // Read cookies + let cookiesJSON = fs.readFileSync(shared.cookiesCachePath); + let cookies = JSON.parse(cookiesJSON); - // Check if the cookies have expired - for (let cookie of cookies) { - if (isCookieExpired(cookie)) return null; - } + // Check if the cookies have expired + for (let cookie of cookies) { + if (isCookieExpired(cookie)) return null; + } - // Cookies loaded and verified - return cookies; - - } else return null; + // Cookies loaded and verified + return cookies; + } else return null; } /** * @private @@ -334,31 +340,34 @@ function loadCookies() { * @returns {Boolean} true if the cookie has expired, false otherwise */ function isCookieExpired(cookie) { - // Local variables - let expiredCookies = false; + // Local variables + let expiredCookies = false; - // Ignore cookies that never expire - let expirationUnixTimestamp = cookie['expire']; + // Ignore cookies that never expire + let expirationUnixTimestamp = cookie["expire"]; - if (expirationUnixTimestamp !== '-1') { - // Convert UNIX epoch timestamp to normal Date - let expirationDate = new Date(expirationUnixTimestamp * 1000); + if (expirationUnixTimestamp !== "-1") { + // Convert UNIX epoch timestamp to normal Date + let expirationDate = new Date(expirationUnixTimestamp * 1000); - if (expirationDate < Date.now()) { - if (shared.debug) console.log('Cookie ' + cookie['name'] + ' expired, you need to re-authenticate'); - expiredCookies = true; - } + if (expirationDate < Date.now()) { + if (shared.debug) + console.log( + "Cookie " + cookie["name"] + " expired, you need to re-authenticate" + ); + expiredCookies = true; } + } - return expiredCookies; + return expiredCookies; } //#endregion Cookies functions //#region Latest Updates page parserer /** * @private - * If present, it reads the file containing the searched values (engines or states) - * from the disk, otherwise it connects to the F95 portal (at the page + * If present, it reads the file containing the searched values (engines or states) + * from the disk, otherwise it connects to the F95 portal (at the page * https://f95zone.to/latest) and downloads them. * @param {puppeteer.Page} page Page used to locate the required elements * @param {String} path Path to disk of the JSON file containing the data to read / write @@ -366,24 +375,34 @@ function isCookieExpired(cookie) { * @param {String} elementRequested Required element (engines or states) used to detail log messages * @returns {Promise} List of required values in uppercase */ -async function loadValuesFromLatestPage(page, path, selector, elementRequested) { - // If the values already exist they are loaded from disk without having to connect to F95 - if (shared.debug) console.log('Load ' + elementRequested + ' from disk...'); - if (fs.existsSync(path)) { - let valueJSON = fs.readFileSync(path); - return JSON.parse(valueJSON); - } +async function loadValuesFromLatestPage( + page, + path, + selector, + elementRequested +) { + // If the values already exist they are loaded from disk without having to connect to F95 + if (shared.debug) console.log("Load " + elementRequested + " from disk..."); + if (fs.existsSync(path)) { + let valueJSON = fs.readFileSync(path); + return JSON.parse(valueJSON); + } - // Otherwise, connect and download the data from the portal - if (shared.debug) console.log('No ' + elementRequested + ' cached, downloading...'); - let values = await getValuesFromLatestPage(page, selector, 'Getting ' + elementRequested + ' from page'); - fs.writeFileSync(path, JSON.stringify(values)); - return values; + // Otherwise, connect and download the data from the portal + if (shared.debug) + console.log("No " + elementRequested + " cached, downloading..."); + let values = await getValuesFromLatestPage( + page, + selector, + "Getting " + elementRequested + " from page" + ); + fs.writeFileSync(path, JSON.stringify(values)); + return values; } /** * @private - * Gets all the textual values of the elements present - * in the F95 portal page and identified by the selector + * Gets all the textual values of the elements present + * in the F95 portal page and identified by the selector * passed by parameter * @param {puppeteer.Page} page Page used to locate items specified by the selector * @param {String} selector CSS selector @@ -391,18 +410,20 @@ async function loadValuesFromLatestPage(page, path, selector, elementRequested) * @return {Promise} List of uppercase strings indicating the textual values of the elements identified by the selector */ async function getValuesFromLatestPage(page, selector, logMessage) { - if (shared.debug) console.log(logMessage); + if (shared.debug) console.log(logMessage); - let result = []; - let elements = await page.$$(selector); + let result = []; + let elements = await page.$$(selector); - for (let element of elements) { - let text = await element.evaluate( /* istanbul ignore next */ e => e.innerText); + for (let element of elements) { + let text = await element.evaluate( + /* istanbul ignore next */ (e) => e.innerText + ); - // Save as upper text for better match if used in query - result.push(text.toUpperCase()); - } - return result; + // Save as upper text for better match if used in query + result.push(text.toUpperCase()); + } + return result; } //#endregion @@ -416,52 +437,62 @@ async function getValuesFromLatestPage(page, selector, logMessage) { * @returns {Promise} Result of the operation */ async function loginF95(browser, username, password) { - let page = await preparePage(browser); // Set new isolated page - await page.goto(constURLs.F95_LOGIN_URL); // Go to login page + let page = await preparePage(browser); // Set new isolated page + await page.goto(constURLs.F95_LOGIN_URL); // Go to login page - // Explicitly wait for the required items to load - await page.waitForSelector(constSelectors.USERNAME_INPUT); - await page.waitForSelector(constSelectors.PASSWORD_INPUT); - await page.waitForSelector(constSelectors.LOGIN_BUTTON); - await page.type(constSelectors.USERNAME_INPUT, username); // Insert username - await page.type(constSelectors.PASSWORD_INPUT, password); // Insert password - await page.click(constSelectors.LOGIN_BUTTON); // Click on the login button - await page.waitForNavigation({ - waitUntil: shared.WAIT_STATEMENT - }); // Wait for page to load + // Explicitly wait for the required items to load + await page.waitForSelector(constSelectors.USERNAME_INPUT); + await page.waitForSelector(constSelectors.PASSWORD_INPUT); + await page.waitForSelector(constSelectors.LOGIN_BUTTON); + await page.type(constSelectors.USERNAME_INPUT, username); // Insert username + await page.type(constSelectors.PASSWORD_INPUT, password); // Insert password + await page.click(constSelectors.LOGIN_BUTTON); // Click on the login button + await page.waitForNavigation({ + waitUntil: shared.WAIT_STATEMENT, + }); // Wait for page to load - // Prepare result - let result = new LoginResult(); + // Prepare result + let result = new LoginResult(); - // Check if the user is logged in - result.success = await page.evaluate( /* istanbul ignore next */ (selector) => + // Check if the user is logged in + result.success = await page.evaluate( + /* istanbul ignore next */ (selector) => + document.querySelector(selector) !== null, + constSelectors.AVATAR_INFO + ); + + // Save cookies to avoid re-auth + if (result.success) { + let c = await page.cookies(); + fs.writeFileSync(shared.cookiesCachePath, JSON.stringify(c)); + result.message = "Authentication successful"; + } + // Obtain the error message + else if ( + await page.evaluate( + /* istanbul ignore next */ (selector) => document.querySelector(selector) !== null, - constSelectors.AVATAR_INFO); + constSelectors.LOGIN_MESSAGE_ERROR + ) + ) { + let errorMessage = await page.evaluate( + /* istanbul ignore next */ (selector) => + document.querySelector(selector).innerText, + constSelectors.LOGIN_MESSAGE_ERROR + ); - // Save cookies to avoid re-auth - if (result.success) { - let c = await page.cookies(); - fs.writeFileSync(shared.cookiesCachePath, JSON.stringify(c)); - result.message = 'Authentication successful'; - } - // Obtain the error message - else if (await page.evaluate( /* istanbul ignore next */ (selector) => - document.querySelector(selector) !== null, - constSelectors.LOGIN_MESSAGE_ERROR)) { - let errorMessage = await page.evaluate( /* istanbul ignore next */ (selector) => - document.querySelector(selector).innerText, - constSelectors.LOGIN_MESSAGE_ERROR); + if (errorMessage === "Incorrect password. Please try again.") { + result.message = "Incorrect password"; + } else if ( + errorMessage === + "The requested user '" + username + "' could not be found." + ) { + result.message = "Incorrect username"; + } else result.message = errorMessage; + } else result.message = "Unknown error"; - if (errorMessage === 'Incorrect password. Please try again.') { - result.message = 'Incorrect password'; - } else if (errorMessage === "The requested user '" + username + "' could not be found.") { - result.message = 'Incorrect username'; - } else result.message = errorMessage; - - } else result.message = "Unknown error"; - - await page.close(); // Close the page - return result; + await page.close(); // Close the page + return result; } /** * @private @@ -470,55 +501,60 @@ async function loginF95(browser, username, password) { * @returns {Promise} URL list */ async function getUserWatchedGameThreads(browser) { - let page = await preparePage(browser); // Set new isolated page - await page.goto(constURLs.F95_WATCHED_THREADS); // Go to the thread page + let page = await preparePage(browser); // Set new isolated page + await page.goto(constURLs.F95_WATCHED_THREADS); // Go to the thread page - // Explicitly wait for the required items to load - await page.waitForSelector(constSelectors.WATCHED_THREAD_FILTER_POPUP_BUTTON); + // Explicitly wait for the required items to load + await page.waitForSelector(constSelectors.WATCHED_THREAD_FILTER_POPUP_BUTTON); - // Show the popup - await page.click(constSelectors.WATCHED_THREAD_FILTER_POPUP_BUTTON); - await page.waitForSelector(constSelectors.UNREAD_THREAD_CHECKBOX); - await page.waitForSelector(constSelectors.ONLY_GAMES_THREAD_OPTION); - await page.waitForSelector(constSelectors.FILTER_THREADS_BUTTON); + // Show the popup + await page.click(constSelectors.WATCHED_THREAD_FILTER_POPUP_BUTTON); + await page.waitForSelector(constSelectors.UNREAD_THREAD_CHECKBOX); + await page.waitForSelector(constSelectors.ONLY_GAMES_THREAD_OPTION); + await page.waitForSelector(constSelectors.FILTER_THREADS_BUTTON); - // Set the filters - await page.evaluate( /* istanbul ignore next */ (selector) => - document.querySelector(selector).removeAttribute('checked'), - constSelectors.UNREAD_THREAD_CHECKBOX); // Also read the threads already read + // Set the filters + await page.evaluate( + /* istanbul ignore next */ (selector) => + document.querySelector(selector).removeAttribute("checked"), + constSelectors.UNREAD_THREAD_CHECKBOX + ); // Also read the threads already read - await page.click(constSelectors.ONLY_GAMES_THREAD_OPTION); + await page.click(constSelectors.ONLY_GAMES_THREAD_OPTION); - // Filter the threads - await page.click(constSelectors.FILTER_THREADS_BUTTON); - await page.waitForSelector(constSelectors.WATCHED_THREAD_URLS); + // Filter the threads + await page.click(constSelectors.FILTER_THREADS_BUTTON); + await page.waitForSelector(constSelectors.WATCHED_THREAD_URLS); - // Get the threads urls - let urls = []; - let nextPageExists = false; - do { - // Get all the URLs - for (let handle of await page.$$(constSelectors.WATCHED_THREAD_URLS)) { - let src = await page.evaluate( /* istanbul ignore next */ (element) => element.href, handle); - // If 'unread' is left, it will redirect to the last unread post - let url = new URL(src.replace('/unread', '')); - urls.push(url); - } - - nextPageExists = await page.evaluate( /* istanbul ignore next */ (selector) => - document.querySelector(selector), - constSelectors.WATCHED_THREAD_NEXT_PAGE); - - // Click to next page - if (nextPageExists) { - await page.click(constSelectors.WATCHED_THREAD_NEXT_PAGE); - await page.waitForSelector(constSelectors.WATCHED_THREAD_URLS); - } + // Get the threads urls + let urls = []; + let nextPageExists = false; + do { + // Get all the URLs + for (let handle of await page.$$(constSelectors.WATCHED_THREAD_URLS)) { + let src = await page.evaluate( + /* istanbul ignore next */ (element) => element.href, + handle + ); + // If 'unread' is left, it will redirect to the last unread post + let url = new URL(src.replace("/unread", "")); + urls.push(url); } - while (nextPageExists); - await page.close(); - return urls; + nextPageExists = await page.evaluate( + /* istanbul ignore next */ (selector) => document.querySelector(selector), + constSelectors.WATCHED_THREAD_NEXT_PAGE + ); + + // Click to next page + if (nextPageExists) { + await page.click(constSelectors.WATCHED_THREAD_NEXT_PAGE); + await page.waitForSelector(constSelectors.WATCHED_THREAD_URLS); + } + } while (nextPageExists); + + await page.close(); + return urls; } //#endregion User @@ -531,42 +567,42 @@ async function getUserWatchedGameThreads(browser) { * @returns {Promise} List of URL of possible games obtained from the preliminary research on the F95 portal */ async function getSearchGameResults(browser, gamename) { - if (shared.debug) console.log('Searching ' + gamename + ' on F95Zone'); + if (shared.debug) console.log("Searching " + gamename + " on F95Zone"); - let page = await preparePage(browser); // Set new isolated page - await page.setCookie(...shared.cookies); // Set cookies to avoid login - await page.goto(constURLs.F95_SEARCH_URL, { - waitUntil: shared.WAIT_STATEMENT - }); // Go to the search form and wait for it + let page = await preparePage(browser); // Set new isolated page + await page.setCookie(...shared.cookies); // Set cookies to avoid login + await page.goto(constURLs.F95_SEARCH_URL, { + waitUntil: shared.WAIT_STATEMENT, + }); // Go to the search form and wait for it - // Explicitly wait for the required items to load - await page.waitForSelector(constSelectors.SEARCH_FORM_TEXTBOX); - await page.waitForSelector(constSelectors.TITLE_ONLY_CHECKBOX); - await page.waitForSelector(constSelectors.SEARCH_BUTTON); + // Explicitly wait for the required items to load + await page.waitForSelector(constSelectors.SEARCH_FORM_TEXTBOX); + await page.waitForSelector(constSelectors.TITLE_ONLY_CHECKBOX); + await page.waitForSelector(constSelectors.SEARCH_BUTTON); - await page.type(constSelectors.SEARCH_FORM_TEXTBOX, gamename) // Type the game we desire - await page.click(constSelectors.TITLE_ONLY_CHECKBOX) // Select only the thread with the game in the titles - await page.click(constSelectors.SEARCH_BUTTON); // Execute search - await page.waitForNavigation({ - waitUntil: shared.WAIT_STATEMENT - }); // Wait for page to load + await page.type(constSelectors.SEARCH_FORM_TEXTBOX, gamename); // Type the game we desire + await page.click(constSelectors.TITLE_ONLY_CHECKBOX); // Select only the thread with the game in the titles + await page.click(constSelectors.SEARCH_BUTTON); // Execute search + await page.waitForNavigation({ + waitUntil: shared.WAIT_STATEMENT, + }); // Wait for page to load - // Select all conversation titles - let threadTitleList = await page.$$(constSelectors.THREAD_TITLE); + // Select all conversation titles + let threadTitleList = await page.$$(constSelectors.THREAD_TITLE); - // For each title extract the info about the conversation - if (shared.debug) console.log('Extracting info from conversation titles'); - let results = []; - for (let title of threadTitleList) { - let gameUrl = await getOnlyGameThreads(page, title); + // For each title extract the info about the conversation + if (shared.debug) console.log("Extracting info from conversation titles"); + let results = []; + for (let title of threadTitleList) { + let gameUrl = await getOnlyGameThreads(page, title); - // Append the game's informations - if (gameUrl !== null) results.push(gameUrl); - } - if (shared.debug) console.log('Find ' + results.length + ' conversations'); - await page.close(); // Close the page + // Append the game's informations + if (gameUrl !== null) results.push(gameUrl); + } + if (shared.debug) console.log("Find " + results.length + " conversations"); + await page.close(); // Close the page - return results; + return results; } /** * @private @@ -576,23 +612,29 @@ async function getSearchGameResults(browser, gamename) { * @return {Promise} URL of the game/mod */ async function getOnlyGameThreads(page, titleHandle) { - const GAME_RECOMMENDATION_PREFIX = 'RECOMMENDATION'; + const GAME_RECOMMENDATION_PREFIX = "RECOMMENDATION"; - // Get the URL of the thread from the title - let relativeURLThread = await page.evaluate( /* istanbul ignore next */ (element) => element.querySelector('a').href, titleHandle); - let url = new URL(relativeURLThread, constURLs.F95_BASE_URL); + // Get the URL of the thread from the title + let relativeURLThread = await page.evaluate( + /* istanbul ignore next */ (element) => element.querySelector("a").href, + titleHandle + ); + let url = new URL(relativeURLThread, constURLs.F95_BASE_URL); - // Parse prefixes to ignore game recommendation - for (let element of await titleHandle.$$('span[dir="auto"]')) { - // Elaborate the prefixes - let prefix = await page.evaluate( /* istanbul ignore next */ element => element.textContent.toUpperCase(), element); - prefix = prefix.replace('[', '').replace(']', ''); + // Parse prefixes to ignore game recommendation + for (let element of await titleHandle.$$('span[dir="auto"]')) { + // Elaborate the prefixes + let prefix = await page.evaluate( + /* istanbul ignore next */ (element) => element.textContent.toUpperCase(), + element + ); + prefix = prefix.replace("[", "").replace("]", ""); - // This is not a game nor a mod, we can exit - if (prefix === GAME_RECOMMENDATION_PREFIX) return null; - } - return url; + // This is not a game nor a mod, we can exit + if (prefix === GAME_RECOMMENDATION_PREFIX) return null; + } + return url; } //#endregion Game search -//#endregion Private methods \ No newline at end of file +//#endregion Private methods diff --git a/test/index-test.js b/test/index-test.js index f1119f1..ec427c6 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -1,4 +1,4 @@ -"use strict" +"use strict"; const expect = require("chai").expect; const F95API = require("../app/index"); @@ -15,166 +15,167 @@ const FAKE_PASSWORD = "fake_password"; F95API.setIsolation(true); describe("Login without cookies", function () { - //#region Set-up - this.timeout(30000); // All tests in this suite get 30 seconds before timeout + //#region Set-up + this.timeout(30000); // All tests in this suite get 30 seconds before timeout - beforeEach("Remove all cookies", function () { - // Runs before each test in this block - if (fs.existsSync(COOKIES_SAVE_PATH)) fs.unlinkSync(COOKIES_SAVE_PATH); - F95API.logout(); - }); - //#endregion Set-up + beforeEach("Remove all cookies", function () { + // Runs before each test in this block + if (fs.existsSync(COOKIES_SAVE_PATH)) fs.unlinkSync(COOKIES_SAVE_PATH); + F95API.logout(); + }); + //#endregion Set-up - it("Test with valid credentials", async function () { - const result = await F95API.login(USERNAME, PASSWORD); - expect(result.success).to.be.true; - expect(result.message).equal("Authentication successful"); - }); - it("Test with invalid username", async function () { - const result = await F95API.login(FAKE_USERNAME, FAKE_PASSWORD); - expect(result.success).to.be.false; - expect(result.message).to.equal("Incorrect username"); - }); - it("Test with invalid password", async function () { - const result = await F95API.login(USERNAME, FAKE_PASSWORD); - expect(result.success).to.be.false; - expect(result.message).to.equal("Incorrect password"); - }); - it("Test with invalid credentials", async function () { - const result = await F95API.login(FAKE_USERNAME, FAKE_PASSWORD); - expect(result.success).to.be.false; - expect(result.message).to.equal("Incorrect username"); // It should first check the username - }); + it("Test with valid credentials", async function () { + const result = await F95API.login(USERNAME, PASSWORD); + expect(result.success).to.be.true; + expect(result.message).equal("Authentication successful"); + }); + it("Test with invalid username", async function () { + const result = await F95API.login(FAKE_USERNAME, FAKE_PASSWORD); + expect(result.success).to.be.false; + expect(result.message).to.equal("Incorrect username"); + }); + it("Test with invalid password", async function () { + const result = await F95API.login(USERNAME, FAKE_PASSWORD); + expect(result.success).to.be.false; + expect(result.message).to.equal("Incorrect password"); + }); + it("Test with invalid credentials", async function () { + const result = await F95API.login(FAKE_USERNAME, FAKE_PASSWORD); + expect(result.success).to.be.false; + expect(result.message).to.equal("Incorrect username"); // It should first check the username + }); }); describe("Login with cookies", function () { - //#region Set-up - this.timeout(30000); // All tests in this suite get 30 seconds before timeout + //#region Set-up + this.timeout(30000); // All tests in this suite get 30 seconds before timeout - before("Log in to create cookies then logout", async function () { - // Runs once before the first test in this block - if (!fs.existsSync(COOKIES_SAVE_PATH)) await F95API.login(USERNAME, PASSWORD); // Download cookies - F95API.logout(); - }); - //#endregion Set-up + before("Log in to create cookies then logout", async function () { + // Runs once before the first test in this block + if (!fs.existsSync(COOKIES_SAVE_PATH)) + await F95API.login(USERNAME, PASSWORD); // Download cookies + F95API.logout(); + }); + //#endregion Set-up - it("Test with valid credentials", async function () { - const result = await F95API.login(USERNAME, PASSWORD); - expect(result.success).to.be.true; - expect(result.message).equal("Logged with cookies"); - }); + it("Test with valid credentials", async function () { + const result = await F95API.login(USERNAME, PASSWORD); + expect(result.success).to.be.true; + expect(result.message).equal("Logged with cookies"); + }); }); describe("Load base data without cookies", function () { - //#region Set-up - this.timeout(30000); // All tests in this suite get 30 seconds before timeout + //#region Set-up + this.timeout(30000); // All tests in this suite get 30 seconds before timeout - before("Delete cache if exists", function () { - // Runs once before the first test in this block - if (fs.existsSync(ENGINES_SAVE_PATH)) fs.unlinkSync(ENGINES_SAVE_PATH); - if (fs.existsSync(STATUSES_SAVE_PATH)) fs.unlinkSync(STATUSES_SAVE_PATH); - }); - //#endregion Set-up + before("Delete cache if exists", function () { + // Runs once before the first test in this block + if (fs.existsSync(ENGINES_SAVE_PATH)) fs.unlinkSync(ENGINES_SAVE_PATH); + if (fs.existsSync(STATUSES_SAVE_PATH)) fs.unlinkSync(STATUSES_SAVE_PATH); + }); + //#endregion Set-up - it("With login", async function () { - let loginResult = await F95API.login(USERNAME, PASSWORD); - expect(loginResult.success).to.be.true; + it("With login", async function () { + let loginResult = await F95API.login(USERNAME, PASSWORD); + expect(loginResult.success).to.be.true; - let result = await F95API.loadF95BaseData(); + let result = await F95API.loadF95BaseData(); - let enginesCacheExists = fs.existsSync(ENGINES_SAVE_PATH); - let statusesCacheExists = fs.existsSync(STATUSES_SAVE_PATH); + let enginesCacheExists = fs.existsSync(ENGINES_SAVE_PATH); + let statusesCacheExists = fs.existsSync(STATUSES_SAVE_PATH); - expect(result).to.be.true; - expect(enginesCacheExists).to.be.true; - expect(statusesCacheExists).to.be.true; - }); + expect(result).to.be.true; + expect(enginesCacheExists).to.be.true; + expect(statusesCacheExists).to.be.true; + }); - it("Without login", async function () { - F95API.logout(); - let result = await F95API.loadF95BaseData(); - expect(result).to.be.false; - }); + it("Without login", async function () { + F95API.logout(); + let result = await F95API.loadF95BaseData(); + expect(result).to.be.false; + }); }); describe("Search game data", function () { - //#region Set-up - this.timeout(60000); // All tests in this suite get 60 seconds before timeout + //#region Set-up + this.timeout(60000); // All tests in this suite get 60 seconds before timeout - beforeEach("Prepare API", function () { - // Runs once before the first test in this block - F95API.logout(); - }); - //#endregion Set-up + beforeEach("Prepare API", function () { + // Runs once before the first test in this block + F95API.logout(); + }); + //#endregion Set-up - it("Search game when logged", async function () { - const loginResult = await F95API.login(USERNAME, PASSWORD); - expect(loginResult.success).to.be.true; + it("Search game when logged", async function () { + const loginResult = await F95API.login(USERNAME, PASSWORD); + expect(loginResult.success).to.be.true; - const loadResult = await F95API.loadF95BaseData(); - expect(loadResult).to.be.true; + const loadResult = await F95API.loadF95BaseData(); + expect(loadResult).to.be.true; - // This test depend on the data on F95Zone at - // https://f95zone.to/threads/kingdom-of-deception-v0-10-8-hreinn-games.2733/ - const result = (await F95API.getGameData("Kingdom of Deception", false))[0]; - let src = "https://attachments.f95zone.to/2018/09/162821_f9nXfwF.png"; + // This test depend on the data on F95Zone at + // https://f95zone.to/threads/kingdom-of-deception-v0-10-8-hreinn-games.2733/ + const result = (await F95API.getGameData("Kingdom of Deception", false))[0]; + let src = "https://attachments.f95zone.to/2018/09/162821_f9nXfwF.png"; - // Test only the main information - expect(result.name).to.equal("Kingdom of Deception"); - expect(result.author).to.equal("Hreinn Games"); - expect(result.isMod, "Should be false").to.be.false; - expect(result.engine).to.equal("REN'PY"); - // expect(result.previewSource).to.equal(src); could be null -> Why sometimes doesn't get the image? - }); - it("Search game when not logged", async function () { - const result = await F95API.getGameData("Kingdom of Deception", false); - expect(result, "Without being logged should return null").to.be.null; - }); + // Test only the main information + expect(result.name).to.equal("Kingdom of Deception"); + expect(result.author).to.equal("Hreinn Games"); + expect(result.isMod, "Should be false").to.be.false; + expect(result.engine).to.equal("REN'PY"); + // expect(result.previewSource).to.equal(src); could be null -> Why sometimes doesn't get the image? + }); + it("Search game when not logged", async function () { + const result = await F95API.getGameData("Kingdom of Deception", false); + expect(result, "Without being logged should return null").to.be.null; + }); }); describe("Load user data", function () { - //#region Set-up - this.timeout(30000); // All tests in this suite get 30 seconds before timeout - //#endregion Set-up + //#region Set-up + this.timeout(30000); // All tests in this suite get 30 seconds before timeout + //#endregion Set-up - it("Retrieve when logged", async function () { - // Login - await F95API.login(USERNAME, PASSWORD); + it("Retrieve when logged", async function () { + // Login + await F95API.login(USERNAME, PASSWORD); - // Then retrieve user data - let data = await F95API.getUserData(); + // Then retrieve user data + let data = await F95API.getUserData(); - expect(data).to.exist; - expect(data.username).to.equal(USERNAME); - }); - it("Retrieve when not logged", async function () { - // Logout - F95API.logout(); + expect(data).to.exist; + expect(data.username).to.equal(USERNAME); + }); + it("Retrieve when not logged", async function () { + // Logout + F95API.logout(); - // Try to retrieve user data - let data = await F95API.getUserData(); + // Try to retrieve user data + let data = await F95API.getUserData(); - expect(data).to.be.null; - }); + expect(data).to.be.null; + }); }); describe("Check game version", function () { - //#region Set-up - this.timeout(30000); // All tests in this suite get 30 seconds before timeout - //#endregion Set-up + //#region Set-up + this.timeout(30000); // All tests in this suite get 30 seconds before timeout + //#endregion Set-up - it("Get game version", async function () { - const loginResult = await F95API.login(USERNAME, PASSWORD); - expect(loginResult.success).to.be.true; + it("Get game version", async function () { + const loginResult = await F95API.login(USERNAME, PASSWORD); + expect(loginResult.success).to.be.true; - const loadResult = await F95API.loadF95BaseData(); - expect(loadResult).to.be.true; + const loadResult = await F95API.loadF95BaseData(); + expect(loadResult).to.be.true; - // This test depend on the data on F95Zone at - // https://f95zone.to/threads/kingdom-of-deception-v0-10-8-hreinn-games.2733/ - const result = (await F95API.getGameData("Kingdom of Deception", false))[0]; + // This test depend on the data on F95Zone at + // https://f95zone.to/threads/kingdom-of-deception-v0-10-8-hreinn-games.2733/ + const result = (await F95API.getGameData("Kingdom of Deception", false))[0]; - let version = await F95API.getGameVersion(result); - expect(version).to.be.equal(result.version); - }); -}); \ No newline at end of file + let version = await F95API.getGameVersion(result); + expect(version).to.be.equal(result.version); + }); +});