Send notifications to a Telegram bot for events in an AWS account

How to add a Telegram target for an SNS topic

Author's image
Tamás Sallai
4 mins

Event-driven Telegram bot

In the previous article we looked into how to get started with Telegram bot development. The bot that we implemented there was reactive: it responded to messages sent by a user, but it could not send messages later.

In this article, we'll create a bot that sends messages when an event happens in an AWS account. This is useful as a monitoring tool, for example, a CloudWatch Alarm can trigger the bot and send a message. It can also be integrated with EventBridge, which is a powerful tool to monitor what is happening in the account. For example, the bot can send a message when an EC2 instance is stopped.

Why it's better than SMS?

First, SMS costs money and its setup is increasingly complicated due to regulation. But the main benefit of a bot is two-way communication. Not only you can get an event that an EC2 instance is stopped, but you could also fetch its logs, or even restart it. While all these require writing code, the possibilities are there to make it more than a dumb notification service.

Let's jump in to the implementation!

Managing subscriptions

The first task is to manage subscriptions. Since multiple people can use a Telegram bot and it needs the chat IDs to send messages, we need a way for users to subscribe and unsubscribe. This also requires a database to store the current subscriptions, for which DynamoDB is a great choice.

To manage a subscription, three functions are needed, one to start, one to stop, and one to query the current status:

const stop = async (chat_id) => {
	await client.send(new DeleteItemCommand({
		TableName: subscribers_table,
		Key: {chat_id: {S: String(chat_id)}},
	}));
};
const start = async (chat_id) => {
	await client.send(new PutItemCommand({
		TableName: subscribers_table,
		Item: {
			chat_id: {S: String(chat_id)},
		},
	}));
};
const isListening = async (chat_id) => {
	const res = await client.send(new GetItemCommand({
		TableName: subscribers_table,
		Key: {chat_id: {S: String(chat_id)}},
	}));
	return res.Item !== undefined;
};

With these, the bot needs to process control messages sent to it:

if (update.message) {
	const {message: {chat: {id: chat_id}, text}} = update;
	if (text === "/start") {
		await start(chat_id);
		await sendTelegramCommand("sendMessage", {
			chat_id,
			text: "Subscribed to updates",
		});
	}
	if (text === "/stop") {
		await stop(chat_id);
		await sendTelegramCommand("sendMessage", {
			chat_id,
			text: "Updates stopped",
		});
	}
	if (text === "/status") {
		const status = await isListening(chat_id);
		await sendTelegramCommand("sendMessage", {
			chat_id,
			text: String(status),
		});
	}
}

This is enough for users to manage their subscription status:

And under the hood, the bot keeps track of the chat IDs in the DynamoDB table:

Error handling

In Telegram, users can block bots, in which case it should register an unsubscription. When that happens, Telegram sends an update via the Webhook, and the bot can stop the notifications for that chat:

if (update.my_chat_member) {
	if (update.my_chat_member.new_chat_member.status === "kicked") {
		await stop(update.my_chat_member.chat.id);
	}
}

Publishing messages

Now that we have a list of subscribed chats, let's see how a Lambda function can send messages to all of them!

In this example we'll process SNS messages, so there are Records in the event and a message is in the Sns.Message field in the record:

import {sendTelegramCommand} from "./telegram-control.js";
import {DynamoDBClient, paginateScan} from "@aws-sdk/client-dynamodb";

export const handler = async (event) => {
	const client = new DynamoDBClient();
	const {subscribers_table} = process.env;

	for await (const page of paginateScan({client}, {TableName: subscribers_table})) {
		// for each page of chats
		await Promise.all(page.Items.map(async ({chat_id: {S: chat_id}}) => {
			// for each subscriber
			await Promise.all(event.Records.map((async (record) => {
				// for each record
				await sendTelegramCommand("sendMessage", {
					chat_id,
					text: record.Sns.Message,
				});
			})));
		}));
	}
};

Then subscribe the Lambda function to the SNS topic:

Testing

Now that the whole flow is set up, let's publish a message to the SNS topic:

On all subscribed Telegram chats, the message is sent by the bot:

CloudWatch alarms

A natural next step for a notification service is to send alarms from CloudWatch as these track the urgent events in an account. Since they support SNS as a target for a state change, it's easy to set up:

Then when the alarm's state is changed, the Telegram bot sends a message:

Conclusion

Telegram bots can track subscribers and they can send messages when an external event happens. This makes a bot suitable to send a notification for important and urgent events.

March 1, 2022
In this article