import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { TimedExecuteConfig } from '../../entities/timedexecute-config.entity'; import { NatsService } from '../../services/nats.service'; import { CreateTimedExecuteConfigDto } from './dto/create-timedexecute-config.dto'; import { UpdateTimedExecuteConfigDto } from './dto/update-timedexecute-config.dto'; @Injectable() export class TimedExecuteService { private readonly logger = new Logger(TimedExecuteService.name); constructor( @InjectRepository(TimedExecuteConfig) private readonly repo: Repository, private readonly natsService: NatsService, ) {} /** List configs for a license (summaries — no JSONB) */ async getConfigs(licenseId: string) { const configs = await this.repo.find({ where: { license_id: licenseId }, select: ['id', 'config_name', 'description', 'is_active', 'created_at', 'updated_at'], order: { created_at: 'DESC' }, }); return { configs }; } /** Get full config with JSONB data */ async getConfig(licenseId: string, configId: string) { const config = await this.repo.findOne({ where: { id: configId, license_id: licenseId }, }); if (!config) throw new NotFoundException('TimedExecute config not found'); return { config }; } /** Create a new config */ async createConfig(licenseId: string, dto: CreateTimedExecuteConfigDto) { const config = this.repo.create({ license_id: licenseId, config_name: dto.config_name, description: dto.description || null, config_data: dto.config_data || {}, }); const saved = await this.repo.save(config); return { config: saved }; } /** Update an existing config */ async updateConfig(licenseId: string, configId: string, dto: UpdateTimedExecuteConfigDto) { const config = await this.repo.findOne({ where: { id: configId, license_id: licenseId }, }); if (!config) throw new NotFoundException('TimedExecute config not found'); if (dto.config_name !== undefined) config.config_name = dto.config_name; if (dto.description !== undefined) config.description = dto.description; if (dto.config_data !== undefined) config.config_data = dto.config_data; if (dto.is_active !== undefined) config.is_active = dto.is_active; config.updated_at = new Date(); const saved = await this.repo.save(config); return { config: saved }; } /** Delete a config */ async deleteConfig(licenseId: string, configId: string) { const result = await this.repo.delete({ id: configId, license_id: licenseId }); if (result.affected === 0) throw new NotFoundException('TimedExecute config not found'); return { deleted: true }; } /** Deploy config to game server via NATS */ async applyToServer(licenseId: string, configId: string) { const config = await this.repo.findOne({ where: { id: configId, license_id: licenseId }, }); if (!config) throw new NotFoundException('TimedExecute config not found'); const jsonString = JSON.stringify(config.config_data, null, 2); try { // Write TimedExecute.json via file manager NATS await this.natsService.request( `corrosion.${licenseId}.files.cmd`, { func: 'fm_save', path: 'server://oxide/config/TimedExecute.json', content: jsonString, }, 30000, ); // Reload TimedExecute plugin via RCON await this.natsService.publish( `corrosion.${licenseId}.cmd.server`, { action: 'command', command: 'oxide.reload TimedExecute', timestamp: new Date().toISOString(), }, ); // Mark this config as active, deactivate others await this.repo.update({ license_id: licenseId }, { is_active: false }); await this.repo.update( { id: configId, license_id: licenseId }, { is_active: true, updated_at: new Date() }, ); return { success: true, message: `Config "${config.config_name}" deployed to server`, config_name: config.config_name, }; } catch (error) { this.logger.error(`Failed to deploy TimedExecute config: ${(error as Error).message}`); throw new HttpException( 'Failed to deploy TimedExecute config — agent may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } } /** Import TimedExecute.json from game server via NATS */ async importFromServer(licenseId: string, configName: string, description?: string) { try { // Read TimedExecute.json from server via file manager NATS const response = await this.natsService.request( `corrosion.${licenseId}.files.cmd`, { func: 'fm_preview', path: 'server://oxide/config/TimedExecute.json', }, 30000, ); if (!response) { throw new HttpException( 'No response from agent — it may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } // Parse the response content as JSON const responseData = response as Record; let configData: Record; if (typeof responseData.content === 'string') { configData = JSON.parse(responseData.content); } else if (typeof responseData.content === 'object') { configData = responseData.content; } else { throw new HttpException( 'Unexpected response format from agent', HttpStatus.BAD_GATEWAY, ); } // Create new config row const config = this.repo.create({ license_id: licenseId, config_name: configName, description: description || 'Imported from server', config_data: configData, }); const saved = await this.repo.save(config); return { config: saved }; } catch (error) { if (error instanceof HttpException) throw error; this.logger.error(`Failed to import TimedExecute config from server: ${(error as Error).message}`); throw new HttpException( 'Failed to import TimedExecute config — agent may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } } }