How To Create A Telegram Bot From Scratch ( Tutorial )

Telegram Bot From Scratch

How To Create A Telegram Bot From Scratch (Tutorial)

Hello everyone! We all know that this type of topic with the creation of Telegram Bot has been discussed a lot of times all over the internet.

And if you follow our post’s then you might know that we have created a couple of our own Telegram Bots previously and have shown you in some or other way how you can achieve this just by using proper tools.

In this tutorial, we are going to show you how you can create your own Telegram Chat Bot from the scratch by using BotFather and Heroku.

Post Content

  1. BotFather.
  2. Installing and configuring pipenv. First start.
  3. Handlers. The response to commands and messages.
  4. Basics of interaction. The answer to the commands.
  5. Basics of interaction. Reply to text messages.
  6. Basics of interaction. Answer pictures, documents, audio, and others.
  7. Building a chain of answers.
  8. Add the parser to the chain.
  9. Theory. Methods of interaction with the bot.
  10. Markups. Adding keyboards for the quick response.
  11. We have a bot on Heroku.

To begin with, it is necessary to decide what our bot will do. We decided to write a banal simple bot, which will parse and give us the headlines from the SmartSpate

And so, let’s start.

BotFather

First, we need to register our bot in Telegram.

  • In the search, we type in a @BotFather and go into a dialogue with the Father of Bots.
  • Then, write/newbot. Specify the name of the bot (what is displayed in the dialogs). We indicate its login, by which it can be found.

It should end with Bot/botSmartSpate

  • After that, you have to get an API key and a link to the bot. It is advisable to save the API key and go into a dialog with the bot so that you do not have to dig into the correspondence with BotFather
  • Next, we add a couple of commands to it: assign /setcommands and one message, because /setcommands does not add a command, but sets them from scratch, send commands to it.

all – sparse headers from the tab “Smart Topics”
top – sparse headers from the “Web Developer Topics”

On this work with BotFather is over, let’s move on to the next part.

Installing and configuring pipenv. First start.

First, create a file, which will be the bot ‘s main bot.py code. If the bot is large, then immediately create files where you will put functions, classes, etc., otherwise, the code readability tends to zero. We will add parser.py

Install pipenv, if it is not already available.

For Windows:


pip install pipenv

For Linux:


sudo pip3 install pipenv

Install pipenv in the project folder:


pipenv install

Install the libraries. We will work with PyTelegramBotAPI. But, so that it works properly on Heroku servers, we specify version 2.2.3. Also for parsing, we add BeautifulSoup4.


pipenv install PyTelegramBotAPI==2.2.3
pipenv install beautifulsoup4

Starting writing code!

We open bot.py, import libraries and create the main variables.


import telebot
import parser
#main variables
TOKEN = "555555555:AAAAaaaAaaA1a1aA1AAAAAAaaAAaa4AA"
bot = telebot.TeleBot(TOKEN)

Run the bot and check for errors.

For Windows:


python bot.py

For Linux:


python3 bot.py

If there are no errors, we will continue.

Handlers. We respond to commands and messages

It’s time to teach the bot to answer us. It is even possible to make his answers useful.

Basics of interaction. Response to commands

To interact with the user, i.e. Handlers are used to respond to its commands and messages.

Let’s start with the simplest: answer the /start and /go commands


