Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
124 changes: 124 additions & 0 deletions src/bitrix24/bitrix24.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
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<string>("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<string>("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<WebhookProcessResult> {
this.logger.info(`Handling Bitrix24 webhook: ${webhook.event}`);

Expand Down
40 changes: 40 additions & 0 deletions src/client/client.controller.ts
Original file line number Diff line number Diff line change
@@ -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<InstanceStatusDto[]> {
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<CreateInstanceResponseDto> {
const { idInstance, qrCodeBase64 } = await this.bitrix24Service.createInstance(
createInstanceDto.portal_url,
createInstanceDto.member_id,
);
return {
idInstance: idInstance.toString(),
qrCodeBase64,
};
}
}
9 changes: 9 additions & 0 deletions src/client/client.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
7 changes: 7 additions & 0 deletions src/client/dto/client-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsString, IsNotEmpty } from 'class-validator';

export class ClientStatusRequestDto {
@IsString()
@IsNotEmpty()
portal_url: string;
}
4 changes: 4 additions & 0 deletions src/client/dto/create-instance-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class CreateInstanceResponseDto {
idInstance: string;
qrCodeBase64: string;
}
11 changes: 11 additions & 0 deletions src/client/dto/create-instance.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsString, IsNotEmpty } from 'class-validator';

export class CreateInstanceRequestDto {
@IsString()
@IsNotEmpty()
portal_url: string;

@IsString()
@IsNotEmpty()
member_id: string;
}
8 changes: 8 additions & 0 deletions src/client/dto/instance-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { InstanceState } from "@prisma/client";

export class InstanceStatusDto {
idInstance: string;
name: string;
status: InstanceState | string;
phoneNumber?: string;
}