@codebam/cf-workers-telegram-bot 11.18.0 → 12.1.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.
@@ -1,6 +1,7 @@
1
1
  import { InlineQueryResult as TelegramInlineQueryResult } from '@grammyjs/types';
2
2
  /** Interface for common Telegram API parameters */
3
3
  export interface TelegramApiBaseParams {
4
+ [key: string]: unknown;
4
5
  chat_id: number | string;
5
6
  message_thread_id?: number;
6
7
  business_connection_id?: string | number;
@@ -95,7 +96,19 @@ export interface SendVoiceParams extends TelegramApiBaseParams {
95
96
  }
96
97
  /** Type for all possible API parameters */
97
98
  export type TelegramApiParams = SendMessageParams | SendPhotoParams | SendVideoParams | SendVoiceParams | SendChatActionParams | AnswerCallbackParams | AnswerInlineParams | AnswerGuestParams | SendInvoiceParams | AnswerPreCheckoutParams | Record<string, unknown>;
98
- /** Class representing the Telegram API and all its methods */
99
+ /** Interface for edit message text parameters */
100
+ export interface EditMessageTextParams extends Partial<TelegramApiBaseParams> {
101
+ [key: string]: unknown;
102
+ chat_id?: number | string;
103
+ message_id?: number;
104
+ inline_message_id?: string;
105
+ text: string;
106
+ parse_mode?: string;
107
+ disable_web_page_preview?: boolean;
108
+ reply_markup?: object;
109
+ business_connection_id?: string | number;
110
+ }
111
+ /** Class representing the telegram API and all its methods */
99
112
  export default class TelegramApi {
100
113
  /**
101
114
  * Get the API URL for a given bot API and slug
@@ -104,7 +117,7 @@ export default class TelegramApi {
104
117
  * @param data - data to append to the request
105
118
  * @returns Request object with the full URL and parameters
106
119
  */
107
- getApiUrl(botApi: string, slug: string, data: TelegramApiParams): Request;
120
+ getApiUrl(botApi: string, slug: string, data: TelegramApiParams, method?: 'GET' | 'POST'): Request;
108
121
  /**
109
122
  * Fetch a URL and log the response
110
123
  * @param url - the URL to fetch
@@ -195,16 +208,7 @@ export default class TelegramApi {
195
208
  * @param data - data to append to the request
196
209
  * @returns Promise with the API response
197
210
  */
198
- editMessageText(botApi: string, data: {
199
- chat_id?: number | string;
200
- message_id?: number;
201
- inline_message_id?: string;
202
- text: string;
203
- parse_mode?: string;
204
- disable_web_page_preview?: boolean;
205
- reply_markup?: object;
206
- business_connection_id?: string | number;
207
- }): Promise<Response>;
211
+ editMessageText(botApi: string, data: EditMessageTextParams): Promise<Response>;
208
212
  sendMessageDraft(botApi: string, data: SendMessageDraftParams): Promise<Response>;
209
213
  /**
210
214
  * Send an invoice to a user
@@ -220,6 +224,13 @@ export default class TelegramApi {
220
224
  * @returns Promise with the API response
221
225
  */
222
226
  answerPreCheckoutQuery(botApi: string, data: AnswerPreCheckoutParams): Promise<Response>;
227
+ /**
228
+ * Get information about a business connection
229
+ * @param botApi - full URL to the telegram API without slug
230
+ * @param business_connection_id - unique identifier of the business connection
231
+ * @returns Promise with the API response
232
+ */
233
+ getBusinessConnection(botApi: string, business_connection_id: string): Promise<Response>;
223
234
  /**
224
235
  * Get basic information about the bot
225
236
  * @param botApi - full URL to the telegram API without slug
@@ -1,4 +1,4 @@
1
- /** Class representing the Telegram API and all its methods */
1
+ /** Class representing the telegram API and all its methods */
2
2
  export default class TelegramApi {
3
3
  /**
4
4
  * Get the API URL for a given bot API and slug
@@ -7,15 +7,24 @@ export default class TelegramApi {
7
7
  * @param data - data to append to the request
8
8
  * @returns Request object with the full URL and parameters
9
9
  */
10
- getApiUrl(botApi, slug, data) {
11
- const request = new URL(botApi + (slug.startsWith('/') || botApi.endsWith('/') ? '' : '/') + slug);
12
- const params = new URLSearchParams();
13
- for (const [key, value] of Object.entries(data)) {
14
- if (value !== undefined) {
15
- params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));
10
+ getApiUrl(botApi, slug, data, method = 'GET') {
11
+ const baseUrl = botApi + (slug.startsWith('/') || botApi.endsWith('/') ? '' : '/') + slug;
12
+ if (method === 'GET') {
13
+ const url = new URL(baseUrl);
14
+ for (const [key, value] of Object.entries(data)) {
15
+ if (value !== undefined) {
16
+ url.searchParams.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));
17
+ }
16
18
  }
19
+ return new Request(url.toString());
17
20
  }
18
- return new Request(`${request.toString()}?${params.toString()}`);
21
+ return new Request(baseUrl, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ },
26
+ body: JSON.stringify(data),
27
+ });
19
28
  }
