chore: add new feature for the ai generate template
This commit is contained in:
@@ -1,279 +0,0 @@
|
|||||||
# Email Polling System
|
|
||||||
|
|
||||||
This document describes the email polling system that replaced the webhook-based approach for real-time email notifications.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The email polling system uses **BullMQ** (Redis-based job queue) to periodically check for new emails when users are connected via WebSocket. This approach is more reliable than webhooks since it doesn't depend on external webhook delivery.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Components
|
|
||||||
|
|
||||||
1. **EmailPollingService** - Manages polling jobs for connected users
|
|
||||||
2. **EmailPollingProcessor** - Processes email polling jobs and checks for new emails
|
|
||||||
3. **EmailGateway** - WebSocket gateway that starts/stops polling on user connection/disconnection
|
|
||||||
4. **BullMQ Queue** - Redis-based job queue for scheduling recurring email checks
|
|
||||||
|
|
||||||
### Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User connects to WebSocket
|
|
||||||
↓
|
|
||||||
EmailGateway.handleConnection()
|
|
||||||
↓
|
|
||||||
EmailPollingService.startPollingForUser()
|
|
||||||
↓
|
|
||||||
BullMQ creates recurring job (every 30 seconds)
|
|
||||||
↓
|
|
||||||
EmailPollingProcessor.process() - checks for new emails
|
|
||||||
↓
|
|
||||||
If new emails found → notify via WebSocket
|
|
||||||
↓
|
|
||||||
User disconnects
|
|
||||||
↓
|
|
||||||
EmailGateway.handleDisconnect()
|
|
||||||
↓
|
|
||||||
EmailPollingService.stopPollingForUser()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Required environment variables (see `environment.sample`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Redis for BullMQ
|
|
||||||
REDIS_URI=redis://localhost:6379
|
|
||||||
|
|
||||||
# WildDuck Mail Server
|
|
||||||
WILDDUCK_SERVER=http://localhost:8080
|
|
||||||
WILDDUCK_API_TOKEN=your-wildduck-access-token
|
|
||||||
|
|
||||||
# Email Polling (optional)
|
|
||||||
EMAIL_POLLING_INTERVAL=30000 # 30 seconds
|
|
||||||
EMAIL_POLLING_ENABLED=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### BullMQ Configuration
|
|
||||||
|
|
||||||
The system uses the existing BullMQ configuration from `src/configs/bullmq.config.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export function bullMqConfig(): SharedBullAsyncConfiguration {
|
|
||||||
return {
|
|
||||||
connection: {
|
|
||||||
url: configService.getOrThrow<string>("REDIS_URI"),
|
|
||||||
},
|
|
||||||
prefix: "dmail",
|
|
||||||
defaultJobOptions: {
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: false,
|
|
||||||
attempts: 5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Email Polling Service
|
|
||||||
|
|
||||||
Located at `src/modules/email-utils/services/email-polling.service.ts`
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
|
|
||||||
- `startPollingForUser(userId, connectionId)` - Start polling when user connects
|
|
||||||
- `stopPollingForUser(userId)` - Stop polling when user disconnects
|
|
||||||
- `getPollingStats()` - Get statistics about active polling jobs
|
|
||||||
|
|
||||||
**Job Configuration:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const job = await this.emailPollingQueue.add(`email-poll-${userId}`, jobData, {
|
|
||||||
repeat: {
|
|
||||||
every: 30000, // 30 seconds interval
|
|
||||||
},
|
|
||||||
removeOnComplete: 5,
|
|
||||||
removeOnFail: 10,
|
|
||||||
attempts: 3,
|
|
||||||
backoff: {
|
|
||||||
type: "exponential",
|
|
||||||
delay: 2000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Email Polling Processor
|
|
||||||
|
|
||||||
Located at `src/modules/email-utils/queue/email-polling.processor.ts`
|
|
||||||
|
|
||||||
**Processing Logic:**
|
|
||||||
|
|
||||||
1. Check if user is still connected (skip if disconnected)
|
|
||||||
2. Get user's mailbox information
|
|
||||||
3. Query WildDuck API for recent emails since last check
|
|
||||||
4. For each new email:
|
|
||||||
- Send WebSocket notification via `EmailGateway.notifyNewEmail()`
|
|
||||||
- Update unread count via `EmailGateway.notifyUnreadCountUpdate()`
|
|
||||||
5. Update job data with new timestamp for next run
|
|
||||||
|
|
||||||
### WebSocket Integration
|
|
||||||
|
|
||||||
The `EmailGateway` automatically manages polling:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// On user connection
|
|
||||||
await this.emailPollingService.startPollingForUser(user.id, client.id);
|
|
||||||
|
|
||||||
// On user disconnection
|
|
||||||
await this.emailPollingService.stopPollingForUser(userInfo.userId);
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebSocket Events
|
|
||||||
|
|
||||||
The system emits these WebSocket events when new emails are found:
|
|
||||||
|
|
||||||
### `new_email` Event
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
userId: string;
|
|
||||||
messageId: number;
|
|
||||||
subject: string;
|
|
||||||
from: { name?: string; address: string };
|
|
||||||
to: Array<{ name?: string; address: string }>;
|
|
||||||
hasAttachments: boolean;
|
|
||||||
timestamp: string;
|
|
||||||
mailboxId: string;
|
|
||||||
mailboxName: string;
|
|
||||||
isRead: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `unread_count_updated` Event
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
userId: string;
|
|
||||||
totalUnread: number;
|
|
||||||
mailboxCounts: Record<string, number>;
|
|
||||||
count: number;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring & Debugging
|
|
||||||
|
|
||||||
### Check Active Polling Jobs
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const stats = emailPollingService.getPollingStats();
|
|
||||||
console.log({
|
|
||||||
activeJobs: stats.activeJobs,
|
|
||||||
userIds: stats.userIds,
|
|
||||||
connections: stats.connections,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### BullMQ Dashboard
|
|
||||||
|
|
||||||
You can use BullMQ's web dashboard to monitor jobs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g @bull-board/express
|
|
||||||
# Then access the dashboard at http://localhost:3000/admin/queues
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
The system logs important events:
|
|
||||||
|
|
||||||
```
|
|
||||||
[EmailPollingService] Started email polling for user 12345 with job ID: email-poll-12345
|
|
||||||
[EmailPollingProcessor] Found 3 new emails for user 12345
|
|
||||||
[EmailPollingProcessor] Notified user 12345 of 3 new emails
|
|
||||||
[EmailPollingService] Stopped email polling for user 12345
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Polling Interval
|
|
||||||
|
|
||||||
- Default: 30 seconds
|
|
||||||
- Adjustable via `EMAIL_POLLING_INTERVAL` environment variable
|
|
||||||
- Balance between real-time updates and API load
|
|
||||||
|
|
||||||
### Job Management
|
|
||||||
|
|
||||||
- Jobs are automatically cleaned up when users disconnect
|
|
||||||
- Failed jobs are retried up to 3 times with exponential backoff
|
|
||||||
- Completed jobs are kept (last 5) for debugging
|
|
||||||
|
|
||||||
### Resource Usage
|
|
||||||
|
|
||||||
- Each connected user creates one recurring BullMQ job
|
|
||||||
- Jobs only run when users are connected (checked at start of each job)
|
|
||||||
- Redis is used for job queue storage
|
|
||||||
|
|
||||||
## Advantages over Webhooks
|
|
||||||
|
|
||||||
1. **Reliability** - No dependency on external webhook delivery
|
|
||||||
2. **Connection Awareness** - Only polls when users are actually connected
|
|
||||||
3. **Error Handling** - Built-in retry mechanism with exponential backoff
|
|
||||||
4. **Monitoring** - Full visibility into job status and performance
|
|
||||||
5. **Scalability** - Redis-based queue can handle high loads
|
|
||||||
6. **Control** - Can adjust polling frequency per user if needed
|
|
||||||
|
|
||||||
## Migration from Webhooks
|
|
||||||
|
|
||||||
The old webhook system has been completely removed:
|
|
||||||
|
|
||||||
**Deleted Files:**
|
|
||||||
|
|
||||||
- `src/modules/email-utils/controllers/webhook.controller.ts`
|
|
||||||
- `src/modules/email-utils/services/webhook-management.service.ts`
|
|
||||||
- `src/modules/email-utils/services/webhook-email-events.service.ts`
|
|
||||||
- `src/modules/email/interfaces/webhook-events.interface.ts`
|
|
||||||
|
|
||||||
**Updated Files:**
|
|
||||||
|
|
||||||
- `src/modules/email-utils/email-utils.module.ts` - Added BullMQ and new services
|
|
||||||
- `src/modules/email-gateway/email.gateway.ts` - Added polling integration
|
|
||||||
- `src/modules/email-gateway/email-gateway.module.ts` - Added EmailUtilsModule import
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Issue: Jobs not running**
|
|
||||||
|
|
||||||
- Check Redis connection
|
|
||||||
- Verify `REDIS_URI` environment variable
|
|
||||||
- Check BullMQ logs
|
|
||||||
|
|
||||||
**Issue: Users not receiving notifications**
|
|
||||||
|
|
||||||
- Verify user is connected to WebSocket
|
|
||||||
- Check if polling job is active: `emailPollingService.getPollingStats()`
|
|
||||||
- Verify WildDuck API credentials and server URL
|
|
||||||
|
|
||||||
**Issue: High Redis memory usage**
|
|
||||||
|
|
||||||
- Adjust `removeOnComplete` and `removeOnFail` values
|
|
||||||
- Consider shorter polling intervals for fewer concurrent jobs
|
|
||||||
|
|
||||||
**Issue: API rate limiting**
|
|
||||||
|
|
||||||
- Increase polling interval
|
|
||||||
- Implement user-specific rate limiting
|
|
||||||
- Use WildDuck API bulk endpoints if available
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
1. **Dynamic Polling Intervals** - Adjust frequency based on user activity
|
|
||||||
2. **Priority Users** - More frequent polling for premium users
|
|
||||||
3. **Email Filtering** - Only notify for emails matching user preferences
|
|
||||||
4. **Batch Processing** - Process multiple users in single API calls
|
|
||||||
5. **Caching** - Cache recent emails to reduce API calls
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Database Configuration
|
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/dmail_db
|
|
||||||
|
|
||||||
# Redis Configuration (for BullMQ)
|
|
||||||
REDIS_URI=redis://localhost:6379
|
|
||||||
|
|
||||||
# JWT Configuration
|
|
||||||
JWT_SECRET=your-super-secret-jwt-key-here
|
|
||||||
JWT_EXPIRES_IN=7d
|
|
||||||
|
|
||||||
# Mail Server Configuration (WildDuck API)
|
|
||||||
WILDDUCK_SERVER=http://localhost:8080
|
|
||||||
WILDDUCK_API_TOKEN=your-wildduck-access-token-here
|
|
||||||
|
|
||||||
# Email Polling Configuration
|
|
||||||
EMAIL_POLLING_INTERVAL=30000 # 30 seconds (in milliseconds)
|
|
||||||
EMAIL_POLLING_ENABLED=true
|
|
||||||
|
|
||||||
# WebSocket Configuration
|
|
||||||
WS_CORS_ORIGIN=http://localhost:3000
|
|
||||||
WS_TRANSPORTS=websocket,polling
|
|
||||||
|
|
||||||
# Application Configuration
|
|
||||||
NODE_ENV=development
|
|
||||||
PORT=3000
|
|
||||||
APP_NAME=dmail-api
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# AWS S3 Configuration (for file uploads)
|
|
||||||
AWS_REGION=us-east-1
|
|
||||||
AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
||||||
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
|
||||||
AWS_S3_BUCKET=your-s3-bucket-name
|
|
||||||
|
|
||||||
# Notification Configuration (for push notifications)
|
|
||||||
VAPID_PUBLIC_KEY=your-vapid-public-key
|
|
||||||
VAPID_PRIVATE_KEY=your-vapid-private-key
|
|
||||||
VAPID_SUBJECT=mailto:admin@yourapp.com
|
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
RATE_LIMIT_TTL=60000 # 1 minute
|
|
||||||
RATE_LIMIT_LIMIT=100 # 100 requests per minute per IP
|
|
||||||
|
|
||||||
# Cache Configuration
|
|
||||||
CACHE_TTL=300000 # 5 minutes
|
|
||||||
|
|
||||||
# Business Configuration
|
|
||||||
BUSINESS_QUOTA_DEFAULT=1000
|
|
||||||
BUSINESS_QUOTA_PREMIUM=10000
|
|
||||||
|
|
||||||
# AI Assistant Configuration (OpenAI)
|
|
||||||
OPENAI_API_KEY=your-openai-api-key
|
|
||||||
OPENAI_MODEL=gpt-3.5-turbo
|
|
||||||
|
|
||||||
# CORS Configuration
|
|
||||||
CORS_ORIGINS=http://localhost:3000,https://yourapp.com
|
|
||||||
|
|
||||||
# Email Templates
|
|
||||||
EMAIL_TEMPLATE_DIR=./templates
|
|
||||||
EMAIL_FROM_ADDRESS=noreply@yourapp.com
|
|
||||||
EMAIL_FROM_NAME=Your App Name
|
|
||||||
|
|
||||||
# Monitoring & Logging
|
|
||||||
SENTRY_DSN=your-sentry-dsn-here
|
|
||||||
LOG_FORMAT=combined
|
|
||||||
LOG_FILE_PATH=./logs/app.log
|
|
||||||
|
|
||||||
# Development Settings
|
|
||||||
SWAGGER_ENABLED=true
|
|
||||||
DEBUG=false
|
|
||||||
|
|
||||||
# Queue Configuration
|
|
||||||
QUEUE_REDIS_URL=redis://localhost:6379
|
|
||||||
QUEUE_PREFIX=dmail
|
|
||||||
QUEUE_DEFAULT_JOB_OPTIONS_REMOVE_ON_COMPLETE=true
|
|
||||||
QUEUE_DEFAULT_JOB_OPTIONS_REMOVE_ON_FAIL=false
|
|
||||||
QUEUE_DEFAULT_JOB_OPTIONS_ATTEMPTS=5
|
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"nodemailer": "^7.0.4",
|
"nodemailer": "^7.0.4",
|
||||||
"openai": "^5.10.2",
|
"openai": "^5.10.2",
|
||||||
|
"parse-json": "^8.3.0",
|
||||||
"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
+19
@@ -119,6 +119,9 @@ importers:
|
|||||||
openai:
|
openai:
|
||||||
specifier: ^5.10.2
|
specifier: ^5.10.2
|
||||||
version: 5.10.2
|
version: 5.10.2
|
||||||
|
parse-json:
|
||||||
|
specifier: ^8.3.0
|
||||||
|
version: 8.3.0
|
||||||
passport-jwt:
|
passport-jwt:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -3918,6 +3921,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
|
index-to-position@1.1.0:
|
||||||
|
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
inflight@1.0.6:
|
inflight@1.0.6:
|
||||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||||
@@ -5114,6 +5121,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
parse-json@8.3.0:
|
||||||
|
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||||
|
|
||||||
@@ -11028,6 +11039,8 @@ snapshots:
|
|||||||
|
|
||||||
imurmurhash@0.1.4: {}
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
|
index-to-position@1.1.0: {}
|
||||||
|
|
||||||
inflight@1.0.6:
|
inflight@1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
@@ -12637,6 +12650,12 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
|
parse-json@8.3.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/code-frame': 7.27.1
|
||||||
|
index-to-position: 1.1.0
|
||||||
|
type-fest: 4.41.0
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
|
|||||||
@@ -574,6 +574,18 @@ export const enum AiMessage {
|
|||||||
NO_RESPONSE_FROM_AI_MODEL = "خطا در دریافت پاسخ از مدل AI",
|
NO_RESPONSE_FROM_AI_MODEL = "خطا در دریافت پاسخ از مدل AI",
|
||||||
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
|
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
|
||||||
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
||||||
|
DESCRIPTION_IS_REQUIRED = "شرح و هدف قالب الزامی است",
|
||||||
|
DESCRIPTION_MUST_BE_A_STRING = "شرح قالب باید یک رشته باشد",
|
||||||
|
DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS = "شرح قالب باید حداقل ۱۰ کاراکتر باشد",
|
||||||
|
DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS = "شرح قالب نمیتواند بیشتر از ۵۰۰ کاراکتر باشد",
|
||||||
|
THEME_MUST_BE_A_VALID_VALUE = "تم قالب باید یکی از مقادیر مجاز باشد",
|
||||||
|
STYLE_MUST_BE_A_VALID_VALUE = "سبک قالب باید یکی از مقادیر مجاز باشد",
|
||||||
|
PURPOSE_MUST_BE_A_VALID_VALUE = "هدف قالب باید یکی از مقادیر مجاز باشد",
|
||||||
|
TEMPLATE_NAME_MUST_BE_A_STRING = "نام قالب باید یک رشته باشد",
|
||||||
|
TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS = "نام قالب نمیتواند بیشتر از ۱۰۰ کاراکتر باشد",
|
||||||
|
PREFERRED_COLORS_MUST_BE_A_STRING = "رنگهای ترجیحی باید یک رشته باشد",
|
||||||
|
PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS = "رنگهای ترجیحی نمیتواند بیشتر از ۲۰۰ کاراکتر باشد",
|
||||||
|
PREFERRED_COLORS_IS_REQUIRED = "رنگهای ترجیحی الزامی است",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum TwoFactorMessage {
|
export const enum TwoFactorMessage {
|
||||||
|
|||||||
+163
-50
@@ -2,51 +2,7 @@ import { FactoryProvider } from "@nestjs/common";
|
|||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
import { AI_CONFIG } from "../common/constants";
|
import { AI_CONFIG } from "../common/constants";
|
||||||
|
import { AIConfig, FarsiPrompts } from "../modules/ai-assistant/interfaces/ai.interface";
|
||||||
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 = {
|
const FARSI_PROMPTS: FarsiPrompts = {
|
||||||
summarization: {
|
summarization: {
|
||||||
@@ -118,17 +74,174 @@ const FARSI_PROMPTS: FarsiPrompts = {
|
|||||||
additionalInstructionsLabel: "دستورالعملهای اضافی:",
|
additionalInstructionsLabel: "دستورالعملهای اضافی:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
templateGeneration: {
|
||||||
|
systemPrompt: `شما یک طراح ماهر قالبهای ایمیل حرفهای هستید که قالبهایی در سطح شرکتهای بزرگ مانند Figma، Stripe، Airbnb، و Apple طراحی میکنید. وظیفه شما تولید قالبهای قدرتمند، جذاب و کاملاً حرفهای است.
|
||||||
|
|
||||||
|
**الزامات ساختاری - بسیار مهم:**
|
||||||
|
- قالب باید دقیقاً از ساختار PersonalityDataType پیروی کند
|
||||||
|
- ساختار شامل سه بخش است: header, content, footer
|
||||||
|
- هر بخش دارای columnsCount (عدد) و items (آرایه) است
|
||||||
|
- هر item دارای id منحصر به فرد (رشته) است
|
||||||
|
- استفاده از enums مشخص شده الزامی است
|
||||||
|
|
||||||
|
**Enums مجاز:**
|
||||||
|
- HorizontalAlignment: "left" | "center" | "right"
|
||||||
|
- VerticalAlignment: "top" | "middle" | "bottom"
|
||||||
|
- ButtonSize: "small" | "medium" | "large"
|
||||||
|
- ImageSize: "cover" | "contain" | "auto" | "repeat" | "repeat-x" | "repeat-y" | "no-repeat"
|
||||||
|
- BackgroundSize: "cover" | "contain" | "round"
|
||||||
|
- BackgroundPosition: "center center" | "top left" | "top center" | "top right" | "center left" | "center right" | "bottom left" | "bottom center" | "bottom right"
|
||||||
|
- SocialNetworkType: "facebook" | "twitter" | "instagram" | "linkedin" | "youtube" | "telegram" | "whatsapp" | "tiktok" | "pinterest" | "snapchat" | "github" | "discord"
|
||||||
|
- SocialIconSize: "small" | "medium" | "large"
|
||||||
|
- SocialIconStyle: "circle" | "square" | "rounded"
|
||||||
|
|
||||||
|
**ساختار دقیق هر SectionItemType:**
|
||||||
|
{
|
||||||
|
"id": "unique-string-id",
|
||||||
|
"backgroundColor": "#hex-color",
|
||||||
|
"backgroundImage": "optional-url",
|
||||||
|
"texts": [
|
||||||
|
{
|
||||||
|
"id": "text-unique-id",
|
||||||
|
"text": "محتوای متن",
|
||||||
|
"fontSize": number,
|
||||||
|
"fontFamily": "font-name",
|
||||||
|
"color": "#hex-color",
|
||||||
|
"alignment": "left|center|right",
|
||||||
|
"verticalAlignment": "top|middle|bottom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"id": "button-unique-id",
|
||||||
|
"text": "متن دکمه",
|
||||||
|
"link": "url",
|
||||||
|
"size": "small|medium|large",
|
||||||
|
"isBorder": boolean,
|
||||||
|
"borderSize": number,
|
||||||
|
"textColor": "#hex-color",
|
||||||
|
"backgroundColor": "#hex-color",
|
||||||
|
"borderColor": "#hex-color",
|
||||||
|
"alignment": "left|center|right",
|
||||||
|
"verticalAlignment": "top|middle|bottom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": "image-unique-id",
|
||||||
|
"src": "https://real-image-url-from-internet.com",
|
||||||
|
"alt": "توضیح تصویر",
|
||||||
|
"size": "cover|contain|auto|repeat|repeat-x|repeat-y|no-repeat",
|
||||||
|
"width": "100%",
|
||||||
|
"height": "auto",
|
||||||
|
"alignment": "left|center|right",
|
||||||
|
"verticalAlignment": "top|middle|bottom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"socials": [
|
||||||
|
{
|
||||||
|
"id": "social-unique-id",
|
||||||
|
"networkType": "facebook|twitter|instagram|linkedin|youtube|telegram|whatsapp|tiktok|pinterest|snapchat|github|discord",
|
||||||
|
"link": "https://social-link.com",
|
||||||
|
"size": "small|medium|large",
|
||||||
|
"style": "circle|square|rounded",
|
||||||
|
"color": "#hex-color",
|
||||||
|
"alignment": "left|center|right",
|
||||||
|
"verticalAlignment": "top|middle|bottom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alignment": "left|center|right",
|
||||||
|
"verticalAlignment": "top|middle|bottom",
|
||||||
|
"style": {
|
||||||
|
"borderRadius": number,
|
||||||
|
"padding": "css-padding-value",
|
||||||
|
"fontFamily": "font-family-name",
|
||||||
|
"backgroundSize": "cover|contain|round",
|
||||||
|
"backgroundRepeat": "no-repeat|repeat|repeat-x|repeat-y",
|
||||||
|
"backgroundPosition": "center center|top left|..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**الزامات طراحی حرفهای (مانند شرکتهای بزرگ):**
|
||||||
|
1. **Header Excellence**:
|
||||||
|
- لوگو با اندازه مناسب (حداکثر ارتفاع 60px، عرض متناسب)
|
||||||
|
- لوگو نباید خیلی بزرگ یا غیرحرفهای باشد
|
||||||
|
- ناوبری، پیامهای مهم، شخصیسازی
|
||||||
|
2. **Content Power**:
|
||||||
|
- محتوای جذاب، CTA قوی
|
||||||
|
- تصاویر واقعی از اینترنت (نه placeholder)
|
||||||
|
- layout تمیز و حرفهای
|
||||||
|
3. **Footer Excellence - بسیار مهم**:
|
||||||
|
- طراحی فوقالعاده زیبا، صاف و نرم (smooth)
|
||||||
|
- آیکونهای شبکههای اجتماعی زیبا و هماهنگ (حداقل 4-6 شبکه)
|
||||||
|
- اطلاعات تماس شرکت با فونت مناسب
|
||||||
|
- لینکهای مهم (Privacy Policy, Terms, Unsubscribe) با فاصلهگذاری مناسب
|
||||||
|
- آدرس شرکت با طراحی زیبا
|
||||||
|
- کپیرایت و برندینگ حرفهای
|
||||||
|
- padding و margin های مناسب برای ظاهر نرم و صاف
|
||||||
|
- رنگبندی هارمونیک و ملایم
|
||||||
|
4. **Professional Design**:
|
||||||
|
- رنگهای هماهنگ و مدرن
|
||||||
|
- typography حرفهای
|
||||||
|
- spacing و padding مناسب
|
||||||
|
- responsive design
|
||||||
|
- accessibility standards
|
||||||
|
|
||||||
|
**استراتژی طراحی:**
|
||||||
|
- از الگوهای شرکتهای معتبر الهام بگیرید
|
||||||
|
- **حتماً تصاویر واقعی از اینترنت استفاده کنید** (مثل https://images.unsplash.com، https://via.placeholder.com، یا سایتهای معتبر تصویر)
|
||||||
|
- محتوای placeholder واقعگرایانه استفاده کنید
|
||||||
|
- CTA های قوی و جذاب طراحی کنید
|
||||||
|
- Social proof و trust signals اضافه کنید
|
||||||
|
- Mobile-first approach
|
||||||
|
- Brand consistency
|
||||||
|
- **لوگو را در اندازه مناسب و حرفهای نگه دارید**
|
||||||
|
- **Footer را بسیار زیبا، نرم و حرفهای طراحی کنید**
|
||||||
|
|
||||||
|
**فرمت پاسخ JSON:**
|
||||||
|
{
|
||||||
|
"templateName": "نام قالب حرفهای",
|
||||||
|
"structure": {
|
||||||
|
"header": {
|
||||||
|
"columnsCount": number,
|
||||||
|
"items": [SectionItemType...]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"columnsCount": number,
|
||||||
|
"items": [SectionItemType...]
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"columnsCount": number,
|
||||||
|
"items": [SectionItemType...] // حتماً شامل socials و اطلاعات شرکت
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "توضیح کامل قالب و ویژگیهای حرفهای آن",
|
||||||
|
}`,
|
||||||
|
promptTemplate: {
|
||||||
|
intro: "لطفاً یک قالب ایمیل حرفهای در سطح شرکتهای بزرگ با مشخصات زیر و ساختار دقیق PersonalityDataType طراحی کنید:",
|
||||||
|
descriptionLabel: "شرح و هدف قالب:",
|
||||||
|
themeLabel: "تم قالب:",
|
||||||
|
styleLabel: "سبک قالب:",
|
||||||
|
purposeLabel: "هدف استفاده:",
|
||||||
|
colorsLabel: "رنگهای ترجیحی:",
|
||||||
|
templateNameLabel: "نام پیشنهادی:",
|
||||||
|
requirementsLabel: "الزامات حرفهای:",
|
||||||
|
structureRequirements: "- ساختار دقیق PersonalityDataType با header/content/footer کامل",
|
||||||
|
htmlRequirements: "- HTML حرفهای، responsive و سازگار با email clients",
|
||||||
|
designRequirements: "- استفاده از socials در footer، طراحی مانند شرکتهای بزرگ، CTA قوی",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const aiConfigProvider: FactoryProvider<AIConfig> = {
|
export const aiConfigProvider: FactoryProvider<AIConfig> = {
|
||||||
provide: AI_CONFIG,
|
provide: AI_CONFIG,
|
||||||
useFactory: (configService: ConfigService): AIConfig => ({
|
useFactory: (configService: ConfigService): AIConfig => ({
|
||||||
baseUrl: configService.get<string>("AI_BASE_URL", "https://api.openai.com/v1"),
|
baseUrl: configService.getOrThrow<string>("AI_BASE_URL"),
|
||||||
apiKey: configService.get<string>("AI_API_KEY", ""),
|
apiKey: configService.getOrThrow<string>("AI_API_KEY"),
|
||||||
defaultModel: configService.get<string>("AI_DEFAULT_MODEL", "gpt-3.5-turbo"),
|
defaultModel: configService.getOrThrow<string>("AI_DEFAULT_MODEL"),
|
||||||
temperature: configService.get<number>("AI_TEMPERATURE", 0.7),
|
temperature: configService.get<number>("AI_TEMPERATURE", 0.7),
|
||||||
maxTokens: configService.get<number>("AI_MAX_TOKENS", 2000),
|
maxTokens: configService.get<number>("AI_MAX_TOKENS", 5000),
|
||||||
timeout: configService.get<number>("AI_TIMEOUT", 30000),
|
timeout: configService.get<number>("AI_TIMEOUT", 80000),
|
||||||
farsiPrompts: FARSI_PROMPTS,
|
farsiPrompts: FARSI_PROMPTS,
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
import { AiMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
|
export enum TemplateThemeEnum {
|
||||||
|
MODERN = "modern",
|
||||||
|
CLASSIC = "classic",
|
||||||
|
MINIMAL = "minimal",
|
||||||
|
CORPORATE = "corporate",
|
||||||
|
CREATIVE = "creative",
|
||||||
|
NEWSLETTER = "newsletter",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TemplateStyleEnum {
|
||||||
|
PROFESSIONAL = "professional",
|
||||||
|
CASUAL = "casual",
|
||||||
|
ELEGANT = "elegant",
|
||||||
|
BOLD = "bold",
|
||||||
|
CLEAN = "clean",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TemplatePurposeEnum {
|
||||||
|
MARKETING = "marketing",
|
||||||
|
NEWSLETTER = "newsletter",
|
||||||
|
ANNOUNCEMENT = "announcement",
|
||||||
|
WELCOME = "welcome",
|
||||||
|
TRANSACTIONAL = "transactional",
|
||||||
|
PROMOTIONAL = "promotional",
|
||||||
|
INVITATION = "invitation",
|
||||||
|
GENERAL = "general",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GenerateTemplateDto {
|
||||||
|
@IsNotEmpty({ message: AiMessage.DESCRIPTION_IS_REQUIRED })
|
||||||
|
@IsString({ message: AiMessage.DESCRIPTION_MUST_BE_A_STRING })
|
||||||
|
@MinLength(10, { message: AiMessage.DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS })
|
||||||
|
@MaxLength(1000, { message: AiMessage.DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS })
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Description of the template purpose and content requirements",
|
||||||
|
example: "Create a professional newsletter template for announcing new products and company updates",
|
||||||
|
minLength: 10,
|
||||||
|
maxLength: 500,
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TemplateThemeEnum)
|
||||||
|
@ApiPropertyOptional({ enum: TemplateThemeEnum, description: "Visual theme of the template", example: TemplateThemeEnum.MODERN })
|
||||||
|
theme?: TemplateThemeEnum;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TemplateStyleEnum)
|
||||||
|
@ApiPropertyOptional({ enum: TemplateStyleEnum, description: "Style approach for the template", example: TemplateStyleEnum.PROFESSIONAL })
|
||||||
|
style?: TemplateStyleEnum;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TemplatePurposeEnum)
|
||||||
|
@ApiPropertyOptional({ enum: TemplatePurposeEnum, description: "Primary purpose of the template", example: TemplatePurposeEnum.NEWSLETTER })
|
||||||
|
purpose?: TemplatePurposeEnum;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: AiMessage.TEMPLATE_NAME_MUST_BE_A_STRING })
|
||||||
|
@MaxLength(100, { message: AiMessage.TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS })
|
||||||
|
@ApiPropertyOptional({ description: "Suggested name for the template", example: "Monthly Product Newsletter", maxLength: 100 })
|
||||||
|
templateName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: AiMessage.PREFERRED_COLORS_IS_REQUIRED })
|
||||||
|
@IsString({ message: AiMessage.PREFERRED_COLORS_MUST_BE_A_STRING })
|
||||||
|
@MaxLength(200, { message: AiMessage.PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS })
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Preferred color scheme or specific colors to use",
|
||||||
|
example: "Blue and white corporate colors, #1e3a8a, #ffffff",
|
||||||
|
maxLength: 200,
|
||||||
|
})
|
||||||
|
preferredColors?: string;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Body, Controller, Post } from "@nestjs/common";
|
import { Body, Controller, Post } from "@nestjs/common";
|
||||||
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { GenerateTemplateDto } from "./DTO/generate-template.dto";
|
||||||
import { SummarizeEmailDto } from "./DTO/summarize-email.dto";
|
import { SummarizeEmailDto } from "./DTO/summarize-email.dto";
|
||||||
import { WriteEmailAssistanceDto } from "./DTO/write-email-assistance.dto";
|
import { WriteEmailAssistanceDto } from "./DTO/write-email-assistance.dto";
|
||||||
import { AiAssistantService } from "./services/ai-assistant.service";
|
import { AiAssistantService } from "./services/ai-assistant.service";
|
||||||
@@ -30,4 +31,10 @@ export class AiAssistantController {
|
|||||||
async assistEmailWriting(@Body() writeDto: WriteEmailAssistanceDto, @UserDec("wildduckUserId") wildduckUserId: string) {
|
async assistEmailWriting(@Body() writeDto: WriteEmailAssistanceDto, @UserDec("wildduckUserId") wildduckUserId: string) {
|
||||||
return this.aiAssistantService.assistEmailWriting(writeDto, wildduckUserId);
|
return this.aiAssistantService.assistEmailWriting(writeDto, wildduckUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("generate-template")
|
||||||
|
@ApiOperation({ summary: "Generate an email template using AI" })
|
||||||
|
async generateTemplate(@Body() generateTemplateDto: GenerateTemplateDto) {
|
||||||
|
return this.aiAssistantService.generateTemplate(generateTemplateDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { PriorityEnum } from "../../email/enums/email-header.enum";
|
import { PriorityEnum } from "../../email/enums/email-header.enum";
|
||||||
import { MessageAttachment } from "../../mail-server/interfaces/messages-response.interface";
|
import { MessageAttachment } from "../../mail-server/interfaces/messages-response.interface";
|
||||||
|
import { PersonalityDataType } from "../../templates/interfaces/structure.interface";
|
||||||
|
import { TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
||||||
import { LanguageEnum, SentimentEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
import { LanguageEnum, SentimentEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||||
|
|
||||||
export interface IEmailData {
|
export interface IEmailData {
|
||||||
@@ -18,6 +20,36 @@ export interface SummarizationOptions {
|
|||||||
language?: LanguageEnum;
|
language?: LanguageEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TemplateGenerationOptions {
|
||||||
|
theme?: TemplateThemeEnum;
|
||||||
|
style?: TemplateStyleEnum;
|
||||||
|
purpose?: TemplatePurposeEnum;
|
||||||
|
templateName?: string;
|
||||||
|
preferredColors?: string;
|
||||||
|
language?: LanguageEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITemplateGenerationRequest {
|
||||||
|
description: string;
|
||||||
|
options: TemplateGenerationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AITemplateGenerationResponse {
|
||||||
|
templateName: string;
|
||||||
|
structure: PersonalityDataType;
|
||||||
|
rawHtml: string;
|
||||||
|
description: string;
|
||||||
|
designPrinciples: string[];
|
||||||
|
suggestedImprovements: string[];
|
||||||
|
colorScheme: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
accent?: string;
|
||||||
|
background: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface AISummaryResponse {
|
export interface AISummaryResponse {
|
||||||
summary: string;
|
summary: string;
|
||||||
keyPoints: string[];
|
keyPoints: string[];
|
||||||
@@ -37,4 +69,5 @@ export interface AIWritingResponse {
|
|||||||
export interface AIServiceInterface {
|
export interface AIServiceInterface {
|
||||||
summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
|
summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
|
||||||
assistEmailWriting(prompt: string): Promise<AIWritingResponse>;
|
assistEmailWriting(prompt: string): Promise<AIWritingResponse>;
|
||||||
|
generateTemplate(request: ITemplateGenerationRequest): Promise<AITemplateGenerationResponse>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
templateGeneration: {
|
||||||
|
systemPrompt: string;
|
||||||
|
promptTemplate: {
|
||||||
|
intro: string;
|
||||||
|
descriptionLabel: string;
|
||||||
|
themeLabel: string;
|
||||||
|
styleLabel: string;
|
||||||
|
purposeLabel: string;
|
||||||
|
colorsLabel: string;
|
||||||
|
templateNameLabel: string;
|
||||||
|
requirementsLabel: string;
|
||||||
|
structureRequirements: string;
|
||||||
|
htmlRequirements: string;
|
||||||
|
designRequirements: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,10 +5,11 @@ import { AiService } from "./ai.service";
|
|||||||
import { AiMessage } from "../../../common/enums/message.enum";
|
import { AiMessage } from "../../../common/enums/message.enum";
|
||||||
import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface";
|
import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface";
|
||||||
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
|
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
|
||||||
|
import { GenerateTemplateDto, TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
||||||
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
|
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
|
||||||
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
|
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
|
||||||
import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||||
import { IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
|
import { IEmailData, ITemplateGenerationRequest, SummarizationOptions, TemplateGenerationOptions } from "../interfaces/ai-service.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiAssistantService {
|
export class AiAssistantService {
|
||||||
@@ -37,6 +38,51 @@ export class AiAssistantService {
|
|||||||
return { assistance };
|
return { assistance };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************************* */
|
||||||
|
async generateTemplate(generateTemplateDto: GenerateTemplateDto) {
|
||||||
|
if (!generateTemplateDto.description?.trim()) {
|
||||||
|
throw new BadRequestException(AiMessage.DESCRIPTION_IS_REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateRequest: ITemplateGenerationRequest = {
|
||||||
|
description: generateTemplateDto.description.trim(),
|
||||||
|
options: this.buildTemplateGenerationOptions(generateTemplateDto),
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiResponse = await this.aiService.generateTemplate(templateRequest);
|
||||||
|
|
||||||
|
return {
|
||||||
|
template: {
|
||||||
|
name: aiResponse.templateName,
|
||||||
|
description: aiResponse.description,
|
||||||
|
structure: aiResponse.structure,
|
||||||
|
rawHtml: aiResponse.rawHtml,
|
||||||
|
colorScheme: aiResponse.colorScheme,
|
||||||
|
},
|
||||||
|
designInsights: {
|
||||||
|
designPrinciples: aiResponse.designPrinciples,
|
||||||
|
suggestedImprovements: aiResponse.suggestedImprovements,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
theme: generateTemplateDto.theme || TemplateThemeEnum.MODERN,
|
||||||
|
style: generateTemplateDto.style || TemplateStyleEnum.PROFESSIONAL,
|
||||||
|
purpose: generateTemplateDto.purpose || TemplatePurposeEnum.GENERAL,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************************* */
|
||||||
|
private buildTemplateGenerationOptions(dto: GenerateTemplateDto): TemplateGenerationOptions {
|
||||||
|
return {
|
||||||
|
theme: dto.theme,
|
||||||
|
style: dto.style,
|
||||||
|
purpose: dto.purpose,
|
||||||
|
templateName: dto.templateName,
|
||||||
|
preferredColors: dto.preferredColors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
//************************************************* */
|
//************************************************* */
|
||||||
private extractEmailData(message: MessageDetails) {
|
private extractEmailData(message: MessageDetails) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import { Inject, Injectable, InternalServerErrorException } from "@nestjs/common";
|
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
|
import parseJson from "parse-json";
|
||||||
|
|
||||||
import { AI_CONFIG } from "../../../common/constants";
|
import { AI_CONFIG } from "../../../common/constants";
|
||||||
import { AiMessage } from "../../../common/enums/message.enum";
|
import { AiMessage } from "../../../common/enums/message.enum";
|
||||||
import { AIConfig } from "../../../configs/ai.config";
|
|
||||||
import { EmailPurposeEnum, LanguageEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
import { EmailPurposeEnum, LanguageEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||||
import { AIServiceInterface, AISummaryResponse, AIWritingResponse, IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
|
import {
|
||||||
|
AIServiceInterface,
|
||||||
|
AISummaryResponse,
|
||||||
|
AITemplateGenerationResponse,
|
||||||
|
AIWritingResponse,
|
||||||
|
IEmailData,
|
||||||
|
ITemplateGenerationRequest,
|
||||||
|
SummarizationOptions,
|
||||||
|
} from "../interfaces/ai-service.interface";
|
||||||
|
import { AIConfig } from "../interfaces/ai.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiService implements AIServiceInterface {
|
export class AiService implements AIServiceInterface {
|
||||||
|
private readonly logger = new Logger(AiService.name);
|
||||||
|
|
||||||
private readonly openai: OpenAI;
|
private readonly openai: OpenAI;
|
||||||
private readonly language: LanguageEnum = LanguageEnum.PERSIAN;
|
private readonly language: LanguageEnum = LanguageEnum.PERSIAN;
|
||||||
private readonly defaultTone: SummaryToneEnum = SummaryToneEnum.PROFESSIONAL;
|
private readonly defaultTone: SummaryToneEnum = SummaryToneEnum.PROFESSIONAL;
|
||||||
@@ -47,7 +58,7 @@ export class AiService implements AIServiceInterface {
|
|||||||
const response = completion.choices[0]?.message?.content;
|
const response = completion.choices[0]?.message?.content;
|
||||||
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||||
|
|
||||||
const parsedResponse = JSON.parse(response) as AISummaryResponse;
|
const parsedResponse = this.parseJsonResponse<AISummaryResponse>(response);
|
||||||
|
|
||||||
return parsedResponse;
|
return parsedResponse;
|
||||||
}
|
}
|
||||||
@@ -74,10 +85,51 @@ export class AiService implements AIServiceInterface {
|
|||||||
const response = completion.choices[0]?.message?.content;
|
const response = completion.choices[0]?.message?.content;
|
||||||
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||||
|
|
||||||
const parsedResponse = JSON.parse(response) as AIWritingResponse;
|
const parsedResponse = this.parseJsonResponse<AIWritingResponse>(response);
|
||||||
|
|
||||||
return parsedResponse;
|
return parsedResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************************* */
|
||||||
|
async generateTemplate(request: ITemplateGenerationRequest): Promise<AITemplateGenerationResponse> {
|
||||||
|
const prompt = this.buildTemplateGenerationPrompt(request);
|
||||||
|
|
||||||
|
const completion = await this.openai.chat.completions.create({
|
||||||
|
model: this.aiConfig.defaultModel,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: this.getTemplateGenerationSystemPrompt(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 = this.parseJsonResponse<AITemplateGenerationResponse>(response);
|
||||||
|
|
||||||
|
return parsedResponse;
|
||||||
|
}
|
||||||
|
//************************************************* */
|
||||||
|
|
||||||
|
private parseJsonResponse<T>(response: string): T {
|
||||||
|
try {
|
||||||
|
return parseJson(response) as T;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("error parsing json response", `${error instanceof Error ? error.message : "Unknown error"}`);
|
||||||
|
return response as T;
|
||||||
|
// throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//************************************************* */
|
//************************************************* */
|
||||||
private getSummarizationSystemPrompt(options: SummarizationOptions): string {
|
private getSummarizationSystemPrompt(options: SummarizationOptions): string {
|
||||||
const language = options.language || this.language;
|
const language = options.language || this.language;
|
||||||
@@ -98,6 +150,11 @@ export class AiService implements AIServiceInterface {
|
|||||||
.replace("{language}", language);
|
.replace("{language}", language);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************************* */
|
||||||
|
private getTemplateGenerationSystemPrompt(): string {
|
||||||
|
return this.aiConfig.farsiPrompts.templateGeneration.systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
//************************************************* */
|
//************************************************* */
|
||||||
private buildSummarizationPrompt(emailData: IEmailData, options: SummarizationOptions): string {
|
private buildSummarizationPrompt(emailData: IEmailData, options: SummarizationOptions): string {
|
||||||
const template = this.aiConfig.farsiPrompts.summarization.promptTemplate;
|
const template = this.aiConfig.farsiPrompts.summarization.promptTemplate;
|
||||||
@@ -119,4 +176,39 @@ export class AiService implements AIServiceInterface {
|
|||||||
|
|
||||||
return prompt;
|
return prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************************* */
|
||||||
|
private buildTemplateGenerationPrompt(request: ITemplateGenerationRequest): string {
|
||||||
|
const template = this.aiConfig.farsiPrompts.templateGeneration.promptTemplate;
|
||||||
|
let prompt = `${template.intro}\n\n`;
|
||||||
|
|
||||||
|
prompt += `${template.descriptionLabel} ${request.description}\n\n`;
|
||||||
|
|
||||||
|
if (request.options.theme) {
|
||||||
|
prompt += `${template.themeLabel} ${request.options.theme}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.options.style) {
|
||||||
|
prompt += `${template.styleLabel} ${request.options.style}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.options.purpose) {
|
||||||
|
prompt += `${template.purposeLabel} ${request.options.purpose}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.options.preferredColors) {
|
||||||
|
prompt += `${template.colorsLabel} ${request.options.preferredColors}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.options.templateName) {
|
||||||
|
prompt += `${template.templateNameLabel} ${request.options.templateName}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `\n${template.requirementsLabel}\n`;
|
||||||
|
prompt += `${template.structureRequirements}\n`;
|
||||||
|
prompt += `${template.htmlRequirements}\n`;
|
||||||
|
prompt += `${template.designRequirements}\n`;
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { UserRepository } from "../../users/repositories/user.repository";
|
|||||||
import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant";
|
import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant";
|
||||||
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
||||||
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
||||||
import { EmailRecipientDto, EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
import { EmailRecipientDto, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
||||||
import { PriorityEnum } from "../enums/email-header.enum";
|
import { PriorityEnum } from "../enums/email-header.enum";
|
||||||
import { IEmailMap } from "../interfaces/email-map.interface";
|
import { IEmailMap } from "../interfaces/email-map.interface";
|
||||||
|
|
||||||
@@ -38,8 +38,6 @@ export class EmailService {
|
|||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async sendEmail(wildduckUserId: string, userId: string, sendEmailDto: SendEmailDto) {
|
async sendEmail(wildduckUserId: string, userId: string, sendEmailDto: SendEmailDto) {
|
||||||
this.logger.log(`Sending ${EmailType.GENERAL} email from user ${wildduckUserId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`);
|
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
|
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
|
||||||
if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND);
|
||||||
this.logger.log(`Processing email through business template for business: ${user.business.id}`);
|
this.logger.log(`Processing email through business template for business: ${user.business.id}`);
|
||||||
|
|||||||
@@ -32,17 +32,67 @@ export enum BackgroundSize {
|
|||||||
ROUND = "round",
|
ROUND = "round",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum BackgroundPosition {
|
||||||
|
CENTER = "center center",
|
||||||
|
TOP_LEFT = "top left",
|
||||||
|
TOP_CENTER = "top center",
|
||||||
|
TOP_RIGHT = "top right",
|
||||||
|
CENTER_LEFT = "center left",
|
||||||
|
CENTER_RIGHT = "center right",
|
||||||
|
BOTTOM_LEFT = "bottom left",
|
||||||
|
BOTTOM_CENTER = "bottom center",
|
||||||
|
BOTTOM_RIGHT = "bottom right",
|
||||||
|
}
|
||||||
|
|
||||||
export enum SectionName {
|
export enum SectionName {
|
||||||
HEADER = "header",
|
HEADER = "header",
|
||||||
CONTENT = "content",
|
CONTENT = "content",
|
||||||
FOOTER = "footer",
|
FOOTER = "footer",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PersonalityDataType = {
|
// New enums for element selection
|
||||||
header: SectionType;
|
export enum ElementType {
|
||||||
content: SectionType;
|
TEXT = "text",
|
||||||
footer: SectionType;
|
BUTTON = "button",
|
||||||
};
|
IMAGE = "image",
|
||||||
|
SOCIAL = "social",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Social network types
|
||||||
|
export enum SocialNetworkType {
|
||||||
|
FACEBOOK = "facebook",
|
||||||
|
TWITTER = "twitter",
|
||||||
|
INSTAGRAM = "instagram",
|
||||||
|
LINKEDIN = "linkedin",
|
||||||
|
YOUTUBE = "youtube",
|
||||||
|
TELEGRAM = "telegram",
|
||||||
|
WHATSAPP = "whatsapp",
|
||||||
|
TIKTOK = "tiktok",
|
||||||
|
PINTEREST = "pinterest",
|
||||||
|
SNAPCHAT = "snapchat",
|
||||||
|
GITHUB = "github",
|
||||||
|
DISCORD = "discord",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SocialIconSize {
|
||||||
|
SMALL = "small",
|
||||||
|
MEDIUM = "medium",
|
||||||
|
LARGE = "large",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SocialIconStyle {
|
||||||
|
CIRCLE = "circle",
|
||||||
|
SQUARE = "square",
|
||||||
|
ROUNDED = "rounded",
|
||||||
|
}
|
||||||
|
|
||||||
|
// New type for selected element
|
||||||
|
export type SelectedElement = {
|
||||||
|
type: ElementType;
|
||||||
|
elementId: string;
|
||||||
|
itemId: string;
|
||||||
|
sectionKey: keyof PersonalityDataType;
|
||||||
|
} | null;
|
||||||
|
|
||||||
export type SectionType = {
|
export type SectionType = {
|
||||||
columnsCount: number;
|
columnsCount: number;
|
||||||
@@ -56,6 +106,7 @@ export type SectionItemType = {
|
|||||||
texts: TextType[];
|
texts: TextType[];
|
||||||
buttons: ButtonType[];
|
buttons: ButtonType[];
|
||||||
images: ImageType[];
|
images: ImageType[];
|
||||||
|
socials: SocialType[];
|
||||||
alignment?: HorizontalAlignment;
|
alignment?: HorizontalAlignment;
|
||||||
verticalAlignment?: VerticalAlignment;
|
verticalAlignment?: VerticalAlignment;
|
||||||
style?: {
|
style?: {
|
||||||
@@ -103,14 +154,33 @@ export type ImageType = {
|
|||||||
verticalAlignment?: VerticalAlignment;
|
verticalAlignment?: VerticalAlignment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SocialType = {
|
||||||
|
id: string;
|
||||||
|
networkType: SocialNetworkType;
|
||||||
|
link: string;
|
||||||
|
size: SocialIconSize;
|
||||||
|
style: SocialIconStyle;
|
||||||
|
color?: string;
|
||||||
|
alignment?: HorizontalAlignment;
|
||||||
|
verticalAlignment?: VerticalAlignment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TemplateType = {
|
||||||
|
name: string;
|
||||||
|
rawHtml: string;
|
||||||
|
structure: PersonalityDataType;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
//main type for the template structure
|
||||||
|
export type PersonalityDataType = {
|
||||||
|
header: SectionType;
|
||||||
|
content: SectionType;
|
||||||
|
footer: SectionType;
|
||||||
|
};
|
||||||
|
|
||||||
export interface EmailContent {
|
export interface EmailContent {
|
||||||
text?: string;
|
text?: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessedEmailResult {
|
|
||||||
content: string;
|
|
||||||
isHtml: boolean;
|
|
||||||
hasTemplate: boolean;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,52 +1,36 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { TemplatesService } from "./templates.service";
|
import { TemplatesService } from "./templates.service";
|
||||||
import { UserMessage } from "../../../common/enums/message.enum";
|
import { UserMessage } from "../../../common/enums/message.enum";
|
||||||
import { UserRepository } from "../../users/repositories/user.repository";
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { EmailContent, ProcessedEmailResult } from "../interfaces/structure.interface";
|
import { EmailContent } from "../interfaces/structure.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TemplateProcessorService {
|
export class TemplateProcessorService {
|
||||||
private readonly logger = new Logger(TemplateProcessorService.name);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly templatesService: TemplatesService,
|
private readonly templatesService: TemplatesService,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//**************************************************** */
|
//**************************************************** */
|
||||||
async processEmailContent(businessId: string, userId: string, emailContent: EmailContent): Promise<ProcessedEmailResult> {
|
async processEmailContent(businessId: string, userId: string, emailContent: EmailContent) {
|
||||||
try {
|
const selectedTemplate = await this.getSelectedTemplate(businessId, userId);
|
||||||
// Get selected template for the business
|
|
||||||
const selectedTemplate = await this.getSelectedTemplate(businessId, userId);
|
|
||||||
|
|
||||||
if (!selectedTemplate) {
|
if (!selectedTemplate) {
|
||||||
// No template - return original text content
|
|
||||||
return {
|
|
||||||
content: emailContent.text || emailContent.html || "",
|
|
||||||
isHtml: false,
|
|
||||||
hasTemplate: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process with template
|
|
||||||
const processedContent = this.injectContentIntoTemplate(selectedTemplate.rawHtml, emailContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: processedContent,
|
|
||||||
isHtml: true,
|
|
||||||
hasTemplate: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to process email for business ${businessId}:`, error);
|
|
||||||
|
|
||||||
// Fallback to original content
|
|
||||||
return {
|
return {
|
||||||
content: emailContent.text || emailContent.html || "",
|
content: emailContent.text || emailContent.html || "",
|
||||||
isHtml: false,
|
isHtml: false,
|
||||||
hasTemplate: false,
|
hasTemplate: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processedContent = await this.injectContentIntoTemplate(selectedTemplate.rawHtml, emailContent);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: processedContent,
|
||||||
|
isHtml: true,
|
||||||
|
hasTemplate: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//**************************************************** */
|
//**************************************************** */
|
||||||
@@ -54,14 +38,13 @@ export class TemplateProcessorService {
|
|||||||
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, deletedAt: null }, { populate: ["template"] });
|
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, deletedAt: null }, { populate: ["template"] });
|
||||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
if (user.template) return user.template;
|
if (user.template && user.template.rawHtml) return user.template;
|
||||||
|
|
||||||
const templates = await this.templatesService.getTemplatesByBusinessId(businessId);
|
return this.templatesService.getSelectedTemplateByBusinessId(businessId);
|
||||||
return templates.find((template) => template.selected) || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//**************************************************** */
|
//**************************************************** */
|
||||||
private injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent): string {
|
private async injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent) {
|
||||||
if (!templateHtml) {
|
if (!templateHtml) {
|
||||||
return emailContent.text || emailContent.html || "";
|
return emailContent.text || emailContent.html || "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ export class TemplatesService {
|
|||||||
return this.templateRepository.find({ business: { id: businessId }, deletedAt: null }, { populate: ["business"] });
|
return this.templateRepository.find({ business: { id: businessId }, deletedAt: null }, { populate: ["business"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//**************************************************** */
|
||||||
|
async getSelectedTemplateByBusinessId(businessId: string) {
|
||||||
|
return this.templateRepository.findOne({ business: { id: businessId }, deletedAt: null, selected: true }, { populate: ["business"] });
|
||||||
|
}
|
||||||
|
|
||||||
//**************************************************** */
|
//**************************************************** */
|
||||||
async setSelectedTemplate(id: string, businessId: string) {
|
async setSelectedTemplate(id: string, businessId: string) {
|
||||||
const template = await this.templateRepository.findTemplateByIdWithoutRawHtmlAndStructure(id, businessId);
|
const template = await this.templateRepository.findTemplateByIdWithoutRawHtmlAndStructure(id, businessId);
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export class User extends BaseEntity {
|
|||||||
@OneToMany(() => RefreshToken, (token) => token.user)
|
@OneToMany(() => RefreshToken, (token) => token.user)
|
||||||
refreshTokens = new Collection<RefreshToken>(this);
|
refreshTokens = new Collection<RefreshToken>(this);
|
||||||
|
|
||||||
|
//=========================
|
||||||
|
|
||||||
@ManyToOne(() => Role, { deleteRule: "restrict" })
|
@ManyToOne(() => Role, { deleteRule: "restrict" })
|
||||||
role!: Role;
|
role!: Role;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user