oggui/ogWebconsole/src/app/services/plugin.service.ts

373 lines
13 KiB
TypeScript

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from '@services/config.service';
import {
PluginCommand,
RoleDefinition,
PluginBadge,
PluginManifest,
PluginGroup
} from './plugin/plugin.types';
import { PluginStateService } from './plugin/plugin-state.service';
import { PluginBadgeService } from './plugin/plugin-badge.service';
import { PluginRolesService } from './plugin/plugin-roles.service';
// Re-exportar todos los tipos para compatibilidad transparente con componentes existentes
export * from './plugin/plugin.types';
@Injectable({
providedIn: 'root'
})
export class PluginService {
private http = inject(HttpClient);
private configService = inject(ConfigService);
private translateService = inject(TranslateService);
private stateService = inject(PluginStateService);
private badgeService = inject(PluginBadgeService);
private rolesService = inject(PluginRolesService);
private allPlugins: PluginManifest[] = [];
private activePlugins: PluginManifest[] = [];
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
public plugins$ = this.pluginsSubject.asObservable();
public statusUpdates$ = this.stateService.statusUpdates$;
constructor() {
this.refreshPlugins().subscribe();
this.stateService.initMercureSubscription();
// Re-aplicar traducciones dinámicas al cambiar de idioma
this.translateService.onLangChange.subscribe((event) => {
this.loadPluginTranslations(event.lang);
});
}
// --- Gestión de estado delegado a PluginStateService ---
public setPluginStatus(pluginName: string, idOrUuid: string | number, status: string): void {
this.stateService.setPluginStatus(pluginName, idOrUuid, status);
}
public getPluginStatus(pluginName: string, idOrUuid: string | number): string | undefined {
return this.stateService.getPluginStatus(pluginName, idOrUuid);
}
public setPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number, status: string): void {
this.stateService.setPartitionPluginStatus(pluginName, idOrUuid, disk, partition, status);
}
public getPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string | undefined {
return this.stateService.getPartitionPluginStatus(pluginName, idOrUuid, disk, partition);
}
public clearPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): void {
this.stateService.clearPartitionPluginStatus(pluginName, idOrUuid, disk, partition);
}
public clearPluginStatus(pluginName: string, idOrUuid: string | number): void {
this.stateService.clearPluginStatus(pluginName, idOrUuid);
}
public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any, disk?: number, partition?: number): void {
this.stateService.setPluginMetadata(pluginName, idOrUuid, metadata, disk, partition);
}
public getPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): any {
return this.stateService.getPluginMetadata(pluginName, idOrUuid, disk, partition);
}
public clearPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): void {
this.stateService.clearPluginMetadata(pluginName, idOrUuid, disk, partition);
}
public loadActivePluginsStatus(): Observable<void> {
return this.stateService.loadActivePluginsStatus(this.activePlugins);
}
// --- Registro, ciclo de vida e i18n de Plugins ---
refreshPlugins(): Observable<PluginManifest[]> {
const apiUrl = this.configService.apiUrl;
if (!apiUrl) {
return of([]);
}
return this.http.get<PluginManifest[]>(`${apiUrl}/plugins?all=true`).pipe(
catchError(() => of([])),
switchMap((plugins) => {
if (!plugins || plugins.length === 0) {
this.allPlugins = [];
this.activePlugins = [];
this.pluginsSubject.next([]);
return of([]);
}
// Descubrir y fusionar manifiestos extendidos de cada plugin
const manifestRequests = plugins.map(p =>
this.http.get<Partial<PluginManifest>>(`${apiUrl}/plugins/${p.name}/manifest`).pipe(
catchError(() => of({})),
map(extra => ({
...p,
...extra,
enabled: Boolean(p.enabled) // La base de datos es la única fuente de verdad para el estado de activación
}))
)
);
return forkJoin(manifestRequests).pipe(
tap((fullPlugins) => {
this.allPlugins = fullPlugins;
this.activePlugins = fullPlugins.filter(p => p.enabled);
this.pluginsSubject.next(this.allPlugins);
this.loadPluginTranslations();
this.loadActivePluginsStatus().subscribe();
})
);
})
);
}
togglePlugin(name: string, enabled?: boolean): Observable<any> {
const apiUrl = this.configService.apiUrl;
if (!apiUrl) return of(null);
return this.http.post(`${apiUrl}/plugins/${name}/toggle`, enabled !== undefined ? { enabled } : {}).pipe(
switchMap(() => this.refreshPlugins())
);
}
public loadPluginTranslations(lang?: string): void {
const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es';
const apiUrl = this.configService.apiUrl;
for (const plugin of this.allPlugins) {
// 1. Si el plugin declara i18n directamente en el manifest, lo inyectamos
if (plugin.i18n && typeof plugin.i18n === 'object') {
for (const [l, dict] of Object.entries(plugin.i18n)) {
if (dict && typeof dict === 'object') {
this.translateService.setTranslation(l, dict, true);
}
}
}
// 2. Si el plugin tiene endpoint /i18n/{lang} (carpeta i18n/ estandarizada), lo descargamos bajo demanda
if (apiUrl) {
this.http.get<Record<string, any>>(`${apiUrl}/plugins/${plugin.name}/i18n/${currentLang}`).pipe(
catchError(() => of(null)),
tap((catalog) => {
if (catalog && typeof catalog === 'object' && Object.keys(catalog).length > 0) {
this.translateService.setTranslation(currentLang, catalog, true);
}
})
).subscribe();
}
}
}
getPlugins(): PluginManifest[] {
return this.activePlugins.filter(p => p.enabled);
}
getAllPlugins(): PluginManifest[] {
return this.allPlugins;
}
hasPlugins(): boolean {
return this.allPlugins.length > 0;
}
hasActivePlugins(): boolean {
return this.getPlugins().length > 0;
}
isPluginActive(name: string): boolean {
return this.activePlugins.some(p => p.name === name && p.enabled);
}
getPlugin(name: string): PluginManifest | undefined {
return this.allPlugins.find(p => p.name === name);
}
getPluginGroups(): PluginGroup[] {
return this.getPlugins()
.filter(p => p.commands && p.commands.length > 0)
.map(p => {
const translatedTitle = p.titleKey ? this.translateService.instant(p.titleKey) : p.title;
const filteredCommands = p.commands
.filter(cmd => this.isCommandAllowedForUser(cmd, p.name))
.map(cmd => {
const key = cmd.translationKey || cmd.labelKey;
if (key) {
const trans = this.translateService.instant(key);
if (trans && trans !== key) {
return { ...cmd, label: trans };
}
}
return cmd;
});
return {
plugin: {
...p,
title: (translatedTitle && translatedTitle !== p.titleKey) ? translatedTitle : p.title
},
commands: filteredCommands
};
})
.filter(group => group.commands.length > 0);
}
getCommands(): PluginCommand[] {
const commands: PluginCommand[] = [];
for (const group of this.getPluginGroups()) {
if (group.commands && Array.isArray(group.commands)) {
commands.push(...group.commands);
}
}
return commands;
}
findCommand(slug: string): PluginCommand | undefined {
return this.getCommands().find(c => c.slug === slug);
}
isCommandSupportedForClients(command: PluginCommand, clients: any[], pluginName?: string): boolean {
if (!clients || clients.length === 0) {
return true;
}
if (command.os && command.os.length > 0) {
const requiredOsList = command.os.map(os => os.toLowerCase());
const osSupported = clients.every(client => {
const statusStr = (client.status || '').toLowerCase();
const matchesStatus = requiredOsList.some(req => statusStr.includes(req));
const hasMatchingPartition = (client.partitions || []).some((partition: any) => {
const osName = (partition.operativeSystem?.name || partition.operativeSystem || '').toLowerCase();
return requiredOsList.some(req => osName.includes(req));
});
return matchesStatus || hasMatchingPartition;
});
if (!osSupported) {
return false;
}
}
if (pluginName && (command.requireStatus || command.excludeStatus)) {
return clients.every(client => {
const idKey = client.id ? String(client.id) : null;
const uuidKey = client.uuid || client.name;
const currentStatus = (idKey ? this.getPluginStatus(pluginName, idKey) : null)
|| (uuidKey ? this.getPluginStatus(pluginName, uuidKey) : null)
|| 'DISABLED';
if (command.excludeStatus && command.excludeStatus.length > 0) {
const excludedUpper = command.excludeStatus.map(s => s.toUpperCase());
if (excludedUpper.includes(currentStatus.toUpperCase())) {
return false;
}
}
if (command.requireStatus && command.requireStatus.length > 0) {
const requiredUpper = command.requireStatus.map(s => s.toUpperCase());
if (!requiredUpper.includes(currentStatus.toUpperCase())) {
return false;
}
}
return true;
});
}
return true;
}
// --- Roles y permisos delegados a PluginRolesService ---
isCommandAllowedForUser(command: PluginCommand, pluginName: string): boolean {
return this.rolesService.isCommandAllowedForUser(command, this.getPluginConfig(pluginName));
}
getUserRoles(): Observable<RoleDefinition[]> {
return this.rolesService.getUserRoles();
}
getCommandRoles(pluginName: string, commandSlug: string): string[] {
return this.rolesService.getCommandRoles(this.getPlugin(pluginName), commandSlug, this.getPluginConfig(pluginName));
}
savePluginCommandRoles(pluginName: string, commandRoles: Record<string, string[]>): void {
const currentConfig = this.getPluginConfig(pluginName);
const updatedConfig = {
...currentConfig,
command_roles: commandRoles
};
this.savePluginConfig(pluginName, updatedConfig);
}
// --- Badges delegados a PluginBadgeService ---
getClientBadges(client: any): PluginBadge[] {
return this.badgeService.getClientBadges(this.activePlugins, client);
}
getPartitionBadges(client: any, diskNumber: number, partitionNumber: number): PluginBadge[] {
return this.badgeService.getPartitionBadges(this.activePlugins, client, diskNumber, partitionNumber);
}
// --- Configuración de Plugins (Local + Backend) ---
getPluginConfig(pluginName: string): Record<string, any> {
const plugin = this.getPlugin(pluginName);
const defaults = plugin?.defaultConfig || {};
const storageKey = `og_plugin_config_${pluginName}`;
const saved = localStorage.getItem(storageKey);
if (saved) {
try {
return { ...defaults, ...(plugin?.config || {}), ...JSON.parse(saved) };
} catch {
return { ...defaults, ...(plugin?.config || {}) };
}
}
return { ...defaults, ...(plugin?.config || {}) };
}
savePluginConfig(pluginName: string, config: Record<string, any>): void {
const storageKey = `og_plugin_config_${pluginName}`;
localStorage.setItem(storageKey, JSON.stringify(config));
const plugin = this.getPlugin(pluginName);
if (plugin) {
plugin.config = config;
}
const apiUrl = this.configService.apiUrl;
if (apiUrl) {
this.http.post(`${apiUrl}/plugins/${pluginName}/config`, config).pipe(
catchError((err) => {
console.warn(`[PluginService] Could not persist config for ${pluginName} to backend`, err);
return of(null);
})
).subscribe();
}
}
resetPluginConfig(pluginName: string): Record<string, any> {
const plugin = this.getPlugin(pluginName);
const defaults = plugin?.defaultConfig || {};
const storageKey = `og_plugin_config_${pluginName}`;
localStorage.removeItem(storageKey);
this.savePluginConfig(pluginName, defaults);
return defaults;
}
}