error/missing data handling + extracted embeds + better interaction replies

This commit is contained in:
Danya H 2024-06-06 23:41:17 +01:00
parent 2b341e5107
commit c1bfce676f
21 changed files with 541 additions and 211 deletions

View File

@ -14,6 +14,7 @@
"@discordx/importer": "^1.3.0", "@discordx/importer": "^1.3.0",
"@discordx/pagination": "^3.5.1", "@discordx/pagination": "^3.5.1",
"better-sqlite3": "^11.0.0", "better-sqlite3": "^11.0.0",
"chalk": "^4.1.2",
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"discordx": "^11.9.2", "discordx": "^11.9.2",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",

View File

@ -17,6 +17,9 @@ importers:
better-sqlite3: better-sqlite3:
specifier: ^11.0.0 specifier: ^11.0.0
version: 11.0.0 version: 11.0.0
chalk:
specifier: ^4.1.2
version: 4.1.2
discord.js: discord.js:
specifier: ^14.15.3 specifier: ^14.15.3
version: 14.15.3 version: 14.15.3
@ -45,9 +48,6 @@ importers:
prettier: prettier:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.1 version: 3.3.1
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.14.2)(typescript@5.4.5)
tsup: tsup:
specifier: ^8.1.0 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) 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==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chokidar@3.6.0: chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
@ -796,6 +800,10 @@ packages:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'} engines: {node: '>=4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
human-signals@2.1.0: human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
@ -1120,6 +1128,10 @@ packages:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'} engines: {node: '>=4'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
tar-fs@2.1.1: tar-fs@2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
@ -1281,6 +1293,7 @@ snapshots:
'@cspotcode/source-map-support@0.8.1': '@cspotcode/source-map-support@0.8.1':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
optional: true
'@discordjs/builders@1.8.2': '@discordjs/builders@1.8.2':
dependencies: dependencies:
@ -1517,6 +1530,7 @@ snapshots:
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
optional: true
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
@ -1592,13 +1606,17 @@ snapshots:
'@sapphire/snowflake@3.5.3': {} '@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': {} '@types/estree@1.0.5': {}
@ -1612,9 +1630,11 @@ snapshots:
'@vladfrangu/async_event_emitter@2.2.4': {} '@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: {} ansi-regex@5.0.1: {}
@ -1633,7 +1653,8 @@ snapshots:
normalize-path: 3.0.0 normalize-path: 3.0.0
picomatch: 2.3.1 picomatch: 2.3.1
arg@4.1.3: {} arg@4.1.3:
optional: true
array-union@2.1.0: {} array-union@2.1.0: {}
@ -1683,6 +1704,11 @@ snapshots:
cac@6.7.14: {} cac@6.7.14: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chokidar@3.6.0: chokidar@3.6.0:
dependencies: dependencies:
anymatch: 3.1.3 anymatch: 3.1.3
@ -1707,7 +1733,8 @@ snapshots:
concat-map@0.0.1: {} concat-map@0.0.1: {}
create-require@1.1.1: {} create-require@1.1.1:
optional: true
cross-spawn@7.0.3: cross-spawn@7.0.3:
dependencies: dependencies:
@ -1729,7 +1756,8 @@ snapshots:
detect-libc@2.0.3: {} detect-libc@2.0.3: {}
diff@4.0.2: {} diff@4.0.2:
optional: true
dir-glob@3.0.1: dir-glob@3.0.1:
dependencies: dependencies:
@ -1908,6 +1936,8 @@ snapshots:
has-flag@3.0.0: {} has-flag@3.0.0: {}
has-flag@4.0.0: {}
human-signals@2.1.0: {} human-signals@2.1.0: {}
ieee754@1.2.1: {} ieee754@1.2.1: {}
@ -1962,7 +1992,8 @@ snapshots:
magic-bytes.js@1.10.0: {} magic-bytes.js@1.10.0: {}
make-error@1.3.6: {} make-error@1.3.6:
optional: true
merge-stream@2.0.0: {} merge-stream@2.0.0: {}
@ -2209,6 +2240,10 @@ snapshots:
dependencies: dependencies:
has-flag: 3.0.0 has-flag: 3.0.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
tar-fs@2.1.1: tar-fs@2.1.1:
dependencies: dependencies:
chownr: 1.1.4 chownr: 1.1.4
@ -2265,6 +2300,7 @@ snapshots:
typescript: 5.4.5 typescript: 5.4.5
v8-compile-cache-lib: 3.0.1 v8-compile-cache-lib: 3.0.1
yn: 3.1.1 yn: 3.1.1
optional: true
tslib@1.14.1: {} tslib@1.14.1: {}
@ -2321,7 +2357,8 @@ snapshots:
util-deprecate@1.0.2: {} 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: {} webidl-conversions@4.0.2: {}
@ -2353,4 +2390,5 @@ snapshots:
yaml@2.4.3: {} yaml@2.4.3: {}
yn@3.1.1: {} yn@3.1.1:
optional: true

