import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { BetterChatConfig } from '../../entities/betterchat-config.entity'; import { InstancesService } from '../instances/instances.service'; import { CreateBetterChatConfigDto } from './dto/create-betterchat-config.dto'; import { UpdateBetterChatConfigDto } from './dto/update-betterchat-config.dto'; @Injectable() export class BetterChatService { private readonly logger = new Logger(BetterChatService.name); constructor( @InjectRepository(BetterChatConfig) private readonly repo: Repository, private readonly instancesService: InstancesService, ) {} /** 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('BetterChat config not found'); return { config }; } /** Create a new config */ async createConfig(licenseId: string, dto: CreateBetterChatConfigDto) { 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: UpdateBetterChatConfigDto) { const config = await this.repo.findOne({ where: { id: configId, license_id: licenseId }, }); if (!config) throw new NotFoundException('BetterChat 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('BetterChat 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('BetterChat config not found'); const jsonString = JSON.stringify(config.config_data, null, 2); try { // Write BetterChat.json via Rust agent await this.instancesService.writeFileForLicense( licenseId, 'oxide/config/BetterChat.json', jsonString, ); // Reload BetterChat plugin via RCON await this.instancesService.rconForLicense(licenseId, 'oxide.reload BetterChat'); // 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 BetterChat config: ${(error as Error).message}`); throw new HttpException( 'Failed to deploy BetterChat config — agent may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } } /** Import BetterChat.json from game server via NATS */ async importFromServer(licenseId: string, configName: string, description?: string) { try { // Read BetterChat.json from server via Rust agent const result = await this.instancesService.readFileForLicense( licenseId, 'oxide/config/BetterChat.json', ); if (!result) { throw new HttpException( 'No response from agent — it may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } // Parse the response content as JSON const responseData = (result as any).content; let configData: Record; if (typeof responseData === 'string') { configData = JSON.parse(responseData); } else if (typeof responseData === 'object') { configData = responseData; } 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 BetterChat config from server: ${(error as Error).message}`); throw new HttpException( 'Failed to import BetterChat config — agent may be offline', HttpStatus.SERVICE_UNAVAILABLE, ); } } }