aboutsummaryrefslogtreecommitdiff
path: root/174bg
diff options
context:
space:
mode:
authorAlex Pooley (@zuedev) <zuedev@gmail.com>2026-07-21 23:02:52 +0100
committerAlex Pooley (@zuedev) <zuedev@gmail.com>2026-07-21 23:02:52 +0100
commitda0509c91376f56f133872eddd2e4282d76f215e (patch)
tree7d39fb76ab89855d17fab28ca7e000b3acd9a89d /174bg
parent764f90577eeadd653f3f6248fbf2b0918c97a3d5 (diff)
downloadunnamed-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')
-rw-r--r--174bg/discord-bot/README.md38
-rw-r--r--174bg/discord-bot/commands/_index.js5
-rw-r--r--174bg/discord-bot/commands/pocketbase.js395
-rw-r--r--174bg/discord-bot/controllers/http.js25
-rw-r--r--174bg/discord-bot/controllers/pocketbase.js18
-rw-r--r--174bg/discord-bot/main.js62
-rw-r--r--174bg/discord-bot/utilities/getUrl.js14
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();
-};