first commit v3 initialiazed a temporary repository for darmanet client

This commit is contained in:
2026-09-08 16:04:01 +03:30
commit 5c7fd95052
216 changed files with 30050 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
import {
BadRequestException,
Controller,
Get,
Param,
Post,
Query,
Req,
UploadedFile,
UseGuards,
UseInterceptors,
StreamableFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { tmpdir } from 'node:os';
import { extname } from 'node:path';
import { randomUUID } from 'node:crypto';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiOperation,
ApiParam,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import type { Request } from 'express';
import { AdminGuard } from 'src/auth/guards/admin.guard';
import { AdminIdentity, CurrentIdentity } from 'src/common/decorators/Identity.decorator';
import { Permissions } from 'src/common/decorators/permission.decorator';
import { Permission } from 'src/common/types/permissions.catalog';
import { AdminDocument } from 'src/database/model/admin.model';
import { MAX_CHAT_VOICE_BYTES } from 'src/storage/storage.constants';
import { AttachmentSessionExpertGuard } from './attachment-session-expert.guard';
import { AttachmentSessionUserGuard } from './attachment-session-user.guard';
import { ChatAttachmentsService } from './chat-attachments.service';
const attachmentDiskStorage = diskStorage({
destination: (_req, _file, cb) => cb(null, tmpdir()),
filename: (_req, file, cb) => {
const raw = extname(file.originalname || '').toLowerCase().slice(0, 24);
const safe = /^\.[a-z0-9.]{1,20}$/.test(raw) ? raw : '';
cb(null, `chat-att-${randomUUID()}${safe}`);
},
});
@ApiTags('chat-attachments')
@Controller('chat/attachments')
@ApiBearerAuth()
export class ChatAttachmentsController {
constructor(private readonly attachments: ChatAttachmentsService) {}
private parseDurationSec(raw: unknown): number | undefined {
if (raw === undefined || raw === null || raw === '') return undefined;
const n = Number(raw);
return Number.isFinite(n) ? n : undefined;
}
@Post('user/:sessionId')
@ApiOperation({
summary:
'Upload chat attachment (voice FFmpeg→m4a, no image, no PDF). Then sendMessage with message=storageKey, presetMessageId, type matching fileType.',
})
@ApiParam({ name: 'sessionId' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['file', 'fileType'],
properties: {
file: { type: 'string', format: 'binary' },
fileType: {
type: 'string',
enum: ['voice'], //? 'image', 'document'
},
durationSec: { type: 'number', description: 'Voice: optional duration hint' },
},
},
})
@UseGuards(AttachmentSessionUserGuard)
@UseInterceptors(
FileInterceptor('file', {
storage: attachmentDiskStorage,
limits: { fileSize: MAX_CHAT_VOICE_BYTES },
}),
)
uploadUser(
@Param('sessionId') sessionId: string,
@UploadedFile() file: Express.Multer.File,
@Req() req: Request,
@CurrentIdentity() user: { _id?: unknown },
) {
if (!file) throw new BadRequestException('file_required');
if (!user?._id) throw new BadRequestException('user_token_required');
const fileType = this.attachments.parseFileType(
(req.body as Record<string, unknown>)?.fileType,
);
return this.attachments.uploadAttachment({
sessionId,
file,
uploaderId: String(user._id),
uploaderRole: 'User',
fileType,
durationSec: this.parseDurationSec(
(req.body as Record<string, unknown>)?.durationSec,
),
});
}
@Post('expert/:sessionId')
@ApiOperation({ summary: 'Upload chat attachment (expert JWT)' })
@ApiParam({ name: 'sessionId' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['file', 'fileType'],
properties: {
file: { type: 'string', format: 'binary' },
fileType: { type: 'string', enum: ['voice', 'image', 'document'] },
durationSec: { type: 'number' },
},
},
})
@UseGuards(AdminGuard, AttachmentSessionExpertGuard)
@Permissions(Permission.AttachmentsExpert)
@UseInterceptors(
FileInterceptor('file', {
storage: attachmentDiskStorage,
limits: { fileSize: MAX_CHAT_VOICE_BYTES },
}),
)
uploadExpert(
@Param('sessionId') sessionId: string,
@UploadedFile() file: Express.Multer.File,
@Req() req: Request,
@AdminIdentity() admin: AdminDocument,
) {
if (!file) throw new BadRequestException('file_required');
const fileType = this.attachments.parseFileType(
(req.body as Record<string, unknown>)?.fileType,
);
return this.attachments.uploadAttachment({
sessionId,
file,
uploaderId: String(admin._id),
uploaderRole: 'Expert',
fileType,
durationSec: this.parseDurationSec(
(req.body as Record<string, unknown>)?.durationSec,
),
});
}
@Get('user/:sessionId/stream')
@ApiOperation({
summary: 'Stream a chat attachment (private object). Query: key = full storageKey.',
})
@ApiParam({ name: 'sessionId' })
@ApiQuery({
name: 'key',
required: true,
description: 'Full object key (same as message text for attachment messages)',
example: 'chats/.../voice/....m4a',
})
@UseGuards(AttachmentSessionUserGuard)
async streamUser(
@Param('sessionId') sessionId: string,
@Query('key') storageKey: string,
): Promise<StreamableFile> {
const { stream, contentType } =
await this.attachments.streamPrivateAttachment({
sessionId,
storageKey,
});
return new StreamableFile(stream, {
type: contentType,
disposition: `inline; filename="attachment"`,
});
}
@Get('expert/:sessionId/stream')
@ApiOperation({ summary: 'Stream attachment (expert JWT)' })
@ApiParam({ name: 'sessionId' })
@ApiQuery({ name: 'key', required: true })
@UseGuards(AdminGuard, AttachmentSessionExpertGuard)
async streamExpert(
@Param('sessionId') sessionId: string,
@Query('key') storageKey: string,
): Promise<StreamableFile> {
const { stream, contentType } =
await this.attachments.streamPrivateAttachment({
sessionId,
storageKey,
});
return new StreamableFile(stream, {
type: contentType,
disposition: `inline; filename="attachment"`,
});
}
}