Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/commands/starboard/about.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": {
"ru": "Модуль закрепления сообщений",
"en": "Message pinning module",
"uk": "Модуль закріплення повідомлень"
}
}
29 changes: 29 additions & 0 deletions src/commands/starboard/index.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
4 changes: 4 additions & 0 deletions src/libs/Controller/ControllerInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ControllerInterface
{
init (): any;
}
94 changes: 94 additions & 0 deletions src/libs/Controller/StarboardController.ts
Original file line number Diff line number Diff line change
@@ -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<void>
{
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<void>
{
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<void>
{
// TODO: logic...
}
}
21 changes: 21 additions & 0 deletions src/libs/Discord/Channel/Repository/ChannelRepository.ts
Original file line number Diff line number Diff line change
@@ -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<GuildBasedChannel>
{
const channel = await this.bot.getGuild().channels.fetch(id);

if (channel === null) {
throw new Error('Channel ' + id + ' not found!');
}

return channel;
}
}
23 changes: 23 additions & 0 deletions src/libs/Discord/Message/Repository/MessageRepository.ts
Original file line number Diff line number Diff line change
@@ -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>
{
// Либа всегда гарантирует Message?)
// Очевидно пиздит, она тут не может такое гарантировать, потому проверяем всё равно
const message = await channel.messages.fetch(messageId);

if (!message) {
throw new Error('Message ' + messageId + ' not found!');
}

return message;
}
}
4 changes: 4 additions & 0 deletions src/libs/Discord/Reaction/Enum/ReactionEmojiEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class ReactionEmojiEnum
{
public static readonly STAR = '⭐';
}
36 changes: 36 additions & 0 deletions src/libs/Discord/Webhook/Repository/WebhookRepository.ts
Original file line number Diff line number Diff line change
@@ -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<Webhook> {
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;
}
}
15 changes: 15 additions & 0 deletions src/libs/System/BaseClass.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
21 changes: 21 additions & 0 deletions src/libs/System/Bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Client, Guild } from 'discord.js';

export class Bot
{
public getClient (): Client<true>
{
// @ts-ignore
return global.client;
}

public getId (): string
{
return this.getClient().user.id;
}

public getGuild (): Guild
{
// @ts-ignore
return global.guild;
}
}
86 changes: 86 additions & 0 deletions src/libs/System/Logger.ts
Original file line number Diff line number Diff line change
@@ -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, ' ');
}
}
Loading