@entergreat/messenger 1.0.0
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/.npmrc.example +12 -0
- package/README.md +211 -0
- package/package.json +29 -0
- package/src/core/Messenger.js +96 -0
- package/src/core/PlatformAdapter.js +30 -0
- package/src/features/conversation/ConversationManager.js +64 -0
- package/src/features/ratelimit/RateLimiter.js +38 -0
- package/src/index.js +5 -0
- package/src/platforms/telegram/TelegramAdapter.js +104 -0
- package/src/platforms/telegram/TelegramReceiver.js +98 -0
- package/src/platforms/telegram/index.js +8 -0
- package/src/utils/logger.js +10 -0
package/.npmrc.example
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# NPM Registry Configuration
|
|
2
|
+
# Copy this file to .npmrc and configure for your setup
|
|
3
|
+
|
|
4
|
+
# For GitHub Packages (private package)
|
|
5
|
+
# @entergreat:registry=https://npm.pkg.github.com
|
|
6
|
+
# //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
|
|
7
|
+
|
|
8
|
+
# For private npm registry
|
|
9
|
+
# registry=https://your-private-registry.com
|
|
10
|
+
|
|
11
|
+
# Standard npm registry (public packages)
|
|
12
|
+
registry=https://registry.npmjs.org/
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# @entergreat/messenger
|
|
2
|
+
|
|
3
|
+
Telegram messaging library for notifications and interactive bots.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @entergreat/messenger
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Send Notifications (eg-light)
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { Messenger, telegram } from '@entergreat/messenger';
|
|
17
|
+
|
|
18
|
+
const messenger = new Messenger();
|
|
19
|
+
messenger.addPlatform('telegram', telegram({
|
|
20
|
+
token: process.env.TELEGRAM_BOT_TOKEN,
|
|
21
|
+
chatId: process.env.TELEGRAM_CHAT_ID
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Send text
|
|
25
|
+
await messenger.sendText(null, 'Pipeline complete!');
|
|
26
|
+
|
|
27
|
+
// Send file
|
|
28
|
+
await messenger.sendFile(null, '/path/to/results.csv', {
|
|
29
|
+
caption: 'Results',
|
|
30
|
+
fileName: 'results.csv'
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Interactive Bot (eg-bot)
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import { Messenger, telegram } from '@entergreat/messenger';
|
|
38
|
+
|
|
39
|
+
const messenger = new Messenger();
|
|
40
|
+
|
|
41
|
+
messenger
|
|
42
|
+
.addPlatform('telegram', telegram({
|
|
43
|
+
token: process.env.TELEGRAM_BOT_TOKEN,
|
|
44
|
+
allowDynamicRecipients: true,
|
|
45
|
+
allowedChatIds: '123,456,789'
|
|
46
|
+
}))
|
|
47
|
+
.command('/start', async (ctx) => {
|
|
48
|
+
await ctx.reply('Welcome!');
|
|
49
|
+
})
|
|
50
|
+
.command('/echo', async (ctx) => {
|
|
51
|
+
const text = ctx.args.join(' ');
|
|
52
|
+
await ctx.reply(`You said: ${text}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await messenger.startReceiving();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Conversation Flow
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import { ConversationManager } from '@entergreat/messenger';
|
|
62
|
+
|
|
63
|
+
messenger.command('/register', async (ctx) => {
|
|
64
|
+
const conv = new ConversationManager([
|
|
65
|
+
{
|
|
66
|
+
field: 'name',
|
|
67
|
+
prompt: 'What is your name?',
|
|
68
|
+
required: true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
field: 'email',
|
|
72
|
+
prompt: 'What is your email?',
|
|
73
|
+
required: true,
|
|
74
|
+
validate: (v) => v.includes('@') ? null : 'Invalid email'
|
|
75
|
+
}
|
|
76
|
+
], {
|
|
77
|
+
onComplete: async (data, reply) => {
|
|
78
|
+
await reply(`Done! Name: ${data.name}, Email: ${data.email}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ctx.messenger.setConversation(ctx.chatId, conv);
|
|
83
|
+
await ctx.reply(conv.start());
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Rate Limiting
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import { RateLimiter } from '@entergreat/messenger';
|
|
91
|
+
|
|
92
|
+
const limiter = new RateLimiter({ maxRequests: 3, windowMs: 24 * 60 * 60 * 1000 });
|
|
93
|
+
|
|
94
|
+
messenger.command('/limited', async (ctx) => {
|
|
95
|
+
const check = limiter.check(ctx.userId);
|
|
96
|
+
if (!check.allowed) {
|
|
97
|
+
await ctx.reply(`Rate limit exceeded. Try again in ${check.resetIn}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
limiter.increment(ctx.userId);
|
|
101
|
+
await ctx.reply(`OK! ${check.remaining - 1} uses left today`);
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Config
|
|
106
|
+
|
|
107
|
+
### Fixed Recipient (Notifications)
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
telegram({
|
|
111
|
+
token: 'bot_token',
|
|
112
|
+
chatId: '123456789',
|
|
113
|
+
parseMode: 'Markdown' // or 'HTML'
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Dynamic Recipients (Bots)
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
telegram({
|
|
121
|
+
token: 'bot_token',
|
|
122
|
+
allowDynamicRecipients: true,
|
|
123
|
+
allowedChatIds: [123, 456, 789], // whitelist
|
|
124
|
+
pollTimeout: 30 // polling timeout in seconds
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API
|
|
129
|
+
|
|
130
|
+
### Messenger
|
|
131
|
+
|
|
132
|
+
- `addPlatform(name, adapter)` - Add Telegram adapter
|
|
133
|
+
- `sendText(recipient, text, options)` - Send text message
|
|
134
|
+
- `sendFile(recipient, path, options)` - Send file
|
|
135
|
+
- `sendImage(recipient, path, options)` - Send image
|
|
136
|
+
- `command(name, handler)` - Register command
|
|
137
|
+
- `onMessage(handler)` - Register message handler
|
|
138
|
+
- `startReceiving()` - Start polling
|
|
139
|
+
- `stopReceiving()` - Stop polling
|
|
140
|
+
- `healthCheck()` - Check bot status
|
|
141
|
+
|
|
142
|
+
### Context Object
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
{
|
|
146
|
+
chatId: 123456789,
|
|
147
|
+
userId: 987654321,
|
|
148
|
+
message: 'Hello',
|
|
149
|
+
reply: async (text) => {},
|
|
150
|
+
messenger: messengerInstance
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### ConversationManager
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
new ConversationManager(steps, { onComplete, onCancel })
|
|
158
|
+
|
|
159
|
+
// Step format:
|
|
160
|
+
{
|
|
161
|
+
field: 'name',
|
|
162
|
+
prompt: 'Question?',
|
|
163
|
+
required: true,
|
|
164
|
+
default: 'value',
|
|
165
|
+
validate: (value) => error || null,
|
|
166
|
+
transform: (value) => transformedValue
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### RateLimiter
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
new RateLimiter({ maxRequests: 3, windowMs: 86400000 })
|
|
174
|
+
|
|
175
|
+
limiter.check(userId) // { allowed, remaining, resetIn }
|
|
176
|
+
limiter.increment(userId) // Record usage
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Migration
|
|
180
|
+
|
|
181
|
+
### From eg-light
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// Before
|
|
185
|
+
const delivery = new DeliveryService();
|
|
186
|
+
await delivery.sendUpdate('Message');
|
|
187
|
+
|
|
188
|
+
// After
|
|
189
|
+
const messenger = new Messenger();
|
|
190
|
+
messenger.addPlatform('telegram', telegram({ token, chatId }));
|
|
191
|
+
await messenger.sendText(null, 'Message');
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### From eg-bot
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
// Before
|
|
198
|
+
const bot = new Bot(token);
|
|
199
|
+
bot.register('/start', handler);
|
|
200
|
+
await bot.start();
|
|
201
|
+
|
|
202
|
+
// After
|
|
203
|
+
const messenger = new Messenger();
|
|
204
|
+
messenger.addPlatform('telegram', telegram({ token, allowDynamicRecipients: true }));
|
|
205
|
+
messenger.command('/start', handler);
|
|
206
|
+
await messenger.startReceiving();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entergreat/messenger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-platform messaging and bot framework for EnterGreat services",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"messenger",
|
|
9
|
+
"telegram",
|
|
10
|
+
"slack",
|
|
11
|
+
"email",
|
|
12
|
+
"whatsapp",
|
|
13
|
+
"teams",
|
|
14
|
+
"bot",
|
|
15
|
+
"notifications"
|
|
16
|
+
],
|
|
17
|
+
"author": "EnterGreat",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/entergreat/messenger.git"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"logger-standard": "^1.0.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export class Messenger {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.platform = null;
|
|
4
|
+
this.adapter = null;
|
|
5
|
+
this.receiver = null;
|
|
6
|
+
this.commandHandlers = new Map();
|
|
7
|
+
this.messageHandlers = [];
|
|
8
|
+
this.conversations = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
addPlatform(name, adapter) {
|
|
12
|
+
this.platform = name;
|
|
13
|
+
this.adapter = adapter;
|
|
14
|
+
|
|
15
|
+
const ReceiverClass = adapter.getReceiverClass();
|
|
16
|
+
if (ReceiverClass) {
|
|
17
|
+
this.receiver = new ReceiverClass(adapter, this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async send(recipient, content, options = {}) {
|
|
24
|
+
const method = options.method || 'sendText';
|
|
25
|
+
return await this.adapter[method](recipient, content, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async sendText(recipient, text, options = {}) {
|
|
29
|
+
return await this.adapter.sendText(recipient, text, options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async sendFile(recipient, filePath, options = {}) {
|
|
33
|
+
return await this.adapter.sendFile(recipient, filePath, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async sendImage(recipient, imagePath, options = {}) {
|
|
37
|
+
return await this.adapter.sendImage(recipient, imagePath, options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
command(commandName, handler) {
|
|
41
|
+
this.commandHandlers.set(commandName, handler);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onMessage(handler) {
|
|
46
|
+
this.messageHandlers.push(handler);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
conversation(chatId) {
|
|
51
|
+
return this.conversations.get(chatId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setConversation(chatId, conversation) {
|
|
55
|
+
this.conversations.set(chatId, conversation);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clearConversation(chatId) {
|
|
59
|
+
this.conversations.delete(chatId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async handleCommand(command, context) {
|
|
63
|
+
const handler = this.commandHandlers.get(command);
|
|
64
|
+
if (handler) {
|
|
65
|
+
await handler(context);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async handleMessage(context) {
|
|
72
|
+
for (const handler of this.messageHandlers) {
|
|
73
|
+
if (await handler(context)) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async startReceiving() {
|
|
79
|
+
if (!this.receiver) {
|
|
80
|
+
throw new Error('Platform does not support receiving messages');
|
|
81
|
+
}
|
|
82
|
+
await this.receiver.start();
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async stopReceiving() {
|
|
87
|
+
if (this.receiver) {
|
|
88
|
+
await this.receiver.stop();
|
|
89
|
+
}
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async healthCheck() {
|
|
94
|
+
return await this.adapter.healthCheck();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class PlatformAdapter {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.config = config;
|
|
4
|
+
this.validateConfig();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
validateConfig() {
|
|
8
|
+
throw new Error('validateConfig() must be implemented');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async sendText(recipient, text, options) {
|
|
12
|
+
throw new Error('sendText() must be implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async sendFile(recipient, filePath, options) {
|
|
16
|
+
throw new Error('sendFile() must be implemented');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async sendImage(recipient, imagePath, options) {
|
|
20
|
+
throw new Error('sendImage() must be implemented');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getReceiverClass() {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async healthCheck() {
|
|
28
|
+
return { status: 'ok' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class ConversationManager {
|
|
2
|
+
constructor(steps, options = {}) {
|
|
3
|
+
this.steps = steps;
|
|
4
|
+
this.onComplete = options.onComplete;
|
|
5
|
+
this.onCancel = options.onCancel;
|
|
6
|
+
this.currentStep = 0;
|
|
7
|
+
this.data = {};
|
|
8
|
+
this.active = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get isActive() {
|
|
12
|
+
return this.active;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
start() {
|
|
16
|
+
this.currentStep = 0;
|
|
17
|
+
this.data = {};
|
|
18
|
+
this.active = true;
|
|
19
|
+
const step = this.steps[0];
|
|
20
|
+
return `${step.prompt}${step.required ? '' : ` (/skip for ${step.default})`}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async handle(text, reply) {
|
|
24
|
+
if (!this.active) return false;
|
|
25
|
+
|
|
26
|
+
if (text === '/cancel') {
|
|
27
|
+
this.active = false;
|
|
28
|
+
this.data = {};
|
|
29
|
+
await (this.onCancel ? this.onCancel(reply) : reply('Cancelled.'));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const step = this.steps[this.currentStep];
|
|
34
|
+
|
|
35
|
+
if (text === '/skip') {
|
|
36
|
+
if (step.required) {
|
|
37
|
+
await reply(`This field is required. ${step.prompt}`);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
this.data[step.field] = step.default;
|
|
41
|
+
} else {
|
|
42
|
+
if (step.validate) {
|
|
43
|
+
const error = step.validate(text);
|
|
44
|
+
if (error) {
|
|
45
|
+
await reply(`${error}\n${step.prompt}`);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.data[step.field] = step.transform ? step.transform(text) : text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.currentStep++;
|
|
53
|
+
|
|
54
|
+
if (this.currentStep >= this.steps.length) {
|
|
55
|
+
this.active = false;
|
|
56
|
+
if (this.onComplete) await this.onComplete(this.data, reply);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const nextStep = this.steps[this.currentStep];
|
|
61
|
+
await reply(`${nextStep.prompt}${nextStep.required ? '' : ` (/skip for ${nextStep.default})`}`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class RateLimiter {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.maxRequests = options.maxRequests || 3;
|
|
4
|
+
this.windowMs = options.windowMs || 24 * 60 * 60 * 1000;
|
|
5
|
+
this.usage = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
check(identifier) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
const entry = this.usage.get(identifier);
|
|
11
|
+
|
|
12
|
+
if (!entry || now >= entry.resetAt) {
|
|
13
|
+
return { allowed: true, remaining: this.maxRequests };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (entry.count >= this.maxRequests) {
|
|
17
|
+
const msLeft = entry.resetAt - now;
|
|
18
|
+
return {
|
|
19
|
+
allowed: false,
|
|
20
|
+
remaining: 0,
|
|
21
|
+
resetIn: `${Math.floor(msLeft / 3600000)}h ${Math.ceil((msLeft % 3600000) / 60000)}m`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { allowed: true, remaining: this.maxRequests - entry.count };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
increment(identifier) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const entry = this.usage.get(identifier);
|
|
31
|
+
|
|
32
|
+
if (!entry || now >= entry.resetAt) {
|
|
33
|
+
this.usage.set(identifier, { count: 1, resetAt: now + this.windowMs });
|
|
34
|
+
} else {
|
|
35
|
+
entry.count++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Messenger } from './core/Messenger.js';
|
|
2
|
+
export { telegram, TelegramAdapter } from './platforms/telegram/index.js';
|
|
3
|
+
export { ConversationManager } from './features/conversation/ConversationManager.js';
|
|
4
|
+
export { RateLimiter } from './features/ratelimit/RateLimiter.js';
|
|
5
|
+
export { createLogger } from './utils/logger.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PlatformAdapter } from '../../core/PlatformAdapter.js';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { TelegramReceiver } from './TelegramReceiver.js';
|
|
4
|
+
import { createLogger } from '../../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('telegram');
|
|
7
|
+
|
|
8
|
+
export class TelegramAdapter extends PlatformAdapter {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
super(config);
|
|
11
|
+
this.baseUrl = `https://api.telegram.org/bot${config.token}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
validateConfig() {
|
|
15
|
+
if (!this.config.token) throw new Error('Token required');
|
|
16
|
+
if (!this.config.chatId && !this.config.allowDynamicRecipients) {
|
|
17
|
+
throw new Error('chatId required (or set allowDynamicRecipients: true)');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getRecipientId(recipient) {
|
|
22
|
+
return recipient || this.config.chatId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async sendText(recipient, text, options = {}) {
|
|
26
|
+
const chatId = this.getRecipientId(recipient);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${this.baseUrl}/sendMessage`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
chat_id: chatId,
|
|
33
|
+
text,
|
|
34
|
+
parse_mode: options.parseMode || this.config.parseMode || 'Markdown'
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = await res.json();
|
|
39
|
+
if (!result.ok) throw new Error(result.description);
|
|
40
|
+
return { status: 'success', messageId: result.result.message_id };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
logger.error(`sendText failed: ${err.message}`);
|
|
43
|
+
return { status: 'failed', error: err.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async sendFile(recipient, filePath, options = {}) {
|
|
48
|
+
const chatId = this.getRecipientId(recipient);
|
|
49
|
+
try {
|
|
50
|
+
const form = new FormData();
|
|
51
|
+
form.append('chat_id', chatId);
|
|
52
|
+
form.append('document', new Blob([await readFile(filePath)]), options.fileName || filePath.split('/').pop());
|
|
53
|
+
if (options.caption) form.append('caption', options.caption);
|
|
54
|
+
|
|
55
|
+
const res = await fetch(`${this.baseUrl}/sendDocument`, { method: 'POST', body: form });
|
|
56
|
+
const result = await res.json();
|
|
57
|
+
if (!result.ok) throw new Error(result.description);
|
|
58
|
+
return { status: 'success', messageId: result.result.message_id };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logger.error(`sendFile failed: ${err.message}`);
|
|
61
|
+
return { status: 'failed', error: err.message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async sendImage(recipient, imagePath, options = {}) {
|
|
66
|
+
const chatId = this.getRecipientId(recipient);
|
|
67
|
+
try {
|
|
68
|
+
const form = new FormData();
|
|
69
|
+
form.append('chat_id', chatId);
|
|
70
|
+
form.append('photo', new Blob([await readFile(imagePath)]), options.fileName || imagePath.split('/').pop());
|
|
71
|
+
if (options.caption) form.append('caption', options.caption);
|
|
72
|
+
|
|
73
|
+
const res = await fetch(`${this.baseUrl}/sendPhoto`, { method: 'POST', body: form });
|
|
74
|
+
const result = await res.json();
|
|
75
|
+
if (!result.ok) throw new Error(result.description);
|
|
76
|
+
return { status: 'success', messageId: result.result.message_id };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.error(`sendImage failed: ${err.message}`);
|
|
79
|
+
return { status: 'failed', error: err.message };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getReceiverClass() {
|
|
84
|
+
return TelegramReceiver;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async healthCheck() {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${this.baseUrl}/getMe`);
|
|
90
|
+
const result = await res.json();
|
|
91
|
+
if (!result.ok) return { status: 'error', error: result.description };
|
|
92
|
+
return {
|
|
93
|
+
status: 'ok',
|
|
94
|
+
bot: {
|
|
95
|
+
id: result.result.id,
|
|
96
|
+
username: result.result.username,
|
|
97
|
+
firstName: result.result.first_name
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return { status: 'error', error: err.message };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createLogger } from '../../utils/logger.js';
|
|
2
|
+
|
|
3
|
+
const logger = createLogger('telegram');
|
|
4
|
+
|
|
5
|
+
export class TelegramReceiver {
|
|
6
|
+
constructor(adapter, messenger) {
|
|
7
|
+
this.adapter = adapter;
|
|
8
|
+
this.messenger = messenger;
|
|
9
|
+
this.offset = 0;
|
|
10
|
+
this.running = false;
|
|
11
|
+
this.pollTimeout = adapter.config.pollTimeout || 30;
|
|
12
|
+
this.allowedChatIds = this.parseAllowedChatIds();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
parseAllowedChatIds() {
|
|
16
|
+
const { allowedChatIds } = this.adapter.config;
|
|
17
|
+
if (!allowedChatIds) return null;
|
|
18
|
+
if (Array.isArray(allowedChatIds)) return allowedChatIds.map(Number);
|
|
19
|
+
return allowedChatIds.split(',').map(id => Number(id.trim()));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isAllowed(chatId) {
|
|
23
|
+
return !this.allowedChatIds || this.allowedChatIds.includes(chatId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getUpdates() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${this.adapter.baseUrl}/getUpdates`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ offset: this.offset, timeout: this.pollTimeout })
|
|
32
|
+
});
|
|
33
|
+
const result = await res.json();
|
|
34
|
+
if (!result.ok) throw new Error(result.description);
|
|
35
|
+
return result.result || [];
|
|
36
|
+
} catch (err) {
|
|
37
|
+
logger.error(`getUpdates failed: ${err.message}`);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async handleUpdate(update) {
|
|
43
|
+
const message = update.message;
|
|
44
|
+
if (!message?.text) return;
|
|
45
|
+
|
|
46
|
+
const chatId = message.chat.id;
|
|
47
|
+
if (!this.isAllowed(chatId)) return;
|
|
48
|
+
|
|
49
|
+
const text = message.text.trim();
|
|
50
|
+
const context = {
|
|
51
|
+
chatId,
|
|
52
|
+
userId: message.from.id,
|
|
53
|
+
message: text,
|
|
54
|
+
raw: update,
|
|
55
|
+
reply: async (msg) => await this.adapter.sendText(chatId, msg),
|
|
56
|
+
messenger: this.messenger
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const conversation = this.messenger.conversation(chatId);
|
|
60
|
+
if (conversation?.isActive) {
|
|
61
|
+
await conversation.handle(text, context.reply);
|
|
62
|
+
if (!conversation.isActive) this.messenger.clearConversation(chatId);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (text.startsWith('/')) {
|
|
67
|
+
const handled = await this.messenger.handleCommand(text, context);
|
|
68
|
+
if (!handled) await context.reply(`Unknown command: ${text.split(' ')[0]}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this.messenger.handleMessage(context);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async start() {
|
|
76
|
+
if (this.running) return;
|
|
77
|
+
this.running = true;
|
|
78
|
+
logger.info('Polling started');
|
|
79
|
+
|
|
80
|
+
while (this.running) {
|
|
81
|
+
try {
|
|
82
|
+
const updates = await this.getUpdates();
|
|
83
|
+
for (const update of updates) {
|
|
84
|
+
this.offset = update.update_id + 1;
|
|
85
|
+
await this.handleUpdate(update);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error(`Polling error: ${err.message}`);
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async stop() {
|
|
95
|
+
this.running = false;
|
|
96
|
+
logger.info('Polling stopped');
|
|
97
|
+
}
|
|
98
|
+
}
|