@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 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
+ }
@@ -0,0 +1,8 @@
1
+ import { TelegramAdapter } from './TelegramAdapter.js';
2
+
3
+ export function telegram(config) {
4
+ return new TelegramAdapter(config);
5
+ }
6
+
7
+ export { TelegramAdapter } from './TelegramAdapter.js';
8
+ export { TelegramReceiver } from './TelegramReceiver.js';
@@ -0,0 +1,10 @@
1
+ import { Logger } from 'logger-standard';
2
+
3
+ export const createLogger = (service) => {
4
+ return new Logger({
5
+ service,
6
+ showDate: process.env.LOG_SHOW_DATE === 'true'
7
+ });
8
+ };
9
+
10
+ export const logger = createLogger('messenger');