diff --git a/config/app.js b/config/app.js index ee07e8b4..89f4b120 100644 --- a/config/app.js +++ b/config/app.js @@ -18,6 +18,7 @@ const appConfig = { callback: process.env.CALLBACK || 'callback', }, apiUrl: process.env.API_URL || 'https://api.faforever.com', + wordpressUrl: process.env.WP_URL || 'https://direct.faforever.com', extractorInterval: process.env.EXTRACTOR_INTERVAL || 5, playerCountInterval: process.env.PLAYER_COUNT_INTERVAL || 15 } diff --git a/express.js b/express.js index 8f969c35..75d2878d 100644 --- a/express.js +++ b/express.js @@ -13,10 +13,12 @@ const newsRouter = require('./routes/views/news'); const staticMarkdownRouter = require('./routes/views/staticMarkdownRouter'); const leaderboardRouter = require('./routes/views/leaderboardRouter'); const authRouter = require('./routes/views/auth'); +const dataRouter = require('./routes/views/dataRouter'); app.locals.clanInvitations = {}; //Execute Middleware +app.use(middleware.injectServices); app.use(middleware.initLocals); app.use(middleware.clientChecks); @@ -74,6 +76,7 @@ app.use('/', authRouter) app.use('/', staticMarkdownRouter) app.use('/news', newsRouter) app.use('/leaderboards', leaderboardRouter) +app.use('/data', dataRouter) // --- UNPROTECTED ROUTES --- const appGetRouteArray = [ diff --git a/grunt/nodemon.js b/grunt/nodemon.js index 7039112d..631823a2 100644 --- a/grunt/nodemon.js +++ b/grunt/nodemon.js @@ -8,7 +8,6 @@ module.exports = { 'node_modules/**', 'grunt/**', 'Gruntfile.js', - 'public/js/app/members/**' ] } } diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..98493223 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +const config = { + setupFilesAfterEnv: ['./tests/setup.js'], +}; + +module.exports = config; diff --git a/lib/CacheService.js b/lib/CacheService.js new file mode 100644 index 00000000..c58be858 --- /dev/null +++ b/lib/CacheService.js @@ -0,0 +1,9 @@ +const NodeCache = require("node-cache"); +const cacheService = new NodeCache( + { + stdTTL: 300, // use 5 min for all caches if not changed with ttl + checkperiod: 600 // cleanup memory every 10 min + } +) + +module.exports = cacheService diff --git a/lib/LeaderboardService.js b/lib/LeaderboardService.js index 501de2c8..efc86c58 100644 --- a/lib/LeaderboardService.js +++ b/lib/LeaderboardService.js @@ -1,10 +1,11 @@ +const {MutexService} = require("./MutexService"); const leaderboardTTL = 60 * 60 // 1 hours ttl as a relaxation for https://github.com/FAForever/website/issues/482 class LeaderboardService { - constructor(cacheService, mutexService, leaderboardRepository, lockTimeout = 3000) { + constructor(cacheService, leaderboardRepository, lockTimeout = 3000) { this.lockTimeout = lockTimeout this.cacheService = cacheService - this.mutexService = mutexService + this.mutexService = new MutexService() this.leaderboardRepository = leaderboardRepository } diff --git a/lib/LeaderboardServiceFactory.js b/lib/LeaderboardServiceFactory.js index 0f8cef30..1d9ad1a0 100644 --- a/lib/LeaderboardServiceFactory.js +++ b/lib/LeaderboardServiceFactory.js @@ -1,16 +1,7 @@ const LeaderboardService = require("./LeaderboardService"); const LeaderboardRepository = require("./LeaderboardRepository"); -const {MutexService} = require("./MutexService"); -const NodeCache = require("node-cache"); const {Axios} = require("axios"); - -const leaderboardMutex = new MutexService() -const cacheService = new NodeCache( - { - stdTTL: 300, // use 5 min for all caches if not changed with ttl - checkperiod: 600 // cleanup memory every 10 min - } -); +const cacheService = require('./CacheService') module.exports = (javaApiBaseURL, token) => { const config = { @@ -19,5 +10,5 @@ module.exports = (javaApiBaseURL, token) => { }; const javaApiClient = new Axios(config) - return new LeaderboardService(cacheService, leaderboardMutex, new LeaderboardRepository(javaApiClient)) + return new LeaderboardService(cacheService, new LeaderboardRepository(javaApiClient)) } diff --git a/lib/Scheduler.js b/lib/Scheduler.js new file mode 100644 index 00000000..bf11aee6 --- /dev/null +++ b/lib/Scheduler.js @@ -0,0 +1,18 @@ +const {EventEmitter} = require("events") + +module.exports = class Scheduler extends EventEmitter { + constructor(eventName, action, ms) { + super() + this.eventName = eventName + this.action = action + this.handle = undefined + this.interval = ms + this.addListener(this.eventName, this.action); + } + + start() { + if (!this.handle) { + this.handle = setInterval(() => this.emit(this.eventName), this.interval); + } + } +} diff --git a/lib/WordpressRepository.js b/lib/WordpressRepository.js new file mode 100644 index 00000000..41ce7701 --- /dev/null +++ b/lib/WordpressRepository.js @@ -0,0 +1,103 @@ +const {convert} = require("url-slug"); + +class WordpressRepository { + constructor(wordpressClient) { + this.wordpressClient = wordpressClient + } + async fetchNews() { + let response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,content.rendered,date,categories&categories=587') + + if (response.status !== 200) { + throw new Error('WordpressRepository::fetchNews failed with response status "' + response.status + '"') + } + + const rawNewsData = JSON.parse(response.data) + + if (typeof rawNewsData !== 'object' || rawNewsData === null) { + throw new Error('WordpressRepository::mapNewsResponse malformed response, not an object') + } + + return rawNewsData.map(item => ({ + slug: convert(item.title.rendered), + bcSlug: item.title.rendered.replace(/ /g, '-'), + date: item.date, + title: item.title.rendered, + content: item.content.rendered, + author: item._embedded.author[0].name, + media: item._embedded['wp:featuredmedia'][0].source_url, + })) + } + + async fetchTournamentNews() { + let response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=content.rendered,categories&categories=638') + + if (response.status !== 200) { + throw new Error('WordpressRepository::fetchTournamentNews failed with response status "' + response.status + '"') + } + + let dataObjectToArray = JSON.parse(response.data); + + let sortedData = dataObjectToArray.map(item => ({ + content: item.content.rendered, + category: item.categories + })); + + return sortedData.filter(article => article.category[1] !== 284) + } + + async fetchContentCreators() { + const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=content.rendered,categories&categories=639') + + if (response.status !== 200) { + throw new Error('WordpressRepository::fetchContentCreators failed with response status "' + response.status + '"') + } + + const items = JSON.parse(response.data); + + return items.map(item => ({ + content: item.content.rendered, + })) + } + + async fetchFafTeams() { + const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=content.rendered,categories&categories=636') + + if (response.status !== 200) { + throw new Error('WordpressRepository::fetchFafTeams failed with response status "' + response.status + '"') + } + + const items = JSON.parse(response.data); + + return items.map(item => ({ + content: item.content.rendered, + })) + } + + async fetchNewshub() { + const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,newshub_externalLinkUrl,newshub_sortIndex,content.rendered,date,categories&categories=283') + + if (response.status !== 200) { + throw new Error('WordpressRepository::fetchNewshub failed with response status "' + response.status + '"') + } + + const items = JSON.parse(response.data); + const sortedData = items.map(item => ({ + category: item.categories, + sortIndex: item.newshub_sortIndex, + link: item.newshub_externalLinkUrl, + date: item.date, + title: item.title.rendered, + content: item.content.rendered, + author: item._embedded.author[0].name, + media: item._embedded['wp:featuredmedia'][0].source_url, + })); + + sortedData.sort((articleA, articleB) => articleB.sortIndex - articleA.sortIndex); + + return sortedData.filter((article) => { + return article.category[1] !== 284; + }) + } +} + +module.exports = WordpressRepository diff --git a/lib/WordpressService.js b/lib/WordpressService.js new file mode 100644 index 00000000..648e6921 --- /dev/null +++ b/lib/WordpressService.js @@ -0,0 +1,128 @@ +const {MutexService} = require("./MutexService"); +const wordpressTTL = 60 * 60 + +class WordpressService { + constructor(cacheService, wordpressRepository, lockTimeout = 3000) { + this.lockTimeout = lockTimeout + this.cacheService = cacheService + this.mutexServices = { + news: new MutexService(), + tournament: new MutexService(), + creators: new MutexService(), + teams: new MutexService(), + newshub: new MutexService(), + } + this.wordpressRepository = wordpressRepository + } + + getCacheKey(name) { + return 'WordpressService_' + name + } + + async getNews(ignoreCache = false) { + const cacheKey = this.getCacheKey('news') + + if (this.cacheService.has(cacheKey) && ignoreCache === false) { + return this.cacheService.get(cacheKey) + } + + if (this.mutexServices.news.locked) { + await this.mutexServices.news.acquire(() => { + }, this.lockTimeout) + return this.getNews() + } + + await this.mutexServices.news.acquire(async () => { + const result = await this.wordpressRepository.fetchNews() + this.cacheService.set(cacheKey, result, wordpressTTL); + }) + + return this.getNews() + } + + async getTournamentNews(ignoreCache = false) { + const cacheKey = this.getCacheKey('tournament-news') + + if (this.cacheService.has(cacheKey) && ignoreCache === false) { + return this.cacheService.get(cacheKey) + } + + if (this.mutexServices.tournament.locked) { + await this.mutexService.acquire(() => { + }, this.lockTimeout) + return this.getTournamentNews() + } + + await this.mutexServices.tournament.acquire(async () => { + const result = await this.wordpressRepository.fetchTournamentNews() + this.cacheService.set(cacheKey, result, wordpressTTL); + }) + + return this.getTournamentNews() + } + + async getContentCreators(ignoreCache = false) { + const cacheKey = this.getCacheKey('content-creators') + + if (this.cacheService.has(cacheKey) && ignoreCache === false) { + return this.cacheService.get(cacheKey) + } + + if (this.mutexServices.creators.locked) { + await this.mutexServices.creators.acquire(() => { + }, this.lockTimeout) + return this.getContentCreators() + } + + await this.mutexServices.creators.acquire(async () => { + const result = await this.wordpressRepository.fetchContentCreators() + this.cacheService.set(cacheKey, result, wordpressTTL); + }) + + return this.getContentCreators() + } + + async getFafTeams(ignoreCache = false) { + const cacheKey = this.getCacheKey('faf-teams') + + if (this.cacheService.has(cacheKey) && ignoreCache === false) { + return this.cacheService.get(cacheKey) + } + + if (this.mutexServices.teams.locked) { + await this.mutexService.acquire(() => { + }, this.lockTimeout) + return this.getFafTeams() + } + + await this.mutexServices.teams.acquire(async () => { + const result = await this.wordpressRepository.fetchFafTeams() + this.cacheService.set(cacheKey, result, wordpressTTL); + }) + + return this.getFafTeams() + } + + async getNewshub(ignoreCache = false) { + const cacheKey = this.getCacheKey('newshub') + + if (this.cacheService.has(cacheKey) && ignoreCache === false) { + return this.cacheService.get(cacheKey) + } + + if (this.mutexServices.newshub.locked) { + await this.mutexService.acquire(() => { + }, this.lockTimeout) + return this.getNewshub() + } + + await this.mutexServices.newshub.acquire(async () => { + const result = await this.wordpressRepository.fetchNewshub() + this.cacheService.set(cacheKey, result, wordpressTTL); + }) + + return this.getNewshub() + } +} + +module.exports = WordpressService diff --git a/lib/WordpressServiceFactory.js b/lib/WordpressServiceFactory.js new file mode 100644 index 00000000..4d580cc8 --- /dev/null +++ b/lib/WordpressServiceFactory.js @@ -0,0 +1,13 @@ +const WordpressService = require("./WordpressService") +const WordpressRepository = require("./WordpressRepository") +const {Axios} = require("axios") +const cacheService = require('./CacheService') + +module.exports = (wordpressBaseURL) => { + const config = { + baseURL: wordpressBaseURL + }; + const wordpressClient = new Axios(config) + + return new WordpressService(cacheService, new WordpressRepository(wordpressClient)) +} diff --git a/public/js/app/content-creators.js b/public/js/app/content-creators.js index 1fcf710b..e674852d 100644 --- a/public/js/app/content-creators.js +++ b/public/js/app/content-creators.js @@ -1,5 +1,5 @@ async function getWordpress() { - const response = await fetch(`./js/app/members/content-creators.json`); + const response = await fetch('/data/content-creators.json'); const data = await response.json(); let insertWordpress = document.getElementById('contentCreatorWordpress'); insertWordpress.insertAdjacentHTML('beforeend', `${data[0].content}`); diff --git a/public/js/app/faf-teams.js b/public/js/app/faf-teams.js index d7ce3fcf..f47c58b4 100644 --- a/public/js/app/faf-teams.js +++ b/public/js/app/faf-teams.js @@ -2,7 +2,7 @@ let teamSelection = document.querySelectorAll('.teamSelection'); let teamContainer = document.querySelectorAll('.teamContainer'); async function getWordpress() { - const response = await fetch(`js/app/members/faf-teams.json`); + const response = await fetch('/data/faf-teams.json'); const data = await response.json(); let insertWordpress = document.getElementById('insertWordpress'); insertWordpress.insertAdjacentHTML('beforeend', `${data[0].content}`); diff --git a/public/js/app/members/.gitignore b/public/js/app/members/.gitignore deleted file mode 100644 index 94a2dd14..00000000 --- a/public/js/app/members/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.json \ No newline at end of file diff --git a/public/js/app/newshub.js b/public/js/app/newshub.js index 2a801b30..a586738d 100644 --- a/public/js/app/newshub.js +++ b/public/js/app/newshub.js @@ -1,11 +1,11 @@ async function getNewshub() { - const response = await fetch(`js/app/members/newshub.json`); + const response = await fetch('/data/newshub.json'); const data = await response.json(); return await data; } async function getTournament() { - const response = await fetch(`js/app/members/tournament-news.json`); + const response = await fetch('/data/tournament-news.json'); const data = await response.json(); return await data; } @@ -72,8 +72,6 @@ let arrowRight = document.getElementById('clientArrowRigth'); let arrowLeft = document.getElementById('clientArrowLeft'); let newsPosition = 0; let newsLimit = 0; -let newsMove = clientContainer[0].offsetWidth; -console.log(newsMove); let spawnStyle = getComputedStyle(clientSpawn).columnGap; let columnGap = spawnStyle.slice(0, 2); diff --git a/routes/middleware.js b/routes/middleware.js index 16c4bab7..5e9f8b83 100755 --- a/routes/middleware.js +++ b/routes/middleware.js @@ -1,10 +1,7 @@ -/** - Initialises the standard view locals +const WordpressServiceFactory = require("../lib/WordpressServiceFactory"); +const appConfig = require("../config/app"); +const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) - The included layout depends on the navLinks array to generate - the navigation in the header, you may wish to change this array - or replace it with your own templates / logic. -*/ exports.initLocals = function(req, res, next) { let locals = res.locals; locals.navLinks = []; @@ -52,3 +49,11 @@ exports.isAuthenticated = (redirectUrlAfterLogin = null, isApiRequest = false) = return res.redirect('/login') } } + +exports.injectServices = function(req, res, next) { + req.services = { + wordpressService: wordpressService + } + + next() +} diff --git a/routes/views/account/get/report.js b/routes/views/account/get/report.js index f8954f44..f7e65d4e 100644 --- a/routes/views/account/get/report.js +++ b/routes/views/account/get/report.js @@ -101,23 +101,7 @@ exports = module.exports = function (req, res) { } locals.reportable_members = {}; - const recentMembersPath = 'members/recent.json'; - if (fs.existsSync(recentMembersPath)){ - fs.readFile(recentMembersPath, 'utf8', function (err, data) { - try { - locals.reportable_members = JSON.parse(data); - } catch (e) { - const moment = require('moment'); - console.log(moment().format("DD-MM-YYYY - HH:mm:ss") + " - The list of reportable members could not be read from the disk: " + e.toString()); - } - - res.render('account/report', {flash: flash}); - }); - } - else - { - res.render('account/report', {flash: flash}); - } + res.render('account/report', {flash: flash}) } ) }; diff --git a/routes/views/dataRouter.js b/routes/views/dataRouter.js new file mode 100644 index 00000000..8b0faeec --- /dev/null +++ b/routes/views/dataRouter.js @@ -0,0 +1,38 @@ +const express = require('express'); +const router = express.Router(); +const {AcquireTimeoutError} = require('../../lib/MutexService'); + +const getData = async (req, res, name, data) => { + try { + return res.json(data) + } catch (e) { + if (e instanceof AcquireTimeoutError) { + return res.status(503).json({error: 'timeout reached'}) + } + + console.error('[error] dataRouter::get:' + name + '.json failed with "' + e.toString() + '"') + + if (!res.headersSent) { + return res.status(500).json({error: 'unexpected error'}) + } + throw e + } +} +router.get('/newshub.json', async (req, res) => { + getData(req, res, 'newshub', await req.services.wordpressService.getNewshub()) +}) + +router.get('/tournament-news.json', async (req, res) => { + getData(req, res, 'tournament-news', await req.services.wordpressService.getTournamentNews()) +}) +router.get('/faf-teams.json', async (req, res) => { + getData(req, res, 'faf-teams', await req.services.wordpressService.getFafTeams()) +}) + +router.get('/content-creators.json', async (req, res) => { + getData(req, res, 'content-creators', await req.services.wordpressService.getContentCreators()) +}) + + + +module.exports = router diff --git a/routes/views/news.js b/routes/views/news.js index f1b7a3a5..3188b1d7 100644 --- a/routes/views/news.js +++ b/routes/views/news.js @@ -1,12 +1,5 @@ const express = require('express'); const router = express.Router(); -const fs = require('fs') - -function getNewsArticles() { - const readFile = fs.readFileSync('./public/js/app/members/news.json', - {encoding:'utf8', flag:'r'}); - return JSON.parse(readFile); -} function getNewsArticleBySlug(articles, slug) { let [newsArticle] = articles.filter((entry) => { @@ -28,21 +21,22 @@ function getNewsArticleByDeprecatedSlug(articles, slug) { return newsArticle ?? null } -router.get(`/`, (req, res) => { - res.render('news', {news: getNewsArticles()}); +router.get(`/`, async (req, res) => { + res.render('news', {news: await req.services.wordpressService.getNews()}) }) -router.get(`/:slug`, (req, res) => { - const newsArticles = getNewsArticles(); +router.get(`/:slug`, async (req, res) => { + const newsArticles = await req.services.wordpressService.getNews() + const newsArticle = getNewsArticleBySlug(newsArticles, req.params.slug) if (newsArticle === null) { const newsArticleByOldSlug = getNewsArticleByDeprecatedSlug(newsArticles, req.params.slug) - + if (newsArticleByOldSlug) { // old slug style, here for backward compatibility res.redirect(301, newsArticleByOldSlug.slug) - + return } @@ -54,7 +48,6 @@ router.get(`/:slug`, (req, res) => { res.render('newsArticle', { newsArticle: newsArticle }); - }) module.exports = router diff --git a/scripts/cron-jobs.js b/scripts/cron-jobs.js index 53995244..cb5fe181 100644 --- a/scripts/cron-jobs.js +++ b/scripts/cron-jobs.js @@ -1,16 +1,27 @@ const appConfig = require("../config/app") +const WordpressServiceFactory = require("../lib/WordpressServiceFactory"); +const Scheduler = require("../lib/Scheduler"); -module.exports = () => { - try { - require(`./extractor`).run() - } catch (e) { - console.error(`Error running extractor script. Make sure the API is available (will try again after interval).`, e) +const warmupWordpressCache = async () => { + const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) + + const successHandler = (name) => { + console.info(name, 'cache generated') } - setInterval(() => { - try { - require(`./extractor`).run() - } catch (e) { - console.error(`extractor caused the error`, e) - } - }, appConfig.extractorInterval * 60 * 1000) + const errorHandler = (e, name) => { + console.error(name, e.toString(), 'cache failed') + } + + wordpressService.getNews(true).then(() => successHandler('getNews')).catch((e) => errorHandler(e, 'getNews')) + wordpressService.getNewshub(true).then(() => successHandler('getNewshub')).catch((e) => errorHandler(e, 'getNewshub')) + wordpressService.getContentCreators(true).then(() => successHandler('getContentCreators')).catch((e) => errorHandler(e, 'getContentCreators')) + wordpressService.getTournamentNews(true).then(() => successHandler('getTournamentNews')).catch((e) => errorHandler(e, 'getTournamentNews')) + wordpressService.getFafTeams(true).then(() => successHandler('getFafTeams')).catch((e) => errorHandler(e, 'getFafTeams')) +} + +module.exports = async () => { + await warmupWordpressCache() + + const wordpressScheduler = new Scheduler('createWordpressCaches', warmupWordpressCache, 60 * 59 * 1000) + wordpressScheduler.start() } diff --git a/scripts/extractor.js b/scripts/extractor.js deleted file mode 100644 index e8c543cb..00000000 --- a/scripts/extractor.js +++ /dev/null @@ -1,181 +0,0 @@ -require("dotenv").config(); - -//Default values for API Calls -process.env.API_URL = process.env.API_URL || 'https://api.faforever.com'; -process.env.WP_URL = process.env.WP_URL || 'https:direct.faforever.com'; - -const fs = require('fs'); -const axios = require('axios'); -const {convert} = require('url-slug') - -let d = new Date(); -let timeFilter = 12; -let minusTimeFilter = d.setMonth(d.getMonth() - timeFilter); -let currentDate = new Date(minusTimeFilter).toISOString(); - - -//TODO: Manage to make a loop of sorts of the URLs and "let data = dataObjectToArray.map" because it repeats alot and it could be done in a for loop/array since almost all API calls below have an extremely similar syntax/behavior. - -async function getTournamentNews() { - - try { - let response = await axios.get(`${process.env.WP_URL}/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=content.rendered,categories&categories=638`); - //Now we get a js array rather than a js object. Otherwise we can't sort it out. - let dataObjectToArray = Object.values(response.data); - - let sortedData = dataObjectToArray.map(item => ({ - content: item.content.rendered, - category: item.categories - })); - let newshubData = sortedData.filter(article => article.category[1] !== 284); - return await newshubData; - } catch (e) { - console.error(currentDate, '- [error] extractor::getTournamentNews failed with =>', e.toString()); - return null; - } -} - -async function news() { - try { - let response = await axios.get(`${process.env.WP_URL}/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,content.rendered,date,categories&categories=587`); - - //Now we get a js array rather than a js object. Otherwise we can't sort it out. - let dataObjectToArray = Object.values(response.data); - let data = dataObjectToArray.map(item => ({ - slug: convert(item.title.rendered), - bcSlug: item.title.rendered.replace(/ /g, '-'), - date: item.date, - title: item.title.rendered, - content: item.content.rendered, - author: item._embedded.author[0].name, - media: item._embedded['wp:featuredmedia'][0].source_url, - })); - return await data; - } catch (e) { - console.error(currentDate, '- [error] extractor::news failed with =>', e.toString()); - return null; - } -} - -async function newshub() { - - try { - let response = await axios.get(`${process.env.WP_URL}/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,newshub_externalLinkUrl,newshub_sortIndex,content.rendered,date,categories&categories=283`); - - let dataObjectToArray = await Object.values(response.data); - let sortedData = await dataObjectToArray.map(item => ({ - category: item.categories, - sortIndex: item.newshub_sortIndex, - link: item.newshub_externalLinkUrl, - date: item.date, - title: item.title.rendered, - content: item.content.rendered, - author: item._embedded.author[0].name, - media: item._embedded['wp:featuredmedia'][0].source_url, - })); - sortedData.sort((articleA, articleB) => articleB.sortIndex - articleA.sortIndex); - - function onlyActiveArticles(article) { - return article.category[1] !== 284; - } - - let data = sortedData.filter(onlyActiveArticles); - return await data; - } catch (e) { - console.error(currentDate, '- [error] extractor::newshub failed with =>', e.toString()); - return null; - } -} - - -async function fafTeams() { - - try { - let response = await axios.get(`${process.env.WP_URL}/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=content.rendered,categories&categories=636`); - - let dataObjectToArray = Object.values(response.data); - let data = dataObjectToArray.map(item => ({ - content: item.content.rendered, - })); - return await data; - } catch (e) { - console.error(currentDate, '- [error] extractor::fafTeams failed with =>', e.toString()); - return null; - } - -} - -async function contentCreators() { - try { - let response = await axios.get(`${process.env.WP_URL}/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=content.rendered,categories&categories=639`); - - let dataObjectToArray = Object.values(response.data); - let data = dataObjectToArray.map(item => ({ - content: item.content.rendered, - })); - return await data; - } catch (e) { - console.error(currentDate, '- [error] extractor::contentCreators failed with =>', e.toString()); - return null; - } - - -} - -async function getAllClans() { - - return []; - //disabled due https://github.com/FAForever/website/issues/445 - // try { - // let response = await axios.get(`${process.env.API_URL}/data/clan?sort=createTime&include=leader&fields[clan]=name,tag,description,leader,memberships,createTime&fields[player]=login&page[number]=1&page[size]=3000`); - // - // let dataObjectToArray = Object.values(response.data); - // let clanLeader = dataObjectToArray[2].map(item => ({ - // leaderName: item.attributes.login - // })); - // let clanValues = dataObjectToArray[0].map(item => ({ - // //id: item.id, - // name: item.attributes.name, - // tag: item.attributes.tag, - // createTime: item.attributes.createTime, - // //description: item.attributes.description, - // population: item.relationships.memberships.data.length - // })); - // const combineArrays = (array1, array2) => array1.map((x, i) => [x, array2[i]]); - // let clanData = combineArrays(clanLeader, clanValues); - // clanData.sort((playerA, playerB) => playerA[1].population - playerB[1].population); - // return await clanData; - // - // } catch (e) { - // console.log(e); - // return null; - // } - -} - -module.exports.run = function run() { - - // Do not change the order of these/make sure they match the order of fileNames below - const extractorFunctions = [ - getTournamentNews(), news(), contentCreators(), newshub(), fafTeams(), getAllClans(), - ]; - //Make sure to not change the order of these since they match the order of extractorFunctions - const fileNames = [ - 'tournament-news', 'news', 'content-creators', 'newshub', 'faf-teams', 'getAllClans', - ]; - - fileNames.forEach((fileName, index) => { - extractorFunctions[index] - .then(data => { - fs.writeFile(`public/js/app/members/${fileName}.json`, JSON.stringify(data), error => { - if (error) { - console.error(currentDate, '- [error] extractor::run', fileName, 'failed with =>', error.toString()); - } else { - console.log(`${currentDate} - ${fileName} file created.`); - } - }); - }); - }); -}; - - diff --git a/templates/views/newshub.pug b/templates/views/newshub.pug index f309e826..8b0fa2ae 100644 --- a/templates/views/newshub.pug +++ b/templates/views/newshub.pug @@ -19,8 +19,6 @@ html(lang='en') //- Customise the stylesheet for your site by editing /public/styles/site.sass link(href="/styles/css/site.min.css?version="+Date.now(), rel="stylesheet") - script(async, defer, data-domain="faforever.com", src="https://plausible.faforever.com/js/plausible.js") - script(src="https://kit.fontawesome.com/33e7e258d3.js" crossorigin='anonymous') body diff --git a/tests/LeaderboardService.test.js b/tests/LeaderboardService.test.js index 439991b8..acaeca8c 100644 --- a/tests/LeaderboardService.test.js +++ b/tests/LeaderboardService.test.js @@ -34,7 +34,6 @@ beforeEach(() => { new NodeCache( { stdTTL: 300, checkperiod: 600 } ), - new MutexService(), new LeaderboardRepository(axios) ) }) diff --git a/tests/integration/NewsRouter.test.js b/tests/integration/NewsRouter.test.js index ec25d414..ffda5ec3 100644 --- a/tests/integration/NewsRouter.test.js +++ b/tests/integration/NewsRouter.test.js @@ -1,21 +1,17 @@ const request = require('supertest') const express = require('express') const newsRouter = require( "../../routes/views/news") -const fs = require('fs') +const middleware = require("../../routes/middleware"); const app = new express(); app.set('views', 'templates/views'); app.set('view engine', 'pug'); +app.use(middleware.injectServices); app.use("/news", newsRouter) describe('News Routes', function () { - const testFile = fs.readFileSync('tests/integration/testData/news.json',{encoding:'utf8', flag:'r'}) test('responds to /', async () => { - jest.mock('fs') - jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(testFile) - - const res = await request(app).get('/news'); expect(res.header['content-type']).toBe('text/html; charset=utf-8'); expect(res.statusCode).toBe(200); @@ -26,10 +22,6 @@ describe('News Routes', function () { }); test('responds to /:slug', async () => { - jest.mock('fs') - jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(testFile) - - const res = await request(app).get('/news/balance-patch-3750-is-live'); expect(res.header['content-type']).toBe('text/html; charset=utf-8'); expect(res.statusCode).toBe(200); @@ -37,10 +29,6 @@ describe('News Routes', function () { }); test('responds to /:slug with redirect if called with old slug', async () => { - jest.mock('fs') - jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(testFile) - - const res = await request(app).get('/news/Balance-Patch-3750-Is-Live'); expect(res.statusCode).toBe(301); expect(res.header['location']).toBe('balance-patch-3750-is-live'); diff --git a/tests/integration/testData/content-creators.json b/tests/integration/testData/content-creators.json new file mode 100644 index 00000000..12a13d98 --- /dev/null +++ b/tests/integration/testData/content-creators.json @@ -0,0 +1 @@ +[{"content":"\n
These fellow casters and streamers below might not be the toughest of the pack but they sure know how to make up in entertainment value either through their words or their editing skills. Great to watch when you just want to chill.
\nBiggest FAF youtuber, everyone knows who Gyle is with his Gyle Casts.
\n \n \nUpcoming FAF caster with a mix of fun commentary casts and analysis videos every then.
\n \n \nKing of supcom memes, casts a little bit of everything and his videos always have their meme edits
\n \n \nTheDuelist creates casts, plays mid level ladder matches, and breaks down units and strategies mathematically.
\n \n \nAngry european badger that casts games with his seductive bear voice.
\n \n \nDual Gap lover, plays DG every day every week.
\n \n \nThe Dual Gap Apprentice! Plays DG and other maps sometimes..
\n \n \nBiiiig Russian streamer with very entertaining casts.
\n \n \nNew Caster dealing with all types of casts from 1v1 all the way up to 8v8 Check him out.
\n \nThese fellow casters and streamers below are part of the toughest of the pack. They sure know how what they are doing and may provide sound advice. Plus some of them are actually entertaining too! If you want to watch some FAF while also learn, be sure to take a look at one of these individuals and even ask them questions on their stream!
\nIrish FAF veteran caster that streams constantly and participates on the top tourneys.
\n \n \nGerman upcoming pro, grinds hard trying to reach the top of FAF tournaments..
\n \n \nFunny Russian magician with unexpected maneuvers and meme plays.
\n \n \nHigh-rated Russian Streamer.
\n \n \nSetons Macro God, #1 FAF Gamer
\n \n \nFtXCommando’s best “friend” and FAFLive #1 caster.
\n \n \nPolish top player, some consider him the best player, great teacher!
\n \n
\n\n\n\nThe promotions team creates a steady flow of content for the client newshub, the website news articles and FAF content on social media. It is also the team that maintains the FAF Website, communicates with other communities for collaboration opportunities, promotes FAF tournaments
The trainer team are a group of interested people in the development of the FAF community. Their goals are providing opportunities to new players to develop their own skills and adapting them within the community. Aiding players on becoming part of the competitive scene.
The FAFLive team is in charge of the FAFLive Twitch account. That is the official channel where FAF tourneys are casted by the top players and other prominent community members. They work towards a pleasant and smooth viewing experience for FAFLive Events.
The tournament team makes sure FAF tournaments go as planned and quickly fix any unexpected issues. They are made of tournament directors who help ensure the rules of each tournament are followed correctly.
The matchmaking team curate and update the 1v1, 2v2 and 4v4 matchmaker pools every month. Determining the rating brackets and the pool breakdown by bracket, create matchmaker events to increase player engagement and gather feedback on what maps are liked by the community.
The balance team, as you may have guessed, has the continuous goal of balancing FAF’s gameplay to make it more fun, fair and engaging. Making sure all four factions are equally viable to play.
The creative team moderates the map and mod vault, deciding on the ranking of maps, curating vault rules and recommended mods and maps. They also make sure to handle any mods or maps that break the rules or are inappropriate.
The moderation team acts as the final point for all moderation decisions. They make sure the FAF forums, aeolus chat and FAF Discords maintain a civil environment. Basically the FAF Police.
The DevOps team enables the promise of Forever in Forged Alliance Forever. They aim to maintain an open and reproducible, built to last and scale and as simple as possible but as complex as required Forged Alliance Forever.
The largest tournament of the year is back again with a total prize pool of $1900! Time to gear up and try to qualify yourself on the 25th of November to enter the group stage! Click here for more information
\n\n\n\n\n","author":"Jip","media":"https://direct.faforever.com/wp-content/uploads/2023/11/lots-small.png"},{"category":[283],"sortIndex":"1000","link":"https://forum.faforever.com/topic/6753/looking-for-feedback-on-whether-people-perceive-more-or-less-connection-problems","date":"2023-10-13T15:06:53","title":"Did the recent work on the client help?","content":"\nThe recent work to mitigate the DDOS attacks are bearing fruit! All that fruit is stuffed in the v2023.11.0 of the client. Go get it while it is hot! Please leave your experience on whether there are more or less connectivity problems in the feedback channel on the forums by clicking on this news item.
\n\n\n\n\n","author":"Jip","media":"https://direct.faforever.com/wp-content/uploads/2023/06/server_issues.png"},{"category":[283],"sortIndex":"910","link":"https://www.youtube.com/watch?v=12iT8o6pJW4&list=PLp2GJBSquXYfJdFxPEnqkpVTmqh3ir4ic&index=3 ","date":"2023-11-16T15:00:50","title":"Terms & phrases you should know!","content":"\nDo you know what ASF’s, RAS spam, and build capacity are? Worry not if you don’t, TheGreenSquier will go over the most used terms, phrases, and acronyms in this video!
\n","author":"Jip","media":"https://direct.faforever.com/wp-content/uploads/2023/11/Iceberg_Thumbnail_alt_4.png"},{"category":[283],"sortIndex":"880","link":"https://forum.faforever.com/topic/6791/brigadier-fletcher-player-tournament/1","date":"2023-11-16T14:56:25","title":"‘Brigadier Fletcher’ Player tournament","content":"\nTime to duke it out for two teams on the submissions of the equally named mapping tournament! Read up all about it here and don’t forget to give the submitted maps a play or two!
\n","author":"Jip","media":"https://direct.faforever.com/wp-content/uploads/2023/11/brigadier-fletcher-player-tournament.png"},{"category":[283],"sortIndex":"800","link":"https://wiki.faforever.com/en/Development/Mapping","date":"2023-09-09T15:57:08","title":"Modern mapping tutorials","content":"\nNow’s the ideal time to master map-making! Thanks to Prohibitorum, FAForever Wiki’s mapping section is up-to-date with the latest tutorials.
\n","author":"Jip","media":"https://direct.faforever.com/wp-content/uploads/2023/09/luminarybreakdown.jpg"},{"category":[283],"sortIndex":"750","link":"https://wiki.faforever.com/en/Development/Mapping/Gaea/Texturing","date":"2023-09-13T14:17:34","title":"Texturing guide using Gaea","content":"\nAnother valuable piece of content for map authors: Learn all about procedurally generating stratum masks! With thanks to Prohibitorum for his time and effort
Deadline is 12th of November, 2023
\n \n\n\n\nNovember 25th 14:00 UTC
\n1v1 – Bo3
\n\n","category":[638]}] \ No newline at end of file diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 00000000..4f30e579 --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,10 @@ +const fs = require("fs"); +const wordpressService = require("../lib/WordpressService"); +beforeEach(() => { + const newsFile = JSON.parse(fs.readFileSync('tests/integration/testData/news.json',{encoding:'utf8', flag:'r'})) + jest.spyOn(wordpressService.prototype, 'getNews').mockResolvedValue(newsFile); +}) + +afterEach(() => { + jest.restoreAllMocks() +})