View File

@ -1,22 +1,29 @@
import { ButtonComponent, Discord, Slash, SlashOption } from 'discordx' import { ButtonComponent, Discord, Slash, SlashOption } from 'discordx'
import { import {
ActionRowBuilder, type AllowedThreadTypeForTextChannel,
ApplicationCommandOptionType, ApplicationCommandOptionType,
ButtonBuilder,
ButtonInteraction, ButtonInteraction,
ButtonStyle,
channelMention,
CommandInteraction,
EmbedBuilder,
GuildMember,
MessageActionRowComponentBuilder,
Role,
ChannelType, ChannelType,
CommandInteraction,
GuildMember,
GuildTextThreadCreateOptions,
MessageCreateOptions,
Role,
roleMention, roleMention,
userMention, userMention,
} from 'discord.js' } from 'discord.js'
import { db, DBTableEnum } from '../../db' 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() @Discord()
export class CreateTicketSystem { export class CreateTicketSystem {
@ -35,139 +42,151 @@ export class CreateTicketSystem {
role: Role, role: Role,
interaction: CommandInteraction, 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 pricesChannelId = await db.get(DBTableEnum.PRICE_CHANNEL)
const bannerURL = await db.get(DBTableEnum.BANNER_URL) const bannerURL = await db.get(DBTableEnum.BANNER_URL)
const workload = await db.get<Status>(DBTableEnum.WORKLOAD) const workload = await db.get<Workload>(DBTableEnum.WORKLOAD)
await db.set(DBTableEnum.TICKET_ROLE, role.id)
// create ticket embed if (!pricesChannelId) {
const createBtn = new ButtonBuilder() logger.error(
.setLabel('Open a ticket') 'Missing prices channel',
.setEmoji('👉') 'Set using /set-prices-channel',
.setStyle(ButtonStyle.Success)
.setCustomId('create-btn')
const row =
new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
createBtn,
) )
await interaction.editReply(
let embedCreate = new EmbedBuilder() '❌ Missing prices channel\nSet using /set-prices-channel',
.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) 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({ await interaction.channel?.send({
components: [row], components: [ticketCreateButton()],
embeds: [embedCreate], embeds: [ticketCreateEmbed({ bannerURL, pricesChannelId })],
}) })
// workload embed // workload embed
let embedStatus = new EmbedBuilder() const workloadMessage = await interaction.channel.send({
.setTitle(`Workload status by orders`) embeds: [ticketWorkloadEmbed({ workload })],
.setDescription('**Status:**\n' + getStatusMessage(workload))
const workloadMessage = await interaction.channel?.send({
embeds: [embedStatus],
}) })
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<void> {
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<AllowedThreadTypeForTextChannel> =
{
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({ await interaction.reply({
content: 'Created ticket system', embeds: [
ticketCreatedEmbed({
username: interaction.user.username,
threadChannelId: threadChannel.id,
}),
],
ephemeral: true, ephemeral: true,
}) })
} }
// listen to button
@ButtonComponent({ id: 'create-btn' })
async createBtn(interaction: ButtonInteraction): Promise<void> {
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<MessageActionRowComponentBuilder>().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' }) @ButtonComponent({ id: 'close-btn' })
async closeBtn(interaction: ButtonInteraction): Promise<void> { async closeBtn(interaction: ButtonInteraction): Promise<void> {
console.log('close button') await interaction.deferReply({ ephemeral: true })
await interaction.channel?.delete() if (!interaction.channel) {
await interaction.editReply('❌ Ticket channel does not exist')
await interaction.reply('Deleted') return
} }
} await interaction.channel.delete()
await interaction.editReply('Deleted ticket')
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'
} }
} }

