diff --git a/README.md b/README.md index 73b15ac..e9a5590 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,62 @@ bot.on('message:audio', (message) => { }); ``` +## Webhooks +To use webhooks, you need to set up a server that will receive updates from Telegram. You can use the [express](https://www.npmjs.com/package/express) library for this purpose. + +This example demonstrates a basic Telegram bot that responds with a reaction to any incoming message using Webhooks. The use of [ngrok](https://www.npmjs.com/package/ngrok) as a tunneling service simplifies the setup, allowing the bot to be easily deployed in various environments without complex network configuration. This approach is ideal for quick testing and development purposes. For production use, you should consider deploying the bot on a server with a public IP address and a valid SSL certificate. +```typescript +import 'dotenv/config'; +import * as ngrok from 'ngrok'; +import express from "express"; +import {TelegramBot} from "./src"; +import {Update} from "./src/types"; + +const port = 3001; + +const bot = new TelegramBot({ + botToken: process.env.TEST_TELEGRAM_TOKEN as string, +}); + +bot.on('message', async (message) => { + await bot.setMessageReaction({ + chat_id: message.chat.id, + message_id: message.message_id, + reaction: [{ + type: 'emoji', emoji: '👍' + }] + }); +}); + +const app = express(); +app.use(express.json()); +app.post('/', async (req, res) => { + try { + await bot.processUpdate(req.body as Update); + res.sendStatus(200); + } catch (e) { + res.sendStatus(500); + } +}); + +(async () => { + app.listen(port, async () => { + const url = await ngrok.connect({ + proto: 'http', + addr: port, + }); + await bot.setWebhook({url}); + console.log('Set Webhook to', url); + }) +})(); + +process.on('SIGINT', async () => { + await bot.deleteWebhook(); + await ngrok.disconnect(); + console.log('Webhook deleted'); +}); +``` + ## Tests ```bash diff --git a/package.json b/package.json index 68360b7..6a481ef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "commonjs", "name": "typescript-telegram-bot-api", - "version": "0.1.20", + "version": "0.1.21", "description": "Telegram Bot API wrapper for Node.js written in TypeScript", "repository": "github:Borodin/typescript-telegram-bot-api", "main": "dist/index.js", diff --git a/src/index.ts b/src/index.ts index 34c785e..f8c29bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,6 +59,7 @@ import { MessageTypes, StarTransactions, InputPaidMedia, + WebhookInfo, } from './types/'; import * as TelegramTypes from './types/'; import { TelegramError } from './errors'; @@ -186,6 +187,10 @@ export class TelegramBot extends EventEmitter { } } + async processUpdate(update: Update) { + this.polling.emitUpdate(update); + } + /** * ## getUpdates * Use this method to receive incoming updates using long polling [wiki](https://en.wikipedia.org/wiki/Push_technology#Long_polling). Returns an Array of Update objects. @@ -210,6 +215,53 @@ export class TelegramBot extends EventEmitter { ); } + /** + * ## setWebhook + * Use this method to specify a URL and receive incoming updates via an outgoing webhook. Whenever there is an update + * for the bot, we will send an HTTPS POST request to the specified URL, containing a JSON-serialized Update. In case + * of an unsuccessful request, we will give up after a reasonable amount of attempts. Returns True on success. + * + * If you'd like to make sure that the webhook was set by you, you can specify secret data in the parameter + * ```secret_token```. If specified, the request will contain a header ```“X-Telegram-Bot-Api-Secret-Token”``` with + * the secret token as content. + * @see https://core.telegram.org/bots/api#setwebhook + */ + async setWebhook(options: { + url: string; + certificate?: InputFile; + ip_address?: string; + max_connections?: number; + allowed_updates?: UpdateType[]; + drop_pending_updates?: boolean; + secret_token?: string; + }): Promise { + return await this.callApi('setWebhook', { + ...options, + allowed_updates: JSON.stringify(options?.allowed_updates), + }); + } + + /** + * ## deleteWebhook + * Use this method to remove webhook integration if you decide to switch back to getUpdates. Returns True on success. + * @see https://core.telegram.org/bots/api#deletewebhook + */ + async deleteWebhook(options?: { + drop_pending_updates?: boolean; + }): Promise { + return await this.callApi('deleteWebhook', options); + } + + /** + * ## getWebhookInfo + * Use this method to get current webhook status. Requires no parameters. On success, returns a WebhookInfo object. + * If the bot is using getUpdates, will return an object with the url field empty. + * @see https://core.telegram.org/bots/api#getwebhookinfo + */ + async getWebhookInfo(): Promise { + return await this.callApi('getWebhookInfo'); + } + /** * ## getMe * A simple method for testing your bot's authentication token. Requires no parameters. Returns basic information about the bot in form of a User object. diff --git a/src/pooling.ts b/src/pooling.ts index a7a1510..e655ea2 100644 --- a/src/pooling.ts +++ b/src/pooling.ts @@ -25,7 +25,7 @@ export class Polling { }); } - private emitUpdate(update: Update) { + emitUpdate(update: Update) { Object.keys(update).forEach((key) => { if (key !== 'update_id') { const eventType = key as keyof EventTypes; diff --git a/src/types/WebhookInfo.ts b/src/types/WebhookInfo.ts new file mode 100644 index 0000000..4825f62 --- /dev/null +++ b/src/types/WebhookInfo.ts @@ -0,0 +1,52 @@ +/** + * ## getWebhookInfo + * Use this method to get current webhook status. Requires no parameters. On success, returns a WebhookInfo object. If the bot is using getUpdates, will return an object with the url field empty. + * @see https://core.telegram.org/bots/api#getwebhookinfo + */ + +export type WebhookInfo = { + /** + * Webhook URL, may be empty if webhook is not set up + */ + url: string; + + /** + * True, if a custom certificate was provided for webhook certificate checks + */ + has_custom_certificate: boolean; + + /** + * Number of updates awaiting delivery + */ + pending_update_count: number; + + /** + * Optional. Currently used webhook IP address + */ + ip_address?: string; + + /** + * Optional. Unix time for the most recent error that happened when trying to deliver an update via webhook + */ + last_error_date?: number; + + /** + * Optional. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook + */ + last_error_message?: string; + + /** + * Optional. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters + */ + last_synchronization_error_date?: number; + + /** + * Optional. The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery + */ + max_connections?: string; + + /** + * Optional. A list of update types the bot is subscribed to. Defaults to all update types except chat_member + */ + allowed_updates?: string[]; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 7c7fbdf..eceb9a6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -214,6 +214,7 @@ export * from './VideoNote'; export * from './Voice'; export * from './WebAppData'; export * from './WebAppInfo'; +export * from './WebhookInfo'; export * from './WriteAccessAllowed'; import { ResponseParameters } from './ResponseParameters'; diff --git a/tests/index.test.ts b/tests/index.test.ts index ddd6ff6..d00f31f 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -74,6 +74,46 @@ describe('.stopPolling()', () => { }); }); +describe('.setWebhook()', () => { + it('should set webhook', async () => { + await expect( + bot.setWebhook({ + url: 'https://example.com', + }), + ).resolves.toBe(true); + }); + + afterAll(async () => { + await bot.deleteWebhook(); + }); +}); + +describe('.deleteWebhook()', () => { + it('should delete webhook', async () => { + await expect(bot.deleteWebhook()).resolves.toBe(true); + }); +}); + +describe('.getWebhookInfo()', () => { + it('should get webhook info', async () => { + await bot.setWebhook({ + url: 'https://example.com', + }); + await expect(bot.getWebhookInfo()).resolves.toHaveProperty( + 'url', + 'https://example.com', + ); + }); + + it('should return empty url when webhook is not set', async () => { + await expect(bot.getWebhookInfo()).resolves.toHaveProperty('url', ''); + }); + + afterEach(async () => { + await bot.deleteWebhook(); + }); +}); + describe('.isTelegramError()', () => { it('should return true for Telegram-related errors', () => { const telegramError = new TelegramError({