telegix 1.1.1
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/LICENSE +21 -0
- package/README.md +1534 -0
- package/index.d.ts +539 -0
- package/index.js +38 -0
- package/lib/album.js +57 -0
- package/lib/api.js +1840 -0
- package/lib/chataction.js +40 -0
- package/lib/cluster.js +68 -0
- package/lib/composer.js +419 -0
- package/lib/context.js +970 -0
- package/lib/errors.js +67 -0
- package/lib/format.js +115 -0
- package/lib/i18n.js +158 -0
- package/lib/inline-debounce.js +49 -0
- package/lib/inline.js +79 -0
- package/lib/markdownv2.js +29 -0
- package/lib/markup.js +321 -0
- package/lib/payment.js +91 -0
- package/lib/polling.js +101 -0
- package/lib/prompt.js +62 -0
- package/lib/ratelimit.js +59 -0
- package/lib/rich.js +609 -0
- package/lib/scenes.js +206 -0
- package/lib/serialize.js +141 -0
- package/lib/session.js +145 -0
- package/lib/telegix.js +176 -0
- package/lib/webapp.js +65 -0
- package/lib/webhook.js +86 -0
- package/package.json +42 -0
package/lib/scenes.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Pure JS Scenes & Wizard Dialog Engine
|
|
3
|
+
* @module telegix/scenes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Composer, compose } from './composer.js';
|
|
7
|
+
|
|
8
|
+
export class BaseScene extends Composer {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} id - Unique Scene identifier
|
|
11
|
+
*/
|
|
12
|
+
constructor(id) {
|
|
13
|
+
super();
|
|
14
|
+
if (!id || typeof id !== 'string') {
|
|
15
|
+
throw new Error('BaseScene requires a valid string ID');
|
|
16
|
+
}
|
|
17
|
+
this.id = id;
|
|
18
|
+
this.enterHandlers = [];
|
|
19
|
+
this.leaveHandlers = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handler executed when user enters the scene
|
|
24
|
+
* @param {...Function} handlers
|
|
25
|
+
*/
|
|
26
|
+
enter(...handlers) {
|
|
27
|
+
this.enterHandlers.push(...handlers);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handler executed when user leaves the scene
|
|
33
|
+
* @param {...Function} handlers
|
|
34
|
+
*/
|
|
35
|
+
leave(...handlers) {
|
|
36
|
+
this.leaveHandlers.push(...handlers);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class WizardScene extends BaseScene {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} id - Wizard Scene ID
|
|
44
|
+
* @param {...Function} steps - Middleware step functions
|
|
45
|
+
*/
|
|
46
|
+
constructor(id, ...steps) {
|
|
47
|
+
super(id);
|
|
48
|
+
this.steps = steps;
|
|
49
|
+
|
|
50
|
+
this.use(async (ctx, next) => {
|
|
51
|
+
if (!ctx.scene?.session) return next();
|
|
52
|
+
const cursor = ctx.scene.session.cursor || 0;
|
|
53
|
+
const step = this.steps[cursor];
|
|
54
|
+
if (!step) {
|
|
55
|
+
return ctx.scene.leave();
|
|
56
|
+
}
|
|
57
|
+
return step(ctx, next);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.enter((ctx, next) => {
|
|
61
|
+
ctx.scene.session.cursor = 0;
|
|
62
|
+
const step = this.steps[0];
|
|
63
|
+
if (step) {
|
|
64
|
+
return step(ctx, next);
|
|
65
|
+
}
|
|
66
|
+
return next();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class Stage extends Composer {
|
|
72
|
+
/**
|
|
73
|
+
* @param {Array<BaseScene>} scenes
|
|
74
|
+
* @param {object} [options]
|
|
75
|
+
*/
|
|
76
|
+
constructor(scenes = [], options = {}) {
|
|
77
|
+
super();
|
|
78
|
+
this.scenes = new Map();
|
|
79
|
+
this.options = {
|
|
80
|
+
defaultScene: null,
|
|
81
|
+
...options,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const scene of scenes) {
|
|
85
|
+
this.register(scene);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.use(this.middleware());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register a scene
|
|
93
|
+
* @param {BaseScene} scene
|
|
94
|
+
*/
|
|
95
|
+
register(scene) {
|
|
96
|
+
if (!scene || !scene.id) {
|
|
97
|
+
throw new Error('Stage.register requires a valid BaseScene instance with an ID');
|
|
98
|
+
}
|
|
99
|
+
this.scenes.set(scene.id, scene);
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns Stage middleware
|
|
105
|
+
*/
|
|
106
|
+
middleware() {
|
|
107
|
+
const stageInstance = this;
|
|
108
|
+
return async (ctx, next) => {
|
|
109
|
+
if (!ctx.session) {
|
|
110
|
+
throw new Error('Telegix Stage: session middleware is required before Stage middleware!');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ctx.session.__scenes = ctx.session.__scenes || {};
|
|
114
|
+
const sceneSession = ctx.session.__scenes;
|
|
115
|
+
|
|
116
|
+
// Setup scene control helper on ctx
|
|
117
|
+
const sceneControl = {
|
|
118
|
+
get session() {
|
|
119
|
+
const currentId = sceneSession.current;
|
|
120
|
+
if (!currentId) return {};
|
|
121
|
+
sceneSession.state = sceneSession.state || {};
|
|
122
|
+
return sceneSession.state;
|
|
123
|
+
},
|
|
124
|
+
get current() {
|
|
125
|
+
const currentId = sceneSession.current;
|
|
126
|
+
return currentId ? stageInstance.scenes.get(currentId) || null : null;
|
|
127
|
+
},
|
|
128
|
+
get state() {
|
|
129
|
+
return sceneSession.state || {};
|
|
130
|
+
},
|
|
131
|
+
enter: async (sceneId, initialState = {}) => {
|
|
132
|
+
const scene = stageInstance.scenes.get(sceneId);
|
|
133
|
+
if (!scene) {
|
|
134
|
+
throw new Error(`Telegix Stage: Scene '${sceneId}' not found!`);
|
|
135
|
+
}
|
|
136
|
+
sceneSession.current = sceneId;
|
|
137
|
+
sceneSession.state = { ...initialState };
|
|
138
|
+
sceneSession.cursor = 0;
|
|
139
|
+
|
|
140
|
+
if (scene.enterHandlers.length > 0) {
|
|
141
|
+
const enterFn = compose(scene.enterHandlers);
|
|
142
|
+
await enterFn(ctx, async () => {});
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
reenter: async () => {
|
|
146
|
+
const currentId = sceneSession.current;
|
|
147
|
+
if (currentId) {
|
|
148
|
+
await sceneControl.enter(currentId, sceneSession.state);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
leave: async () => {
|
|
152
|
+
const currentId = sceneSession.current;
|
|
153
|
+
if (currentId) {
|
|
154
|
+
const scene = stageInstance.scenes.get(currentId);
|
|
155
|
+
if (scene && scene.leaveHandlers.length > 0) {
|
|
156
|
+
const leaveFn = compose(scene.leaveHandlers);
|
|
157
|
+
await leaveFn(ctx, async () => {});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
delete sceneSession.current;
|
|
161
|
+
delete sceneSession.state;
|
|
162
|
+
delete sceneSession.cursor;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Setup wizard helper on ctx
|
|
167
|
+
const wizardControl = {
|
|
168
|
+
get cursor() {
|
|
169
|
+
return sceneSession.cursor || 0;
|
|
170
|
+
},
|
|
171
|
+
set cursor(val) {
|
|
172
|
+
sceneSession.cursor = val;
|
|
173
|
+
},
|
|
174
|
+
get state() {
|
|
175
|
+
return sceneControl.state;
|
|
176
|
+
},
|
|
177
|
+
selectStep: (index) => {
|
|
178
|
+
sceneSession.cursor = index;
|
|
179
|
+
},
|
|
180
|
+
next: () => {
|
|
181
|
+
sceneSession.cursor = (sceneSession.cursor || 0) + 1;
|
|
182
|
+
},
|
|
183
|
+
back: () => {
|
|
184
|
+
sceneSession.cursor = Math.max(0, (sceneSession.cursor || 0) - 1);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
ctx.scene = sceneControl;
|
|
189
|
+
ctx.wizard = wizardControl;
|
|
190
|
+
|
|
191
|
+
const currentSceneId = sceneSession.current || this.options.defaultScene;
|
|
192
|
+
if (!currentSceneId) {
|
|
193
|
+
return next();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scene = this.scenes.get(currentSceneId);
|
|
197
|
+
if (!scene) {
|
|
198
|
+
return next();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return scene.middleware()(ctx, next);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const Scene = BaseScene;
|
package/lib/serialize.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Message & Update Serializer Utility
|
|
3
|
+
* Standardizes raw Telegram updates into a clean, feature-rich structured object.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function serializeMessage(msg) {
|
|
7
|
+
if (!msg) return null;
|
|
8
|
+
|
|
9
|
+
const serialized = {
|
|
10
|
+
id: msg.message_id,
|
|
11
|
+
chatId: msg.chat?.id,
|
|
12
|
+
senderId: msg.from?.id,
|
|
13
|
+
from: msg.from || {},
|
|
14
|
+
chat: msg.chat || {},
|
|
15
|
+
date: msg.date,
|
|
16
|
+
text: msg.text || msg.caption || '',
|
|
17
|
+
type: 'unknown',
|
|
18
|
+
media: null,
|
|
19
|
+
quoted: null,
|
|
20
|
+
mentioned: [],
|
|
21
|
+
raw: msg,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Determine message type and extract media/content
|
|
25
|
+
if (msg.text) {
|
|
26
|
+
serialized.type = 'text';
|
|
27
|
+
} else if (msg.photo && msg.photo.length > 0) {
|
|
28
|
+
serialized.type = 'photo';
|
|
29
|
+
const photo = msg.photo[msg.photo.length - 1];
|
|
30
|
+
serialized.media = {
|
|
31
|
+
fileId: photo.file_id,
|
|
32
|
+
fileUniqueId: photo.file_unique_id,
|
|
33
|
+
fileSize: photo.file_size,
|
|
34
|
+
width: photo.width,
|
|
35
|
+
height: photo.height,
|
|
36
|
+
};
|
|
37
|
+
} else if (msg.video) {
|
|
38
|
+
serialized.type = 'video';
|
|
39
|
+
serialized.media = {
|
|
40
|
+
fileId: msg.video.file_id,
|
|
41
|
+
fileUniqueId: msg.video.file_unique_id,
|
|
42
|
+
fileSize: msg.video.file_size,
|
|
43
|
+
duration: msg.video.duration,
|
|
44
|
+
mimeType: msg.video.mime_type,
|
|
45
|
+
width: msg.video.width,
|
|
46
|
+
height: msg.video.height,
|
|
47
|
+
};
|
|
48
|
+
} else if (msg.document) {
|
|
49
|
+
serialized.type = 'document';
|
|
50
|
+
serialized.media = {
|
|
51
|
+
fileId: msg.document.file_id,
|
|
52
|
+
fileUniqueId: msg.document.file_unique_id,
|
|
53
|
+
fileName: msg.document.file_name,
|
|
54
|
+
fileSize: msg.document.file_size,
|
|
55
|
+
mimeType: msg.document.mime_type,
|
|
56
|
+
};
|
|
57
|
+
} else if (msg.audio) {
|
|
58
|
+
serialized.type = 'audio';
|
|
59
|
+
serialized.media = {
|
|
60
|
+
fileId: msg.audio.file_id,
|
|
61
|
+
fileUniqueId: msg.audio.file_unique_id,
|
|
62
|
+
duration: msg.audio.duration,
|
|
63
|
+
performer: msg.audio.performer,
|
|
64
|
+
title: msg.audio.title,
|
|
65
|
+
fileSize: msg.audio.file_size,
|
|
66
|
+
mimeType: msg.audio.mime_type,
|
|
67
|
+
};
|
|
68
|
+
} else if (msg.voice) {
|
|
69
|
+
serialized.type = 'voice';
|
|
70
|
+
serialized.media = {
|
|
71
|
+
fileId: msg.voice.file_id,
|
|
72
|
+
fileUniqueId: msg.voice.file_unique_id,
|
|
73
|
+
duration: msg.voice.duration,
|
|
74
|
+
fileSize: msg.voice.file_size,
|
|
75
|
+
mimeType: msg.voice.mime_type,
|
|
76
|
+
};
|
|
77
|
+
} else if (msg.sticker) {
|
|
78
|
+
serialized.type = 'sticker';
|
|
79
|
+
serialized.media = {
|
|
80
|
+
fileId: msg.sticker.file_id,
|
|
81
|
+
fileUniqueId: msg.sticker.file_unique_id,
|
|
82
|
+
emoji: msg.sticker.emoji,
|
|
83
|
+
isAnimated: msg.sticker.is_animated,
|
|
84
|
+
isVideo: msg.sticker.is_video,
|
|
85
|
+
};
|
|
86
|
+
} else if (msg.contact) {
|
|
87
|
+
serialized.type = 'contact';
|
|
88
|
+
serialized.contact = msg.contact;
|
|
89
|
+
} else if (msg.location) {
|
|
90
|
+
serialized.type = 'location';
|
|
91
|
+
serialized.location = msg.location;
|
|
92
|
+
} else if (msg.poll) {
|
|
93
|
+
serialized.type = 'poll';
|
|
94
|
+
serialized.poll = msg.poll;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle Quoted / Replied Message
|
|
98
|
+
if (msg.reply_to_message) {
|
|
99
|
+
serialized.quoted = serializeMessage(msg.reply_to_message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Extract mentions from entities
|
|
103
|
+
const entities = msg.entities || msg.caption_entities || [];
|
|
104
|
+
for (const entity of entities) {
|
|
105
|
+
if (entity.type === 'mention') {
|
|
106
|
+
const mentionText = serialized.text.substring(entity.offset, entity.offset + entity.length);
|
|
107
|
+
serialized.mentioned.push(mentionText);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
serialized.isGroup = ['group', 'supergroup'].includes(serialized.chat?.type);
|
|
112
|
+
serialized.isPrivate = serialized.chat?.type === 'private';
|
|
113
|
+
serialized.isChannel = serialized.chat?.type === 'channel';
|
|
114
|
+
|
|
115
|
+
return serialized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function serializeUpdate(update) {
|
|
119
|
+
const result = {
|
|
120
|
+
updateId: update.update_id,
|
|
121
|
+
type: 'unknown',
|
|
122
|
+
message: null,
|
|
123
|
+
callbackQuery: update.callback_query || null,
|
|
124
|
+
inlineQuery: update.inline_query || null,
|
|
125
|
+
raw: update,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const msg = update.message || update.edited_message || update.channel_post || update.edited_channel_post || update.callback_query?.message;
|
|
129
|
+
if (msg) {
|
|
130
|
+
result.message = serializeMessage(msg);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (update.message) result.type = 'message';
|
|
134
|
+
else if (update.callback_query) result.type = 'callback_query';
|
|
135
|
+
else if (update.inline_query) result.type = 'inline_query';
|
|
136
|
+
else if (update.chat_member) result.type = 'chat_member';
|
|
137
|
+
else if (update.chat_boost) result.type = 'chat_boost';
|
|
138
|
+
else if (update.paid_message_price_changed) result.type = 'paid_message_price_changed';
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
package/lib/session.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Session Management Middleware
|
|
3
|
+
* @module telegix/session
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
export class MemorySessionStore {
|
|
9
|
+
constructor(ttl = Infinity) {
|
|
10
|
+
this.map = new Map();
|
|
11
|
+
this.ttl = ttl;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async get(key) {
|
|
15
|
+
const item = this.map.get(key);
|
|
16
|
+
if (!item) return undefined;
|
|
17
|
+
if (Date.now() > item.expiresAt) {
|
|
18
|
+
this.map.delete(key);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return item.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async set(key, value) {
|
|
25
|
+
const expiresAt = this.ttl === Infinity ? Infinity : Date.now() + this.ttl;
|
|
26
|
+
this.map.set(key, { value, expiresAt });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async delete(key) {
|
|
30
|
+
this.map.delete(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async clear() {
|
|
34
|
+
this.map.clear();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class FileSessionStore {
|
|
39
|
+
constructor(filePath = 'telegix_sessions.json', ttl = Infinity) {
|
|
40
|
+
this.filePath = filePath;
|
|
41
|
+
this.ttl = ttl;
|
|
42
|
+
this.cache = null;
|
|
43
|
+
this.loaded = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _load() {
|
|
47
|
+
if (this.loaded) return;
|
|
48
|
+
try {
|
|
49
|
+
const data = await fs.readFile(this.filePath, 'utf8');
|
|
50
|
+
const parsed = JSON.parse(data);
|
|
51
|
+
this.cache = new Map(Object.entries(parsed));
|
|
52
|
+
} catch {
|
|
53
|
+
this.cache = new Map();
|
|
54
|
+
}
|
|
55
|
+
this.loaded = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async _save() {
|
|
59
|
+
if (!this.cache) return;
|
|
60
|
+
try {
|
|
61
|
+
const obj = Object.fromEntries(this.cache.entries());
|
|
62
|
+
await fs.writeFile(this.filePath, JSON.stringify(obj, null, 2), 'utf8');
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error('Telegix FileSessionStore save error:', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async get(key) {
|
|
69
|
+
await this._load();
|
|
70
|
+
const item = this.cache.get(key);
|
|
71
|
+
if (!item) return undefined;
|
|
72
|
+
if (Date.now() > item.expiresAt) {
|
|
73
|
+
this.cache.delete(key);
|
|
74
|
+
await this._save();
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return item.value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async set(key, value) {
|
|
81
|
+
await this._load();
|
|
82
|
+
const expiresAt = this.ttl === Infinity ? Infinity : Date.now() + this.ttl;
|
|
83
|
+
this.cache.set(key, { value, expiresAt });
|
|
84
|
+
await this._save();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async delete(key) {
|
|
88
|
+
await this._load();
|
|
89
|
+
this.cache.delete(key);
|
|
90
|
+
await this._save();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async clear() {
|
|
94
|
+
await this._load();
|
|
95
|
+
this.cache.clear();
|
|
96
|
+
await this._save();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Session middleware for Telegix
|
|
102
|
+
* @param {object} [options]
|
|
103
|
+
* @param {Function} [options.getSessionKey] - Custom function returning session key
|
|
104
|
+
* @param {object} [options.store] - Session store implementing get/set/delete
|
|
105
|
+
* @param {Function} [options.initial] - Function returning initial session data
|
|
106
|
+
* @param {number} [options.ttl] - Time-to-live in ms for default memory store
|
|
107
|
+
* @returns {Function} Telegix middleware
|
|
108
|
+
*/
|
|
109
|
+
export function session(options = {}) {
|
|
110
|
+
const store = options.store || new MemorySessionStore(options.ttl);
|
|
111
|
+
const getSessionKey =
|
|
112
|
+
options.getSessionKey ||
|
|
113
|
+
((ctx) => {
|
|
114
|
+
const chatId = ctx.chatId;
|
|
115
|
+
const userId = ctx.userId;
|
|
116
|
+
if (!chatId && !userId) return null;
|
|
117
|
+
return `${chatId ?? ''}:${userId ?? ''}`;
|
|
118
|
+
});
|
|
119
|
+
const initial = options.initial || (() => ({}));
|
|
120
|
+
|
|
121
|
+
return async (ctx, next) => {
|
|
122
|
+
const key = getSessionKey(ctx);
|
|
123
|
+
if (!key) {
|
|
124
|
+
return next();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let sessionData = await store.get(key);
|
|
128
|
+
if (sessionData === undefined || sessionData === null) {
|
|
129
|
+
sessionData = initial(ctx);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Attach session to ctx
|
|
133
|
+
ctx.session = sessionData;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await next();
|
|
137
|
+
} finally {
|
|
138
|
+
if (ctx.session === null || ctx.session === undefined) {
|
|
139
|
+
await store.delete(key);
|
|
140
|
+
} else {
|
|
141
|
+
await store.set(key, ctx.session);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
package/lib/telegix.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Pure JavaScript Telegram Bot Framework
|
|
3
|
+
* @module telegix
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Composer } from './composer.js';
|
|
7
|
+
import { Telegram } from './api.js';
|
|
8
|
+
import { Context } from './context.js';
|
|
9
|
+
import { Polling } from './polling.js';
|
|
10
|
+
import { createWebhookCallback } from './webhook.js';
|
|
11
|
+
import { TelegixError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
export class Telegix extends Composer {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} token - Telegram Bot Token from @BotFather
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {string} [options.apiRoot='https://api.telegram.org']
|
|
18
|
+
* @param {boolean} [options.testEnv=false]
|
|
19
|
+
* @param {number} [options.timeout=60000]
|
|
20
|
+
* @param {object} [options.botInfo] - Pre-fetched bot info
|
|
21
|
+
*/
|
|
22
|
+
constructor(token, options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
if (!token || typeof token !== 'string') {
|
|
26
|
+
throw new TelegixError('Telegix: Telegram Bot Token is required.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.token = token.trim();
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.telegram = new Telegram(this.token, options);
|
|
32
|
+
this.api = this.telegram; // alias
|
|
33
|
+
this.botInfo = options.botInfo || null;
|
|
34
|
+
this.polling = null;
|
|
35
|
+
this.errorHandler = (err, ctx) => {
|
|
36
|
+
console.error('Telegix Error:', err);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Custom error catcher
|
|
42
|
+
* @param {Function} handler - (err: Error, ctx?: Context) => void
|
|
43
|
+
* @returns {this}
|
|
44
|
+
*/
|
|
45
|
+
catch(handler) {
|
|
46
|
+
if (typeof handler !== 'function') {
|
|
47
|
+
throw new TypeError('Telegix.catch() expects a function handler');
|
|
48
|
+
}
|
|
49
|
+
this.errorHandler = handler;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle an incoming Telegram update object
|
|
55
|
+
* @param {object} update
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async handleUpdate(update) {
|
|
59
|
+
if (!update || typeof update !== 'object') return;
|
|
60
|
+
|
|
61
|
+
const ctx = new Context(update, this.telegram, this.botInfo);
|
|
62
|
+
try {
|
|
63
|
+
const fn = this.middleware();
|
|
64
|
+
await fn(ctx, () => Promise.resolve());
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (this.errorHandler) {
|
|
67
|
+
await this.errorHandler(err, ctx);
|
|
68
|
+
} else {
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Start polling for updates
|
|
76
|
+
* @param {object} [options]
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async startPolling(options = {}) {
|
|
80
|
+
if (this.polling) {
|
|
81
|
+
await this.polling.stop();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!this.botInfo) {
|
|
85
|
+
try {
|
|
86
|
+
this.botInfo = await this.telegram.getMe();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn('Telegix: Warning: Could not fetch getMe() before polling:', err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.polling = new Polling(
|
|
93
|
+
this.telegram,
|
|
94
|
+
(update) => this.handleUpdate(update),
|
|
95
|
+
{
|
|
96
|
+
onError: (err) => {
|
|
97
|
+
if (this.errorHandler) this.errorHandler(err);
|
|
98
|
+
},
|
|
99
|
+
...options,
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await this.polling.start();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stop bot polling
|
|
108
|
+
* @param {string} [reason]
|
|
109
|
+
*/
|
|
110
|
+
async stop(reason = 'manual') {
|
|
111
|
+
if (this.polling) {
|
|
112
|
+
await this.polling.stop();
|
|
113
|
+
this.polling = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Launch bot using long-polling or webhook
|
|
119
|
+
* @param {object} [options]
|
|
120
|
+
* @param {object|boolean} [options.polling=true] - Long polling configuration or true
|
|
121
|
+
* @param {object} [options.webhook] - Webhook configuration { domain, hookPath, port, secretToken }
|
|
122
|
+
* @param {boolean} [options.dropPendingUpdates=false]
|
|
123
|
+
* @returns {Promise<object>} Bot Info
|
|
124
|
+
*/
|
|
125
|
+
async launch(options = {}) {
|
|
126
|
+
// 1. Fetch bot info
|
|
127
|
+
if (!this.botInfo) {
|
|
128
|
+
this.botInfo = await this.telegram.getMe();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`š Telegix Bot started: @${this.botInfo.username} (ID: ${this.botInfo.id})`);
|
|
132
|
+
|
|
133
|
+
// 2. Setup shutdown hooks
|
|
134
|
+
const handleExit = (signal) => {
|
|
135
|
+
console.log(`\nš Telegix Bot stopping due to ${signal}...`);
|
|
136
|
+
this.stop(signal).then(() => {
|
|
137
|
+
process.exit(0);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (typeof process !== 'undefined' && process.once) {
|
|
142
|
+
process.once('SIGINT', () => handleExit('SIGINT'));
|
|
143
|
+
process.once('SIGTERM', () => handleExit('SIGTERM'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 3. Webhook or Polling
|
|
147
|
+
if (options.webhook) {
|
|
148
|
+
const { domain, hookPath = '/telegix-webhook', port = 3000, secretToken } = options.webhook;
|
|
149
|
+
const url = `${domain.replace(/\/$/, '')}${hookPath.startsWith('/') ? hookPath : `/${hookPath}`}`;
|
|
150
|
+
|
|
151
|
+
await this.telegram.setWebhook(url, {
|
|
152
|
+
secret_token: secretToken,
|
|
153
|
+
drop_pending_updates: options.dropPendingUpdates,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(`š Webhook set to: ${url}`);
|
|
157
|
+
} else {
|
|
158
|
+
const pollingOptions =
|
|
159
|
+
typeof options.polling === 'object'
|
|
160
|
+
? options.polling
|
|
161
|
+
: { dropPendingUpdates: options.dropPendingUpdates };
|
|
162
|
+
await this.startPolling(pollingOptions);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.botInfo;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns a standard HTTP webhook callback handler
|
|
170
|
+
* @param {string} [path='/']
|
|
171
|
+
* @param {object} [options]
|
|
172
|
+
*/
|
|
173
|
+
webhookCallback(path = '/', options = {}) {
|
|
174
|
+
return createWebhookCallback(this, path, options);
|
|
175
|
+
}
|
|
176
|
+
}
|