diff options
| author | Alex Pooley (@zuedev) <zuedev@gmail.com> | 2026-07-21 23:02:52 +0100 |
|---|---|---|
| committer | Alex Pooley (@zuedev) <zuedev@gmail.com> | 2026-07-21 23:02:52 +0100 |
| commit | da0509c91376f56f133872eddd2e4282d76f215e (patch) | |
| tree | 7d39fb76ab89855d17fab28ca7e000b3acd9a89d /174bg/discord-bot | |
| parent | 764f90577eeadd653f3f6248fbf2b0918c97a3d5 (diff) | |
| download | unnamed-group-da0509c91376f56f133872eddd2e4282d76f215e.tar unnamed-group-da0509c91376f56f133872eddd2e4282d76f215e.tar.gz unnamed-group-da0509c91376f56f133872eddd2e4282d76f215e.tar.bz2 unnamed-group-da0509c91376f56f133872eddd2e4282d76f215e.tar.xz unnamed-group-da0509c91376f56f133872eddd2e4282d76f215e.zip | |
more org changes
Diffstat (limited to '174bg/discord-bot')
| -rw-r--r-- | 174bg/discord-bot/README.md | 38 | ||||
| -rw-r--r-- | 174bg/discord-bot/commands/_index.js | 5 | ||||
| -rw-r--r-- | 174bg/discord-bot/commands/pocketbase.js | 395 | ||||
| -rw-r--r-- | 174bg/discord-bot/controllers/http.js | 25 | ||||
| -rw-r--r-- | 174bg/discord-bot/controllers/pocketbase.js | 18 | ||||
| -rw-r--r-- | 174bg/discord-bot/main.js | 62 | ||||
| -rw-r--r-- | 174bg/discord-bot/utilities/getUrl.js | 14 |
7 files changed, 45 insertions, 512 deletions
diff --git a/174bg/discord-bot/README.md b/174bg/discord-bot/README.md index dc65293..eaff01a 100644 --- a/174bg/discord-bot/README.md +++ b/174bg/discord-bot/README.md @@ -1,41 +1,3 @@ # 174BG Discord Bot This repository contains the source code for the official Discord bot for the 174th Battle Group community. The bot is written in JavaScript using the [discord.js](https://discord.js.org) library. - -## Features - -- `/create-requisition-ticket` — Creates a private ticket channel under the **tickets** category so a member can submit a request to the Quartermaster role. -- `/delete-all-requisition-tickets` — Bulk-deletes all open requisition tickets. Restricted to administrators. -- Logs ticket activity (creation and closure) to a **logs** channel. - -## Setup - -1. Install dependencies: - - ```sh - npm install - ``` - -2. Copy `.env.example` to `.env` and fill in the values: - - ``` - DISCORD_CLIENT_ID= - DISCORD_BOT_TOKEN= - ``` - -3. Run the bot: - - ```sh - npm start - ``` - - During development, use `npm run dev` (or `npm run dev:watch` to restart on file changes) to load the `.env` file automatically. - -## Deployment - -A `Dockerfile` is provided to build and run the bot in a container: - -```sh -docker build -t 174bg-discord-bot . -docker run --env-file .env 174bg-discord-bot -``` diff --git a/174bg/discord-bot/commands/_index.js b/174bg/discord-bot/commands/_index.js index 77452ff..2421b4d 100644 --- a/174bg/discord-bot/commands/_index.js +++ b/174bg/discord-bot/commands/_index.js @@ -1,8 +1,7 @@ import requisition from "./requisition.js"; import closeTicket from "./close-ticket.js"; import oncallCheck from "./oncall-check.js"; -import pocketbase from "./pocketbase.js"; -export { requisition, closeTicket, oncallCheck, pocketbase }; +export { requisition, closeTicket, oncallCheck }; -export default [requisition, closeTicket, oncallCheck, pocketbase]; +export default [requisition, closeTicket, oncallCheck]; diff --git a/174bg/discord-bot/commands/pocketbase.js b/174bg/discord-bot/commands/pocketbase.js deleted file mode 100644 index 5724568..0000000 --- a/174bg/discord-bot/commands/pocketbase.js +++ /dev/null @@ -1,395 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js"; -import { login } from "../controllers/pocketbase.js"; -import uex from "../.cache/uex.json" with { type: "json" }; - -export default { - data: new SlashCommandBuilder() - .setName("pocketbase") - .setDescription("Runs various PocketBase commands.") - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommandGroup((group) => - group - .setName("populate") - .setDescription("Populates the PocketBase database with data.") - .addSubcommand((subcommand) => - subcommand - .setName("uex_items") - .setDescription("Populates the uex_items collection with data from the bot's UEX cache.") - ) - .addSubcommand((subcommand) => - subcommand - .setName("uex_space_stations") - .setDescription("Populates the uex_space_stations collection with data from the bot's UEX cache.") - ) - .addSubcommand((subcommand) => - subcommand - .setName("uex_points_of_interest") - .setDescription("Populates the uex_points_of_interest collection with data from the bot's UEX cache.") - ) - .addSubcommand((subcommand) => - subcommand - .setName("inventory_locations") - .setDescription("Populates the inventory_locations collection with data from the bot's UEX cache.") - ) - .addSubcommand((subcommand) => - subcommand - .setName("uex_vehicles") - .setDescription("Populates the uex_vehicles collection with data from the bot's UEX cache.") - ) - ), - execute: async (interaction) => { - // only zuedev can run any of these commands - if (interaction.member.id !== "723361818940276736") return interaction.reply({ - content: "You do not have permission to use this command.", - ephemeral: true, - }); - - const subcommandGroup = interaction.options.getSubcommandGroup(); - const subcommand = interaction.options.getSubcommand(); - - if (subcommandGroup === "populate") { - switch (subcommand) { - case "uex_items": - populateUexItems(interaction); - break; - case "uex_space_stations": - populateUexSpaceStations(interaction); - break; - case "uex_points_of_interest": - populateUexPointsOfInterest(interaction); - break; - case "inventory_locations": - populateInventoryLocations(interaction); - break; - case "uex_vehicles": - populateUexVehicles(interaction); - break; - } - } - }, -}; - -async function populateUexItems(interaction) { - const pb = await login(); - - const items = uex.items ?? []; - - interaction.reply({ - content: `Populating uex_items collection with ${items.length} items...`, - ephemeral: true, - }); - - let ops = 0; - let failed = 0; - let updates = 0; - let creates = 0; - - for (const item of items) { - try { - // does it already exist? - const existingItem = await pb.collection("uex_items").getFullList({ - filter: `id="${item.id}"`, - }); - - if (existingItem.length > 0) { - // compare times to see if we need to update - // example pocketbase: "2026-07-18 12:30:02.405Z" - // example uex: 1746219762 - if (new Date(existingItem[0].updated).getTime() < item.updated * 1000) { - await pb.collection("uex_items").update(existingItem[0].id, { - name: item.name, - }); - updates++; - } - } else { - await pb.collection("uex_items").create({ - id: item.id, - name: item.name, - }); - creates++; - } - ops++; - - // update the interaction every 1000 operations to avoid timeout - if (ops % 1000 === 0) { - interaction.editReply({ - content: `Populating uex_items collection with ${items.length} items... (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); - } - } catch (error) { - console.error(`Failed to create item ${item.name}:`, error); - failed++; - } - } - - interaction.editReply({ - content: `Successfully populated uex_items collection with ${items.length} items. (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); -} - -async function populateUexSpaceStations(interaction) { - const pb = await login(); - - const stations = uex.space_stations ?? []; - - interaction.reply({ - content: `Populating uex_space_stations collection with ${stations.length} stations...`, - ephemeral: true, - }); - - let ops = 0; - let failed = 0; - let updates = 0; - let creates = 0; - - for (const station of stations) { - try { - // does it already exist? - const existingStation = await pb.collection("uex_space_stations").getFullList({ - filter: `id="${station.id}"`, - }); - - if (existingStation.length > 0) { - // compare times to see if we need to update - // example pocketbase: "2026-07-18 12:30:02.405Z" - // example uex: 1746219762 - if (new Date(existingStation[0].updated).getTime() < station.updated * 1000) { - await pb.collection("uex_space_stations").update(existingStation[0].id, { - name: station.name, - }); - updates++; - } - } else { - await pb.collection("uex_space_stations").create({ - id: station.id, - name: station.name, - }); - creates++; - } - ops++; - - // update the interaction every 1000 operations to avoid timeout - if (ops % 1000 === 0) { - interaction.editReply({ - content: `Populating uex_space_stations collection with ${stations.length} stations... (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); - } - } catch (error) { - console.error(`Failed to create space station ${station.name}:`, error); - failed++; - } - } - - interaction.editReply({ - content: `Successfully populated uex_space_stations collection with ${stations.length} stations. (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); -} - -async function populateUexPointsOfInterest(interaction) { - const pb = await login(); - - const pointsOfInterest = uex.points_of_interest ?? []; - - interaction.reply({ - content: `Populating uex_points_of_interest collection with ${pointsOfInterest.length} points of interest...`, - ephemeral: true, - }); - - let ops = 0; - let failed = 0; - let updates = 0; - let creates = 0; - - for (const point of pointsOfInterest) { - try { - // does it already exist? - const existingPoint = await pb.collection("uex_points_of_interest").getFullList({ - filter: `name="${point.name}"`, - }); - - if (existingPoint.length > 0) { - await pb.collection("uex_points_of_interest").update(existingPoint[0].id, { - name: point.name, - }); - updates++; - } else { - await pb.collection("uex_points_of_interest").create({ - id: point.id, - name: point.name, - }); - creates++; - } - ops++; - - // update the interaction every 1000 operations to avoid timeout - if (ops % 1000 === 0) { - interaction.editReply({ - content: `Populating uex_points_of_interest collection with ${pointsOfInterest.length} points of interest... (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); - } - } catch (error) { - console.error(`Failed to create point of interest ${point.name}:`, error); - failed++; - } - } - - interaction.editReply({ - content: `Successfully populated uex_points_of_interest collection with ${pointsOfInterest.length} points of interest. (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); -} - -async function populateInventoryLocations(interaction) { - const pb = await login(); - - const stations = uex.space_stations ?? []; - const pointsOfInterest = uex.points_of_interest ?? []; - - // merge + dedupe by name (case-insensitive) - const locationMap = new Map(); - - for (const station of stations) { - if (!station?.name) continue; - const key = station.name.trim().toLowerCase(); - locationMap.set(key, { - id: station.id, - name: station.name, - updated: station.updated ?? 0, - }); - } - - for (const point of pointsOfInterest) { - if (!point?.name) continue; - const key = point.name.trim().toLowerCase(); - const existing = locationMap.get(key); - - // keep whichever has the newest timestamp when duplicates exist - if (!existing || (point.updated ?? 0) > (existing.updated ?? 0)) { - locationMap.set(key, { - id: point.id, - name: point.name, - updated: point.updated ?? 0, - }); - } - } - - const locations = Array.from(locationMap.values()); - - interaction.reply({ - content: `Populating inventory_locations collection with ${locations.length} locations...`, - ephemeral: true, - }); - - let ops = 0; - let failed = 0; - let updates = 0; - let creates = 0; - - for (const location of locations) { - try { - const safeName = String(location.name).replaceAll('"', '\\"'); - - const existingLocation = await pb.collection("inventory_locations").getFullList({ - filter: `name="${safeName}"`, - }); - - if (existingLocation.length > 0) { - const pbUpdated = new Date(existingLocation[0].updated).getTime(); - const uexUpdated = (location.updated ?? 0) * 1000; - - if (pbUpdated < uexUpdated) { - await pb.collection("inventory_locations").update(existingLocation[0].name, { - name: location.name, - }); - updates++; - } - } else { - await pb.collection("inventory_locations").create({ - name: location.name, - }); - creates++; - } - - ops++; - - if (ops % 1000 === 0) { - interaction.editReply({ - content: `Populating inventory_locations collection with ${locations.length} locations... (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); - } - } catch (error) { - console.error(`Failed to create inventory location ${location?.name}:`, error); - failed++; - } - } - - interaction.editReply({ - content: `Successfully populated inventory_locations collection with ${locations.length} locations. (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); -} - -async function populateUexVehicles(interaction) { - const pb = await login(); - - const vehicles = uex.vehicles ?? []; - - interaction.reply({ - content: `Populating uex_vehicles collection with ${vehicles.length} vehicles...`, - ephemeral: true, - }); - - let ops = 0; - let failed = 0; - let updates = 0; - let creates = 0; - - for (const vehicle of vehicles) { - try { - const existingVehicle = await pb.collection("uex_vehicles").getFullList({ - filter: `id="${vehicle.id}"`, - }); - - if (existingVehicle.length > 0) { - const pbUpdated = new Date(existingVehicle[0].updated).getTime(); - const uexUpdated = (vehicle.updated ?? 0) * 1000; - - if (pbUpdated < uexUpdated) { - await pb.collection("uex_vehicles").update(existingVehicle[0].id, { - name: vehicle.name, - }); - updates++; - } - } else { - await pb.collection("uex_vehicles").create({ - id: vehicle.id, - name: vehicle.name, - }); - creates++; - } - - ops++; - - if (ops % 1000 === 0) { - interaction.editReply({ - content: `Populating uex_vehicles collection with ${vehicles.length} vehicles... (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); - } - } catch (error) { - console.error(`Failed to create vehicle ${vehicle?.name}:`, error); - failed++; - } - } - - interaction.editReply({ - content: `Successfully populated uex_vehicles collection with ${vehicles.length} vehicles. (${ops} processed, ${updates} updated, ${creates} created, ${failed} failed)`, - ephemeral: true, - }); -}
\ No newline at end of file diff --git a/174bg/discord-bot/controllers/http.js b/174bg/discord-bot/controllers/http.js new file mode 100644 index 0000000..816b73c --- /dev/null +++ b/174bg/discord-bot/controllers/http.js @@ -0,0 +1,25 @@ +/** + * Controller module for interacting with HTTP resources, mainly for fetching JSON data from external APIs. + */ + +/** + * Fetches data from a specified URL and returns the response as JSON or text. + * @param {string} url - The URL to fetch data from. + * @returns {Promise<Object|string|null>} A Promise that resolves to the parsed JSON object, text, or null if the request fails. + */ +export async function get(url) { + const response = await fetch(url); + + if (!response.ok) return null; + + // is it json? + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + return await response.json(); + } + + // fallback to text + return await response.text(); +}; + +return { get };
\ No newline at end of file diff --git a/174bg/discord-bot/controllers/pocketbase.js b/174bg/discord-bot/controllers/pocketbase.js index b808ce4..4046391 100644 --- a/174bg/discord-bot/controllers/pocketbase.js +++ b/174bg/discord-bot/controllers/pocketbase.js @@ -1,17 +1,35 @@ +/** + * Controller module for interacting with the PocketBase API. + */ + import PocketBase from "pocketbase"; const { POCKETBASE_URL, POCKETBASE_SUPERUSER_EMAIL, POCKETBASE_SUPERUSER_PASSWORD } = process.env; +/** + * Logs in to the PocketBase API using the superuser credentials. + * @returns {Promise<PocketBase>} A Promise that resolves to the authenticated PocketBase instance. + */ export async function login() { const pb = new PocketBase(POCKETBASE_URL); await pb.collection("_superusers").authWithPassword(POCKETBASE_SUPERUSER_EMAIL, POCKETBASE_SUPERUSER_PASSWORD); return pb; } +/** + * Logs out of the PocketBase API by clearing the authentication store. + * @param {PocketBase} pb - The PocketBase instance to log out from. + * @returns {Promise<void>} A Promise that resolves when the logout is complete. + */ export async function logout(pb) { await pb.authStore.clear(); } +/** + * Retrieves the full list of records from a specified collection in the PocketBase API. + * @param {string} collectionName - The name of the collection to retrieve records from. + * @returns {Promise<RecordModel[]>} A Promise that resolves to an array of records from the specified collection. + */ export async function getFullList(collectionName) { const pb = await login(); const data = await pb.collection(collectionName).getFullList(); diff --git a/174bg/discord-bot/main.js b/174bg/discord-bot/main.js index 8a83bd9..b0de475 100644 --- a/174bg/discord-bot/main.js +++ b/174bg/discord-bot/main.js @@ -1,4 +1,3 @@ -import fs from "fs"; import { Client, Events, @@ -12,7 +11,6 @@ import { ButtonBuilder, ButtonStyle, } from "discord.js"; -import getUrl from "./utilities/getUrl.js"; import commands from "./commands/_index.js"; const client = new Client({ @@ -23,8 +21,6 @@ client.on(Events.ClientReady, async (readyClient) => { console.log(`Logged in as ${readyClient.user.tag}!`); await registerCommands(); - - await buildCache(); }); client.on(Events.InteractionCreate, async (interaction) => { @@ -79,62 +75,4 @@ async function registerCommands() { } catch (error) { console.error(error); } -} - -async function buildCache() { - if (!fs.existsSync("./.cache")) fs.mkdirSync("./.cache"); - - const uex = {}; - - uex.categories = (await getUrl("https://api.uexcorp.uk/2.0/categories"))?.data; - - for (const category of uex.categories) { - uex.items = uex.items || []; - if (category.type === "item") { - uex.items = uex.items.concat((await getUrl(`https://api.uexcorp.uk/2.0/items?id_category=${category.id}`))?.data || []); - } - } - - uex.space_stations = (await getUrl("https://api.uexcorp.uk/2.0/space_stations"))?.data; - uex.cities = (await getUrl("https://api.uexcorp.uk/2.0/cities"))?.data; - uex.outposts = (await getUrl("https://api.uexcorp.uk/2.0/outposts"))?.data; - uex.points_of_interest = (await getUrl("https://api.uexcorp.uk/2.0/poi"))?.data; - uex.terminals = (await getUrl("https://api.uexcorp.uk/2.0/terminals"))?.data; - uex.vehicles = (await getUrl("https://api.uexcorp.uk/2.0/vehicles"))?.data; - uex.vehicles_pledge_prices = (await getUrl("https://api.uexcorp.uk/2.0/vehicles_prices"))?.data; - uex.vehicles_buy_prices = (await getUrl("https://api.uexcorp.uk/2.0/vehicles_purchases_prices"))?.data; - uex.vehicles_rent_prices = (await getUrl("https://api.uexcorp.uk/2.0/vehicles_rentals_prices"))?.data; - - // is there a difference between the current cache and the new cache? - if (fs.existsSync("./.cache/uex.json")) { - const currentCache = JSON.parse(fs.readFileSync("./.cache/uex.json", "utf-8")); - - if (JSON.stringify(currentCache) === JSON.stringify(uex)) { - console.log("No changes detected in UEX cache."); - console.log("If you want to force a cache update, delete the .cache/uex.json file and restart the bot."); - for (const [key, value] of Object.entries(uex)) { - console.log(`Found ${value.length} ${key.replace(/_/g, " ")}.`); - } - return; - } - - console.log("Changes detected in UEX cache. Updating cache."); - - // backup the current cache YYMMDDHHMMSS - fs.writeFileSync( - `./.cache/uex.json.bak.${new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "")}`, - JSON.stringify(currentCache, null, 2), - ); - } else { - console.log("No existing UEX cache found. Creating new cache."); - } - - await fs.writeFileSync( - "./.cache/uex.json", - JSON.stringify(uex, null, 2), - ); - - for (const [key, value] of Object.entries(uex)) { - console.log(`Cached ${value.length} ${key.replace(/_/g, " ")}.`); - } }
\ No newline at end of file diff --git a/174bg/discord-bot/utilities/getUrl.js b/174bg/discord-bot/utilities/getUrl.js deleted file mode 100644 index a6e22ef..0000000 --- a/174bg/discord-bot/utilities/getUrl.js +++ /dev/null @@ -1,14 +0,0 @@ -export default async (url) => { - const response = await fetch(url); - - if (!response.ok) return null; - - // is it json? - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - return await response.json(); - } - - // fallback to text - return await response.text(); -}; |
