forked from Chatbot/v3-api
first commit v3 initialiazed a temporary repository for darmanet client
This commit is contained in:
202
src/chat-attachments/chat-attachments.controller.ts
Normal file
202
src/chat-attachments/chat-attachments.controller.ts
Normal 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"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user