Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
330
marketing-agent/services/orchestrator/src/routes/campaigns.js
Normal file
330
marketing-agent/services/orchestrator/src/routes/campaigns.js
Normal file
@@ -0,0 +1,330 @@
|
||||
import { Campaign } from '../models/Campaign.js';
|
||||
import { CampaignExecutor } from '../services/CampaignExecutor.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
const campaignExecutor = CampaignExecutor.getInstance();
|
||||
|
||||
const routes = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/campaigns',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { status, type, page = 1, limit = 10 } = request.query;
|
||||
const where = {};
|
||||
|
||||
if (status) where.status = status;
|
||||
if (type) where.type = type;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows: campaigns } = await Campaign.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
campaigns,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
totalPages: Math.ceil(count / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch campaigns:', error);
|
||||
throw Boom.internal('Failed to fetch campaigns');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/campaigns/{id}',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const campaign = await Campaign.findByPk(request.params.id);
|
||||
|
||||
if (!campaign) {
|
||||
throw Boom.notFound('Campaign not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: campaign
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch campaign:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to fetch campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/campaigns',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
goals,
|
||||
targetAudience,
|
||||
message,
|
||||
startDate,
|
||||
endDate,
|
||||
budget
|
||||
} = request.payload;
|
||||
|
||||
const campaign = await Campaign.create({
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
goals,
|
||||
targetAudience,
|
||||
metadata: { message },
|
||||
startDate,
|
||||
endDate,
|
||||
budget,
|
||||
createdBy: request.auth.credentials?.user?.id || 'system',
|
||||
status: 'draft'
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: campaign
|
||||
}).code(201);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create campaign:', error);
|
||||
throw Boom.internal('Failed to create campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/campaigns/{id}',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const campaign = await Campaign.findByPk(request.params.id);
|
||||
|
||||
if (!campaign) {
|
||||
throw Boom.notFound('Campaign not found');
|
||||
}
|
||||
|
||||
// Don't allow updating active campaigns
|
||||
if (campaign.status === 'active') {
|
||||
throw Boom.badRequest('Cannot update active campaign');
|
||||
}
|
||||
|
||||
const allowedUpdates = [
|
||||
'name', 'description', 'goals', 'targetAudience',
|
||||
'startDate', 'endDate', 'budget', 'metadata'
|
||||
];
|
||||
|
||||
allowedUpdates.forEach(field => {
|
||||
if (request.payload[field] !== undefined) {
|
||||
campaign[field] = request.payload[field];
|
||||
}
|
||||
});
|
||||
|
||||
await campaign.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: campaign
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to update campaign:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to update campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/campaigns/{id}',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const campaign = await Campaign.findByPk(request.params.id);
|
||||
|
||||
if (!campaign) {
|
||||
throw Boom.notFound('Campaign not found');
|
||||
}
|
||||
|
||||
// Don't allow deleting active campaigns
|
||||
if (campaign.status === 'active') {
|
||||
throw Boom.badRequest('Cannot delete active campaign');
|
||||
}
|
||||
|
||||
await campaign.destroy();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Campaign deleted successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete campaign:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to delete campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/campaigns/{id}/execute',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const result = await campaignExecutor.executeCampaign(request.params.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to execute campaign:', error);
|
||||
throw Boom.internal(error.message || 'Failed to execute campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/campaigns/{id}/pause',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const result = await campaignExecutor.pauseCampaign(request.params.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to pause campaign:', error);
|
||||
throw Boom.internal(error.message || 'Failed to pause campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/campaigns/{id}/resume',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const result = await campaignExecutor.resumeCampaign(request.params.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to resume campaign:', error);
|
||||
throw Boom.internal(error.message || 'Failed to resume campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/campaigns/{id}/progress',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const progress = await campaignExecutor.getCampaignProgress(request.params.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: progress
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get campaign progress:', error);
|
||||
throw Boom.internal(error.message || 'Failed to get campaign progress');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/campaigns/{id}/statistics',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const campaign = await Campaign.findByPk(request.params.id);
|
||||
|
||||
if (!campaign) {
|
||||
throw Boom.notFound('Campaign not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: campaign.statistics || {}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get campaign statistics:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to get campaign statistics');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/campaigns/{id}/clone',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const sourceCampaign = await Campaign.findByPk(request.params.id);
|
||||
|
||||
if (!sourceCampaign) {
|
||||
throw Boom.notFound('Campaign not found');
|
||||
}
|
||||
|
||||
const clonedData = sourceCampaign.toJSON();
|
||||
delete clonedData.id;
|
||||
delete clonedData.createdAt;
|
||||
delete clonedData.updatedAt;
|
||||
|
||||
clonedData.name = `${clonedData.name} (Copy)`;
|
||||
clonedData.status = 'draft';
|
||||
clonedData.statistics = {
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
failedTasks: 0,
|
||||
messagesSent: 0,
|
||||
conversionsAchieved: 0,
|
||||
totalCost: 0
|
||||
};
|
||||
|
||||
const clonedCampaign = await Campaign.create(clonedData);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: clonedCampaign
|
||||
}).code(201);
|
||||
} catch (error) {
|
||||
logger.error('Failed to clone campaign:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to clone campaign');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default routes;
|
||||
11
marketing-agent/services/orchestrator/src/routes/index.js
Normal file
11
marketing-agent/services/orchestrator/src/routes/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import campaignRoutes from './campaigns.js';
|
||||
import messageRoutes from './messages.js';
|
||||
import workflowRoutes from './workflows.js';
|
||||
|
||||
const routes = [
|
||||
...campaignRoutes,
|
||||
...messageRoutes,
|
||||
...workflowRoutes
|
||||
];
|
||||
|
||||
export default routes;
|
||||
481
marketing-agent/services/orchestrator/src/routes/messages.js
Normal file
481
marketing-agent/services/orchestrator/src/routes/messages.js
Normal file
@@ -0,0 +1,481 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import axios from 'axios';
|
||||
import Boom from '@hapi/boom';
|
||||
import { TemplateClient } from '../../../../shared/utils/templateClient.js';
|
||||
|
||||
// Initialize template client
|
||||
const templateClient = new TemplateClient({
|
||||
baseURL: process.env.TEMPLATE_SERVICE_URL || 'http://localhost:3010'
|
||||
});
|
||||
|
||||
// In-memory storage for templates (should be moved to database)
|
||||
const messageTemplates = new Map();
|
||||
|
||||
// Predefined templates
|
||||
const defaultTemplates = [
|
||||
{
|
||||
id: 'welcome-1',
|
||||
name: 'Welcome Message',
|
||||
content: 'Welcome to {{company}}! 👋\n\nWe\'re excited to have you here. {{personalized_message}}',
|
||||
category: 'onboarding',
|
||||
variables: ['company', 'personalized_message'],
|
||||
usage: 0
|
||||
},
|
||||
{
|
||||
id: 'promo-1',
|
||||
name: 'Special Offer',
|
||||
content: '🎉 Special Offer Alert!\n\n{{offer_title}}\n\n✨ {{discount}}% OFF\n📅 Valid until: {{end_date}}\n\n👉 {{cta_text}}',
|
||||
category: 'promotional',
|
||||
variables: ['offer_title', 'discount', 'end_date', 'cta_text'],
|
||||
usage: 0
|
||||
},
|
||||
{
|
||||
id: 'reminder-1',
|
||||
name: 'Event Reminder',
|
||||
content: '⏰ Reminder: {{event_name}}\n\n📅 Date: {{event_date}}\n📍 Location: {{location}}\n\nDon\'t forget to {{action}}!',
|
||||
category: 'reminder',
|
||||
variables: ['event_name', 'event_date', 'location', 'action'],
|
||||
usage: 0
|
||||
},
|
||||
{
|
||||
id: 'survey-1',
|
||||
name: 'Feedback Request',
|
||||
content: 'Hi {{name}}! 👋\n\nWe\'d love to hear your thoughts about {{product_service}}.\n\nCould you spare 2 minutes for a quick survey?\n\n🔗 {{survey_link}}\n\nThank you! 🙏',
|
||||
category: 'engagement',
|
||||
variables: ['name', 'product_service', 'survey_link'],
|
||||
usage: 0
|
||||
}
|
||||
];
|
||||
|
||||
// Initialize default templates
|
||||
defaultTemplates.forEach(template => {
|
||||
messageTemplates.set(template.id, template);
|
||||
});
|
||||
|
||||
const routes = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/messages/templates',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { category } = request.query;
|
||||
let templates = Array.from(messageTemplates.values());
|
||||
|
||||
if (category) {
|
||||
templates = templates.filter(t => t.category === category);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: templates
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch templates:', error);
|
||||
throw Boom.internal('Failed to fetch templates');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/messages/templates/{id}',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const template = messageTemplates.get(request.params.id);
|
||||
|
||||
if (!template) {
|
||||
throw Boom.notFound('Template not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: template
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to fetch template');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/messages/templates',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { name, content, category, variables = [] } = request.payload;
|
||||
|
||||
if (!name || !content || !category) {
|
||||
throw Boom.badRequest('Missing required fields: name, content, category');
|
||||
}
|
||||
|
||||
const template = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name,
|
||||
content,
|
||||
category,
|
||||
variables,
|
||||
usage: 0,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
messageTemplates.set(template.id, template);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: template
|
||||
}).code(201);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to create template');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/messages/templates/{id}',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const template = messageTemplates.get(request.params.id);
|
||||
|
||||
if (!template) {
|
||||
throw Boom.notFound('Template not found');
|
||||
}
|
||||
|
||||
const { name, content, category, variables } = request.payload;
|
||||
|
||||
if (name) template.name = name;
|
||||
if (content) template.content = content;
|
||||
if (category) template.category = category;
|
||||
if (variables) template.variables = variables;
|
||||
|
||||
template.updatedAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: template
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to update template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to update template');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/messages/templates/{id}',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
if (!messageTemplates.has(request.params.id)) {
|
||||
throw Boom.notFound('Template not found');
|
||||
}
|
||||
|
||||
messageTemplates.delete(request.params.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Template deleted successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to delete template');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/messages/templates/{id}/preview',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const template = messageTemplates.get(request.params.id);
|
||||
|
||||
if (!template) {
|
||||
throw Boom.notFound('Template not found');
|
||||
}
|
||||
|
||||
const { variables } = request.payload;
|
||||
let content = template.content;
|
||||
|
||||
// Replace variables
|
||||
if (variables) {
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
|
||||
content = content.replace(regex, value);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
original: template.content,
|
||||
preview: content,
|
||||
variables: template.variables
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to preview template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
throw Boom.internal('Failed to preview template');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/messages/history',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
// TODO: Implement actual message history from database
|
||||
const mockHistory = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
campaignId: 'c1',
|
||||
campaignName: 'Summer Sale 2025',
|
||||
recipient: '+1234567890',
|
||||
content: 'Special offer: 20% off all items!',
|
||||
status: 'delivered',
|
||||
sentAt: new Date(Date.now() - 3600000).toISOString(),
|
||||
deliveredAt: new Date(Date.now() - 3590000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
campaignId: 'c2',
|
||||
campaignName: 'Welcome Series',
|
||||
recipient: '+0987654321',
|
||||
content: 'Welcome to our service!',
|
||||
status: 'delivered',
|
||||
sentAt: new Date(Date.now() - 7200000).toISOString(),
|
||||
deliveredAt: new Date(Date.now() - 7190000).toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: mockHistory
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch message history:', error);
|
||||
throw Boom.internal('Failed to fetch message history');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/messages/send',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { accountId, recipient, content, parseMode = 'md' } = request.payload;
|
||||
|
||||
if (!accountId || !recipient || !content) {
|
||||
throw Boom.badRequest('Missing required fields: accountId, recipient, content');
|
||||
}
|
||||
|
||||
// Send via gramjs-adapter
|
||||
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
|
||||
const response = await axios.post(`${gramjsUrl}/messages/send`, {
|
||||
accountId,
|
||||
chatId: recipient,
|
||||
message: content,
|
||||
parseMode
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('Failed to send message:', error);
|
||||
if (error.response?.status === 400) {
|
||||
throw Boom.badRequest(error.response.data.error || 'Invalid request');
|
||||
}
|
||||
throw Boom.internal(error.response?.data?.error || 'Failed to send message');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/messages/send-with-template',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const {
|
||||
accountId,
|
||||
recipient,
|
||||
templateId,
|
||||
templateName,
|
||||
language = 'en',
|
||||
data = {},
|
||||
parseMode = 'md'
|
||||
} = request.payload;
|
||||
|
||||
if (!accountId || !recipient || (!templateId && !templateName)) {
|
||||
throw Boom.badRequest('Missing required fields: accountId, recipient, and either templateId or templateName');
|
||||
}
|
||||
|
||||
// Render template
|
||||
let renderResult;
|
||||
if (templateId) {
|
||||
renderResult = await templateClient.renderTemplate(templateId, data);
|
||||
} else {
|
||||
renderResult = await templateClient.renderTemplateByName(templateName, language, data);
|
||||
}
|
||||
|
||||
if (!renderResult.success) {
|
||||
throw Boom.badRequest(renderResult.error || 'Failed to render template');
|
||||
}
|
||||
|
||||
// Send via gramjs-adapter
|
||||
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
|
||||
const response = await axios.post(`${gramjsUrl}/messages/send`, {
|
||||
accountId,
|
||||
chatId: recipient,
|
||||
message: renderResult.content,
|
||||
parseMode: renderResult.format === 'html' ? 'html' : parseMode
|
||||
});
|
||||
|
||||
return {
|
||||
...response.data,
|
||||
template: {
|
||||
id: templateId,
|
||||
name: templateName,
|
||||
format: renderResult.format
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to send message with template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
if (error.response?.status === 400) {
|
||||
throw Boom.badRequest(error.response.data.error || 'Invalid request');
|
||||
}
|
||||
throw Boom.internal(error.response?.data?.error || 'Failed to send message with template');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/messages/bulk',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { accountId, recipients, content, parseMode = 'md' } = request.payload;
|
||||
|
||||
if (!accountId || !recipients || !content) {
|
||||
throw Boom.badRequest('Missing required fields: accountId, recipients, content');
|
||||
}
|
||||
|
||||
// Send via gramjs-adapter
|
||||
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
|
||||
const response = await axios.post(`${gramjsUrl}/messages/bulk`, {
|
||||
accountId,
|
||||
recipients,
|
||||
message: content,
|
||||
parseMode
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('Failed to send bulk messages:', error);
|
||||
throw Boom.internal(error.response?.data?.error || 'Failed to send bulk messages');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/messages/bulk-with-template',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const {
|
||||
accountId,
|
||||
recipients,
|
||||
templateId,
|
||||
templateName,
|
||||
language = 'en',
|
||||
data = {},
|
||||
personalizedData = {},
|
||||
parseMode = 'md'
|
||||
} = request.payload;
|
||||
|
||||
if (!accountId || !recipients || (!templateId && !templateName)) {
|
||||
throw Boom.badRequest('Missing required fields: accountId, recipients, and either templateId or templateName');
|
||||
}
|
||||
|
||||
// Prepare batch render requests
|
||||
const renderRequests = recipients.map(recipient => {
|
||||
const recipientData = {
|
||||
...data,
|
||||
...(personalizedData[recipient] || {})
|
||||
};
|
||||
|
||||
return {
|
||||
templateId,
|
||||
templateName,
|
||||
language,
|
||||
data: recipientData,
|
||||
recipient
|
||||
};
|
||||
});
|
||||
|
||||
// Batch render templates
|
||||
const batchResult = await templateClient.batchRenderTemplates(renderRequests);
|
||||
|
||||
if (!batchResult.success) {
|
||||
throw Boom.badRequest(batchResult.error || 'Failed to render templates');
|
||||
}
|
||||
|
||||
// Prepare messages for bulk send
|
||||
const messages = batchResult.results.map(result => ({
|
||||
recipient: result.recipient,
|
||||
content: result.content,
|
||||
format: result.format
|
||||
}));
|
||||
|
||||
// Send via gramjs-adapter
|
||||
const gramjsUrl = process.env.GRAMJS_ADAPTER_URL || 'http://gramjs-adapter:3003';
|
||||
const response = await axios.post(`${gramjsUrl}/messages/bulk-custom`, {
|
||||
accountId,
|
||||
messages: messages.map(msg => ({
|
||||
chatId: msg.recipient,
|
||||
message: msg.content,
|
||||
parseMode: msg.format === 'html' ? 'html' : parseMode
|
||||
}))
|
||||
});
|
||||
|
||||
return {
|
||||
...response.data,
|
||||
template: {
|
||||
id: templateId,
|
||||
name: templateName,
|
||||
recipientCount: recipients.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to send bulk messages with template:', error);
|
||||
if (error.isBoom) throw error;
|
||||
if (error.response?.status === 400) {
|
||||
throw Boom.badRequest(error.response.data.error || 'Invalid request');
|
||||
}
|
||||
throw Boom.internal(error.response?.data?.error || 'Failed to send bulk messages with template');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/workflows',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
// TODO: Implement workflow management
|
||||
return {
|
||||
success: true,
|
||||
data: []
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch workflows:', error);
|
||||
throw Boom.internal('Failed to fetch workflows');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default routes;
|
||||
Reference in New Issue
Block a user