View File

@ -1,10 +1,8 @@
import { Discord, Slash, SlashOption } from 'discordx' import { Discord, Slash, SlashOption } from 'discordx'
import { import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js'
ApplicationCommandOptionType,
CommandInteraction,
TextChannel,
} from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetFeedbackChannel { export class SetFeedbackChannel {
@ -23,7 +21,16 @@ export class SetFeedbackChannel {
url: string, url: string,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.BANNER_URL, url) await interaction.deferReply({ ephemeral: true })
await interaction.reply({ ephemeral: true, content: 'Success.' }) 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}`,
})
} }
} }

View File

@ -4,7 +4,9 @@ import {
CommandInteraction, CommandInteraction,
TextChannel, TextChannel,
} from 'discord.js' } from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetFeedbackChannel { export class SetFeedbackChannel {
@ -16,14 +18,25 @@ export class SetFeedbackChannel {
async setFeedbackChannel( async setFeedbackChannel(
@SlashOption({ @SlashOption({
name: 'channel', name: 'channel',
description: 'channel description', description: 'Channel description',
type: ApplicationCommandOptionType.Channel, type: ApplicationCommandOptionType.Channel,
required: true, required: true,
}) })
channel: TextChannel, channel: TextChannel,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.FEEDBACK_CHANNEL, channel.id) await interaction.deferReply({ ephemeral: true })
await interaction.reply({ ephemeral: true, content: 'Success.' }) 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}`,
})
} }
} }

View File

@ -4,26 +4,39 @@ import {
CommandInteraction, CommandInteraction,
TextChannel, TextChannel,
} from 'discord.js' } from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetOrderChannel { export class SetOrderChannel {
@Slash({ @Slash({
description: 'Set a make an order channel', description: 'Set make an order channel',
name: 'set-order-channel', name: 'set-order-channel',
defaultMemberPermissions: 'Administrator', defaultMemberPermissions: 'Administrator',
}) })
async setOrderChannel( async setOrderChannel(
@SlashOption({ @SlashOption({
name: 'channel', name: 'channel',
description: 'channel description', description: 'Make an order channel',
type: ApplicationCommandOptionType.Channel, type: ApplicationCommandOptionType.Channel,
required: true, required: true,
}) })
channel: TextChannel, channel: TextChannel,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.MAKE_AN_ORDER_CHANNEL, channel.id) await interaction.deferReply({ ephemeral: true })
await interaction.reply({ ephemeral: true, content: 'Success.' }) 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}`,
})
} }
} }

View File

@ -4,7 +4,9 @@ import {
CommandInteraction, CommandInteraction,
TextChannel, TextChannel,
} from 'discord.js' } from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetPortfolioChannel { export class SetPortfolioChannel {
@ -16,14 +18,25 @@ export class SetPortfolioChannel {
async setPortfolioChannel( async setPortfolioChannel(
@SlashOption({ @SlashOption({
name: 'channel', name: 'channel',
description: 'channel description', description: 'Portfolio channel',
type: ApplicationCommandOptionType.Channel, type: ApplicationCommandOptionType.Channel,
required: true, required: true,
}) })
channel: TextChannel, channel: TextChannel,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.PORTFOLIO_CHANNEL, channel.id) await interaction.deferReply({ ephemeral: true })
await interaction.reply({ ephemeral: true, content: 'Success.' }) 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}`,
})
} }
} }

View File

