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/lib/errors.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Telegix - Telegram Bot API Error Classes
3
+ * @module telegix/errors
4
+ */
5
+
6
+ export class TelegixError extends Error {
7
+ /**
8
+ * @param {string} message
9
+ */
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'TelegixError';
13
+ }
14
+ }
15
+
16
+ export class TelegramError extends TelegixError {
17
+ /**
18
+ * @param {object} response - Telegram API response
19
+ * @param {number} [response.error_code] - HTTP/Telegram error code (e.g. 400, 401, 403, 404, 429)
20
+ * @param {string} [response.description] - Description from Telegram
21
+ * @param {object} [response.parameters] - Extra parameters (e.g. retry_after, migrate_to_chat_id)
22
+ * @param {string} [method] - The Telegram method that was called
23
+ * @param {object} [payload] - The payload that was sent
24
+ */
25
+ constructor(response, method = '', payload = {}) {
26
+ const errorCode = response?.error_code || 500;
27
+ const description = response?.description || 'Unknown Telegram API Error';
28
+ super(`Telegram API Error [${errorCode}]: ${description} (Method: ${method})`);
29
+ this.name = 'TelegramError';
30
+ this.errorCode = errorCode;
31
+ this.description = description;
32
+ this.parameters = response?.parameters || {};
33
+ this.method = method;
34
+ this.payload = payload;
35
+
36
+ if (this.parameters.retry_after) {
37
+ this.retryAfter = this.parameters.retry_after;
38
+ }
39
+ if (this.parameters.migrate_to_chat_id) {
40
+ this.migrateToChatId = this.parameters.migrate_to_chat_id;
41
+ }
42
+ }
43
+ }
44
+
45
+ export class NetworkError extends TelegixError {
46
+ /**
47
+ * @param {Error} error - Underlying network error
48
+ * @param {string} [method] - The Telegram method
49
+ */
50
+ constructor(error, method = '') {
51
+ super(`Network Error while calling ${method}: ${error.message}`);
52
+ this.name = 'NetworkError';
53
+ this.cause = error;
54
+ this.method = method;
55
+ }
56
+ }
57
+
58
+ export class PollingError extends TelegixError {
59
+ /**
60
+ * @param {Error} error - Original error
61
+ */
62
+ constructor(error) {
63
+ super(`Polling Error: ${error.message}`);
64
+ this.name = 'PollingError';
65
+ this.cause = error;
66
+ }
67
+ }
package/lib/format.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Telegix - Text Formatting Engine (HTML & MarkdownV2)
3
+ * @module telegix/format
4
+ */
5
+
6
+ /**
7
+ * Escape HTML special characters for Telegram Bot API
8
+ * @param {string} text
9
+ * @returns {string}
10
+ */
11
+ export function escapeHtml(text) {
12
+ if (text === null || text === undefined) return '';
13
+ return String(text)
14
+ .replace(/&/g, '&')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;');
17
+ }
18
+
19
+ /**
20
+ * Escape MarkdownV2 special characters for Telegram Bot API
21
+ * Characters: _ * [ ] ( ) ~ ` > # + - = | { } . !
22
+ * @param {string} text
23
+ * @returns {string}
24
+ */
25
+ export function escapeMarkdown(text) {
26
+ if (text === null || text === undefined) return '';
27
+ return String(text).replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, '\\$1');
28
+ }
29
+
30
+ /**
31
+ * HTML Formatting Helpers
32
+ */
33
+ export const html = {
34
+ escape: escapeHtml,
35
+ bold: (text) => `<b>${escapeHtml(text)}</b>`,
36
+ italic: (text) => `<i>${escapeHtml(text)}</i>`,
37
+ underline: (text) => `<u>${escapeHtml(text)}</u>`,
38
+ strikethrough: (text) => `<s>${escapeHtml(text)}</s>`,
39
+ spoiler: (text) => `<span class="tg-spoiler">${escapeHtml(text)}</span>`,
40
+ code: (text) => `<code>${escapeHtml(text)}</code>`,
41
+ pre: (codeText, language = '') => {
42
+ const langAttr = language ? ` class="language-${escapeHtml(language)}"` : '';
43
+ return `<pre><code${langAttr}>${escapeHtml(codeText)}</code></pre>`;
44
+ },
45
+ link: (text, url) => `<a href="${escapeHtml(url)}">${escapeHtml(text)}</a>`,
46
+ mention: (text, userId) => `<a href="tg://user?id=${userId}">${escapeHtml(text)}</a>`,
47
+ customEmoji: (text, customEmojiId) => `<tg-emoji emoji-id="${customEmojiId}">${escapeHtml(text)}</tg-emoji>`,
48
+ quote: (text) => `<blockquote>${escapeHtml(text)}</blockquote>`,
49
+ expandableBlockquote: (text) => `<blockquote expandable>${escapeHtml(text)}</blockquote>`,
50
+ };
51
+
52
+ /**
53
+ * MarkdownV2 Formatting Helpers
54
+ */
55
+ export const markdown = {
56
+ escape: escapeMarkdown,
57
+ bold: (text) => `*${escapeMarkdown(text)}*`,
58
+ italic: (text) => `_${escapeMarkdown(text)}_`,
59
+ underline: (text) => `__${escapeMarkdown(text)}__`,
60
+ strikethrough: (text) => `~${escapeMarkdown(text)}~`,
61
+ spoiler: (text) => `||${escapeMarkdown(text)}||`,
62
+ code: (text) => `\`${escapeMarkdown(text)}\``,
63
+ pre: (codeText, language = '') => `\`\`\`${language}\n${codeText.replace(/\\/g, '\\\\').replace(/`/g, '\\`')}\n\`\`\``,
64
+ link: (text, url) => `[${escapeMarkdown(text)}](${url.replace(/([)\\])/g, '\\$1')})`,
65
+ mention: (text, userId) => `[${escapeMarkdown(text)}](tg://user?id=${userId})`,
66
+ customEmoji: (text, customEmojiId) => `![${escapeMarkdown(text)}](tg://emoji?id=${customEmojiId})`,
67
+ quote: (text) => text.split('\n').map((line) => `>${escapeMarkdown(line)}`).join('\n'),
68
+ expandableBlockquote: (text) => `**>${escapeMarkdown(text)}||`,
69
+ };
70
+
71
+ /**
72
+ * Tagged template literal for safe HTML formatting
73
+ * Example: fmt`Hello <b>${username}</b>! Your balance is ${100} Stars.`
74
+ * @param {TemplateStringsArray} strings
75
+ * @param {...any} values
76
+ * @returns {string}
77
+ */
78
+ export function fmt(strings, ...values) {
79
+ let result = '';
80
+ for (let i = 0; i < strings.length; i++) {
81
+ result += strings[i];
82
+ if (i < values.length) {
83
+ const val = values[i];
84
+ // If it's already an HTML string with formatting or null/undefined
85
+ if (val === null || val === undefined) {
86
+ // do nothing
87
+ } else if (typeof val === 'object' && val.rawHtml) {
88
+ result += val.rawHtml;
89
+ } else {
90
+ result += escapeHtml(String(val));
91
+ }
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
97
+ // Attach helper methods directly to fmt
98
+ fmt.bold = html.bold;
99
+ fmt.italic = html.italic;
100
+ fmt.underline = html.underline;
101
+ fmt.strikethrough = html.strikethrough;
102
+ fmt.spoiler = html.spoiler;
103
+ fmt.code = html.code;
104
+ fmt.pre = html.pre;
105
+ fmt.link = html.link;
106
+ fmt.mention = html.mention;
107
+ fmt.customEmoji = html.customEmoji;
108
+ fmt.quote = html.quote;
109
+ fmt.expandableBlockquote = html.expandableBlockquote;
110
+ fmt.escape = escapeHtml;
111
+ fmt.html = html;
112
+ fmt.markdown = markdown;
113
+ fmt.raw = (str) => ({ rawHtml: String(str) });
114
+
115
+ export const Format = fmt;
package/lib/i18n.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Telegix - Internationalization (i18n) Middleware & Helper
3
+ * @module telegix/i18n
4
+ */
5
+
6
+ function getNestedValue(obj, path) {
7
+ if (!obj || typeof obj !== 'object') return undefined;
8
+ if (obj[path] !== undefined) return obj[path];
9
+
10
+ const keys = path.split('.');
11
+ let current = obj;
12
+ for (const k of keys) {
13
+ if (current && typeof current === 'object' && k in current) {
14
+ current = current[k];
15
+ } else {
16
+ return undefined;
17
+ }
18
+ }
19
+ return current;
20
+ }
21
+
22
+ export class I18n {
23
+ /**
24
+ * @param {object} [options]
25
+ * @param {string} [options.defaultLocale='en']
26
+ * @param {object} [options.translations={}]
27
+ * @param {Function} [options.localeFn]
28
+ * @param {boolean} [options.useSession=true]
29
+ */
30
+ constructor(options = {}) {
31
+ this.defaultLocale = options.defaultLocale || 'en';
32
+ this.translations = options.translations || {};
33
+ this.useSession = options.useSession !== false;
34
+ this.localeFn =
35
+ options.localeFn ||
36
+ ((ctx) => {
37
+ if (this.useSession && ctx.session) {
38
+ const sessionLocale = ctx.session.__locale || ctx.session.locale || ctx.session.language;
39
+ if (sessionLocale) return sessionLocale;
40
+ }
41
+ return ctx.from?.language_code || this.defaultLocale;
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Register or add translations for a locale
47
+ * @param {string} locale
48
+ * @param {object} dict
49
+ */
50
+ addTranslation(locale, dict) {
51
+ this.translations[locale] = {
52
+ ...(this.translations[locale] || {}),
53
+ ...dict,
54
+ };
55
+ return this;
56
+ }
57
+
58
+ /**
59
+ * Add multiple translation dictionaries
60
+ * @param {Record<string, object>} translations
61
+ */
62
+ addTranslations(translations = {}) {
63
+ for (const [locale, dict] of Object.entries(translations)) {
64
+ this.addTranslation(locale, dict);
65
+ }
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Translate a key with optional interpolation params and pluralization
71
+ * @param {string} locale
72
+ * @param {string} key
73
+ * @param {object} [params]
74
+ * @returns {string}
75
+ */
76
+ t(locale, key, params = {}) {
77
+ const activeDict = this.translations[locale] || {};
78
+ const defaultDict = this.translations[this.defaultLocale] || {};
79
+
80
+ let val = getNestedValue(activeDict, key);
81
+ if (val === undefined) {
82
+ val = getNestedValue(defaultDict, key);
83
+ }
84
+ if (val === undefined) {
85
+ val = key;
86
+ }
87
+
88
+ // Handle pluralization if val is an object (e.g. { one: '1 item', other: '{{count}} items' })
89
+ if (typeof val === 'object' && val !== null) {
90
+ const count = Number(params.count);
91
+ if (!isNaN(count)) {
92
+ if (count === 0 && val.zero) {
93
+ val = val.zero;
94
+ } else if (count === 1 && val.one) {
95
+ val = val.one;
96
+ } else if (val.other) {
97
+ val = val.other;
98
+ } else {
99
+ val = JSON.stringify(val);
100
+ }
101
+ } else {
102
+ val = JSON.stringify(val);
103
+ }
104
+ }
105
+
106
+ let text = String(val);
107
+
108
+ // Interpolate variables like {{name}} or {name}
109
+ for (const [k, v] of Object.entries(params)) {
110
+ text = text.replace(new RegExp(`{{\\s*${k}\\s*}}`, 'g'), String(v));
111
+ text = text.replace(new RegExp(`{\\s*${k}\\s*}`, 'g'), String(v));
112
+ }
113
+
114
+ return text;
115
+ }
116
+
117
+ /**
118
+ * Create i18n middleware for Telegix
119
+ */
120
+ middleware() {
121
+ return (ctx, next) => {
122
+ let rawLocale = this.localeFn(ctx) || this.defaultLocale;
123
+ // If locale like 'en-US' or 'id-ID', check exact first, then language prefix
124
+ let activeLocale = this.defaultLocale;
125
+ if (this.translations[rawLocale]) {
126
+ activeLocale = rawLocale;
127
+ } else {
128
+ const prefix = String(rawLocale).split('-')[0].toLowerCase();
129
+ if (this.translations[prefix]) {
130
+ activeLocale = prefix;
131
+ }
132
+ }
133
+
134
+ const self = this;
135
+ const i18nContext = {
136
+ get locale() {
137
+ return activeLocale;
138
+ },
139
+ set locale(newLocale) {
140
+ activeLocale = newLocale;
141
+ if (self.useSession && ctx.session) {
142
+ ctx.session.__locale = newLocale;
143
+ }
144
+ },
145
+ setLocale(newLocale) {
146
+ this.locale = newLocale;
147
+ return activeLocale;
148
+ },
149
+ t: (key, params) => self.t(activeLocale, key, params),
150
+ };
151
+
152
+ ctx.i18n = i18nContext;
153
+ ctx.t = (key, params) => i18nContext.t(key, params);
154
+
155
+ return next();
156
+ };
157
+ }
158
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Telegix - Inline Query Debouncer & Cache Helper
3
+ */
4
+
5
+ const queryCache = new Map();
6
+ const debounceTimers = new Map();
7
+
8
+ /**
9
+ * Middleware or helper to debounce inline queries and cache results
10
+ * @param {object} [options] - { windowMs?: number, cacheTtlMs?: number }
11
+ */
12
+ export function inlineDebounceMiddleware(options = {}) {
13
+ const windowMs = options.windowMs || 300;
14
+ const cacheTtlMs = options.cacheTtlMs || 60000;
15
+
16
+ return async (ctx, next) => {
17
+ if (!ctx.inlineQuery) {
18
+ return next();
19
+ }
20
+
21
+ const userId = ctx.from?.id;
22
+ const query = ctx.inlineQuery.query || '';
23
+ if (!userId) return next();
24
+
25
+ const cacheKey = `${userId}:${query}`;
26
+ const now = Date.now();
27
+
28
+ // Check cache
29
+ if (queryCache.has(cacheKey)) {
30
+ const cached = queryCache.get(cacheKey);
31
+ if (now - cached.timestamp < cacheTtlMs) {
32
+ return ctx.answerInlineQuery(cached.results, cached.options);
33
+ } else {
34
+ queryCache.delete(cacheKey);
35
+ }
36
+ }
37
+
38
+ // Attach cache helper to ctx
39
+ ctx.cacheInlineResults = (results, extraOptions = {}) => {
40
+ queryCache.set(cacheKey, {
41
+ results,
42
+ options: extraOptions,
43
+ timestamp: Date.now(),
44
+ });
45
+ };
46
+
47
+ return next();
48
+ };
49
+ }
package/lib/inline.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Telegix - Inline Query Pagination & Result Builders
3
+ */
4
+
5
+ export class InlineQueryResultBuilder {
6
+ static article(id, title, messageText, options = {}) {
7
+ return {
8
+ type: 'article',
9
+ id: String(id),
10
+ title,
11
+ input_message_content: {
12
+ message_text: messageText,
13
+ parse_mode: options.parseMode || 'HTML',
14
+ ...options.inputMessageContent,
15
+ },
16
+ description: options.description,
17
+ thumb_url: options.thumbUrl,
18
+ thumb_width: options.thumbWidth,
19
+ thumb_height: options.thumbHeight,
20
+ reply_markup: options.replyMarkup,
21
+ };
22
+ }
23
+
24
+ static photo(id, photoUrl, options = {}) {
25
+ return {
26
+ type: 'photo',
27
+ id: String(id),
28
+ photo_url: photoUrl,
29
+ thumb_url: options.thumbUrl || photoUrl,
30
+ caption: options.caption,
31
+ parse_mode: options.parseMode || 'HTML',
32
+ caption_entities: options.captionEntities,
33
+ description: options.description,
34
+ title: options.title,
35
+ reply_markup: options.replyMarkup,
36
+ photo_width: options.photoWidth,
37
+ photo_height: options.photoHeight,
38
+ };
39
+ }
40
+
41
+ static document(id, documentUrl, title, options = {}) {
42
+ return {
43
+ type: 'document',
44
+ id: String(id),
45
+ title,
46
+ document_url: documentUrl,
47
+ mime_type: options.mimeType || 'application/pdf',
48
+ caption: options.caption,
49
+ description: options.description,
50
+ reply_markup: options.replyMarkup,
51
+ };
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Helper to paginate and answer inline queries easily
57
+ * @param {object} ctx - Context
58
+ * @param {Array} items - All items to paginate
59
+ * @param {Function} formatterFn - Function(item, index) returning InlineQueryResult
60
+ * @param {object} [options] - { limit: 10, cacheTime: 300, isPersonal: true }
61
+ */
62
+ export async function paginateInlineQuery(ctx, items, formatterFn, options = {}) {
63
+ const limit = options.limit || 10;
64
+ const cacheTime = options.cacheTime !== undefined ? options.cacheTime : 300;
65
+ const isPersonal = options.isPersonal !== undefined ? options.isPersonal : true;
66
+
67
+ const offset = parseInt(ctx.inlineQuery?.offset || '0', 10) || 0;
68
+ const pageItems = items.slice(offset, offset + limit);
69
+
70
+ const results = pageItems.map((item, index) => formatterFn(item, offset + index));
71
+ const nextOffset = offset + limit < items.length ? String(offset + limit) : '';
72
+
73
+ return ctx.answerInlineQuery(results, {
74
+ next_offset: nextOffset,
75
+ cache_time: cacheTime,
76
+ is_personal: isPersonal,
77
+ ...options.extra,
78
+ });
79
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Telegix - MarkdownV2 Escape Utility
3
+ */
4
+
5
+ /**
6
+ * Escapes special characters for Telegram MarkdownV2 format.
7
+ * Characters to escape: _ * [ ] ( ) ~ ` > # + - = | { } . !
8
+ * @param {string} str - Raw text string to escape
9
+ * @returns {string} Escaped string safe for MarkdownV2
10
+ */
11
+ export function escapeMarkdownV2(str) {
12
+ if (typeof str !== 'string') return '';
13
+ return str.replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
14
+ }
15
+
16
+ /**
17
+ * Helper to build safe MarkdownV2 snippets
18
+ */
19
+ export const mdv2 = {
20
+ escape: escapeMarkdownV2,
21
+ bold: (text) => `*${escapeMarkdownV2(text)}*`,
22
+ italic: (text) => `_${escapeMarkdownV2(text)}_`,
23
+ underline: (text) => `__${escapeMarkdownV2(text)}__`,
24
+ strikethrough: (text) => `~${escapeMarkdownV2(text)}~`,
25
+ spoiler: (text) => `||${escapeMarkdownV2(text)}||`,
26
+ code: (text) => `\`${text.replace(/`/g, '\\`')}\``,
27
+ pre: (text, language = '') => `\`\`\`${language}\n${text}\n\`\`\``,
28
+ link: (text, url) => `[${escapeMarkdownV2(text)}](${url})`,
29
+ };