20
29
  /**
21
30
  * Fetch a URL and log the response
@@ -31,12 +40,20 @@ export default class TelegramApi {
31
40
  let errorDescription = '';
32
41
  try {
33
42
  const json = (await cloned.json());
34
- errorDescription = json.description ? `: ${json.description}` : '';
43
+ errorDescription = json.description || '';
35
44
  }
36
45
  catch {
37
46
  // ignore
38
47
  }
39
- throw new Error(`Telegram API error: ${String(response.status)} ${response.statusText}${errorDescription}`);
48
+ if (errorDescription.includes('BUSINESS_CONNECTION_INVALID') || errorDescription.includes('BUSINESS_PEER_INVALID')) {
49
+ console.warn(`Telegram API business error: ${errorDescription}`);
50
+ throw new Error('BUSINESS_CONNECTION_INVALID');
51
+ }
52
+ if (errorDescription.includes('PEER_ID_INVALID')) {
53
+ console.warn(`Telegram API peer error: ${errorDescription}`);
54
+ throw new Error('PEER_ID_INVALID');
55
+ }
56
+ throw new Error(`Telegram API error: ${String(response.status)} ${response.statusText}${errorDescription ? ': ' + errorDescription : ''}`);
40
57
  }
41
58
  const cloned = response.clone();
42
59
  try {
@@ -59,7 +76,7 @@ export default class TelegramApi {
59
76
  * @returns Promise with the API response
60
77
  */
61
78
  async sendChatAction(botApi, data) {
62
- const url = this.getApiUrl(botApi, 'sendChatAction', data);
79
+ const url = this.getApiUrl(botApi, 'sendChatAction', data, 'POST');
63
80
  return await this.fetchAndLog(url, 'sendChatAction', data);
64
81
  }
65
82
  /**
@@ -73,7 +90,7 @@ export default class TelegramApi {
73
90
  if (!data.file_id || data.file_id === '') {
74
91
  throw new Error('No file_id provided');
75
92
  }
76
- const url = this.getApiUrl(botApi, 'getFile', data);
93
+ const url = this.getApiUrl(botApi, 'getFile', data, 'POST');
77
94
  const response = await this.fetchAndLog(url, 'getFile', data);
78
95
  const json = await response.json();
79
96
  if (!json.ok || !json.result?.file_path) {
@@ -92,7 +109,7 @@ export default class TelegramApi {
92
109
  * @returns Promise with the API response
93
110
  */
94
111
  async sendMessage(botApi, data) {
95
- const url = this.getApiUrl(botApi, 'sendMessage', data);
112
+ const url = this.getApiUrl(botApi, 'sendMessage', data, 'POST');
96
113
  return await this.fetchAndLog(url, 'sendMessage', data);
97
114
  }
98
115
  /**
@@ -102,7 +119,7 @@ export default class TelegramApi {
102
119
  * @returns Promise with the API response
103
120
  */
104
121
  async sendVideo(botApi, data) {
105
- const url = this.getApiUrl(botApi, 'sendVideo', data);
122
+ const url = this.getApiUrl(botApi, 'sendVideo', data, 'POST');
106
123
  return await this.fetchAndLog(url, 'sendVideo', data);
107
124
  }
108
125
  /**
@@ -112,7 +129,7 @@ export default class TelegramApi {
112
129
  * @returns Promise with the API response
113
130
  */
114
131
  async sendPhoto(botApi, data) {
115
- const url = this.getApiUrl(botApi, 'sendPhoto', data);
132
+ const url = this.getApiUrl(botApi, 'sendPhoto', data, 'POST');
116
133
  return await this.fetchAndLog(url, 'sendPhoto', data);
117
134
  }
118
135
  /**
@@ -122,7 +139,7 @@ export default class TelegramApi {
122
139
  * @returns Promise with the API response
123
140
  */
124
141
  async sendVoice(botApi, data) {
125
- const url = this.getApiUrl(botApi, 'sendVoice', data);
142
+ const url = this.getApiUrl(botApi, 'sendVoice', data, 'POST');
126
143
  return await this.fetchAndLog(url, 'sendVoice', data);
127
144
  }
128
145
  /**
@@ -139,7 +156,7 @@ export default class TelegramApi {
139
156
  is_personal: data.is_personal,
140
157
  next_offset: data.next_offset,
141
158
  };
142
- const url = this.getApiUrl(botApi, 'answerInlineQuery', params);
159
+ const url = this.getApiUrl(botApi, 'answerInlineQuery', params, 'POST');
143
160
  return await this.fetchAndLog(url, 'answerInlineQuery', params);
144
161
  }
145
162
  /**
@@ -149,7 +166,7 @@ export default class TelegramApi {
149
166
  * @returns Promise with the API response
150
167
  */