@ -4,7 +4,9 @@ import {
CommandInteraction, CommandInteraction,
TextChannel, TextChannel,
} from 'discord.js' } from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetPriceChannel { export class SetPriceChannel {
@ -16,14 +18,23 @@ export class SetPriceChannel {
async setPriceChannel( async setPriceChannel(
@SlashOption({ @SlashOption({
name: 'channel', name: 'channel',
description: 'channel description', description: 'Price channel',
type: ApplicationCommandOptionType.Channel, type: ApplicationCommandOptionType.Channel,
required: true, required: true,
}) })
channel: TextChannel, channel: TextChannel,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.PRICE_CHANNEL, channel.id) await interaction.deferReply({ ephemeral: true })
await interaction.reply({ ephemeral: true, content: 'Success.' }) 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}`,
})
} }
} }

View File

@ -1,7 +1,9 @@
import { Discord, Slash, SlashChoice, SlashOption } from 'discordx' import { Discord, Slash, SlashChoice, SlashOption } from 'discordx'
import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js' import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { Status } from '../../utils/enums.ts' import { Workload } from '../../utils'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetStatus { export class SetStatus {
@ -11,7 +13,7 @@ export class SetStatus {
defaultMemberPermissions: 'Administrator', defaultMemberPermissions: 'Administrator',
}) })
async setPriceChannel( async setPriceChannel(
@SlashChoice(Status.AVAILABLE, Status.BUSY, Status.NOT_AVAILABLE) @SlashChoice(Workload.AVAILABLE, Workload.BUSY, Workload.NOT_AVAILABLE)
@SlashOption({ @SlashOption({
name: 'status', name: 'status',
description: 'Current workload status', description: 'Current workload status',
@ -21,10 +23,16 @@ export class SetStatus {
status: string, status: string,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.WORKLOAD, status).then(x => console.log(x)) await interaction.deferReply({ ephemeral: true })
await interaction.reply({ await db.set(DBTableEnum.WORKLOAD, status).catch(async () => {
ephemeral: true, await interaction.editReply({
content: `Status set to: ${status}`, content: `❌ Failed to workload status`,
})
return
})
logger.database(DBTableEnum.WORKLOAD, status)
await interaction.editReply({
content: `✔️ Set workload status to ${status}`,
}) })
} }
} }

View File

@ -6,6 +6,7 @@ import {
TextChannel, TextChannel,
} from 'discord.js' } from 'discord.js'
import { db, DBTableEnum } from '../../db' import { db, DBTableEnum } from '../../db'
import { logger } from '../../lib'
@Discord() @Discord()
export class SetWelcomeChannel { export class SetWelcomeChannel {
@ -17,22 +18,41 @@ export class SetWelcomeChannel {
async setWelcomeChannel( async setWelcomeChannel(
@SlashOption({ @SlashOption({
name: 'channel', name: 'channel',
description: 'channel description', description: 'Welcome channel',
type: ApplicationCommandOptionType.Channel, type: ApplicationCommandOptionType.Channel,
required: true, required: true,
}) })
channel: TextChannel, channel: TextChannel,
@SlashOption({ @SlashOption({
name: 'role', name: 'role',
description: 'Role which will be given to user', description: 'Role which will be given to user on join',
type: ApplicationCommandOptionType.Role, type: ApplicationCommandOptionType.Role,
required: true, required: true,
}) })
role: Role, role: Role,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await db.set(DBTableEnum.WELCOME_CHANNEL, channel.id) await interaction.deferReply({ ephemeral: true })
await db.set(DBTableEnum.WELCOME_ROLE, role.id) await db
await interaction.reply({ ephemeral: true, content: 'Success.' }) .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}`,
})
} }
} }

View File