@ bot.message_handler (commands = ['start', 'go'])
def start_handler (message):
bot.send_message (message.chat.id, 'Hello, when I grow up, I'll parse the headers with the Hub')
bot.polling ()

Now let’s see what it is and how it works. We pass in the message_handler the command parameter equal to the array with strings – the commands to which it will respond in the manner described below. (He will respond to all these commands in the same way). Next, we use send_message, write to it the chat id (you can get it from message.chat.id), in which to send the message and, in fact, the message itself. We can not forget to write bot.polling () at the end of the code, otherwise, the bot will immediately shut down.

Now you can start the bot and write to it /start or /go and it will respond.

It should end with Bot/botSmartSpate

This is a json object that stores information about the sender, chat, and the message itself.


{
 'content_type': 'text',
 'message_id': 5,
 'from_user':
 {
 'id': 333960329,
 'first_name': 'Nybkox',
 'username': 'nybkox',
 'last_name': None
 },
 'date': 1520186598,
 'chat':
 {
 'type': 'private',
 'last_name': None,
 'first_name': 'Nybkox',
 'username': 'nybkox',
 'id': 333960329,
 'title': None,
 'all_members_are_administrators': None
 },
 'forward_from_chat': None,
 'forward_from': None,
 'forward_date': None,
 'reply_to_message': None,
 'edit_date': None,
 'text': '/start',
 'entities': [<telebot.types.MessageEntity object at 0x7f3061f42710>],
 'audio': None,
 'document': None,
 'photo': None,
 'sticker': None,
 'video': None,
 'voice': None,
 'caption': None,
 'contact': None,
 'location': None,
 'venue': None,
 'new_chat_member': None,
 'left_chat_member': None,
 'new_chat_title': None,
 'new_chat_photo': None,
 'delete_chat_photo': None,
 'group_chat_created': None,
 'supergroup_chat_created': None,
 'channel_chat_created': None,
 'migrate_to_chat_id': None,
 'migrate_from_chat_id': None,
 'pinned_message': None
}

Basics of interaction. Reply to text messages

Now we will process the text messages of the bot. The most important thing we need to know is that the message text is stored in the message.text and that to process the text in the message_handler it is necessary to pass the content_types = [‘text’].

Let’s add this code.


@ bot.message_handler (content_types = ['text'])
def text_handler (message):
text = message.text.lower ()
chat_id = message.chat.id
if text == "hello":
bot.send_message (chat_id, 'Hello, I'm a bot-a huber parser.')
elif text == "how are you?":
bot.send_message (chat_id, 'Ok, and you?')
else:
bot.send_message (chat_id, 'Sorry, I did not understand :(')

Here we added a couple of variables: the text of the message was brought out (in the lower case, so that there were no unnecessary problems with those who wrote the caps, fence, etc.) into the variable text, brought message.chat.id into a separate variable, so that each time does not refer to message. We also built a small branch to answer certain messages, as well as an answer to the case of an incomprehensible bot message.

Final code


import bs4
import parser

#main variables
TOKEN = "555555555: AAAAaaaAaaA1a1aA1AAAAAAaaAAaa4AA"
bot = telebot.TeleBot (TOKEN)

#handlers
@ bot.message_handler (commands = ['start', 'go'])
def start_handler (message):
bot.send_message (message.chat.id, 'Hello, when I grow up, I'll parse headers with a hub')

@ bot.message_handler (content_types = ['text'])
def text_handler (message):
text = message.text.lower ()
chat_id = message.chat.id
if text == "hello":
bot.send_message (chat_id, 'Hello, I'm a bot-a huber parser.')
elif text == "how are you?":
bot.send_message (chat_id, 'Ok, and you?')
else:
bot.send_message (chat_id, 'Sorry, I did not understand you :(')

bot.polling ()

Basics of interaction. Answer pictures, documents, audio, and others

To answer pictures, stickers, documents, audio, etc. you just need to change the content_types = [‘text’].

Consider an example with a picture by adding this code.


@ bot.message_handler (content_types = ['photo'])
def text_handler (message):
chat_id = message.chat.id
bot.send_message (chat_id, 'Beautiful.')

All types of content:


text, audio, document, photo, sticker, video, video_note, voice, location, contact, new_chat_members, left_chat_member, new_chat_title, new_chat_photo, delete_chat_photo, group_chat_created, supergroup_chat_created, channel_chat_created, migrate_to_chat_id, migrate_from_chat_id, pinned_message

Building a chain of answers

It’s time to finish with elementary actions and start something serious. Let’s try to build a chain of answers. For this we need register_next_step_handler (). Let’s create a simple example, on which we will understand how register_next_step_handler () works.


@ bot.message_handler (commands = ['start', 'go'])
def start_handler (message):
chat_id = message.chat.id
text = message.text
msg = bot.send_message (chat_id, 'How old are you?')
bot.register_next_step_handler (msg, askAge)

def askAge (message):
chat_id = message.chat.id
text = message.text
if not text.isdigit ():
msg = bot.send_message (chat_id, 'Age must be a number, enter again.')
bot.register_next_step_handler (msg, askAge) #askSource
return
msg = bot.send_message (chat_id, 'Thank you, I remembered that you are' + text + 'yo.')

And so, in the first function bot.register_next_step_handler (msg, asking) was added, we send the message to it, which we want to send, and the next link to which to go after the user’s response.

In the second function, everything is more interesting, there is a check whether the user entered a number, and if not, then the function recursively calls itself, with the message “Age must be a number, enter again.”. If the user entered everything correctly, he receives a response.

  • But, there is a problem here. You can re-invoke the /go or /start command, and a mess will begin.

It is not difficult to fix this, we will add a variable to check the execution status of the script.


@ bot.message_handler (commands = ['start', 'go'])
def start_handler (message):
global isRunning
if not isRunning:
chat_id = message.chat.id
text = message.text
msg = bot.send_message (chat_id, 'How old are you?')
bot.register_next_step_handler (msg, askAge) #askSource
isRunning = True

def askAge (message):
chat_id = message.chat.id
text = message.text
if not text.isdigit ():
msg = bot.send_message (chat_id, 'Age must be a number, enter again.')
bot.register_next_step_handler (msg, askAge) #askSource
return
msg = bot.send_message (chat_id, 'Thank you, I remembered that you are' + text + 'yo.')
isRunning = False

With the construction of simple chains, we figured out, let’s go further.

Adding the parser to the chain

First, you need the parser itself. Let’s pay attention to the fact that in the tabs “Smart Topics” and “Most Viewed” there are additional filters: pages, respectively.
The parser can, of course, be written in 1 function, but we will break it into 2, so it will be easier to read the code.

Parser:


import urllib.request
from bs4 import BeautifulSoup

def getTitlesFromAll (amount, rating = 'all'):
output = ''
for i in range (1, amount + 1):
try:
if rating == 'all':
html = urllib.request.urlopen ('https://smartspate.com/smart-topics/page'+ str (i) +' / '). read ()
else:
html = urllib.request.urlopen ('https://smartspate.com/smart-topics/'+ rating +' / page '+ str (i) +' / '). read ()
except urllib.error.HTTPError:
print ('Error 404 Not Found')
break
soup = BeautifulSoup (html, 'html.parser')
title = soup.find_all ('a', class_ = 'post__title_link')
for i in title:
i = i.get_text ()
output + = ('- "' + i + '", \ n')
return output

def getTitlesFromTop (amount, age = 'daily'):
output = ''
for i in range (1, amount + 1):
try:
html = urllib.request.urlopen ('https://smartspate.com/webdeveloper-topics/'+ age +' / page '+ str (i) +' / '). read ()
except urllib.error.HTTPError:
print ('Error 404 Not Found')
break
soup = BeautifulSoup (html, 'html.parser')
title = soup.find_all ('a', class_ = 'post__title_link')
for i in title:
i = i.get_text ()
output + = ('- "' + i + '", \ n')
return output

As a result, the parser returns a row with the article headers based on our requests.
We try, using the knowledge gained, to write a bot associated with the parser. We decided to create a separate class (this is probably the wrong method, but this already applies to the python, not to the main topic of the article), and to store the data in the object of this class.

Final code:

Bot.py:


import telebot
import bs4
from Task import Task
import parser

#main variables
TOKEN = '509706011: AAF7ghlYpqS5n7uF8kN0VGDCaaHnxfZxofg'
bot = telebot.TeleBot (TOKEN)
task = Task ()

#handlers
@ bot.message_handler (commands = ['start', 'go'])
def start_handler (message):
if not task.isRunning:
chat_id = message.chat.id
msg = bot.send_message (chat_id, 'Where to parse?')
bot.register_next_step_handler (msg, askSource)
task.isRunning = True

def askSource (message):
chat_id = message.chat.id
text = message.text.lower ()
if text in task.names [0]:
task.mySource = 'top'
msg = bot.send_message (chat_id, 'For what time period?')
bot.register_next_step_handler (msg, askAge)
elif text in task.names [1]:
task.mySource = 'all'
msg = bot.send_message (chat_id, 'What is the minimum rating threshold?')
bot.register_next_step_handler (msg, askRating)
else:
msg = bot.send_message (chat_id, 'Enter the section correctly.')
bot.register_next_step_handler (msg, askSource)
return

def askAge (message):
chat_id = message.chat.id
text = message.text.lower ()
filters = task.filters [0]
if text not in filters:
msg = bot.send_message (chat_id, 'There is no such time interval. Please enter the threshold correctly.')
bot.register_next_step_handler (msg, askAge)
return
task.myFilter = task.filters_code_names [0] [filters.index (text)]
msg = bot.send_message (chat_id, 'How many pages are parsing?')
bot.register_next_step_handler (msg, askAmount)

def askRating (message):
chat_id = message.chat.id
text = message.text.lower ()
filters = task.filters [1]
if text not in filters:
msg = bot.send_message (chat_id, 'There is no such threshold, enter the threshold correctly.')
bot.register_next_step_handler (msg, askRating)
return
task.myFilter = task.filters_code_names [1] [filters.index (text)]
msg = bot.send_message (chat_id, 'How many pages are parsing?')
bot.register_next_step_handler (msg, askAmount)

def askAmount (message):
chat_id = message.chat.id
text = message.text.lower ()
if not text.isdigit ():
msg = bot.send_message (chat_id, 'The number of pages must be a number. Please enter the correct number.')
bot.register_next_step_handler (msg, askAmount)
return
if int (text) <1 or int (text)> 11:
msg = bot.send_message (chat_id, 'The number of pages should be> 0 and <11. Enter correctly.')
bot.register_next_step_handler (msg, askAmount)
return
task.isRunning = False
output = ''
if task.mySource == 'top':
output = parser.getTitlesFromTop (int (text), task.myFilter)
else:
output = parser.getTitlesFromAll (int (text), task.myFilter)
msg = bot.send_message (chat_id, output)

bot.polling (none_stop = True)

Task.py:


class Task ():
isRunning = False
names = [
['smart-topics', 'smart-topics', 'top'],
['webdeveloper-topics', 'webdeveloper-topics', 'all']
]
filters = [
['page'],
['without a threshold', '1', '2', '3', '4']
]
mySource = ''
myFilter = ''
def __init __ (self):
return

Parser.py:


import urllib.request
from bs4 import BeautifulSoup

def getTitlesFromAll (amount, rating = 'all'):
output = ''
for i in range (1, amount + 1):
try:
if rating == 'all':
html = urllib.request.urlopen ('https://smartspate.com/smart-topics/page'+ str (i) +' / '). read ()
else:
html = urllib.request.urlopen ('https://smartspate.com/smart-topics/'+ rating +' / page '+ str (i) +' / '). read ()
except urllib.error.HTTPError:
print ('Error 404 Not Found')
break
soup = BeautifulSoup (html, 'html.parser')
title = soup.find_all ('a', class_ = 'post__title_link')
for i in title:
i = i.get_text ()
output + = ('- "' + i + '", \ n')
return output

def getTitlesFromTop (amount, age = 'daily'):
output = ''
for i in range (1, amount + 1):
try:
html = urllib.request.urlopen ('https://smartspate.com/webdeveloper-topics/'+ age +' / page '+ str (i) +' / '). read ()
except urllib.error.HTTPError:
print ('Error 404 Not Found')
break
soup = BeautifulSoup (html, 'html.parser')
title = soup.find_all ('a', class_ = 'post__title_link')
for i in title:
i = i.get_text ()
output + = ('- "' + i + '", \ n')
return output

Theory. Methods of interaction with the bot.

We use long polling to get data about the messages from the bot.

bot.polling (none_stop = True)

There is the same option to use the root of another method – webhooks. So the bot itself will send us the data about the receipt of the message, etc. But this method is more difficult to configure, and for a simple show Bot, we decided not to use it.

Also in additional materials, there will be links to everything that was used and what was said.

Markups. Adding keyboards for the quick response.

Finally, the main code is completed. Now you can rest and write makeup. we think you’ve seen them many times, but still, I’ll attach a screenshot. [SCREENSHOT]

  • We will output the markup in a separate file – markups.py.

There is nothing complicated in writing markups. You just need to create a markup, specify a couple of parameters, create a pair of buttons and add them to the markup, then simply specify reply_markup = markup in send_message.

For instance:

markups.py:


from telebot import types
source_markup = types.ReplyKeyboardMarkup (row_width = 2, resize_keyboard = True)
source_markup_btn1 = types.KeyboardButton ('smart-topics')
source_markup_btn2 = types.KeyboardButton ('Webdeveloper-topics')
source_markup.add (source_markup_btn1, source_markup_btn2)

In the parameters of the markups, specify the width of the line and the resizing of the buttons, otherwise, they are huge.

You can, of course, fill out each line separately.


markup = types.ReplyKeyboardMarkup()
itembtna = types.KeyboardButton('a')
itembtnv = types.KeyboardButton('v')
itembtnc = types.KeyboardButton('c')
itembtnd = types.KeyboardButton('d')
itembtne = types.KeyboardButton('e')
markup.row(itembtna, itembtnv)
markup.row(itembtnc, itembtnd, itembtne)

bot.py


def start_handler (message):
if not task.isRunning:
chat_id = message.chat.id
msg = bot.send_message (chat_id, 'Where to parse?', reply_markup = m.source_markup)
bot.register_next_step_handler (msg, askSource)
task.isRunning = True

Apply the knowledge to our bot.


from telebot import types

start_markup = types.ReplyKeyboardMarkup (row_width = 1, resize_keyboard = True)
start_markup_btn1 = types.KeyboardButton ('/ start')
start_markup.add (start_markup_btn1)

source_markup = types.ReplyKeyboardMarkup (row_width = 2, resize_keyboard = True)
source_markup_btn1 = types.KeyboardButton ('smart-topics')
source_markup_btn2 = types.KeyboardButton ('webdeveloper-topics')
source_markup.add (source_markup_btn1, source_markup_btn2)

age_markup = types.ReplyKeyboardMarkup (row_width = 3, resize_keyboard = True)
age_markup_btn1 = types.KeyboardButton ('page')
age_markup.add (age_markup_btn1, age_markup_btn2, age_markup_btn3)

amount_markup = types.ReplyKeyboardMarkup (row_width = 3, resize_keyboard = True)
amount_markup_btn1 = types.KeyboardButton ('1')
amount_markup_btn2 = types.KeyboardButton ('3')
amount_markup_btn3 = types.KeyboardButton ('5')
amount_markup.add (amount_markup_btn1, amount_markup_btn2, amount_markup_btn3)


import telebot
import bs4
from Task import Task
import parser
import markups as m

#main variables
TOKEN = '509706011: AAF7aaaaaaaaaaaaaaaaaaaAAAaaAAaAaAAAaa'
bot = telebot.TeleBot (TOKEN)
task = Task ()

#handlers
@ bot.message_handler (commands = ['start', 'go'])
def start_handler (message):
if not task.isRunning:
chat_id = message.chat.id
msg = bot.send_message (chat_id, 'Where to parse?', reply_markup = m.source_markup)
bot.register_next_step_handler (msg, askSource)
task.isRunning = True

def askSource (message):
chat_id = message.chat.id
text = message.text.lower ()
if text in task.names [0]:
task.mySource = 'top'
msg = bot.send_message (chat_id, 'For what time period?', reply_markup = m.age_markup)
bot.register_next_step_handler (msg, askAge)
elif text in task.names [1]:
task.mySource = 'all'
msg = bot.send_message (chat_id, 'What is the minimum rating threshold?', reply_markup = m.rating_markup)
bot.register_next_step_handler (msg, askRating)
else:
msg = bot.send_message (chat_id, 'Enter the section correctly.')
bot.register_next_step_handler (msg, askSource)
return

def askAge (message):
chat_id = message.chat.id
text = message.text.lower ()
filters = task.filters [0]
if text not in filters:
msg = bot.send_message (chat_id, 'There is no such time interval. Please enter the threshold correctly.')
bot.register_next_step_handler (msg, askAge)
return
task.myFilter = task.filters_code_names [0] [filters.index (text)]
msg = bot.send_message (chat_id, 'How many pages do you parse?', reply_markup = m.amount_markup)
bot.register_next_step_handler (msg, askAmount)

def askRating (message):
chat_id = message.chat.id
text = message.text.lower ()
filters = task.filters [1]
if text not in filters:
msg = bot.send_message (chat_id, 'There is no such threshold, enter the threshold correctly.')
bot.register_next_step_handler (msg, askRating)
return
task.myFilter = task.filters_code_names [1] [filters.index (text)]
msg = bot.send_message (chat_id, 'How many pages do you parse?', reply_markup = m.amount_markup)
bot.register_next_step_handler (msg, askAmount)

def askAmount (message):
chat_id = message.chat.id
text = message.text.lower ()
if not text.isdigit ():
msg = bot.send_message (chat_id, 'The number of pages must be a number. Please enter the correct number.')
bot.register_next_step_handler (msg, askAmount)
return
if int (text) <1 or int (text)> 5:
msg = bot.send_message (chat_id, 'The number of pages should be> 0 and <6. Enter correctly.')
bot.register_next_step_handler (msg, askAmount)
return
task.isRunning = False
print (task.mySource + "|" + task.myFilter + '|' + text) #
output = ''
if task.mySource == 'top':
output = parser.getTitlesFromTop (int (text), task.myFilter)
else:
output = parser.getTitlesFromAll (int (text), task.myFilter)
msg = bot.send_message (chat_id, output, reply_markup = m.start_markup)

bot.polling (none_stop = True)

Now we remove TOKEN from bot.py, here it is not needed because we will upload this file to GitHub.

Hooray! With the code, vprintsipe have understood. Now the most important thing is that the bot’s deporting is not clumsy.

Deployment of bot on Heroku.

To begin with, you need to register with Heroku and Github.

Now create a repository on the GitHub. (click the plus sign to the left of your avatar)
We need Procfile (Profile.windows for windows). Create it and write to it bot: python3 bot.py

Through the same terminal that was used to start the bot, we upload the files to the GitHub. (Prior delete the __pycache__ folder).


echo "# HabrParser_Bot" >> README.md
git init
git add.
git add *
git commit -m "Initial Commit" -a
git remote add origin origin https://github.com/name/botname.git # Specify your link
git push -u origin master

GitHub asks for login and password, quietly enter and override the bot’s deblocking to the hacker. We write everything in the same terminal.


heroku login # Enter your email and password
heroku create --region eu habrparserbot # Do not forget to change the name of the application
# P.S. In a name there can be only letters in the lower case, numbers and a dash.
heroku addons: create heroku-redis: hobby-dev -a habrparserbot # And change the name again!
heroku buildpacks: set heroku / python
git push heroku master
heroku config: set TOKEN = 55555555: AAF7aaaaaAaAAA5AAaaaAAaaAA5aA1a # Specify your token
heroku ps: scale bot = 1 # run the bot
heroku logs --tail # include logs

To turn off the bot


heroku ps: stop bot

Congratulations!

The work is finished, your bot should working remotely.