import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PlayerAction } from '../../entities/player-action.entity'; import { PlayerSession } from '../../entities/player-session.entity'; import { NatsService } from '../../services/nats.service'; import { PlayerActionDto } from './dto/player-action.dto'; export interface Player { steam_id: string; player_name: string; status: 'online' | 'offline' | 'banned'; last_seen?: Date; ban_expires?: Date | null; total_sessions?: number; total_playtime_seconds?: number; } @Injectable() export class PlayersService { constructor( @InjectRepository(PlayerAction) private readonly actionRepo: Repository, @InjectRepository(PlayerSession) private readonly sessionRepo: Repository, private readonly natsService: NatsService, ) {} /** * Get players for a license. * * Primary source: player_sessions table (tracks session lifecycles). * Secondary source: player_actions table (determines ban status). * A player whose most recent action is a 'ban' (not subsequently 'unban') is shown as banned. * A player with an open session (session_end IS NULL) is shown as online. */ async getPlayers(licenseId: string): Promise<{ players: Player[] }> { // Get distinct players from session history const sessions = await this.sessionRepo .createQueryBuilder('session') .where('session.license_id = :licenseId', { licenseId }) .orderBy('session.session_start', 'DESC') .getMany(); // Build per-player session aggregates const playerMap = new Map(); for (const session of sessions) { if (!playerMap.has(session.steam_id)) { const isOnline = session.session_end === null; playerMap.set(session.steam_id, { steam_id: session.steam_id, player_name: session.player_name, status: isOnline ? 'online' : 'offline', last_seen: session.session_start, ban_expires: null, total_sessions: 0, total_playtime_seconds: 0, }); } const entry = playerMap.get(session.steam_id)!; entry.total_sessions = (entry.total_sessions || 0) + 1; entry.total_playtime_seconds = (entry.total_playtime_seconds || 0) + (session.duration_seconds || 0); } // Overlay ban status from most recent action per player const recentActions = await this.actionRepo .createQueryBuilder('action') .where('action.license_id = :licenseId', { licenseId }) .orderBy('action.created_at', 'DESC') .getMany(); // Track the most recent action per steam_id to determine ban state const latestActionBySteamId = new Map(); for (const action of recentActions) { if (!latestActionBySteamId.has(action.steam_id)) { latestActionBySteamId.set(action.steam_id, action); } } for (const [steamId, action] of latestActionBySteamId) { if (action.action_type === 'ban') { // Add player from actions even if they have no sessions if (!playerMap.has(steamId)) { playerMap.set(steamId, { steam_id: action.steam_id, player_name: action.player_name, status: 'banned', last_seen: action.created_at, ban_expires: action.duration_minutes ? new Date(action.created_at.getTime() + action.duration_minutes * 60000) : null, total_sessions: 0, total_playtime_seconds: 0, }); } else { const entry = playerMap.get(steamId)!; entry.status = 'banned'; entry.ban_expires = action.duration_minutes ? new Date(action.created_at.getTime() + action.duration_minutes * 60000) : null; } } } const players = Array.from(playerMap.values()); return { players }; } /** * Perform a moderation action on a player. * Supported actions: kick, ban, unban, warn, note. * kick/ban/unban are forwarded to the game server via NATS. */ async performAction( licenseId: string, userId: string, dto: PlayerActionDto, ): Promise<{ success: boolean }> { // Insert action record const action = this.actionRepo.create({ license_id: licenseId, steam_id: dto.steam_id, player_name: dto.player_name, action_type: dto.action_type, reason: dto.reason || null, duration_minutes: dto.duration_minutes || null, performed_by: userId, }); await this.actionRepo.save(action); // Forward kick, ban, and unban to the game server via NATS if (dto.action_type === 'kick' || dto.action_type === 'ban' || dto.action_type === 'unban') { await this.natsService.sendServerCommand(licenseId, dto.action_type, { steam_id: dto.steam_id, reason: dto.reason, duration_minutes: dto.duration_minutes, }); } return { success: true }; } }