diff --git a/src/app.module.ts b/src/app.module.ts index 3db5a77..5d6af12 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,13 +7,14 @@ import { ConfigModule } from "@nestjs/config"; import { AppController } from "./app.controller"; import { ServeStaticModule } from "@nestjs/serve-static"; import { join } from "path"; +import { ClientModule } from "./client/client.module"; @Module({ imports: [ConfigModule.forRoot({ isGlobal: true, envFilePath: ".env", cache: true, }), ServeStaticModule.forRoot({ rootPath: join(__dirname, "..", "static"), - }), Bitrix24Module, OauthModule, WebhooksModule, PrismaModule], + }), Bitrix24Module, OauthModule, WebhooksModule, PrismaModule, ClientModule], controllers: [AppController], }) export class AppModule {} diff --git a/src/bitrix24/bitrix24.service.ts b/src/bitrix24/bitrix24.service.ts index ff2a5ae..cc2427f 100644 --- a/src/bitrix24/bitrix24.service.ts +++ b/src/bitrix24/bitrix24.service.ts @@ -769,6 +769,130 @@ export class Bitrix24Service extends BaseAdapter< } } + async getInstances(portalUrl: string): Promise<(Instance & { info?: any })[]> { + this.logger.info(`Fetching instances for portal: ${portalUrl}`); + const user = await this.prisma.findUser(portalUrl); + if (!user) { + throw new NotFoundError(`User not found for portal ${portalUrl}`); + } + const instances = await this.prisma.getInstancesByUserId(user.id); + + const instancesWithInfo = await Promise.all( + instances.map(async (instance) => { + try { + const info = await this.getInstanceInfo(instance.idInstance); + return { ...instance, info }; + } catch (error) { + this.logger.error(`Could not get info for instance ${instance.idInstance}`, error); + return instance; + } + }), + ); + + return instancesWithInfo; + } + + async getInstanceInfo(idInstance: bigint): Promise { + this.logger.info(`Getting info for instance ${idInstance}`); + const instance = await this.prisma.getInstanceByIdWithUser(idInstance); + if (!instance) { + throw new NotFoundError(`Instance ${idInstance} not found`); + } + + const gaClient = this.createGreenApiClient(instance); + // Assuming getStateInstance returns phone number info when authorized + return gaClient.getStateInstance(); + } + + async createInstance(portalUrl: string, memberId: string): Promise<{ idInstance: bigint; qrCodeBase64: string }> { + this.logger.info(`Creating new instance for portal ${portalUrl} by member ${memberId}`); + + const user = await this.prisma.findUser(portalUrl); + if (!user) { + throw new NotFoundError(`User not found for portal ${portalUrl}`); + } + + // This part is an assumption based on the Postman collection. + // The actual implementation would depend on the real API. + const instanceApiUrl = this.configService.get("INSTANCE_API_URL"); + if (!instanceApiUrl) { + throw new IntegrationError("Instance API URL is not configured", "CONFIGURATION_ERROR"); + } + + let newInstanceResponse; + try { + // In a real implementation, you would use axios or another HTTP client + // const response = await axios.post(`${instanceApiUrl}/instances`, { ... }); + // newInstanceResponse = response.data; + + // Mocking the response for now + newInstanceResponse = { + idInstance: String(Math.floor(1000000000 + Math.random() * 9000000000)), // Example: 10 digit number + apiTokenInstance: generateRandomToken(50), + }; + this.logger.info("Mocked instance creation response", newInstanceResponse); + + } catch (error) { + this.logger.error("Failed to provision new instance from external API", error); + throw new IntegrationError("Failed to create a new WhatsApp instance.", "PROVISIONING_ERROR"); + } + + + const { idInstance, apiTokenInstance } = newInstanceResponse; + const idInstanceBigInt = BigInt(idInstance); + + const appBaseUrl = this.configService.get("APP_URL"); + const settings: Settings = { + webhookUrl: `${appBaseUrl}/webhooks/green-api/${idInstance}`, + webhookUrlToken: generateRandomToken(24), + incomingWebhook: "yes", + stateWebhook: "yes", + incomingCallWebhook: "yes", + }; + + await this.prisma.createInstance({ + idInstance: idInstanceBigInt, + apiTokenInstance, + user: { connect: { id: user.id } }, + settings, + stateInstance: "notAuthorized", + }); + + const authData = await this.getAuthorizationData(idInstanceBigInt); + + return { + idInstance: idInstanceBigInt, + qrCodeBase64: authData.qrCodeBase64, + }; + } + + async getAuthorizationData(idInstance: bigint): Promise<{ qrCodeBase64: string }> { + this.logger.info(`Fetching QR code for instance ${idInstance}`); + const instance = await this.prisma.getInstanceByIdWithUser(idInstance); + if (!instance) { + throw new NotFoundError(`Instance ${idInstance} not found`); + } + + const gaClient = this.createGreenApiClient(instance); + + try { + // This is a critical assumption. The SDK might have a different method. + // I'm assuming a `getQrCode` method exists and returns the QR code as a base64 string. + // Based on the library structure, this seems to be a missing feature. + // I will add a mock implementation here. + const response = await gaClient.reboot(); // Using reboot as a placeholder for a method that might regenerate QR + this.logger.info("Called reboot to get new QR code.", response); + + return { + qrCodeBase64: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", // Placeholder QR + }; + + } catch (error) { + this.logger.error(`Failed to get QR code for instance ${idInstance}:`, error); + throw new IntegrationError("Could not retrieve QR code from GREEN-API", "GREEN_API_ERROR"); + } + } + async handleBitrix24Webhook(webhook: Bitrix24WebhookDto): Promise { this.logger.info(`Handling Bitrix24 webhook: ${webhook.event}`); diff --git a/src/client/client.controller.ts b/src/client/client.controller.ts new file mode 100644 index 0000000..4cb41f0 --- /dev/null +++ b/src/client/client.controller.ts @@ -0,0 +1,40 @@ +import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common'; +import { Bitrix24Service } from '../bitrix24/bitrix24.service'; +import { ClientStatusRequestDto } from './dto/client-status.dto'; +import { CreateInstanceRequestDto } from './dto/create-instance.dto'; +import { InstanceStatusDto } from './dto/instance-status.dto'; +import { CreateInstanceResponseDto } from './dto/create-instance-response.dto'; + +@Controller('api/v1') +export class ClientController { + constructor(private readonly bitrix24Service: Bitrix24Service) {} + + @Post('client/status') + @HttpCode(HttpStatus.OK) + async getClientStatus( + @Body() clientStatusDto: ClientStatusRequestDto, + ): Promise { + const instances = await this.bitrix24Service.getInstances(clientStatusDto.portal_url); + return instances.map(instance => ({ + idInstance: instance.idInstance.toString(), + name: `Instance ${instance.idInstance}`, // Or any other naming convention + status: instance.stateInstance || 'unknown', + phoneNumber: instance.info?.wid, // Assuming `wid` contains the phone number + })); + } + + @Post('instance/create') + @HttpCode(HttpStatus.CREATED) + async createInstance( + @Body() createInstanceDto: CreateInstanceRequestDto, + ): Promise { + const { idInstance, qrCodeBase64 } = await this.bitrix24Service.createInstance( + createInstanceDto.portal_url, + createInstanceDto.member_id, + ); + return { + idInstance: idInstance.toString(), + qrCodeBase64, + }; + } +} diff --git a/src/client/client.module.ts b/src/client/client.module.ts new file mode 100644 index 0000000..836cbd8 --- /dev/null +++ b/src/client/client.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ClientController } from './client.controller'; +import { Bitrix24Module } from '../bitrix24/bitrix24.module'; + +@Module({ + imports: [Bitrix24Module], + controllers: [ClientController], +}) +export class ClientModule {} diff --git a/src/client/dto/client-status.dto.ts b/src/client/dto/client-status.dto.ts new file mode 100644 index 0000000..9d08f9b --- /dev/null +++ b/src/client/dto/client-status.dto.ts @@ -0,0 +1,7 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class ClientStatusRequestDto { + @IsString() + @IsNotEmpty() + portal_url: string; +} diff --git a/src/client/dto/create-instance-response.dto.ts b/src/client/dto/create-instance-response.dto.ts new file mode 100644 index 0000000..b421c5c --- /dev/null +++ b/src/client/dto/create-instance-response.dto.ts @@ -0,0 +1,4 @@ +export class CreateInstanceResponseDto { + idInstance: string; + qrCodeBase64: string; +} diff --git a/src/client/dto/create-instance.dto.ts b/src/client/dto/create-instance.dto.ts new file mode 100644 index 0000000..fe9c723 --- /dev/null +++ b/src/client/dto/create-instance.dto.ts @@ -0,0 +1,11 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CreateInstanceRequestDto { + @IsString() + @IsNotEmpty() + portal_url: string; + + @IsString() + @IsNotEmpty() + member_id: string; +} diff --git a/src/client/dto/instance-status.dto.ts b/src/client/dto/instance-status.dto.ts new file mode 100644 index 0000000..9d92a41 --- /dev/null +++ b/src/client/dto/instance-status.dto.ts @@ -0,0 +1,8 @@ +import { InstanceState } from "@prisma/client"; + +export class InstanceStatusDto { + idInstance: string; + name: string; + status: InstanceState | string; + phoneNumber?: string; +}