aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--174bg/discord-bot/.gitignore3
-rw-r--r--174bg/discord-bot/commands/_index.js6
-rw-r--r--174bg/discord-bot/commands/close.js55
-rw-r--r--174bg/discord-bot/commands/requisition.js32
-rw-r--r--174bg/discord-bot/main.js323
-rw-r--r--174bg/discord-bot/utilities/createTicket.js52
-rw-r--r--174bg/discord-bot/utilities/notifyRoleByName.js19
7 files changed, 196 insertions, 294 deletions
diff --git a/174bg/discord-bot/.gitignore b/174bg/discord-bot/.gitignore
index 511bcd0..9b6ab89 100644
--- a/174bg/discord-bot/.gitignore
+++ b/174bg/discord-bot/.gitignore
@@ -1,3 +1,2 @@
/.env
-/node_modules/
-/uex-cache/ \ No newline at end of file
+/node_modules/ \ No newline at end of file
diff --git a/174bg/discord-bot/commands/_index.js b/174bg/discord-bot/commands/_index.js
new file mode 100644
index 0000000..06615dd
--- /dev/null
+++ b/174bg/discord-bot/commands/_index.js
@@ -0,0 +1,6 @@
+import requisition from "./requisition.js";
+import close from "./close.js";
+
+export { requisition, close };
+
+export default [requisition, close];
diff --git a/174bg/discord-bot/commands/close.js b/174bg/discord-bot/commands/close.js
new file mode 100644
index 0000000..25bdd41
--- /dev/null
+++ b/174bg/discord-bot/commands/close.js
@@ -0,0 +1,55 @@
+import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
+
+export default {
+ data: new SlashCommandBuilder()
+ .setName("close")
+ .setDescription(
+ "Closes the current ticket by logging the contents and deleting the channel.",
+ )
+ .setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
+ execute: async (interaction) => {
+ const channel = interaction.channel;
+
+ if (!channel || !channel.name.startsWith("ticket-")) {
+ return await interaction.reply({
+ content: "This command can only be used in a ticket channel.",
+ ephemeral: true,
+ });
+ }
+
+ // log the contents of the ticket and send to the logs channel if it exists
+ const messages = await channel.messages.fetch({ limit: 100 });
+ const log = messages
+ .map((message) => `${message.author.tag}: ${message.content}`)
+ .reverse()
+ .join("\n");
+
+ console.log(`Ticket log for ${channel.name}:\n${log}`);
+
+ const logsChannel = interaction.guild.channels.cache.find(
+ (ch) => ch.name === "logs" && ch.isTextBased(),
+ );
+
+ if (logsChannel)
+ await logsChannel.send({
+ content: `# Ticket log for ${channel.name}`,
+ files: [
+ {
+ name: `${channel.name}.txt`,
+ attachment: Buffer.from(
+ messages
+ .map(
+ (message) =>
+ `${message.author.tag}/${message.author.id}: ${message.content}`,
+ )
+ .reverse()
+ .join("\n"),
+ ),
+ },
+ ],
+ });
+
+ // delete the channel
+ await channel.delete();
+ },
+};
diff --git a/174bg/discord-bot/commands/requisition.js b/174bg/discord-bot/commands/requisition.js
new file mode 100644
index 0000000..be3cb8a
--- /dev/null
+++ b/174bg/discord-bot/commands/requisition.js
@@ -0,0 +1,32 @@
+import { SlashCommandBuilder } from "discord.js";
+import createTicket from "../utilities/createTicket.js";
+import notifyRoleByName from "../utilities/notifyRoleByName.js";
+
+export default {
+ data: new SlashCommandBuilder()
+ .setName("requisition")
+ .setDescription(
+ "Request items from the quartermaster. A new requisition ticket will be created.",
+ )
+ .addStringOption((option) =>
+ option
+ .setName("contents")
+ .setDescription("What are you requesting? Be as specific as possible.")
+ .setRequired(true),
+ ),
+ execute: async (interaction) => {
+ const contents = interaction.options.getString("contents");
+
+ const ticket = await createTicket(interaction, ["quartermaster"]);
+
+ await ticket.send({
+ content: `**New requisition request from <@${interaction.user.id}>**: ${contents}`,
+ });
+
+ // reply to the user
+ await interaction.reply({
+ content: `Your requisition ticket has been created: <#${ticket.id}>`,
+ ephemeral: true,
+ });
+ },
+};
diff --git a/174bg/discord-bot/main.js b/174bg/discord-bot/main.js
index 30d2a1d..fc4da76 100644
--- a/174bg/discord-bot/main.js
+++ b/174bg/discord-bot/main.js
@@ -4,175 +4,15 @@ import {
GatewayIntentBits,
REST,
Routes,
+ ChannelType,
PermissionFlagsBits,
SlashCommandBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
} from "discord.js";
-import fs from "node:fs";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const commands = [
- {
- data: new SlashCommandBuilder()
- .setName("scope")
- .setDescription("Returns the current scope of the bot."),
- execute: async (interaction) => {
- await interaction.reply(codewrap("json", JSON.stringify({
- guildIdFromEnv: process.env.DISCORD_GUILD_ID,
- guildIdFromInteraction: interaction.guild.id,
- doGuildIdsMatch:
- process.env.DISCORD_GUILD_ID === interaction.guild.id,
- }, null, 2)));
- }
- },
- {
- data: new SlashCommandBuilder()
- .setName("create-requisition-ticket")
- .setDescription(
- "Creates a requisition ticket for the quartermaster to review.",
- )
- .addStringOption((option) =>
- option
- .setName("contents")
- .setDescription(
- "What are you requesting? Be as specific as possible.",
- )
- .setRequired(true),
- ),
- execute: async (interaction) => {
- // extract options
- const [contents] = [interaction.options.getString("contents")];
-
- // get "tickets" category
- const ticketsCategory = interaction.guild.channels.cache.find(
- (channel) =>
- channel.name.toLowerCase() === "tickets" && channel.type === 4,
- );
-
- if (!ticketsCategory)
- return await interaction.reply("Tickets category not found.");
-
- // get the quartermaster role id
- let quartermasterRole = interaction.guild.roles.cache.find(
- (role) => role.name.toLowerCase() === "quartermaster",
- );
-
- if (!quartermasterRole)
- {
- // no qm role? make it
- const newRole = await interaction.guild.roles.create({
- name: "Quartermaster",
- color: "Blue",
- });
-
- quartermasterRole = newRole;
- }
-
- // create a new text channel under the "tickets" category with the following format:
- // req-<timestamp_short>
- const timestamp = new Date()
- .toISOString()
- .replace(/[-:.T]/g, "")
- .slice(2, 16); // YYMMDDHHMMSS
- const channelName = `req-${timestamp}`;
-
- const newChannel = await interaction.guild.channels.create({
- name: channelName,
- type: 0, // text channel
- parent: ticketsCategory.id,
- // set permissions so that only the user who created the ticket and the quartermaster role can view it
- permissionOverwrites: [
- {
- id: interaction.user.id,
- allow: [
- PermissionFlagsBits.ViewChannel,
- PermissionFlagsBits.SendMessages,
- ],
- },
- {
- id: quartermasterRole.id,
- allow: [
- PermissionFlagsBits.ViewChannel,
- PermissionFlagsBits.SendMessages,
- ],
- },
- ],
- });
-
- // send a normal message to the new channel with a close button that only the quartermaster role can see
- const row = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId("close_req_ticket")
- .setLabel("Close Ticket")
- .setStyle(ButtonStyle.Danger)
- .setDisabled(false),
- );
-
- await newChannel.send({
- content: `**Requisition Ticket #${timestamp}**\n**Contents:** ${contents}\n**Created by:** <@${interaction.user.id}>`,
- components: [row],
- });
-
- // reply to the user
- await interaction.reply({
- content: `Your requisition ticket has been created: <#${newChannel.id}>`,
- ephemeral: true,
- });
-
- // log
- sendMessageToLogsChannel(
- `Requisition ticket <#${newChannel.id}> created by <@${interaction.user.id}>`,
- );
-
- // notify all members with the quartermaster role if it exists
- // (member cache is kept up to date via the GuildMembers intent, so no
- // need to re-fetch all members here)
- const quartermasterMembers = quartermasterRole.members;
- for (const member of quartermasterMembers.values()) {
- await member.send(
- `A new requisition ticket has been created: <#${newChannel.id}>`,
- );
- }
- },
- },
- {
- data: new SlashCommandBuilder()
- .setName("delete-all-requisition-tickets")
- .setDescription("Deletes all requisition tickets.")
- .setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
- execute: async (interaction) => {
- // get "tickets" category
- const ticketsCategory = interaction.guild.channels.cache.find(
- (channel) =>
- channel.name.toLowerCase() === "tickets" && channel.type === 4,
- );
-
- if (!ticketsCategory)
- return await interaction.reply("Tickets category not found.");
-
- // get all channels under the "tickets" category that start with "req-"
- const requisitionTickets = ticketsCategory.children.cache.filter(
- (channel) => channel.name.startsWith("req-"),
- );
-
- // delete all requisition tickets
- for (const ticket of requisitionTickets.values()) {
- await ticket.delete();
- }
-
- // log
- sendMessageToLogsChannel(
- `<@${interaction.user.id}> used \`/delete-all-requisition-tickets\` and deleted **${requisitionTickets.size}** requisition tickets.`,
- );
- },
- },
-];
+import commands from "./commands/_index.js";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
@@ -181,41 +21,7 @@ const client = new Client({
client.on(Events.ClientReady, async (readyClient) => {
console.log(`Logged in as ${readyClient.user.tag}!`);
- try {
- const rest = new REST({ version: "10" }).setToken(
- process.env.DISCORD_BOT_TOKEN,
- );
-
- // clear global commands
- await rest.put(Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), {
- body: [],
- });
-
- console.log("Cleared global application commands.");
-
- // register guild-scoped commands
- await rest.put(Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.DISCORD_GUILD_ID), {
- body: commands.map((command) => command.data.toJSON()),
- });
-
- const guild = await client.guilds.fetch(process.env.DISCORD_GUILD_ID);
-
- // fetch all members once so the cache stays populated; the GuildMembers
- // intent keeps it in sync afterward via gateway events, avoiding repeated
- // opcode 8 (Request Guild Members) calls that can get rate limited
- await guild.members.fetch();
-
- console.log(`Successfully registered application commands for guild ${process.env.DISCORD_GUILD_ID} (${guild ? guild.name : "Unknown"}).`);
- } catch (error) {
- console.error(error);
- }
-
- cacheUexData();
-
- // every hour
- setInterval(async () => {
- await cacheUexData();
- }, 60 * 60 * 1000); // 1 hour
+ await registerCommands();
});
client.on(Events.InteractionCreate, async (interaction) => {
@@ -232,109 +38,42 @@ client.on(Events.InteractionCreate, async (interaction) => {
await command.execute(interaction);
}
}
-
- if (interaction.isButton()) {
- if (interaction.customId === "close_req_ticket") {
- // get the quartermaster role id
- const quartermasterRole = interaction.guild.roles.cache.find(
- (role) => role.name.toLowerCase() === "quartermaster",
- );
-
- if (!quartermasterRole)
- return await interaction.reply("Quartermaster role not found.");
-
- // check if the user has the quartermaster role or is an administrator
- if (
- !interaction.member.roles.cache.has(quartermasterRole.id) &&
- !interaction.member.permissions.has(PermissionFlagsBits.Administrator)
- ) {
- return await interaction.reply({
- content: "You do not have permission to close this ticket.",
- ephemeral: true,
- });
- }
-
- // fetch all messages in the channel
- const messages = await interaction.channel.messages.fetch();
-
- // capture channel info before it's deleted (and removed from cache)
- const channelId = interaction.channel.id;
- const channelName = interaction.channel.name;
-
- // delete the channel
- await interaction.channel.delete();
-
- // log
- sendMessageToLogsChannel({
- content: `<#${channelId}> closed by <@${interaction.user.id}>`,
- files: [
- {
- name: `${channelName}.txt`,
- attachment: Buffer.from(
- messages
- .map(
- (message) =>
- `${message.author.tag}/${message.author.id}: ${message.content}`,
- )
- .reverse()
- .join("\n"),
- ),
- },
- ],
- });
- }
- }
});
client.login(process.env.DISCORD_BOT_TOKEN);
-function sendMessageToLogsChannel(message) {
- const logsChannel = client.channels.cache.find(
- (channel) => channel.guild.id === process.env.DISCORD_GUILD_ID && channel.name === "logs" && channel.type === 0,
- );
-
- if (!logsChannel) return console.error("Logs channel not found.");
-
- logsChannel.send(message);
-}
-
-function codewrap(language, content) {
- return "```" + language + "\n" + content + "\n```";
-}
-
-async function cacheUexData() {
- const list = [
- "https://api.uexcorp.uk/2.0/commodities_raw_prices_all",
- "https://api.uexcorp.uk/2.0/items_prices_all",
- "https://api.uexcorp.uk/2.0/fuel_prices_all",
- "https://api.uexcorp.uk/2.0/commodities_prices_all",
- "https://api.uexcorp.uk/2.0/vehicles_purchases_prices_all",
- "https://api.uexcorp.uk/2.0/vehicles_rentals_prices_all"
- ];
-
- const cacheDir = path.join(__dirname, "uex-cache");
- fs.mkdirSync(cacheDir, { recursive: true });
+async function registerCommands() {
+ try {
+ const rest = new REST({ version: "10" }).setToken(
+ process.env.DISCORD_BOT_TOKEN,
+ );
- for (const url of list) {
- try {
- const response = await fetch(url);
+ // clear global commands
+ await rest.put(Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), {
+ body: [],
+ });
- if (!response.ok) {
- throw new Error(`Received status ${response.status}`);
- }
+ console.log("Cleared global application commands.");
- const data = await response.json();
+ // register guild-scoped commands
+ await rest.put(
+ Routes.applicationGuildCommands(
+ process.env.DISCORD_CLIENT_ID,
+ process.env.DISCORD_GUILD_ID,
+ ),
+ {
+ body: commands.map((command) => command.data.toJSON()),
+ },
+ );
- if (data === null || typeof data !== "object") {
- throw new Error("Response was not a JSON object/array");
- }
+ const guild = await client.guilds.fetch(process.env.DISCORD_GUILD_ID);
- // write to file
- const filePath = path.join(cacheDir, `${url.split("/").pop()}.json`);
+ await guild.members.fetch();
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
- } catch (error) {
- console.error(`Error fetching ${url}:`, error);
- }
+ console.log(
+ `Successfully registered application commands for guild ${process.env.DISCORD_GUILD_ID} (${guild ? guild.name : "Unknown"}).`,
+ );
+ } catch (error) {
+ console.error(error);
}
-} \ No newline at end of file
+}
diff --git a/174bg/discord-bot/utilities/createTicket.js b/174bg/discord-bot/utilities/createTicket.js
new file mode 100644
index 0000000..9b573d6
--- /dev/null
+++ b/174bg/discord-bot/utilities/createTicket.js
@@ -0,0 +1,52 @@
+import { ChannelType, PermissionFlagsBits } from "discord.js";
+
+export default async (interaction, roleNamesWithAccess) => {
+ const ticketsCategory = interaction.guild.channels.cache.find(
+ (channel) =>
+ channel.name.toLowerCase() === "tickets" &&
+ channel.type === ChannelType.GuildCategory,
+ );
+
+ if (!ticketsCategory)
+ return await interaction.reply("Tickets category not found.");
+
+ // get the roles that should have access to this ticket
+ const rolesWithAccess = roleNamesWithAccess.map((roleName) => {
+ return interaction.guild.roles.cache.find(
+ (role) => role.name.toLowerCase() === roleName.toLowerCase(),
+ );
+ });
+
+ // create a new text channel under the "tickets" category with the following format: ticket-<timestamp_short>
+ const timestamp = new Date()
+ .toISOString()
+ .replace(/[-:.T]/g, "")
+ .slice(2, 16);
+
+ const channelName = `ticket-${timestamp}`;
+
+ const newChannel = await interaction.guild.channels.create({
+ name: channelName,
+ type: ChannelType.GuildText,
+ parent: ticketsCategory.id,
+ // set permissions so that only the user who created the ticket and the roles with access can view it
+ permissionOverwrites: [
+ {
+ id: interaction.user.id,
+ allow: [
+ PermissionFlagsBits.ViewChannel,
+ PermissionFlagsBits.SendMessages,
+ ],
+ },
+ ...rolesWithAccess.map((role) => ({
+ id: role.id,
+ allow: [
+ PermissionFlagsBits.ViewChannel,
+ PermissionFlagsBits.SendMessages,
+ ],
+ })),
+ ],
+ });
+
+ return newChannel;
+};
diff --git a/174bg/discord-bot/utilities/notifyRoleByName.js b/174bg/discord-bot/utilities/notifyRoleByName.js
new file mode 100644
index 0000000..1b2b82a
--- /dev/null
+++ b/174bg/discord-bot/utilities/notifyRoleByName.js
@@ -0,0 +1,19 @@
+export default async (guild, roleName, message) => {
+ // get the role by name
+ const role = guild.roles.cache.find(
+ (role) => role.name.toLowerCase() === roleName.toLowerCase(),
+ );
+
+ if (!role) {
+ console.error(`Role "${roleName}" not found.`);
+ return;
+ }
+
+ // get all members with the role
+ const membersWithRole = role.members;
+
+ // send a DM to each member with the role
+ for (const member of membersWithRole.values()) {
+ await member.send(message);
+ }
+};