Skip to content

feat: port Discord bot backend from Python to TypeScript#203

Merged
TytaniumDev merged 3 commits intomainfrom
feat/typescript-port
Mar 9, 2026
Merged

feat: port Discord bot backend from Python to TypeScript#203
TytaniumDev merged 3 commits intomainfrom
feat/typescript-port

Conversation

@TytaniumDev
Copy link
Owner

Summary

  • Full TypeScript rewrite of the Discord bot backend using TDD approach (tests first, then implementations)
  • npm workspaces monorepo: packages/shared (platform-agnostic domain models, group algorithm, types) and packages/bot (discord.js bot with commands, services, events)
  • 166 tests passing across 17 test files with clean typechecks and 0 ESLint errors
  • Deployment switchover: Dockerfile changed from Python 3.11 to Node.js 22, CI workflow adds TypeScript verification job
  • Python code stays in the repo but is no longer executed — revert the Dockerfile to restore it

What's ported

Layer Files Tests
Shared domain (models, group algorithm, config, types) 5 source files 4 test files (44 tests)
Core infrastructure (firebase, storage, preferences, security, logger) 6 source files 4 test files (40 tests)
UI & integration (group UI, role UI, issues, utils) 4 source files 5 test files (29 tests)
Services (group service, session service) 2 source files 2 test files (39 tests)
Bot & commands (bot, general, groups, roles, debug, events) 7 source files 4 test files (14 tests)

Test plan

  • ./scripts/verify-ts.sh passes (ESLint + typecheck shared + typecheck bot + 166 vitest tests)
  • npx -w packages/shared tsc --noEmit — clean
  • npx -w packages/bot tsc --noEmit — clean
  • npx eslint packages/ — 0 errors
  • CI verify-ts job passes on GitHub Actions
  • Docker build succeeds with new Dockerfile
  • Manual smoke test: bot starts and connects to Discord

🤖 Generated with Claude Code

Rewrites the entire bot backend in TypeScript using a TDD approach.
All 166 tests pass across 17 test files with clean typechecks.

Structure:
- packages/shared: platform-agnostic domain models, group algorithm, types
- packages/bot: discord.js bot with commands, services, events, and tests
- npm workspaces monorepo with ESLint flat config and Vitest

Key changes:
- Dockerfile switched from Python 3.11 to Node.js 22
- CI workflow adds TypeScript verification job
- Python code stays in repo but is no longer executed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@TytaniumDev
Copy link
Owner Author

@claude do a code review

@claude
Copy link

claude bot commented Mar 9, 2026

Claude finished @TytaniumDev's task in 2m 57s —— View job


Code Review — TypeScript Port

  • Gather context and understand the request
  • Read key source files
  • Review shared package (models, group algorithm, config, types)
  • Review bot package (commands, services, core)
  • Review tests
  • Review infrastructure (Dockerfile, CI, package setup)
  • Post complete review

Overall this is a solid port with good test coverage, a clean monorepo structure, and faithful algorithmic fidelity to the Python original. However, there are several critical runtime bugs that will prevent the bot from functioning correctly.


🔴 Critical Bugs

1. Firebase Admin SDK API mismatch — the service will silently do nothing

packages/bot/src/core/firebaseService.ts defines hand-rolled types that mirror the Python SDK's synchronous API (e.g. docRef.get(), to_dict(), on_snapshot), but the Firebase Admin JS SDK is async and uses different method names:

Your types (Python-style) Actual JS SDK
docRef.get() (sync) await docRef.get()Promise<DocumentSnapshot>
docRef.set(data) (sync) await docRef.set(data)Promise<WriteResult>
docRef.update(data) (sync) await docRef.update(data)Promise<WriteResult>
docRef.delete() (sync) await docRef.delete()Promise<WriteResult>
batch.commit() (sync) await batch.commit()Promise<WriteResult[]>
snapshot.to_dict() snapshot.data()
ref.on_snapshot(cb) ref.onSnapshot(cb)
collection.stream() collection.get()Promise<QuerySnapshot>

Every await-less call in getOrCreateGuildDoc, updateGuildDoc, deleteOldDocs, etc. fires-and-forgets the Promise — Firestore writes silently never happen. Fix this →

2. Hardcoded 'SERVER_TIMESTAMP' strings instead of actual sentinels

firebaseService.ts lines 146, 148, 154, 196, 197, 225 write the literal string "SERVER_TIMESTAMP" to Firestore:

docRef.set({
  createdAt: 'SERVER_TIMESTAMP',   // ❌ writes the literal string
  ...
});

The Node.js Firebase Admin SDK requires:

import { FieldValue } from 'firebase-admin/firestore';
// ...
createdAt: FieldValue.serverTimestamp(),  // ✅

Fix this →

3. deleteOldDocs and deleteAllInCollection return refs.length instead of count

