diff --git a/.gitignore b/.gitignore index 3735d76..6e76c73 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ /.env /BaseClasses/ /commands/diplomacy/ +/commands/handler/ /Error/ /libs/ diff --git a/commands/handler/README.md b/commands/handler/README.md deleted file mode 100644 index d8de8e8..0000000 --- a/commands/handler/README.md +++ /dev/null @@ -1,65 +0,0 @@ -#### About - -##### handler -Обработчик сообщений пользователей. -Модуль является сборником функций, которые как либо реагируют на сообщения пользователей в разных каналах. -Так же при инициализации бота, модули, в которых есть обработчик команд, добавляются в массив, который позже используется для их вызова. - -#### Functions: -Функции хранятся в директории `./functions/` в виде файлов и инициализируются `./initFunctions.js`. -Файлы функций должны возвращать объект, подобный объекту модуля: -```js -module.exports = { - - /** - * Содержит значение активности функции. - * Вне зависимости от значения, будет подключен и инициализирован. - */ - active: (Boolean), // required - - /** - * Короткое обозначение функции. Содержит объект локализации. - * Обязательная локализация - русская. - * Для обозначений языков используется формат ISO 639-1 - */ - title: (Object) { // required - [Language code ISO 639-1]: (String), - }, - - /** - * Условия вызова функции. - * Если true - то функция будет вызываться всегда, при любых сообщениях. - * Такие же условия вызова имеют, например обработчики в других модулях, а детальные проверки уже внутри самого модуля. - */ - allChannels: (Boolean), // required - - /** - * Продвинутые условия вызова. Не активны, если "allChannels" имеет значение true. - * Содержит объект, где ключ - ID канала или категории, а значение показывает, распространяется ли функция на треды. Желательно в комментарии указать название канала. - * - * Функция будет вызываться при отправке сообщения в указанном канале или категории. - * Если сообщение отправлено в треде, то будет дополнительно проверятся, разрешён ли вызов функции в треде. - */ - allowedChannels: (Object) { // required - [Snowflake ID channel]: (Boolean), - }, - - /** - * Функция инициализации. Используется при инициализации модуля. - * Всегда должен возвращать объект модуля (this). - * @return {Object} this - */ - init: async function(){ - return this; - }, - - /** - * Индексная функция. - * @param {Message} msg Сообщение пользователя - */ - call: async function(msg){ - ... - } - -} -``` diff --git a/commands/handler/functions/autoAlive.js b/commands/handler/functions/autoAlive.js deleted file mode 100644 index 3bf77c0..0000000 --- a/commands/handler/functions/autoAlive.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Автоматическое выдача alive', - en: 'Automatic issuance of alive', - uk: 'Автоматичне видача alive' - }, - - allChannels: false, - - allowedChannels: { - '648775464545943602': false // #welcome - }, - - init: async function () { - this.role = await guild.roles.fetch('648762974277992448'); - client.on('guildMemberAdd', async member => { - if (!member.user.bot) { - await toggleRole(this.role, member); - } - }); - return this; - }, - - call: async function (msg) { - if (!msg.member.user.bot && !msg.member.roles.cache.has(this.role.id)) { - toggleRole(this.role, msg.member).then(result => { - msg.reply({ - content: reaction.emoji.success + ' ' + result, - allowedMentions: constants.AM_NONE - }); - }); - } - } - -}; diff --git a/commands/handler/functions/autoThread.js b/commands/handler/functions/autoThread.js deleted file mode 100644 index 322b3f4..0000000 --- a/commands/handler/functions/autoThread.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Автоматическое создание треда', - en: 'Automatic thread creation', - uk: 'Автоматичне створення треду' - }, - - allChannels: false, - allowedChannels: { - '500300930466709515': false, // #предложения - '595198087643922445': false // #новости - }, - - init: async function () { - return this; - }, - - regex: /^\*\*([^\n]+)\*\*\s*\n/i, - - call: async function (msg) { - const match = msg.content.match(this.regex); - let title = ''; - - if (match) { - title += match[1]; - } else { - const name = msg.member.toName(); - const time = new Date(msg.createdTimestamp).toISO(); - title += name + ' ' + time; - } - - msg.startThread({ name: title }); - } - -}; diff --git a/commands/handler/functions/destroyBot.js b/commands/handler/functions/destroyBot.js deleted file mode 100644 index 465c817..0000000 --- a/commands/handler/functions/destroyBot.js +++ /dev/null @@ -1,35 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Прекращение работы бота при обнаружения запуска в другом месте', - en: 'Bot terminates when it detects running elsewhere', - uk: 'Припинення роботи робота при виявленні запуску в іншому місці' - }, - - allChannels: false, - allowedChannels: { - '574997373219110922': false // #logs - }, - - init: async function () { - return this; - }, - - call: async function (msg) { - if (msg.author.id != client.user.id) return; - if (msg.embeds[0]?.title != 'Бот запущен') return; - if (msg.embeds[0]?.footer?.text == sessionId) return; - - await msg.reply({ - content: 'Прекращена работа бота-дубликата у ' + - (process.env.USERNAME ?? 'Host') - }); - - log.error('Прекращение работы бота: бот запущен в другом месте'); - - return process.kill(process.pid); - } - -}; diff --git a/commands/handler/functions/electionsReactions.js b/commands/handler/functions/electionsReactions.js deleted file mode 100644 index 3e93eea..0000000 --- a/commands/handler/functions/electionsReactions.js +++ /dev/null @@ -1,29 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Прикрепление реакций ЗА и ПРОТИВ на выборах', - en: 'Attaching reactions FOR and AGAINST in the elections', - uk: 'Прикріплення реакцій ЗА та ПРОТИ на виборах' - }, - - allChannels: false, - allowedChannels: { - '612280548777525249': false // #выборы - }, - - init: async function () { - return this; - }, - - call: async function (msg) { - if (!msg.content.startsWith('<@') || !msg.content.endsWith('>')) return; - - try { - await msg.react(reaction.emoji.Sg3); - await msg.react(reaction.emoji.Sg0); - } catch (e) {} - } - -}; diff --git a/commands/handler/functions/fixLink.js b/commands/handler/functions/fixLink.js deleted file mode 100644 index 26b15b5..0000000 --- a/commands/handler/functions/fixLink.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = { - - active: false, - - title: { - ru: 'Исправление нерабочей ссылки', - en: 'Fix broken link', - uk: 'Виправлення неробочого посилання' - }, - - allChannels: true, - allowedChannels: {}, - - init: async function () { - return this; - }, - - regex: /https?:\/\/media\.discordapp\.net\/\S+((\.webm)|(\.mp4))/i, - - call: async function (msg) { - if (!this.regex.test(msg.content)) return; - - await msg.delete(); - await msg.channel.send({ - content: msg.author.toString() + ': ' + - msg.content.replace('media.discordapp.net', 'cdn.discordapp.com'), - reply: { - messageReference: msg.reference?.messageId, - failIfNotExists: false - } - }); - } - -}; diff --git a/commands/handler/functions/nhentaiLink.js b/commands/handler/functions/nhentaiLink.js deleted file mode 100644 index 95e8c0d..0000000 --- a/commands/handler/functions/nhentaiLink.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Преобразование кода в ссылку', - en: 'Convert code to link', - uk: 'Перетворення коду на посилання' - }, - - allChannels: false, - allowedChannels: { - '681790010550255617': true // #nsfw - }, - - init: async function () { - return this; - }, - - regex: /^[0-9]{2,}$/, - - call: async function (msg) { - if (!this.regex.test(msg.content)) return; - if (msg.author.bot) return; - - await msg.channel.send({ - content: msg.author.toString() + ': https://nhentai.net/g/' + - msg.content + '/', - allowedMentions: constants.AM_NONE, - reply: { - messageReference: msg.reference?.messageId, - failIfNotExists: false - } - }); - } - -}; diff --git a/commands/handler/functions/pingPromoter.js b/commands/handler/functions/pingPromoter.js deleted file mode 100644 index cbc0c1c..0000000 --- a/commands/handler/functions/pingPromoter.js +++ /dev/null @@ -1,63 +0,0 @@ -module.exports = { - - ROLE_ID: '1107382381700206672', - MONITORING_BOT_ID: '464272403766444044', - COMMAND_ID: '', - COOLDOWN_UP: 4*3600*1000, - - active: true, - - title: { - ru: 'Автоматическое упоминание людей, готовых продвигать сервер', - en: 'Automatic mention of people who are ready to promote the server', - uk: 'Автоматичне згадування людей, що готові просувати сервер' - }, - - allChannels: false, - - allowedChannels: { - '610371610620198922': false // #рандом - }, - - init: async function () { - this.role = await guild.roles.fetch(this.ROLE_ID); - return this; - }, - - call: async function (msg) { - - if(msg.member.user.id !== this.MONITORING_BOT_ID) return; - initLog(msg, 'pingPromoter'); - - let embed; - for(let i = 0; i < 3; i++) { - - msg.log('Fetch updated message. Try ' + i) - - await sleep(1000); - if(!msg?.id) return; - - const msgUpdated = await msg.channel.messages.fetch(msg.id); - embed = msgUpdated.embeds[0]; - - if(embed) { - msg.log('Embed found'); - break; - }; - - }; - - if(!embed?.description?.includes('Успешный Up!')) { - msg.log('Embed is invalid. Description: ' + embed.description); - return; - }; - - msg.log('CD started'); - await sleep(this.COOLDOWN_UP); - msg.log('CD finished'); - - await msg.channel.send(this.role.toString() + ' ' + this.COMMAND_ID); - - } - -}; diff --git a/commands/handler/functions/rule.js b/commands/handler/functions/rule.js deleted file mode 100644 index 7abec51..0000000 --- a/commands/handler/functions/rule.js +++ /dev/null @@ -1,42 +0,0 @@ -const fetch = require('node-fetch'); - -module.exports = { - - active: true, - - title: { - ru: 'Отправляет текст пункта Устава', - en: 'Sends the text of the clause of the Charter', - uk: 'Надсилає текст пункту Статуту' - }, - - allChannels: true, - allowedChannels: {}, - - init: async function () { - try { - this.rules = await (await fetch( - constants.SITE_LINK + '/rules?j=true' - )).json(); - } catch (e) { - this.active = false; - } - - return this; - }, - - call: async function (msg) { - if (msg.content.length < 2) return; - - msg.content = msg.content.replace('а', 'a'); - msg.content = msg.content.replace(/^r\.?/, ''); - - if (!this.rules[msg.content]) return; - - await msg.channel.send({ - content: constants.SITE_LINK + '/rules?f=' + msg.content, - messageReference: msg.reference?.messageId - }); - } - -}; diff --git a/commands/handler/functions/userReactions.js b/commands/handler/functions/userReactions.js deleted file mode 100644 index 5a08e80..0000000 --- a/commands/handler/functions/userReactions.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Прикрепление реакций найденных в сообщении', - en: 'Attaching reactions found in the message', - uk: 'Прикріплення реакцій знайдених у повідомленні' - }, - - allChannels: false, - allowedChannels: { - '572472723624951839': false // #ивенты - }, - - init: async function () { - return this; - }, - - regex: { - global: /<:[^:]+:([0-9]+)>/gi, - local: /<:[^:]+:([0-9]+)>/i - }, - - call: async function (msg) { - const emojis = msg.content.match(this.regex.global); - if (!emojis) return; - - try { - emojis.forEach(emoji => { - emoji = emoji.match(this.regex.local)[1]; - emoji = msg.guild.emojis.cache.get(emoji); - if (emoji) msg.react(emoji); - }); - } catch (e) {} - } - -}; diff --git a/commands/handler/functions/votingReactions.js b/commands/handler/functions/votingReactions.js deleted file mode 100644 index ecde856..0000000 --- a/commands/handler/functions/votingReactions.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = { - - active: true, - - title: { - ru: 'Прикрепление реакций ЗА и ПРОТИВ', - en: 'Addition of reactions FOR and AGAINST', - uk: 'Приєднання реакції ЗА і ПРОТИВ' - }, - - allChannels: false, - allowedChannels: { - '500300930466709515': false // #предложения - }, - - init: async function () { - return this; - }, - - call: async msg => { - try { - await msg.react(reaction.emoji.Sg3); - await msg.react(reaction.emoji.Sg0); - } catch (e) {} - } - -}; diff --git a/commands/handler/index.js b/commands/handler/index.js deleted file mode 100644 index c02ca4d..0000000 --- a/commands/handler/index.js +++ /dev/null @@ -1,199 +0,0 @@ -const SlashOptions = require('../../BaseClasses/SlashOptions'); -const BaseCommand = require('../../BaseClasses/BaseCommand'); -const LangSingle = require('../../BaseClasses/LangSingle'); - -const fetch = require('node-fetch'); -const { title } = require('./about.json'); - -const initFunctions = require('./initFunctions'); - -class Handler extends BaseCommand { - - /** - * Объект функций модуля - * @type {Object} - */ - functions = {}; - - /** - * Массив функций, вызываемых при сообщении в любом канале - * @type {Object} - */ - allChannels = {}; - - /** - * Объект каналов и категориий, содержашие объекты функций - * @type {Object} - */ - allowedChannelsFunctions = {}; - - /** - * Массив модулей где есть обработчик сообщений. - * Пополняется функцией "initMessageHandler()" при инициализации бота. - * @type {Array} - */ - commands = []; - - constructor (path) { - super(path); - - this.category = 'Утилиты'; - this.name = 'handler'; - this.title = this.description = new LangSingle(title); - - return new Promise(async resolve => { - await this.siteStatusCheck(); - - if (!this.siteStatus) { - log.initText += log.error(path + ': Сайт недоступен'); - } - - const { - functions, - allChannels, - allowedChannelsFunctions - } = await initFunctions(); - - this.functions = functions; - this.allChannels = allChannels; - this.allowedChannelsFunctions = allowedChannelsFunctions; - - client.on('messageCreate', async msg => { - if (msg.channel.type === 'DM') return; - if (msg.channel.guild.id !== guild.id) return; - - msg.indexFunc = 'handler'; - - await this.call(msg); - }); - - resolve(this); - }); - } - - - /** - * Проверяет статус сайта и возвращает результат - * @return {Boolean} - */ - - async siteStatusCheck () { - try { - const response = await fetch(constants.SITE_LINK, { - redirect: 'manual' - }); - return this.siteStatus = response.status === 200; - } catch (e) { - return this.siteStatus = false; - } - } - - - /** - * Обработка сообщения, которое не является командой - * @param {Message} msg Сообщение пользователя - */ - async call (msg) { - for (let command of this.commands) { - this.commandMessage(commands[command], msg); - } - - const thread = msg.channel.isThread(); - const channel = thread ? msg.channel.parentId : msg.channel.id; - const category = thread - ? msg.channel.parent?.parentId - : msg.channel?.parentId; - - if(!channel || !category) return; - - let functions = new Set(); - - this.addAllowedChannelsFunctions(functions, category, thread); - this.addAllowedChannelsFunctions(functions, channel, thread); - this.addAllChannelsFunctions(functions); - - this.callFunctions(functions, msg); - - } - - /** - * Обработка сообщения сторонним модулем. - * При двух ошибках в модуле подряд - отключает ссылку на обработчик модуля. - * @param {Object} command Объект модуля - * @param {Message} msg Сообщение пользователя - */ - async commandMessage (command, msg) { - try { - if (command.active) await command.message(msg); - } catch (e) { - const active = e.handler(command.name, false); - if (!active) delete this.commands[command.name]; - } - } - - /** - * Добавляет к списку functions, функции которые необходимо вызвать. - * Выборка из функций каналов или категорий, а так же проверка на тред. - * @param {Set} functions Список функций, которые необходимо вызвать - * @param {String} id ID канала или категории - * @param {Boolean} thread Является ли канал тредом, в котором написано - * сообщение - */ - async addAllowedChannelsFunctions (functions, id, thread) { - if (!this.allowedChannelsFunctions[id]) return; - - for (let name in this.allowedChannelsFunctions[id]) { - if (!thread || this.allowedChannelsFunctions[id][name]) { - functions.add(name); - } - } - } - - /** - * Добавляет к списку functions, функции которые необходимо вызвать. - * Добавляет общие функции, не зависящие от канала. - * @param {Set} functions Список функций, которые необходимо вызвать - */ - async addAllChannelsFunctions (functions) { - for (let name in this.allChannels) { - functions.add(name); - } - } - - /** - * Вызов функций. - * При двух ошибках в функции подряд - отключает функцию. - * @param {Set} functions Список функций, которые необходимо вызвать - * @param {Message} msg Сообщение пользователя - */ - async callFunctions (functions, msg) { - for (let name of functions) { - - try { - await this.functions[name].call(msg); - } catch (e) { - const active = e.handler('handler/func/' + name, false); - if (!active) await this.shutdownFunction(name); - } - - } - } - - /** - * Отключение функции. - * Перебирает объект разрешённых каналов в поиске удаляемой функции и удаляет - * её. - * @param {String} name Название функции - */ - async shutdownFunction (name) { - this.functions[name].active = false; - if (this.allChannels[name]) delete this.allChannels[name]; - for (let id in this.allowedChannelsFunctions) { - if (this.allowedChannelsFunctions[id][name]) { - delete this.allowedChannelsFunctions[id][name]; - } - } - } -} - -module.exports = Handler; diff --git a/commands/handler/initFunctions.js b/commands/handler/initFunctions.js deleted file mode 100644 index 9c9d40a..0000000 --- a/commands/handler/initFunctions.js +++ /dev/null @@ -1,57 +0,0 @@ -const fs = require('fs'); -const dir = './commands/handler/functions/'; - -let data = { - functions: {}, - allChannels: {}, - allowedChannelsFunctions: {} -}; - -/** - * Возвращает функции-обработчики сообщений пользователя - * @return {Object} functions Объект функций модуля - * @return {Object} allChannels Объект названий функций, вызываемых при - * сообщении в любом канале - * @return {Object} allowedChannelsFunctions Объект каналов и категориий, - * содержашие объекты функций - */ -module.exports = async () => { - - const files = fs.readdirSync(dir); - for (const file of files) { - - const timeStart = process.hrtime(); - - const name = file.split('.')[0]; - - let func = require('./functions/' + file); - - if (func.active) data.functions[name] = await func.init(); - - if (func.active) { - if (func.allChannels) { - - data.allChannels[name] = true; - - } else { - - for (let id in func.allowedChannels) { - if (!data.allowedChannelsFunctions[id]) { - data.allowedChannelsFunctions[id] = {}; - } - data.allowedChannelsFunctions[id][name] = func.allowedChannels[id]; - } - - } - } - - const timeEnd = process.hrtime(timeStart); - const pref = (timeEnd[0] * 1000) + (timeEnd[1] / 1000000); - - log.initText += log.load(dir + file, pref, func.active); - - } - - return data; - -}; diff --git a/commands/levels/index.js b/commands/levels/index.js index 2c2131b..e3c712b 100644 --- a/commands/levels/index.js +++ b/commands/levels/index.js @@ -152,7 +152,6 @@ class Levels extends BaseCommand { * @param {Message} msg Сообщение пользователя */ async message (msg) { - if (msg.author.bot) return; const channel = msg.channel.isThread() ? msg.channel.parent : msg.channel; if (!channel) return; if (this.noXPChannels.includes(channel.parentId)) return; diff --git a/init.js b/init.js index d4578b3..a2fba0d 100644 --- a/init.js +++ b/init.js @@ -1,3 +1,5 @@ +const { SiteClient } = require('./libs/SiteClient/SiteClient.js'); + /** * Массив файлов инициализации в порядке подключения. * @@ -33,6 +35,9 @@ module.exports = async () => { console.log('Start init.js'); + // TODO: need refactor + await SiteClient.init(); + for (const module of init) { await module(); } diff --git a/init/commands.js b/init/commands.js index 69461c8..62faf7d 100644 --- a/init/commands.js +++ b/init/commands.js @@ -69,15 +69,6 @@ const sendApplicationGuildCommands = () => { }); }; -/** - * Добавляет к модулю "handler" массив модулей с обработкой сообщений - */ -const initMessageHandler = () => { - if (!commands.handler?.active) return; - - commands.handler.commands = messageCommands; -}; - /** * Определяет модули бота */ @@ -108,9 +99,6 @@ module.exports = async () => { commands[name] = await new Command(path); if (commands[name].active) { - if (commands[name].message.getSource() !== DEFAULT_FUNC) { - messageCommands.push(name); - } if (commands[name].slash.getSource() !== DEFAULT_FUNC) { addSlashCommand(commands[name]); } @@ -126,7 +114,5 @@ module.exports = async () => { sendApplicationGuildCommands(); - initMessageHandler(); - global.commands = commands; -} +}; diff --git a/src/commands/diplomacy/index.ts b/src/commands/diplomacy/index.ts index 4a73235..3edb702 100644 --- a/src/commands/diplomacy/index.ts +++ b/src/commands/diplomacy/index.ts @@ -14,7 +14,7 @@ import { DiplomacyStatService } from '../../libs/Diplomacy/DiplomacyStatService' /** * @TODO: need refactor */ -export class Diplomacy extends BaseCommand { +export class DiplomacyController extends BaseCommand { public static readonly GAME_ID: GameID = 56981; public static readonly INTERVAL_PING = 2 * 3600; @@ -39,21 +39,21 @@ export class Diplomacy extends BaseCommand { this.slashOptions = slashOptions; this.game = new DiplomacyGame( - Diplomacy.GAME_ID, - Diplomacy.INTERVAL_FETCH + DiplomacyController.GAME_ID, + DiplomacyController.INTERVAL_FETCH ); // @ts-ignore @TODO: Надо бы уже избавиться от Promise в constructor return new Promise(async resolve => { try { // @ts-ignore @TODO: Надо бы уже избавиться от глобальных переменных - this.channel = guild.channels.cache.get(Diplomacy.CHANNEL); + this.channel = guild.channels.cache.get(DiplomacyController.CHANNEL); await this.notifyNewTurn(); setInterval( async () => this.notifyNewTurn(), - Diplomacy.INTERVAL_FETCH * 1000 + DiplomacyController.INTERVAL_FETCH * 1000 ); } catch (e) { // @ts-ignore @@ -85,14 +85,14 @@ export class Diplomacy extends BaseCommand { public async slash (int: CommandInteraction) { const flag = int.options.getString('flag'); - let ephemeral = flag !== Diplomacy.FLAG_PING && flag !== Diplomacy.FLAG_PUBLIC; + let ephemeral = flag !== DiplomacyController.FLAG_PING && flag !== DiplomacyController.FLAG_PUBLIC; await int.deferReply({ ephemeral: ephemeral }); try { - const res = await this.update(flag === Diplomacy.FLAG_PING); + const res = await this.update(flag === DiplomacyController.FLAG_PING); /** * Если выясняем, что наступил новый ход - убираем ответ клиенту. @@ -115,14 +115,14 @@ export class Diplomacy extends BaseCommand { await int.followUp({ content: res.pingList }); - } else if (flag === Diplomacy.FLAG_PING) { + } else if (flag === DiplomacyController.FLAG_PING) { await int.followUp({ content: // @ts-ignore int.str( 'Mentions were suppressed because too little time had passed since last mentions. Timeout will pass' ) - + ' ', + + ' ', ephemeral: true }); } @@ -149,7 +149,7 @@ export class Diplomacy extends BaseCommand { if ( this.lastPing !== undefined - && this.lastPing + Diplomacy.INTERVAL_PING >= game.getUpdateAt() + && this.lastPing + DiplomacyController.INTERVAL_PING >= game.getUpdateAt() ) { ping = false; } diff --git a/src/commands/handler/Handler/AutoAliveHandler.ts b/src/commands/handler/Handler/AutoAliveHandler.ts new file mode 100644 index 0000000..8df3d91 --- /dev/null +++ b/src/commands/handler/Handler/AutoAliveHandler.ts @@ -0,0 +1,44 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Snowflake } from 'discord-api-types/v6'; +import { Message, Role } from 'discord.js'; + +export class AutoAliveHandler extends Handler { + + protected static instance: AutoAliveHandler; + + protected readonly ROLE_ID: Snowflake = '648762974277992448'; + + protected role!: Role; + + public title: LangSingle = new LangSingle({ + ru: 'Автоматическое выдача alive', + en: 'Automatic issuance of alive', + uk: 'Автоматичне видача alive' + }); + + public createMessagePerms = HandlerPermissions.init() + .setRoleRequire(this.ROLE_ID, false) + .setAllowAllUsers(true) + .setAllowAllChannels(); + + public static async init () { + if (!this.instance) { + this.instance = new AutoAliveHandler(); + // @ts-ignore + this.instance.role = await guild.roles.fetch(this.ROLE_ID); + if (!this.instance.role) { + console.error('Role Alive not found'); + this.instance.active = false; + } + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + await msg.member?.roles.add(this.role); + } + +} diff --git a/src/commands/handler/Handler/AutoThreadHandler.ts b/src/commands/handler/Handler/AutoThreadHandler.ts new file mode 100644 index 0000000..90dee14 --- /dev/null +++ b/src/commands/handler/Handler/AutoThreadHandler.ts @@ -0,0 +1,49 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Message } from 'discord.js'; + +export class AutoThreadHandler extends Handler { + + protected static instance: AutoThreadHandler; + + protected readonly REGEXP: RegExp = /^\*\*([^\n]+)\*\*\s*\n/i; + + public title: LangSingle = new LangSingle({ + ru: 'Автоматическое создание треда', + en: 'Automatic thread creation', + uk: 'Автоматичне створення треду' + }); + + public createMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowBot() + .setAllowChannel('500300930466709515') + .setAllowChannel('595198087643922445'); + + public static async init () { + if (!this.instance) { + this.instance = new AutoThreadHandler(); + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + const match = msg.content.match(this.REGEXP); + let title = ''; + + if (match) { + title += match[1]; + } else { + if (!msg.member) return; + + const name = msg.member.toString(); + const time = new Date(msg.createdTimestamp).toJSON(); + title += name + ' ' + time; + } + + await msg.startThread({ name: title }); + } + +} diff --git a/src/commands/handler/Handler/DestroyBotHandler.ts b/src/commands/handler/Handler/DestroyBotHandler.ts new file mode 100644 index 0000000..0f44f10 --- /dev/null +++ b/src/commands/handler/Handler/DestroyBotHandler.ts @@ -0,0 +1,43 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Client, Message } from 'discord.js'; + +export class DestroyBotHandler extends Handler { + + protected static instance: DestroyBotHandler; + + public title: LangSingle = new LangSingle({ + ru: 'Прекращение работы бота при обнаружения запуска в другом месте', + en: 'Bot terminates when it detects running elsewhere', + uk: 'Припинення роботи робота при виявленні запуску в іншому місці' + }); + + public static async init (client: Client) { + if (!this.instance) { + this.instance = new DestroyBotHandler(); + this.instance.createMessagePerms = HandlerPermissions.init() + .setAllowUser(client.user.id) + .setAllowBot() + .setAllowChannel('574997373219110922'); + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + if (msg.embeds[0]?.title != 'Бот запущен') return; + // @ts-ignore + if (msg.embeds[0]?.footer?.text == global.sessionId) return; + + await msg.reply({ + content: 'Прекращена работа бота-дубликата у ' + + (process.env.USERNAME ?? 'Host') + }); + + console.error('Прекращение работы бота: бот запущен в другом месте'); + + process.kill(process.pid); + } + +} diff --git a/src/commands/handler/Handler/ElectionsReactionsHandler.ts b/src/commands/handler/Handler/ElectionsReactionsHandler.ts new file mode 100644 index 0000000..b88f0c5 --- /dev/null +++ b/src/commands/handler/Handler/ElectionsReactionsHandler.ts @@ -0,0 +1,39 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Message } from 'discord.js'; + +export class ElectionsReactionsHandler extends Handler { + + protected static instance: ElectionsReactionsHandler; + + public title = new LangSingle({ + ru: 'Прикрепление реакций ЗА и ПРОТИВ на выборах', + en: 'Attaching reactions FOR and AGAINST in the elections', + uk: 'Прикріплення реакцій ЗА та ПРОТИ на виборах' + }); + + public createMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowChannel('612280548777525249'); + + public static async init () { + if (!this.instance) { + this.instance = new ElectionsReactionsHandler(); + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + if (!msg.content.startsWith('<@') || !msg.content.endsWith('>')) return; + + try { + // @ts-ignore + await msg.react(reaction.emoji.Sg3); + // @ts-ignore + await msg.react(reaction.emoji.Sg0); + } catch (e) {} + } + +} diff --git a/src/commands/handler/Handler/Handler.ts b/src/commands/handler/Handler/Handler.ts new file mode 100644 index 0000000..c04c42e --- /dev/null +++ b/src/commands/handler/Handler/Handler.ts @@ -0,0 +1,33 @@ +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Message } from 'discord.js'; + +export abstract class Handler { + + protected active: boolean = true; + + public abstract title: LangSingle; + + public createMessagePerms: HandlerPermissions = HandlerPermissions.init(false); + public updateMessagePerms: HandlerPermissions = HandlerPermissions.init(false); + public deleteMessagePerms: HandlerPermissions = HandlerPermissions.init(false); + + public isActive (): boolean { + return this.active; + } + + public getName (): string { + return this.constructor.name; + } + + public async createMessageHandle (msg: Message): Promise {} + + public async updateMessageHandle (oldMsg: Message, newMsg: Message): Promise {} + + public async deleteMessageHandle (msg: Message): Promise {} + + public shutdown () { + this.active = false; + } + +} \ No newline at end of file diff --git a/src/commands/handler/Handler/HentaiLink.ts b/src/commands/handler/Handler/HentaiLink.ts new file mode 100644 index 0000000..e651bcb --- /dev/null +++ b/src/commands/handler/Handler/HentaiLink.ts @@ -0,0 +1,40 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Message } from 'discord.js'; + +export class HentaiLink extends Handler { + + protected static instance: HentaiLink; + + protected readonly REGEXP: RegExp = /^[0-9]{2,}$/; + + public title: LangSingle = new LangSingle({ + ru: 'Преобразование кода в ссылку', + en: 'Convert code to link', + uk: 'Перетворення коду на посилання' + }); + + public createMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowChannel('681790010550255617', true, true); + + public static async init () { + if (!this.instance) { + this.instance = new HentaiLink(); + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + if (!this.REGEXP.test(msg.content)) return; + if (msg.author.bot) return; + + await msg.reply({ + content: msg.author.toString() + ': https://nhentai.net/g/' + msg.content + '/', + allowedMentions: { parse: [] } + }); + } + +} diff --git a/src/commands/handler/Handler/LevelsHandler.ts b/src/commands/handler/Handler/LevelsHandler.ts new file mode 100644 index 0000000..62fab6f --- /dev/null +++ b/src/commands/handler/Handler/LevelsHandler.ts @@ -0,0 +1,33 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Message } from 'discord.js'; + +export class LevelsHandler extends Handler { + + protected static instance: LevelsHandler; + + public title = new LangSingle({ + ru: 'Отлов сообщения для уровней', + en: 'Catching a message for levels', + uk: 'Вилов повідомлення для рівнів' + }); + + public createMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowAllChannels(true, true); + + public static async init () { + if (!this.instance) { + this.instance = new LevelsHandler(); + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + // @ts-ignore + global.commands.levels.message(msg); + } + +} diff --git a/src/commands/handler/Handler/PingPromoterHandler.ts b/src/commands/handler/Handler/PingPromoterHandler.ts new file mode 100644 index 0000000..c9afd7a --- /dev/null +++ b/src/commands/handler/Handler/PingPromoterHandler.ts @@ -0,0 +1,86 @@ +import { Snowflake } from 'discord-api-types/v6'; +import { Message, Role } from 'discord.js'; +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; + +export class PingPromoterHandler extends Handler { + + protected static instance: PingPromoterHandler; + + protected readonly ROLE_ID: Snowflake = '1107382381700206672'; + protected readonly MONITORING_BOT_ID: Snowflake = '464272403766444044'; + protected readonly COMMAND: string = ''; + protected readonly TIMEOUT: number = 4 * 3600 * 1000; + protected role!: Role; + + public title = new LangSingle({ + ru: 'Автоматическое упоминание людей, готовых продвигать сервер', + en: 'Automatic mention of people who are ready to promote the server', + uk: 'Автоматичне згадування людей, що готові просувати сервер' + }); + + public updateMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowBot() + .setAllowChannel('610371610620198922'); + + public static async init () { + if (!this.instance) { + this.instance = new PingPromoterHandler(); + + // @ts-ignore TODO: ебучие глобал переменные + this.instance.role = await guild.roles.fetch(this.ROLE_ID); + if (!this.instance.role) { + console.error('Role Promoter not found'); + this.instance.active = false; + } + } + + return this.instance; + } + + public async updateMessageHandle (msg: Message): Promise { + if (msg?.member?.user.id !== this.MONITORING_BOT_ID) return; + + // @ts-ignore /functions/log.js + initLog(msg, 'pingPromoter'); + // @ts-ignore + const log = msg.log; + // @ts-ignore /functions/sleep.js + const sleep = global.sleep; + + let embed; + for (let i = 0; i < 5; i++) { + log('Fetch updated message. Try ' + i); + + await sleep(1000); + + try { + const msgUpdated = await msg.channel.messages.fetch(msg.id); + + embed = msgUpdated.embeds[0]; + } catch (e) { + log(e); + return; + } + + if (embed) { + log('Embed found'); + break; + } + } + + if (!embed?.description?.includes('Успешный Up!')) { + log('Embed is invalid. Description: ' + embed?.description); + return; + } + + log('Start sleep'); + await sleep(this.TIMEOUT); + log('End sleep'); + + await msg.channel.send(this.role.toString() + ' ' + this.COMMAND); + } + +} diff --git a/src/commands/handler/Handler/RuleHandler.ts b/src/commands/handler/Handler/RuleHandler.ts new file mode 100644 index 0000000..d25f29e --- /dev/null +++ b/src/commands/handler/Handler/RuleHandler.ts @@ -0,0 +1,56 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Rules, SiteClient } from '../../../libs/Site/SiteClient'; +import { Message } from 'discord.js'; + +export class RuleHandler extends Handler { + + protected static instance: RuleHandler; + + protected siteClient!: SiteClient; + + protected rules: Rules = {}; + + public title = new LangSingle({ + ru: 'Отправляет текст пункта Устава', + en: 'Sends the text of the clause of the Charter', + uk: 'Надсилає текст пункту Статуту' + }); + + public createMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowAllChannels(true, true); + + public static async init () { + if (!this.instance) { + this.instance = new RuleHandler(); + this.instance.siteClient = await SiteClient.init(); + + this.instance.rules = await this.instance.siteClient.fetchRules(); + + if (!this.instance.rules) { + console.error('Error fetch rules from site'); + this.instance.active = false; + } + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + if (msg.content.length < 2) return; + + const ruleKey = msg.content + .replace('а', 'a') + .replace(/^r\.?/, ''); + + if (!this.rules.hasOwnProperty(ruleKey)) return; + + await msg.reply({ + content: this.siteClient.buildRuleUrl(ruleKey), + allowedMentions: { parse: [] } + }); + } + +} diff --git a/src/commands/handler/Handler/UserReactionsHandler.ts b/src/commands/handler/Handler/UserReactionsHandler.ts new file mode 100644 index 0000000..be68268 --- /dev/null +++ b/src/commands/handler/Handler/UserReactionsHandler.ts @@ -0,0 +1,49 @@ +import { Handler } from './Handler'; +import LangSingle from '../../../BaseClasses/LangSingle'; +import { HandlerPermissions } from '../HandlerPermissions'; +import { Message } from 'discord.js'; + +export class UserReactionsHandler extends Handler { + + protected static instance: UserReactionsHandler; + + protected readonly GLOBAL_EMOJI_REGEX: RegExp = /<:[^:]+:([0-9]+)>/gi; + protected readonly LOCAL_EMOJI_REGEX: RegExp = /<:[^:]+:([0-9]+)>/i; + + public title = new LangSingle({ + ru: 'Прикрепление реакций найденных в сообщении', + en: 'Attaching reactions found in the message', + uk: 'Прикріплення реакцій знайдених у повідомленні' + }); + + public createMessagePerms = HandlerPermissions.init() + .setAllowAllUsers(true) + .setAllowChannel('572472723624951839'); + + public static async init () { + if (!this.instance) { + this.instance = new UserReactionsHandler(); + } + + return this.instance; + } + + public async createMessageHandle (msg: Message): Promise { + const emojis = msg.content.match(this.GLOBAL_EMOJI_REGEX); + if (!emojis) return; + if (!msg.guild) return; + + try { + for (const emoji of emojis) { + const matched = emoji.match(this.LOCAL_EMOJI_REGEX); + if (!matched || !matched.length) return; + + const guildEmoji = msg.guild.emojis.cache.get(matched[0]); + if (!guildEmoji) return; + + await msg.react(guildEmoji); + } + } catch (e) {} + } + +} diff --git a/src/commands/handler/HandlerPermissions.ts b/src/commands/handler/HandlerPermissions.ts new file mode 100644 index 0000000..23bec86 --- /dev/null +++ b/src/commands/handler/HandlerPermissions.ts @@ -0,0 +1,180 @@ +import { Snowflake } from 'discord-api-types/v6'; +import { GuildBasedChannel, GuildMemberRoleManager, Message, User } from 'discord.js'; + +interface Rights { + [key: Snowflake]: boolean; +} + +export class HandlerPermissions { + + protected botRight: boolean = false; + + protected allUsers: boolean = false; + protected allChannels: boolean = false; + protected allChannelThreads: boolean = false; + + protected rolesRights: Rights = {}; + protected usersRights: Rights = {}; + protected channelsRights: Rights = {}; + protected channelThreadsRights: Rights = {}; + + protected constructor ( + protected active: boolean = true + ) {} + + public static init (active: boolean = true): HandlerPermissions { + return new HandlerPermissions(active); + } + + /** Проверка доступности вызова */ + public checkAllowed (msg: Message): boolean { + if (!this.active) return false; + if (msg.channel.type === 'DM') return false; + + const isThread = msg.channel.isThread(); + const channel = isThread ? msg.channel.parent : msg.channel; + + if (!channel || !this.checkAllowedChannel(channel, isThread)) return false; + + const member = msg.member; + + if (!member || !member.user || !member.roles) { + return false; + } + + if (!this.checkAllowedUser(member.user)) { + return false; + } + + return this.checkAllowedRoles(member.roles); + } + + /** Allowed **/ + + /** Возможность проверки прав */ + public isActive (): boolean { + return this.active; + } + + /** + * Проверка доступности вызова по роли. + * + * Возвращает false - если: + * * у текущего юзера роль, которой не должно быть. + * * у текущего юзера нет роли, которая требуется. + */ + public checkAllowedRoles (roles: GuildMemberRoleManager): boolean { + for (const roleId in this.rolesRights) { + if (roles.cache.has(roleId) !== this.rolesRights[roleId]) { + return false; + } + } + + return true; + } + + /** + * Проверка доступности вызова по юзеру. + * + * Возвращает false - если: + * * текущий юзер - бот, а ботам запрещено. + * * текущему юзеру запрещено. + * * всем юзерам запрещено, а текущему не разрешено. + */ + public checkAllowedUser (user: User): boolean { + if (user.bot && !this.botRight) { + return false; + } + + if (this.usersRights.hasOwnProperty(user.id)) { + return this.usersRights[user.id]; + } + + return this.allUsers; + } + + /** + * Проверка доступности вызова по каналу. + * + * Возвращает false - если: + * * сообщение в канале или категории, которые запрещён. + * * запрещено везде, а текущий канал или категория не разрешены. + * * сообщение в треде, когда тред для этого канала или категории запрещены. + * * запрещены треды везде, а треды в текущем канале или категории не разрешены. + * + * Важные нюансы: + * * Не может быть разрешения на тред, но запрет на канал треда. + * * Права на канал приоритетнее прав на категорию. + */ + public checkAllowedChannel (channel: GuildBasedChannel, isThread: boolean = false): boolean { + if (isThread && this.channelThreadsRights.hasOwnProperty(channel.id)) { + return this.channelThreadsRights[channel.id]; + } + + if (this.channelsRights.hasOwnProperty(channel.id)) { + return this.channelsRights[channel.id]; + } + + if (channel.parentId) { + if (isThread && this.channelThreadsRights.hasOwnProperty(channel.parentId)) { + return this.channelThreadsRights[channel.parentId]; + } + + if (this.channelsRights.hasOwnProperty(channel.parentId)) { + return this.channelsRights[channel.parentId]; + } + } + + return isThread ? this.allChannelThreads : this.allChannels; + } + + /** allowAll **/ + + /** Установка разрешения для всех пользователей */ + public setAllowAllUsers (allow: boolean = true): HandlerPermissions { + this.allUsers = allow; + return this; + } + + /** Установка разрешения во все каналы и все треды */ + public setAllowAllChannels ( + allow: boolean = true, + allowThreads: boolean = false + ): HandlerPermissions { + this.allChannels = allow; + this.allChannelThreads = allow && allowThreads; + return this; + } + + /** allow **/ + + /** Установка требования наличия или отсутствия роли для разрешения */ + public setRoleRequire (id: Snowflake, needed: boolean = true): HandlerPermissions { + this.rolesRights[id] = needed; + return this; + } + + /** Установка разрешения для юзера */ + public setAllowUser (id: Snowflake, allow: boolean = true): HandlerPermissions { + this.usersRights[id] = allow; + return this; + } + + /** Установка разрешения для канала и тредов */ + public setAllowChannel ( + id: Snowflake, + allow: boolean = true, + allowThreads: boolean = false + ): HandlerPermissions { + this.channelsRights[id] = allow; + this.channelThreadsRights[id] = allow && allowThreads; + return this; + } + + /** Установка разрешения для ботов */ + public setAllowBot (allow: boolean = true): HandlerPermissions { + this.botRight = allow; + return this; + } + +} \ No newline at end of file diff --git a/src/commands/handler/HandlerRepository.ts b/src/commands/handler/HandlerRepository.ts new file mode 100644 index 0000000..72cfad9 --- /dev/null +++ b/src/commands/handler/HandlerRepository.ts @@ -0,0 +1,71 @@ +import { Handler } from './Handler/Handler'; +import { Client } from 'discord.js'; +import { AutoAliveHandler } from './Handler/AutoAliveHandler'; +import { LevelsHandler } from './Handler/LevelsHandler'; +import { ElectionsReactionsHandler } from './Handler/ElectionsReactionsHandler'; +import { PingPromoterHandler } from './Handler/PingPromoterHandler'; +import { UserReactionsHandler } from './Handler/UserReactionsHandler'; +import { RuleHandler } from './Handler/RuleHandler'; +import { AutoThreadHandler } from './Handler/AutoThreadHandler'; +import { DestroyBotHandler } from './Handler/DestroyBotHandler'; +import { HentaiLink } from './Handler/HentaiLink'; + +export class HandlerRepository { + + protected static instance: HandlerRepository; + + public createMessageHandlerSet: Set = new Set(); + public updateMessageHandlerSet: Set = new Set(); + public deleteMessageHandlerSet: Set = new Set(); + + public static async init (client: Client): Promise { + if (!this.instance) { + this.instance = new HandlerRepository(); + + this.instance.addHandler(await PingPromoterHandler.init()); + this.instance.addHandler(await UserReactionsHandler.init()); + this.instance.addHandler(await RuleHandler.init()); + this.instance.addHandler(await DestroyBotHandler.init(client)); + this.instance.addHandler(await AutoThreadHandler.init()); + this.instance.addHandler(await HentaiLink.init()); + this.instance.addHandler(await ElectionsReactionsHandler.init()); + this.instance.addHandler(await AutoAliveHandler.init()); + + this.instance.addHandler(await LevelsHandler.init()); + } + + return this.instance; + } + + protected addHandler (handler: Handler): void { + if (!handler.isActive()) return; + + if (handler.createMessagePerms.isActive()) { + this.createMessageHandlerSet.add(handler); + } + + if (handler.updateMessagePerms.isActive()) { + this.updateMessageHandlerSet.add(handler); + } + + if (handler.deleteMessagePerms.isActive()) { + this.deleteMessageHandlerSet.add(handler); + } + } + + public shutdownHandler (handler: Handler) { + handler.shutdown(); + + if (handler.createMessagePerms) { + this.createMessageHandlerSet.delete(handler); + } + + if (handler.updateMessagePerms) { + this.updateMessageHandlerSet.delete(handler); + } + + if (handler.deleteMessagePerms) { + this.deleteMessageHandlerSet.delete(handler); + } + } +} \ No newline at end of file diff --git a/commands/handler/about.json b/src/commands/handler/about.json similarity index 100% rename from commands/handler/about.json rename to src/commands/handler/about.json diff --git a/src/commands/handler/index.ts b/src/commands/handler/index.ts new file mode 100644 index 0000000..2a66bd0 --- /dev/null +++ b/src/commands/handler/index.ts @@ -0,0 +1,106 @@ +import BaseCommand from '../../BaseClasses/BaseCommand.js'; +import LangSingle from '../../BaseClasses/LangSingle.js'; +import about from './about.json'; +import { Client, Message, PartialMessage } from 'discord.js'; +import { HandlerRepository } from './HandlerRepository'; +import { Snowflake } from 'discord-api-types/v6'; +import { Handler } from './Handler/Handler'; + +export class HandlerCommand extends BaseCommand { + + protected handlerRepository!: HandlerRepository; + protected homeGuildId: Snowflake; + + public constructor (path: string) { + super(path); + + this.category = 'Утилиты'; + this.name = 'handler'; + this.title = this.description = new LangSingle(about.title); + + // @ts-ignore + const client: Client = global.client; + // @ts-ignore + this.homeGuildId = global.guild.id; + + // @ts-ignore @TODO: Надо бы уже избавиться от Promise в constructor + return new Promise(async resolve => { + this.handlerRepository = await HandlerRepository.init(client); + + if (this.handlerRepository.createMessageHandlerSet.size) { + client.on('messageCreate', async (msg) => { + await this.callMessageCreateHandlers(msg); + }); + } + + if (this.handlerRepository.updateMessageHandlerSet.size) { + client.on('messageUpdate', async (oldMsg, newMsg) => { + await this.callMessageUpdateHandlers(oldMsg, newMsg); + }); + } + + if (this.handlerRepository.deleteMessageHandlerSet.size) { + client.on('messageDelete', async (msg) => { + await this.callMessageDeleteHandlers(msg); + }); + } + + resolve(this); + }); + } + + + protected validate ( + msg: Message + ): boolean { + return msg.channel.type !== 'DM' + && msg.channel.guild.id === this.homeGuildId; + } + + protected callMessageCreateHandlers (msg: Message) { + if (this.validate(msg)) { + this.handlerRepository.createMessageHandlerSet.forEach(handler => { + if (handler.createMessagePerms.checkAllowed(msg)) { + handler.createMessageHandle(msg) + .catch(reason => this.handleError(handler, reason)); + } + }); + } + } + + protected callMessageUpdateHandlers (oldMsg: Message | PartialMessage, newMsg: Message | PartialMessage) { + if ( + oldMsg instanceof Message && this.validate(oldMsg) + && newMsg instanceof Message && this.validate(newMsg) + ) { + this.handlerRepository.updateMessageHandlerSet.forEach(handler => { + if (handler.updateMessagePerms.checkAllowed(newMsg)) { + handler.updateMessageHandle(oldMsg, newMsg) + .catch(reason => this.handleError(handler, reason)); + } + }); + } + } + + protected callMessageDeleteHandlers (msg: Message | PartialMessage) { + if (msg instanceof Message && this.validate(msg)) { + this.handlerRepository.deleteMessageHandlerSet.forEach(handler => { + if (handler.deleteMessagePerms.checkAllowed(msg)) { + handler.deleteMessageHandle(msg) + .catch(reason => this.handleError(handler, reason)); + } + }); + } + } + + protected async handleError (handler: Handler, reason: any) { + const error: Error = reason instanceof Error ? reason : new Error(reason); + + // @ts-ignore + const active = await error.handler('handler/' + handler.getName(), false); + + if (!active) { + this.handlerRepository.shutdownHandler(handler); + } + } +} diff --git a/src/libs/Site/SiteClient.ts b/src/libs/Site/SiteClient.ts new file mode 100644 index 0000000..5c6ab08 --- /dev/null +++ b/src/libs/Site/SiteClient.ts @@ -0,0 +1,59 @@ +import { SiteUrls } from './SiteUrls'; + +export interface Rules { + [key: string]: string; +} + +export class SiteClient { + + protected static instance: SiteClient; + + protected active: boolean = true; + protected lastResponse: Response | null = null; + protected lastBody: any = null; + + public static async init (): Promise { + if (!this.instance) { + this.instance = new SiteClient(); + await this.instance.checkStatus(); + } + + return this.instance; + } + + public async checkStatus (): Promise { + const response = await this.fetch(SiteUrls.HOST); + + return this.active = response?.status === 200; + } + + public isActive (): boolean { + return this.active; + } + + public buildRuleUrl (key: string) { + return SiteUrls.HOST + SiteUrls.RULES_KEY + key; + } + + public async fetchRules (): Promise { + const response = await this.fetch(SiteUrls.HOST + SiteUrls.RULES_JSON, true); + + return response?.status === 200 ? this.lastBody : null; + } + + protected async fetch (url: string, json: boolean = false): Promise { + try { + this.lastResponse = await fetch(url, { + redirect: 'manual' + }); + if (json) { + this.lastBody = await this.lastResponse.json(); + } + } catch (e) { + console.error(e); + this.lastResponse = null; + } + + return this.lastResponse; + } +} \ No newline at end of file diff --git a/src/libs/Site/SiteUrls.ts b/src/libs/Site/SiteUrls.ts new file mode 100644 index 0000000..b41e0b9 --- /dev/null +++ b/src/libs/Site/SiteUrls.ts @@ -0,0 +1,9 @@ +export class SiteUrls { + + public static readonly HOST = 'http://old.igc.su'; + + public static readonly RULES_JSON = '/rules?j=true'; + public static readonly RULES_KEY ='/rules?f='; + + +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f1d9d71..dc03aa2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": [ "ES2020.BigInt", - "es2015.iterable" + "es2015.iterable", + "dom" ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */