Building a simple event notifier via Telegram with NodeJS

Do you ever discover a new service or feature and decide you need updates when some things happen but the app has no notifications?

In this tutorial I’ll try to make a simple but reusable component get data and notify via Telegram when certain conditions we are watching are met.

Requirements

  • NodeJS

Setting up the project

Creating a Telegram Bot

There is more than one way to send yourself a message on Telegram, but the easiest one I think is via a Telegram Bot. Since you create a bot, it will have it’s own chat and this keeps your inbox nice and tidy. Now the actual process of creating a bot and getting it’s id is a bit weird and it includes fittingly talking to bots, so you need to do this on Telegram.

  1. Search in Telegram for @botfather and open a chat with it

Setting up Node

We’ll start by creating a directory, and running “npm init” on it, then we’ll create an entry point for our application, I’ll name mine main.js for simplicity, but you can use index.js or anything that suits you. I’ll also create a “services” directory to store the services we’ll be crawling.

For storing our token and chat ID I recommend installing dotenv and using a .env file.

npm install dotenv

We’ll create a .env file in the root of our project and add our variables.

BOT_TOKEN=x
CHAT_ID=x

That’s it for the setup, next we’ll start coding.

Sending our first Notification

Creating our Notifier Library

Our notifier library is very simple, we only notify via Telegram and the process of notifying is a simple GET Request. The URL format for the request is the following:

https://api.telegram.org/bot{botToken}/sendMessage?chat_id={chatId}&text={message}

For making our request asynchronously we’ll use the Axios library which is quite lightweight yet powerful.

npm install axios

Now let’s create a notifier.js file. We’ll use the values provided in our .env file which will be available under process.env. We’ll require the End Of Line Constant from the os library and axios for our request.

const {EOL} = require('os');
const axios = require('axios');
const TELEGRAM_URL = 'https://api.telegram.org/bot' + process.env.BOT_TOKEN + '/sendMessage?chat_id=' + process.env.CHAT_ID + '&text=';
module.exports = {
notify: async function(title, body) {
axios.get(TELEGRAM_URL + encodeURIComponent(title + EOL + body))
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
}
}

Two things to notice here, we marked our method as async, since we don’t want to wait to continue execution, and we encoded the message as a URI component.

Next we’ll want to test this simple script, we’ll jump to our entrypoint file and start coding. First in order to properly load our .env file, we need to require the “dotenv” library and run it’s “config” method. Then we’ll simply require our notifier library and call it’s notify method. Easy, peasy.

main.js

require('dotenv').config()
const Notifier = require('./notifier');
Notifier.notify('Hello World', 'This is my first notification using NodeJS');

Now let’s run our script:

node run main.js

Success! We got a notification on Telegram from our bot. Now we need to actually pull data from somewhere and add conditions to when to notify, but since we want a reusable component for various services, we’ll have have to start with…

Creating our service base class

Remember I created a “services” directory? Here we’ll store our services that will crawl for data, but first we need a common interface between these services so that we can manage them. We’ll define a BaseService class that all our services will implement.

For starters let’s get basic, we only need three things: a name, a schedule time for it to run and a method for when it runs.

base-service.js

module.exports = class BaseService {constructor(name, schedule) {
if (new.target === BaseService) {
throw new TypeError("Cannot construct BaseService instances directly");
}
this.name = name;
this.schedule = schedule;
}
async run() {
throw new Error('Service method "run" not implemented');
}
}

Notice that we are making it so our class cannot be instantiated and children must implement a “run” method. We also made the method async so they don’t slow down a single thread.

We’ll also create a list of statuses for our services:

service-status.js

const ServiceStatus = Object.freeze({
QUEUED: 'QUEUED',
RUNNING: 'RUNNING',
STOPPED: 'STOPPED'
});
module.exports = ServiceStatus;

For all these services we’ll need a manager that queues them and makes sure they are running, so we’ll create a ServiceManager module as well. This manager will be responsable for running the services, managing their statuses and handling errors. For this simple example we’ll use intervals for running the services.

service-manager.js

const Notifier = require('../notifier');
const ServiceStatus = require('./service-status');
const manager = {add: function(service) {
service.status = ServiceStatus.QUEUED;
this.services.push(service);
},
start: async function(service = null) {
for (const service of this.services) {
this.run(service);
service.interval = setInterval(() => this.run(service), service.schedule);
}
},
stop: function(service) {
clearInterval(service.interval);
service.status = ServiceStatus.STOPPED;
},
restart: function(service) {
this.stop(service);
this.start(service);
},
run: async function(service) {
try {
service.run();
service.status = ServiceStatus.RUNNING;
}
catch (err) {
console.log('[ERROR] ' + service.name + ': ' + err);
this.stop(service);
Notifier.notify(service.name + ' Error', 'Service failed');
}
}
};manager.services = [];module.exports = manager;

Let’s recap on what our manager does. We have and “add” method to add to our list of services; “start”, “stop” and “restart” methods for our servers and a run method that runs each service. We‘re also including our notifier and using it to notify us in case one of our services crashes. You may also have noted I’m using two new properties for the services: status and interval to store the interval id. For readability, go back and include them in the BaseService class constructor.

Since we have our BaseService and our ServiceManager it’s time to actually implement services.

First Service

For the first example I’ll use something simple. I want to be notified via Telegram when my Reddit Inbox has new messages from DMs. We’ll call this our “RedditInboxService”. Since we already have our ServiceManager scheduling and running the jobs and our BaseService class, what we need is to extend our BaseService class and implement our run method to crawl the inbox, check if there are results and notify accordingly.

Reddit uses OAuth, you’ll need to head over to Reddit Dev to get yourself set up for that (remember to save the token in your .env file) but afterwards what we need to do is pretty easy: Crawl Reddit’s Inbox endpoint (https://www.reddit.com/message/unread.json), check the results to see if there are new messages, if there are check the type to see if it is a DM and then notify via Telegram. To keep things short, the response has a simple format: a “data” property with an object, this object contains a “children” property, an array containing all new messages. These messages may be Post Replies, Comment Replies or DMs, this is indicated by their “kind” property, DMs are of kind “t4”.

reddit-inbox-service.js

const Notifier = require('../notifier');
const BaseService = require('./base-service');
const axios = require('axios');
const ENDPOINT = 'https://www.reddit.com/message/unread.json';
module.exports = class RedditInboxService extends BaseService {
async run() {
return axios.get(ENDPOINT, {
headers: {'Authorization': 'basic ' + process.env.REDDIT_TOKEN}
})
.then(response => {
if (response.data.children.length) {
for (const message of data.data.children) {
if (message.kind == 't4')
Notifier.notify(message.data.subject + '(' + message.data.author + ')', message.data.body);
}
}
})
.catch(async function (err) {
console.log(err)
});
}
}

Now let’s add the ServiceManager to our entrypoint, add our Service to it and start the Servicemanager. We’ll schedule the service to run every minute.

require('dotenv').config()
const Notifier = require('./notifier');
const ServiceManager = require('./services/service-manager');
const RedditInboxService = require('./services/reddit-inbox-service');
ServiceManager.add(new RedditInboxService('RedditInboxService', 60000));
ServiceManager.start();

Now let’s run it and cross our fingers…

Success! It scraps the data and notifies, but there’s an issue you may have notice: the notification will be sent each time the job is run, even if has already been sent. We usually don’t want the same notification each time, so we’ll have to check which notifications have already been sent. We’ll use the id property of the message for that. We’ll modify our RedditInboxService so it would look like this:

const Notifier = require('../notifier');
const BaseService = require('./base-service');
const axios = require('axios');
const ENDPOINT = 'https://www.reddit.com/message/unread.json';
module.exports = class RedditInboxService extends BaseService {constructor(name, schedule) {
super(name, schedule);
this.sentMessages = [];
}
async run() {
return axios.get(ENDPOINT, {
headers: {'Authorization': 'basic ' + process.env.REDDIT_TOKEN}
})
.then(response => {
if (response.data.children.length) {
for (const message of data.data.children) {
if (message.kind == 't4' && !this.sentMessages.includes(message.id)) {
Notifier.notify(message.data.subject + '(' + message.data.author + ')', message.data.body);
this.sentMessages.push(message.id);
}
}
}
})
.catch(async function (err) {
console.log(err)
});
}
}

Et voilà, now you will only get a notification once. Now since we made the components very reusable it’s very easy to add more services crawlers if you like. You can get notifications for other services inboxes, cryptocurrency prices, anything you like.

Software Developer at Avantica