From 8cdba0ff3024b749f0406f1d88b3f24300a5e486 Mon Sep 17 00:00:00 2001 From: Dragon Fire Date: Thu, 19 Nov 2020 17:03:56 -0500 Subject: [PATCH] Redis Timer System --- .env.example | 4 ++++ README.md | 7 +++++++ Xiao.js | 5 ++++- commands/other/timer.js | 14 ++++--------- package.json | 3 ++- structures/Client.js | 4 ++++ structures/Redis.js | 22 +++++++++++++++++++++ structures/timer/TimerManager.js | 34 ++++++++++++++++++++++++++++++++ 8 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 structures/Redis.js create mode 100644 structures/timer/TimerManager.js diff --git a/.env.example b/.env.example index 4782e544..ec90546a 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,10 @@ POSTER_TIME= REPORT_CHANNEL_ID= JOIN_LEAVE_CHANNEL_ID= +# Redis info +REDIS_HOST= +REDIS_PASS= + # Emoji IDs GOLD_FISH_EMOJI_ID= GOLD_FISH_EMOJI_NAME= diff --git a/README.md b/README.md index f6265e6d..ce62ea1a 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,13 @@ The difficulty in getting all of these keys is why I recommend * `REPORT_CHANNEL_ID` is the ID of the Discord channel you want to send messages from `report` to. Not required, and if not provided the report command simply DMs the owner. * `JOIN_LEAVE_CHANNEL_ID` is the ID of the Discord channel to send a message to whenever a new server adds or removes the bot. Not required. +### Redis Info + +This is information for connecting to Redis. + +* `REDIS_HOST` is the host for your Redis connection. Probably `127.0.0.1`. +* `REDIS_PASS` is the password for your Redis connection. + ### Emoji IDs All the emoji IDs are the IDs of Discord custom emoji. You need to diff --git a/Xiao.js b/Xiao.js index 93007dc7..2381215b 100644 --- a/Xiao.js +++ b/Xiao.js @@ -50,9 +50,12 @@ client.registry }) .registerCommandsIn(path.join(__dirname, 'commands')); -client.on('ready', () => { +client.on('ready', async () => { client.logger.info(`[READY] Logged in as ${client.user.tag}! ID: ${client.user.id}`); + // Set up existing timers + await client.timers.fetchAll(); + // Push client-related activities client.activities.push( { text: () => `${formatNumber(client.guilds.cache.size)} servers`, type: 'WATCHING' }, diff --git a/commands/other/timer.js b/commands/other/timer.js index f9167c5a..e775aca6 100644 --- a/commands/other/timer.js +++ b/commands/other/timer.js @@ -17,21 +17,15 @@ module.exports = class TimerCommand extends Command { } ] }); - - this.timers = new Map(); } - run(msg, { time }) { - if (this.timers.has(msg.channel.id)) return msg.reply('Only one timer can be set per channel.'); + async run(msg, { time }) { + const exists = await this.client.timers.exists(msg.channel.id, msg.author.id); + if (exists) return msg.reply('Only one timer can be set per channel per user.'); const timeMs = time.startDate.getTime() - Date.now(); - if (timeMs > 600000) return msg.reply('Times above 10 minutes are not currently supported. Sorry!'); const display = moment().add(timeMs, 'ms').fromNow(); const title = time.eventTitle || 'something'; - const timeout = setTimeout(async () => { - await msg.channel.send(`🕰️ ${msg.author}, you wanted me to remind you of: **"${title}"**.`); - this.timers.delete(msg.channel.id); - }, timeMs); - this.timers.set(msg.channel.id, timeout); + await this.client.timers.setTimer(msg.channel.id, timeMs, msg.author.id, title); return msg.say(`🕰️ Okay, I will remind you **"${title}"** ${display}.`); } }; diff --git a/package.json b/package.json index 753e6c12..fb77a490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "119.43.2", + "version": "119.44.0", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": { @@ -48,6 +48,7 @@ "gifencoder": "^2.0.1", "gm": "^1.23.1", "html-entities": "^1.3.1", + "ioredis": "^4.19.2", "js-beautify": "^1.13.0", "mathjs": "^8.0.1", "moment": "^2.29.1", diff --git a/structures/Client.js b/structures/Client.js index 2d928901..a144ea6d 100644 --- a/structures/Client.js +++ b/structures/Client.js @@ -4,6 +4,8 @@ const Collection = require('@discordjs/collection'); const winston = require('winston'); const fs = require('fs'); const path = require('path'); +const Redis = require('./Redis'); +const TimerManager = require('./timer/TimerManager'); const PokemonStore = require('./pokemon/PokemonStore'); const MemePosterClient = require('./MemePoster'); const activities = require('../assets/json/activity'); @@ -30,7 +32,9 @@ module.exports = class XiaoClient extends CommandoClient { winston.format.printf(log => `[${log.timestamp}] [${log.level.toUpperCase()}]: ${log.message}`) ) }); + this.redis = Redis ? Redis.db : null; this.webhook = new WebhookClient(XIAO_WEBHOOK_ID, XIAO_WEBHOOK_TOKEN, { disableMentions: 'everyone' }); + this.timers = new TimerManager(this); this.pokemon = new PokemonStore(); this.memePoster = POSTER_ID && POSTER_TOKEN ? new MemePosterClient(POSTER_ID, POSTER_TOKEN, { subreddits, diff --git a/structures/Redis.js b/structures/Redis.js new file mode 100644 index 00000000..8e9e3675 --- /dev/null +++ b/structures/Redis.js @@ -0,0 +1,22 @@ +const Redis = require('ioredis'); +const { REDIS_HOST, REDIS_PASS } = process.env; +const redis = new Redis({ + port: 6379, + host: REDIS_HOST, + enableReadyCheck: true, + password: REDIS_PASS, + db: 0 +}); + +module.exports = class RedisClient { + static get db() { + return redis; + } + + static start() { + redis.on('connect', () => console.info('[REDIS][CONNECT]: Connecting...')); + redis.on('ready', () => console.info('[REDIS][READY]: Ready!')); + redis.on('error', error => console.error(`[REDIS][ERROR]: Encountered error:\n${error}`)); + redis.on('reconnecting', () => console.warn('[REDIS][RECONNECT]: Reconnecting...')); + } +}; diff --git a/structures/timer/TimerManager.js b/structures/timer/TimerManager.js new file mode 100644 index 00000000..5eedd079 --- /dev/null +++ b/structures/timer/TimerManager.js @@ -0,0 +1,34 @@ +const Redis = require('../Redis'); + +module.exports = class TimerManager { + constructor(client) { + Object.defineProperty(this, 'client', { value: client }); + } + + async fetchAll() { + const timers = await Redis.db.hgetall('timer'); + for (const data of Object.keys(timers)) { + data = JSON.parse(data); + await this.setTimer(data.channelID, new Date(data.time) - new Date(), data.userID, data.title, false); + } + return this; + } + + async setTimer(channelID, time, userID, title, updateRedis = true) { + const data = { time: new Date(Date.now() + time).toISOString(), channelID, userID, title }; + const timeout = setTimeout(async () => { + try { + const channel = await this.client.channels.fetch(channelID); + await channel.send(`🕰️ <@${userID}>, you wanted me to remind you of: **"${title}"**.`); + } finally { + await Redis.db.hdel('timer', `${channelID}-${userID}`); + } + }, time); + if (updateRedis) await Redis.db.hset('timer', { [`${channelID}-${userID}`]: JSON.stringify(data) }); + return timeout; + } + + exists(channelID, userID) { + return Redis.db.hexists('timer', `${channelID}-${userID}`); + } +};