Skip to content

Commit f8a8e6c

Browse files
committed
feat(player-connection): implement getPlayerConnectionHistory command with repository integration
1 parent a57860d commit f8a8e6c

8 files changed

Lines changed: 296 additions & 6 deletions

File tree

packages/core/src/domain/PlayerConnectionHistory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export type PlayerConnectionHistory = {
33
steamId3: string;
44
ipAddress: string;
55
nickname: string;
6+
timestamp?: Date;
67
};

packages/core/src/repository/PlayerConnectionHistoryRepository.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ import { PlayerConnectionHistory } from "../domain/PlayerConnectionHistory";
22

33
export interface PlayerConnectionHistoryRepository {
44
save(params: { connectionHistory: Omit<PlayerConnectionHistory, "id"> }): Promise<PlayerConnectionHistory>;
5+
findBySteamId3(params: { steamId3: string }): Promise<PlayerConnectionHistory[]>;
56
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, it, expect } from "vitest";
2+
import { mock } from "vitest-mock-extended";
3+
import { when } from "vitest-when";
4+
import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
5+
import { PlayerConnectionHistoryRepository } from "@tf2qs/core";
6+
import { getPlayerConnectionHistoryHandlerFactory } from "./handler";
7+
8+
describe("getPlayerConnectionHistoryHandler", () => {
9+
function makeSut() {
10+
const playerConnectionHistoryRepositoryMock = mock<PlayerConnectionHistoryRepository>();
11+
const interactionMock = mock<ChatInputCommandInteraction>();
12+
interactionMock.options = mock();
13+
14+
const sut = getPlayerConnectionHistoryHandlerFactory({
15+
playerConnectionHistoryRepository: playerConnectionHistoryRepositoryMock,
16+
});
17+
18+
return {
19+
sut,
20+
playerConnectionHistoryRepositoryMock,
21+
interactionMock,
22+
};
23+
}
24+
25+
it("should return a message when no connection history is found", async () => {
26+
// Given
27+
const { sut, playerConnectionHistoryRepositoryMock, interactionMock } = makeSut();
28+
const steamId3 = "[U:1:12345678]";
29+
30+
when(interactionMock.options.getString)
31+
.calledWith('player_steam_id3', true)
32+
.thenReturn(steamId3);
33+
34+
when(playerConnectionHistoryRepositoryMock.findBySteamId3)
35+
.calledWith({ steamId3 })
36+
.thenResolve([]);
37+
38+
// When
39+
await sut(interactionMock);
40+
41+
// Then
42+
expect(interactionMock.reply).toHaveBeenCalledWith({
43+
content: expect.stringContaining("No connection history found for: `[U:1:12345678]`"),
44+
flags: MessageFlags.Ephemeral,
45+
});
46+
});
47+
48+
it("should display connection history entries", async () => {
49+
// Given
50+
const { sut, playerConnectionHistoryRepositoryMock, interactionMock } = makeSut();
51+
const steamId3 = "[U:1:12345678]";
52+
const mockHistory = [
53+
{
54+
id: 1,
55+
steamId3,
56+
ipAddress: "192.168.1.1",
57+
nickname: "Player1",
58+
timestamp: new Date("2024-01-15T10:00:00Z"),
59+
},
60+
{
61+
id: 2,
62+
steamId3,
63+
ipAddress: "192.168.1.2",
64+
nickname: "Player2",
65+
timestamp: new Date("2024-01-14T09:00:00Z"),
66+
},
67+
];
68+
69+
when(interactionMock.options.getString)
70+
.calledWith('player_steam_id3', true)
71+
.thenReturn(steamId3);
72+
73+
when(playerConnectionHistoryRepositoryMock.findBySteamId3)
74+
.calledWith({ steamId3 })
75+
.thenResolve(mockHistory);
76+
77+
// When
78+
await sut(interactionMock);
79+
80+
// Then
81+
expect(interactionMock.reply).toHaveBeenCalledWith({
82+
content: expect.stringContaining("Found 2 connection(s)"),
83+
flags: MessageFlags.Ephemeral,
84+
});
85+
86+
const content = interactionMock.reply.mock.calls[0][0].content;
87+
expect(content).toContain("Player1");
88+
expect(content).toContain("192.168.1.1");
89+
expect(content).toContain("Player2");
90+
expect(content).toContain("192.168.1.2");
91+
});
92+
});

packages/entrypoints/src/commands/GetPlayerConnectionHistory/handler.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
2+
import { PlayerConnectionHistoryRepository } from "@tf2qs/core";
23

