📜 ⬆️ ⬇️

Creating and hosting a telegram bot. From A to Z

Hi, habrchane! No matter how hackneyed the theme of creating a bot telegram on python3, I did not find instructions showing the path from the first line of code to the bot deployment (at least all the methods I saw were a bit out of date). In this article I want to show the process of creating a bot from writing BotFather to deploying a bot on Heroku.

The article was a long one, I advise you to go over the contents and click on the item that interests you.

PS Write if you need an article to create a more complex bot, i.e. with webhucks, databases with user settings, etc.
')

For a start it is worth deciding what our bot will do. I decided to write a banal simple bot, which will parse and give us headlines from Habr.
And so, let's start the same.

Botfather


First we need to register our bot in Telegram. For this:

In the search we drive in @BotFather and go into a dialogue with Father Bots.

We write / newbot. Specify the name of the bot (what is displayed in the dialogs). We specify his login by which it can be found.

PS It should end with bot / bot

Here it is. We were given an API key and a link to the bot. It is advisable to save the API key and go into a dialogue with the bot, so you do not have to dig into the correspondence with the BotFather

Next, add a couple of commands to it: write / setcommands and one message , since / setcommands does not add commands, but sets them from scratch, send commands to it.

all - " "
top - ""


This is where the work with BotFather is over, let's move on to the next part.

Install and configure pipenv. First start.


To begin with, we will create a file in which bot.py will be the main bot code . If the bot is large, then immediately create files where you take out functions, classes, etc., otherwise the readability of the code tends to zero. I will add parser.py

Install pipenv , if it certainly does not yet exist.

For Windows:

 pip install pipenv 

For Linux:

 sudo pip3 install pipenv 

Install pipenv in the project folder.

 pipenv install 

Install the libraries of interest to us. I will work with PyTelegramBotAPI. Also for parsing, add BeautifulSoup4.

 pipenv install PyTelegramBotAPI pipenv install beautifulsoup4 

Start writing code!

Open bot.py, import the libraries and create the main variables.

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

Run the bot. Check for errors.

How to start?
For Windows:

 python bot.py 

For Linux:

 python3 bot.py 


If there are no errors, then continue.

Handlers. Responding to commands and messages


It's time to teach the bot to respond to us. It is even possible to make his answers helpful.

Basics of interaction. Response to commands


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

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

 @bot.message_handler(commands=['start', 'go']) def start_handler(message): bot.send_message(message.chat.id, ',   ,      ') bot.polling() 

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

Now you can run the bot and write him / start or / go and he will answer.

PS The message can be not only a string, but, in principle, anything.

PSS What message?
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 message.text and that in order to process the text in message_handler you need to pass content_types = ['text'].

Add this code here.

 @bot.message_handler(content_types=['text']) def text_handler(message): text = message.text.lower() chat_id = message.chat.id if text == "": bot.send_message(chat_id, ',   -  .') elif text == " ?": bot.send_message(chat_id, ',   ?') else: bot.send_message(chat_id, ',     :(') 

Here we added a couple of variables: we carried the text of the message (in lower case, so that there were no unnecessary problems with those who write with a caps, a fence, etc.) into the text variable, carried the message.chat.id into a separate variable, so that every time refer to message. We also built a small branch to respond to certain messages, as well as a response to the case of an incomprehensible bot message.

Summary 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, ',   ,      ') @bot.message_handler(content_types=['text']) def text_handler(message): text = message.text.lower() chat_id = message.chat.id if text == "": bot.send_message(chat_id, ',   -  .') elif text == " ?": bot.send_message(chat_id, ',   ?') else: bot.send_message(chat_id, ',     :(') bot.polling() 


Basics of interaction. The answer to 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, '.') 

All content types:

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

Build a chain of answers.


It's time to end 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'll figure out 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, '  ?') 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, '   ,   .') bot.register_next_step_handler(msg, askAge) #askSource return msg = bot.send_message(chat_id, ',     ' + text + ' .') 

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

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

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

image
This is easy to fix, we will add a variable to check the status of the script execution.

 @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, '  ?') 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, '   ,   .') bot.register_next_step_handler(msg, askAge) #askSource return msg = bot.send_message(chat_id, ',     ' + text + ' .') isRunning = False 

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

Add the parser to the chain.


First you need the parser itself. Note that there are additional filters in the “Best” and “Everything in a row” tabs: day, week, month and ≥10, ≥25, ≥50, ≥100, respectively.
Of course, the parser can also be written to 1 function, but I will break it down into 2, 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://habrahabr.ru/all/page'+ str(i) +'/').read() else: html = urllib.request.urlopen('https://habrahabr.ru/all/'+ 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://habrahabr.ru/top/'+ 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 


At the end, the parser returns us a string with article headers, based on our requests.
We try, using the knowledge gained, to write a bot associated with the parser. I decided to create a separate class (this is most likely the wrong method, but this already applies to python, and not to the main topic of the article), and store the data being changed in the object of this class.

