diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..7cb14b2f --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Your discord token +TOKEN="" +# Prisma Database URL (refer to docs for more details) +# Refer to https://www.prisma.io/docs/concepts/database-connectors to use other databases +DATABASE_URL="file:./tixbot.db" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 5f0d576a..5235e38f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ README.md LICENSE package.json package-lock.json +dist/ diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index baa8de66..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - env: { - browser: true, - commonjs: true, - es2021: true, - }, - overrides: [], - extends: ["eslint:recommended", "plugin:node/recommended", "prettier"], - parserOptions: { - ecmaVersion: "latest", - }, - rules: { - "no-unused-vars": "warn", - "no-console": "off", - "no-undef": "warn", - "no-constant-condition": "warn", - "indent": ["error", "tab"], - "semi": ["error", "always"], - "quotes": [2, "double"], - "semi-style": ["error", "last"], - "no-process-exit": "off", - }, -}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..35b11843 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,27 @@ +{ + "env": { + "es2021": true, + "node": true + }, + "overrides": [], + "extends": ["eslint:recommended", "prettier", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "no-unused-vars": "warn", + "no-console": "off", + "no-undef": "warn", + "no-constant-condition": "warn", + "indent": ["error", "tab"], + "semi": ["error", "always"], + "quotes": [2, "double"], + "semi-style": ["error", "last"], + "no-process-exit": "off", + "node/no-missing-import": "off", + "no-var-requires": "off" + } +} diff --git a/.github/workflows/eslint.yml b/.github/workflows/builder.yml similarity index 80% rename from .github/workflows/eslint.yml rename to .github/workflows/builder.yml index 5e2af47f..c40c0573 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/builder.yml @@ -1,10 +1,11 @@ -name: ESLint Check +name: Source Check on: push: branches: - "main" pull_request: + types: [review_requested, ready_for_review] permissions: contents: read @@ -35,8 +36,11 @@ jobs: key: node_modules-${{ matrix.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} - name: npm i - if: steps.cache-node_modules.outputs.cache-hit != 'true' + if: steps.cache-node_modules.outputs.cache-hit != 'true' || github.event_name == 'pull_request' run: npm i --no-audit - name: ESLint - run: npx --no-install eslint . + run: npm run lint:check + + - name: build + run: npm run build --if-present diff --git a/.gitignore b/.gitignore index eb6d16ab..2ba41346 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules json.sqlite package-lock.json config.jsonc -token.json \ No newline at end of file +token.json +dist +.env +*.db \ No newline at end of file diff --git a/README.md b/README.md index fad5f218..4b657835 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ Ticket Bot is a open source project of an ticket discord bot using [discord.js]( The documentation is available [here](https://doc.ticket.pm/) +## ⚠️ Incompatibility +This new source code you're seeing are completely refactored and will be incompatible with the older version. +I recommend you finish up all of your remaining support ticket and start migrating to the newer version. +If you prefer to stay in the older version, you can download anything that is less than `3.0.0` from the release +or clone from the `old` branch (i.e. `git clone -b old https://github.com/Sayrix/Ticket-Bot.git`) + ## 💬 Discord You can come on the discord: https://discord.gg/VasYV6MEJy diff --git a/config/config.example.jsonc b/config/config.example.jsonc index 7efa26a9..59f2b434 100644 --- a/config/config.example.jsonc +++ b/config/config.example.jsonc @@ -1,31 +1,9 @@ { "clientId": "1111111111111111111", // The id of the discord bot "guildId": "1111111111111111111", // The id of the discord server - "mainColor": "f6c42f", // The hex color of the embeds by default + "mainColor": "#f6c42f", // The hex color of the embeds by default "lang": "main", // If you want to set english please set "main" - /* - Pick a database driver, postgres will take priority over mysql (if both are enabled, which please don't do). - If neither option is enabled, SQLite will be used. - *PostgreSQL will be using default schema* - */ - "postgre": { - "enabled": false, - "host": "postgresql.example.com", // The host of the PostgreSQL database - "user": "postgres", // The user of the PostgreSQL database - "password": "password", // The password of the PostgreSQL database - "database": "postgres", // The name of the PostgreSQL database - "table": "json" // The name of the table where the tickets will be saved - }, - "mysql": { - "enabled": false, - "host": "mysql.example.com", // The host of the MySQL database - "user": "mysql", // The user of the MySQL database - "password": "password", // The password of the MySQL database - "database": "ticketbot", // The name of the MySQL database - "table": "json" // The name of the table where the tickets will be saved - }, - "closeTicketCategoryId": "", // The id of the category where a closed ticket will be moved to. Leave blank to disable this feature "openTicketChannelId": "1111111111111111111", // The id of the channel where the message to create a ticket will be sent @@ -99,6 +77,7 @@ "askReasonWhenClosing": true, // If false the ticket will be closed without asking the reason "createTranscript": true, // If set to true, when the ticket is closed a transcript will be generated and sent in the logs channel + "uuidType": "uuid", // uuid or emoji "status": { "enabled": true, // If you want to enable the status of the bot @@ -108,5 +87,10 @@ "status": "online" // online, idle, dnd, invisible set to online if the type is STREAMING }, - "maxTicketOpened": 0 // The number of tickets the user can open while another one is already open. Set to 0 to unlimited + "maxTicketOpened": 0, // The number of tickets the user can open while another one is already open. Set to 0 to unlimited + /* + Whether or not to minimizing the tracking data that are being sent + Enabling this will cause the telemetry to only send the software version and node version + */ + "minimalTracking": false } diff --git a/config/token.example.json b/config/token.example.json deleted file mode 100644 index 54a69a53..00000000 --- a/config/token.example.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "token": "my.discord.bot.token" -} diff --git a/deploy-commands.js b/deploy-commands.js deleted file mode 100644 index ffbe34c0..00000000 --- a/deploy-commands.js +++ /dev/null @@ -1,36 +0,0 @@ -const fs = require("node:fs"); -const path = require("node:path"); -const jsonc = require("jsonc"); -// eslint-disable-next-line node/no-extraneous-require -const { REST } = require("@discordjs/rest"); -const { Routes } = require("discord.js"); -// eslint-disable-next-line no-unused-vars -const Discord = require("discord.js"); -// eslint-disable-next-line node/no-missing-require, node/no-unpublished-require -const { token } = require("./config/token.json"); - -module.exports = { - /** - * @param {Discord.Client} client - */ - async deployCommands() { - const commands = []; - const commandsPath = path.join(__dirname, "commands"); - const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")); - - const { clientId, guildId } = jsonc.parse(fs.readFileSync(path.join(__dirname, "config/config.jsonc"), "utf8")); - - for (const file of commandFiles) { - const filePath = path.join(commandsPath, file); - const command = require(filePath); - commands.push(command.data.toJSON()); - } - - const rest = new REST({ version: "10" }).setToken(token); - - rest - .put(Routes.applicationGuildCommands(clientId, guildId), { body: commands }) - .then(() => console.log("✅ Successfully registered application commands.")) - .catch(console.error); - }, -}; diff --git a/package.json b/package.json index 2ca2f25e..fdf8a7c8 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "ticket-bot", - "version": "2.4.0", + "version": "3.0.0", "description": "Bot with a ticket system using Discord.js v14", - "main": "index.js", + "main": "dist/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "setup": "npm install && prisma db push", + "build": "rimraf dist && tsc", + "start": "node dist/index.js", "format:fix": "prettier --write .", "format:check": "prettier --check .", - "lint:fix": "eslint --fix .", - "lint:check": "eslint ." + "lint:fix": "eslint --fix . --ext .ts", + "lint:check": "eslint . --ext .ts" }, "repository": { "type": "git", @@ -26,25 +28,35 @@ }, "homepage": "https://github.com/Sayrix/ticket-bot#readme", "dependencies": { + "@prisma/client": "^4.16.1", "axios": "^1.4.0", "better-sqlite3": "^8.4.0", "discord.js": "^14.11.0", + "dotenv": "^16.3.1", "fs-extra": "^11.1.1", "jsonc": "^2.0.0", - "mysql2": "^3.3.5", - "pg": "^8.11.0", - "quick.db": "^9.1.6", + "mongoose": "^7.3.0", "readline": "^1.3.0", "ticket-bot-transcript-uploader": "^1.3.0", "websocket": "^1.0.34" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.4", + "@types/fs-extra": "^11.0.1", + "@types/node": "^20.3.1", + "@types/pg": "^8.10.2", + "@types/websocket": "^1.0.5", + "@typescript-eslint/eslint-plugin": "^5.60.0", + "@typescript-eslint/parser": "^5.60.0", "eslint": "^8.43.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "prisma": "^4.16.1", + "rimraf": "^5.0.1", + "typescript": "^5.1.3" } } diff --git a/prisma/compatible.sql b/prisma/compatible.sql new file mode 100644 index 00000000..7b562102 --- /dev/null +++ b/prisma/compatible.sql @@ -0,0 +1,43 @@ +/* +Copyright © 2023 小兽兽/zhiyan114 (github.com/zhiyan114) +File is licensed respectively under the terms of the Apache License 2.0 +or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE + +This file is from postgre.sql but modified for sqlite and mysql compatibility with prisma. +RANT: I LOVE AND HATE SQLITE. Why can't it just support a little more features... + +For production use, please use "prisma db push" instead or follow the documentation: https://doc.ticket.pm/docs/intro. +*/ + + +/* +this will be used for +* openTicketMessageId +* ticketCount +*/ + +CREATE TABLE IF NOT EXISTS config ( + key TEXT PRIMARY KEY, + value TEXT +); + +/* +this will be used for storing tickets +*/ + +CREATE TABLE IF NOT EXISTS tickets ( + id SERIAL PRIMARY KEY, + channelid TEXT NOT NULL UNIQUE, + messageid TEXT NOT NULL UNIQUE, + category TEXT NOT NULL, + invited TEXT NOT NULL DEFAULT '[]', + reason TEXT NOT NULL, + creator TEXT NOT NULL, + createdat BIGINT NOT NULL, + claimedby TEXT, + claimedat BIGINT, + closedby TEXT, + closedat BIGINT, + closereason TEXT, + transcript TEXT +); diff --git a/prisma/postgre.sql b/prisma/postgre.sql new file mode 100644 index 00000000..b051f03e --- /dev/null +++ b/prisma/postgre.sql @@ -0,0 +1,55 @@ +/* +Copyright © 2023 小兽兽/zhiyan114 (github.com/zhiyan114) +File is licensed respectively under the terms of the Apache License 2.0 +or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE + +For production use, please don't try to use this file, even if you're using postgresql, +Since the code is tailored towards compatibility.sql, it will break. +You have been warned. + +I wrote this in-case multi-db support will eventually be dropped, and I'm a big postgresql fan ^w^ +*/ + + + +/* +this will be used for +* openTicketMessageId +* ticketCount +*/ + +CREATE TABLE IF NOT EXISTS config ( + key VARCHAR(256) PRIMARY KEY, + value TEXT +); + +/* +this will be used for storing tickets +*/ + +CREATE TABLE IF NOT EXISTS tickets ( + id SERIAL PRIMARY KEY, + channelid TEXT NOT NULL UNIQUE, + messageid TEXT NOT NULL UNIQUE, + category JSON NOT NULL, + reason TEXT NOT NULL, + creator TEXT NOT NULL, + createdat TIMESTAMP NOT NULL DEFAULT NOW(), + claimedby TEXT, + claimedat TIMESTAMP, + closedby TEXT, + closedat TIMESTAMP, + closereason TEXT, + transcript TEXT +); + +/* +this will be used to handle ticket invites +*/ + +CREATE TABLE IF NOT EXISTS invites ( + id SERIAL PRIMARY KEY, + ticketid TEXT NOT NULL, + userid TEXT NOT NULL, + CONSTRAINT FK_ticketID FOREIGN KEY(ticketid) REFERENCES tickets(messageid) ON DELETE CASCADE +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..a15fffbe --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,30 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model config { + key String @id + value String? +} + +model tickets { + id Int @id @default(autoincrement()) + channelid String @unique + messageid String @unique + category String + invited String @default("[]") + reason String + creator String + createdat BigInt + claimedby String? + claimedat BigInt? + closedby String? + closedat BigInt? + closereason String? + transcript String? +} diff --git a/src/Types.ts b/src/Types.ts new file mode 100644 index 00000000..93bc27a4 --- /dev/null +++ b/src/Types.ts @@ -0,0 +1,168 @@ +/* Licensed under Apache License 2.0: https://github.com/Sayrix/Ticket-Bot/blob/typescript/LICENSE */ + +import { PrismaClient } from "@prisma/client"; +import type { Client, Collection, ColorResolvable, Interaction, SlashCommandBuilder } from "discord.js"; + +// Config types and setups +type TicketQuestion = { + label: string; + placeholder: string; + style: string; + maxLength: number; +} +export type TicketType = { + codeName: string; + name: string; + description: string; + emoji: string; + color?: ColorResolvable; + categoryId: string; + ticketNameOption: string; + customDescription: string; + cantAccess: string[]; + askQuestions: boolean; + questions: TicketQuestion[]; +} +export type config = { + clientId: string; + guildId: string; + mainColor: ColorResolvable; + lang: string; // Tho can be cs/de/es/fr/main/tr type but we can't guarantee what users put + closeTicketCategoryId: string; + openTicketChannelId: string; + ticketTypes: TicketType[]; + ticketNameOption: string; + ticketNamePrefixWhenClaimed: string; + rolesWhoHaveAccessToTheTickets: string[]; + rolesWhoCanNotCreateTickets: string[]; + pingRoleWhenOpened: boolean; + roleToPingWhenOpenedId: string[]; + logs: boolean; + logsChannelId: string; + claimButton: boolean; + whoCanCloseTicket: "STAFFONLY" | "EVERYONE"; + closeButton: boolean; + askReasonWhenClosing: boolean; + createTranscript: boolean; + uuidType: string, + status: { + enabled: boolean; + text: string; + type: "PLAYING" | "STREAMING"| "LISTENING" | "WATCHING" | "COMPETING", + url?: string, + status: "online" + }, + maxTicketOpened: number; + minimalTracking: boolean; +} +export type locale = { + embeds: { + openTicket: { + title: string, + color?: ColorResolvable, + description: string, + footer: { + text: string + } + }, + ticketOpened: { + title: string, + description: string, + footer: { + text: string, + iconUrl?: string + } + }, + ticketClosed: { + title: string, + description: string + }, + ticketClosedDM: { + title: string, + color?: ColorResolvable, + description: string, + footer: { + text: string, + iconUrl?: string + } + } + }, + modals: { + reasonTicketOpen: { + title: string, + label: string, + placeholder: string + }, + reasonTicketClose: { + title: string, + label: string, + placeholder: string + } + }, + buttons: { + close: { + label: string, + emoji: string + }, + claim: { + label: string, + emoji: string + } + }, + ticketOpenedMessage: string, + ticketOnlyClaimableByStaff: string, + ticketAlreadyClaimed: string, + ticketClaimedMessage: string, + ticketOnlyClosableByStaff: string, + ticketOnlyRenamableByStaff: string; + ticketRenamed: string; + noTickets: string; + ticketAlreadyClosed: string, + ticketCreatingTranscript: string, + ticketTranscriptCreated: string, + ticketLimitReached: string, + + other: { + openTicketButtonMSG: string, + deleteTicketButtonMSG: string, + selectTicketTypePlaceholder: string, + claimedBy: string, + noReasonGiven: string, + unavailable: string + } +} + +export type command = { + data: SlashCommandBuilder; + // eslint-disable-next-line no-unused-vars + execute: (interaction: Interaction, client: DiscordClient) => Promise | void; +} + +export interface DiscordClient extends Client { + config: config; + prisma: PrismaClient; + locales: locale; + // eslint-disable-next-line no-unused-vars + msToHm: (ms: number | Date) => string; + commands: Collection; +} + +export type SayrixSponsorData = { + sponsor: { + login: string; + name: string; + avatarUrl: string; + websiteUrl?: string; + linkUrl: string; + type: string; + avatarUrlHighRes: string; + avatarUrlMediumRes: string; + avatarUrlLowRes: string; + }, + isOneTime: boolean; + monthlyDollars: number; + privacyLevel: string; + tierName: string; + createdAt: string; + provider: string; +} \ No newline at end of file diff --git a/commands/add.js b/src/commands/add.ts similarity index 62% rename from commands/add.js rename to src/commands/add.ts index c4216229..7ccff876 100644 --- a/commands/add.js +++ b/src/commands/add.ts @@ -1,4 +1,6 @@ -const { SlashCommandBuilder } = require("discord.js"); +import {CommandInteraction, SlashCommandBuilder, TextChannel} from "discord.js"; +import { DiscordClient } from "../Types"; +import { log } from "../utils/logs"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -16,23 +18,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default { data: new SlashCommandBuilder() .setName("add") .setDescription("Add someone to the ticket") .addUserOption((input) => input.setName("user").setDescription("The user to add").setRequired(true)), - async execute(interaction, client) { - const added = interaction.options.getUser("user"); - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); + async execute(interaction: CommandInteraction, client: DiscordClient) { + const added = interaction.options.getUser("user", true); + + const ticket = await client.prisma.tickets.findUnique({ + select: { + id: true, + invited: true, + }, + where: { + channelid: interaction.channel?.id + } + }); if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - if (ticket.invited.includes(added.id)) return interaction.reply({ content: "User already added", ephemeral: true }).catch((e) => console.log(e)); + + const invited = JSON.parse(ticket.invited) as string[]; + if (invited.includes(added.id)) return interaction.reply({ content: "User already added", ephemeral: true }).catch((e) => console.log(e)); - if (ticket.invited.lenght >= 25) + if (invited.length >= 25) return interaction.reply({ content: "You can't add more than 25 users", ephemeral: true }).catch((e) => console.log(e)); - client.db.push(`tickets_${interaction.channel.id}.invited`, added.id); + invited.push(added.id); + await client.prisma.tickets.update({ + data: { + invited: JSON.stringify(invited) + }, + where: { + channelid: interaction.channel?.id + } + }); - await interaction.channel.permissionOverwrites + await (interaction.channel as TextChannel | null)?.permissionOverwrites .edit(added, { SendMessages: true, AddReactions: true, @@ -44,19 +65,13 @@ module.exports = { interaction.reply({ content: `> Added <@${added.id}> to the ticket` }).catch((e) => console.log(e)); - client.log( - "userAdded", + log( { - user: { - tag: interaction.user.tag, - id: interaction.user.id, - avatarURL: interaction.user.displayAvatarURL(), - }, - ticketId: ticket.id, - ticketChannelId: interaction.channel.id, - added: { - id: added.id, - }, + LogType: "userAdded", + user: interaction.user, + ticketId: ticket.id.toString(), + ticketChannelId: interaction.channel?.id, + target: added, }, client ); diff --git a/commands/claim.js b/src/commands/claim.ts similarity index 84% rename from commands/claim.js rename to src/commands/claim.ts index f9d80673..e83f49ec 100644 --- a/commands/claim.js +++ b/src/commands/claim.ts @@ -1,4 +1,6 @@ -const { SlashCommandBuilder } = require("discord.js"); +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import { DiscordClient } from "../Types"; +import {claim} from "../utils/claim"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -16,10 +18,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default { data: new SlashCommandBuilder().setName("claim").setDescription("Set the ticket as claimed."), - async execute(interaction, client) { - const { claim } = require("../utils/claim.js"); + async execute(interaction: CommandInteraction, client: DiscordClient) { claim(interaction, client); }, }; diff --git a/commands/close.js b/src/commands/close.ts similarity index 78% rename from commands/close.js rename to src/commands/close.ts index 64182eb8..c3b0edb6 100644 --- a/commands/close.js +++ b/src/commands/close.ts @@ -1,4 +1,7 @@ -const { SlashCommandBuilder } = require("discord.js"); +import { CommandInteraction, GuildMember, SlashCommandBuilder } from "discord.js"; +import { DiscordClient } from "../Types"; +import { closeAskReason } from "../utils/close_askReason"; +import {close} from "../utils/close.js"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -16,12 +19,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default { data: new SlashCommandBuilder().setName("close").setDescription("Close the ticket"), - async execute(interaction, client) { + async execute(interaction: CommandInteraction, client: DiscordClient) { if ( client.config.whoCanCloseTicket === "STAFFONLY" && - !interaction.member.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)) + !(interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)) ) return interaction .reply({ @@ -31,11 +34,9 @@ module.exports = { .catch((e) => console.log(e)); if (client.config.askReasonWhenClosing) { - const { closeAskReason } = require("../utils/close_askReason.js"); closeAskReason(interaction, client); } else { await interaction.deferReply().catch((e) => console.log(e)); - const { close } = require("../utils/close.js"); close(interaction, client); } }, diff --git a/commands/remove.js b/src/commands/remove.ts similarity index 64% rename from commands/remove.js rename to src/commands/remove.ts index 6a0c17fe..7a946aab 100644 --- a/commands/remove.js +++ b/src/commands/remove.ts @@ -1,4 +1,5 @@ -const { SlashCommandBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("discord.js"); +import { SlashCommandBuilder, ActionRowBuilder, StringSelectMenuBuilder, CommandInteraction, User } from "discord.js"; +import { DiscordClient } from "../Types"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -16,26 +17,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default{ data: new SlashCommandBuilder().setName("remove").setDescription("Remove someone from the ticket"), - async execute(interaction, client) { - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); + + async execute(interaction: CommandInteraction, client: DiscordClient) { + const ticket = await client.prisma.tickets.findUnique({ + select: { + invited: true, + }, + where: { + channelid: interaction.channel?.id + } + }); if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - if (ticket.invited.length < 1) return interaction.reply({ content: "There are no users to remove", ephemeral: true }).catch((e) => console.log(e)); + + const invited = JSON.parse(ticket.invited) as string[]; + if (invited.length < 1) return interaction.reply({ content: "There are no users to remove", ephemeral: true }).catch((e) => console.log(e)); - for (let i = 0; i < ticket.invited.length; i++) { - await client.users.fetch(ticket.invited[i]); + const addedUsers: User[] = []; + for (let i = 0; i < invited.length; i++) { + addedUsers.push(await client.users.fetch(invited[i])); } - const addedUsers = ticket.invited.map((user) => client.users.cache.get(user)); - - const row = new ActionRowBuilder().addComponents( + const row = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder() .setCustomId("removeUser") .setPlaceholder("Please select a user to remove") .setMinValues(1) .setMaxValues(ticket.invited.length) .addOptions( + // @TODO: Fix type definitions when I figure it out via ORM migration. For now assign a random type that gets the error removed. addedUsers.map((user) => { return { label: user.tag, diff --git a/commands/rename.js b/src/commands/rename.ts similarity index 72% rename from commands/rename.js rename to src/commands/rename.ts index eae89085..2c6e7368 100644 --- a/commands/rename.js +++ b/src/commands/rename.ts @@ -1,6 +1,5 @@ -const { SlashCommandBuilder } = require("discord.js"); -// eslint-disable-next-line no-unused-vars -const Discord = require("discord.js"); +import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js"; +import { DiscordClient } from "../Types"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -18,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default { data: new SlashCommandBuilder() .setName("rename") .setDescription("Rename the ticket") @@ -29,10 +28,14 @@ module.exports = { * @param {Discord.Client} client * @returns */ - async execute(interaction, client) { - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); + async execute(interaction: CommandInteraction, client: DiscordClient) { + const ticket = await client.prisma.tickets.findUnique({ + where: { + channelid: interaction.channel?.id + } + }); if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - if (!interaction.member.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id))) + if (!(interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id))) return interaction .reply({ content: client.locales.ticketOnlyRenamableByStaff, @@ -40,9 +43,9 @@ module.exports = { }) .catch((e) => console.log(e)); - interaction.channel.setName(interaction.options.getString("name")).catch((e) => console.log(e)); + (interaction.channel as TextChannel)?.setName(interaction.options.get("name", true).value as string).catch((e) => console.log(e)); interaction - .reply({ content: client.locales.ticketRenamed.replace("NEWNAME", interaction.channel.toString()), ephemeral: false }) + .reply({ content: client.locales.ticketRenamed.replace("NEWNAME", (interaction.channel as TextChannel | null)?.toString() ?? "Unknown"), ephemeral: false }) .catch((e) => console.log(e)); }, }; diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts new file mode 100644 index 00000000..66eb3a59 --- /dev/null +++ b/src/deploy-commands.ts @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import path from "node:path"; +import { jsonc } from "jsonc"; +import { REST } from "@discordjs/rest"; +import { Routes } from "discord.js"; +import { DiscordClient } from "./Types"; + +export default { + /** + * @param {Discord.Client} client + */ + async deployCommands(client: DiscordClient) { + const commands = []; + const commandsPath = path.join(__dirname, "commands"); + const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")); + + const { guildId } = jsonc.parse(fs.readFileSync(path.join(__dirname, "../config/config.jsonc"), "utf8")); + + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const command = require(filePath).default; + commands.push(command.data.toJSON()); + } + if(!process.env["TOKEN"]) throw Error("Discord Token Expected, deploy-command"); + const rest = new REST({ version: "10" }).setToken(process.env["TOKEN"]); + + rest + .put(Routes.applicationGuildCommands(client.user?.id ?? "", guildId), { body: commands }) + .then(() => console.log("✅ Successfully registered application commands.")) + .catch(console.error); + }, +}; diff --git a/events/interactionCreate.js b/src/events/interactionCreate.ts similarity index 64% rename from events/interactionCreate.js rename to src/events/interactionCreate.ts index 039a49c5..c57c132a 100644 --- a/events/interactionCreate.js +++ b/src/events/interactionCreate.ts @@ -1,4 +1,11 @@ -const Discord = require("discord.js"); +import { ActionRowBuilder, GuildChannel, GuildMember, Interaction, ModalBuilder, SelectMenuComponentOptionData, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import { DiscordClient } from "../Types"; +import { log } from "../utils/logs"; +import {createTicket} from "../utils/createTicket"; +import { close } from "../utils/close"; +import { claim } from "../utils/claim"; +import { closeAskReason } from "../utils/close_askReason"; +import { deleteTicket } from "../utils/delete"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -16,54 +23,52 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default { name: "interactionCreate", once: false, /** * @param {Discord.Interaction} interaction * @param {Discord.Client} client */ - async execute(interaction, client) { + async execute(interaction: Interaction, client: DiscordClient) { if (interaction.isButton()) { if (interaction.customId === "openTicket") { await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); // Max ticket opened - for (let role of client.config.rolesWhoCanNotCreateTickets) { - if (role && interaction.member.roles.cache.has(role)) { + for (const role of client.config.rolesWhoCanNotCreateTickets) { + if (role && (interaction.member as GuildMember | null)?.roles.cache.has(role)) { return interaction .editReply({ - content: "You can't create a ticket because you are blacklisted", - ephemeral: true, + content: "You can't create a ticket because you are blacklisted" }) .catch((e) => console.log(e)); } } - const all = (await client.db.all()).filter((data) => data.id.startsWith("tickets_")); - const ticketsOpened = all.filter((data) => data.value.creator === interaction.user.id && data.value.closed === false).length; + const ticketsOpened = (await client.prisma.$queryRaw<[{count: bigint}]> + `SELECT COUNT(*) as count FROM tickets`)[0].count; + if (client.config.maxTicketOpened !== 0) { // If maxTicketOpened is 0, it means that there is no limit - if (ticketsOpened > client.config.maxTicketOpened || ticketsOpened === client.config.maxTicketOpened) { + if (ticketsOpened > client.config.maxTicketOpened || ticketsOpened === BigInt(client.config.maxTicketOpened)) { return interaction .editReply({ - content: client.locales.ticketLimitReached.replace("TICKETLIMIT", client.config.maxTicketOpened), - ephemeral: true, + content: client.locales.ticketLimitReached.replace("TICKETLIMIT", client.config.maxTicketOpened.toString()) }) .catch((e) => console.log(e)); } } // Make a select menus of all tickets types + let options: SelectMenuComponentOptionData[] = []; - let options = []; - - for (let x of client.config.ticketTypes) { + for (const x of client.config.ticketTypes) { // x.cantAccess is an array of roles id // If the user has one of the roles, he can't access to this ticket type - const a = { + const a: SelectMenuComponentOptionData = { label: x.name, value: x.codeName, }; @@ -72,11 +77,11 @@ module.exports = { options.push(a); } - for (let x of options) { - let option = client.config.ticketTypes.filter((y) => y.codeName === x.value)[0]; + for (const x of options) { + const option = client.config.ticketTypes.filter((y) => y.codeName === x.value)[0]; if (option.cantAccess) { - for (let role of option.cantAccess) { - if (role && interaction.member.roles.cache.has(role)) { + for (const role of option.cantAccess) { + if (role && (interaction.member as GuildMember | null)?.roles.cache.has(role)) { options = options.filter((y) => y.value !== x.value); } } @@ -84,12 +89,11 @@ module.exports = { } if (options.length <= 0) return interaction.editReply({ - ephemeral: true, content: client.locales.noTickets }); - const row = new Discord.ActionRowBuilder().addComponents( - new Discord.StringSelectMenuBuilder() + const row = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() .setCustomId("selectTicketType") .setPlaceholder(client.locales.other.selectTicketTypePlaceholder) .setMaxValues(1) @@ -98,44 +102,41 @@ module.exports = { interaction .editReply({ - ephemeral: true, components: [row], }) .catch((e) => console.log(e)); } if (interaction.customId === "claim") { - const { claim } = require("../utils/claim.js"); claim(interaction, client); } if (interaction.customId === "close") { await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); - const { close } = require("../utils/close.js"); close(interaction, client, client.locales.other.noReasonGiven); } if (interaction.customId === "close_askReason") { - const { closeAskReason } = require("../utils/close_askReason.js"); closeAskReason(interaction, client); } if (interaction.customId === "deleteTicket") { - const { deleteTicket } = require("../utils/delete.js"); deleteTicket(interaction, client); } } if (interaction.isStringSelectMenu()) { if (interaction.customId === "selectTicketType") { - const all = (await client.db.all()).filter((data) => data.id.startsWith("tickets_")); - const ticketsOpened = all.filter((data) => data.value.creator === interaction.user.id && data.value.closed === false).length; + const ticketsOpened = (await client.prisma.$queryRaw<{count:number}> + `SELECT COUNT(*) as count FROM tickets WHERE closereason IS NOT NULL`) + .count; + if (client.config.maxTicketOpened !== 0) { // If maxTicketOpened is 0, it means that there is no limit if (ticketsOpened > client.config.maxTicketOpened || ticketsOpened === client.config.maxTicketOpened) { return interaction .reply({ - content: client.locales.ticketLimitReached.replace("TICKETLIMIT", client.config.maxTicketOpened), + content: client.locales.ticketLimitReached.replace("TICKETLIMIT", client.config.maxTicketOpened.toString()), ephemeral: true, }) .catch((e) => console.log(e)); @@ -145,44 +146,46 @@ module.exports = { const ticketType = client.config.ticketTypes.find((x) => x.codeName === interaction.values[0]); if (!ticketType) return console.error(`Ticket type ${interaction.values[0]} not found!`); if (ticketType.askQuestions) { - const modal = new client.discord.ModalBuilder().setCustomId("askReason").setTitle(client.locales.modals.reasonTicketOpen.title); + const modal = new ModalBuilder().setCustomId("askReason").setTitle(client.locales.modals.reasonTicketOpen.title); ticketType.questions.forEach((x, i) => { - const input = new client.discord.TextInputBuilder() + const input = new TextInputBuilder() .setCustomId(`input_${interaction.values[0]}_${i}`) .setLabel(x.label) - .setStyle(x.style == "SHORT" ? client.discord.TextInputStyle.Short : client.discord.TextInputStyle.Paragraph) + .setStyle(x.style == "SHORT" ? TextInputStyle.Short : TextInputStyle.Paragraph) .setPlaceholder(x.placeholder) .setMaxLength(x.maxLength); - const firstActionRow = new client.discord.ActionRowBuilder().addComponents(input); + const firstActionRow = new ActionRowBuilder().addComponents(input); modal.addComponents(firstActionRow); }); await interaction.showModal(modal).catch((e) => console.log(e)); } else { - require("../utils/createTicket.js").createTicket(interaction, client, ticketType, client.locales.other.noReasonGiven); + createTicket(interaction, client, ticketType, client.locales.other.noReasonGiven); } } if (interaction.customId === "removeUser") { - const ticket = await client.db.get(`tickets_${interaction.message.channelId}`); - client.db.pull(`tickets_${interaction.message.channel.id}.invited`, interaction.values); + const ticket = await client.prisma.tickets.findUnique({ + select: { + id: true, + }, + where: { + channelid: interaction.message.channelId + } + }); interaction.values.forEach((value) => { - interaction.channel.permissionOverwrites.delete(value).catch((e) => console.log(e)); + (interaction.channel as GuildChannel | null)?.permissionOverwrites.delete(value).catch((e) => console.log(e)); - client.log( - "userRemoved", + log( { - user: { - tag: interaction.user.tag, - id: interaction.user.id, - avatarURL: interaction.user.displayAvatarURL(), - }, - ticketId: ticket.id, - ticketChannelId: interaction.channel.id, - removed: { + LogType: "userRemoved", + user: interaction.user, + ticketId: ticket?.id.toString(), + ticketChannelId: interaction.channel?.id, + target: { id: value, }, }, @@ -203,16 +206,16 @@ module.exports = { if (interaction.isModalSubmit()) { if (interaction.customId === "askReason") { - const type = interaction.fields.fields.first().customId.split("_")[1]; + const type = interaction.fields.fields.first()?.customId.split("_")[1]; const ticketType = client.config.ticketTypes.find((x) => x.codeName === type); - if (!ticketType) return console.error(`Ticket type ${interaction.values[0]} not found!`); - require("../utils/createTicket.js").createTicket(interaction, client, ticketType, interaction.fields.fields); + // Using customId until the value can be figured out + if (!ticketType) return console.error(`Ticket type ${interaction.customId} not found!`); + createTicket(interaction, client, ticketType, interaction.fields.fields); } if (interaction.customId === "askReasonClose") { await interaction.deferReply().catch((e) => console.log(e)); - const { close } = require("../utils/close.js"); - close(interaction, client, interaction.fields.fields.first().value); + close(interaction, client, interaction.fields.fields.first()?.value); } } }, diff --git a/events/ready.js b/src/events/ready.ts similarity index 52% rename from events/ready.js rename to src/events/ready.ts index 32d95b8d..d17d095c 100644 --- a/events/ready.js +++ b/src/events/ready.ts @@ -1,8 +1,11 @@ /* eslint-disable no-unused-vars */ -const readline = require("readline"); -const axios = require("axios"); -const Discord = require("discord.js"); -const WebSocketClient = require("websocket").client; +import readline from "readline"; +import axios from "axios"; +import {client as WebSocketClient, connection} from "websocket"; +import { DiscordClient, SayrixSponsorData } from "../Types"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import os from "os"; +import deployCmd from "../deploy-commands"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -20,26 +23,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { +export default { name: "ready", once: true, /** * @param {Discord.Client} client */ - async execute(client) { + async execute(client: DiscordClient) { if (!client.config.guildId) { console.log("⚠️⚠️⚠️ Please add the guild id in the config.jsonc file. ⚠️⚠️⚠️"); process.exit(0); } await client.guilds.fetch(client.config.guildId); - await client.guilds.cache.get(client.config.guildId).members.fetch(); - if (!client.guilds.cache.get(client.config.guildId).members.me.permissions.has("Administrator")) { + await client.guilds.cache.get(client.config.guildId)?.members.fetch(); + if (!client.guilds.cache.get(client.config.guildId)?.members.me?.permissions.has("Administrator")) { console.log("\n⚠️⚠️⚠️ I don't have the Administrator permission, to prevent any issues please add the Administrator permission to me. ⚠️⚠️⚠️"); process.exit(0); } - const embedMessageId = await client.db.get("temp.openTicketMessageId"); + const embedMessageId = (await client.prisma.config.findUnique({ + where: { + key: "openTicketMessageId", + } + }))?.value; await client.channels.fetch(client.config.openTicketChannelId).catch(() => { console.error("The channel to open tickets is not found!"); process.exit(0); @@ -55,66 +62,42 @@ module.exports = { process.exit(0); } - let embed = client.embeds.openTicket; - - /* - Copyright 2023 Sayrix (github.com/Sayrix) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - embed.color = parseInt(client.config.mainColor, 16); + const embedDat = {...client.locales.embeds.openTicket}; + const footer = embedDat.footer.text.replace("ticket.pm", ""); // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - embed.footer.text = "ticket.pm" + client.embeds.ticketOpened.footer.text.replace("ticket.pm", ""); // Please respect the LICENSE :D + embedDat.footer.text = `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`; // Please respect the LICENSE :D // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - - /* - Copyright 2023 Sayrix (github.com/Sayrix) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - const row = new Discord.ActionRowBuilder().addComponents( - new Discord.ButtonBuilder().setCustomId("openTicket").setLabel(client.locales.other.openTicketButtonMSG).setStyle(Discord.ButtonStyle.Primary) + const embed = new EmbedBuilder({ + ...embedDat, + color: 0, + }) + .setColor(embedDat.color ?? client.config.mainColor); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("openTicket").setLabel(client.locales.other.openTicketButtonMSG).setStyle(ButtonStyle.Primary) ); try { - const msg = await openTicketChannel?.messages?.fetch(embedMessageId).catch(() => {}); + const msg = embedMessageId ? await openTicketChannel?.messages?.fetch(embedMessageId).catch((ex) => console.error(ex)) : undefined; if (msg && msg.id) { msg.edit({ embeds: [embed], components: [row] }); } else { - client.channels.cache - .get(client.config.openTicketChannelId) - .send({ - embeds: [embed], - components: [row] - }) - .then((msg) => { - client.db.set("temp.openTicketMessageId", msg.id); - }); + const channel = client.channels.cache.get(client.config.openTicketChannelId); + if(!channel || !channel.isTextBased()) return console.error("Invalid openTicketChannelId"); + channel.send({ + embeds: [embed], + components: [row] + }).then((rMsg) => { + client.prisma.config.create({ + data: { + key: "openTicketMessageId", + value: rMsg.id + } + }).then(); // I need .then() for it to execute?!?!?? + }); } } catch (e) { console.error(e); @@ -124,22 +107,34 @@ module.exports = { if (client.config.status) { if (!client.config.status.enabled) return; - let type = client.config.status.type; - if (type === "PLAYING") type = 0; - if (type === "STREAMING") type = 1; - if (type === "LISTENING") type = 2; - if (type === "WATCHING") type = 3; - if (type === "COMPETING") type = 5; + let type = 0; + switch(client.config.status.type) { + case "PLAYING": + type = 0; + break; + case "STREAMING": + type = 1; + break; + case "LISTENING": + type = 2; + break; + case "WATCHING": + type = 3; + break; + case "COMPETING": + type = 4; + break; + } if (client.config.status.type && client.config.status.text) { // If the user just want to set the status but not the activity const url = client.config.status.url; - client.user.setPresence({ + client.user?.setPresence({ activities: [{ name: client.config.status.text, type: type, url: (url && url.trim() !== "") ? url : undefined }], status: client.config.status.status, }); } - client.user.setStatus(client.config.status.status); + client.user?.setStatus(client.config.status.status); } } @@ -148,24 +143,48 @@ module.exports = { readline.cursorTo(process.stdout, 0); process.stdout.write( - `\x1b[0m🚀 The bot is ready! Logged in as \x1b[37;46;1m${client.user.tag}\x1b[0m (\x1b[37;46;1m${client.user.id}\x1b[0m) + `\x1b[0m🚀 The bot is ready! Logged in as \x1b[37;46;1m${client.user?.tag}\x1b[0m (\x1b[37;46;1m${client.user?.id}\x1b[0m) \x1b[0m🌟 You can leave a star on GitHub: \x1b[37;46;1mhttps://github.com/Sayrix/ticket-bot \x1b[0m - \x1b[0m📖 Documentation: \x1b[37;46;1mhttps://doc.ticket.pm \x1b[0m \x1b[0m⛅ Host your ticket-bot by being a sponsor from 1$/month: \x1b[37;46;1mhttps://github.com/sponsors/Sayrix \x1b[0m\n`.replace(/\t/g, "") ); - const a = await axios.get("https://raw.githubusercontent.com/Sayrix/sponsors/main/sponsors.json").catch(() => {}); + const a = await axios.get("https://raw.githubusercontent.com/Sayrix/sponsors/main/sponsors.json").catch(() => {return;}); if (a) { - const sponsors = a.data; + const sponsors = a.data as SayrixSponsorData[]; const sponsorsList = sponsors .map((s) => `\x1b]8;;https://github.com/${s.sponsor.login}\x1b\\\x1b[1m${s.sponsor.login}\x1b]8;;\x1b\\\x1b[0m`) .join(", "); process.stdout.write(`\x1b[0m💖 Thanks to our sponsors: ${sponsorsList}\n`); } - let connected; - - function telemetry(connection) { + let connected = false; + + function telemetry(connection: connection) { + let fullInfo: {[key:string]: string | number | {[key:string]: string | number}} = { + os: os.platform(), + osVersion1: os.release(), + osVersion2: os.version(), + uptime: process.uptime(), + ram: { + total: os.totalmem(), + free: os.freemem() + }, + cpu: { + model: os.cpus()[0].model, + cores: os.cpus().length, + arch: os.arch() + } + }; + let moreInfo: {[key:string]: string | undefined} = { + clientName: client?.user?.tag, + clientId: client?.user?.id, + guildId: client?.config?.guildId + }; + // Minimal tracking enabled, remove those info from being sent + if(client.config.minimalTracking) { + fullInfo = {}; + moreInfo = {}; + } connection.sendUTF( JSON.stringify({ type: "telemetry", @@ -175,25 +194,12 @@ module.exports = { users: client?.users?.cache?.size }, infos: { - ticketbotVersion: require("../package.json").version, + // eslint-disable-next-line @typescript-eslint/no-var-requires + ticketbotVersion: require("../../package.json").version, nodeVersion: process.version, - os: require("os").platform(), - osVersion1: require("os").release(), - osVersion2: require("os").version(), - uptime: process.uptime(), - ram: { - total: require("os").totalmem(), - free: require("os").freemem() - }, - cpu: { - model: require("os").cpus()[0].model, - cores: require("os").cpus().length, - arch: require("os").arch() - } + ...fullInfo }, - clientName: client?.user?.tag, - clientId: client?.user?.id, - guildId: client?.config?.guildId + ...moreInfo } }) ); @@ -201,7 +207,7 @@ module.exports = { async function connect() { if (connected) return; - let ws = new WebSocketClient(); + const ws = new WebSocketClient(); ws.on("connectFailed", (e) => { connected = false; @@ -234,8 +240,45 @@ module.exports = { ws.connect("wss://ws.ticket.pm", "echo-protocol"); } + if ((await client.prisma.config.findUnique({ + where: { + key: "firstStart", + } + })) === null) { + await client.prisma.config.create({ + data: { + key: "firstStart", + value: "true", + } + }); + + if(!client.config.minimalTracking) console.warn(` + PRIVACY NOTICES + ------------------------------- + Telemetry is current set to full and the following information are sent to the server anonymously: + * Discord Bot's number of guilds & users + * Current Source Version + * NodeJS Version + * OS Version + * CPU version, name, core count, architecture, and model + * Current Process up-time + * System total ram and freed ram + * Client name and id + * Guild ID + ------------------------------- + If you wish to minimize the information that are being sent, please set "minimalTracking" to true in the config + `.replace(/\t/g, "")); + else console.warn(` + PRIVACY NOTICES + ------------------------------- + Minimal tracking has been enabled; the following information are sent anonymously: + * Current Source Version + * NodeJS Version + ------------------------------- + `.replace(/\t/g, "")); + } connect(); - require("../deploy-commands").deployCommands(client); + deployCmd.deployCommands(client); } }; diff --git a/index.js b/src/index.ts similarity index 62% rename from index.js rename to src/index.ts index e8c2be2b..57fbed33 100644 --- a/index.js +++ b/src/index.ts @@ -14,15 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -const fs = require("fs-extra"); -const path = require("node:path"); -const { Client, Collection, GatewayIntentBits } = require("discord.js"); -// eslint-disable-next-line node/no-missing-require, node/no-unpublished-require -const { token } = require("./config/token.json"); -const { QuickDB, MySQLDriver } = require("quick.db"); -const jsonc = require("jsonc"); - -process.on("unhandledRejection", (reason, promise, a) => { +import { Interaction } from "discord.js"; +import fs from "fs-extra"; +import path from "node:path"; +import { Client, Collection, GatewayIntentBits } from "discord.js"; +import { jsonc } from "jsonc"; +import { DiscordClient, config, locale } from "./Types"; +import { config as envconf } from "dotenv"; +import { PrismaClient } from "@prisma/client"; + +// Initalize .env file as environment +try {envconf();} +catch(ex) {console.log(".env failed to load");} + +// Although invalid type, it should be good enough for now until more stuff needs to be handled here +process.on("unhandledRejection", (reason: string, promise: string, a: string) => { console.log(reason, promise, a); }); @@ -40,101 +46,42 @@ Connecting to Discord... // Update Detector fetch("https://api.github.com/repos/Sayrix/Ticket-Bot/tags").then((res) => { - if (Math.floor(res.status / 100) !== 2) return console.warn("[Version Check] Failed to pull latest version from server"); + if (Math.floor(res.status / 100) !== 2) return console.warn("🔄 Failed to pull latest version from server"); res.json().then((json) => { // Assumign the format stays consistent (i.e. x.x.x) - const latest = json[0].name.split(".").map((k) => parseInt(k)); - const current = require("./package.json") - .version.split(".") - .map((k) => parseInt(k)); + const latest = json[0].name.split(".").map((k: string) => parseInt(k)); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const current = require("../package.json").version.split(".") + .map((k: string) => parseInt(k)); if ( latest[0] > current[0] || (latest[0] === current[0] && latest[1] > current[1]) || (latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]) ) - console.warn(`[Version Check] New version available: ${json[0].name}; Current Version: ${current.join(".")}`); - else console.log("[Version Check] Up to date"); + console.warn(`🔄 New version available: ${json[0].name}; Current Version: ${current.join(".")}`); + else console.log("🔄 The ticket-bot is up to date"); }); }); -const config = jsonc.parse(fs.readFileSync(path.join(__dirname, "config/config.jsonc"), "utf8")); + +const config: config = jsonc.parse(fs.readFileSync(path.join(__dirname, "/../config/config.jsonc"), "utf8")); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers], presence: { status: config.status?.status ?? "online" } -}); +}) as DiscordClient; // All variables stored in the client object -client.discord = require("discord.js"); -client.config = jsonc.parse(fs.readFileSync(path.join(__dirname, "config/config.jsonc"), "utf8")); - -let db = null; - -if (client.config.postgre?.enabled) { - // PostgreSQL Support. - (async () => { - try { - // eslint-disable-next-line node/no-missing-require - require.resolve("pg"); - } catch (e) { - console.error("pg driver is not installed!\n\nPlease run \"npm i pg\" in the console!"); - throw e.code; - } - const PostgresDriver = require("./utils/pgsqlDriver"); - const pgsql = new PostgresDriver({ - host: client.config.postgre?.host, - user: client.config.postgre?.user, - password: client.config.postgre?.password, - database: client.config.postgre?.database - }); - - await pgsql.connect(); - - db = new QuickDB({ - driver: pgsql, - table: client.config.postgre?.table ?? "json" - }); - client.db = db; - })(); -} else if (client.config.mysql?.enabled) { - // MySQL Support - (async () => { - try { - // eslint-disable-next-line node/no-missing-require - require.resolve("mysql2"); - } catch (e) { - console.error("mysql2 is not installed!\n\nPlease run \"npm i mysql2\" in the console!"); - throw e.code; - } - - const mysql = new MySQLDriver({ - host: client.config.mysql?.host, - user: client.config.mysql?.user, - password: client.config.mysql?.password, - database: client.config.mysql?.database, - charset: "utf8mb4" - }); - - await mysql.connect(); - - db = new QuickDB({ - driver: mysql, - table: client.config.mysql?.table ?? "json" - }); - client.db = db; - })(); -} else { - // SQLite Support - db = new QuickDB(); - client.db = db; -} - -client.locales = require(`./locales/${client.config.lang}.json`); -client.embeds = client.locales.embeds; -client.log = require("./utils/logs.js").log; -client.msToHm = function dhm(ms) { +client.config = config; +client.prisma = new PrismaClient(); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +client.locales = require(`../locales/${client.config.lang}.json`) as locale; +client.msToHm = function dhm(ms: number | Date) { + if(ms instanceof Date) ms = ms.getTime(); + const days = Math.floor(ms / (24 * 60 * 60 * 1000)); const daysms = ms % (24 * 60 * 60 * 1000); const hours = Math.floor(daysms / (60 * 60 * 1000)); @@ -157,12 +104,13 @@ const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith for (const file of commandFiles) { const filePath = path.join(commandsPath, file); - const command = require(filePath); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const command = require(filePath).default; client.commands.set(command.data.name, command); } // Execute commands -client.on("interactionCreate", async (interaction) => { +client.on("interactionCreate", async (interaction: Interaction) => { if (!interaction.isChatInputCommand()) return; const command = client.commands.get(interaction.commandName); if (!command) return; @@ -184,7 +132,8 @@ const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith(".j for (const file of eventFiles) { const filePath = path.join(eventsPath, file); - const event = require(filePath); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const event = require(filePath).default; if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { @@ -193,7 +142,7 @@ for (const file of eventFiles) { } // Login the bot -client.login(token); +client.login(process.env["TOKEN"]); /* Copyright 2023 Sayrix (github.com/Sayrix) diff --git a/src/utils/claim.ts b/src/utils/claim.ts new file mode 100644 index 00000000..c71a805d --- /dev/null +++ b/src/utils/claim.ts @@ -0,0 +1,122 @@ +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, CommandInteraction, EmbedBuilder, GuildMember, TextChannel } from "discord.js"; +import { DiscordClient } from "../Types"; +import { log } from "./logs"; + + +/** +* @param {Discord.CommandInteraction} interaction +* @param {Discord.Client} client +*/ +export const claim = async(interaction: ButtonInteraction | CommandInteraction, client: DiscordClient) => { + const ticket = await client.prisma.tickets.findUnique({ + where: { + channelid: interaction.channel?.id + } + }); + const claimed = ticket?.claimedat && ticket.claimedby; + + if (!ticket) + return interaction.reply({ + content: "Ticket not found", + ephemeral: true, + }); + + const canClaim = (interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)); + + if (!canClaim) + return interaction + .reply({ + content: client.locales.ticketOnlyClaimableByStaff, + ephemeral: true, + }) + .catch((e) => console.log(e)); + + if (claimed) + return interaction + .reply({ + content: client.locales.ticketAlreadyClaimed, + ephemeral: true, + }) + .catch((e) => console.log(e)); + + log( + { + LogType: "ticketClaim", + user: interaction.user, + ticketId: ticket.id.toString(), + ticketChannelId: interaction.channel?.id, + ticketCreatedAt: ticket.createdat, + }, + client + ); + + await client.prisma.tickets.update({ + data: { + claimedby: interaction.user.id, + claimedat: Date.now() + }, + where: { + channelid: interaction.channel?.id, + } + }); + + const msg = await interaction.channel?.messages.fetch(ticket.messageid); + const oldEmbed = msg?.embeds[0].data; + const newEmbed = new EmbedBuilder(oldEmbed) + .setDescription(oldEmbed?.description + `\n\n ${client.locales.other.claimedBy.replace("USER", `<@${interaction.user.id}>`)}`); + + const row = new ActionRowBuilder(); + msg?.components[0].components.map((x) => { + const btnBuilder = new ButtonBuilder(x.data); + if (x.customId === "claim") btnBuilder.setDisabled(true); + row.addComponents(btnBuilder); + }); + + msg?.edit({ + content: msg.content, + embeds: [newEmbed], + components: [row], + }).catch((e) => console.log(e)); + + interaction + .reply({ + content: client.locales.ticketClaimedMessage.replace("USER", `<@${interaction.user.id}>`), + ephemeral: false, + }) + .catch((e) => console.log(e)); + + if (client.config.ticketNamePrefixWhenClaimed) { + (interaction.channel as TextChannel | null)?.setName(`${client.config.ticketNamePrefixWhenClaimed}${(interaction.channel as TextChannel | null)?.name}`).catch((e) => console.log(e)); + } +}; +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/src/utils/close.ts b/src/utils/close.ts new file mode 100644 index 00000000..0b08a479 --- /dev/null +++ b/src/utils/close.ts @@ -0,0 +1,245 @@ +import { generateMessages } from "ticket-bot-transcript-uploader"; +import zlib from "zlib"; +import axios from "axios"; +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, CommandInteraction, ComponentType, EmbedBuilder, GuildMember, Message, ModalSubmitInteraction, TextChannel } from "discord.js"; +import { DiscordClient } from "../Types"; +import { log } from "./logs"; +let domain = "https://ticket.pm/"; + +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +export async function close(interaction: ButtonInteraction | CommandInteraction | ModalSubmitInteraction, client: DiscordClient, reason?: string) { + if (!client.config.createTranscript) domain = client.locales.other.unavailable; + + const ticket = await client.prisma.tickets.findUnique({ + where: { + channelid: interaction.channel?.id + } + }); + const ticketClosed = ticket?.closedat && ticket.closedby; + if (!ticket) return interaction.editReply({ content: "Ticket not found" }).catch((e) => console.log(e)); + + if ( + client.config.whoCanCloseTicket === "STAFFONLY" && + !(interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)) + ) + return interaction + .editReply({ + content: client.locales.ticketOnlyClosableByStaff + }) + .catch((e) => console.log(e)); + + if (ticketClosed) + return interaction + .editReply({ + content: client.locales.ticketAlreadyClosed + }) + .catch((e) => console.log(e)); + + log( + { + LogType: "ticketClose", + user: interaction.user, + ticketId: ticket.id, + ticketChannelId: interaction.channel?.id, + ticketCreatedAt: ticket.createdat, + reason: reason + }, + client + ); + + // Normally the user that closes the ticket will get posted here, but we'll do it when the ticket finalizes + + const creator = ticket.creator; + const invited = JSON.parse(ticket.invited) as string[]; + + (interaction.channel as TextChannel | null)?.permissionOverwrites + .edit(creator, { + ViewChannel: false + }) + .catch((e: unknown) => console.log(e)); + invited.forEach(async (user) => { + (interaction.channel as TextChannel | null)?.permissionOverwrites + .edit(user, { + ViewChannel: false + }) + .catch((e) => console.log(e)); + }); + + interaction + .editReply({ + content: client.locales.ticketCreatingTranscript + }) + .catch((e) => console.log(e)); + + async function _close(id: string) { + if (client.config.closeTicketCategoryId) (interaction.channel as TextChannel | null)?.setParent(client.config.closeTicketCategoryId).catch((e) => console.log(e)); + + // We can guarantee this is not null because it's already checked above. + // Should re-write this to prevent nested functions :/ + let ticket = (await client.prisma.tickets.findUnique({ + where: { + channelid: interaction.channel?.id + } + })); + if(!ticket) return console.error("close.ts: UNEXPECTED ERROR - _close func encountered null ticket"); + + const msg = await interaction.channel?.messages.fetch(ticket.messageid); + const embed = new EmbedBuilder(msg?.embeds[0].data); + + const rowAction = new ActionRowBuilder(); + msg?.components[0]?.components?.map((x) => { + if(x.type !== ComponentType.Button) return; + const builder = new ButtonBuilder(x.data); + if (x.customId === "close") builder.setDisabled(true); + if (x.customId === "close_askReason") builder.setDisabled(true); + rowAction.addComponents(builder); + }); + + msg?.edit({ + content: msg.content, + embeds: [embed], + components: [rowAction] + }) + .catch((e) => console.log(e)); + + interaction.channel?.send({ + content: client.locales.ticketTranscriptCreated.replace( + "TRANSCRIPTURL", + domain === client.locales.other.unavailable ? client.locales.other.unavailable : `<${domain}${id}>` + ) + }).catch((e) => console.log(e)); + + ticket = await client.prisma.tickets.update({ + data: { + closedby: interaction.user.id, + closedat: Date.now(), + closereason: reason, + transcript: domain === client.locales.other.unavailable ? client.locales.other.unavailable : `${domain}${id}` + }, + where: { + channelid: interaction.channel?.id + } + }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("deleteTicket").setLabel(client.locales.other.deleteTicketButtonMSG).setStyle(ButtonStyle.Danger) + ); + const lEmbed = client.locales.embeds; + interaction.channel?.send({ + embeds: [ + JSON.parse( + JSON.stringify(lEmbed.ticketClosed) + .replace("TICKETCOUNT", ticket.id.toString()) + .replace("REASON", (ticket.closereason ?? client.locales.other.noReasonGiven).replace(/[\n\r]/g, "\\n")) + .replace("CLOSERNAME", interaction.user.tag) + ) + ], + components: [row] + }) + .catch((e) => console.log(e)); + + const footer = lEmbed.ticketClosedDM.footer.text.replace("ticket.pm", ""); + const ticketClosedDMEmbed = new EmbedBuilder({ + ...lEmbed, + color: 0, + }) + .setColor(lEmbed.ticketClosedDM.color ?? client.config.mainColor) + .setDescription( + client.locales.embeds.ticketClosedDM.description + .replace("TICKETCOUNT", ticket.id.toString()) + .replace("TRANSCRIPTURL", `[\`${domain}${id}\`](${domain}${id})`) + .replace("REASON", ticket.closereason ?? client.locales.other.noReasonGiven) + .replace("CLOSERNAME", interaction.user.tag) + ) + .setFooter({ + // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) + text: `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`, // Please respect the LICENSE :D + // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) + iconURL: lEmbed.ticketClosedDM.footer.iconUrl + }); + + client.users.fetch(creator).then((user) => { + user + .send({ + embeds: [ticketClosedDMEmbed] + }) + .catch((e) => console.log(e)); + }); + } + + if (!client.config.createTranscript) { + _close(""); + return; + } + + async function fetchAll() { + const collArray: Collection>[] = []; + let lastID = (interaction.channel as TextChannel | null)?.lastMessageId; + // eslint-disable-next-line no-constant-condition + while (true) { + // using if statement for this check causes a TypeScript bug. Hard to reproduce; thus, bug report won't be accepted. + if(!lastID) break; + const fetched = await interaction.channel?.messages.fetch({ limit: 100, before: lastID }); + if (fetched?.size === 0) { + break; + } + if(fetched) + collArray.push(fetched); + lastID = fetched?.last()?.id; + if (fetched?.size !== 100) { + break; + } + } + const messages = collArray[0].concat(...collArray.slice(1)); + return messages; + } + + const messages = await fetchAll(); + const premiumKey = ""; + + const messagesJSON = await generateMessages(messages, premiumKey, "https://m.ticket.pm"); + zlib.gzip(JSON.stringify(messagesJSON), async (err, compressed) => { + if (err) { + console.error(err); + } else { + const ts = await axios + .post(`${domain}upload?key=${premiumKey}&uuid=${client.config.uuidType}`, JSON.stringify(compressed), { + headers: { + "Content-Type": "application/json" + } + }) + .catch(console.error); + _close(ts?.data); + } + }); +} + +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/src/utils/close_askReason.ts b/src/utils/close_askReason.ts new file mode 100644 index 00000000..0f8205ab --- /dev/null +++ b/src/utils/close_askReason.ts @@ -0,0 +1,60 @@ +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ActionRowBuilder, ButtonInteraction, CommandInteraction, GuildMember, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import { DiscordClient } from "../Types"; + +export const closeAskReason = async(interaction: CommandInteraction | ButtonInteraction, client: DiscordClient) => { + if ( + client.config.whoCanCloseTicket === "STAFFONLY" && + !(interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)) + ) + return interaction + .reply({ + content: client.locales.ticketOnlyClosableByStaff, + ephemeral: true, + }) + .catch((e) => console.log(e)); + + const modal = new ModalBuilder().setCustomId("askReasonClose").setTitle(client.locales.modals.reasonTicketClose.title); + + const input = new TextInputBuilder() + .setCustomId("reason") + .setLabel(client.locales.modals.reasonTicketClose.label) + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder(client.locales.modals.reasonTicketClose.placeholder) + .setMaxLength(256); + + const firstActionRow = new ActionRowBuilder().addComponents(input); + modal.addComponents(firstActionRow); + await interaction.showModal(modal).catch((e) => console.log(e)); +}; + +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/src/utils/createTicket.ts b/src/utils/createTicket.ts new file mode 100644 index 00000000..9d465e8d --- /dev/null +++ b/src/utils/createTicket.ts @@ -0,0 +1,227 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Collection, EmbedBuilder, ModalSubmitInteraction, PermissionFlagsBits, StringSelectMenuInteraction, TextInputComponent } from "discord.js"; +import { DiscordClient } from "../Types"; +import {TicketType} from "../Types"; +import { log } from "./logs"; + +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @param {Discord.Interaction} interaction + * @param {Discord.Client} client + * @param {Object} ticketType + * @param {Object|string} reasons + */ +export const createTicket = async (interaction: StringSelectMenuInteraction | ModalSubmitInteraction, client: DiscordClient, ticketType: TicketType, reasons?: Collection | string) => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async function (resolve, reject) { + await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); + + const reason: string[] = []; + let allReasons = ""; + + if (typeof reasons === "object") { + reasons.forEach(async (r) => { + reason.push(r.value); + }); + allReasons = reason.map((r, i) => `Question ${i + 1}: ${r}`).join(", "); + } + if(typeof reasons === "string") allReasons = reasons; + + let ticketName = ""; + + let ticketCount = (await client.prisma.$queryRaw<[{count: bigint}]> + `SELECT COUNT(*) as count FROM tickets`)[0].count; + + if (ticketType.ticketNameOption) { + ticketName = ticketType.ticketNameOption + .replace("USERNAME", interaction.user.username) + .replace("USERID", interaction.user.id) + .replace("TICKETCOUNT", ticketCount.toString() ?? "0"); + } else { + ticketName = client.config.ticketNameOption + .replace("USERNAME", interaction.user.username) + .replace("USERID", interaction.user.id) + .replace("TICKETCOUNT", ticketCount.toString() ?? "0"); + } + if(!interaction.guild) return console.error("Interaction createTicket was not executed in a guild"); + + const channel = await client.guilds.cache.get(client.config.guildId)?.channels.create({ + name: ticketName, + parent: ticketType.categoryId, + permissionOverwrites: [ + { + id: interaction.guild.roles.everyone, + deny: [PermissionFlagsBits.ViewChannel], + }, + ], + }); + + if (!channel) return reject("Couldn't create the ticket channel."); + log( + { + LogType: "ticketCreate", + user: interaction.user, + reason: allReasons, + ticketChannelId: channel.id + }, + client + ); + + // Client.db is set here and incremented ticket count + ticketCount++; + + channel.permissionOverwrites + .edit(interaction.user, { + SendMessages: true, + AddReactions: true, + ReadMessageHistory: true, + AttachFiles: true, + ViewChannel: true, + }) + .catch((e) => console.log(e)); + + if (client.config.rolesWhoHaveAccessToTheTickets.length > 0) { + client.config.rolesWhoHaveAccessToTheTickets.forEach(async (role) => { + channel.permissionOverwrites + .edit(role, { + SendMessages: true, + AddReactions: true, + ReadMessageHistory: true, + AttachFiles: true, + ViewChannel: true, + }) + .catch((e) => console.log(e)); + }); + } + const lEmbeds = client.locales.embeds; + const footer = lEmbeds.ticketOpened.footer.text.replace("ticket.pm", ""); + if(ticketType.color?.toString().trim() === "") ticketType.color = undefined; + const ticketOpenedEmbed = new EmbedBuilder({ + ...lEmbeds.ticketOpened, + color: 0, + }) + .setColor(ticketType.color ?? client.config.mainColor) + .setTitle(lEmbeds.ticketOpened.title.replace("CATEGORYNAME", ticketType.name)) + .setDescription( + ticketType.customDescription + ? ticketType.customDescription + .replace("CATEGORYNAME", ticketType.name) + .replace("USERNAME", interaction.user.username) + .replace("USERID", interaction.user.id) + .replace("TICKETCOUNT", ticketCount.toString() || "0") + .replace("REASON1", reason[0]) + .replace("REASON2", reason[1]) + .replace("REASON3", reason[2]) + .replace("REASON4", reason[3]) + .replace("REASON5", reason[4]) + .replace("REASON6", reason[5]) + .replace("REASON7", reason[6]) + .replace("REASON8", reason[7]) + .replace("REASON9", reason[8]) + : lEmbeds.ticketOpened.description + .replace("CATEGORYNAME", ticketType.name) + .replace("USERNAME", interaction.user.username) + .replace("USERID", interaction.user.id) + .replace("TICKETCOUNT", ticketCount.toString() || "0") + .replace("REASON1", reason[0]) + .replace("REASON2", reason[1]) + .replace("REASON3", reason[2]) + .replace("REASON4", reason[3]) + .replace("REASON5", reason[4]) + .replace("REASON6", reason[5]) + .replace("REASON7", reason[6]) + .replace("REASON8", reason[7]) + .replace("REASON9", reason[8]) + ) + .setFooter({ + // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) + text: `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`, // Please respect the LICENSE :D + // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) + iconURL: lEmbeds.ticketOpened.footer.iconUrl + }); + + const row = new ActionRowBuilder(); + + if (client.config.closeButton) { + if (client.config.askReasonWhenClosing) { + row.addComponents( + new ButtonBuilder() + .setCustomId("close_askReason") + .setLabel(client.locales.buttons.close.label) + .setEmoji(client.locales.buttons.close.emoji) + .setStyle(ButtonStyle.Danger) + ); + } else { + row.addComponents( + new ButtonBuilder() + .setCustomId("close") + .setLabel(client.locales.buttons.close.label) + .setEmoji(client.locales.buttons.close.emoji) + .setStyle(ButtonStyle.Danger) + ); + } + } + + if (client.config.claimButton) { + row.addComponents( + new ButtonBuilder() + .setCustomId("claim") + .setLabel(client.locales.buttons.claim.label) + .setEmoji(client.locales.buttons.claim.emoji) + .setStyle(ButtonStyle.Primary) + ); + } + + const body = { + embeds: [ticketOpenedEmbed], + content: `<@${interaction.user.id}> ${ + client.config.pingRoleWhenOpened ? client.config.roleToPingWhenOpenedId.map((x) => `<@&${x}>`).join(", ") : "" + }`, + components: [] as ActionRowBuilder[], + }; + + if (row.components.length > 0) body.components = [row]; + + channel + .send(body) + .then((msg) => { + client.prisma.tickets.create({ + data: { + category: JSON.stringify(ticketType), + reason: allReasons, + creator: interaction.user.id, + createdat: Date.now(), + channelid: channel.id, + messageid: msg.id + } + }).then(); // Again why tf do I need .then()?!?!? + msg.pin().then(() => { + msg.channel.bulkDelete(1); + }); + interaction + .editReply({ + content: client.locales.ticketOpenedMessage.replace("TICKETCHANNEL", `<#${channel.id}>`), + components: [], + + }) + .catch((e) => console.log(e)); + + resolve(true); + }) + .catch((e) => console.log(e)); + }); +}; diff --git a/utils/delete.js b/src/utils/delete.ts similarity index 60% rename from utils/delete.js rename to src/utils/delete.ts index 65943dac..fc38cf9f 100644 --- a/utils/delete.js +++ b/src/utils/delete.ts @@ -14,29 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports = { - async deleteTicket(interaction, client) { - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); - if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - - client.log( - "ticketDelete", - { - user: { - tag: interaction.user.tag, - id: interaction.user.id, - avatarURL: interaction.user.displayAvatarURL(), - }, - ticketId: ticket.id, - ticketCreatedAt: ticket.createdAt, - transcriptURL: ticket.transcriptURL, - }, - client - ); - - await interaction.deferUpdate(); - interaction.channel.delete().catch((e) => console.log(e)); - }, +import { ButtonInteraction } from "discord.js"; +import { DiscordClient } from "../Types"; +import { log } from "./logs"; + +export const deleteTicket = async (interaction: ButtonInteraction, client: DiscordClient) => { + const ticket = await client.prisma.tickets.findUnique({ + where: { + channelid: interaction.channel?.id + } + }); + + if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); + log( + { + LogType: "ticketDelete", + user: interaction.user, + ticketId: ticket.id, + ticketCreatedAt: ticket.createdat, + transcriptURL: ticket.transcript ?? undefined, + }, + client + ); + await interaction.deferUpdate(); + interaction.channel?.delete().catch((e) => console.log(e)); }; /* diff --git a/src/utils/logs.ts b/src/utils/logs.ts new file mode 100644 index 00000000..f9f82d2d --- /dev/null +++ b/src/utils/logs.ts @@ -0,0 +1,185 @@ +import Discord, { ChannelType, TextChannel, User } from "discord.js"; +import { DiscordClient } from "../Types"; + +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +type log = { + LogType: "ticketCreate" + user: User + ticketChannelId?: string; + reason?: string; +} | { + LogType: "ticketClaim" | "ticketClose" + user: User + ticketChannelId?: string; + ticketId?: string | number; + reason?: string; + ticketCreatedAt: number | bigint; +} | { + LogType: "ticketDelete" + user: User + ticketChannelId?: string; + ticketId?: string | number; + reason?: string; + ticketCreatedAt: number | bigint; + transcriptURL?: string; + +} | { + LogType: "userAdded" | "userRemoved" + user: User; + target: { + id?: string + }; + ticketChannelId?: string; + reason?: string; + ticketId?: string | number; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const log = async(logs: log, client: DiscordClient) => { + if (!client.config.logs) return; + if (!client.config.logsChannelId) return; + const channel = await client.channels + .fetch(client.config.logsChannelId) + .catch((e) => console.error("The channel to log events is not found!\n", e)); + if (!channel) return console.error("The channel to log events is not found!"); + if (!channel.isTextBased() || + channel.type === ChannelType.DM || + channel.type === ChannelType.PrivateThread || + channel.type === ChannelType.PublicThread) return console.error("Invalid Channel!"); + + const webhook = (await (channel as TextChannel).fetchWebhooks()).find((wh) => wh.token) ?? + await (channel as TextChannel).createWebhook({ name: "Ticket Bot Logs" }); + + if (logs.LogType === "ticketCreate") { + const embed = new Discord.EmbedBuilder() + .setColor("#3ba55c") + .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) + .setDescription(`${logs.user.tag} (<@${logs.user.id}>) Created a ticket (<#${logs.ticketChannelId}>) with the reason: \`${logs.reason}\``); + + webhook + .send({ + username: "Ticket Created", + avatarURL: "https://i.imgur.com/M38ZmjM.png", + embeds: [embed], + }) + .catch((e) => console.log(e)); + } + if (logs.LogType === "ticketClaim") { + const embed = new Discord.EmbedBuilder() + .setColor("#faa61a") + .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) + .setDescription( + `${logs.user.tag} (<@${logs.user.id}>) Claimed the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>) after ${client.msToHm( + new Date(Number(BigInt(Date.now()) - BigInt(logs.ticketCreatedAt))) + )} of creation` + ); + webhook + .send({ + username: "Ticket Claimed", + avatarURL: "https://i.imgur.com/qqEaUyR.png", + embeds: [embed], + }) + .catch((e) => console.log(e)); + } + + if (logs.LogType === "ticketClose") { + const embed = new Discord.EmbedBuilder() + .setColor("#ed4245") + .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) + .setDescription( + `${logs.user.tag} (<@${logs.user.id}>) Closed the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>) with the reason: \`${ + logs.reason + }\` after ${client.msToHm(Number(BigInt(Date.now()) - BigInt(logs.ticketCreatedAt)))} of creation` + ); + + webhook + .send({ + username: "Ticket Closed", + avatarURL: "https://i.imgur.com/5ShDA4g.png", + embeds: [embed], + }) + .catch((e) => console.log(e)); + } + + if (logs.LogType === "ticketDelete") { + const embed = new Discord.EmbedBuilder() + .setColor("#ed4245") + .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) + .setDescription( + `${logs.user.tag} (<@${logs.user.id}>) Deleted the ticket n°${logs.ticketId} after ${client.msToHm( + new Date(Number(BigInt(Date.now()) - BigInt(logs.ticketCreatedAt))) + )} of creation\n\nTranscript: ${logs.transcriptURL}` + ); + + webhook + .send({ + username: "Ticket Deleted", + avatarURL: "https://i.imgur.com/obTW2BS.png", + embeds: [embed], + }) + .catch((e) => console.log(e)); + } + + if (logs.LogType === "userAdded") { + const embed = new Discord.EmbedBuilder() + .setColor("#3ba55c") + .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) + .setDescription( + `${logs.user.tag} (<@${logs.user.id}>) Added <@${logs.target.id}> (${logs.target.id}) to the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>)` + ); + + webhook + .send({ + username: "User Added", + avatarURL: "https://i.imgur.com/G6QPFBV.png", + embeds: [embed], + }) + .catch((e) => console.log(e)); + } + if (logs.LogType === "userRemoved") { + const embed = new Discord.EmbedBuilder() + .setColor("#ed4245") + .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) + .setDescription( + `${logs.user.tag} (<@${logs.user.id}>) Removed <@${logs.target.id}> (${logs.target.id}) from the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>)` + ); + webhook + .send({ + username: "User Removed", + avatarURL: "https://i.imgur.com/eFJ8xxC.png", + embeds: [embed], + }) + .catch((e) => console.log(e)); + } +}; + +/* +Copyright 2023 Sayrix (github.com/Sayrix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..25b5b7ee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src/", + "allowJs": true, + "module": "CommonJS", + "target": "ESNext", + "strict": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": false, + "sourceMap": true, + "inlineSources": true, + "sourceRoot": "/" + }, + "include": [ + "./src/**/*", + ] +} \ No newline at end of file diff --git a/utils/claim.js b/utils/claim.js deleted file mode 100644 index 54fc6d5c..00000000 --- a/utils/claim.js +++ /dev/null @@ -1,116 +0,0 @@ -// eslint-disable-next-line no-unused-vars -const Discord = require("discord.js"); - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -module.exports = { - /** - * @param {Discord.CommandInteraction} interaction - * @param {Discord.Client} client - */ - async claim(interaction, client) { - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); - if (!ticket) - return interaction.reply({ - content: "Ticket not found", - ephemeral: true, - }); - - const canClaim = interaction.member.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)); - - if (!canClaim) - return interaction - .reply({ - content: client.locales.ticketOnlyClaimableByStaff, - ephemeral: true, - }) - .catch((e) => console.log(e)); - - if (ticket.claimed) - return interaction - .reply({ - content: client.locales.ticketAlreadyClaimed, - ephemeral: true, - }) - .catch((e) => console.log(e)); - - client.log( - "ticketClaim", - { - user: { - tag: interaction.user.tag, - id: interaction.user.id, - avatarURL: interaction.user.displayAvatarURL(), - }, - ticketId: ticket.id, - ticketChannelId: interaction.channel.id, - ticketCreatedAt: ticket.createdAt, - }, - client - ); - - await client.db.set(`tickets_${interaction.channel.id}.claimed`, true); - await client.db.set(`tickets_${interaction.channel.id}.claimedBy`, interaction.user.id); - await client.db.set(`tickets_${interaction.channel.id}.claimedAt`, Date.now()); - - await interaction.channel.messages.fetch(); - const messageId = await client.db.get(`tickets_${interaction.channel.id}.messageId`); - const msg = interaction.channel.messages.cache.get(messageId); - - const embed = msg.embeds[0].data; - embed.description = embed.description + `\n\n ${client.locales.other.claimedBy.replace("USER", `<@${interaction.user.id}>`)}`; - - msg.components[0].components.map((x) => { - if (x.data.custom_id === "claim") x.data.disabled = true; - }); - - msg - .edit({ - content: msg.content, - embeds: [embed], - components: msg.components, - }) - .catch((e) => console.log(e)); - - interaction - .reply({ - content: client.locales.ticketClaimedMessage.replace("USER", `<@${interaction.user.id}>`), - ephemeral: false, - }) - .catch((e) => console.log(e)); - - if (client.config.ticketNamePrefixWhenClaimed) { - interaction.channel.setName(`${client.config.ticketNamePrefixWhenClaimed}${interaction.channel.name}`).catch((e) => console.log(e)); - } - }, -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ diff --git a/utils/close.js b/utils/close.js deleted file mode 100644 index 59494ec0..00000000 --- a/utils/close.js +++ /dev/null @@ -1,270 +0,0 @@ -const { generateMessages } = require("ticket-bot-transcript-uploader"); -const zlib = require("zlib"); -const axios = require("axios"); -const Discord = require("discord.js"); -let domain = "https://ticket.pm/"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -module.exports = { - async close(interaction, client, reason) { - if (!client.config.createTranscript) domain = client.locales.other.unavailable; - - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); - if (!ticket) return interaction.editReply({ content: "Ticket not found" }).catch((e) => console.log(e)); - - if ( - client.config.whoCanCloseTicket === "STAFFONLY" && - !interaction.member.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)) - ) - return interaction - .editReply({ - content: client.locales.ticketOnlyClosableByStaff, - ephemeral: true - }) - .catch((e) => console.log(e)); - - if (ticket.closed) - return interaction - .editReply({ - content: client.locales.ticketAlreadyClosed, - ephemeral: true - }) - .catch((e) => console.log(e)); - - client.log( - "ticketClose", - { - user: { - tag: interaction.user.tag, - id: interaction.user.id, - avatarURL: interaction.user.displayAvatarURL() - }, - ticketId: ticket.id, - ticketChannelId: interaction.channel.id, - ticketCreatedAt: ticket.createdAt, - reason: reason - }, - client - ); - - await client.db.set(`tickets_${interaction.channel.id}.closedBy`, interaction.user.id); - await client.db.set(`tickets_${interaction.channel.id}.closedAt`, Date.now()); - - if (reason) { - await client.db.set(`tickets_${interaction.channel.id}.closeReason`, reason); - } else { - await client.db.set(`tickets_${interaction.channel.id}.closeReason`, client.locales.other.noReasonGiven); - } - - const creator = await client.db.get(`tickets_${interaction.channel.id}.creator`); - const invited = await client.db.get(`tickets_${interaction.channel.id}.invited`); - - interaction.channel.permissionOverwrites - .edit(creator, { - ViewChannel: false - }) - .catch((e) => console.log(e)); - - invited.forEach(async (user) => { - interaction.channel.permissionOverwrites - .edit(user, { - ViewChannel: false - }) - .catch((e) => console.log(e)); - }); - - interaction - .editReply({ - content: client.locales.ticketCreatingTranscript - }) - .catch((e) => console.log(e)); - - await interaction.channel.messages.fetch(); - - async function close(id) { - if (client.config.closeTicketCategoryId) interaction.channel.setParent(client.config.closeTicketCategoryId).catch((e) => console.log(e)); - - const messageId = await client.db.get(`tickets_${interaction.channel.id}.messageId`); - const msg = interaction.channel.messages.cache.get(messageId); - const embed = msg.embeds[0].data; - - msg.components[0]?.components?.map((x) => { - if (x.data.custom_id === "close") x.data.disabled = true; - if (x.data.custom_id === "close_askReason") x.data.disabled = true; - }); - - msg - .edit({ - content: msg.content, - embeds: [embed], - components: msg.components - }) - .catch((e) => console.log(e)); - - await client.db.set(`tickets_${interaction.channel.id}.closed`, true); - - interaction.channel - .send({ - content: client.locales.ticketTranscriptCreated.replace( - "TRANSCRIPTURL", - domain === client.locales.other.unavailable ? client.locales.other.unavailable : `<${domain}${id}>` - ) - }) - .catch((e) => console.log(e)); - await client.db.set( - `tickets_${interaction.channel.id}.transcriptURL`, - domain === client.locales.other.unavailable ? client.locales.other.unavailable : `${domain}${id}` - ); - const ticket = await client.db.get(`tickets_${interaction.channel.id}`); - - const row = new Discord.ActionRowBuilder().addComponents( - new Discord.ButtonBuilder().setCustomId("deleteTicket").setLabel(client.locales.other.deleteTicketButtonMSG).setStyle(Discord.ButtonStyle.Danger) - ); - - interaction.channel - .send({ - embeds: [ - JSON.parse( - JSON.stringify(client.locales.embeds.ticketClosed) - .replace("TICKETCOUNT", ticket.id) - .replace("REASON", ticket.closeReason.replace(/[\n\r]/g, "\\n")) - .replace("CLOSERNAME", interaction.user.tag) - ) - ], - components: [row] - }) - .catch((e) => console.log(e)); - - const tiketClosedDMEmbed = new Discord.EmbedBuilder() - .setColor(client.embeds.ticketClosedDM.color ? client.embeds.ticketClosedDM.color : client.config.mainColor) - .setDescription( - client.embeds.ticketClosedDM.description - .replace("TICKETCOUNT", ticket.id) - .replace("TRANSCRIPTURL", `[\`${domain}${id}\`](${domain}${id})`) - .replace("REASON", ticket.closeReason) - .replace("CLOSERNAME", interaction.user.tag) - ) - - /* - Copyright 2023 Sayrix (github.com/Sayrix) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - .setFooter({ - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - text: "ticket.pm" + client.embeds.ticketClosedDM.footer.text.replace("ticket.pm", ""), // Please respect the LICENSE :D - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - iconUrl: client.embeds.ticketClosedDM.footer.iconUrl - }); - - /* - Copyright 2023 Sayrix (github.com/Sayrix) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - client.users.fetch(creator).then((user) => { - user - .send({ - embeds: [tiketClosedDMEmbed] - }) - .catch((e) => console.log(e)); - }); - } - - if (!client.config.createTranscript) { - close(""); - return; - } - - async function fetchAll() { - let collArray = new Array(); - let lastID = interaction.channel.lastMessageID; - // eslint-disable-next-line no-constant-condition - while (true) { - const fetched = await interaction.channel.messages.fetch({ limit: 100, before: lastID }); - if (fetched.size === 0) { - break; - } - collArray.push(fetched); - lastID = fetched.last().id; - if (fetched.size !== 100) { - break; - } - } - const messages = collArray[0].concat(...collArray.slice(1)); - return messages; - } - - const messages = await fetchAll(); - const premiumKey = ""; - - const messagesJSON = await generateMessages(messages, premiumKey, "https://m.ticket.pm"); - zlib.gzip(JSON.stringify(messagesJSON), async (err, compressed) => { - if (err) { - console.error(err); - } else { - const ts = await axios - .post(`${domain}upload?key=${premiumKey}`, JSON.stringify(compressed), { - headers: { - "Content-Type": "application/json" - } - }) - .catch(console.error); - close(ts.data); - } - }); - } -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ diff --git a/utils/close_askReason.js b/utils/close_askReason.js deleted file mode 100644 index 8408901e..00000000 --- a/utils/close_askReason.js +++ /dev/null @@ -1,61 +0,0 @@ -const Discord = require("discord.js"); - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -module.exports = { - async closeAskReason(interaction, client) { - if ( - client.config.whoCanCloseTicket === "STAFFONLY" && - !interaction.member.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id)) - ) - return interaction - .reply({ - content: client.locales.ticketOnlyClosableByStaff, - ephemeral: true, - }) - .catch((e) => console.log(e)); - - const modal = new Discord.ModalBuilder().setCustomId("askReasonClose").setTitle(client.locales.modals.reasonTicketClose.title); - - const input = new Discord.TextInputBuilder() - .setCustomId("reason") - .setLabel(client.locales.modals.reasonTicketClose.label) - .setStyle(Discord.TextInputStyle.Paragraph) - .setPlaceholder(client.locales.modals.reasonTicketClose.placeholder) - .setMaxLength(256); - - const firstActionRow = new Discord.ActionRowBuilder().addComponents(input); - modal.addComponents(firstActionRow); - await interaction.showModal(modal).catch((e) => console.log(e)); - }, -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ diff --git a/utils/createTicket.js b/utils/createTicket.js deleted file mode 100644 index 2efd41e4..00000000 --- a/utils/createTicket.js +++ /dev/null @@ -1,253 +0,0 @@ -const Discord = require("discord.js"); - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -module.exports = { - /** - * @param {Discord.Interaction} interaction - * @param {Discord.Client} client - * @param {Object} ticketType - * @param {Object|string} reasons - */ - async createTicket(interaction, client, ticketType, reasons) { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async function (resolve, reject) { - await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); - - let reason = []; - let allReasons; - if (typeof reasons === "object") { - reasons.forEach(async (r) => { - reason.push(r.value); - }); - - allReasons = reason.map((r, i) => `Question ${i + 1}: ${r}`).join(", "); - } - let ticketName = new String(); - - if (ticketType.ticketNameOption) { - ticketName = ticketType.ticketNameOption - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", (await client.db.get("temp.ticketCount")) || 0); - } else { - ticketName = client.config.ticketNameOption - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", (await client.db.get("temp.ticketCount")) || 0); - } - - const channel = await client.guilds.cache.get(client.config.guildId).channels.create({ - name: ticketName, - parent: ticketType.categoryId, - permissionOverwrites: [ - { - id: interaction.guild.roles.everyone, - deny: [Discord.PermissionFlagsBits.ViewChannel], - }, - ], - }); - - if (!channel) return reject("Couldn't create the ticket channel."); - - client.log( - "ticketCreate", - { - user: { - tag: interaction.user.tag, - id: interaction.user.id, - avatarURL: interaction.user.displayAvatarURL(), - }, - reason: allReasons, - ticketChannelId: channel.id, - }, - client - ); - - await client.db.add("temp.ticketCount", 1); - const ticketId = await client.db.get("temp.ticketCount"); - await client.db.set(`tickets_${channel.id}`, { - id: ticketId - 1, - category: ticketType, - reason: allReasons, - creator: interaction.user.id, - invited: [], - createdAt: Date.now(), - claimed: false, - claimedBy: null, - claimedAt: null, - closed: false, - closedBy: null, - closedAt: null, - }); - - channel.permissionOverwrites - .edit(interaction.user, { - SendMessages: true, - AddReactions: true, - ReadMessageHistory: true, - AttachFiles: true, - ViewChannel: true, - }) - .catch((e) => console.log(e)); - - if (client.config.rolesWhoHaveAccessToTheTickets.length > 0) { - client.config.rolesWhoHaveAccessToTheTickets.forEach(async (role) => { - channel.permissionOverwrites - .edit(role, { - SendMessages: true, - AddReactions: true, - ReadMessageHistory: true, - AttachFiles: true, - ViewChannel: true, - }) - .catch((e) => console.log(e)); - }); - } - - const ticketOpenedEmbed = new Discord.EmbedBuilder() - .setColor(ticketType.color ? ticketType.color : client.config.mainColor) - .setTitle(client.embeds.ticketOpened.title.replace("CATEGORYNAME", ticketType.name)) - .setDescription( - ticketType.customDescription - ? ticketType.customDescription - .replace("CATEGORYNAME", ticketType.name) - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", (await client.db.get("temp.ticketCount")) || 0) - .replace("REASON1", reason[0]) - .replace("REASON2", reason[1]) - .replace("REASON3", reason[2]) - .replace("REASON4", reason[3]) - .replace("REASON5", reason[4]) - .replace("REASON6", reason[5]) - .replace("REASON7", reason[6]) - .replace("REASON8", reason[7]) - .replace("REASON9", reason[8]) - : client.embeds.ticketOpened.description - .replace("CATEGORYNAME", ticketType.name) - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", (await client.db.get("temp.ticketCount")) || 0) - .replace("REASON1", reason[0]) - .replace("REASON2", reason[1]) - .replace("REASON3", reason[2]) - .replace("REASON4", reason[3]) - .replace("REASON5", reason[4]) - .replace("REASON6", reason[5]) - .replace("REASON7", reason[6]) - .replace("REASON8", reason[7]) - .replace("REASON9", reason[8]) - ) - /* - Copyright 2023 Sayrix (github.com/Sayrix) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - .setFooter({ - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - text: "ticket.pm" + client.embeds.ticketOpened.footer.text.replace("ticket.pm", ""), // Please respect the LICENSE :D - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - iconUrl: client.embeds.ticketOpened.footer.iconUrl, - }); - /* - Copyright 2023 Sayrix (github.com/Sayrix) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - const row = new Discord.ActionRowBuilder(); - - if (client.config.closeButton) { - if (client.config.askReasonWhenClosing) { - row.addComponents( - new Discord.ButtonBuilder() - .setCustomId("close_askReason") - .setLabel(client.locales.buttons.close.label) - .setEmoji(client.locales.buttons.close.emoji) - .setStyle(Discord.ButtonStyle.Danger) - ); - } else { - row.addComponents( - new Discord.ButtonBuilder() - .setCustomId("close") - .setLabel(client.locales.buttons.close.label) - .setEmoji(client.locales.buttons.close.emoji) - .setStyle(Discord.ButtonStyle.Danger) - ); - } - } - - if (client.config.claimButton) { - row.addComponents( - new Discord.ButtonBuilder() - .setCustomId("claim") - .setLabel(client.locales.buttons.claim.label) - .setEmoji(client.locales.buttons.claim.emoji) - .setStyle(Discord.ButtonStyle.Primary) - ); - } - - const body = { - embeds: [ticketOpenedEmbed], - content: `<@${interaction.user.id}> ${ - client.config.pingRoleWhenOpened ? client.config.roleToPingWhenOpenedId.map((x) => `<@&${x}>`).join(", ") : "" - }`, - }; - - if (row.components.length > 0) body.components = [row]; - - channel - .send(body) - .then((msg) => { - client.db.set(`tickets_${channel.id}.messageId`, msg.id); - msg.pin().then(() => { - msg.channel.bulkDelete(1); - }); - interaction - .editReply({ - content: client.locales.ticketOpenedMessage.replace("TICKETCHANNEL", `<#${channel.id}>`), - components: [], - ephemeral: true, - }) - .catch((e) => console.log(e)); - - resolve(true); - }) - .catch((e) => console.log(e)); - }); - }, -}; diff --git a/utils/logs.js b/utils/logs.js deleted file mode 100644 index 373214a8..00000000 --- a/utils/logs.js +++ /dev/null @@ -1,153 +0,0 @@ -const Discord = require("discord.js"); - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -module.exports = { - async log(logsType, logs, client) { - if (!client.config.logs) return; - if (!client.config.logsChannelId) return; - const channel = await client.channels - .fetch(client.config.logsChannelId) - .catch((e) => console.error("The channel to log events is not found!\n", e)); - if (!channel) return console.error("The channel to log events is not found!"); - - const webhook = (await channel.fetchWebhooks()).find((wh) => wh.token) ?? - await channel.createWebhook({ name: "Ticket Bot Logs" }); - - if (logsType === "ticketCreate") { - const embed = new Discord.EmbedBuilder() - .setColor("3ba55c") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.avatarURL }) - .setDescription(`${logs.user.tag} (<@${logs.user.id}>) Created a ticket (<#${logs.ticketChannelId}>) with the reason: \`${logs.reason}\``); - - webhook - .send({ - username: "Ticket Created", - avatarURL: "https://i.imgur.com/M38ZmjM.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logsType === "ticketClaim") { - const embed = new Discord.EmbedBuilder() - .setColor("faa61a") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.avatarURL }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Claimed the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>) after ${client.msToHm( - new Date(Date.now() - logs.ticketCreatedAt) - )} of creation` - ); - - webhook - .send({ - username: "Ticket Claimed", - avatarURL: "https://i.imgur.com/qqEaUyR.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logsType === "ticketClose") { - const embed = new Discord.EmbedBuilder() - .setColor("ed4245") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.avatarURL }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Closed the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>) with the reason: \`${ - logs.reason - }\` after ${client.msToHm(new Date(Date.now() - logs.ticketCreatedAt))} of creation` - ); - - webhook - .send({ - username: "Ticket Closed", - avatarURL: "https://i.imgur.com/5ShDA4g.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logsType === "ticketDelete") { - const embed = new Discord.EmbedBuilder() - .setColor("ed4245") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.avatarURL }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Deleted the ticket n°${logs.ticketId} after ${client.msToHm( - new Date(Date.now() - logs.ticketCreatedAt) - )} of creation\n\nTranscript: ${logs.transcriptURL}` - ); - - webhook - .send({ - username: "Ticket Deleted", - avatarURL: "https://i.imgur.com/obTW2BS.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logsType === "userAdded") { - const embed = new Discord.EmbedBuilder() - .setColor("3ba55c") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.avatarURL }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Added <@${logs.added.id}> (${logs.added.id}) to the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>)` - ); - - webhook - .send({ - username: "User Added", - avatarURL: "https://i.imgur.com/G6QPFBV.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logsType === "userRemoved") { - const embed = new Discord.EmbedBuilder() - .setColor("ed4245") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.avatarURL }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Removed <@${logs.removed.id}> (${logs.removed.id}) from the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>)` - ); - - webhook - .send({ - username: "User Removed", - avatarURL: "https://i.imgur.com/eFJ8xxC.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - }, -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ diff --git a/utils/pgsqlDriver.js b/utils/pgsqlDriver.js deleted file mode 100644 index 8a776e8f..00000000 --- a/utils/pgsqlDriver.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Official pre-released quick.db Postgresql driver. Remove this file when the driver is officially released ~ zhiyan114 - * Why add this? PostgreSQL is a better database than MySQL. Alternative would be to use Prisma, - * but I don't want to esentially re-write the database logic and cause confusions. - * Source: https://github.com/plexidev/quick.db/blob/dev/src/drivers/PostgresDriver.ts - * LICENSE: https://github.com/plexidev/quick.db/blob/dev/LICENSE.md - */ - -// eslint-disable-next-line node/no-missing-require -const { Client } = require("pg"); - -module.exports = class PostgresDriver { - instance; - config; - conn; - - constructor(config) { - this.config = config; - } - - static createSingleton(config) { - if (!this.instance) this.instance = new PostgresDriver(config); - return this.instance; - } - - async connect() { - this.conn = new Client(this.config); - await this.conn.connect(); - } - - async disconnect() { - this.checkConnection(); - await this.conn.end(); - } - - checkConnection() { - if (!this.conn) { - throw new Error("No connection to postgres database"); - } - } - - async prepare(table) { - this.checkConnection(); - await this.conn.query(`CREATE TABLE IF NOT EXISTS ${table} (id VARCHAR(255), value TEXT)`); - } - - async getAllRows(table) { - this.checkConnection(); - const queryResult = await this.conn.query(`SELECT * FROM ${table}`); - return queryResult.rows.map((row) => ({ - id: row.id, - value: JSON.parse(row.value) - })); - } - - async getRowByKey(table, key) { - this.checkConnection(); - const queryResult = await this.conn.query(`SELECT value FROM ${table} WHERE id = $1`, [key]); - - if (queryResult.rowCount < 1) return [null, false]; - return [JSON.parse(queryResult.rows[0].value), true]; - } - - async setRowByKey(table, key, value, update) { - this.checkConnection(); - - const stringifiedValue = JSON.stringify(value); - - if (update) { - await this.conn.query(`UPDATE ${table} SET value = $1 WHERE id = $2`, [stringifiedValue, key]); - } else { - await this.conn.query(`INSERT INTO ${table} (id, value) VALUES ($1, $2)`, [key, stringifiedValue]); - } - - return value; - } - - async deleteAllRows(table) { - this.checkConnection(); - const queryResult = await this.conn.query(`DELETE FROM ${table}`); - return queryResult.rowCount; - } - - async deleteRowByKey(table, key) { - this.checkConnection(); - const queryResult = await this.conn.query(`DELETE FROM ${table} WHERE id = $1`, [key]); - return queryResult.rowCount; - } -};