chore: add ai service
This commit is contained in:
@@ -64,6 +64,7 @@
|
|||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"nodemailer": "^7.0.4",
|
"nodemailer": "^7.0.4",
|
||||||
|
"openai": "^5.10.2",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
|
|||||||
Generated
+17
@@ -110,6 +110,9 @@ importers:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.0.4
|
version: 7.0.4
|
||||||
|
openai:
|
||||||
|
specifier: ^5.10.2
|
||||||
|
version: 5.10.2
|
||||||
passport-jwt:
|
passport-jwt:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -4594,6 +4597,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||||
engines: {node: '>=8'}
|
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:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -11232,6 +11247,8 @@ snapshots:
|
|||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
openai@5.10.2: {}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { jwtConfig } from "./configs/jwt.config";
|
|||||||
import { databaseConfig } from "./configs/mikro-orm.config";
|
import { databaseConfig } from "./configs/mikro-orm.config";
|
||||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||||
|
import { AiAssistantModule } from "./modules/ai-assistant/ai-assistant.module";
|
||||||
import { AuthModule } from "./modules/auth/auth.module";
|
import { AuthModule } from "./modules/auth/auth.module";
|
||||||
import { DomainsModule } from "./modules/domains/domains.module";
|
import { DomainsModule } from "./modules/domains/domains.module";
|
||||||
import { EmailModule } from "./modules/email/email.module";
|
import { EmailModule } from "./modules/email/email.module";
|
||||||
@@ -37,6 +38,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
|||||||
ThrottlerModule.forRootAsync(rateLimitConfig()),
|
ThrottlerModule.forRootAsync(rateLimitConfig()),
|
||||||
MikroOrmModule.forRootAsync(databaseConfig),
|
MikroOrmModule.forRootAsync(databaseConfig),
|
||||||
UtilsModule,
|
UtilsModule,
|
||||||
|
AiAssistantModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
DomainsModule,
|
DomainsModule,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||||
|
export const AI_CONFIG = "AI_CONFIG";
|
||||||
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
|
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
|
||||||
export const AUTH_THROTTLE_LIMIT = 5;
|
export const AUTH_THROTTLE_LIMIT = 5;
|
||||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||||
|
|||||||
@@ -558,3 +558,9 @@ export const enum SettingMessageEnum {
|
|||||||
EMAIL_MUST_BE_BOOLEAN = "تنظیمات ارسال ایمیل باید یک boolean باشد",
|
EMAIL_MUST_BE_BOOLEAN = "تنظیمات ارسال ایمیل باید یک boolean باشد",
|
||||||
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی آن را ندارید",
|
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 = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<AIConfig> = {
|
||||||
|
provide: AI_CONFIG,
|
||||||
|
useFactory: (configService: ConfigService): AIConfig => ({
|
||||||
|
baseUrl: configService.get<string>("AI_BASE_URL", "https://api.openai.com/v1"),
|
||||||
|
apiKey: configService.get<string>("AI_API_KEY", ""),
|
||||||
|
defaultModel: configService.get<string>("AI_DEFAULT_MODEL", "gpt-3.5-turbo"),
|
||||||
|
temperature: configService.get<number>("AI_TEMPERATURE", 0.7),
|
||||||
|
maxTokens: configService.get<number>("AI_MAX_TOKENS", 2000),
|
||||||
|
timeout: configService.get<number>("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;
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
|
|
||||||
export function smsConfigs() {
|
|
||||||
return {
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory(configService: ConfigService) {
|
|
||||||
return {
|
|
||||||
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
|
||||||
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
|
||||||
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
|
|
||||||
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
|
|
||||||
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
|
|
||||||
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
|
|
||||||
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
|
|
||||||
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
|
|
||||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
|
||||||
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
|
||||||
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
|
||||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
|
||||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
|
||||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
|
||||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
|
||||||
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
|
||||||
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
@@ -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<AISummaryResponse>;
|
||||||
|
assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise<AIWritingResponse>;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AISummaryResponse> {
|
||||||
|
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<AIWritingResponse> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
|
||||||
@@ -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<ISmsVerifyResponse>(
|
|
||||||
`${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<ISmsVerifyResponse>(
|
|
||||||
`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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<ISmsGetLineResponse>(`${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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,22 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
import { S3_CONFIG, SMS_CONFIG } from "./constants";
|
import { S3_CONFIG } from "./constants";
|
||||||
import { CacheService } from "./services/cache.service";
|
import { CacheService } from "./services/cache.service";
|
||||||
import { OTPService } from "./services/otp.service";
|
import { OTPService } from "./services/otp.service";
|
||||||
import { PasswordService } from "./services/password.service";
|
import { PasswordService } from "./services/password.service";
|
||||||
import { SmsService } from "./services/sms.service";
|
|
||||||
import { S3Configs } from "../../configs/s3.config";
|
import { S3Configs } from "../../configs/s3.config";
|
||||||
import { smsConfigs } from "../../configs/sms.config";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
OTPService,
|
OTPService,
|
||||||
PasswordService,
|
PasswordService,
|
||||||
CacheService,
|
CacheService,
|
||||||
SmsService,
|
|
||||||
{
|
|
||||||
provide: SMS_CONFIG,
|
|
||||||
useFactory: smsConfigs().useFactory,
|
|
||||||
inject: smsConfigs().inject,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: S3_CONFIG,
|
provide: S3_CONFIG,
|
||||||
useFactory: S3Configs().useFactory,
|
useFactory: S3Configs().useFactory,
|
||||||
inject: S3Configs().inject,
|
inject: S3Configs().inject,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [SMS_CONFIG, S3_CONFIG, OTPService, PasswordService, CacheService, SmsService],
|
exports: [S3_CONFIG, OTPService, PasswordService, CacheService],
|
||||||
})
|
})
|
||||||
export class UtilsModule {}
|
export class UtilsModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user