diff --git a/migrations/20260207160515_player_connection_history.ts b/migrations/20260207160515_player_connection_history.ts new file mode 100644 index 00000000..bd4d5916 --- /dev/null +++ b/migrations/20260207160515_player_connection_history.ts @@ -0,0 +1,18 @@ +import type { Knex } from "knex"; + + +export async function up(knex: Knex): Promise { + await knex.schema.createTable("player_connection_history", (table) => { + table.increments("id").primary(); + table.string("steam_id_3").notNullable(); + table.string("ip_address").notNullable(); + table.string("nickname").notNullable(); + table.timestamp("timestamp").notNullable().defaultTo(knex.fn.now()); + }); +} + + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists("player_connection_history"); +} + diff --git a/packages/core/index.ts b/packages/core/index.ts index ca8d0edb..0f39824c 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -18,6 +18,7 @@ export * from './src/repository/UserRepository'; export * from './src/repository/UserBanRepository'; export * from './src/repository/UserCreditsRepository'; export * from './src/repository/ServerStatusMetricsRepository'; +export * from './src/repository/PlayerConnectionHistoryRepository'; // Services export * from './src/services/BackgroundTaskQueue'; diff --git a/packages/core/src/domain/PlayerConnectionHistory.ts b/packages/core/src/domain/PlayerConnectionHistory.ts new file mode 100644 index 00000000..7248bf6a --- /dev/null +++ b/packages/core/src/domain/PlayerConnectionHistory.ts @@ -0,0 +1,7 @@ +export type PlayerConnectionHistory = { + id?: number; + steamId3: string; + ipAddress: string; + nickname: string; + timestamp: Date; +}; diff --git a/packages/core/src/domain/index.ts b/packages/core/src/domain/index.ts index bc17d399..b75f86d4 100644 --- a/packages/core/src/domain/index.ts +++ b/packages/core/src/domain/index.ts @@ -14,5 +14,6 @@ export * from "./GuildParameters"; export * from "./Cost"; export * from "./DateRange"; export * from "./ServerActivity"; +export * from "./PlayerConnectionHistory"; export { ServerStatusParser } from "./ServerStatus"; export * from "./ServerStatusMetric"; \ No newline at end of file diff --git a/packages/core/src/repository/PlayerConnectionHistoryRepository.ts b/packages/core/src/repository/PlayerConnectionHistoryRepository.ts new file mode 100644 index 00000000..4515dfa1 --- /dev/null +++ b/packages/core/src/repository/PlayerConnectionHistoryRepository.ts @@ -0,0 +1,5 @@ +import { PlayerConnectionHistory } from "../domain/PlayerConnectionHistory"; + +export interface PlayerConnectionHistoryRepository { + save(params: { connectionHistory: Omit }): Promise; +} diff --git a/packages/entrypoints/src/discordBot.ts b/packages/entrypoints/src/discordBot.ts index b20f04c8..2c8af3de 100644 --- a/packages/entrypoints/src/discordBot.ts +++ b/packages/entrypoints/src/discordBot.ts @@ -29,6 +29,7 @@ import { SQLiteServerRepository } from "@tf2qs/providers"; import { SQliteUserCreditsRepository } from "@tf2qs/providers"; import { SQliteUserRepository } from "@tf2qs/providers"; import { SQliteServerStatusMetricsRepository } from "@tf2qs/providers"; +import { SQlitePlayerConnectionHistoryRepository } from "@tf2qs/providers"; import { AdyenPaymentService } from "@tf2qs/providers"; import { ChancePasswordGeneratorService } from "@tf2qs/providers"; import { defaultAWSServiceFactory } from "@tf2qs/providers"; @@ -134,6 +135,10 @@ export async function startDiscordBot() { knex: KnexConnectionManager.client }) + const playerConnectionHistoryRepository = new SQlitePlayerConnectionHistoryRepository({ + knex: KnexConnectionManager.client + }) + const backgroundTaskQueue = new InMemoryBackgroundTaskQueue(defaultGracefulShutdownManager); const deleteServerUseCase = new DeleteServerForUser({ serverManagerFactory: serverManagerFactory, @@ -372,7 +377,8 @@ export async function startDiscordBot() { serverRepository, userRepository, eventLogger, - backgroundTaskQueue + backgroundTaskQueue, + playerConnectionHistoryRepository }); initializeExpress({}) diff --git a/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.test.ts b/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.test.ts index 058ccbeb..4083beea 100644 --- a/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.test.ts +++ b/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.test.ts @@ -68,5 +68,25 @@ describe("clientConnected command parser", () => { }); }); + + it("should save connection history to repository", async () => { + const { services, command, handler } = createTestEnvironment(); + if (!command || !handler) throw new Error("Command or handler is undefined"); + + await handler({ + args: command.args, + password: "test-password", + services, + }); + + expect(services.playerConnectionHistoryRepository.save).toHaveBeenCalledWith({ + connectionHistory: { + steamId3: "U:1:29162964", + ipAddress: "169.254.249.16", + nickname: "sonikro", + timestamp: expect.any(Date), + }, + }); + }); }); }); diff --git a/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.ts b/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.ts index 1e1322c1..2ddff7bf 100644 --- a/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.ts +++ b/packages/entrypoints/src/udp/srcdsCommands/ClientConnected.ts @@ -21,7 +21,7 @@ export const clientConnected: SRCDSCommandParser = (rawStri raw: rawString, args: { nickname, ipAddress, steamId3 }, type: "clientConnected", - handler: async ({ args }) => { + handler: async ({ args, services }) => { const { nickname, ipAddress, steamId3 } = args; logger.emit({ @@ -33,6 +33,16 @@ export const clientConnected: SRCDSCommandParser = (rawStri steamId3, }, }); + + // Persist connection history + await services.playerConnectionHistoryRepository.save({ + connectionHistory: { + steamId3, + ipAddress, + nickname, + timestamp: new Date(), + }, + }); }, }; }; diff --git a/packages/entrypoints/src/udp/srcdsCommands/UDPCommandServices.ts b/packages/entrypoints/src/udp/srcdsCommands/UDPCommandServices.ts index 3d846e15..7ec611b5 100644 --- a/packages/entrypoints/src/udp/srcdsCommands/UDPCommandServices.ts +++ b/packages/entrypoints/src/udp/srcdsCommands/UDPCommandServices.ts @@ -4,6 +4,7 @@ import { UserRepository } from "@tf2qs/core"; import { EventLogger } from "@tf2qs/core"; import { ServerCommander } from "@tf2qs/core"; import { BackgroundTaskQueue } from "@tf2qs/core"; +import { PlayerConnectionHistoryRepository } from "@tf2qs/core"; /** * List of services available to UDP Command Handlers @@ -15,4 +16,5 @@ export type UDPCommandsServices = { userRepository: UserRepository eventLogger: EventLogger; backgroundTaskQueue: BackgroundTaskQueue; + playerConnectionHistoryRepository: PlayerConnectionHistoryRepository; } diff --git a/packages/providers/src/repository/SQlitePlayerConnectionHistoryRepository.ts b/packages/providers/src/repository/SQlitePlayerConnectionHistoryRepository.ts new file mode 100644 index 00000000..8e4523fc --- /dev/null +++ b/packages/providers/src/repository/SQlitePlayerConnectionHistoryRepository.ts @@ -0,0 +1,30 @@ +import { Knex } from "knex"; +import { PlayerConnectionHistory, PlayerConnectionHistoryRepository } from "@tf2qs/core"; + +type SQlitePlayerConnectionHistoryRepositoryDependencies = { + knex: Knex; +}; + +export class SQlitePlayerConnectionHistoryRepository implements PlayerConnectionHistoryRepository { + constructor(private readonly dependencies: SQlitePlayerConnectionHistoryRepositoryDependencies) {} + + async save(params: { connectionHistory: Omit }): Promise { + const { connectionHistory } = params; + const { knex } = this.dependencies; + + const [id] = await knex("player_connection_history").insert({ + steam_id_3: connectionHistory.steamId3, + ip_address: connectionHistory.ipAddress, + nickname: connectionHistory.nickname, + timestamp: connectionHistory.timestamp, + }); + + return { + id, + steamId3: connectionHistory.steamId3, + ipAddress: connectionHistory.ipAddress, + nickname: connectionHistory.nickname, + timestamp: connectionHistory.timestamp, + }; + } +} diff --git a/packages/providers/src/repository/index.ts b/packages/providers/src/repository/index.ts index 0adfbb43..0c1e5b7b 100644 --- a/packages/providers/src/repository/index.ts +++ b/packages/providers/src/repository/index.ts @@ -8,3 +8,4 @@ export * from './SQliteServerRepository'; export * from './SQliteUserCreditsRepository'; export * from './SQliteUserRepository'; export * from './SQliteServerStatusMetricsRepository'; +export * from './SQlitePlayerConnectionHistoryRepository';