151
168
  async answerCallback(botApi, data) {
152
- const url = this.getApiUrl(botApi, 'answerCallbackQuery', data);
169
+ const url = this.getApiUrl(botApi, 'answerCallbackQuery', data, 'POST');
153
170
  return await this.fetchAndLog(url, 'answerCallbackQuery', data);
154
171
  }
155
172
  /**
@@ -159,7 +176,7 @@ export default class TelegramApi {
159
176
  * @returns Promise with the API response
160
177
  */
161
178
  async answerGuestQuery(botApi, data) {
162
- const url = this.getApiUrl(botApi, 'answerGuestQuery', data);
179
+ const url = this.getApiUrl(botApi, 'answerGuestQuery', data, 'POST');
163
180
  return await this.fetchAndLog(url, 'answerGuestQuery', data);
164
181
  }
165
182
  /**
@@ -169,7 +186,7 @@ export default class TelegramApi {
169
186
  * @returns Promise with the API response
170
187
  */
171
188
  async deleteMessage(botApi, data) {
172
- const url = this.getApiUrl(botApi, 'deleteMessage', data);
189
+ const url = this.getApiUrl(botApi, 'deleteMessage', data, 'POST');
173
190
  return await this.fetchAndLog(url, 'deleteMessage', data);
174
191
  }
175
192
  /**
@@ -179,11 +196,11 @@ export default class TelegramApi {
179
196
  * @returns Promise with the API response
180
197
  */
181
198
  async editMessageText(botApi, data) {
182
- const url = this.getApiUrl(botApi, 'editMessageText', data);
199
+ const url = this.getApiUrl(botApi, 'editMessageText', data, 'POST');
183
200
  return await this.fetchAndLog(url, 'editMessageText', data);
184
201
  }
185
202
  async sendMessageDraft(botApi, data) {
186
- const url = this.getApiUrl(botApi, 'sendMessageDraft', data);
203
+ const url = this.getApiUrl(botApi, 'sendMessageDraft', data, 'POST');
187
204
  return await this.fetchAndLog(url, 'sendMessageDraft', data);
188
205
  }
189
206
  /**
@@ -193,7 +210,7 @@ export default class TelegramApi {
193
210
  * @returns Promise with the API response
194
211
  */
195
212
  async sendInvoice(botApi, data) {
196
- const url = this.getApiUrl(botApi, 'sendInvoice', data);
213
+ const url = this.getApiUrl(botApi, 'sendInvoice', data, 'POST');
197
214
  return await this.fetchAndLog(url, 'sendInvoice', data);
198
215
  }
199
216
  /**
@@ -203,16 +220,26 @@ export default class TelegramApi {
203
220
  * @returns Promise with the API response
204
221
  */
205
222
  async answerPreCheckoutQuery(botApi, data) {
206
- const url = this.getApiUrl(botApi, 'answerPreCheckoutQuery', data);
223
+ const url = this.getApiUrl(botApi, 'answerPreCheckoutQuery', data, 'POST');
207
224
  return await this.fetchAndLog(url, 'answerPreCheckoutQuery', data);
208
225
  }
226
+ /**
227
+ * Get information about a business connection
228
+ * @param botApi - full URL to the telegram API without slug
229
+ * @param business_connection_id - unique identifier of the business connection
230
+ * @returns Promise with the API response
231
+ */
232
+ async getBusinessConnection(botApi, business_connection_id) {
233
+ const url = this.getApiUrl(botApi, 'getBusinessConnection', { business_connection_id }, 'POST');
234
+ return await this.fetchAndLog(url, 'getBusinessConnection', { business_connection_id });
235
+ }
209
236
  /**
210
237
  * Get basic information about the bot
211
238
  * @param botApi - full URL to the telegram API without slug
212
239
  * @returns Promise with the API response
213
240
  */
214
241
  async getMe(botApi) {
215
- const url = this.getApiUrl(botApi, 'getMe', {});
242
+ const url = this.getApiUrl(botApi, 'getMe', {}, 'GET');
216
243
  return await this.fetchAndLog(url, 'getMe', {});
217
244
  }
218
245
  }