Summary 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, ' ?') 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, '   ?') bot.register_next_step_handler(msg, askAge) elif text in task.names[1]: task.mySource = 'all' msg = bot.send_message(chat_id, '   ?') bot.register_next_step_handler(msg, askRating) else: msg = bot.send_message(chat_id, '  .   .') 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, '   .   .') bot.register_next_step_handler(msg, askAge) return task.myFilter = task.filters_code_names[0][filters.index(text)] msg = bot.send_message(chat_id, '  ?') 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, '  .   .') bot.register_next_step_handler(msg, askRating) return task.myFilter = task.filters_code_names[1][filters.index(text)] msg = bot.send_message(chat_id, '  ?') 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, '    .  .') bot.register_next_step_handler(msg, askAmount) return if int(text) < 1 or int(text) > 11: msg = bot.send_message(chat_id, '    >0  <11.  .') 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) 

Then added none_stop=True) to bot.polling , because of this, the bot will not fall with every error.
Task.py
 class Task(): isRunning = False names = [ ['', '', ''], ['', ' ', 'all'] ] filters = [ ['', '', ''], [' ', '10', '25', '50', '100'] ] filters_code_names = [ ['daily', 'weekly', 'monthly'], ['all', 'top10', 'top25', 'top50', 'top100'] ] 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://habrahabr.ru/all/page'+ str(i) +'/').read() else: html = urllib.request.urlopen('https://habrahabr.ru/all/'+ 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://habrahabr.ru/top/'+ 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 also an option to use another method in the root - webhuki. So the bot itself will send us data about the receipt of the message, etc. But this method is more difficult to configure, and, for a simple exponential bot, I decided not to use it.

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

Marcapu. Add keyboard for quick response.


Finally, the main code is added. Now you can take a break and write stamps. I think you have repeatedly seen them, but still, I will attach a screenshot. [SCREENSHOT]

I will display the markups in a separate file - markups.py.

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

Example
markups.py
 from telebot import types source_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True) source_markup_btn1 = types.KeyboardButton('') source_markup_btn2 = types.KeyboardButton(' ') source_markup.add(source_markup_btn1, source_markup_btn2) 

In the parameters of the markup we specify the width of the string and the resizing of the buttons, otherwise they are huge.

You can of course fill in each row 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, ' ?', reply_markup=m.source_markup) bot.register_next_step_handler(msg, askSource) task.isRunning = True 


Apply this knowledge to our bot.

Summary code
markups.py

 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('') source_markup_btn2 = types.KeyboardButton(' ') source_markup.add(source_markup_btn1, source_markup_btn2) age_markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True) age_markup_btn1 = types.KeyboardButton('') age_markup_btn2 = types.KeyboardButton('') age_markup_btn3 = types.KeyboardButton('') age_markup.add(age_markup_btn1, age_markup_btn2, age_markup_btn3) rating_markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True) rating_markup_btn1 = types.KeyboardButton(' ') rating_markup_btn2 = types.KeyboardButton('10') rating_markup_btn3 = types.KeyboardButton('25') rating_markup_btn4 = types.KeyboardButton('50') rating_markup_btn5 = types.KeyboardButton('100') rating_markup.row(rating_markup_btn1, rating_markup_btn2) rating_markup.row(rating_markup_btn3, rating_markup_btn4, rating_markup_btn5) 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) 

bot.py
 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, ' ?', 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, '   ?', 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, '   ?', reply_markup=m.rating_markup) bot.register_next_step_handler(msg, askRating) else: msg = bot.send_message(chat_id, '  .   .') 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, '   .   .') bot.register_next_step_handler(msg, askAge) return task.myFilter = task.filters_code_names[0][filters.index(text)] msg = bot.send_message(chat_id, '  ?', 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, '  .   .') bot.register_next_step_handler(msg, askRating) return task.myFilter = task.filters_code_names[1][filters.index(text)] msg = bot.send_message(chat_id, '  ?', 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, '    .  .') bot.register_next_step_handler(msg, askAmount) return if int(text) < 1 or int(text) > 5: msg = bot.send_message(chat_id, '    >0  <6.  .') 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) 



Hooray! With the code in principle understood. Now the most important thing is the deployment of the bot is not heroku.

Deploy the bot on Heroku.


First you need to register on Heroka and Github .

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

Now we delete TOKEN from bot.py, here it is not needed, because we will upload this file to the github. Through the same terminal that was used to launch the bot, upload files to the githab. (Previously 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 #   git push -u origin master 

Git asks for a username and password, quietly enter and break the deployment of the bot to heroku. We write everything in the same terminal.

Now we return TOKEN to bot.py, here it is needed, because we will upload this file to the hero.

 heroku login # email   heroku create --region eu habrparserbot #    #PS         ,   . heroku addons:create heroku-redis:hobby-dev -a habrparserbot #   ! heroku buildpacks:set heroku/python git push heroku master heroku ps:scale bot=1 #   heroku logs --tail #  

To turn off the bot
 heroku ps:stop bot 

And, do not forget before pouring on the githab and remove TOKEN from our bot.py. After all, we do not need anyone to use it. You can of course use .gitignore and put the tokens in a separate file.
Congratulations!

Work is over, the bot is working remotely.

Links


Ending bot code on githaba
API to control the bot
Pro Deploing
Pro pipenv
Big Guide, maybe someone will come in handy

Conclusion


If someone was interested, then the purpose of writing the article is completed. If someone wants to see an article about a more complex bot (with webhacks, a connected database with user settings, etc.) - write.

UPDATES
UPD1
  • Added anchors to the content.
  • Changed the code flashing algorithm for githab and heroku.
  • Removed version of PyTelegramBotAPI, because Now herooku works fine with new versions.

Source: https://habr.com/ru/post/350648/


All Articles