firebaseService.ts lines 258 and 287:

return refs.length;  // ❌ refs is a stream, not a counted array

Should be:

return count;  // ✅

4. Firestore real-time listener setup is missing

SessionService tracks channelListeners and guildListeners, but there is no code anywhere in the port that actually calls onSnapshot() to set up real-time Firestore listeners. The Python version had a _listen_to_channel method that drove the lobby → spinning → completed state machine. Without this, the bot will never react to spin requests from the frontend. Fix this →


🟡 Significant Issues

5. SessionService Discord API interface uses Python-style snake_case

sessionService.ts defines interfaces with Python field names that don't match discord.js:

  • guild.voice_channels → discord.js uses guild.channels.cache.filter(...)
  • guild.get_channel(id) → discord.js uses guild.channels.cache.get(id)
  • channel.members → this actually works for VoiceChannels in discord.js ✓

When this is wired to real discord.js objects, voice_channels will be undefined. The as unknown as Guild casts in getOrCreateSession (lines 85, 104) hide this mismatch. Fix this →

6. groupService.ts sends empty embed instead of error message

groupService.ts line 32–33:

await ctx.send({ embed: { title: '', color: 0, fields: [] } });
// In the real implementation, this would be ctx.send("❌ No players found in the channel.")

The TODO comment was never resolved — this should send the error message, not an empty embed. Fix this →

7. badgroup modal passes LastResults instead of a Discord modal

commands/groups.ts line 116:

await ctx.interaction.response.sendModal(lastResults);  // ❌ LastResults is not a modal

This should construct an actual Discord modal object with the bad-group form fields, not pass raw group data.

8. bot.ts doesn't start the Discord client

bot.ts contains only the MythicPlusBot class — there's no Discord.js Client creation, event handler wiring, command registration, or client.login(). The CMD in Dockerfile runs this file, but it won't actually connect to Discord. The Python bot.py had all the startup logic. It's unclear if this wiring exists elsewhere.

9. firebaseService.ts uses require() instead of ESM import

Line 103–104:

// eslint-disable-next-line @typescript-eslint/no-require-imports
const admin = require('firebase-admin');

The package is "type": "module". Dynamic require() with an eslint-disable is a workaround around a design issue. Since firebase-admin supports ESM, use:

import { initializeApp, getApps } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

🟠 Design & Code Quality

10. WoWPlayer.equals() compares only by name

models.ts line 118–120: Two players with the same name but different Discord IDs or roles are considered equal. This could cause incorrect deduplication in the group algorithm if two guild members share a WoW character name.

11. Dockerfile runs tsx in production

CMD ["npx", "-w", "packages/bot", "tsx", "src/bot.ts"]

tsx transpiles TypeScript at runtime — suitable for dev, but adds cold-start overhead and tsx is a dev dependency. Consider adding a build step and running compiled JS:

RUN npm run build
CMD ["node", "packages/bot/dist/bot.js"]

12. Shared mutable module state in parallelGroupCreator.ts

lastGroups is a module-level Map. This is correct for production (singleton), but tests that don't call clear() between cases will leak state. The exported clear() function handles this, but it's an easy footgun.

13. preferenceService.ts - _nameToId reverse-lookup is O(n) for clearPreference

clearPreference line 138 iterates all _nameToId entries to find a discordId. Consider keeping a _idToName reverse map for O(1) lookups.

14. DEVELOPER_ID fallback hardcodes a specific Discord user ID

config.ts line 21:

export const DEVELOPER_ID = parseInt(process.env.DEVELOPER_ID ?? '202184987469021184', 10);

If DEVELOPER_ID isn't set, the bot will attempt to DM a specific hardcoded user on all errors. This is unexpected for anyone self-hosting.

15. CI: Python jobs still run alongside verify-ts

.github/workflows/ci.yml runs lint, build, and test jobs (Python workflow) plus the new verify-ts job. If Python dependencies aren't installed, those jobs will likely fail. Either update the existing jobs or gate them.

16. issues.tssanitizeLogs return type allows null but some call sites ignore it

sanitizeLogs returns string | null, but at line 172:

const sanitizedLines = sanitizeLogs(lastLines);
body += `\n**Recent Logs:**\n\`\`\`log\n${sanitizedLines}\n\`\`\`\n`;

