samarthya-bot 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/backend/.env.example +23 -0
- package/backend/bin/samarthya.js +384 -0
- package/backend/config/constants.js +71 -0
- package/backend/config/db.js +13 -0
- package/backend/controllers/auditController.js +86 -0
- package/backend/controllers/authController.js +154 -0
- package/backend/controllers/chatController.js +158 -0
- package/backend/controllers/fileController.js +268 -0
- package/backend/controllers/platformController.js +54 -0
- package/backend/controllers/screenController.js +91 -0
- package/backend/controllers/telegramController.js +120 -0
- package/backend/controllers/toolsController.js +56 -0
- package/backend/controllers/whatsappController.js +214 -0
- package/backend/fix_toolRegistry.js +25 -0
- package/backend/middleware/auth.js +28 -0
- package/backend/models/AuditLog.js +28 -0
- package/backend/models/BackgroundJob.js +13 -0
- package/backend/models/Conversation.js +40 -0
- package/backend/models/Memory.js +17 -0
- package/backend/models/User.js +24 -0
- package/backend/package-lock.json +3766 -0
- package/backend/package.json +41 -0
- package/backend/public/assets/index-Ckf0GO1B.css +1 -0
- package/backend/public/assets/index-Do4jNsZS.js +19 -0
- package/backend/public/assets/index-Ui-pyZvK.js +25 -0
- package/backend/public/favicon.svg +17 -0
- package/backend/public/index.html +18 -0
- package/backend/public/manifest.json +16 -0
- package/backend/routes/audit.js +9 -0
- package/backend/routes/auth.js +11 -0
- package/backend/routes/chat.js +11 -0
- package/backend/routes/files.js +14 -0
- package/backend/routes/platform.js +18 -0
- package/backend/routes/screen.js +10 -0
- package/backend/routes/telegram.js +8 -0
- package/backend/routes/tools.js +9 -0
- package/backend/routes/whatsapp.js +11 -0
- package/backend/server.js +134 -0
- package/backend/services/background/backgroundService.js +81 -0
- package/backend/services/llm/llmService.js +444 -0
- package/backend/services/memory/memoryService.js +159 -0
- package/backend/services/planner/plannerService.js +182 -0
- package/backend/services/security/securityService.js +166 -0
- package/backend/services/telegram/telegramService.js +49 -0
- package/backend/services/tools/toolRegistry.js +879 -0
- package/backend/services/whatsapp/whatsappService.js +254 -0
- package/backend/test_email.js +29 -0
- package/backend/test_parser.js +10 -0
- package/package.json +49 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const llmService = require('../llm/llmService');
|
|
2
|
+
const toolRegistry = require('../tools/toolRegistry');
|
|
3
|
+
const securityService = require('../security/securityService');
|
|
4
|
+
const memoryService = require('../memory/memoryService');
|
|
5
|
+
const AuditLog = require('../../models/AuditLog');
|
|
6
|
+
|
|
7
|
+
class PlannerService {
|
|
8
|
+
/**
|
|
9
|
+
* Process a user message through the full pipeline:
|
|
10
|
+
* 1. Security scan input
|
|
11
|
+
* 2. Load user context & memories
|
|
12
|
+
* 3. Build system prompt with available tools
|
|
13
|
+
* 4. Send to LLM
|
|
14
|
+
* 5. Parse tool calls if any
|
|
15
|
+
* 6. Execute tools (with permission checks)
|
|
16
|
+
* 7. Log audit trail
|
|
17
|
+
* 8. Extract & store new memories
|
|
18
|
+
* 9. Return final response
|
|
19
|
+
*/
|
|
20
|
+
async processMessage(user, conversationHistory, userMessage, onProgress = null) {
|
|
21
|
+
const result = {
|
|
22
|
+
response: '',
|
|
23
|
+
toolCalls: [],
|
|
24
|
+
sensitiveDataWarnings: [],
|
|
25
|
+
tokensUsed: 0,
|
|
26
|
+
model: '',
|
|
27
|
+
language: llmService.detectLanguage(userMessage)
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Step 1: Security scan on user input
|
|
31
|
+
const inputScan = securityService.scanForSensitiveData(userMessage);
|
|
32
|
+
if (inputScan.length > 0) {
|
|
33
|
+
result.sensitiveDataWarnings = inputScan;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Step 2: Load memories
|
|
37
|
+
const memories = await memoryService.getUserContext(user._id || user.id);
|
|
38
|
+
|
|
39
|
+
// Step 3: Get tools for user's active pack
|
|
40
|
+
const tools = toolRegistry.getToolsForPack(user.activePack || 'personal');
|
|
41
|
+
|
|
42
|
+
// Step 4: Build system prompt and call LLM
|
|
43
|
+
const systemPrompt = llmService.buildSystemPrompt(user, tools, memories);
|
|
44
|
+
|
|
45
|
+
const messages = [
|
|
46
|
+
...conversationHistory.map(m => ({ role: m.role, content: m.content })),
|
|
47
|
+
{ role: 'user', content: userMessage }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const llmResponse = await llmService.chat(messages, systemPrompt, user);
|
|
51
|
+
result.response = llmResponse.content;
|
|
52
|
+
result.tokensUsed = llmResponse.tokensUsed;
|
|
53
|
+
result.model = llmResponse.model;
|
|
54
|
+
|
|
55
|
+
// Step 5: Execute Agentic Loop (up to 20 steps for massive multi-tasking)
|
|
56
|
+
let loopCount = 0;
|
|
57
|
+
const maxLoops = 20;
|
|
58
|
+
let currentMessages = [...messages, { role: 'assistant', content: llmResponse.content }];
|
|
59
|
+
let currentResponse = llmResponse.content;
|
|
60
|
+
|
|
61
|
+
while (loopCount < maxLoops) {
|
|
62
|
+
loopCount++;
|
|
63
|
+
|
|
64
|
+
// Extract planning / thought process and stream it to the user
|
|
65
|
+
const thoughtProcess = currentResponse.replace(/```(?:tool_call|json)?\s*[\s\S]*?```/g, '').trim();
|
|
66
|
+
if (thoughtProcess && onProgress) {
|
|
67
|
+
// Send only if there's substantial text, maybe filtering short OKs
|
|
68
|
+
if (thoughtProcess.length > 10) {
|
|
69
|
+
await onProgress(`\n${thoughtProcess}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const toolCalls = llmService.parseToolCalls(currentResponse);
|
|
74
|
+
if (toolCalls.length === 0) {
|
|
75
|
+
// Done! No more tools to call
|
|
76
|
+
result.response = currentResponse;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stepToolResults = [];
|
|
81
|
+
for (const tc of toolCalls) {
|
|
82
|
+
const toolResult = {
|
|
83
|
+
toolName: tc.tool,
|
|
84
|
+
arguments: tc.args,
|
|
85
|
+
status: 'pending',
|
|
86
|
+
riskLevel: securityService.assessRisk(tc.tool),
|
|
87
|
+
result: null
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const permCheck = securityService.requiresPermission(tc.tool, user.permissions);
|
|
91
|
+
if (permCheck === 'blocked') {
|
|
92
|
+
toolResult.status = 'blocked';
|
|
93
|
+
toolResult.result = `🚫 Tool "${tc.tool}" blocked by your permission settings.`;
|
|
94
|
+
} else {
|
|
95
|
+
try {
|
|
96
|
+
toolResult.status = 'running';
|
|
97
|
+
console.log(`🔧 Executing tool: ${tc.tool}`, JSON.stringify(tc.args));
|
|
98
|
+
const execResult = await toolRegistry.executeTool(tc.tool, tc.args, user);
|
|
99
|
+
toolResult.result = execResult.result;
|
|
100
|
+
if (execResult.notificationParams) toolResult.notificationParams = execResult.notificationParams;
|
|
101
|
+
toolResult.status = execResult.success ? 'completed' : 'failed';
|
|
102
|
+
console.log(`✅ Tool ${tc.tool}: ${toolResult.status}`);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
toolResult.status = 'failed';
|
|
105
|
+
toolResult.result = `Error: ${error.message}`;
|
|
106
|
+
console.error(`❌ Tool ${tc.tool} error:`, error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
stepToolResults.push(toolResult);
|
|
111
|
+
|
|
112
|
+
// Audit log
|
|
113
|
+
try {
|
|
114
|
+
await AuditLog.create({
|
|
115
|
+
userId: user._id || user.id,
|
|
116
|
+
action: `Tool: ${tc.tool}`,
|
|
117
|
+
category: toolRegistry.getTool(tc.tool)?.category || 'tool',
|
|
118
|
+
details: {
|
|
119
|
+
toolName: tc.tool,
|
|
120
|
+
input: tc.args,
|
|
121
|
+
output: toolResult.result,
|
|
122
|
+
riskLevel: toolResult.riskLevel,
|
|
123
|
+
permissionGranted: toolResult.status !== 'blocked'
|
|
124
|
+
},
|
|
125
|
+
status: toolResult.status === 'completed' ? 'success' :
|
|
126
|
+
toolResult.status === 'blocked' ? 'blocked' : 'failed'
|
|
127
|
+
});
|
|
128
|
+
} catch (e) { console.error('Audit log error:', e); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result.toolCalls.push(...stepToolResults);
|
|
132
|
+
|
|
133
|
+
if (stepToolResults.some(tc => tc.status === 'completed' || tc.status === 'failed')) {
|
|
134
|
+
const toolResultsText = stepToolResults
|
|
135
|
+
.map(tc => {
|
|
136
|
+
let text = `Tool "${tc.toolName}" result:\n${tc.result}`;
|
|
137
|
+
if (tc.status === 'failed') {
|
|
138
|
+
text += `\n\nFAILURE DETECTED. Please analyze the error, auto-correct your parameters, and retry.`;
|
|
139
|
+
}
|
|
140
|
+
return text;
|
|
141
|
+
})
|
|
142
|
+
.join('\n\n');
|
|
143
|
+
|
|
144
|
+
currentMessages.push({
|
|
145
|
+
role: 'user',
|
|
146
|
+
content: `[System Temporary Memory] Tool Execution Results:\n${toolResultsText}\n\nReview the results. If failed, apply FAILURE RECOVERY by retrying with different parameters. If all tasks are completed, output the final completion message.`
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const followUp = await llmService.chat(currentMessages, systemPrompt, user);
|
|
150
|
+
currentResponse = followUp.content;
|
|
151
|
+
currentMessages.push({ role: 'assistant', content: currentResponse });
|
|
152
|
+
if (followUp.tokensUsed) result.tokensUsed += followUp.tokensUsed;
|
|
153
|
+
} else {
|
|
154
|
+
// Tools were blocked or something weird happened - exit loop early
|
|
155
|
+
result.response = currentResponse;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (loopCount >= maxLoops) {
|
|
161
|
+
result.response = currentResponse + '\n\n*(Note: Task reached maximum automated steps limit and was paused for your review.)*';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 8: Scan output for sensitive data
|
|
165
|
+
const outputScan = securityService.scanForSensitiveData(result.response);
|
|
166
|
+
if (outputScan.length > 0) {
|
|
167
|
+
result.sensitiveDataWarnings = [...result.sensitiveDataWarnings, ...outputScan];
|
|
168
|
+
result.response = securityService.maskSensitiveData(result.response);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 9: Extract memories from conversation
|
|
172
|
+
try {
|
|
173
|
+
await memoryService.extractFromMessage(user._id || user.id, userMessage);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
// Non-critical
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = new PlannerService();
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const { SENSITIVE_PATTERNS } = require('../../config/constants');
|
|
2
|
+
|
|
3
|
+
class SecurityService {
|
|
4
|
+
/**
|
|
5
|
+
* Scan text for sensitive Indian data patterns (PAN, Aadhaar, Phone, etc.)
|
|
6
|
+
*/
|
|
7
|
+
scanForSensitiveData(text) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
|
|
10
|
+
if (!text || typeof text !== 'string') return findings;
|
|
11
|
+
|
|
12
|
+
// PAN Card
|
|
13
|
+
const panMatches = text.match(SENSITIVE_PATTERNS.PAN);
|
|
14
|
+
if (panMatches) {
|
|
15
|
+
findings.push({
|
|
16
|
+
type: 'PAN Card',
|
|
17
|
+
typeHi: 'पैन कार्ड',
|
|
18
|
+
count: panMatches.length,
|
|
19
|
+
risk: 'high',
|
|
20
|
+
icon: '🔴',
|
|
21
|
+
message: `${panMatches.length} PAN number(s) detected`,
|
|
22
|
+
messageHi: `${panMatches.length} पैन नंबर मिला`
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Aadhaar
|
|
27
|
+
const aadhaarMatches = text.match(SENSITIVE_PATTERNS.AADHAAR);
|
|
28
|
+
if (aadhaarMatches) {
|
|
29
|
+
findings.push({
|
|
30
|
+
type: 'Aadhaar Number',
|
|
31
|
+
typeHi: 'आधार नंबर',
|
|
32
|
+
count: aadhaarMatches.length,
|
|
33
|
+
risk: 'critical',
|
|
34
|
+
icon: '🔴',
|
|
35
|
+
message: `${aadhaarMatches.length} Aadhaar number(s) detected`,
|
|
36
|
+
messageHi: `${aadhaarMatches.length} आधार नंबर मिला`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Phone Numbers
|
|
41
|
+
const phoneMatches = text.match(SENSITIVE_PATTERNS.PHONE);
|
|
42
|
+
if (phoneMatches) {
|
|
43
|
+
findings.push({
|
|
44
|
+
type: 'Phone Number',
|
|
45
|
+
typeHi: 'फ़ोन नंबर',
|
|
46
|
+
count: phoneMatches.length,
|
|
47
|
+
risk: 'medium',
|
|
48
|
+
icon: '🟡',
|
|
49
|
+
message: `${phoneMatches.length} phone number(s) detected`,
|
|
50
|
+
messageHi: `${phoneMatches.length} फ़ोन नंबर मिला`
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Email
|
|
55
|
+
const emailMatches = text.match(SENSITIVE_PATTERNS.EMAIL);
|
|
56
|
+
if (emailMatches) {
|
|
57
|
+
findings.push({
|
|
58
|
+
type: 'Email Address',
|
|
59
|
+
typeHi: 'ईमेल',
|
|
60
|
+
count: emailMatches.length,
|
|
61
|
+
risk: 'low',
|
|
62
|
+
icon: '🟢',
|
|
63
|
+
message: `${emailMatches.length} email address(es) detected`,
|
|
64
|
+
messageHi: `${emailMatches.length} ईमेल मिला`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// IFSC Code
|
|
69
|
+
const ifscMatches = text.match(SENSITIVE_PATTERNS.IFSC);
|
|
70
|
+
if (ifscMatches) {
|
|
71
|
+
findings.push({
|
|
72
|
+
type: 'IFSC Code',
|
|
73
|
+
typeHi: 'IFSC कोड',
|
|
74
|
+
count: ifscMatches.length,
|
|
75
|
+
risk: 'high',
|
|
76
|
+
icon: '🔴',
|
|
77
|
+
message: `${ifscMatches.length} IFSC code(s) detected`,
|
|
78
|
+
messageHi: `${ifscMatches.length} IFSC कोड मिला`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return findings;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Mask sensitive data in text
|
|
87
|
+
* Preserves UPI IDs and UPI links (phone numbers inside UPI should NOT be masked)
|
|
88
|
+
*/
|
|
89
|
+
maskSensitiveData(text) {
|
|
90
|
+
if (!text) return text;
|
|
91
|
+
let masked = text;
|
|
92
|
+
|
|
93
|
+
// Step 1: Temporarily protect UPI links and UPI IDs from masking
|
|
94
|
+
const upiPlaceholders = [];
|
|
95
|
+
// Protect full UPI deep links: upi://pay?pa=...
|
|
96
|
+
masked = masked.replace(/upi:\/\/pay\?[^\s\n`"')]+/gi, (match) => {
|
|
97
|
+
const idx = upiPlaceholders.length;
|
|
98
|
+
upiPlaceholders.push(match);
|
|
99
|
+
return `__UPI_LINK_${idx}__`;
|
|
100
|
+
});
|
|
101
|
+
// Protect UPI IDs: something@upihandle (e.g. 9301105706@yespop)
|
|
102
|
+
masked = masked.replace(/[\w.\-+]+@[a-zA-Z]{2,}/g, (match) => {
|
|
103
|
+
// Only protect if it looks like a UPI ID (ends with known UPI handles or short handle)
|
|
104
|
+
const upiHandles = ['ybl', 'okhdfcbank', 'okicici', 'okaxis', 'oksbi', 'paytm',
|
|
105
|
+
'apl', 'yespop', 'upi', 'ibl', 'sbi', 'axisbank', 'icici', 'hdfcbank',
|
|
106
|
+
'kotak', 'pnb', 'boi', 'cnrb', 'unionbank', 'indianbank', 'federal'];
|
|
107
|
+
const handle = match.split('@')[1]?.toLowerCase();
|
|
108
|
+
if (handle && (upiHandles.includes(handle) || handle.length <= 6)) {
|
|
109
|
+
const idx = upiPlaceholders.length;
|
|
110
|
+
upiPlaceholders.push(match);
|
|
111
|
+
return `__UPI_LINK_${idx}__`;
|
|
112
|
+
}
|
|
113
|
+
return match; // Not a UPI ID, leave for email detection
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Step 2: Apply masking
|
|
117
|
+
masked = masked.replace(SENSITIVE_PATTERNS.PAN, (match) => match.substring(0, 2) + '***' + match.substring(match.length - 2));
|
|
118
|
+
masked = masked.replace(SENSITIVE_PATTERNS.AADHAAR, (match) => 'XXXX XXXX ' + match.trim().slice(-4));
|
|
119
|
+
masked = masked.replace(SENSITIVE_PATTERNS.PHONE, (match) => match.substring(0, 4) + '****' + match.substring(match.length - 2));
|
|
120
|
+
|
|
121
|
+
// Step 3: Restore protected UPI content
|
|
122
|
+
for (let i = 0; i < upiPlaceholders.length; i++) {
|
|
123
|
+
masked = masked.replace(`__UPI_LINK_${i}__`, upiPlaceholders[i]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return masked;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Assess risk level of a tool action
|
|
131
|
+
*/
|
|
132
|
+
assessRisk(toolName, args) {
|
|
133
|
+
const highRiskTools = ['file_delete', 'send_email', 'browser_automation'];
|
|
134
|
+
const mediumRiskTools = ['file_write', 'calendar_schedule'];
|
|
135
|
+
const lowRiskTools = ['web_search', 'calculate', 'weather_info', 'summarize_text', 'note_take'];
|
|
136
|
+
|
|
137
|
+
if (highRiskTools.includes(toolName)) return 'high';
|
|
138
|
+
if (mediumRiskTools.includes(toolName)) return 'medium';
|
|
139
|
+
if (lowRiskTools.includes(toolName)) return 'low';
|
|
140
|
+
return 'medium';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if action requires user permission
|
|
145
|
+
*/
|
|
146
|
+
requiresPermission(toolName, userPermissions) {
|
|
147
|
+
const toolPermissionMap = {
|
|
148
|
+
'file_read': 'fileAccess',
|
|
149
|
+
'file_write': 'fileAccess',
|
|
150
|
+
'file_delete': 'fileAccess',
|
|
151
|
+
'send_email': 'emailAccess',
|
|
152
|
+
'browser_automation': 'browserAccess',
|
|
153
|
+
'calendar_schedule': 'calendarAccess'
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const permKey = toolPermissionMap[toolName];
|
|
157
|
+
if (!permKey) return false;
|
|
158
|
+
|
|
159
|
+
const perm = userPermissions?.[permKey] || 'ask';
|
|
160
|
+
if (perm === 'allow_always') return false;
|
|
161
|
+
if (perm === 'never') return 'blocked';
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = new SecurityService();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class TelegramService {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.botToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
4
|
+
this.apiUrl = `https://api.telegram.org/bot${this.botToken}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async sendMessage(chatId, text) {
|
|
8
|
+
if (!this.botToken) {
|
|
9
|
+
console.error('Telegram Bot Token not configured!');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(`${this.apiUrl}/sendMessage`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
chat_id: chatId,
|
|
19
|
+
text: text,
|
|
20
|
+
parse_mode: 'Markdown'
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const error = await response.text();
|
|
26
|
+
console.error('Telegram send error:', error);
|
|
27
|
+
return { success: false, error };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { success: true };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Telegram Service Error:', error);
|
|
33
|
+
return { success: false, error: error.message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async setWebhook(url) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(`${this.apiUrl}/setWebhook?url=${url}`);
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
console.log('Telegram Webhook Setup:', data);
|
|
42
|
+
return data;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Telegram webhook setup error:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = new TelegramService();
|