@ -1,10 +1,9 @@
import { Discord, Slash, SlashChoice, SlashOption } from 'discordx' import { Discord, Slash, SlashChoice, SlashOption } from 'discordx'
import { import { ApplicationCommandOptionType, CommandInteraction } from 'discord.js'
ApplicationCommandOptionType,
CommandInteraction,
EmbedBuilder,
} from 'discord.js'
import { db, DBTableEnum } from '../db' import { db, DBTableEnum } from '../db'
import { logger } from '../lib'
import { feedbackEmbed } from '../utils'
@Discord() @Discord()
export class Feedback { export class Feedback {
@ -16,37 +15,68 @@ export class Feedback {
@SlashChoice('⭐', '⭐⭐', '⭐⭐⭐', '⭐⭐⭐⭐', '⭐⭐⭐⭐⭐') @SlashChoice('⭐', '⭐⭐', '⭐⭐⭐', '⭐⭐⭐⭐', '⭐⭐⭐⭐⭐')
@SlashOption({ @SlashOption({
name: 'rating', name: 'rating',
description: 'Leave you review', description: 'Your review',
required: true, required: true,
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
}) })
rating: string, rating: string,
@SlashOption({ @SlashOption({
name: 'description', name: 'description',
description: 'Leave a description about job', description: 'Leave a review about the job',
required: true, required: true,
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
}) })
description: string, description: string,
interaction: CommandInteraction, interaction: CommandInteraction,
) { ) {
await interaction.deferReply({ ephemeral: true })
if (!interaction.guild) return
const reviewChannelID = await db.get(DBTableEnum.FEEDBACK_CHANNEL) const reviewChannelID = await db.get(DBTableEnum.FEEDBACK_CHANNEL)
if (!reviewChannelID) {
const embed = new EmbedBuilder() logger.error(
.setTitle(`⭐ Review by ${interaction.user.username}`) 'Missing feedback channel in database',
.setDescription(`Author: <@${interaction.user.id}>`) 'Recreate using /set-feedback-channel',
.setThumbnail(interaction.user.avatarURL()) )
.setFields([ await interaction.editReply(
{ name: 'Evaluation of support work:', value: rating }, '❌ Feedback channel is not set. Please try again later',
{ name: 'Commentary:', value: '```' + description + '```' }, )
])
const channel = await interaction.guild?.channels.fetch(reviewChannelID)
if (channel?.isTextBased()) {
channel.send({ embeds: [embed] })
} }
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!')
} }
} }

View File

@ -1,5 +1,7 @@
import { Discord, Slash } from 'discordx' import { Discord, Slash } from 'discordx'
import { CommandInteraction, EmbedBuilder } from 'discord.js' import { CommandInteraction } from 'discord.js'
import { paymentsEmbed } from '../utils'
@Discord() @Discord()
export class Payments { export class Payments {
@ -8,20 +10,7 @@ export class Payments {
description: 'See available payments', description: 'See available payments',
}) })
async payments(interaction: CommandInteraction) { async payments(interaction: CommandInteraction) {
const embed = new EmbedBuilder().setTitle(`Payments`).setFields([ await interaction.deferReply()
{ await interaction.editReply({ embeds: [paymentsEmbed()] })
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] })
} }
} }

View File

@ -5,7 +5,7 @@ import { CommandInteraction } from 'discord.js'
export class Ping { export class Ping {
@Slash({ @Slash({
name: 'ping', name: 'ping',
description: 'ping the bot', description: 'Show ping (ms)',
defaultMemberPermissions: ['Administrator'], defaultMemberPermissions: ['Administrator'],
}) })
async ping(interaction: CommandInteraction) { async ping(interaction: CommandInteraction) {

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './logger'

15
src/lib/logger.ts Normal file
View File

@ -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()

View File

@ -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 + '```' },
])
}

View File

@ -0,0 +1,3 @@
export * from './tickets'
export * from './feedback'
export * from './payments'

View File

@ -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',
},
])
}

108
src/utils/embeds/tickets.ts Normal file
View File

@ -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<MessageActionRowComponentBuilder>().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<MessageActionRowComponentBuilder>().addComponents(
closeBtn,
)
}

View File

@ -1,4 +1,4 @@
export enum Status { export enum Workload {
AVAILABLE = 'AVAILABLE', AVAILABLE = 'AVAILABLE',
BUSY = 'BUSY', BUSY = 'BUSY',
NOT_AVAILABLE = 'NOT_AVAILABLE', NOT_AVAILABLE = 'NOT_AVAILABLE',

2
src/utils/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './embeds'
export * from './enums'