-
Notifications
You must be signed in to change notification settings - Fork 6
get player connection history command for owners only #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { SlashCommandBuilder } from "discord.js"; | ||
|
|
||
| export const getPlayerConnectionHistoryDefinition = new SlashCommandBuilder() | ||
| .setName('get-player-connection-history') | ||
| .setDescription('Gets the connection history for a specific player (Owner only)') | ||
| .setDefaultMemberPermissions(0) // Hides from everyone by default | ||
| .addStringOption(option => | ||
| option.setName('player_steam_id3') | ||
| .setDescription('Player Steam ID3 (e.g., U:1:12345678)') | ||
| .setRequired(true) | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { mock } from "vitest-mock-extended"; | ||
| import { when } from "vitest-when"; | ||
| import { ChatInputCommandInteraction, MessageFlags } from "discord.js"; | ||
| import { PlayerConnectionHistoryRepository } from "@tf2qs/core"; | ||
| import { getPlayerConnectionHistoryHandlerFactory } from "./handler"; | ||
|
|
||
| describe("getPlayerConnectionHistoryHandler", () => { | ||
| function makeSut() { | ||
| const playerConnectionHistoryRepositoryMock = mock<PlayerConnectionHistoryRepository>(); | ||
| const interactionMock = mock<ChatInputCommandInteraction>(); | ||
| interactionMock.options = mock(); | ||
|
|
||
| const sut = getPlayerConnectionHistoryHandlerFactory({ | ||
| playerConnectionHistoryRepository: playerConnectionHistoryRepositoryMock, | ||
| }); | ||
|
|
||
| return { | ||
| sut, | ||
| playerConnectionHistoryRepositoryMock, | ||
| interactionMock, | ||
| }; | ||
| } | ||
|
|
||
| it("should return a message when no connection history is found", async () => { | ||
| // Given | ||
| const { sut, playerConnectionHistoryRepositoryMock, interactionMock } = makeSut(); | ||
| const steamId3 = "[U:1:12345678]"; | ||
|
|
||
| when(interactionMock.options.getString) | ||
| .calledWith('player_steam_id3', true) | ||
| .thenReturn(steamId3); | ||
|
|
||
| when(playerConnectionHistoryRepositoryMock.findBySteamId3) | ||
| .calledWith({ steamId3 }) | ||
| .thenResolve([]); | ||
|
|
||
| // When | ||
| await sut(interactionMock); | ||
|
|
||
| // Then | ||
| expect(interactionMock.reply).toHaveBeenCalledWith({ | ||
| content: expect.stringContaining("No connection history found for: `[U:1:12345678]`"), | ||
| flags: MessageFlags.Ephemeral, | ||
| }); | ||
| }); | ||
|
|
||
| it("should display connection history entries", async () => { | ||
| // Given | ||
| const { sut, playerConnectionHistoryRepositoryMock, interactionMock } = makeSut(); | ||
| const steamId3 = "[U:1:12345678]"; | ||
| const mockHistory = [ | ||
| { | ||
| id: 1, | ||
| steamId3, | ||
| ipAddress: "192.168.1.1", | ||
| nickname: "Player1", | ||
| timestamp: new Date("2024-01-15T10:00:00Z"), | ||
| }, | ||
| { | ||
| id: 2, | ||
| steamId3, | ||
| ipAddress: "192.168.1.2", | ||
| nickname: "Player2", | ||
| timestamp: new Date("2024-01-14T09:00:00Z"), | ||
| }, | ||
| ]; | ||
|
|
||
| when(interactionMock.options.getString) | ||
| .calledWith('player_steam_id3', true) | ||
| .thenReturn(steamId3); | ||
|
|
||
| when(playerConnectionHistoryRepositoryMock.findBySteamId3) | ||
| .calledWith({ steamId3 }) | ||
| .thenResolve(mockHistory); | ||
|
|
||
| // When | ||
| await sut(interactionMock); | ||
|
|
||
| // Then | ||
| expect(interactionMock.reply).toHaveBeenCalledWith({ | ||
| content: expect.stringContaining("Found 2 connection(s)"), | ||
| flags: MessageFlags.Ephemeral, | ||
| }); | ||
|
|
||
| const firstReplyArg = interactionMock.reply.mock.calls[0][0]; | ||
| const content = typeof firstReplyArg === "string" | ||
| ? firstReplyArg | ||
| : "content" in firstReplyArg && typeof firstReplyArg.content === "string" | ||
| ? firstReplyArg.content | ||
| : ""; | ||
|
|
||
| expect(content).toContain("Player1"); | ||
| expect(content).toContain("192.168.1.1"); | ||
| expect(content).toContain("Player2"); | ||
| expect(content).toContain("192.168.1.2"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { ChatInputCommandInteraction, MessageFlags } from "discord.js"; | ||
| import { PlayerConnectionHistoryRepository } from "@tf2qs/core"; | ||
|
|
||
| type GetPlayerConnectionHistoryHandlerDependencies = { | ||
| playerConnectionHistoryRepository: PlayerConnectionHistoryRepository; | ||
| }; | ||
|
|
||
| export function getPlayerConnectionHistoryHandlerFactory(dependencies: GetPlayerConnectionHistoryHandlerDependencies) { | ||
| return async (interaction: ChatInputCommandInteraction) => { | ||
| const playerSteamId3 = interaction.options.getString('player_steam_id3', true); | ||
|
|
||
| const history = await dependencies.playerConnectionHistoryRepository.findBySteamId3({ | ||
| steamId3: playerSteamId3, | ||
| }); | ||
|
Comment on lines
+10
to
+14
|
||
|
|
||
| if (history.length === 0) { | ||
| await interaction.reply({ | ||
| content: `🔍 **Player Connection History**\n\nNo connection history found for: \`${playerSteamId3}\``, | ||
| flags: MessageFlags.Ephemeral | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| const historyLines = history.map((entry, index) => { | ||
| const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString() : 'Unknown'; | ||
| return `${index + 1}. **${entry.nickname}** - IP: \`${entry.ipAddress}\` - ${timestamp}`; | ||
| }); | ||
|
Comment on lines
+24
to
+27
|
||
|
|
||
| const content = [ | ||
| `🔍 **Player Connection History for ${playerSteamId3}**`, | ||
| ``, | ||
| `Found ${history.length} connection(s):`, | ||
| ``, | ||
| ...historyLines, | ||
| ].join('\n'); | ||
|
|
||
| await interaction.reply({ | ||
| content, | ||
| flags: MessageFlags.Ephemeral | ||
| }); | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { getPlayerConnectionHistoryDefinition } from "./definition"; | ||
| export { getPlayerConnectionHistoryHandlerFactory } from "./handler"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,7 @@ export async function startDiscordBot() { | |
| // Define the bot token | ||
| const token = process.env.DISCORD_TOKEN!; | ||
| const clientId = process.env.DISCORD_CLIENT_ID!; | ||
| const ownerGuildId = process.env.DISCORD_OWNER_GUILD_ID; | ||
|
|
||
| // Check if token and clientId are available | ||
| if (!token) { | ||
|
|
@@ -193,7 +194,8 @@ export async function startDiscordBot() { | |
| }), | ||
| userCreditsRepository, | ||
| configManager: defaultConfigManager, | ||
| backgroundTaskQueue | ||
| backgroundTaskQueue, | ||
| playerConnectionHistoryRepository | ||
| }) | ||
|
|
||
| // Schedule jobs | ||
|
|
@@ -279,7 +281,16 @@ export async function startDiscordBot() { | |
| }) | ||
|
|
||
| // Slash commands | ||
| const commands = Object.values(discordCommands).map(command => command.definition) | ||
| const allCommands = Object.values(discordCommands); | ||
|
|
||
| // Separate regular commands from owner-only commands | ||
| const regularCommands = allCommands | ||
| .filter(cmd => !cmd.ownerOnly) | ||
| .map(cmd => cmd.definition); | ||
|
|
||
|
Comment on lines
+286
to
+290
|
||
| const ownerOnlyCommands = allCommands | ||
| .filter(cmd => cmd.ownerOnly) | ||
| .map(cmd => cmd.definition); | ||
|
|
||
| // Register commands with Discord API | ||
| const rest = new REST({ version: '10' }).setToken(token); | ||
|
|
@@ -290,11 +301,33 @@ export async function startDiscordBot() { | |
| body: 'Started refreshing application (/) commands.' | ||
| }); | ||
|
|
||
| // Register commands globally (this applies to all guilds) | ||
| // Register regular commands globally (this applies to all guilds) | ||
| await rest.put(Routes.applicationCommands(clientId), { | ||
| body: commands.map(command => command.toJSON()), // Convert CommandBuilder to JSON | ||
| body: regularCommands.map(command => command.toJSON()), // Convert CommandBuilder to JSON | ||
| }); | ||
|
|
||
| logger.emit({ | ||
| severityText: 'INFO', | ||
| body: `Successfully registered ${regularCommands.length} global command(s).` | ||
| }); | ||
|
|
||
| // Register owner-only commands to the specific guild if configured | ||
| if (ownerGuildId && ownerOnlyCommands.length > 0) { | ||
| await rest.put(Routes.applicationGuildCommands(clientId, ownerGuildId), { | ||
| body: ownerOnlyCommands.map(command => command.toJSON()), | ||
| }); | ||
|
|
||
| logger.emit({ | ||
| severityText: 'INFO', | ||
| body: `Successfully registered ${ownerOnlyCommands.length} owner-only command(s) to guild ${ownerGuildId}.` | ||
| }); | ||
| } else if (ownerOnlyCommands.length > 0) { | ||
| logger.emit({ | ||
| severityText: 'WARN', | ||
| body: 'DISCORD_OWNER_GUILD_ID not set. Owner-only commands will not be registered.' | ||
| }); | ||
| } | ||
|
|
||
| logger.emit({ | ||
| severityText: 'INFO', | ||
| body: 'Successfully reloaded application (/) commands.' | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handler tests use bracketed SteamID3 (
[U:1:...]), but production ingestion storessteamId3without brackets (e.g.,U:1:29162964). Updating the test inputs (or explicitly testing normalization of bracketed inputs) will better reflect real usage.