3-
export function getPlayerConnectionHistoryHandlerFactory() {
4+
type GetPlayerConnectionHistoryHandlerDependencies = {
5+
playerConnectionHistoryRepository: PlayerConnectionHistoryRepository;
6+
};
7+
8+
export function getPlayerConnectionHistoryHandlerFactory(dependencies: GetPlayerConnectionHistoryHandlerDependencies) {
49
return async (interaction: ChatInputCommandInteraction) => {
510
const playerSteamId3 = interaction.options.getString('player_steam_id3', true);
611

7-
// Dummy implementation - just echo back for now
12+
const history = await dependencies.playerConnectionHistoryRepository.findBySteamId3({
13+
steamId3: playerSteamId3,
14+
});
15+
16+
if (history.length === 0) {
17+
await interaction.reply({
18+
content: `🔍 **Player Connection History**\n\nNo connection history found for: \`${playerSteamId3}\``,
19+
flags: MessageFlags.Ephemeral
20+
});
21+
return;
22+
}
23+
24+
const historyLines = history.map((entry, index) => {
25+
const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString() : 'Unknown';
26+
return `${index + 1}. **${entry.nickname}** - IP: \`${entry.ipAddress}\` - ${timestamp}`;
27+
});
28+
29+
const content = [
30+
`🔍 **Player Connection History for ${playerSteamId3}**`,
31+
``,
32+
`Found ${history.length} connection(s):`,
33+
``,
34+
...historyLines,
35+
].join('\n');
36+
837
await interaction.reply({
9-
content: `🔍 **Player Connection History**\n\nSearching for player: \`${playerSteamId3}\`\n\n_This is a dummy implementation. Full functionality coming soon!_`,
38+
content,
1039
flags: MessageFlags.Ephemeral
1140
});
1241
};

packages/entrypoints/src/commands/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChatInputCommandInteraction, SlashCommandOptionsOnlyBuilder } from "discord.js";
2-
import { UserCreditsRepository } from "@tf2qs/core";
2+
import { UserCreditsRepository, PlayerConnectionHistoryRepository } from "@tf2qs/core";
33
import { CreateCreditsPurchaseOrder } from "@tf2qs/core";
44
import { CreateServerForUser } from "@tf2qs/core";
55
import { GetServerStatus } from "@tf2qs/core";
@@ -32,6 +32,7 @@ export type CommandDependencies = {
3232
backgroundTaskQueue: BackgroundTaskQueue;
3333
getServerStatus: GetServerStatus;
3434
getUserServers: GetUserServers;
35+
playerConnectionHistoryRepository: PlayerConnectionHistoryRepository;
3536
}
3637

3738
export function createCommands(dependencies: CommandDependencies): Record<string, Command> {
@@ -61,7 +62,9 @@ export function createCommands(dependencies: CommandDependencies): Record<string
6162
getPlayerConnectionHistory: {
6263
name: "get-player-connection-history",
6364
definition: getPlayerConnectionHistoryDefinition,
64-
handler: getPlayerConnectionHistoryHandlerFactory(),
65+
handler: getPlayerConnectionHistoryHandlerFactory({
66+
playerConnectionHistoryRepository: dependencies.playerConnectionHistoryRepository,
67+
}),
6568
ownerOnly: true,
6669
},
6770
status: {

packages/entrypoints/src/discordBot.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ export async function startDiscordBot() {
194194
}),
195195
userCreditsRepository,
196196
configManager: defaultConfigManager,
197-
backgroundTaskQueue
197+
backgroundTaskQueue,
198+
playerConnectionHistoryRepository
198199
})
199200

