📜 ⬆️ ⬇️

Telegram bot, webhook and 50 lines of code

How again? Another tutorial chewing on official Telegram documentation, did you think? Yes, but no! This is more a discussion on how to build a functional bot service using Python3.5 + , asyncio and aiohttp . The more interesting that the title is actually disingenuous ...

So what is the sly headline? Firstly, the code is not 50 lines, but only 39, and secondly, the bot is not so complicated, just an echo-bot. But it seems to me that this is enough to believe that making your own bot service is not as difficult as it may seem.

Telegram-bot in 39 lines of code
import asyncio import aiohttp from aiohttp import web import json TOKEN = '111111111:AAHKeYAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' API_URL = 'https://api.telegram.org/bot%s/sendMessage' % TOKEN async def handler(request): data = await request.json() headers = { 'Content-Type': 'application/json' } message = { 'chat_id': data['message']['chat']['id'], 'text': data['message']['text'] } async with aiohttp.ClientSession(loop=loop) as session: async with session.post(API_URL, data=json.dumps(message), headers=headers) as resp: try: assert resp.status == 200 except: return web.Response(status=500) return web.Response(status=200) async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) return app if __name__ == '__main__': loop = asyncio.get_event_loop() try: app = loop.run_until_complete(init_app(loop)) web.run_app(app, host='0.0.0.0', port=23456) except Exception as e: print('Error create server: %r' % e) finally: pass loop.close() 


Further, in a few words, what for and how to make the best of what is already there.

Content:


  1. What we use
  2. How to use
  3. What can be improved
  4. Real world

1. What we use



And yet, you must have a domain name under control, a valid or self-signed certificate. Access to the server to which the domain name points to configure the reverse proxy to the address of the service.

To the content

2. How to use


Server


The state of the aiohttp library at the moment is such that with its use you can build a full-fledged web server in Django-style [4] .
')
For standalone service, all power is not useful, so server creation is limited to a few lines.

We initialize the web application:

 async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) return app 

NB Please note that here we define the routing and set the handler of incoming messages.

And start the web server:

 app = loop.run_until_complete(init_app(loop)) web.run_app(app, host='0.0.0.0', port=23456) 

Customer


To send a message, we use the sendMessage method from the Telegram API, for this you need to send a properly prepared URL POST request with parameters as a JSON object. And we do this using aiohttp:

 TOKEN = '111111111:AAHKeYAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' API_URL = 'https://api.telegram.org/bot%s/sendMessage' % TOKEN ... async def handler(request): data = await request.json() headers = { 'Content-Type': 'application/json' } message = { 'chat_id': data['message']['chat']['id'], 'text': data['message']['text'] } async with aiohttp.ClientSession(loop=loop) as session: async with session.post(API_URL, data=json.dumps(message), headers=headers) as resp: try: assert resp.status == 200 except: return web.Response(status=500) return web.Response(status=200) 

NB Please note that in case of successful processing of an incoming message and successful sending of an “echo”, the handler returns an empty response with the HTTP status 200. If this is not done, Telegram services will continue to “pull” hook requests for some time or will receive 200 in response, or until the time specified for the message has expired.

To the content

3. What can be improved?


There is no limit to perfection, a couple of ideas on how to make service more functional.

We use middleware


Suppose there was a need to filter incoming messages. Preprocessing messages can be done on special web handlers; in terms of aiohtttp , these are middlewares [5] .

Example, define middleware to ignore messages from users from the blacklist:

 async def middleware_factory(app, handler): async def middleware_handler(request): data = await request.json() if data['message']['from']['id'] in black_list: return web.Response(status=200) return await handler(request) return middleware_handler 

And we add the handler during web application initialization:

 async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) app.middlewares.append(middleware_factory) return app 

Thoughts on the processing of incoming messages


If the bot is more complicated than the parrot repeater, then we can suggest the following hierarchy of Api → Conversation → CustomConversation objects .

Pseudocode:

 class Api(object): URL = 'https://api.telegram.org/bot%s/%s' def __init__(self, token, loop): self._token = token self._loop = loop async def _request(self, method, message): headers = { 'Content-Type': 'application/json' } async with aiohttp.ClientSession(loop=self._loop) as session: async with session.post(self.URL % (self._token, method), data=json.dumps(message), headers=headers) as resp: try: assert resp.status == 200 except: pass async def sendMessage(self, chatId, text): message = { 'chat_id': chatId, 'text': text } await self._request('sendMessage', message) class Conversation(Api): def __init__(self, token, loop): super().__init__(token, loop) async def _handler(self, message): pass async def handler(self, request): message = await request.json() asyncio.ensure_future(self._handler(message['message'])) return aiohttp.web.Response(status=200) class EchoConversation(Conversation): def __init__(self, token, loop): super().__init__(token, loop) async def _handler(self, message): await self.sendMessage(message['chat']['id'], message['text']) 

Inheriting from Conversation and redefining _handler we get custom handlers, depending on the bot's functionality - weather, financial, etc.

And our service turns into a farm:

 echobot = EchoConversation(TOKEN1, loop) weatherbot = WeatherConversation(TOKEN2, loop) finbot = FinanceConversation(TOKEN3, loop) ... app.router.add_post('/api/v1/echo', echobot.handler) app.router.add_post('/api/v1/weather', weatherbot.handler) app.router.add_post('/api/v1/finance', finbot.handler) 

To the content

4. The real world


Register webhook


Create data.json :

 { "url": "https://bots.domain.tld/api/v1/echo" } 

And we call the corresponding API method in any available way, for example:

 curl -X POST -d @data.json -H "Content-Type: application/json" "https://api.telegram.org/botYOURBOTTOKEN/setWebhook" 

NB Your domain, the hook on which you set, must be resolved, otherwise the setWebhook method will not work.

We use a proxy server


As the documentation says: ports currently supported for Webhooks: 443, 80, 88, 8443.

How to be in the case of self-hosted, when the necessary ports are probably already occupied by the web server, and we did not configure the connection via HTTPS?

The answer is simple, starting the service on any available local interface and using reverse proxies, and nginx is better to find something else here, let it take on the task of organizing an HTTPS connection and forwarding requests to our service.

To the content

Conclusion


I hope that working with the bot via web-brows did not seem much more difficult than long polling, as for me it is even easier, more flexible and more transparent. Additional costs for the organization of the server should not frighten the real bosses.

Let your ideas find a worthy tool for implementation.

Useful:


  1. Telegram Bot API
  2. 18.5. asyncio - Asynchronous I / O, event loop, coroutines and tasks
  3. aiohttp: Asynchronous HTTP Client / Server
  4. aiohttp: Server Tutorial
  5. aiohttp: Server Usage - Middlewares

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


All Articles