refactor: integration modules

This commit is contained in:
kastov 2026-08-14 14:26:45 +03:00
parent 8ff4c83873
commit 4e4f5596e9
No known key found for this signature in database
GPG key ID: 1B27BE29057F4C90
10 changed files with 73 additions and 13 deletions

View file

@ -192,10 +192,6 @@ jobs:
- `remnawave/node:${{github.ref_name}}`
- `ghcr.io/remnawave/node:${{github.ref_name}}`
Docker images with bundled HAProxy:
- `remnawave/node:latest-hp` / `remnawave/node:${{github.ref_name}}-hp`
- `ghcr.io/remnawave/node:latest-hp` / `ghcr.io/remnawave/node:${{github.ref_name}}-hp`
send-telegram-message:
name: Send Telegram message
needs: [merge, create-release]

View file

@ -30,6 +30,9 @@ RUN apk add --no-cache curl \
&& rm -f /tmp/asn-prefixes-lmdb.tar.gz
FROM scratch AS integration-bin
FROM node:24.19-trixie-slim
ARG S6_OVERLAY_VERSION=3.2.3.0
@ -80,6 +83,7 @@ RUN apt-get update \
ARG INTEGRATIONS=""
RUN --mount=type=bind,source=docker/integrations,target=/mnt/integrations \
--mount=type=bind,from=integration-bin,target=/mnt/bin \
sh /mnt/integrations/install.sh "${INTEGRATIONS}"
ENV NODE_ENV=production

View file

@ -1,12 +1,13 @@
import { z } from 'zod';
import { NodeSystemSchema } from '../../models';
import { REST_API } from '../../api';
import { NodeMetadataSchema, NodeSystemSchema } from '../../models';
export namespace StartXrayCommand {
export const url = REST_API.XRAY.START;
export const RequestSchema = z.object({
internals: z.object({
metadata: NodeMetadataSchema.optional(),
forceRestart: z.boolean().default(false),
hashes: z.object({
emptyConfig: z.string(),

View file

@ -1,3 +1,4 @@
export * from './node-system.schema';
export * from './torrent-blocker.report.schema';
export * from './xray-webhook.schema';
export * from './node-metadata.schema';

View file

@ -0,0 +1,11 @@
import { z } from 'zod';
export const NodeMetadataSchema = z.object({
name: z.string(),
uuid: z.string(),
id: z.number(),
tags: z.array(z.string()),
countryCode: z.string(),
});
export type TNodeMetadata = z.infer<typeof NodeMetadataSchema>;

View file

@ -1,6 +1,6 @@
{
"name": "@remnawave/node-contract",
"version": "2.9.0",
"version": "2.9.1",
"description": "A node-contract library for Remnawave Panel",
"keywords": [],
"homepage": "https://github.com/remnawave",

View file

@ -1,3 +1,7 @@
import { Type } from '@nestjs/common';
import { TNodeMetadata } from '@libs/contracts/models';
export const NODE_INTEGRATIONS = 'NODE_INTEGRATIONS' as const;
export interface INodeIntegrationResult {
@ -7,7 +11,16 @@ export interface INodeIntegrationResult {
export interface INodeIntegration {
readonly name: string;
sync(coreConfig: unknown): Promise<INodeIntegrationResult>;
sync(
integrationConfig: Record<string, unknown>,
nodeMetadata: TNodeMetadata,
): Promise<INodeIntegrationResult>;
stop(): Promise<void>;
}
export interface INodeIntegrationDescriptor {
module: Type;
service: Type<INodeIntegration>;
isAvailable: () => boolean;
}

View file

@ -1,18 +1,34 @@
import { Global, Module } from '@nestjs/common';
import { ConditionalModule } from '@nestjs/config';
import { INodeIntegration, NODE_INTEGRATIONS } from './integrations.contract';
import {
INodeIntegration,
INodeIntegrationDescriptor,
NODE_INTEGRATIONS,
} from './integrations.contract';
import { IntegrationsService } from './integrations.service';
const context = require.context('./', true, /\.integration\.ts$/);
const DESCRIPTORS: INodeIntegrationDescriptor[] = context
.keys()
.map((key) => (context(key) as { descriptor: INodeIntegrationDescriptor }).descriptor);
@Global()
@Module({
imports: [],
imports: DESCRIPTORS.map((descriptor) =>
ConditionalModule.registerWhen(descriptor.module, descriptor.isAvailable, { debug: false }),
),
providers: [
IntegrationsService,
{
provide: NODE_INTEGRATIONS,
useFactory: (...integrations: (INodeIntegration | undefined)[]) =>
integrations.filter((integration) => integration !== undefined),
inject: [],
inject: DESCRIPTORS.map((descriptor) => ({
token: descriptor.service,
optional: true,
})),
},
],
exports: [IntegrationsService],

View file

@ -1,5 +1,7 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { TNodeMetadata } from '@libs/contracts/models';
import {
INodeIntegration,
INodeIntegrationResult,
@ -18,14 +20,21 @@ export class IntegrationsService {
}
}
public async sync(coreConfig: unknown): Promise<INodeIntegrationResult> {
public async sync(
integrationConfig: unknown | undefined,
nodeMetadata: TNodeMetadata | undefined,
): Promise<INodeIntegrationResult> {
if (this.integrations.length === 0) return { error: null };
if (!nodeMetadata) return { error: 'Node metadata is missing.' };
const errors: string[] = [];
for (const integration of this.integrations) {
try {
const { error } = await integration.sync(coreConfig);
const { error } = await integration.sync(
this.asRecord(integrationConfig),
nodeMetadata,
);
if (error) errors.push(`[${integration.name}] ${error}`);
} catch (error) {
@ -49,4 +58,10 @@ export class IntegrationsService {
}
}
}
private asRecord(value: unknown): Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
}

View file

@ -124,7 +124,10 @@ export class XrayService implements OnApplicationBootstrap {
this.isXrayStartedProccesing = true;
try {
const integrations = await this.integrations.sync(body.xrayConfig);
const integrations = await this.integrations.sync(
body.xrayConfig.integrations,
body.internals.metadata,
);
if (integrations.error) {
this.logger.error(`Failed to sync integrations: ${integrations.error}`);