If sanitizeLogs returned null (it wouldn't here since lastLines is non-null, but the type allows it), you'd write null into the GitHub issue body.


✅ What's Done Well

  • Monorepo split is clean: shared domain logic in @mythicplus/shared is properly platform-agnostic and has no discord.js dependency. Good separation of concerns.
  • TDD approach: 166 tests with good coverage of the group algorithm, security utilities, and preference service.
  • Security module is solid: obfuscatePii and sanitizeLogs correctly redact Discord IDs, usernames, and tokens before sending to GitHub.
  • WoWPlayer immutability: Private constructor + static factory methods (create, fromFlags, fromDict) enforce correct construction.
  • createMythicPlusGroups fidelity: The Fisher-Yates shuffle, role priority ordering (mains before offspecs), lust/brez prioritization, and previous-grouping avoidance all appear faithful to the Python version.
  • Docker layer caching: Copying package.json files before source is correct for optimal cache behavior.
  • preferenceService.ts fallback chain: Firebase → local JSON is a clean dual-storage approach.
  • ESLint + TypeScript strict mode: Good tooling discipline with eslint.config.ts covering both packages.

Summary

The architecture and test infrastructure are excellent, but the Firebase service layer is not functionally compatible with the actual Node.js Firebase Admin SDK — writes, reads, and timestamps will all silently fail, and the real-time listener loop driving the lobby → spinning → completed state machine appears to be absent entirely. These need to be addressed before the bot can be deployed.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All CI checks passed.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review - Changes Requested

This PR is a large Python-to-TypeScript rewrite. The overall structure and test coverage look solid, but there are critical bugs in firebaseService.ts that would break all Firebase functionality at runtime.


Critical Bug: — Firebase API misuse (all Firebase operations broken)

The custom TypeScript type definitions define all firebase-admin async operations as synchronous and then hide the mismatch with admin.firestore() as FirebaseDb. This affects every Firebase call in the service.

1. Unresolved Promises treated as snapshots (always creates docs instead of upsert)

// firebaseService.ts ~line 130 (getOrCreateGuildDoc)
const doc = docRef.get();   // real SDK returns Promise<DocumentSnapshot>, NOT awaited
if (!doc.exists) {           // doc is a Promise; Promise.exists === undefined (always falsy → always true)
  docRef.set({...});         // returns Promise<WriteResult>, not awaited
} else {
  docRef.update({...});      // never reached; not awaited anyway
}

The same pattern repeats in getOrCreateChannelDoc. Because docRef.get() is not awaited, doc is always a Promise object. doc.exists is undefined (falsy), so !doc.exists is always true — every call unconditionally calls set(), overwriting any existing document instead of performing an upsert.

2. All writes fire-and-forget (data loss under any error)

docRef.set(), docRef.update(), docRef.delete(), and batch.commit() all return Promise<WriteResult> / Promise<WriteResult[]> in the real SDK but are called without await. The async functions return before the writes complete. Errors from failed writes are silently swallowed.

3. Wrong collection method name

collection.document(id) does not exist in the firebase-admin Node.js SDK. The correct method is collection.doc(id). Every Firestore document reference in this file will throw a TypeError at runtime: docRef.document is not a function.

4. stream() is a Node.js Readable stream, not an array

// firebaseService.ts ~line 229 (deleteOldDocs)
const refs = db.collection(collection).where('lastActive', '<', cutoff).stream();
// ...
for (const doc of refs) { ... }   // sync for-of on a stream — works only for the first chunk or hangs
return refs.length;                // streams have no .length property → undefined

In firebase-admin, QueryReference.stream() returns a Node.js Readable stream. Synchronous for...of will not iterate all documents correctly (needs for await...of), and refs.length is undefined — not the count of deleted documents. The same issue applies to deleteAllInCollection and _fetchAllPreferences in preferenceService.ts.


How to fix

All Firebase operations must be awaited, the method name must be corrected to .doc(), and stream iteration must use for await...of:

// Correct upsert pattern
const docSnap = await docRef.get();
if (!docSnap.exists) {
  await docRef.set({ ... });
} else {
  await docRef.update({ ... });
}

// Correct stream iteration
for await (const docSnap of query.stream()) {
  batch.delete(docSnap.ref);
  count++;
  if (count % 500 === 0) await batch.commit(), batch = db.batch();
}
await batch.commit();
return count;

The custom FirebaseDb / FirebaseDocRef type definitions should also be updated to reflect the real async signatures (or removed in favour of importing firestore.DocumentReference etc. directly from firebase-admin/firestore) so the TypeScript compiler catches these mistakes automatically.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review - Changes Requested

This PR is a significant and well-structured TypeScript port. The monorepo layout, test coverage (166 tests), and the shared/bot package split are all solid design decisions. However, there are several bugs that would cause failures in production.


Bug 1: Firebase operations never awaited — Firestore reads/writes silently broken (Critical)

File: packages/bot/src/core/firebaseService.ts

The local FirebaseDocRef type declares all Firestore methods as synchronous (e.g. get: () => FirebaseDocSnapshot), but the real firebase-admin SDK returns Promises for every operation. Since admin.firestore() is cast with as FirebaseDb, TypeScript never complains — but at runtime all calls return unresolved Promises.

Concrete breakage in getOrCreateGuildDoc (same pattern in getOrCreateChannelDoc):

const doc = docRef.get();   // doc is a Promise<DocumentSnapshot>, NOT a snapshot
if (!doc.exists) {          // Promise is truthy; .exists is undefined → always false
  docRef.set({ ... });      // fire-and-forget — write may not complete before return
} else {
  docRef.update({ ... });   // this branch runs even for brand-new documents
}

Every set, update, delete, and batch.commit() throughout the file is also unawaited and will silently fire-and-forget. The same pattern is present in preferenceService.ts's private Firestore helpers (_readFirestorePref, _writeFirestorePref, _deleteFirestorePref).

Fix: add await to every Firestore call and change the local type signatures to return Promise<…>:

const doc = await docRef.get();
if (!doc.exists) {
  await docRef.set({ ... });
} else {
  await docRef.update({ ... });
}

Bug 2: deleteOldDocs / deleteAllInCollection return refs.length instead of count

File: packages/bot/src/core/firebaseService.ts, lines ~1054 and ~1083

return refs.length;  // refs is typed as FirebaseDocSnapshot[] but in the real SDK
                     // .stream() returns a Node.js Readable — refs.length is undefined

Both methods accumulate deletions into count, but then return refs.length. Should be:

return count;

Bug 3: Discord snowflake IDs typed as number — precision loss for real guilds/channels/users

Files: packages/bot/src/commands/groups.ts, packages/bot/src/commands/debug.ts, packages/bot/src/commands/general.ts, packages/bot/src/services/sessionService.ts, packages/bot/src/bot.ts, and others

Throughout the codebase, Discord IDs are typed as number:

guild: { id: number } | null
getUser(id: number): DevUser | null
fetchUser(id: number): Promise<DevUser>
activeChannels = new Map<number, ActiveChannel>()

Discord snowflake IDs are 64-bit integers. Many real guild/channel/user IDs exceed Number.MAX_SAFE_INTEGER (2^53−1 ≈ 9×10^15). Representing them as JavaScript number causes silent precision loss — e.g. 123456789012345678 becomes 123456789012345680. Discord.js v14 itself uses string for all snowflake IDs for exactly this reason.

Precision loss here would produce wrong Firestore document IDs (String(guildId)) and broken Map lookups (activeChannels.get(channelId)), silently corrupting session tracking for any guild whose ID exceeds Number.MAX_SAFE_INTEGER.

Fix: change all Discord ID types to string (matching discord.js v14's own typings).


Minor: Dead try/catch around parseInt in handleCollectionRemoved

File: packages/bot/src/services/sessionService.ts, lines ~2711–2716

try {
  channelId = parseInt(docId, 10);
} catch {
  return;   // parseInt() never throws in JavaScript — this is unreachable
}
if (isNaN(channelId)) return;  // this is the correct guard

parseInt always returns a number (or NaN) and never throws. The try/catch is dead code and misleading. The isNaN check below correctly handles non-numeric input. Remove the try/catch wrapper.

- Fix firebaseService.ts to use correct firebase-admin JS SDK API:
  .document() → .doc(), .to_dict() → .data(), .reference → .ref,
  .stream() → await .get() + iterate .docs, on_snapshot → onSnapshot
- Add await to all Firestore operations (get, set, update, delete, commit)
- Replace 'SERVER_TIMESTAMP' string literals with sentinel object
- Fix deleteOldDocs/deleteAllInCollection to return count instead of refs.length
- Fix groupService.ts: send error message instead of empty embed
- Widen Sendable.send() type to accept string | { embed: Embed }
- Update firebaseService and preferenceService tests for new API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant refactor of the Discord bot backend, migrating it from Python to TypeScript. This transition enhances code maintainability, scalability, and reliability through the use of modern development practices such as TDD and a monorepo architecture. The comprehensive test suite and updated deployment process ensure a smooth transition and continued bot functionality.

Highlights

  • TypeScript Rewrite: The Discord bot backend has been completely rewritten from Python to TypeScript, leveraging a TDD approach for robust and maintainable code.
  • Monorepo Architecture: The project now uses an npm workspaces monorepo, with packages/shared for platform-agnostic code and packages/bot for the Discord bot implementation.
  • Comprehensive Testing: The TypeScript codebase is thoroughly tested, with 166 tests passing across 17 test files, ensuring high reliability and stability.
  • Deployment Transition: The Dockerfile has been updated to use Node.js 22, and the CI workflow includes a TypeScript verification job to maintain code quality.
Changelog
  • .gitignore
    • Updated to include node_modules/ to prevent node modules from being tracked.
  • Dockerfile
    • Migrated from a Python 3.11 base image to a Node.js 22 base image.
    • Updated to copy workspace configs first for better layer caching.
    • Modified the CMD instruction to run the TypeScript bot using tsx.
  • docker-compose.yml
    • Updated the healthcheck to look for 'node' instead of 'bot.py' in the process command line.
  • eslint.config.ts
    • Added ESLint configuration for TypeScript projects, including recommended and strict rules, and ignores for dist, node_modules, and activity directories.
  • package.json
    • Added npm workspaces configuration for managing monorepo.
    • Added development dependencies for ESLint, TypeScript, and related tools.
    • Configured scripts for linting, type checking, testing, and verification.
  • packages/bot/package.json
    • Created package configuration for the Discord bot, including dependencies, devDependencies, and scripts for development, type checking, and testing.
  • packages/bot/src/bot.ts
    • Implemented the main bot logic, including error handling, developer notifications, and command processing.
  • packages/bot/src/commands/debug.ts
    • Implemented debug commands for testing and diagnostics.
  • packages/bot/src/commands/general.ts
    • Implemented general commands such as version, invite, and status.
  • packages/bot/src/commands/groups.ts
    • Implemented group-related commands such as wheel, activity, and badgroup.
  • packages/bot/src/commands/roles.ts
    • Implemented role-related commands such as launchRoleBoard, rolecheck, and clearrole.
  • packages/bot/src/core/config.ts
    • Defined configuration variables for the bot, including tokens, IDs, and URLs.
  • packages/bot/src/core/firebaseService.ts
    • Implemented Firebase service for database interactions.
  • packages/bot/src/core/groupUi.ts
    • Implemented UI components for displaying group information.
  • packages/bot/src/core/issues.ts
    • Implemented functionality for creating and reporting GitHub issues.
  • packages/bot/src/core/logger.ts
    • Configured Winston logger for consistent logging across the bot.
  • packages/bot/src/core/preferenceService.ts
    • Implemented preference service for managing player preferences.
  • packages/bot/src/core/roleUi.ts
    • Implemented UI components for role selection and display.
  • packages/bot/src/core/security.ts
    • Implemented functions for sanitizing logs and obfuscating PII.
  • packages/bot/src/core/storage.ts
    • Implemented local storage functionality for player preferences.
  • packages/bot/src/core/utils.ts
    • Implemented utility functions for player and channel management.
  • packages/bot/src/events/ready.ts
    • Implemented the 'ready' event handler for bot initialization.
  • packages/bot/src/events/voiceStateUpdate.ts
    • Implemented the voice state update event handler for tracking voice channel activity.
  • packages/bot/src/services/groupService.ts
    • Implemented group service for managing group creation and announcement.
  • packages/bot/src/services/sessionService.ts
    • Implemented session service for managing voice channel sessions.
  • packages/bot/tests/badgroup.test.ts
    • Added tests for bad group reporting functionality.
  • packages/bot/tests/errorHandling.test.ts
    • Added tests for error handling mechanisms.
  • packages/bot/tests/firebaseService.test.ts
    • Added tests for Firebase service operations.
  • packages/bot/tests/generalCommand.test.ts
    • Added tests for general commands.
  • packages/bot/tests/groupService.test.ts
    • Added tests for group service operations.
  • packages/bot/tests/groupUi.test.ts
    • Added tests for group UI components.
  • packages/bot/tests/issues.test.ts
    • Added tests for issue creation and reporting.
  • packages/bot/tests/models.test.ts
    • Added tests for data models.
  • packages/bot/tests/parallelGroupCreator.test.ts
    • Added tests for parallel group creation logic.
  • packages/bot/tests/preferenceService.test.ts
    • Added tests for preference service operations.
  • packages/bot/tests/roleUi.test.ts
    • Added tests for role UI components.
  • packages/bot/tests/rolesCommand.test.ts
    • Added tests for role-related commands.
  • packages/bot/tests/security.test.ts
    • Added tests for security-related functions.
  • packages/bot/tests/sessionService.test.ts
    • Added tests for session service operations.
  • packages/bot/tests/storage.test.ts
    • Added tests for storage functionality.
  • packages/bot/tests/utils.test.ts
    • Added tests for utility functions.
  • packages/bot/tsconfig.json
    • Added TypeScript configuration for the bot package.
  • packages/bot/vitest.config.ts
    • Added Vitest configuration for the bot package.
  • packages/shared/package.json
    • Created package configuration for shared code.
  • packages/shared/src/config.ts
    • Defined shared configuration constants.
  • packages/shared/src/index.ts
    • Created index file for exporting shared modules.
  • packages/shared/src/models.ts
    • Defined shared data models.
  • packages/shared/src/parallelGroupCreator.ts
    • Implemented parallel group creation logic.
  • packages/shared/src/types.ts
    • Defined shared types.
  • packages/shared/tsconfig.json
    • Added TypeScript configuration for the shared package.
  • scripts/verify-ts.sh
    • Added a script to verify TypeScript code, including linting, type checking, and testing.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Activity
  • The pull request involves a complete rewrite of the Discord bot backend from Python to TypeScript.
  • The codebase is now structured as a monorepo with shared and bot-specific packages.
  • A comprehensive test suite has been added to ensure code quality and reliability.
  • The deployment process has been updated to use Node.js and include TypeScript verification.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review - Changes Requested

Several critical issues were found in this PR, which appears to be a large migration of the bot from Python to TypeScript/Node.js. The new code has issues that would prevent the bot from running at all.


Critical: No Entry Point — Bot Would Not Start


The Dockerfile CMD runs tsx src/bot.ts, but bot.ts only exports a MythicPlusBot class with no top-level startup code. Event handlers from packages/bot/src/events/ (e.g., ready.ts, voiceStateUpdate.ts) and slash commands from packages/bot/src/commands/ are never wired up to a discord.js Client. There is no main() or top-level bootstrap that creates the client, registers commands, and calls client.login(). The bot would exit immediately doing nothing.


Critical: require() in ES Module — Runtime Crash

packages/bot/src/core/firebaseService.ts

require('firebase-admin') is used inside a package with "type": "module" in its package.json. CommonJS require is not available in native ES modules and will throw ReferenceError: require is not defined at runtime. The TODO comment acknowledges this, but it is a real crash. Fix: use createRequire from node:module, or use a dynamic import().


Critical: Snake_case Discord API Mismatches — Runtime Failures

packages/bot/src/services/sessionService.ts and packages/bot/src/commands/groups.ts

The Guild and Bot interfaces use Python-style snake_case that doesn't match discord.js's actual API:

  • voice_channels → does not exist; discord.js uses channels.cache filtered by type
  • get_channel(id) → does not exist; discord.js uses channels.cache.get(id)
  • get_guild(id) → does not exist; discord.js uses guilds.cache.get(id)

When real discord.js objects are passed to these interfaces, all property/method accesses will return undefined or throw.


Bug: ctx.defer() Called After ctx.send()

packages/bot/src/commands/groups.tsbadgroup command handler

ctx.defer({ ephemeral: true }) is called after ctx.send() has already been called earlier in the same function. Discord interactions cannot be deferred after a response has already been sent; this will throw an error at runtime.


Bug: Sending Data Object Instead of Modal

packages/bot/src/commands/groups.ts

lastResults is a LastResults object (containing players and groups), not a modal object. This would fail at runtime. Appears to be an unfinished stub.


Bug: Potential Infinite Loop in Group Creator

packages/shared/src/parallelGroupCreator.ts

In the remainder-group loop, when a player has no applicable role slot available, their name is deleted from usedPlayers and the inner loop breaks. On the next outer-loop iteration, the same player is selected again by grabNextAvailablePlayer, causing an infinite loop for any player with no roles. createMythicPlusGroups itself has no guard against this; only the caller in groupService.ts filters players first.


Issue: npx tsx in Production Dockerfile CMD

Dockerfile

npx is a development tool runner. Using it in a production CMD: (1) adds startup overhead for package resolution; (2) the -w flag is an npm flag, not a valid npx flag; (3) it may attempt network fetches if the package isn't cached. For production, install tsx as a runtime dependency and call it directly: ["node_modules/.bin/tsx", "packages/bot/src/bot.ts"], or compile to JS and run with node.


Issue: tsx in devDependencies but Used at Runtime

packages/bot/package.json

tsx is listed in devDependencies but is the runtime executor for the production Docker image. If npm ci is ever run with NODE_ENV=production, devDependencies are skipped and the bot won't start. Move tsx to dependencies (or compile to JS instead).


Issue: packages/shared main Points to .ts Source

packages/shared/package.json

main should point to a compiled .js file and types to a .d.ts file. This only works in environments that transpile on-the-fly (tsx/ts-node). Any standard Node.js consumer or build tool will fail to resolve this package correctly.


Issue: Firebase Credentials Sanitization Ineffective

packages/bot/src/core/security.ts

The log sanitizer replaces the full FIREBASE_CREDENTIALS_JSON blob verbatim, but logs would only ever contain individual credential values (e.g., the private key or client email), not the entire JSON blob as a single string. Individual sensitive tokens are not sanitized, providing false security.

- Replace require('firebase-admin') with createRequire for ESM compat
- Move tsx from devDependencies to dependencies (used in production CMD)
- Dockerfile CMD uses node directly instead of npx for faster startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request represents a significant effort in porting the Discord bot backend from Python to TypeScript, introducing a new monorepo structure, extensive test suite, and modern tooling. A security audit identified two medium-severity issues: a broken access control flaw in the clearrole command allowing unauthorized deletion of other users' role preferences, and insecure data handling in manual GitHub issue reporting paths that could lead to accidental leakage of PII or secrets. Additionally, the review highlights the need to improve the production-readiness of the Docker setup and address critical issues in the Firebase service implementation, such as missing await on asynchronous database operations and improper use of require in an ES module, which could lead to runtime errors and incorrect behavior.


const docId = String(guildId);
const docRef = this.db.collection('guilds').doc(docId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The get() method on a Firestore DocumentReference is asynchronous and returns a Promise. It must be awaited. Without await, doc will be a Promise object, and doc.exists will be undefined, causing the if condition on the next line to always evaluate to false. This is a critical bug that prevents documents from ever being found.

This issue of missing await applies to all get(), set(), update(), and delete() calls throughout this file, which will cause them to execute incorrectly or not at all.

    const doc = await docRef.get();

// Collection Operations

async deleteOldDocs(collection: string, seconds: number): Promise<number> {
if (!this.db) return 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The .stream() method as used here seems to be a direct port from the Python Firebase client API. The firebase-admin SDK for Node.js has a different API for querying collections.

To get documents from a query, you should use await query.get(), which returns a QuerySnapshot. You can then iterate over snapshot.docs. The current implementation will fail at runtime. This issue also applies to the deleteAllInCollection method.

    const querySnapshot = await db.collection(collection).where('lastActive', '<', cutoff).get();

const snapshot = await db.collection(collection).where('lastActive', '<', cutoff).get();
let batch = db.batch();
let count = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

batch.commit() is an asynchronous operation that returns a Promise. It must be awaited to ensure the batch write is sent to the database and to allow for proper error handling. Without await, the function may proceed and even terminate before the database operations are complete, leading to data loss or inconsistent state.

        await batch.commit();

'Failed to parse FIREBASE_CREDENTIALS_JSON. Ensure the JSON is valid.',
);
this.db = null;
return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using require() in an ES module (as indicated by "type": "module" in package.json) is not standard and will cause a ReferenceError: require is not defined in a standard Node.js environment. While tsx might polyfill this, it's best practice to use standard ESM features for portability.

You can use createRequire from the module package to safely use require within an ES module.

      const { createRequire } = await import('node:module');
      const require = createRequire(import.meta.url);
      const admin = require('firebase-admin');

Comment on lines +72 to +82
} else {
await ctx.send(`❌ You had no saved roles, **${wowName}**.`);
}
} else {
const discordId = prefSvc.resolveDiscordId(name);
if (discordId) {
await prefSvc.clearPreference(discordId);
await ctx.send(`✅ Cleared saved roles for **${name}**.`);
} else {
await ctx.send(`❌ No saved roles found for **${name}**.`);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The clearrole command allows any user to delete the saved role preferences of any other user by specifying their WoW name. The implementation lacks an authorization check to verify if the requester is an administrator or the owner of the roles being cleared. This could be abused to clear other users' data.

Comment on lines +153 to +213
export async function submitGithubIssueModal(
data: GitHubIssueModalData,
): Promise<Record<string, unknown>> {
const versionStr = getVersionString();

let body =
`**Reporter:** ${data.reporterName} (\`${data.reporterId}\`)\n` +
`**Version:** ${versionStr}\n\n` +
`**Description:**\n${data.description}\n`;

if (data.extraInfo) {
const sectionTitle =
data.issueType === 'bug' ? 'Reproduction Steps' : 'Benefit/Impact';
body += `\n**${sectionTitle}:**\n${data.extraInfo}\n`;
}

if (data.issueType === 'bug' && data.includeLogs) {
const lastLines = await getRecentLogs();
if (lastLines) {
const sanitizedLines = sanitizeLogs(lastLines);
body += `\n**Recent Logs:**\n\`\`\`log\n${sanitizedLines}\n\`\`\`\n`;
}
}

const labels =
data.issueType === 'bug' ? ['bug', 'jules'] : ['enhancement', 'jules'];
return createGithubIssue(data.title, body, labels);
}

export interface BadGroupReportData {
reporterName: string;
reporterId: string | number;
title: string;
description: string;
players: { toTestString(): string }[];
groups: { toTestString(): string }[];
}

export async function reportBadGroup(
data: BadGroupReportData,
): Promise<Record<string, unknown>> {
const formattedTitle = `[Bad Group] ${data.title}`;
const versionStr = getVersionString();

const reproInfo =
`**Input Players:**\n\`\`\`python\n[${data.players.map((p) => p.toTestString()).join(', ')}]\n\`\`\`\n` +
`**Resulting Groups:**\n\`\`\`python\n[${data.groups.map((g) => g.toTestString()).join(', ')}]\n\`\`\`\n`;

let body =
`**Reporter:** ${data.reporterName} (\`${data.reporterId}\`)\n` +
`**Version:** ${versionStr}\n\n` +
`**Description:**\n${data.description}\n\n` +
reproInfo;

const lastLines = await getRecentLogs();
if (lastLines) {
const sanitizedLines = sanitizeLogs(lastLines);
body += `\n**Recent Logs:**\n\`\`\`log\n${sanitizedLines}\n\`\`\`\n`;
}

return createGithubIssue(formattedTitle, body, ['bug', 'bad-group']);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

User-supplied input (title, description, and extraInfo) is directly embedded into the body and title of GitHub issues created via submitGithubIssueModal and reportBadGroup. Unlike the automated error reporting flow, these manual reporting paths do not use the sanitizeForGithub or sanitizeLogs functions, potentially leading to the exposure of PII or secrets in a public repository if a user accidentally includes them in their report.

COPY packages/ packages/

CMD ["python", "bot.py"]
CMD ["node", "node_modules/.bin/tsx", "packages/bot/src/bot.ts"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For production deployments, it's better to compile the TypeScript code to JavaScript during the build process and run the output with node directly, rather than using tsx at runtime. This avoids the overhead of on-the-fly compilation and removes a development dependency from the final image.

A multi-stage Dockerfile would be ideal for this. You can have a builder stage that installs all dependencies and compiles the code, and a final, smaller production stage that copies only the necessary node_modules and the compiled JavaScript from the dist directory.

CMD ["node", "packages/bot/dist/bot.js"]

}

async handleEventError(eventName: string, error: Error, ...args: unknown[]): Promise<void> {
const context = `Event: ${eventName}\nArgs: ${JSON.stringify(args)}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

JSON.stringify(args) can throw a TypeError if the args array contains circular references, which can happen with complex objects like those from discord.js. An unhandled exception within an error handler is problematic. It's safer to wrap this call in a try...catch block to ensure the error reporting process itself doesn't fail.

    let argsString: string;
    try {
      argsString = JSON.stringify(args);
    } catch {
      argsString = '[Could not serialize arguments]';
    }
    const context = `Event: ${eventName}\nArgs: ${argsString}`;

@TytaniumDev TytaniumDev enabled auto-merge (squash) March 9, 2026 18:27
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review - Changes Requested

Three bugs were found in the TypeScript rewrite.


1. is never replaced with a real Firestore server timestamp ()

The file exports:

// Sentinel for Firestore server timestamps.
// Replaced with FieldValue.serverTimestamp() at initialization time.
export const SERVER_TIMESTAMP = { __sentinel: 'serverTimestamp' } as const;

But _initializeFirebase() never replaces this constant with admin.firestore.FieldValue.serverTimestamp(). Because it is a const, the exported value will always be the plain object { __sentinel: 'serverTimestamp' }.

Consequences:

  • getOrCreateGuildDoc and getOrCreateChannelDoc store plain objects for createdAt and lastActive instead of real Firestore Timestamps.
  • deleteOldDocs queries where('lastActive', '<', cutoff) where cutoff is a JS Date. Because lastActive is stored as a plain object (not a Firestore Timestamp), this query will never match any documents, so the cleanup job silently does nothing and documents accumulate indefinitely.

Fix: replace usages at the call site, e.g. pass admin.firestore.FieldValue.serverTimestamp() directly, or export a factory function instead of a const sentinel.


2. Log file sent to developer unsanitized ()

In sendErrorToDev, the log file is read and attached as a raw Discord DM:

if (fs.existsSync(LOG_FILE)) {
  const logData = fs.readFileSync(LOG_FILE);
  files.push({ filename: 'bot.log', content: Buffer.isBuffer(logData) ? logData : Buffer.from(logData) });
}

The logger in logger.ts writes messages verbatim — no redaction at write time. If BOT_TOKEN, FIREBASE_CREDENTIALS_JSON, or other secrets appear in a log line, they will be sent in plaintext to the developer's Discord DM. This is inconsistent with the GitHub issue path in issues.ts, where logs are passed through sanitizeForGithub(lastLines) before posting.

Fix: apply sanitizeLogs (from security.ts) to the log buffer before attaching it.


3. Dead try/catch around parseInt creates false sense of safety ()

In handleCollectionRemoved:

try {
  channelId = parseInt(docId, 10);
} catch {
  return;
}
if (isNaN(channelId)) return;

parseInt in JavaScript/TypeScript never throws — it returns NaN for non-numeric input. The catch branch is unreachable dead code. Any real error inside this block (e.g., if the surrounding code were extended) would be silently swallowed. The correct guard is only the isNaN check on the next line.

Fix: remove the try/catch wrapper; keep only the isNaN(channelId) guard.

@TytaniumDev TytaniumDev merged commit b2835ca into main Mar 9, 2026
28 checks passed
@TytaniumDev TytaniumDev deleted the feat/typescript-port branch March 9, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant