diff --git a/package.json b/package.json index 7a757fd..2570813 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@discordx/importer": "^1.3.0", "@discordx/pagination": "^3.5.1", "better-sqlite3": "^11.0.0", + "chalk": "^4.1.2", "discord.js": "^14.15.3", "discordx": "^11.9.2", "dotenv": "^16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 383e827..9075616 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: better-sqlite3: specifier: ^11.0.0 version: 11.0.0 + chalk: + specifier: ^4.1.2 + version: 4.1.2 discord.js: specifier: ^14.15.3 version: 14.15.3 @@ -45,9 +48,6 @@ importers: prettier: specifier: ^3.3.0 version: 3.3.1 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.2)(typescript@5.4.5) tsup: specifier: ^8.1.0 version: 8.1.0(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5))(typescript@5.4.5) @@ -628,6 +628,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -796,6 +800,10 @@ packages: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -1120,6 +1128,10 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} @@ -1281,6 +1293,7 @@ snapshots: '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + optional: true '@discordjs/builders@1.8.2': dependencies: @@ -1517,6 +1530,7 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -1592,13 +1606,17 @@ snapshots: '@sapphire/snowflake@3.5.3': {} - '@tsconfig/node10@1.0.11': {} + '@tsconfig/node10@1.0.11': + optional: true - '@tsconfig/node12@1.0.11': {} + '@tsconfig/node12@1.0.11': + optional: true - '@tsconfig/node14@1.0.3': {} + '@tsconfig/node14@1.0.3': + optional: true - '@tsconfig/node16@1.0.4': {} + '@tsconfig/node16@1.0.4': + optional: true '@types/estree@1.0.5': {} @@ -1612,9 +1630,11 @@ snapshots: '@vladfrangu/async_event_emitter@2.2.4': {} - acorn-walk@8.3.2: {} + acorn-walk@8.3.2: + optional: true - acorn@8.11.3: {} + acorn@8.11.3: + optional: true ansi-regex@5.0.1: {} @@ -1633,7 +1653,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - arg@4.1.3: {} + arg@4.1.3: + optional: true array-union@2.1.0: {} @@ -1683,6 +1704,11 @@ snapshots: cac@6.7.14: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -1707,7 +1733,8 @@ snapshots: concat-map@0.0.1: {} - create-require@1.1.1: {} + create-require@1.1.1: + optional: true cross-spawn@7.0.3: dependencies: @@ -1729,7 +1756,8 @@ snapshots: detect-libc@2.0.3: {} - diff@4.0.2: {} + diff@4.0.2: + optional: true dir-glob@3.0.1: dependencies: @@ -1908,6 +1936,8 @@ snapshots: has-flag@3.0.0: {} + has-flag@4.0.0: {} + human-signals@2.1.0: {} ieee754@1.2.1: {} @@ -1962,7 +1992,8 @@ snapshots: magic-bytes.js@1.10.0: {} - make-error@1.3.6: {} + make-error@1.3.6: + optional: true merge-stream@2.0.0: {} @@ -2209,6 +2240,10 @@ snapshots: dependencies: has-flag: 3.0.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + tar-fs@2.1.1: dependencies: chownr: 1.1.4 @@ -2265,6 +2300,7 @@ snapshots: typescript: 5.4.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true tslib@1.14.1: {} @@ -2321,7 +2357,8 @@ snapshots: util-deprecate@1.0.2: {} - v8-compile-cache-lib@3.0.1: {} + v8-compile-cache-lib@3.0.1: + optional: true webidl-conversions@4.0.2: {} @@ -2353,4 +2390,5 @@ snapshots: yaml@2.4.3: {} - yn@3.1.1: {} + yn@3.1.1: + optional: true diff --git a/src/commands/admin/create-ticket-system.ts b/src/commands/admin/create-ticket-system.ts index 55f2616..05ed7af 100644 --- a/src/commands/admin/create-ticket-system.ts +++ b/src/commands/admin/create-ticket-system.ts @@ -1,22 +1,29 @@ import { ButtonComponent, Discord, Slash, SlashOption } from 'discordx' import { - ActionRowBuilder, + type AllowedThreadTypeForTextChannel, ApplicationCommandOptionType, - ButtonBuilder, ButtonInteraction, - ButtonStyle, - channelMention, - CommandInteraction, - EmbedBuilder, - GuildMember, - MessageActionRowComponentBuilder, - Role, ChannelType, + CommandInteraction, + GuildMember, + GuildTextThreadCreateOptions, + MessageCreateOptions, + Role, roleMention, userMention, } from 'discord.js' + import { db, DBTableEnum } from '../../db' -import { Status } from '../../utils/enums' +import { + Workload, + ticketCreateButton, + ticketCreatedEmbed, + ticketCreateEmbed, + ticketEntityButton, + ticketEntityEmbed, + ticketWorkloadEmbed, +} from '../../utils' +import { logger } from '../../lib' @Discord() export class CreateTicketSystem { @@ -35,139 +42,151 @@ export class CreateTicketSystem { role: Role, interaction: CommandInteraction, ) { + await interaction.deferReply({ ephemeral: true }) + + // exit on undefined channel + if (!interaction.channel) { + await interaction.editReply( + '❌ Failed to access interaction channel', + ) + return + } + const pricesChannelId = await db.get(DBTableEnum.PRICE_CHANNEL) const bannerURL = await db.get(DBTableEnum.BANNER_URL) - const workload = await db.get(DBTableEnum.WORKLOAD) - await db.set(DBTableEnum.TICKET_ROLE, role.id) + const workload = await db.get(DBTableEnum.WORKLOAD) - // create ticket embed - const createBtn = new ButtonBuilder() - .setLabel('Open a ticket') - .setEmoji('πŸ‘‰') - .setStyle(ButtonStyle.Success) - .setCustomId('create-btn') - - const row = - new ActionRowBuilder().addComponents( - createBtn, + if (!pricesChannelId) { + logger.error( + 'Missing prices channel', + 'Set using /set-prices-channel', ) - - let embedCreate = new EmbedBuilder() - .setTitle(`Create a ticket`) - .setDescription( - `Hey, want to order a design? - Then open a ticket and we'll answer it in a jiffy! - Dear customer, before opening a ticket\n - I strongly recommend that you familiarize yourself with the prices of basic interfaces - ` + - channelMention(pricesChannelId), + await interaction.editReply( + '❌ Missing prices channel\nSet using /set-prices-channel', ) - .setImage(bannerURL) + return + } + if (!bannerURL) { + logger.error('Missing banner', 'Set using /set-banner-url') + await interaction.editReply( + '❌ Missing banner\nSet using /set-banner-url', + ) + return + } + if (!workload) { + logger.error( + 'Missing workload status', + 'Set using /set-workload-status', + ) + await interaction.editReply( + '❌ Missing workload status\nSet using /set-workload-status', + ) + return + } + await db + .set(DBTableEnum.TICKET_ROLE, role.id) + .then(() => logger.database(DBTableEnum.TICKET_ROLE, role.id)) + + // create ticket embed + button await interaction.channel?.send({ - components: [row], - embeds: [embedCreate], + components: [ticketCreateButton()], + embeds: [ticketCreateEmbed({ bannerURL, pricesChannelId })], }) // workload embed - let embedStatus = new EmbedBuilder() - .setTitle(`Workload status by orders`) - .setDescription('**Status:**\n' + getStatusMessage(workload)) - - const workloadMessage = await interaction.channel?.send({ - embeds: [embedStatus], + const workloadMessage = await interaction.channel.send({ + embeds: [ticketWorkloadEmbed({ workload })], }) + await db.set(DBTableEnum.WORKLOAD_MESSAGE, workloadMessage.id) - await db.set(DBTableEnum.WORKLOAD_MESSAGE, workloadMessage?.id) + // close interaction + await interaction.editReply('βœ”οΈ Created ticket system') + } + // listen to create button click + @ButtonComponent({ id: 'create-btn' }) + async createBtn(interaction: ButtonInteraction): Promise { + await interaction.deferReply({ ephemeral: true }) + + // exit on undefined + if ( + !(interaction.member instanceof GuildMember) || + !interaction.channel || + interaction.channel.type !== ChannelType.GuildText + ) { + await interaction.editReply('❌ Failed to access interaction data') + return + } + if (!interaction.guild) { + await interaction.editReply('❌ Failed to access interaction guild') + return + } + + const ticketRole = await db.get(DBTableEnum.TICKET_ROLE) + + // create ticket channel + const threadChannelSettings: GuildTextThreadCreateOptions = + { + name: `${interaction.user.username}`, + type: ChannelType.PrivateThread, + invitable: false, + } + const threadChannel = await interaction.channel.threads.create( + threadChannelSettings, + ) + + // welcoming message in ticket + const threadChannelMessage: MessageCreateOptions = { + content: `${userMention(interaction.user.id)} ${roleMention(ticketRole)}`, + embeds: [ + ticketEntityEmbed({ + username: interaction.user.username, + }), + ], + components: [ticketEntityButton()], + } + await threadChannel.send(threadChannelMessage) + + // add role members and ticket owner + const user = await interaction.guild.members.fetch(interaction.user.id) + const role = await interaction.guild.roles.fetch(ticketRole) + const owner = await interaction.guild.fetchOwner() + + if (!role) { + logger.error( + 'Missing ticket role', + 'Recreate using /create-ticket-system', + ) + } else { + role.members.map( + async member => await threadChannel.members.add(member), + ) + } + + await threadChannel.members.add(user) + await threadChannel.members.add(owner) + + // close interaction await interaction.reply({ - content: 'Created ticket system', + embeds: [ + ticketCreatedEmbed({ + username: interaction.user.username, + threadChannelId: threadChannel.id, + }), + ], ephemeral: true, }) } - // listen to button - @ButtonComponent({ id: 'create-btn' }) - async createBtn(interaction: ButtonInteraction): Promise { - if ( - !(interaction.member instanceof GuildMember) || - interaction.channel?.type !== ChannelType.GuildText - ) { - await interaction.reply('failed') - return - } - - const threadChannel = await interaction.channel.threads.create({ - name: `${interaction.user.username}`, - type: ChannelType.PrivateThread, - invitable: false, - }) - - let createdEmbed = new EmbedBuilder() - .setTitle(`${interaction.user.username} opened a ticket`) - .setDescription( - `You successfully opened a ticket. Your ticket: ${channelMention(threadChannel.id)}`, - ) - - await interaction.reply({ embeds: [createdEmbed], ephemeral: true }) - - let threadEmbed = new EmbedBuilder() - .setTitle(`${interaction.user.username} opened a ticket`) - .setDescription( - `You successfully opened a ticket. We will be with you soon`, - ) - .setFooter({ - text: 'You can close ticket by clicking the button below', - }) - - const ticketRole = await db.get(DBTableEnum.TICKET_ROLE) - - const closeBtn = new ButtonBuilder() - .setLabel('Close ticket') - .setStyle(ButtonStyle.Danger) - .setCustomId('close-btn') - - const row = - new ActionRowBuilder().addComponents( - closeBtn, - ) - - await threadChannel.send({ - content: `${userMention(interaction.user.id)} ${roleMention(ticketRole)}`, - embeds: [threadEmbed], - components: [row], - }) - - const user = await interaction.guild?.members.fetch(interaction.user.id) - const role = await interaction.guild?.roles.fetch(ticketRole) - - role?.members.map( - async members => await threadChannel.members.add(members), - ) - - if (!user) return - - await threadChannel.members.add(user) - } - @ButtonComponent({ id: 'close-btn' }) async closeBtn(interaction: ButtonInteraction): Promise { - console.log('close button') - await interaction.channel?.delete() - - await interaction.reply('Deleted') - } -} - -function getStatusMessage(status: Status | null) { - if (!status) return 'No information provided' - switch (status) { - case Status.AVAILABLE: - return ':green_circle: - Available for orders' - case Status.BUSY: - return ':yellow_circle: - Available for orders, but there may be delays' - case Status.NOT_AVAILABLE: - return ':red_circle: - Currently unavailable for orders' - default: - return 'No information provided' + await interaction.deferReply({ ephemeral: true }) + if (!interaction.channel) { + await interaction.editReply('❌ Ticket channel does not exist') + return + } + await interaction.channel.delete() + await interaction.editReply('Deleted ticket') } } diff --git a/src/commands/admin/set-banner-url.ts b/src/commands/admin/set-banner-url.ts index 4f2c943..de5d4db 100644 --- a/src/commands/admin/set-banner-url.ts +++ b/src/commands/admin/set-banner-url.ts @@ -1,10 +1,8 @@ import { Discord, Slash, SlashOption } from 'discordx' -import { - ApplicationCommandOptionType, - CommandInteraction, - TextChannel, -} from 'discord.js' +import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js' + import { db, DBTableEnum } from '../../db' +import { logger } from '../../lib' @Discord() export class SetFeedbackChannel { @@ -23,7 +21,16 @@ export class SetFeedbackChannel { url: string, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.BANNER_URL, url) - await interaction.reply({ ephemeral: true, content: 'Success.' }) + await interaction.deferReply({ ephemeral: true }) + await db.set(DBTableEnum.BANNER_URL, url).catch(async () => { + await interaction.editReply({ + content: `❌ Failed to set banner`, + }) + return + }) + logger.database(DBTableEnum.BANNER_URL, url) + await interaction.editReply({ + content: `βœ”οΈ Set banner URL to ${url}`, + }) } } diff --git a/src/commands/admin/set-feedback-channel.ts b/src/commands/admin/set-feedback-channel.ts index 56b4ed4..9ffba39 100644 --- a/src/commands/admin/set-feedback-channel.ts +++ b/src/commands/admin/set-feedback-channel.ts @@ -4,7 +4,9 @@ import { CommandInteraction, TextChannel, } from 'discord.js' + import { db, DBTableEnum } from '../../db' +import { logger } from '../../lib' @Discord() export class SetFeedbackChannel { @@ -16,14 +18,25 @@ export class SetFeedbackChannel { async setFeedbackChannel( @SlashOption({ name: 'channel', - description: 'channel description', + description: 'Channel description', type: ApplicationCommandOptionType.Channel, required: true, }) channel: TextChannel, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.FEEDBACK_CHANNEL, channel.id) - await interaction.reply({ ephemeral: true, content: 'Success.' }) + await interaction.deferReply({ ephemeral: true }) + await db + .set(DBTableEnum.FEEDBACK_CHANNEL, channel.id) + .catch(async () => { + await interaction.editReply({ + content: `❌ Failed to set feedback channel`, + }) + return + }) + logger.database(DBTableEnum.FEEDBACK_CHANNEL, channel.id) + await interaction.editReply({ + content: `βœ”οΈ Set feedback channel to ${channel.id}`, + }) } } diff --git a/src/commands/admin/set-order-channel.ts b/src/commands/admin/set-order-channel.ts index bb3540b..69845f3 100644 --- a/src/commands/admin/set-order-channel.ts +++ b/src/commands/admin/set-order-channel.ts @@ -4,26 +4,39 @@ import { CommandInteraction, TextChannel, } from 'discord.js' + import { db, DBTableEnum } from '../../db' +import { logger } from '../../lib' @Discord() export class SetOrderChannel { @Slash({ - description: 'Set a make an order channel', + description: 'Set make an order channel', name: 'set-order-channel', defaultMemberPermissions: 'Administrator', }) async setOrderChannel( @SlashOption({ name: 'channel', - description: 'channel description', + description: 'Make an order channel', type: ApplicationCommandOptionType.Channel, required: true, }) channel: TextChannel, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.MAKE_AN_ORDER_CHANNEL, channel.id) - await interaction.reply({ ephemeral: true, content: 'Success.' }) + await interaction.deferReply({ ephemeral: true }) + await db + .set(DBTableEnum.MAKE_AN_ORDER_CHANNEL, channel.id) + .catch(async () => { + await interaction.editReply({ + content: `❌ Failed to set make an order channel`, + }) + return + }) + logger.database(DBTableEnum.MAKE_AN_ORDER_CHANNEL, channel.id) + await interaction.editReply({ + content: `βœ”οΈ Set make an order channel to ${channel.id}`, + }) } } diff --git a/src/commands/admin/set-portfolio-channel.ts b/src/commands/admin/set-portfolio-channel.ts index 5320342..d9263ab 100644 --- a/src/commands/admin/set-portfolio-channel.ts +++ b/src/commands/admin/set-portfolio-channel.ts @@ -4,7 +4,9 @@ import { CommandInteraction, TextChannel, } from 'discord.js' + import { db, DBTableEnum } from '../../db' +import { logger } from '../../lib' @Discord() export class SetPortfolioChannel { @@ -16,14 +18,25 @@ export class SetPortfolioChannel { async setPortfolioChannel( @SlashOption({ name: 'channel', - description: 'channel description', + description: 'Portfolio channel', type: ApplicationCommandOptionType.Channel, required: true, }) channel: TextChannel, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.PORTFOLIO_CHANNEL, channel.id) - await interaction.reply({ ephemeral: true, content: 'Success.' }) + await interaction.deferReply({ ephemeral: true }) + await db + .set(DBTableEnum.PORTFOLIO_CHANNEL, channel.id) + .catch(async () => { + await interaction.editReply({ + content: `❌ Failed to set portfolio channel`, + }) + return + }) + logger.database(DBTableEnum.PORTFOLIO_CHANNEL, channel.id) + await interaction.editReply({ + content: `βœ”οΈ Set portfolio channel to ${channel.id}`, + }) } } diff --git a/src/commands/admin/set-price-channel.ts b/src/commands/admin/set-price-channel.ts index 1cbff30..af52a60 100644 --- a/src/commands/admin/set-price-channel.ts +++ b/src/commands/admin/set-price-channel.ts @@ -4,7 +4,9 @@ import { CommandInteraction, TextChannel, } from 'discord.js' + import { db, DBTableEnum } from '../../db' +import { logger } from '../../lib' @Discord() export class SetPriceChannel { @@ -16,14 +18,23 @@ export class SetPriceChannel { async setPriceChannel( @SlashOption({ name: 'channel', - description: 'channel description', + description: 'Price channel', type: ApplicationCommandOptionType.Channel, required: true, }) channel: TextChannel, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.PRICE_CHANNEL, channel.id) - await interaction.reply({ ephemeral: true, content: 'Success.' }) + await interaction.deferReply({ ephemeral: true }) + await db.set(DBTableEnum.PRICE_CHANNEL, channel.id).catch(async () => { + await interaction.editReply({ + content: `❌ Failed to set price channel`, + }) + return + }) + logger.database(DBTableEnum.PRICE_CHANNEL, channel.id) + await interaction.editReply({ + content: `βœ”οΈ Set price channel to ${channel.id}`, + }) } } diff --git a/src/commands/admin/set-status.ts b/src/commands/admin/set-status.ts index 0f89aed..679f53a 100644 --- a/src/commands/admin/set-status.ts +++ b/src/commands/admin/set-status.ts @@ -1,7 +1,9 @@ import { Discord, Slash, SlashChoice, SlashOption } from 'discordx' import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js' + import { db, DBTableEnum } from '../../db' -import { Status } from '../../utils/enums.ts' +import { Workload } from '../../utils' +import { logger } from '../../lib' @Discord() export class SetStatus { @@ -11,7 +13,7 @@ export class SetStatus { defaultMemberPermissions: 'Administrator', }) async setPriceChannel( - @SlashChoice(Status.AVAILABLE, Status.BUSY, Status.NOT_AVAILABLE) + @SlashChoice(Workload.AVAILABLE, Workload.BUSY, Workload.NOT_AVAILABLE) @SlashOption({ name: 'status', description: 'Current workload status', @@ -21,10 +23,16 @@ export class SetStatus { status: string, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.WORKLOAD, status).then(x => console.log(x)) - await interaction.reply({ - ephemeral: true, - content: `Status set to: ${status}`, + await interaction.deferReply({ ephemeral: true }) + await db.set(DBTableEnum.WORKLOAD, status).catch(async () => { + await interaction.editReply({ + content: `❌ Failed to workload status`, + }) + return + }) + logger.database(DBTableEnum.WORKLOAD, status) + await interaction.editReply({ + content: `βœ”οΈ Set workload status to ${status}`, }) } } diff --git a/src/commands/admin/set-welcome-channel.ts b/src/commands/admin/set-welcome-channel.ts index ac7ebf9..c6ec421 100644 --- a/src/commands/admin/set-welcome-channel.ts +++ b/src/commands/admin/set-welcome-channel.ts @@ -6,6 +6,7 @@ import { TextChannel, } from 'discord.js' import { db, DBTableEnum } from '../../db' +import { logger } from '../../lib' @Discord() export class SetWelcomeChannel { @@ -17,22 +18,41 @@ export class SetWelcomeChannel { async setWelcomeChannel( @SlashOption({ name: 'channel', - description: 'channel description', + description: 'Welcome channel', type: ApplicationCommandOptionType.Channel, required: true, }) channel: TextChannel, @SlashOption({ name: 'role', - description: 'Role which will be given to user', + description: 'Role which will be given to user on join', type: ApplicationCommandOptionType.Role, required: true, }) role: Role, interaction: CommandInteraction, ) { - await db.set(DBTableEnum.WELCOME_CHANNEL, channel.id) - await db.set(DBTableEnum.WELCOME_ROLE, role.id) - await interaction.reply({ ephemeral: true, content: 'Success.' }) + await interaction.deferReply({ ephemeral: true }) + await db + .set(DBTableEnum.WELCOME_CHANNEL, channel.id) + .catch(async () => { + await interaction.editReply({ + content: `❌ Failed to welcome channel`, + }) + return + }) + logger.database(DBTableEnum.WELCOME_CHANNEL, channel.id) + + await db.set(DBTableEnum.WELCOME_ROLE, role.id).catch(async () => { + await interaction.editReply({ + content: `❌ Failed to entry role`, + }) + return + }) + logger.database(DBTableEnum.WELCOME_ROLE, role.id) + + await interaction.editReply({ + content: `βœ”οΈ Set welcome channel to ${channel.id}\nβœ”οΈ Set entry role to ${role.id}`, + }) } } diff --git a/src/commands/feedback.ts b/src/commands/feedback.ts index 0382445..3980592 100644 --- a/src/commands/feedback.ts +++ b/src/commands/feedback.ts @@ -1,10 +1,9 @@ import { Discord, Slash, SlashChoice, SlashOption } from 'discordx' -import { - ApplicationCommandOptionType, - CommandInteraction, - EmbedBuilder, -} from 'discord.js' +import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js' + import { db, DBTableEnum } from '../db' +import { logger } from '../lib' +import { feedbackEmbed } from '../utils' @Discord() export class Feedback { @@ -16,37 +15,68 @@ export class Feedback { @SlashChoice('⭐', '⭐⭐', '⭐⭐⭐', '⭐⭐⭐⭐', '⭐⭐⭐⭐⭐') @SlashOption({ name: 'rating', - description: 'Leave you review', + description: 'Your review', required: true, type: ApplicationCommandOptionType.String, }) rating: string, @SlashOption({ name: 'description', - description: 'Leave a description about job', + description: 'Leave a review about the job', required: true, type: ApplicationCommandOptionType.String, }) description: string, interaction: CommandInteraction, ) { + await interaction.deferReply({ ephemeral: true }) + + if (!interaction.guild) return + const reviewChannelID = await db.get(DBTableEnum.FEEDBACK_CHANNEL) - - const embed = new EmbedBuilder() - .setTitle(`⭐ Review by ${interaction.user.username}`) - .setDescription(`Author: <@${interaction.user.id}>`) - .setThumbnail(interaction.user.avatarURL()) - .setFields([ - { name: 'Evaluation of support work:', value: rating }, - { name: 'Commentary:', value: '```' + description + '```' }, - ]) - - const channel = await interaction.guild?.channels.fetch(reviewChannelID) - - if (channel?.isTextBased()) { - channel.send({ embeds: [embed] }) + if (!reviewChannelID) { + logger.error( + 'Missing feedback channel in database', + 'Recreate using /set-feedback-channel', + ) + await interaction.editReply( + '❌ Feedback channel is not set. Please try again later', + ) } - await interaction.reply({ ephemeral: true, content: 'Review sent!' }) + const reviewChannel = + await interaction.guild.channels.fetch(reviewChannelID) + if (!reviewChannel) { + logger.error( + 'Missing feedback discord channel', + `Feedback channel id exists in database (${reviewChannelID}) but is not found in channels list`, + ) + await interaction.editReply( + '❌ Feedback channel is not set. Please try again later', + ) + } + + if (!reviewChannel || !reviewChannel.isTextBased()) { + logger.error( + 'Missing feedback discord channel', + `Feedback channel id exists in database (${reviewChannelID}) but is not a text channel or is not found in channels list`, + ) + await interaction.editReply( + '❌ Feedback channel is not set. Please try again later', + ) + return + } + + await reviewChannel.send({ + embeds: [ + feedbackEmbed({ + user: interaction.user, + description, + rating, + }), + ], + }) + + await interaction.editReply('βœ”οΈ Review sent successfully!') } } diff --git a/src/commands/payments.ts b/src/commands/payments.ts index b698f2e..207ad9b 100644 --- a/src/commands/payments.ts +++ b/src/commands/payments.ts @@ -1,5 +1,7 @@ import { Discord, Slash } from 'discordx' -import { CommandInteraction, EmbedBuilder } from 'discord.js' +import { CommandInteraction } from 'discord.js' + +import { paymentsEmbed } from '../utils' @Discord() export class Payments { @@ -8,20 +10,7 @@ export class Payments { description: 'See available payments', }) async payments(interaction: CommandInteraction) { - const embed = new EmbedBuilder().setTitle(`Payments`).setFields([ - { - name: 'Crypto:', - value: - 'Wallet number: `TRdGKNPABvoTrdwHgfUTX65DbqbguTh6cc`\n' + - 'Crypto name: `USDT`\n' + - 'Network name: `TRC20`', - }, - { - name: 'PayPal:', - value: '`khamidalakkhali@gmail.com` | Types: Friends and Family', - }, - ]) - - await interaction.reply({ embeds: [embed] }) + await interaction.deferReply() + await interaction.editReply({ embeds: [paymentsEmbed()] }) } } diff --git a/src/commands/ping.ts b/src/commands/ping.ts index a2bb0b3..f639d51 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -5,13 +5,13 @@ import { CommandInteraction } from 'discord.js' export class Ping { @Slash({ name: 'ping', - description: 'ping the bot', + description: 'Show ping (ms)', defaultMemberPermissions: ['Administrator'], }) async ping(interaction: CommandInteraction) { await interaction.deferReply({ ephemeral: true }) await interaction.editReply({ - content: `πŸ“Pong! ${interaction.client.ws.ping} ms`, + content: `πŸ“ Pong! ${interaction.client.ws.ping} ms`, }) } } diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..488d888 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +export * from './logger' diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..0f3d2b8 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,15 @@ +import c from 'chalk' + +class Logger { + error(msg: string, err: string) { + console.log(c.red.bold('Error: ') + c.red(msg) + '\n' + err + '\n') + } + + database(field: string, value: string) { + console.log( + c.cyan.bold('Entry: ') + c.cyan(field) + '\n' + value + '\n', + ) + } +} + +export const logger = new Logger() diff --git a/src/utils/embeds/feedback.ts b/src/utils/embeds/feedback.ts new file mode 100644 index 0000000..783a61e --- /dev/null +++ b/src/utils/embeds/feedback.ts @@ -0,0 +1,22 @@ +import { EmbedBuilder, User } from 'discord.js' + +type FeedbackEmbedProps = { + user: User + rating: string + description: string +} + +export function feedbackEmbed({ + user, + rating, + description, +}: FeedbackEmbedProps) { + return new EmbedBuilder() + .setTitle(`⭐ Review by ${user.username}`) + .setDescription(`Author: <@${user.id}>`) + .setThumbnail(user.avatarURL()) + .setFields([ + { name: 'Evaluation of work:', value: rating }, + { name: 'Commentary:', value: '```' + description + '```' }, + ]) +} diff --git a/src/utils/embeds/index.ts b/src/utils/embeds/index.ts new file mode 100644 index 0000000..df67e0e --- /dev/null +++ b/src/utils/embeds/index.ts @@ -0,0 +1,3 @@ +export * from './tickets' +export * from './feedback' +export * from './payments' diff --git a/src/utils/embeds/payments.ts b/src/utils/embeds/payments.ts new file mode 100644 index 0000000..296e1e4 --- /dev/null +++ b/src/utils/embeds/payments.ts @@ -0,0 +1,17 @@ +import { EmbedBuilder } from 'discord.js' + +export function paymentsEmbed() { + return new EmbedBuilder().setTitle(`Payments`).setFields([ + { + name: 'Crypto:', + value: + 'Wallet number: `TRdGKNPABvoTrdwHgfUTX65DbqbguTh6cc`\n' + + 'Crypto name: `USDT`\n' + + 'Network name: `TRC20`', + }, + { + name: 'PayPal:', + value: '`khamidalakkhali@gmail.com` | Types: Friends and Family', + }, + ]) +} diff --git a/src/utils/embeds/tickets.ts b/src/utils/embeds/tickets.ts new file mode 100644 index 0000000..07f211d --- /dev/null +++ b/src/utils/embeds/tickets.ts @@ -0,0 +1,108 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + channelMention, + EmbedBuilder, + MessageActionRowComponentBuilder, +} from 'discord.js' +import { Workload } from '../enums' + +export function ticketCreateButton() { + const createBtn = new ButtonBuilder() + .setLabel('Open a ticket') + .setEmoji('πŸ‘‰') + .setStyle(ButtonStyle.Success) + .setCustomId('create-btn') + + return new ActionRowBuilder().addComponents( + createBtn, + ) +} + +type TicketCreateEmbedProps = { + pricesChannelId: string + bannerURL: string +} + +export function ticketCreateEmbed({ + pricesChannelId, + bannerURL, +}: TicketCreateEmbedProps) { + return new EmbedBuilder() + .setTitle(`Create a ticket`) + .setDescription( + `Hey, want to order a design? + Then open a ticket and we'll answer it in a jiffy! + Dear customer, before opening a ticket\n + I strongly recommend that you familiarize yourself with the prices of basic interfaces - ` + + channelMention(pricesChannelId), + ) + .setImage(bannerURL) +} + +type TicketWorkloadEmbedProps = { + workload: Workload | null +} + +export function ticketWorkloadEmbed({ workload }: TicketWorkloadEmbedProps) { + return new EmbedBuilder() + .setTitle(`Workload status by orders`) + .setDescription('**Status:**\n' + getStatusMessage(workload)) + + function getStatusMessage(status: Workload | null) { + if (!status) return 'No information provided' + switch (status) { + case Workload.AVAILABLE: + return ':green_circle: - Available for orders' + case Workload.BUSY: + return ':yellow_circle: - Available for orders, but there may be delays' + case Workload.NOT_AVAILABLE: + return ':red_circle: - Currently unavailable for orders' + default: + return 'No information provided' + } + } +} + +type TicketCreatedEmbedProps = { + username: string + threadChannelId: string +} + +export function ticketCreatedEmbed({ + username, + threadChannelId, +}: TicketCreatedEmbedProps) { + return new EmbedBuilder() + .setTitle(`${username} opened a ticket`) + .setDescription( + `You successfully opened a ticket. Your ticket: ${channelMention(threadChannelId)}`, + ) +} + +type TicketEntityEmbedProps = { + username: string +} + +export function ticketEntityEmbed({ username }: TicketEntityEmbedProps) { + return new EmbedBuilder() + .setTitle(`${username} opened a ticket`) + .setDescription( + `You successfully opened a ticket. We will be with you soon`, + ) + .setFooter({ + text: 'You can close ticket by clicking the button below', + }) +} + +export function ticketEntityButton() { + const closeBtn = new ButtonBuilder() + .setLabel('Close ticket') + .setStyle(ButtonStyle.Danger) + .setCustomId('close-btn') + + return new ActionRowBuilder().addComponents( + closeBtn, + ) +} diff --git a/src/utils/enums.ts b/src/utils/enums.ts index ac7c84a..c094fad 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -1,4 +1,4 @@ -export enum Status { +export enum Workload { AVAILABLE = 'AVAILABLE', BUSY = 'BUSY', NOT_AVAILABLE = 'NOT_AVAILABLE', diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..21b03fa --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './embeds' +export * from './enums'