From f42280846c471cb0961b45b81f173b0ad9822c8d Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Wed, 23 Jul 2025 17:00:47 +0330 Subject: [PATCH] chore: add ai service --- package.json | 1 + pnpm-lock.yaml | 17 + src/app.module.ts | 2 + src/common/constants/index.ts | 1 + src/common/enums/message.enum.ts | 6 + src/configs/ai.config.ts | 152 +++++ src/configs/sms.config.ts | 49 -- .../ai-assistant/DTO/summarize-email.dto.ts | 26 + .../DTO/write-email-assistance.dto.ts | 146 +++++ src/modules/ai-assistant/README.md | 217 +++++++ .../ai-assistant/ai-assistant.controller.ts | 35 ++ .../ai-assistant/ai-assistant.module.ts | 15 + src/modules/ai-assistant/enums/ai.enum.ts | 32 ++ .../interfaces/ai-service.interface.ts | 50 ++ .../services/ai-assistant.service.ts | 162 ++++++ .../ai-assistant/services/ai.service.ts | 166 ++++++ src/modules/utils/interfaces/ISms.ts | 51 -- src/modules/utils/services/sms.service.ts | 542 ------------------ src/modules/utils/utils.module.ts | 12 +- 19 files changed, 1030 insertions(+), 652 deletions(-) create mode 100644 src/configs/ai.config.ts delete mode 100755 src/configs/sms.config.ts create mode 100644 src/modules/ai-assistant/DTO/summarize-email.dto.ts create mode 100644 src/modules/ai-assistant/DTO/write-email-assistance.dto.ts create mode 100644 src/modules/ai-assistant/README.md create mode 100644 src/modules/ai-assistant/ai-assistant.controller.ts create mode 100644 src/modules/ai-assistant/ai-assistant.module.ts create mode 100644 src/modules/ai-assistant/enums/ai.enum.ts create mode 100644 src/modules/ai-assistant/interfaces/ai-service.interface.ts create mode 100644 src/modules/ai-assistant/services/ai-assistant.service.ts create mode 100644 src/modules/ai-assistant/services/ai.service.ts delete mode 100755 src/modules/utils/interfaces/ISms.ts delete mode 100755 src/modules/utils/services/sms.service.ts diff --git a/package.json b/package.json index 321eaab..94adac4 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "dotenv": "^16.6.1", "fastify": "^5.4.0", "nodemailer": "^7.0.4", + "openai": "^5.10.2", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e67699d..f5aa69a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: nodemailer: specifier: ^7.0.4 version: 7.0.4 + openai: + specifier: ^5.10.2 + version: 5.10.2 passport-jwt: specifier: ^4.0.1 version: 4.0.1 @@ -4594,6 +4597,18 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + openai@5.10.2: + resolution: {integrity: sha512-n+vi74LzHtvlKcDPn9aApgELGiu5CwhaLG40zxLTlFQdoSJCLACORIPC2uVQ3JEYAbqapM+XyRKFy2Thej7bIw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -11232,6 +11247,8 @@ snapshots: is-wsl: 2.2.0 optional: true + openai@5.10.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/src/app.module.ts b/src/app.module.ts index a6dc28f..f9b9a50 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { jwtConfig } from "./configs/jwt.config"; import { databaseConfig } from "./configs/mikro-orm.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; +import { AiAssistantModule } from "./modules/ai-assistant/ai-assistant.module"; import { AuthModule } from "./modules/auth/auth.module"; import { DomainsModule } from "./modules/domains/domains.module"; import { EmailModule } from "./modules/email/email.module"; @@ -37,6 +38,7 @@ import { UtilsModule } from "./modules/utils/utils.module"; ThrottlerModule.forRootAsync(rateLimitConfig()), MikroOrmModule.forRootAsync(databaseConfig), UtilsModule, + AiAssistantModule, AuthModule, UsersModule, DomainsModule, diff --git a/src/common/constants/index.ts b/src/common/constants/index.ts index dc54905..7ca1d08 100755 --- a/src/common/constants/index.ts +++ b/src/common/constants/index.ts @@ -1,4 +1,5 @@ // export const AUTH_THROTTLE = "AUTH_THROTTLE"; +export const AI_CONFIG = "AI_CONFIG"; export const AUTH_THROTTLE_TTL = 1 * 60 * 1000; export const AUTH_THROTTLE_LIMIT = 5; export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000; diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index ec992cc..ce9f779 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -558,3 +558,9 @@ export const enum SettingMessageEnum { EMAIL_MUST_BE_BOOLEAN = "تنظیمات ارسال ایمیل باید یک boolean باشد", NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی آن را ندارید", } + +export const enum AiMessage { + NO_RESPONSE_FROM_AI_MODEL = "خطا در دریافت پاسخ از مدل AI", + MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید", + EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست", +} diff --git a/src/configs/ai.config.ts b/src/configs/ai.config.ts new file mode 100644 index 0000000..50a5623 --- /dev/null +++ b/src/configs/ai.config.ts @@ -0,0 +1,152 @@ +import { FactoryProvider } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; + +import { AI_CONFIG } from "../common/constants"; + +export interface AIConfig { + baseUrl: string; + apiKey: string; + defaultModel: string; + temperature: number; + maxTokens: number; + timeout: number; + farsiPrompts: FarsiPrompts; +} + +export interface FarsiPrompts { + summarization: { + systemPrompt: string; + promptTemplate: { + intro: string; + subjectLabel: string; + fromLabel: string; + toLabel: string; + dateLabel: string; + contentLabel: string; + attachmentsLabel: string; + analysisRequirements: string; + summaryLengthLabel: string; + toneLabel: string; + }; + }; + writingAssistance: { + systemPrompt: string; + promptTemplate: { + intro: string; + mainMessageLabel: string; + emailDetailsLabel: string; + purposeLabel: string; + toneLabel: string; + lengthLabel: string; + keyPointsLabel: string; + contextLabel: string; + originalSubjectLabel: string; + originalSenderLabel: string; + originalContentLabel: string; + additionalInstructionsLabel: string; + }; + }; +} + +const FARSI_PROMPTS: FarsiPrompts = { + summarization: { + systemPrompt: `شما یک تحلیلگر و خلاصه‌کننده ماهر ایمیل هستید. وظیفه شما تحلیل و خلاصه‌سازی ایمیل‌ها به صورت حرفه‌ای و دقیق است. + +الزامات: +- محتوای ایمیل را تحلیل کرده و خلاصه‌ای جامع ارائه دهید +- نکات کلیدی که در ایمیل ذکر شده‌اند را استخراج کنید +- بر اساس محتوای ایمیل، اقدامات قابل انجام پیشنهاد دهید +- سطح اولویت (کم، متوسط، زیاد) را بر اساس شاخص‌های اضطراری تعیین کنید +- احساسات (مثبت، خنثی، منفی) ایمیل را تحلیل کنید +- از لحن {tone} در تحلیل خود استفاده کنید +- سطح جزئیات {length} ارائه دهید +- به زبان {language} پاسخ دهید + +فرمت پاسخ: شیء JSON با ساختار زیر: +{ + "summary": "خلاصه مختصر ایمیل", + "keyPoints": ["نکته کلیدی ۱", "نکته کلیدی ۲", ...], + "suggestedActions": ["اقدام ۱", "اقدام ۲", ...], + "priority": "low|medium|high", + "sentiment": "positive|neutral|negative" +}`, + promptTemplate: { + intro: "لطفاً این ایمیل را تحلیل و خلاصه کنید:", + subjectLabel: "موضوع:", + fromLabel: "از:", + toLabel: "به:", + dateLabel: "تاریخ:", + contentLabel: "محتوا:", + attachmentsLabel: "پیوست‌ها:", + analysisRequirements: "الزامات تحلیل:", + summaryLengthLabel: "- طول خلاصه:", + toneLabel: "- لحن:", + }, + }, + writingAssistance: { + systemPrompt: `شما یک دستیار ماهر نگارش ایمیل هستید. وظیفه شما کمک به کاربران برای نوشتن ایمیل‌های حرفه‌ای و مؤثر است. + +الزامات: +- بر اساس قصد و زمینه کاربر، محتوای ایمیل تولید کنید +- در تمام ایمیل از لحن {tone} استفاده کنید +- ایمیل با طول {length} بسازید +- هدف: {purpose} +- نکات و بهبودهای مفید ارائه دهید +- در صورت امکان، نسخه‌های جایگزین تولید کنید +- به زبان {language} پاسخ دهید + +فرمت پاسخ: شیء JSON با ساختار زیر: +{ + "subject": "موضوع پیشنهادی ایمیل", + "content": "محتوای کامل ایمیل", + "alternatives": ["نسخه جایگزین ۱", "نسخه جایگزین ۲", ...], + "tips": ["نکته نگارش ۱", "نکته نگارش ۲", ...], + "improvements": ["پیشنهاد بهبود ۱", "پیشنهاد بهبود ۲", ...] +}`, + promptTemplate: { + intro: "لطفاً در نوشتن ایمیل با الزامات زیر به من کمک کنید:", + mainMessageLabel: "پیام/قصد اصلی:", + emailDetailsLabel: "جزئیات ایمیل:", + purposeLabel: "- هدف:", + toneLabel: "- لحن:", + lengthLabel: "- طول:", + keyPointsLabel: "نکات کلیدی برای درج:", + contextLabel: "زمینه از ایمیل اصلی:", + originalSubjectLabel: "- موضوع اصلی:", + originalSenderLabel: "- فرستنده اصلی:", + originalContentLabel: "- محتوای اصلی:", + additionalInstructionsLabel: "دستورالعمل‌های اضافی:", + }, + }, +}; + +export const aiConfigProvider: FactoryProvider = { + provide: AI_CONFIG, + useFactory: (configService: ConfigService): AIConfig => ({ + baseUrl: configService.get("AI_BASE_URL", "https://api.openai.com/v1"), + apiKey: configService.get("AI_API_KEY", ""), + defaultModel: configService.get("AI_DEFAULT_MODEL", "gpt-3.5-turbo"), + temperature: configService.get("AI_TEMPERATURE", 0.7), + maxTokens: configService.get("AI_MAX_TOKENS", 2000), + timeout: configService.get("AI_TIMEOUT", 30000), + farsiPrompts: FARSI_PROMPTS, + }), + inject: [ConfigService], +}; + +// Available models for different providers +export const AI_MODELS = { + OPENAI: { + GPT_3_5_TURBO: "gpt-3.5-turbo", + GPT_4: "gpt-4", + GPT_4_TURBO: "gpt-4-turbo-preview", + }, + GEMINI: { + GEMINI_2_0_FLASH: "google/gemini-2.0-flash-001", + GEMINI_2_5_PRO: "google/gemini-2.5-pro-preview", + GEMINI_2_5_FLASH: "google/gemini-2.5-flash-preview", + GEMINI_2_0_FLASH_LITE: "google/gemini-2.0-flash-lite-001", + GEMINI_FLASH_1_5: "google/gemini-flash-1.5", + GEMINI_FLASH_1_5_8B: "google/gemini-flash-1.5-8b", + }, +} as const; diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts deleted file mode 100755 index 50fbb70..0000000 --- a/src/configs/sms.config.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ConfigService } from "@nestjs/config"; - -export function smsConfigs() { - return { - inject: [ConfigService], - useFactory(configService: ConfigService) { - return { - API_URL: configService.getOrThrow("SMS_API_URL"), - API_KEY: configService.getOrThrow("SMS_API_KEY"), - SMS_PATTERN_OTP: configService.getOrThrow("SMS_PATTERN_OTP"), - SMS_PATTERN_INVOICE: configService.getOrThrow("SMS_PATTERN_INVOICE"), - SMS_PATTERN_LOGIN: configService.getOrThrow("SMS_PATTERN_LOGIN"), - SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow("SMS_PATTERN_INVOICE_CREATED"), - SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow("SMS_PATTERN_ANNOUNCEMENT"), - SMS_PATTERN_TICKET_CREATED: configService.getOrThrow("SMS_PATTERN_TICKET_CREATED"), - SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"), - SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow("SMS_PATTERN_INVOICE_APPROVED"), - SMS_PATTERN_INVOICE_PAID: configService.getOrThrow("SMS_PATTERN_INVOICE_PAID"), - SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow("SMS_PATTERN_RECURRING_INVOICE_DRAFT"), - SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow("SMS_PATTERN_SUBSCRIPTION_CANCELLED"), - SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow("SMS_PATTERN_INVOICE_REMINDER"), - SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow("SMS_PATTERN_INVOICE_OVERDUE"), - SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow("SMS_PATTERN_PAYMENT_REMINDER"), - SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow("SMS_PATTERN_PAYMENT_CANCELLATION"), - }; - }, - }; -} - -export interface ISmsConfigs { - API_URL: string; - API_KEY: string; - SMS_PATTERN_OTP: string; - SMS_PATTERN_INVOICE: string; - SMS_PATTERN_LOGIN: string; - SMS_PATTERN_INVOICE_CREATED: string; - SMS_PATTERN_ANNOUNCEMENT: string; - SMS_PATTERN_TICKET_CREATED: string; - SMS_PATTERN_TICKET_ANSWERED: string; - SMS_PATTERN_TICKET_ASSIGNED_ADMIN: string; - SMS_PATTERN_INVOICE_APPROVED: string; - SMS_PATTERN_INVOICE_PAID: string; - SMS_PATTERN_RECURRING_INVOICE_DRAFT: string; - SMS_PATTERN_SUBSCRIPTION_CANCELLED: string; - SMS_PATTERN_INVOICE_REMINDER: string; - SMS_PATTERN_INVOICE_OVERDUE: string; - SMS_PATTERN_PAYMENT_REMINDER: string; - SMS_PATTERN_PAYMENT_CANCELLATION: string; -} diff --git a/src/modules/ai-assistant/DTO/summarize-email.dto.ts b/src/modules/ai-assistant/DTO/summarize-email.dto.ts new file mode 100644 index 0000000..efdba82 --- /dev/null +++ b/src/modules/ai-assistant/DTO/summarize-email.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; + +import { SummaryLength, SummaryTone } from "../enums/ai.enum"; + +export class SummarizeEmailDto { + @IsNotEmpty({ message: "Message ID is required" }) + @IsNumber({}, { message: "Message ID must be a number" }) + @ApiProperty({ description: "The message ID to fetch and summarize from the mail server", example: 12345 }) + messageId: number; + + @IsNotEmpty({ message: "Mailbox ID is required" }) + @IsString({ message: "Mailbox ID must be a string" }) + @ApiProperty({ description: "The mailbox ID where the message is located", example: "507f1f77bcf86cd799439012" }) + mailbox: string; + + @IsOptional() + @IsEnum(SummaryLength, { message: "Summary length must be one of: short, medium, detailed" }) + @ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLength, default: SummaryLength.MEDIUM }) + length?: SummaryLength; + + @IsOptional() + @IsEnum(SummaryTone, { message: "Summary tone must be one of: formal, casual, professional" }) + @ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryTone, default: SummaryTone.PROFESSIONAL }) + tone?: SummaryTone; +} diff --git a/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts b/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts new file mode 100644 index 0000000..ef4530a --- /dev/null +++ b/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts @@ -0,0 +1,146 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator"; + +import { EmailLength, EmailPurpose, WritingTone } from "../enums/ai.enum"; + +export class EmailContextDto { + @IsOptional() + @IsString({ message: "Original email content must be a string" }) + @ApiPropertyOptional({ + description: "Original email content (for replies/forwards)", + example: "Hi, I need to discuss the project timeline...", + }) + originalContent?: string; + + @IsOptional() + @IsString({ message: "Original sender must be a string" }) + @ApiPropertyOptional({ + description: "Original sender (for replies/forwards)", + example: "john.doe@company.com", + }) + originalSender?: string; + + @IsOptional() + @IsString({ message: "Original subject must be a string" }) + @ApiPropertyOptional({ + description: "Original subject (for replies/forwards)", + example: "Project Timeline Discussion", + }) + originalSubject?: string; +} + +export class WriteEmailAssistanceDto { + @IsNotEmpty({ message: "Email purpose is required" }) + @IsEnum(EmailPurpose, { message: "Email purpose must be one of: reply, forward, new, follow_up" }) + @ApiProperty({ + description: "Purpose of the email", + enum: EmailPurpose, + example: EmailPurpose.REPLY, + }) + purpose: EmailPurpose; + + @IsNotEmpty({ message: "Main message or intent is required" }) + @IsString({ message: "Main message must be a string" }) + @ApiProperty({ + description: "Main message or intent of the email", + example: "I want to schedule a meeting to discuss the project timeline and address any concerns.", + }) + mainMessage: string; + + @IsOptional() + @IsString({ message: "Recipient information must be a string" }) + @ApiPropertyOptional({ + description: "Recipient information (optional)", + example: "john.doe@company.com", + }) + recipient?: string; + + @IsOptional() + @IsEnum(WritingTone, { message: "Writing tone must be one of: formal, casual, professional, friendly, urgent" }) + @ApiPropertyOptional({ + description: "Desired tone of the email", + enum: WritingTone, + default: WritingTone.PROFESSIONAL, + }) + tone?: WritingTone; + + @IsOptional() + @IsEnum(EmailLength, { message: "Email length must be one of: short, medium, long" }) + @ApiPropertyOptional({ + description: "Desired length of the email", + enum: EmailLength, + default: EmailLength.MEDIUM, + }) + length?: EmailLength; + + @IsOptional() + @IsArray({ message: "Key points must be an array" }) + @IsString({ each: true, message: "Each key point must be a string" }) + @ApiPropertyOptional({ + description: "Key points to include in the email", + type: [String], + example: ["Discuss project timeline", "Address resource allocation", "Set next meeting date"], + }) + keyPoints?: string[]; + + @IsOptional() + @ValidateNested() + @Type(() => EmailContextDto) + @ApiPropertyOptional({ + description: "Context from original email (for replies/forwards)", + type: EmailContextDto, + }) + context?: EmailContextDto; + + @IsOptional() + @IsString({ message: "Additional instructions must be a string" }) + @ApiPropertyOptional({ + description: "Additional instructions or preferences", + example: "Please keep it concise and mention the deadline", + }) + additionalInstructions?: string; +} + +export class EmailSuggestionDto { + @ApiProperty({ + description: "Suggested email subject", + example: "Re: Project Timeline Discussion - Meeting Request", + }) + subject: string; + + @ApiProperty({ + description: "Generated email content", + example: "Dear John,\n\nThank you for your email regarding the project timeline...", + }) + content: string; + + @ApiProperty({ + description: "Alternative variations of the email", + type: [String], + example: ["More formal version", "Shorter version", "More detailed version"], + }) + alternatives: string[]; +} + +export class EmailWritingResponseDto { + @ApiProperty({ + description: "Primary email suggestion", + type: EmailSuggestionDto, + }) + suggestion: EmailSuggestionDto; + + @ApiProperty({ + description: "Writing tips and recommendations", + type: [String], + example: ["Consider adding a specific deadline", "You might want to include meeting agenda", "Don't forget to CC relevant stakeholders"], + }) + tips: string[]; + + @ApiProperty({ + description: "Suggested improvements for the original request", + type: [String], + example: ["Consider being more specific about the timeline", "Add context about project urgency"], + }) + improvements: string[]; +} diff --git a/src/modules/ai-assistant/README.md b/src/modules/ai-assistant/README.md new file mode 100644 index 0000000..ff3486b --- /dev/null +++ b/src/modules/ai-assistant/README.md @@ -0,0 +1,217 @@ +# AI Assistant Module + +The AI assistant module provides email summarization and writing assistance functionality using OpenAI/Google Gemini models through the [Liara AI platform](https://docs.liara.ir/ai/google-gemini/). + +## Features + +- **Email Summarization**: Analyze emails and extract key points, suggested actions, priority levels, and sentiment analysis +- **Email Writing Assistance**: Generate professional emails with AI assistance based on context and purpose +- **Multi-Model Support**: Compatible with OpenAI GPT models and Google Gemini models +- **Flexible Configuration**: Configurable tone, length, and purpose for different use cases + +## Setup + +### 1. Install Dependencies + +```bash +pnpm add openai +``` + +### 2. Environment Variables + +Add the following environment variables to your `.env` file: + +```env +# AI Service Configuration +AI_BASE_URL=https://api.openai.com/v1 +AI_API_KEY=your_api_key_here +AI_DEFAULT_MODEL=gpt-3.5-turbo +AI_TEMPERATURE=0.7 +AI_MAX_TOKENS=2000 +AI_TIMEOUT=30000 +``` + +### For Liara AI (Google Gemini): + +```env +# Liara AI Configuration +AI_BASE_URL=https://api.liara.ir/v1 +AI_API_KEY=your_liara_api_key +AI_DEFAULT_MODEL=google/gemini-2.0-flash-001 +AI_TEMPERATURE=0.7 +AI_MAX_TOKENS=2000 +AI_TIMEOUT=30000 +``` + +### Available Models + +#### OpenAI Models: + +- `gpt-3.5-turbo` +- `gpt-4` +- `gpt-4-turbo-preview` + +#### Google Gemini Models (via Liara): + +- `google/gemini-2.0-flash-001` +- `google/gemini-2.5-pro-preview` +- `google/gemini-2.5-flash-preview` +- `google/gemini-2.0-flash-lite-001` +- `google/gemini-flash-1.5` +- `google/gemini-flash-1.5-8b` + +## API Endpoints + +### Email Summarization + +```http +POST /ai-assistant/summarize-email +Content-Type: application/json +x-business-id: your-business-id +Authorization: Bearer your-token + +{ + "messageId": 12345, + "mailbox": "507f1f77bcf86cd799439012", + "length": "medium", + "tone": "professional" +} +``` + +**Response:** + +```json +{ + "summary": "John is requesting a meeting to discuss the upcoming project timeline and deliverables.", + "keyPoints": ["Project timeline discussion", "Meeting request", "Deliverables review"], + "suggestedActions": ["Schedule meeting with John", "Prepare project timeline document", "Review current deliverables"], + "priority": "medium", + "sentiment": "neutral" +} +``` + +### Email Writing Assistance + +```http +POST /ai-assistant/write-email +Content-Type: application/json +x-business-id: your-business-id +Authorization: Bearer your-token + +{ + "purpose": "reply", + "mainMessage": "I want to schedule a meeting to discuss the project timeline", + "tone": "professional", + "length": "medium", + "keyPoints": [ + "Discuss timeline", + "Address concerns" + ], + "context": { + "originalSubject": "Project Update", + "originalSender": "john@example.com", + "originalContent": "Hi, I need to discuss the project timeline..." + }, + "additionalInstructions": "Please keep it concise and mention the deadline" +} +``` + +**Response:** + +```json +{ + "suggestion": { + "subject": "Re: Project Update - Meeting Request", + "content": "Hello,\n\nThank you for your email regarding the project timeline...", + "alternatives": ["More concise version available", "More detailed version available", "Alternative tone available"] + }, + "tips": ["Consider adding a specific deadline", "You might want to include meeting agenda"], + "improvements": ["Consider being more specific about the timeline", "Add context about project urgency"] +} +``` + +## Configuration Options + +### Summarization Options + +- **Length**: `short`, `medium`, `detailed` +- **Tone**: `formal`, `casual`, `professional` + +### Writing Assistance Options + +- **Purpose**: `reply`, `forward`, `new`, `follow_up` +- **Tone**: `formal`, `casual`, `professional`, `friendly`, `urgent` +- **Length**: `short`, `medium`, `long` + +## Architecture + +The AI assistant module consists of: + +1. **AiService**: Core service that communicates with AI models +2. **AiAssistantService**: Business logic layer that handles email processing +3. **AiAssistantController**: API endpoints for client interactions +4. **DTOs**: Data transfer objects for request/response validation +5. **Interfaces**: Type definitions for AI service interactions + +## Integration with Liara AI + +This module is designed to work seamlessly with [Liara's AI platform](https://docs.liara.ir/ai/google-gemini/), which provides: + +- OpenAI-compatible API +- Multiple AI model support +- Persian language support +- Scalable infrastructure + +## Error Handling + +The module includes comprehensive error handling: + +- **Message Not Found**: Returns 404 when email message doesn't exist +- **Empty Content**: Returns 400 when email content is empty +- **AI Service Errors**: Logs and re-throws AI service errors +- **Authentication**: Validates user permissions and business context + +## Security + +- All endpoints require authentication +- Business context validation +- Rate limiting (inherited from global configuration) +- Input validation and sanitization + +## Usage Example + +```typescript +// In your service +import { AiAssistantService } from "./modules/ai-assistant/services/ai-assistant.service"; + +@Injectable() +export class EmailProcessorService { + constructor(private readonly aiAssistant: AiAssistantService) {} + + async processIncomingEmail(messageId: number, mailboxId: string, userId: string) { + const summary = await this.aiAssistant.summarizeEmail( + { + messageId, + mailbox: mailboxId, + length: "medium", + tone: "professional", + }, + userId, + ); + + // Process the summary... + return summary; + } +} +``` + +## Contributing + +When adding new AI features: + +1. Update the appropriate interface in `interfaces/ai-service.interface.ts` +2. Implement the logic in `AiService` +3. Add business logic in `AiAssistantService` +4. Create API endpoints in `AiAssistantController` +5. Add proper validation DTOs +6. Update this documentation diff --git a/src/modules/ai-assistant/ai-assistant.controller.ts b/src/modules/ai-assistant/ai-assistant.controller.ts new file mode 100644 index 0000000..a4ad2b2 --- /dev/null +++ b/src/modules/ai-assistant/ai-assistant.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Post } from "@nestjs/common"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; + +import { SummarizeEmailDto } from "./DTO/summarize-email.dto"; +import { WriteEmailAssistanceDto } from "./DTO/write-email-assistance.dto"; +import { AiAssistantService } from "./services/ai-assistant.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; + +@Controller("ai-assistant") +// @ApiHeader({ name: "x-business-id", description: "Business ID" }) +// @UseInterceptors(BusinessInterceptor) +@AuthGuards() +export class AiAssistantController { + constructor(private readonly aiAssistantService: AiAssistantService) {} + + @Post("summarize-email") + @ApiOperation({ summary: "Summarize an email using AI" }) + @ApiResponse({ status: 201, description: "Email successfully summarized" }) + @ApiResponse({ status: 400, description: "Bad request - invalid message ID or mailbox" }) + @ApiResponse({ status: 404, description: "Message not found or access denied" }) + @ApiResponse({ status: 401, description: "Unauthorized - invalid authentication" }) + async summarizeEmail(@Body() summarizeDto: SummarizeEmailDto, @UserDec("wildduckUserId") wildduckUserId: string) { + return this.aiAssistantService.summarizeEmail(summarizeDto, wildduckUserId); + } + + @Post("write-email") + @ApiOperation({ summary: "Get AI assistance for writing emails" }) + @ApiResponse({ status: 201, description: "Email writing assistance generated successfully" }) + @ApiResponse({ status: 400, description: "Bad request - invalid input data" }) + @ApiResponse({ status: 401, description: "Unauthorized - invalid authentication" }) + async assistEmailWriting(@Body() writeDto: WriteEmailAssistanceDto, @UserDec("wildduckUserId") wildduckUserId: string) { + return this.aiAssistantService.assistEmailWriting(writeDto, wildduckUserId); + } +} diff --git a/src/modules/ai-assistant/ai-assistant.module.ts b/src/modules/ai-assistant/ai-assistant.module.ts new file mode 100644 index 0000000..77b7a8c --- /dev/null +++ b/src/modules/ai-assistant/ai-assistant.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; + +import { AiAssistantController } from "./ai-assistant.controller"; +import { AiAssistantService } from "./services/ai-assistant.service"; +import { AiService } from "./services/ai.service"; +import { aiConfigProvider } from "../../configs/ai.config"; +import { MailServerModule } from "../mail-server/mail-server.module"; + +@Module({ + imports: [MailServerModule], + controllers: [AiAssistantController], + providers: [AiAssistantService, AiService, aiConfigProvider], + exports: [AiAssistantService, AiService], +}) +export class AiAssistantModule {} diff --git a/src/modules/ai-assistant/enums/ai.enum.ts b/src/modules/ai-assistant/enums/ai.enum.ts new file mode 100644 index 0000000..2312835 --- /dev/null +++ b/src/modules/ai-assistant/enums/ai.enum.ts @@ -0,0 +1,32 @@ +export enum SummaryLength { + SHORT = "short", + MEDIUM = "medium", + DETAILED = "detailed", +} + +export enum SummaryTone { + FORMAL = "formal", + CASUAL = "casual", + PROFESSIONAL = "professional", +} + +export enum EmailPurpose { + REPLY = "reply", + FORWARD = "forward", + NEW = "new", + FOLLOW_UP = "follow_up", +} + +export enum WritingTone { + FORMAL = "formal", + CASUAL = "casual", + PROFESSIONAL = "professional", + FRIENDLY = "friendly", + URGENT = "urgent", +} + +export enum EmailLength { + SHORT = "short", + MEDIUM = "medium", + LONG = "long", +} diff --git a/src/modules/ai-assistant/interfaces/ai-service.interface.ts b/src/modules/ai-assistant/interfaces/ai-service.interface.ts new file mode 100644 index 0000000..4dc27e8 --- /dev/null +++ b/src/modules/ai-assistant/interfaces/ai-service.interface.ts @@ -0,0 +1,50 @@ +export interface EmailData { + subject: string; + content: string; + htmlContent?: string; + from: string; + to: string[]; + date: Date; + attachments?: unknown[]; +} + +export interface SummarizationOptions { + length: "short" | "medium" | "detailed"; + tone: "formal" | "casual" | "professional"; + language?: "Persian" | "English"; +} + +export interface WritingAssistanceOptions { + purpose: "reply" | "forward" | "new" | "follow_up"; + tone: "formal" | "casual" | "professional" | "friendly" | "urgent"; + length: "short" | "medium" | "long"; + keyPoints?: string[]; + context?: { + originalContent?: string; + originalSender?: string; + originalSubject?: string; + }; + additionalInstructions?: string; + language?: "Persian" | "English"; +} + +export interface AISummaryResponse { + summary: string; + keyPoints: string[]; + suggestedActions: string[]; + priority: "low" | "medium" | "high"; + sentiment: "positive" | "neutral" | "negative"; +} + +export interface AIWritingResponse { + subject: string; + content: string; + alternatives: string[]; + tips: string[]; + improvements: string[]; +} + +export interface AIServiceInterface { + summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise; + assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise; +} diff --git a/src/modules/ai-assistant/services/ai-assistant.service.ts b/src/modules/ai-assistant/services/ai-assistant.service.ts new file mode 100644 index 0000000..c3fd9a2 --- /dev/null +++ b/src/modules/ai-assistant/services/ai-assistant.service.ts @@ -0,0 +1,162 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; + +import { AiService } from "./ai.service"; +import { AiMessage } from "../../../common/enums/message.enum"; +import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface"; +import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service"; +import { SummarizeEmailDto } from "../DTO/summarize-email.dto"; +import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto"; +import { SummaryLength, SummaryTone } from "../enums/ai.enum"; +import { EmailData, SummarizationOptions, WritingAssistanceOptions } from "../interfaces/ai-service.interface"; + +@Injectable() +export class AiAssistantService { + constructor( + private readonly messagesService: WildDuckMessagesService, + private readonly aiService: AiService, + ) {} + + //************************************************* */ + async summarizeEmail(summarizeDto: SummarizeEmailDto, userId: string) { + const message = await firstValueFrom(this.messagesService.getMessage(userId, summarizeDto.mailbox, summarizeDto.messageId)); + + if (!message) throw new NotFoundException(AiMessage.MESSAGE_NOT_FOUND_OR_ACCESS_DENIED); + + const emailData = this.extractEmailData(message); + + const summary = await this.generateEmailSummary(emailData, summarizeDto.length, summarizeDto.tone); + + return summary; + } + + //************************************************* */ + async assistEmailWriting(writeAssistanceDto: WriteEmailAssistanceDto, userId: string) { + const assistance = await this.generateEmailWritingAssistance(writeAssistanceDto, userId); + + return assistance; + } + + //************************************************* */ + private extractEmailData(message: MessageDetails) { + return { + subject: message.subject, + content: this.extractTextContent(message), + htmlContent: this.extractHtmlContent(message), + from: this.extractSenderInfo(message), + to: this.extractRecipientInfo(message), + date: message.date || new Date(), + attachments: message.attachments || [], + }; + } + + //************************************************* */ + private extractTextContent(message: MessageDetails): string { + if (message.text) { + return typeof message.text === "string" ? message.text : message.text || ""; + } + + if (message.html) { + return this.stripHtmlTags(typeof message.html === "string" ? message.html : message.html[0] || ""); + } + + return ""; + } + + //************************************************* */ + private extractHtmlContent(message: MessageDetails): string { + if (message.html) { + return typeof message.html === "string" ? message.html : message.html[0] || ""; + } + return ""; + } + + //************************************************* */ + private extractSenderInfo(message: MessageDetails): string { + if (message.from && message.from.address) { + return message.from.name ? `${message.from.name} <${message.from.address}>` : message.from.address; + } + return ""; + } + + //************************************************* */ + private extractRecipientInfo(message: MessageDetails): string[] { + const recipients: string[] = []; + + if (message.to && Array.isArray(message.to)) { + recipients.push(...message.to.map((recipient) => (recipient.name ? `${recipient.name} <${recipient.address}>` : recipient.address))); + } + + return recipients; + } + + //************************************************* */ + private stripHtmlTags(html: string): string { + return html + .replace(/<[^>]*>/g, "") + .replace(/ /g, " ") + .trim(); + } + + //************************************************* */ + private async generateEmailSummary(emailData: any, length: SummaryLength = SummaryLength.MEDIUM, _tone: SummaryTone = SummaryTone.PROFESSIONAL) { + const content = emailData.content || emailData.htmlContent; + + if (!content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED); + + const aiEmailData: EmailData = { + subject: emailData.subject, + content: emailData.content, + htmlContent: emailData.htmlContent, + from: emailData.from, + to: emailData.to, + date: emailData.date, + attachments: emailData.attachments, + }; + + const summarizationOptions: SummarizationOptions = { + length: length.toLowerCase() as "short" | "medium" | "detailed", + tone: _tone.toLowerCase() as "formal" | "casual" | "professional", + }; + + const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions); + + return { + summary: aiResponse.summary, + keyPoints: aiResponse.keyPoints, + suggestedActions: aiResponse.suggestedActions, + priority: aiResponse.priority, + sentiment: aiResponse.sentiment, + }; + } + + //************************************************* */ + private async generateEmailWritingAssistance(writeAssistanceDto: WriteEmailAssistanceDto, _userId: string) { + const writingOptions: WritingAssistanceOptions = { + purpose: writeAssistanceDto.purpose.toLowerCase() as "reply" | "forward" | "new" | "follow_up", + tone: (writeAssistanceDto.tone?.toLowerCase() as "formal" | "casual" | "professional" | "friendly" | "urgent") || "professional", + length: (writeAssistanceDto.length?.toLowerCase() as "short" | "medium" | "long") || "medium", + keyPoints: writeAssistanceDto.keyPoints, + context: writeAssistanceDto.context + ? { + originalContent: writeAssistanceDto.context.originalContent, + originalSender: writeAssistanceDto.context.originalSender, + originalSubject: writeAssistanceDto.context.originalSubject, + } + : undefined, + additionalInstructions: writeAssistanceDto.additionalInstructions, + }; + + const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.mainMessage, writingOptions); + + return { + suggestion: { + subject: aiResponse.subject, + content: aiResponse.content, + alternatives: aiResponse.alternatives, + }, + tips: aiResponse.tips, + improvements: aiResponse.improvements, + }; + } +} diff --git a/src/modules/ai-assistant/services/ai.service.ts b/src/modules/ai-assistant/services/ai.service.ts new file mode 100644 index 0000000..5c8a834 --- /dev/null +++ b/src/modules/ai-assistant/services/ai.service.ts @@ -0,0 +1,166 @@ +import { Inject, Injectable, InternalServerErrorException } from "@nestjs/common"; +import OpenAI from "openai"; + +import { AI_CONFIG } from "../../../common/constants"; +import { AiMessage } from "../../../common/enums/message.enum"; +import { AIConfig } from "../../../configs/ai.config"; +import { + AIServiceInterface, + AISummaryResponse, + AIWritingResponse, + EmailData, + SummarizationOptions, + WritingAssistanceOptions, +} from "../interfaces/ai-service.interface"; + +@Injectable() +export class AiService implements AIServiceInterface { + private readonly openai: OpenAI; + + constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) { + this.openai = new OpenAI({ + baseURL: this.aiConfig.baseUrl, + apiKey: this.aiConfig.apiKey, + timeout: this.aiConfig.timeout, + }); + } + + async summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise { + const prompt = this.buildSummarizationPrompt(emailData, options); + + const completion = await this.openai.chat.completions.create({ + model: this.aiConfig.defaultModel, + messages: [ + { + role: "system", + content: this.getSummarizationSystemPrompt(options), + }, + { + role: "user", + content: prompt, + }, + ], + temperature: this.aiConfig.temperature, + max_tokens: this.aiConfig.maxTokens, + response_format: { type: "json_object" }, + }); + + const response = completion.choices[0]?.message?.content; + if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL); + + const parsedResponse = JSON.parse(response) as AISummaryResponse; + + return parsedResponse; + } + //************************************************* */ + async assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise { + const prompt = this.buildWritingAssistancePrompt(mainMessage, options); + + const completion = await this.openai.chat.completions.create({ + model: this.aiConfig.defaultModel, + messages: [ + { + role: "system", + content: this.getWritingAssistanceSystemPrompt(options), + }, + { + role: "user", + content: prompt, + }, + ], + temperature: this.aiConfig.temperature, + max_tokens: this.aiConfig.maxTokens, + response_format: { type: "json_object" }, + }); + + const response = completion.choices[0]?.message?.content; + if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL); + + const parsedResponse = JSON.parse(response) as AIWritingResponse; + + return parsedResponse; + } + + //************************************************* */ + private getSummarizationSystemPrompt(options: SummarizationOptions): string { + const language = options.language || "Persian"; + const template = this.aiConfig.farsiPrompts.summarization.systemPrompt; + + return template.replace("{tone}", options.tone).replace("{length}", options.length).replace("{language}", language); + } + + //************************************************* */ + private getWritingAssistanceSystemPrompt(options: WritingAssistanceOptions): string { + const language = options.language || "Persian"; + const template = this.aiConfig.farsiPrompts.writingAssistance.systemPrompt; + + return template + .replace("{tone}", options.tone) + .replace("{length}", options.length) + .replace("{purpose}", options.purpose) + .replace("{language}", language); + } + + //************************************************* */ + private buildSummarizationPrompt(emailData: EmailData, options: SummarizationOptions): string { + const template = this.aiConfig.farsiPrompts.summarization.promptTemplate; + let prompt = `${template.intro}\n\n`; + console.log(emailData); + + prompt += `${template.subjectLabel} ${emailData.subject}\n`; + prompt += `${template.fromLabel} ${emailData.from}\n`; + prompt += `${template.toLabel} ${emailData.to.join(", ")}\n`; + prompt += `${template.dateLabel} ${emailData.date}\n\n`; + prompt += `${template.contentLabel}\n${emailData.content}\n\n`; + + if (emailData.attachments && emailData.attachments.length > 0) { + prompt += `${template.attachmentsLabel} ${emailData.attachments.length} فایل\n\n`; + } + + prompt += `${template.analysisRequirements}\n`; + prompt += `${template.summaryLengthLabel} ${options.length}\n`; + prompt += `${template.toneLabel} ${options.tone}\n`; + + return prompt; + } + + //************************************************* */ + private buildWritingAssistancePrompt(mainMessage: string, options: WritingAssistanceOptions): string { + const template = this.aiConfig.farsiPrompts.writingAssistance.promptTemplate; + let prompt = `${template.intro}\n\n`; + + prompt += `${template.mainMessageLabel} ${mainMessage}\n\n`; + prompt += `${template.emailDetailsLabel}\n`; + prompt += `${template.purposeLabel} ${options.purpose}\n`; + prompt += `${template.toneLabel} ${options.tone}\n`; + prompt += `${template.lengthLabel} ${options.length}\n\n`; + + if (options.keyPoints && options.keyPoints.length > 0) { + prompt += `${template.keyPointsLabel}\n`; + options.keyPoints.forEach((point, index) => { + prompt += `${index + 1}. ${point}\n`; + }); + prompt += `\n`; + } + + if (options.context) { + prompt += `${template.contextLabel}\n`; + if (options.context.originalSubject) { + prompt += `${template.originalSubjectLabel} ${options.context.originalSubject}\n`; + } + if (options.context.originalSender) { + prompt += `${template.originalSenderLabel} ${options.context.originalSender}\n`; + } + if (options.context.originalContent) { + prompt += `${template.originalContentLabel} ${options.context.originalContent}\n`; + } + prompt += `\n`; + } + + if (options.additionalInstructions) { + prompt += `${template.additionalInstructionsLabel} ${options.additionalInstructions}\n\n`; + } + + return prompt; + } +} diff --git a/src/modules/utils/interfaces/ISms.ts b/src/modules/utils/interfaces/ISms.ts deleted file mode 100755 index 7633046..0000000 --- a/src/modules/utils/interfaces/ISms.ts +++ /dev/null @@ -1,51 +0,0 @@ -export interface IParameterArray { - name: TemplateParams; - value: string; -} -export interface ISmsResponse { - status: number; - message: string; -} -//---------------------------------------------- - -export interface ISmsGetLineResponse extends ISmsResponse { - data: string[]; -} - -export interface ISmsVerifyResponse extends ISmsResponse { - data: { MessageId: number; Cost: number }; -} - -//---------------------------------------------- -export interface ISmsVerifyBody { - Parameters: IParameterArray[]; - Mobile: string; - TemplateId: string; -} - -export type TemplateParams = - | "VERIFICATIONCODE" - | "code" - | "invoiceId" - | "price" - | "items" - | "loginDate" - | "invoiceDate" - | "title" - | "description" - | "ticketId" - | "serviceName" - | "date" - | "amount" - | "newBalance" - | "reason" - | "dueDate" - | "lateFee" - | "paidDate" - | "user" - | "subject" - | "planName" - | "message" - | "blogTitle" - | "fullName" - | "mobile"; diff --git a/src/modules/utils/services/sms.service.ts b/src/modules/utils/services/sms.service.ts deleted file mode 100755 index 41c0aee..0000000 --- a/src/modules/utils/services/sms.service.ts +++ /dev/null @@ -1,542 +0,0 @@ -import { HttpService } from "@nestjs/axios"; -import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; -import { AxiosError } from "axios"; -import { catchError, firstValueFrom } from "rxjs"; - -import { SmsMessage } from "../../../common/enums/message.enum"; -import { ISmsConfigs } from "../../../configs/sms.config"; -import { SMS_CONFIG } from "../constants"; -import { ISmsGetLineResponse, ISmsVerifyBody, ISmsVerifyResponse } from "../interfaces/ISms"; - -@Injectable() -export class SmsService { - private readonly logger = new Logger(SmsService.name); - - constructor( - @Inject(SMS_CONFIG) private smsConfigs: ISmsConfigs, - private readonly httpService: HttpService, - ) {} - //************************************************* */ - - async sendSmsVerifyCode(mobile: string, otpCode: string) { - // - const smsData: ISmsVerifyBody = { - Parameters: [{ name: "code", value: otpCode }], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_OTP, - }; - // - try { - const { data } = await firstValueFrom( - this.httpService - .post( - `${this.smsConfigs.API_URL}/send/verify`, - { ...smsData }, - { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }, - ) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending verify sms", err); - throw new InternalServerErrorException("error in sending verify sms"); - }), - ), - ); - - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR); - } - } - //************************************************* */ - - async sendInvoiceVerifyCode(mobile: string, otpCode: string, invoiceId: number, price: number, items: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "code", value: otpCode }, - { name: "invoiceId", value: invoiceId.toString() }, - { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, - { name: "items", value: items }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE, - }; - try { - const { data } = await firstValueFrom( - this.httpService - .post( - `${this.smsConfigs.API_URL}/send/verify`, - { ...smsData }, - { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }, - ) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending verify sms", err); - throw new InternalServerErrorException("error in sending verify sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR); - } - } - //************************************************* */ - - async sendLoginSms(mobile: string, loginDate: string) { - const smsData: ISmsVerifyBody = { - Parameters: [{ name: "loginDate", value: loginDate }], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_LOGIN, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending verify sms", err); - throw new InternalServerErrorException("error in sending verify sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - //************************************************* */ - - async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string, dueDate: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, - { name: "items", value: items }, - { name: "invoiceDate", value: invoiceDate }, - { name: "dueDate", value: dueDate }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - console.error(err); - - this.logger.error("error in sending invoice created sms", err); - throw new InternalServerErrorException("error in sending invoice created sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - //************************************************* */ - async sendAnnouncementSms(mobile: string, title: string, description: string, date: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "title", value: title }, - { name: "description", value: description }, - { name: "date", value: date }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_ANNOUNCEMENT, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending announcement sms", err); - throw new InternalServerErrorException("error in sending announcement sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - - //************************************************* */ - - async sendCreateTicketSms(mobile: string, ticketId: string, subject: string, date: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "ticketId", value: ticketId }, - { name: "subject", value: subject }, - { name: "date", value: date }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_CREATED, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending ticket created sms", err); - throw new InternalServerErrorException("error in sending ticket created sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - //************************************************* */ - - async sendAnswerTicketSms(mobile: string, ticketId: string, subject: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "ticketId", value: ticketId }, - { name: "subject", value: subject }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ANSWERED, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending ticket answered sms", err); - throw new InternalServerErrorException("error in sending ticket answered sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - //************************************************* */ - - async sendTicketAssignedToAdminSms(mobile: string, ticketId: string, subject: string, user: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "ticketId", value: ticketId }, - { name: "subject", value: subject }, - { name: "user", value: user }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ASSIGNED_ADMIN, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending ticket assigned sms", err); - throw new InternalServerErrorException("error in sending ticket assigned sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - //************************************************* */ - async sendInvoiceApprovedSms(mobile: string, invoiceId: string, price: number, dueDate: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, - { name: "dueDate", value: dueDate }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_APPROVED, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending invoice approved sms", err); - throw new InternalServerErrorException("error in sending invoice approved sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - // throw new InternalServerErrorException("error in sending sms"); - } - } - //************************************************* */ - async sendInvoicePaidSms(mobile: string, invoiceId: string, amount: number, paidDate: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "amount", value: amount.toString() }, - { name: "paidDate", value: paidDate }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_PAID, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending invoice paid sms", err); - throw new InternalServerErrorException("error in sending invoice paid sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - // throw new InternalServerErrorException("error in sending sms"); - } - } - - //************************************************* */ - - async sendInvoicePaymentReminderSms(mobile: string, invoiceId: string, amount: number, dueDate: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "amount", value: amount.toString() }, - { name: "dueDate", value: dueDate }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_REMINDER, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending invoice reminder sms", err); - throw new InternalServerErrorException("error in sending invoice reminder sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - - //************************************************* */ - - async sendInvoiceOverdueSms(mobile: string, invoiceId: string, amount: number, lateFee: number, dueDate: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "amount", value: amount.toString() }, - { name: "lateFee", value: lateFee.toString() }, - { name: "dueDate", value: dueDate }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_OVERDUE, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending invoice overdue sms", err); - throw new InternalServerErrorException("error in sending invoice overdue sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - - //************************************************* */ - - async sendRecurringInvoiceDraftSms(adminMobile: string, invoiceId: string, price: number) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, - ], - Mobile: adminMobile, - TemplateId: this.smsConfigs.SMS_PATTERN_RECURRING_INVOICE_DRAFT, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending recurring invoice draft sms", err); - throw new InternalServerErrorException("error in sending recurring invoice draft sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - // throw new InternalServerErrorException("error in sending sms"); - } - } - - //************************************************* */ - - async sendBlockServiceSms(mobile: string, invoiceId: string, planName: string) { - const smsData: ISmsVerifyBody = { - Parameters: [ - { name: "invoiceId", value: invoiceId }, - { name: "planName", value: planName }, - ], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_SUBSCRIPTION_CANCELLED, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending subscription cancelled sms", err); - throw new InternalServerErrorException("error in sending subscription cancelled sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - // throw new InternalServerErrorException("error in sending sms"); - } - } - - //************************************************* */ - //************************************************* */ - - async sendPaymentReminderSms(mobile: string, amount: string) { - const smsData: ISmsVerifyBody = { - Parameters: [{ name: "amount", value: amount }], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_REMINDER, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending payment reminder sms", err); - throw new InternalServerErrorException("error in sending payment reminder sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - //************************************************* */ - - async sendPaymentCancellationSms(mobile: string, amount: string) { - const smsData: ISmsVerifyBody = { - Parameters: [{ name: "amount", value: amount }], - Mobile: mobile, - TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_CANCELLATION, - }; - - try { - const { data } = await firstValueFrom( - this.httpService - .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((err: AxiosError) => { - this.logger.error("error in sending payment cancellation sms", err); - throw new InternalServerErrorException("error in sending payment cancellation sms"); - }), - ), - ); - return data; - } catch (error) { - this.logger.error("error in sending sms", error); - } - } - - async getSmsLines() { - try { - const { data } = await firstValueFrom( - this.httpService - .get(`${this.smsConfigs.API_URL}/lines`, { - headers: { "X-API-KEY": this.smsConfigs.API_KEY }, - }) - .pipe( - catchError((error: AxiosError) => { - this.logger.error("error in getting sms lines", error); - throw new InternalServerErrorException("error in getting sms lines"); - }), - ), - ); - - return data; - } catch (error) { - this.logger.error("error in getting sms lines", error); - } - } -} diff --git a/src/modules/utils/utils.module.ts b/src/modules/utils/utils.module.ts index dec9c67..5e56194 100755 --- a/src/modules/utils/utils.module.ts +++ b/src/modules/utils/utils.module.ts @@ -1,30 +1,22 @@ import { Module } from "@nestjs/common"; -import { S3_CONFIG, SMS_CONFIG } from "./constants"; +import { S3_CONFIG } from "./constants"; import { CacheService } from "./services/cache.service"; import { OTPService } from "./services/otp.service"; import { PasswordService } from "./services/password.service"; -import { SmsService } from "./services/sms.service"; import { S3Configs } from "../../configs/s3.config"; -import { smsConfigs } from "../../configs/sms.config"; @Module({ providers: [ OTPService, PasswordService, CacheService, - SmsService, - { - provide: SMS_CONFIG, - useFactory: smsConfigs().useFactory, - inject: smsConfigs().inject, - }, { provide: S3_CONFIG, useFactory: S3Configs().useFactory, inject: S3Configs().inject, }, ], - exports: [SMS_CONFIG, S3_CONFIG, OTPService, PasswordService, CacheService, SmsService], + exports: [S3_CONFIG, OTPService, PasswordService, CacheService], }) export class UtilsModule {}