200201
// Schedule jobs
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2+
import { Knex, knex } from "knex";
3+
import { SQlitePlayerConnectionHistoryRepository } from "./SQlitePlayerConnectionHistoryRepository";
4+
5+
describe("SQlitePlayerConnectionHistoryRepository", () => {
6+
let db: Knex;
7+
let repository: SQlitePlayerConnectionHistoryRepository;
8+
9+
beforeEach(async () => {
10+
db = knex({
11+
client: "sqlite3",
12+
connection: ":memory:",
13+
useNullAsDefault: true,
14+
});
15+
16+
await db.schema.createTable("player_connection_history", (table) => {
17+
table.increments("id").primary();
18+
table.string("steam_id_3").notNullable();
19+
table.string("ip_address").notNullable();
20+
table.string("nickname").notNullable();
21+
table.timestamp("timestamp").notNullable().defaultTo(db.fn.now());
22+
});
23+
24+
repository = new SQlitePlayerConnectionHistoryRepository({ knex: db });
25+
});
26+
27+
afterEach(async () => {
28+
await db.destroy();
29+
});
30+
31+
describe("save", () => {
32+
it("should save player connection history", async () => {
33+
const result = await repository.save({
34+
connectionHistory: {
35+
steamId3: "[U:1:12345678]",
36+
ipAddress: "192.168.1.1",
37+
nickname: "TestPlayer",
38+
},
39+
});
40+
41+
expect(result).toMatchObject({
42+
id: expect.any(Number),
43+
steamId3: "[U:1:12345678]",
44+
ipAddress: "192.168.1.1",
45+
nickname: "TestPlayer",
46+
});
47+
});
48+
});
49+
50+
describe("findBySteamId3", () => {
51+
it("should return empty array when no history found", async () => {
52+
const result = await repository.findBySteamId3({
53+
steamId3: "[U:1:99999999]",
54+
});
55+
56+
expect(result).toEqual([]);
57+
});
58+
59+
it("should find connection history by steamId3", async () => {
60+
// Insert directly with explicit timestamps to ensure order
61+
await db("player_connection_history").insert({
62+
steam_id_3: "[U:1:12345678]",
63+
ip_address: "192.168.1.1",
64+
nickname: "TestPlayer",
65+
timestamp: new Date("2024-01-01T10:00:00Z"),
66+
});
67+
68+
await db("player_connection_history").insert({
69+
steam_id_3: "[U:1:12345678]",
70+
ip_address: "192.168.1.2",
71+
nickname: "TestPlayer2",
72+
timestamp: new Date("2024-01-01T11:00:00Z"),
73+
});
74+
75+
const result = await repository.findBySteamId3({
76+
steamId3: "[U:1:12345678]",
77+
});
78+
79+
expect(result).toHaveLength(2);
80+
// Most recent first (11:00)
81+
expect(result[0]).toMatchObject({
82+
steamId3: "[U:1:12345678]",
83+
ipAddress: "192.168.1.2",
84+
nickname: "TestPlayer2",
85+
timestamp: expect.any(Date),
86+
});
87+
// Older second (10:00)
88+
expect(result[1]).toMatchObject({
89+
steamId3: "[U:1:12345678]",
90+
ipAddress: "192.168.1.1",
91+
nickname: "TestPlayer",
92+
timestamp: expect.any(Date),
93+
});
94+
});
95+
96+
it("should order results by timestamp descending (newest first)", async () => {
97+
// Insert directly with explicit timestamps
98+
await db("player_connection_history").insert({
99+
steam_id_3: "[U:1:12345678]",
100+
ip_address: "192.168.1.1",
101+
nickname: "OlderConnection",
102+
timestamp: new Date("2024-01-01T10:00:00Z"),
103+
});
104+
105+
await db("player_connection_history").insert({
106+
steam_id_3: "[U:1:12345678]",
107+
ip_address: "192.168.1.2",
108+
nickname: "NewerConnection",
109+
timestamp: new Date("2024-01-01T12:00:00Z"),
110+
});
111+
112+
const result = await repository.findBySteamId3({
113+
steamId3: "[U:1:12345678]",
114+
});
115+
116+
expect(result[0].nickname).toBe("NewerConnection");
117+
expect(result[1].nickname).toBe("OlderConnection");
118+
});
119+
120+
it("should not return history for different steamId3", async () => {
121+
await repository.save({
122+
connectionHistory: {
123+
steamId3: "[U:1:12345678]",
124+
ipAddress: "192.168.1.1",
125+
nickname: "Player1",
126+
},
127+
});
128+
129+
await repository.save({
130+
connectionHistory: {
131+
steamId3: "[U:1:87654321]",
132+
ipAddress: "192.168.1.2",
133+
nickname: "Player2",
134+
},
135+
});
136+
137+
const result = await repository.findBySteamId3({
138+
steamId3: "[U:1:12345678]",
139+
});
140+
141+
expect(result).toHaveLength(1);
142+
expect(result[0].steamId3).toBe("[U:1:12345678]");
143+
});
144+
});
145+
});

packages/providers/src/repository/SQlitePlayerConnectionHistoryRepository.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,22 @@ export class SQlitePlayerConnectionHistoryRepository implements PlayerConnection
2525
nickname: connectionHistory.nickname,
2626
};
2727
}
28+
29+
async findBySteamId3(params: { steamId3: string }): Promise<PlayerConnectionHistory[]> {
30+
const { steamId3 } = params;
31+
const { knex } = this.dependencies;
32+
33+
const rows = await knex("player_connection_history")
34+
.where("steam_id_3", steamId3)
35+
.orderBy("timestamp", "desc")
36+
.select("id", "steam_id_3", "ip_address", "nickname", "timestamp");
37+
38+
return rows.map(row => ({
39+
id: row.id,
40+
steamId3: row.steam_id_3,
41+
ipAddress: row.ip_address,
42+
nickname: row.nickname,
43+
timestamp: new Date(row.timestamp),
44+
}));
45+
}
2846
}

0 commit comments

Comments
 (0)