diff --git a/README.md b/README.md index 3e9a5392..e19ce03b 100644 --- a/README.md +++ b/README.md @@ -1047,7 +1047,7 @@ Total: 515 * **hunger-games:** - [BrantSteele](https://brantsteele.com/) ([Original "Hunger Games Simulator" Game](http://brantsteele.net/hungergames/reaping.php)) * **jeopardy:** - - [jService](https://jservice.xyz/) (API) + - [J! Archive](https://j-archive.com/index.php) (Clue Data) - [Jeopardy](https://www.jeopardy.com/) (Music, Original Show) - [OPTIFONT](http://opti.netii.net/) ([Korinna Agency Font](https://fontmeme.com/fonts/korinna-agency-font/)) - [DrewManDew](https://www.deviantart.com/drewmandew/gallery) ([Blank Background Image](https://www.deviantart.com/drewmandew/art/Blank-Jeopardy-Screen-780893853)) diff --git a/Xiao.js b/Xiao.js index 17e203bb..e068b84a 100644 --- a/Xiao.js +++ b/Xiao.js @@ -304,6 +304,14 @@ client.on('ready', async () => { client.logger.error(`[STYLIZE] Failed to load stylize models\n${err.stack}`); } + // Update Jeopardy questions + try { + const newClues = await client.jeopardy.checkForUpdates(); + client.logger.info(`[JEOPARDY] Added ${newClues} new Jeopardy clues.`); + } catch (err) { + client.logger.error(`[JEOPARDY] Failed to update Jeopardy clues\n${err.stack}`); + } + // Fetch all members try { for (const guild of client.guilds.cache.values()) { diff --git a/commands/games-sp/jeopardy.js b/commands/games-sp/jeopardy.js index 59887b14..06d1d4bd 100644 --- a/commands/games-sp/jeopardy.js +++ b/commands/games-sp/jeopardy.js @@ -18,9 +18,9 @@ module.exports = class JeopardyCommand extends Command { game: true, credit: [ { - name: 'jService', - url: 'https://jservice.xyz/', - reason: 'API' + name: 'J! Archive', + url: 'https://j-archive.com/index.php', + reason: 'Clue Data' }, { name: 'Jeopardy', @@ -44,7 +44,7 @@ module.exports = class JeopardyCommand extends Command { } async run(msg) { - const question = await this.fetchQuestion(); + const question = this.client.jeopardy.clues[Math.floor(Math.random() * this.client.jeopardy.clues.length)]; const clueCard = await this.generateClueCard(question.question.replace(/<\/?i>/gi, '')); const connection = msg.guild ? this.client.dispatchers.get(msg.guild.id) : null; let playing = false; @@ -53,7 +53,7 @@ module.exports = class JeopardyCommand extends Command { connection.play(path.join(__dirname, '..', '..', 'assets', 'sounds', 'jeopardy.mp3')); await reactIfAble(msg, this.client.user, '🔉'); } - const category = question.category ? question.category.title.toUpperCase() : ''; + const category = question.category ? question.category.toUpperCase() : ''; await msg.reply(`${category ? `The category is: **${category}**. ` : ''}30 seconds, good luck.`, { files: [{ attachment: clueCard, name: 'clue-card.png' }] }); @@ -70,11 +70,6 @@ module.exports = class JeopardyCommand extends Command { return msg.reply(`The answer was **${answer}**. Good job!`); } - async fetchQuestion() { - const { body } = await request.get('https://jservice.xyz/api/random-clue'); - return body; - } - async generateClueCard(question) { const bg = await loadImage(path.join(__dirname, '..', '..', 'assets', 'images', 'jeopardy.png')); const canvas = createCanvas(1280, 720); diff --git a/package.json b/package.json index 5ad41d87..f7c8dfd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "152.5.3", + "version": "152.5.4", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": { diff --git a/structures/Client.js b/structures/Client.js index e61974b9..38a8ea86 100644 --- a/structures/Client.js +++ b/structures/Client.js @@ -3,6 +3,7 @@ const request = require('node-superfetch'); const winston = require('winston'); const moment = require('moment-timezone'); const Redis = require('./Redis'); +const JeopardyScrape = require('./JeopardyScrape'); const Tensorflow = require('./Tensorflow'); const FontManager = require('./fonts/FontManager'); const PhoneManager = require('./phone/PhoneManager'); @@ -26,6 +27,7 @@ module.exports = class XiaoClient extends CommandClient { this.redis = new Redis(this); this.timers = new TimerManager(this); this.pokemon = new PokemonStore(); + this.jeopardy = new JeopardyScrape(); this.dispatchers = new Map(); this.cleverbots = new Map(); this.phone = new PhoneManager(this); diff --git a/structures/JeopardyScrape.js b/structures/JeopardyScrape.js new file mode 100644 index 00000000..b81ba494 --- /dev/null +++ b/structures/JeopardyScrape.js @@ -0,0 +1,114 @@ +const request = require('node-superfetch'); +const cheerio = require('cheerio'); +const fs = require('fs'); +const path = require('path'); +const rounds = ['jeopardy_round', 'double_jeopardy_round', 'final_jeopardy_round']; + +module.exports = class JeopardyScrape { + constructor() { + this.clues = []; + this.gameIDs = []; + this.seasons = []; + } + + async fetchSeasons() { + const { text } = await request.get(`https://j-archive.com/listseasons.php`) + const $ = cheerio.load(text); + const seasons = []; + $('table td a').each((j, elem) => { + const href = $(elem).attr('href'); + seasons.push(href.split('id=')[1]); + }); + return seasons.reverse(); + } + + async fetchSeason(season) { + const { text } = await request.get(`https://j-archive.com/showseason.php`) + .query({ season }); + const $ = cheerio.load(text); + const gameIDs = []; + $('table td a').each((j, elem) => { + const href = $(elem).attr('href'); + gameIDs.push(href.split('id=')[1]); + }); + return gameIDs; + } + + async fetchClues(id) { + const { text } = await request.get('http://www.j-archive.com/showgame.php') + .query({ game_id: id }); + const $ = cheerio.load(text); + const clues = []; + for (const round of rounds) { + const questions = $(`#${round} .clue`); + const categories = $(`#${round} .category_name`); + const categoryArr = []; + categories.each((i, elem) => categoryArr.push($(elem).text().toLowerCase())); + questions.each((i, elem) => { + const value = $(elem).find('td[class="clue_value"]').text(); + const question = $(elem).find('td[class="clue_text"]').first().text(); + const answer = $(elem).find('em[class="correct_response"]').text(); + if (!question || !answer || !value) return; + clues.push({ + question, + answer, + category: categoryArr[i % 6], + value: value.match(/[0-9]+/)[0], + gameID: id + }); + }); + } + return clues; + } + + importData() { + const read = fs.readFileSync(path.join(__dirname, '..', 'jeopardy.json'), { encoding: 'utf8' }); + const file = JSON.parse(read); + if (typeof file !== 'object' || Array.isArray(file)) return null; + if (!file.clues || !file.gameIDs || !file.seasons) return null; + for (const season of file.seasons) { + if (typeof season !== 'string') continue; + this.seasons.push(season); + } + for (const gameID of file.gameIDs) { + if (typeof gameID !== 'string') continue; + this.gameIDs.push(gameID); + } + for (const clue of file.clues) { + if (typeof clue !== 'string') continue; + this.clues.push(clue); + } + return file; + } + + exportData() { + const buf = Buffer.from(JSON.stringify({ + clues: this.clues, + gameIDs: this.gameIDs, + seasons: this.seasons + })); + fs.writeFileSync(path.join(__dirname, '..', 'jeopardy.json'), buf, { encoding: 'utf8' }); + return buf; + } + + async checkForUpdates() { + this.importData(); + const cluesBefore = this.clues.length; + const latestSeason = this.seasons[this.seasons.length - 1]; + const seasons = await this.fetchSeasons(); + const newSeasons = seasons.filter(season => !this.seasons.includes(season)); + this.seasons.push(...newSeasons); + if (latestSeason) newSeasons.push(latestSeason); + for (const season of newSeasons) { + const games = await this.fetchSeason(season); + const newGames = games.filter(game => !this.gameIDs.includes(game)); + this.gameIDs.push(...newGames); + for (const gameID of newGames) { + const clues = await this.fetchClues(gameID); + this.clues.push(...clues); + } + } + this.exportData(); + return this.clues.length - cluesBefore; + } +};