From c0778edaa2a10eb017ef7067595ee3acc7fe8fe3 Mon Sep 17 00:00:00 2001 From: BVN4 Date: Mon, 25 Dec 2023 01:54:26 +0300 Subject: [PATCH] Rework --- .editorconfig | 6 +- src/commands/starboard/about.json | 7 ++ src/commands/starboard/index.ts | 29 ++++++ src/libs/Controller/ControllerInterface.ts | 4 + src/libs/Controller/StarboardController.ts | 94 +++++++++++++++++++ .../Channel/Repository/ChannelRepository.ts | 21 +++++ .../Message/Repository/MessageRepository.ts | 23 +++++ .../Reaction/Enum/ReactionEmojiEnum.ts | 4 + .../Webhook/Repository/WebhookRepository.ts | 36 +++++++ src/libs/System/BaseClass.ts | 15 +++ src/libs/System/Bot.ts | 21 +++++ src/libs/System/Logger.ts | 86 +++++++++++++++++ src/libs/System/System.ts | 25 +++++ tsconfig.json | 4 +- 14 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 src/commands/starboard/about.json create mode 100644 src/commands/starboard/index.ts create mode 100644 src/libs/Controller/ControllerInterface.ts create mode 100644 src/libs/Controller/StarboardController.ts create mode 100644 src/libs/Discord/Channel/Repository/ChannelRepository.ts create mode 100644 src/libs/Discord/Message/Repository/MessageRepository.ts create mode 100644 src/libs/Discord/Reaction/Enum/ReactionEmojiEnum.ts create mode 100644 src/libs/Discord/Webhook/Repository/WebhookRepository.ts create mode 100644 src/libs/System/BaseClass.ts create mode 100644 src/libs/System/Bot.ts create mode 100644 src/libs/System/Logger.ts create mode 100644 src/libs/System/System.ts diff --git a/.editorconfig b/.editorconfig index 9f58bad..a3aa6ee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,9 +23,7 @@ ij_editorconfig_space_before_comma = false ij_editorconfig_spaces_around_assignment_operators = true [{*.ats,*.cts,*.mts,*.ts}] -indent_size = 4 indent_style = tab -tab_width = 4 ij_continuation_indent_size = 4 ij_typescript_align_imports = false ij_typescript_align_multiline_array_initializer_expression = false @@ -59,7 +57,7 @@ ij_typescript_call_parameters_right_paren_on_new_line = true ij_typescript_call_parameters_wrap = normal ij_typescript_catch_on_new_line = false ij_typescript_chained_call_dot_on_new_line = true -ij_typescript_class_brace_style = end_of_line +ij_typescript_class_brace_style = next_line ij_typescript_comma_on_new_line = false ij_typescript_do_while_brace_force = always ij_typescript_else_on_new_line = false @@ -95,7 +93,7 @@ ij_typescript_keep_simple_blocks_in_one_line = true ij_typescript_keep_simple_methods_in_one_line = true ij_typescript_line_comment_add_space = true ij_typescript_line_comment_at_first_column = false -ij_typescript_method_brace_style = end_of_line +ij_typescript_method_brace_style = next_line_if_wrapped ij_typescript_method_call_chain_wrap = on_every_item ij_typescript_method_parameters_new_line_after_left_paren = true ij_typescript_method_parameters_right_paren_on_new_line = true diff --git a/src/commands/starboard/about.json b/src/commands/starboard/about.json new file mode 100644 index 0000000..9d64d7b --- /dev/null +++ b/src/commands/starboard/about.json @@ -0,0 +1,7 @@ +{ + "title": { + "ru": "Модуль закрепления сообщений", + "en": "Message pinning module", + "uk": "Модуль закріплення повідомлень" + } +} diff --git a/src/commands/starboard/index.ts b/src/commands/starboard/index.ts new file mode 100644 index 0000000..c11163c --- /dev/null +++ b/src/commands/starboard/index.ts @@ -0,0 +1,29 @@ +import BaseCommand from '../../BaseClasses/BaseCommand.js'; +import LangSingle from '../../BaseClasses/LangSingle.js'; + +import about from './about.json'; +import { StarboardController } from '../../libs/Controller/StarboardController'; +import { System } from '../../libs/System/System'; + +export class Starboard extends BaseCommand +{ + protected starboardController: StarboardController; + + public constructor (path: string) + { + super(path); + + this.category = 'Развлечения'; + this.name = 'starboard'; + this.title = this.description = new LangSingle(about.title); + + this.starboardController = System.get(StarboardController); + + // @ts-ignore @FIXME: Жалко конечно, что конструктор возвращает Promise + return new Promise(async resolve => { + await this.starboardController.init(); + + resolve(this); + }); + } +} diff --git a/src/libs/Controller/ControllerInterface.ts b/src/libs/Controller/ControllerInterface.ts new file mode 100644 index 0000000..100604c --- /dev/null +++ b/src/libs/Controller/ControllerInterface.ts @@ -0,0 +1,4 @@ +export interface ControllerInterface +{ + init (): any; +} \ No newline at end of file diff --git a/src/libs/Controller/StarboardController.ts b/src/libs/Controller/StarboardController.ts new file mode 100644 index 0000000..f8efcf5 --- /dev/null +++ b/src/libs/Controller/StarboardController.ts @@ -0,0 +1,94 @@ +import { ControllerInterface } from './ControllerInterface'; +import { ReactionEmojiEnum } from '../Discord/Reaction/Enum/ReactionEmojiEnum'; +import { BaseGuildTextChannel, Message, TextChannel, Webhook } from 'discord.js'; +import { BaseClass } from '../System/BaseClass'; +import { WebhookRepository } from '../Discord/Webhook/Repository/WebhookRepository'; +import { System } from '../System/System'; +import { ChannelRepository } from '../Discord/Channel/Repository/ChannelRepository'; +import { MessageRepository } from '../Discord/Message/Repository/MessageRepository'; + +export class StarboardController extends BaseClass implements ControllerInterface +{ + protected readonly REACTION_EMOJI = ReactionEmojiEnum.STAR; + protected readonly COUNT = 7; + protected readonly CHANNEL_ID = '938171284553101456'; + protected readonly CHANNEL_NAME = 'starboard'; + + protected channelRepository: ChannelRepository; + protected messageRepository: MessageRepository; + protected webhookRepository: WebhookRepository; + + protected channel!: TextChannel; + protected webhook!: Webhook; + + public constructor () + { + super(); + + this.channelRepository = System.get(ChannelRepository); + this.messageRepository = System.get(MessageRepository); + this.webhookRepository = System.get(WebhookRepository); + } + + public async init (): Promise + { + const channel = await this.channelRepository.fetchOrFailById(this.CHANNEL_ID); + if (!(channel instanceof TextChannel)) { + throw new Error('Starboard channel not text channel!'); + } + + this.webhook = await this.webhookRepository.fetchOrCreateWebhook( + this.channel, + this.CHANNEL_NAME, + this.bot.getId() + ); + + this.bot.getClient().on('raw', async data => { + if (data.t !== 'MESSAGE_REACTION_ADD') return; + if (data.d.emoji.name !== this.REACTION_EMOJI) return; + if (data.d.channel_id === this.CHANNEL_ID) return; + + try { + await this.handleReactionAdd(data.d.channel_id, data.d.message_id); + } catch (e) { + this.logger.error(e, data); + } + }); + } + + protected async handleReactionAdd (channelId: string, messageId: string): Promise + { + const channel = await this.channelRepository.fetchOrFailById(channelId); + if (!(channel instanceof BaseGuildTextChannel)) { + throw new Error('Channel type ' + channelId + ' does not match the BaseGuildTextChannel type!'); + } + + const message = await this.messageRepository.fetchOrFailByIdFromChannel(messageId, channel); + + const messageReaction = message.reactions.cache.get(this.REACTION_EMOJI); + if (!messageReaction) { + throw new Error('Message reaction not found!'); + } + + if (messageReaction.count < this.COUNT) return; + + const users = await messageReaction.users.fetch(); + + if (users.get(this.bot.getId())) return; + + if (message.reference?.messageId) { + let reference = await message.channel.messages + .fetch(message.reference.messageId); + await this.createMessage(reference, true); + } + + await this.createMessage(message); + + await message.react(this.REACTION_EMOJI); + } + + protected async createMessage (message: Message, reference: boolean = false): Promise + { + // TODO: logic... + } +} diff --git a/src/libs/Discord/Channel/Repository/ChannelRepository.ts b/src/libs/Discord/Channel/Repository/ChannelRepository.ts new file mode 100644 index 0000000..7928067 --- /dev/null +++ b/src/libs/Discord/Channel/Repository/ChannelRepository.ts @@ -0,0 +1,21 @@ +import { BaseClass } from '../../../System/BaseClass'; +import { GuildBasedChannel } from 'discord.js'; + +export class ChannelRepository extends BaseClass +{ + public constructor () + { + super(); + } + + public async fetchOrFailById (id: string): Promise + { + const channel = await this.bot.getGuild().channels.fetch(id); + + if (channel === null) { + throw new Error('Channel ' + id + ' not found!'); + } + + return channel; + } +} \ No newline at end of file diff --git a/src/libs/Discord/Message/Repository/MessageRepository.ts b/src/libs/Discord/Message/Repository/MessageRepository.ts new file mode 100644 index 0000000..d5aa69e --- /dev/null +++ b/src/libs/Discord/Message/Repository/MessageRepository.ts @@ -0,0 +1,23 @@ +import { BaseClass } from '../../../System/BaseClass'; +import { Message, TextBasedChannelFields } from 'discord.js'; + +export class MessageRepository extends BaseClass +{ + public constructor () + { + super(); + } + + public async fetchOrFailByIdFromChannel (messageId: string, channel: TextBasedChannelFields): Promise + { + // Либа всегда гарантирует Message?) + // Очевидно пиздит, она тут не может такое гарантировать, потому проверяем всё равно + const message = await channel.messages.fetch(messageId); + + if (!message) { + throw new Error('Message ' + messageId + ' not found!'); + } + + return message; + } +} \ No newline at end of file diff --git a/src/libs/Discord/Reaction/Enum/ReactionEmojiEnum.ts b/src/libs/Discord/Reaction/Enum/ReactionEmojiEnum.ts new file mode 100644 index 0000000..20d8591 --- /dev/null +++ b/src/libs/Discord/Reaction/Enum/ReactionEmojiEnum.ts @@ -0,0 +1,4 @@ +export class ReactionEmojiEnum +{ + public static readonly STAR = '⭐'; +} \ No newline at end of file diff --git a/src/libs/Discord/Webhook/Repository/WebhookRepository.ts b/src/libs/Discord/Webhook/Repository/WebhookRepository.ts new file mode 100644 index 0000000..02f6677 --- /dev/null +++ b/src/libs/Discord/Webhook/Repository/WebhookRepository.ts @@ -0,0 +1,36 @@ +import { BaseClass } from '../../../System/BaseClass'; +import { TextBasedChannelFields, Webhook } from 'discord.js'; + +export class WebhookRepository extends BaseClass +{ + public constructor () + { + super(); + } + + public async fetchOrCreateWebhook ( + channel: TextBasedChannelFields, + webhookName: string, + webhookOwnerId: string = '' + ): Promise { + const webhooks = await channel.fetchWebhooks(); + + let webhook = webhooks + .filter(webhook => webhook.name === webhookName && webhook.owner?.id === webhookOwnerId) + .first(); + + if (!webhook) { + this.logger.warn('Starboard webhook not found! Try create...'); + + webhook = await channel.createWebhook(webhookName, { + reason: 'Starboard webhook' + }); + + if (!webhook) { + throw new Error('Can\'t create webhook for Starboard!'); + } + } + + return webhook; + } +} \ No newline at end of file diff --git a/src/libs/System/BaseClass.ts b/src/libs/System/BaseClass.ts new file mode 100644 index 0000000..2eb0f91 --- /dev/null +++ b/src/libs/System/BaseClass.ts @@ -0,0 +1,15 @@ +import { Logger } from './Logger'; +import { Bot } from './Bot'; +import { System } from './System'; + +export abstract class BaseClass +{ + protected logger: Logger; + protected bot: Bot; + + protected constructor () + { + this.logger = System.get(Logger); + this.bot = System.get(Bot); + } +} \ No newline at end of file diff --git a/src/libs/System/Bot.ts b/src/libs/System/Bot.ts new file mode 100644 index 0000000..c1724b8 --- /dev/null +++ b/src/libs/System/Bot.ts @@ -0,0 +1,21 @@ +import { Client, Guild } from 'discord.js'; + +export class Bot +{ + public getClient (): Client + { + // @ts-ignore + return global.client; + } + + public getId (): string + { + return this.getClient().user.id; + } + + public getGuild (): Guild + { + // @ts-ignore + return global.guild; + } +} \ No newline at end of file diff --git a/src/libs/System/Logger.ts b/src/libs/System/Logger.ts new file mode 100644 index 0000000..ee2acf8 --- /dev/null +++ b/src/libs/System/Logger.ts @@ -0,0 +1,86 @@ +/** ANSI escape code */ +export type CodeANSI = string; + +export type LogStr = string | number | unknown; + +export type LogData = any; + +export type LogPrefix = string; + +export interface CodeANSIMap +{ + [key: string]: CodeANSI; +} + +export class Logger +{ + /** ANSI escape code */ + protected readonly C: CodeANSIMap = { + RESET: '\x1b[0m', // Сброс эффектов + BRIGHT: '\x1b[1m', + DIM: '\x1b[2m', + UNDERSCORE: '\x1b[4m', + BLINK: '\x1b[5m', + REVERSE: '\x1b[7m', + HIDDEN: '\x1b[8m' + }; + + /** Цвета текста */ + protected readonly FG: CodeANSIMap = { + BLACK: '\x1b[30m', + RED: '\x1b[31m', + GREEN: '\x1b[32m', + YELLOW: '\x1b[33m', + BLUE: '\x1b[34m', + MAGENTA: '\x1b[35m', + CYAN: '\x1b[36m', + WHITE: '\x1b[37m', + CRIMSON: '\x1b[38m' + }; + + /** Цвет фона */ + protected readonly BG: CodeANSIMap = { + BLACK: '\x1b[40m', + RED: '\x1b[41m', + GREEN: '\x1b[42m', + YELLOW: '\x1b[43m', + BLUE: '\x1b[44m', + MAGENTA: '\x1b[45m', + CYAN: '\x1b[46m', + WHITE: '\x1b[47m', + CRIMSON: '\x1b[48m' + }; + + public constructor ( + protected prefix: LogPrefix = '' + ) + {} + + public send (color: CodeANSI, str: LogStr, data?: LogData): void + { + const msg = this.C.RESET + this.getCurrentTimestamp() + this.prefix + color + str + this.C.RESET; + + data ? console.log(msg, data) : console.log(msg); + } + + public info (str: LogStr, data?: LogData): void + { + this.send(this.FG.CYAN, str, data); + } + + public warn (str: LogStr, data?: LogData): void + { + this.send(this.FG.YELLOW, '[WARN] ' + str, data); + } + + public error (str: LogStr, data?: LogData): void + { + this.send(this.FG.RED, '[ERROR] ' + str, data); + } + + /** Возвращает временную метку в формате MySQL DATETIME */ + public getCurrentTimestamp (): string + { + return new Date().toJSON().replaceAll(/[TZ]/g, ' '); + } +} \ No newline at end of file diff --git a/src/libs/System/System.ts b/src/libs/System/System.ts new file mode 100644 index 0000000..df6231e --- /dev/null +++ b/src/libs/System/System.ts @@ -0,0 +1,25 @@ +import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; + +/** + * Несложный DI контейнер, собственного производства. + * Этот класс может фигурировать только в конструкторах. + */ +export class System +{ + protected static instanceMap = new Map(); + + public static get (c: new (...args: any) => I): I + { + let instance: I | undefined = this.instanceMap.get(c); + + if (instance !== undefined) { + return instance; + } + + instance = new c(); + + this.instanceMap.set(c, instance); + + return instance; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a183b9a..b669950 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,10 @@ "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": [ + "ES2021.String", "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. */