@@ -160,6 +160,10 @@ export default class TelegramBot {
160
160
  console.log(this.update);
161
161
  const ctx = new TelegramExecutionContext(this, this.update);
162
162
  this.currentContext = ctx;
163
+ if (ctx.shouldProcess && !(await ctx.shouldProcess())) {
164
+ console.log('Skipping update processing based on context validation');
165
+ return new Response('ok');
166
+ }
163
167
  // Run middleware
164
168
  for (const middleware of this.middleware) {
165
169
  const result = await middleware(ctx);
@@ -3,6 +3,10 @@ import TelegramApi from './telegram_api.js';
3
3
  import TelegramBot from './telegram_bot.js';
4
4
  /** Class representing the context of execution */
5
5
  export default class TelegramExecutionContext {
6
+ /** Cache for business connection owners */
7
+ private static businessOwners;
8
+ /** Cache for dead business connections */
9
+ private static poisonedConnections;
6
10
  /** an instance of the telegram bot */
7
11
  bot: TelegramBot;
8
12
  /** an instance of the telegram update */
@@ -43,6 +47,11 @@ export default class TelegramExecutionContext {
43
47
  * Determine the type of update received
44
48
  * @returns The update type as a string
45
49
  */
50
+ /**
51
+ * Determine if the current update should be processed.
52
+ * For business messages, this checks if the connection is valid and has reply permissions.
53
+ */
54
+ shouldProcess(): Promise<boolean>;
46
55
  private determineUpdateType;
47
56
  /**
48
57
  * Get the chat ID from the current update
@@ -65,6 +74,10 @@ export default class TelegramExecutionContext {
65
74
  * @param options - any additional options to pass to sendVideo
66
75
  * @returns Promise with the API response
67
76
  */
77
+ /**
78
+ * Helper to handle business connection fallbacks
79
+ */
80
+ private withBusinessFallback;
68
81
  replyVideo(video: string, options?: Record<string, number | string | boolean>): Promise<Response | null>;
69
82
  /**
70
83
  * Get File from telegram file_id
@@ -148,14 +161,7 @@ export default class TelegramExecutionContext {
148
161
  * @param options - any additional options to pass to sendMessage/editMessageText
149
162
  * @returns Promise with the API response
150
163
  */
151
- streamReply(message: string, draft_id: number, parse_mode?: string, options?: Record<string, number | string | boolean | object>): Promise<Response>;
152
- /**
153
- * Reply to the last message with text
154
- * @param message - text to reply with
155
- * @param parse_mode - one of HTML, MarkdownV2, Markdown, or an empty string for ascii
156
- * @param options - any additional options to pass to sendMessage
157
- * @returns Promise with the API response
158
- */
164
+ streamReply(message: string, draft_id: number, parse_mode?: string, options?: Record<string, number | string | boolean | object>, finish?: boolean): Promise<Response | null>;
159
165
  reply(message: string, parse_mode?: string, reply?: boolean, options?: Record<string, number | string | boolean>): Promise<Response | null>;
160
166
  /**
161
167
  * Send an invoice for Telegram Stars
@@ -165,7 +171,7 @@ export default class TelegramExecutionContext {
165
171
  * @param amount - amount of stars
166
172
  * @returns Promise with the API response
167
173
  */
168
- sendStarsInvoice(title: string, description: string, payload: string, amount: number): Promise<Response>;
174
+ sendStarsInvoice(title: string, description: string, payload: string, amount: number): Promise<Response | null>;
169
175
  /**
170
176
  * Answer a pre-checkout query
171
177
  * @param ok - whether the payment can proceed
@@ -1,6 +1,10 @@
1
1
  import TelegramApi from './telegram_api.js';
2
2
  /** Class representing the context of execution */
3
3
  export default class TelegramExecutionContext {
4
+ /** Cache for business connection owners */
5
+ static businessOwners = new Map();
6
+ /** Cache for dead business connections */
7
+ static poisonedConnections = new Set();
4
8
  /** an instance of the telegram bot */
5
9
  bot;
6
10
  /** an instance of the telegram update */
@@ -63,6 +67,54 @@ export default class TelegramExecutionContext {
63
67
  * Determine the type of update received
64
68
  * @returns The update type as a string
65
69
  */
70
+ /**
71
+ * Determine if the current update should be processed.
72
+ * For business messages, this checks if the connection is valid and has reply permissions.
73
+ */
74
+ async shouldProcess() {
75
+ if (this.update_type !== 'business_message') {
76
+ return true;
77
+ }
78
+ const connectionId = this.update.business_message?.business_connection_id?.toString();
79
+ if (!connectionId) {
80
+ return true;
81
+ }
82
+ if (TelegramExecutionContext.poisonedConnections.has(connectionId)) {
83
+ return false;
84
+ }
85
+ let ownerId = TelegramExecutionContext.businessOwners.get(connectionId);
86
+ if (ownerId === undefined) {
87
+ try {
88
+ const response = await this.api.getBusinessConnection(this.bot.api.toString(), connectionId);
89
+ if (response.status === 200) {
90
+ const json = await response.json();
91
+ if (json.ok && json.result) {
92
+ ownerId = json.result.user?.id || json.result.user_chat_id;
93
+ if (ownerId) {
94
+ TelegramExecutionContext.businessOwners.set(connectionId, ownerId);
95
+ }
96
+ if (json.result.can_reply === false) {
97
+ console.warn('Business connection ' + connectionId + ' lacks reply permissions, poisoning connection');
98
+ TelegramExecutionContext.poisonedConnections.add(connectionId);
99
+ return false;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ catch (e) {
105
+ if (e instanceof Error && e.message === 'BUSINESS_CONNECTION_INVALID') {
106
+ console.warn('Business connection ' + connectionId + ' is invalid, poisoning connection');
107
+ TelegramExecutionContext.poisonedConnections.add(connectionId);
108
+ return false;
109
+ }
110
+ console.warn('Failed to fetch business connection info:', e);
111
+ }
112
+ }
113
+ if (ownerId !== undefined && (this.getChatId() === ownerId.toString() || this.userId === ownerId)) {
114
+ return false;
115
+ }
116
+ return true;
117
+ }
66
118
  determineUpdateType() {
67
119
  if (this.update.message?.photo) {
68
120
  return 'photo';
@@ -123,6 +175,9 @@ export default class TelegramExecutionContext {
123
175
  if (this.update.message?.message_id) {
124
176
  return this.update.message.message_id.toString();
125
177
  }
178
+ else if (this.update.business_message?.message_id) {
179
+ return this.update.business_message.message_id.toString();
180
+ }
126
181
  else if (this.update.guest_message?.message_id) {
127
182
  return this.update.guest_message.message_id.toString();
128
183
  }
@@ -133,7 +188,7 @@ export default class TelegramExecutionContext {
133
188
  * @returns The message thread ID as a number or undefined
134
189
  */
135
190
  getThreadId() {
136
- return this.update.message?.message_thread_id;
191
+ return this.update.message?.message_thread_id ?? this.update.business_message?.message_thread_id;
137
192
  }
138
193
  /**
139
194
  * Reply to the last message with a video
@@ -141,36 +196,85 @@ export default class TelegramExecutionContext {
141
196
  * @param options - any additional options to pass to sendVideo
142
197
  * @returns Promise with the API response
143
198
  */
144
- async replyVideo(video, options = {}) {
145
- switch (this.update_type) {
146
- case 'voice':
147
- case 'message':
148
- return await this.api.sendVideo(this.bot.api.toString(), {
149
- ...options,
150
- chat_id: this.getChatId(),
151
- message_thread_id: this.getThreadId(),
152
- reply_to_message_id: this.getMessageId(),
153
- video,
154
- });
155
- case 'business_message':
156
- return await this.api.sendVideo(this.bot.api.toString(), {
157
- ...options,
158
- chat_id: this.getChatId(),
159
- message_thread_id: this.getThreadId(),
160
- business_connection_id: this.update.business_message?.business_connection_id?.toString() ?? '',
161
- video,
162
- });
163
- case 'guest_message':
164
- return await this.answerGuestQueryVideo(video);
165
- case 'inline':
166
- return await this.api.answerInline(this.bot.api.toString(), {
167
- ...options,
168
- inline_query_id: this.update.inline_query?.id.toString() ?? '',
169
- results: [{ type: 'video', id: crypto.randomUUID(), video_url: video, mime_type: 'video/mp4', thumbnail_url: video, title: 'Video' }],
170
- });
171
- default:
199
+ /**
200
+ * Helper to handle business connection fallbacks
201
+ */
202
+ async withBusinessFallback(params, apiMethod) {
203
+ const connectionId = params.business_connection_id?.toString();
204
+ if (connectionId) {
205
+ if (TelegramExecutionContext.poisonedConnections.has(connectionId)) {
206
+ return null;
207
+ }
208
+ let ownerId = TelegramExecutionContext.businessOwners.get(connectionId);
209
+ if (ownerId === undefined) {
210
+ try {
211
+ const response = await this.api.getBusinessConnection(this.bot.api.toString(), connectionId);
212
+ if (response.status === 200) {
213
+ const json = await response.json();
214
+ if (json.ok && json.result) {
215
+ ownerId = json.result.user?.id || json.result.user_chat_id;
216
+ if (ownerId) {
217
+ TelegramExecutionContext.businessOwners.set(connectionId, ownerId);
218
+ }
219
+ if (json.result.can_reply === false) {
220
+ TelegramExecutionContext.poisonedConnections.add(connectionId);
221
+ return null;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ catch (e) {
227
+ if (e instanceof Error && e.message === 'BUSINESS_CONNECTION_INVALID') {
228
+ TelegramExecutionContext.poisonedConnections.add(connectionId);
229
+ return null;
230
+ }
231
+ }
232
+ }
233
+ if (ownerId !== undefined && params.chat_id?.toString() === ownerId.toString()) {
172
234
  return null;
235
+ }
236
+ }
237
+ try {
238
+ return await apiMethod(this.bot.api.toString(), params);
239
+ }
240
+ catch (e) {
241
+ if (e instanceof Error) {
242
+ if (e.message === 'BUSINESS_CONNECTION_INVALID') {
243
+ if (connectionId) {
244
+ TelegramExecutionContext.poisonedConnections.add(connectionId);
245
+ }
246
+ return null;
247
+ }
248
+ if (e.message === 'PEER_ID_INVALID') {
249
+ return null;
250
+ }
251
+ }
252
+ throw e;
253
+ }
254
+ }
255
+ async replyVideo(video, options = {}) {
256
+ const params = {
257
+ ...options,
258
+ chat_id: this.getChatId(),
259
+ message_thread_id: this.getThreadId(),
260
+ reply_to_message_id: this.getMessageId(),
261
+ video,
262
+ };
263
+ if (this.update_type === 'business_message') {
264
+ params['business_connection_id'] = this.update.business_message?.business_connection_id;
265
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendVideo(api, data));
266
+ }
267
+ if (this.update_type === 'guest_message') {
268
+ return await this.answerGuestQueryVideo(video);
269
+ }
270
+ if (this.update_type === 'inline') {
271
+ return await this.api.answerInline(this.bot.api.toString(), {
272
+ ...options,
273
+ inline_query_id: this.update.inline_query?.id.toString() ?? '',
274
+ results: [{ type: 'video', id: crypto.randomUUID(), video_url: video, mime_type: 'video/mp4', thumbnail_url: video, title: 'Video' }],
275
+ });
173
276
  }
277
+ return await this.api.sendVideo(this.bot.api.toString(), params);
174
278
  }
175
279
  /**
176
280
  * Get File from telegram file_id
@@ -188,37 +292,28 @@ export default class TelegramExecutionContext {
188
292
  * @returns Promise with the API response
189
293
  */
190
294
  async replyPhoto(photo, caption = '', options = {}) {
191
- switch (this.update_type) {
192
- case 'voice':
193
- case 'photo':
194
- case 'message':
195
- return await this.api.sendPhoto(this.bot.api.toString(), {
196
- ...options,
197
- chat_id: this.getChatId(),
198
- message_thread_id: this.getThreadId(),
199
- reply_to_message_id: this.getMessageId(),
200
- photo,
201
- caption,
202
- });
203
- case 'business_message':
204
- return await this.api.sendPhoto(this.bot.api.toString(), {
205
- ...options,
206
- chat_id: this.getChatId(),
207
- message_thread_id: this.getThreadId(),
208
- business_connection_id: this.update.business_message?.business_connection_id?.toString() ?? '',
209
- photo,
210
- caption,
211
- });
212
- case 'guest_message':
213
- return await this.answerGuestQueryPhoto(photo, caption);
214
- case 'inline':
215
- return await this.api.answerInline(this.bot.api.toString(), {
216
- inline_query_id: this.update.inline_query?.id.toString() ?? '',
217
- results: [{ type: 'photo', id: crypto.randomUUID(), photo_url: photo, thumbnail_url: photo }],
218
- });
219
- default:
220
- return null;
295
+ const params = {
296
+ ...options,
297
+ chat_id: this.getChatId(),
298
+ message_thread_id: this.getThreadId(),
299
+ reply_to_message_id: this.getMessageId(),
300
+ photo,
301
+ caption,
302
+ };
303
+ if (this.update_type === 'business_message') {
304
+ params['business_connection_id'] = this.update.business_message?.business_connection_id;
305
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendPhoto(api, data));
306
+ }
307
+ if (this.update_type === 'guest_message') {
308
+ return await this.answerGuestQueryPhoto(photo, caption);
221
309
  }
310
+ if (this.update_type === 'inline') {
311
+ return await this.api.answerInline(this.bot.api.toString(), {
312
+ inline_query_id: this.update.inline_query?.id.toString() ?? '',
313
+ results: [{ type: 'photo', id: crypto.randomUUID(), photo_url: photo, thumbnail_url: photo }],
314
+ });
315
+ }
316
+ return await this.api.sendPhoto(this.bot.api.toString(), params);
222
317
  }
223
318
  /**
224
319
  * Reply to the last message with a voice message
@@ -228,57 +323,41 @@ export default class TelegramExecutionContext {
228
323
  * @returns Promise with the API response
229
324
  */
230
325
  async replyVoice(voice, caption = '', options = {}) {
231
- switch (this.update_type) {
232
- case 'voice':
233
- case 'message':
234
- return await this.api.sendVoice(this.bot.api.toString(), {
235
- ...options,
236
- chat_id: this.getChatId(),
237
- message_thread_id: this.getThreadId(),
238
- reply_to_message_id: this.getMessageId(),
239
- voice,
240
- caption,
241
- });
242
- case 'business_message':
243
- return await this.api.sendVoice(this.bot.api.toString(), {
244
- ...options,
245
- chat_id: this.getChatId(),
246
- message_thread_id: this.getThreadId(),
247
- business_connection_id: this.update.business_message?.business_connection_id?.toString() ?? '',
248
- voice,
249
- caption,
250
- });
251
- case 'guest_message':
252
- return await this.answerGuestQueryVoice(voice, caption);
253
- default:
254
- return null;
326
+ const params = {
327
+ ...options,
328
+ chat_id: this.getChatId(),
329
+ message_thread_id: this.getThreadId(),
330
+ reply_to_message_id: this.getMessageId(),
331
+ voice,
332
+ caption,
333
+ };
334
+ if (this.update_type === 'business_message') {
335
+ params['business_connection_id'] = this.update.business_message?.business_connection_id;
336
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendVoice(api, data));
337
+ }
338
+ if (this.update_type === 'guest_message') {
339
+ return await this.answerGuestQueryVoice(voice, caption);
255
340
  }
341
+ return await this.api.sendVoice(this.bot.api.toString(), params);
256
342
  }
257
343
  /**
258
344
  * Send typing in a chat
259
345
  * @returns Promise with the API response
260
346
  */
261
347
  async sendTyping() {
262
- switch (this.update_type) {
263
- case 'voice':
264
- case 'message':
265
- case 'photo':
266
- case 'document':
267
- return await this.api.sendChatAction(this.bot.api.toString(), {
268
- chat_id: this.getChatId(),
269
- message_thread_id: this.getThreadId(),
270
- action: 'typing',
271
- });
272
- case 'business_message':
273
- return await this.api.sendChatAction(this.bot.api.toString(), {
274
- business_connection_id: this.update.business_message?.business_connection_id?.toString() ?? '',
275
- chat_id: this.getChatId(),
276
- message_thread_id: this.getThreadId(),
277
- action: 'typing',
278
- });
279
- default:
280
- return null;
348
+ if (this.update_type === 'guest_message') {
349
+ return null;
350
+ }
351
+ const params = {
352
+ chat_id: this.getChatId(),
353
+ message_thread_id: this.getThreadId(),
354
+ action: 'typing',
355
+ };
356
+ if (this.update_type === 'business_message') {
357
+ params['business_connection_id'] = this.update.business_message?.business_connection_id;
358
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendChatAction(api, data));
281
359
  }
360
+ return await this.api.sendChatAction(this.bot.api.toString(), params);
282
361
  }
283
362
  /**
284
363
  * Reply to an inline message with a title and content
@@ -356,104 +435,56 @@ export default class TelegramExecutionContext {
356
435
  * @param options - any additional options to pass to sendMessage/editMessageText
357
436
  * @returns Promise with the API response
358
437
  */
359
- async streamReply(message, draft_id, parse_mode = '', options = {}) {
360
- const message_id = this.drafts.get(draft_id);
361
- const business_connection_id = this.update.business_message?.business_connection_id?.toString();
362
- if (message_id) {
363
- return await this.api.editMessageText(this.bot.api.toString(), {
364
- chat_id: this.getChatId(),
365
- message_id,
366
- text: message,
367
- parse_mode,
368
- business_connection_id,
369
- ...options,
370
- });
371
- }
438
+ async streamReply(message, draft_id, parse_mode = '', options = {}, finish = false) {
372
439
  if (this.update_type === 'guest_message') {
373
- if (this.drafts.has(draft_id)) {
374
- return new Response('Query already answered', { status: 200 });
440
+ if (finish) {
441
+ return await this.answerGuestQueryText(message, parse_mode);
375
442
  }
376
- this.drafts.set(draft_id, -1);
377
- return await this.answerGuestQueryText(message, parse_mode);
443
+ return null;
378
444
  }
379
- const response = await this.api.sendMessage(this.bot.api.toString(), {
445
+ if (finish) {
446
+ return await this.reply(message, parse_mode, true, options);
447
+ }
448
+ const params = {
380
449
  ...options,
381
450
  chat_id: this.getChatId(),
382
451
  message_thread_id: this.getThreadId(),
383
452
  text: message,
384
453
  parse_mode,
385
- business_connection_id,
386
- });
387
- if (response.status === 200) {
388
- const cloned = response.clone();
389
- try {
390
- const json = (await cloned.json());
391
- if (json.ok && json.result?.message_id) {
392
- this.drafts.set(draft_id, json.result.message_id);
393
- }
394
- }
395
- catch {
396
- // ignore
397
- }
454
+ draft_id,
455
+ };
456
+ if (this.update_type === 'business_message') {
457
+ params.business_connection_id = this.update.business_message?.business_connection_id;
398
458
  }
399
- return response;
459
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendMessageDraft(api, data));
400
460
  }
401
- /**
402
- * Reply to the last message with text
403
- * @param message - text to reply with
404
- * @param parse_mode - one of HTML, MarkdownV2, Markdown, or an empty string for ascii
405
- * @param options - any additional options to pass to sendMessage
406
- * @returns Promise with the API response
407
- */
408
461
  async reply(message, parse_mode = '', reply = true, options = {}) {
409
- switch (this.update_type) {
410
- case 'voice':
411
- case 'message':
412
- case 'photo':
413
- case 'document':
414
- if (reply) {
415
- return await this.api.sendMessage(this.bot.api.toString(), {
416
- ...options,
417
- chat_id: this.getChatId(),
418
- message_thread_id: this.getThreadId(),
419
- reply_to_message_id: this.getMessageId(),
420
- text: message,
421
- parse_mode,
422
- });
423
- }
424
- return await this.api.sendMessage(this.bot.api.toString(), {
425
- ...options,
426
- chat_id: this.getChatId(),
427
- message_thread_id: this.getThreadId(),
428
- text: message,
429
- parse_mode,
430
- });
431
- case 'guest_message':
432
- return await this.answerGuestQueryText(message, parse_mode);
433
- case 'business_message':
434
- return await this.api.sendMessage(this.bot.api.toString(), {
435
- chat_id: this.getChatId(),
436
- message_thread_id: this.getThreadId(),
437
- text: message,
438
- business_connection_id: this.update.business_message?.business_connection_id?.toString() ?? '',
439
- parse_mode,
440
- });
441
- case 'callback':
442
- if (this.update.callback_query?.message?.chat.id) {
443
- return await this.api.sendMessage(this.bot.api.toString(), {
444
- ...options,
445
- chat_id: this.update.callback_query.message.chat.id.toString(),
446
- message_thread_id: this.getThreadId(),
447
- text: message,
448
- parse_mode,
449
- });
450
- }
451
- return null;
452
- case 'inline':
453
- return await this.replyInline('Response', message, parse_mode);
454
- default:
455
- return null;
462
+ if (this.update_type === 'guest_message') {
463
+ return await this.answerGuestQueryText(message, parse_mode);
464
+ }
465
+ if (this.update_type === 'inline') {
466
+ return await this.replyInline('Response', message, parse_mode);
456
467
  }
468
+ const params = {
469
+ ...options,
470
+ chat_id: this.getChatId(),
471
+ message_thread_id: this.getThreadId(),
472
+ text: message,
473
+ parse_mode,
474
+ };
475
+ if (reply) {
476
+ params.reply_to_message_id = this.getMessageId();
477
+ }
478
+ if (this.update_type === 'business_message') {
479
+ params['business_connection_id'] = this.update.business_message?.business_connection_id;
480
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendMessage(api, data));
481
+ }
482
+ if (this.update_type === 'callback') {
483
+ if (this.update.callback_query?.message?.chat.id) {
484
+ params['chat_id'] = this.update.callback_query.message.chat.id.toString();
485
+ }
486
+ }
487
+ return await this.api.sendMessage(this.bot.api.toString(), params);
457
488
  }
458
489
  /**
459
490
  * Send an invoice for Telegram Stars
@@ -464,17 +495,21 @@ export default class TelegramExecutionContext {
464
495
  * @returns Promise with the API response
465
496
  */
466
497
  async sendStarsInvoice(title, description, payload, amount) {
467
- return await this.api.sendInvoice(this.bot.api.toString(), {
498
+ const params = {
468
499
  chat_id: this.getChatId(),
469
500
  message_thread_id: this.getThreadId(),
470
- business_connection_id: this.update.business_message?.business_connection_id?.toString(),
471
501
  title,
472
502
  description,
473
503
  payload,
474
504
  provider_token: '',
475
505
  currency: 'XTR',
476
506
  prices: [{ label: title, amount }],
477
- });
507
+ };
508
+ if (this.update_type === 'business_message') {
509
+ params['business_connection_id'] = this.update.business_message?.business_connection_id;
510
+ return await this.withBusinessFallback(params, (api, data) => this.api.sendInvoice(api, data));
511
+ }
512
+ return await this.api.sendInvoice(this.bot.api.toString(), params);
478
513
  }
479
514
  /**
480
515
  * Answer a pre-checkout query
package/dist/utils.js CHANGED
@@ -27,16 +27,17 @@ export async function markdownToHtml(s) {
27
27
  return result;
28
28
  };
29
29
  renderer.listitem = (item) => {
30
- return renderer.parser.parseInline(item.tokens);
30
+ return renderer.parser.parse(item.tokens).trim();
31
31
  };
32
32
  renderer.strong = ({ tokens }) => `<b>${renderer.parser.parseInline(tokens)}</b>`;
33
33
  renderer.em = ({ tokens }) => `<i>${renderer.parser.parseInline(tokens)}</i>`;
34
34
  renderer.codespan = ({ text }) => `<code>${text}</code>`;
35
35
  renderer.code = ({ text, lang }) => {
36
+ const escapedText = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
36
37
  if (lang) {
37
- return `<pre><code class="language-${lang}">${text}</code></pre>\n`;
38
+ return `<pre><code class="language-${lang}">${escapedText}</code></pre>\n`;
38
39
  }
39
- return `<pre><code>${text}</code></pre>\n`;
40
+ return `<pre><code>${escapedText}</code></pre>\n`;
40
41
  };
41
42
  renderer.del = ({ tokens }) => `<s>${renderer.parser.parseInline(tokens)}</s>`;
42
43
  renderer.link = ({ href, tokens }) => `<a href="${href}">${renderer.parser.parseInline(tokens)}</a>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebam/cf-workers-telegram-bot",
3
- "version": "11.18.0",
3
+ "version": "12.1.0",
4
4
  "description": "serverless telegram bot on cf workers",
5
5
  "main": "./dist/main.js",
6
6
  "module": "./dist/main.js",
@@ -32,7 +32,7 @@
32
32
  "license": "Apache-2.0",
33
33
  "repository": {
34
34
  "type": "git",
35
- "url": "https://github.com/codebam/cf-workers-telegram-bot.git"
35
+ "url": "git+https://github.com/codebam/cf-workers-telegram-bot.git"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@cloudflare/workers-types": "^4.20260511.1",