Marvin’s Marvellous Guide to All Things Webhook
We currently support two ways of processing bot updates, getUpdates and setWebhook. getUpdates is a pull mechanism, setwebhook is push. Although the concept of a webhook is fairly simple, the setup of the individual components has proven to be tricky for many. This guide provides some extra information for those of you brave enough to venture into the art of the webhook.
There are some advantages of using a webhook over getUpdates. As soon as an update arrives, we’ll kindly deliver it to your bot for processing.
This:
1. Avoids your bot having to ask for updates frequently. 2. Avoids the need for some kind of polling mechanism in your code.
Other advantages may include saving some potential CPU cycles and an increase in response time, these things however depend heavily on the usage pattern of your bot.
Setting a webhook means you supplying Telegram with a location in the form of a URL, on which your bot listens for updates. We need to be able to connect and post updates to that URL.
To ensure that we can do that, there are some basic requirements:
The short version
You’ll need a server that:
- Supports IPv4, IPv6 is currently not supported for webhooks.
- Accepts incoming POSTs from subnets 149.154.160.0/20 and 91.108.4.0/22 on port 443, 80, 88, or 8443.
- Is able to handle TLS1.2(+) HTTPS-traffic.
- Provides a supported, verified or self-signed certificate.
- Uses a CN or SAN that matches the domain you’ve supplied on setup.
- Supplies all intermediate certificates to complete a verification chain.
That’s almost all there’s to it.
If you decide to limit traffic to our specific range of addresses, keep an eye on this document whenever you seem to run into trouble. Our IP-range might change in the future.
The longer version
A domain name
An open port
A webhook needs an open port on your server. We currently support the following ports: 443, 80, 88 and 8443. Other ports are not supported and will not work. Make sure your bot is running on one of those supported ports, and that the bot is reachable via its public address.
If you want to limit access to Telegram only, please allow traffic from 149.154.160.0/20 and 91.108.4.0/22. Whenever something stops working in the future, please check this document again as the range might expand or change.
Always SSL/TLS
Not all SSL/TLS is equal
SSL needs a certificate
Verified or self-signed
Supported certificates
An Untrusted root
Intermediate certificates
More information
Testing your bot
Don’t panic.
The verbose version
How do I get a server with a domain name?
If you use a webhook, we have to deliver requests to your bot to a server we can reach. So yes, you need a server we can connect to. It can be anywhere in the galaxy, if you ensure we can reach the server by domain name (or at least via IP for a self-signed certificate), it will work just fine.
There are quite a few ways to get this done, as a novice however it’s likely that you’re not directly jumping at the chance of crafting this from scratch. Actually, as a novice, we recommend you don’t. It’s likely to be a complex and long ride.
If you got stuck here, make a choice:
- You use getUpdates at the moment and it works, keep it that way. Especially if you’re running your bot from a nice machine that does well. There is nothing wrong with using getUpdates.
- Go with a hosted service and let a bunch of professionals worry about things like registering a domain, setting up DNS, a web server, securing it and so on.
If you're going with a hosted service, make sure to look for a hosting provider that not only supports your code’s needs, for example: support for your PHP version, but one that also handles SSL and allows you to create/deploy certificates.
- Go crazy, dive on the internet and start reading. Once you’re confident that you’ve got all the basic theories down, find yourself a nice hosted VPS or roll your own machine at home and get back to us here.
How do I check for open ports or limit access to my bot?
So you have the hosting thing down and all is good so far, however, when you enter the address of your bot in your browser it seems unreachable.
Explaining every firewall or web server solution in detail isn’t possible for us, which we hope you understand. If you’re running a hosted solution, you’re more likely to have a nice UI where you configure these settings. Head to your configuration panel and check all of them. If you’re on a Linux based VPS with shell access, we have some tips for you:
- Make sure your bot process is indeed configured to listen on the port you’re using.
netstat –ln | grep portnumber
Shows you if your bot is actually listening for incoming requests on the port you expect.
sudo lsof -i | grep process name
Is a simple way to check if that’s actually being listened on by the process your bot is using.
- Make sure it’s listening correctly.
Your bot has to listen on the address you’ve exposed to the outside (your public IP) , it can also listen on all addresses (*: or 0.0.0.0) .
The netstat and lsof -commands mentioned above assist in checking this. If nothing shows up, it is time to check your configuration and fix it. Set the correct IP, make sure it’s listening on a supported port and fire away! Just use a Web Browser to check if you’re reachable. The problem can be in the configuration of your bot, your web server virtual host configuration, or the servers binding configuration.
- If you still can’t reach your address, check your firewall.
sudo iptables –L
OR
sudo ufw status verbose (Ubuntu)
Gives you some insight in the current firewall settings. - If it looks like you’re blocking incoming traffic, let’s fix that.
sudo iptables –A INPUT –p tcp –m tcp –dport portnumber -j ACCEPT
OR
sudo ufw allow portnumber/tcp
Allows incoming traffic on all interfaces to the specified tcp port.
sudo iptables –A INPUT –i interfacename –p tcp –m tcp –dport portnumber -j ACCEPT
OR
sudo ufw allow in on interfacename to any port portnumber proto tcp
Allows incoming traffic to a specific interface and a specific port from everywhere.
sudo ifconfig
Helps you find the interface with the public address you’re going to use.
If you use iptables, make sure to actually SAVE after changing the configuration. On a Debian based system the iptables-persistent package is be a good option. RHEL/CentOS offers a service iptables save -command. A quick online search for "YOUROPERATINGSYSTEM save iptables" also helps.
- If you’re just looking for some hints on how to limit incoming traffic:
sudo iptables –A INPUT –i interfacename –p tcp –m iprange -s 149.154.160.0/20,91.108.4.0/22 –dport portnumber -j ACCEPT
OR
sudo ufw allow in on interfacename to any port portnumber proto tcp from 149.154.160.0/20 sudo ufw allow in on interfacename to any port portnumber proto tcp from 91.108.4.0/22
That’s all for our examples. More information on best practices for setting up your firewall, on whichever operating system you prefer for your bot, is best found on the internet.
SSL/TLS, what is it and why do I have to handle this for a webhook?
You’re already familiar with it in some form or another. Whenever you see that (nicely green) lock in your browser bar, you know it’s reasonably safe to assume that you’ve landed on the site you actually wanted to visit. If you see the green lock, that’s SSL/TLS in action. If you want to learn more about how SSL/TLS works in general, it’s best to search the internet.
The main difference between getUpdates and a webhook is the way the connection takes place. getUpdates means you’ll connect to our server, a webhook means we’ll be connecting to your server instead. Connecting to your server has to be done secure, we have to know for sure it’s you we’re talking to after all. This means you’ll have to handle all that server side encryption stuff, virtually presenting us with a green lock. If you use a web server for us to post to, you need to support SSL/TLS handling on the port/virtual host of your choice. An online search for “YOURWEBSERVER enable HTTPS” will help you.
Not using a regular web server? Have a look at our example page, most examples there include code for handling SSL/TLS in a webhook setup.
How do I check that I’m handling the right version?
You just read up on the whole SSL/TLS stuff, figured out that it’s not all that bad to setup and we add some more requirements. Here are some tips to check if you’re indeed supporting at least TLS1.2.
- Several online services exist that allow you to check your certificate installation,
They give you an overview of your supported TLS versions/Cipher suites and other details. Search online for Symantec crypto report or Qualys ssl. Both supply tools to verify your setup. - Checking locally can also be done, in several ways, here are three options,
- Go simple:
Using Chrome as a browser? Open up the URL to your bot and inspect the certificate details. If you’re supporting TLS Chrome tells you so in the security overview tab. Other browsers are likely able to give you similar basic information. - Using curl:
curl —tlsv1.2 -v -k https://yourbotdomain:yourbotport/
You can add —tlsv1.2 to force curl into using TLS1.2 when trying to connect. -k is optional and used to check against a self-signed certificate. yourbotdomain is the public hostname your webhook is running on. For local testing purposes you can also use the IP. yourbotport is the port you’re using. - Using OpenSSL
openssl s_client -tls1_2 -connect yourbotdomain:yourbotport -servername yourbotdomain
You can add -tls1_2 to force OpenSSL into using TLS1.2 when trying to connect. yourbotdomain is the public hostname your webhook is running on. For local testing purposes you can also use the IP. yourbotport is the port you’re using. Note that https:// isn’t used for OpenSSL. -servername is optional, and included here for some shared hosters, which use SNI to route traffic to the correct domain. When SNI is used you’ll notice that your server appears to be returning a certificate for a different domain than your own. Adding -servername yourbotdomain ensures that SNI negotiation is done, and the correct certificate is returned.
- Forcing TLS in your virtual host on Apache:
SSLProtocol -all +TLSv1.2 - Forcing TLS in your virtual host on Nginx:
ssl_protocols TLSv1.2; - Force TLS for your Java virtual machine through system properties:
-Dhttps.protocols=TLSv1.2 -Djdk.tls.client.protocols=TLSv1.2 - Enabling ssl debug for your JVM:
-Djavax.net.debug=ssl,handshake,record
- Wireshark: excellent packet capturing
- Tcpdump: equally excellent and doesn’t need a GUI
- Charles: web debugging proxy
- Fiddler: web debugging proxy
A certificate, where do I get one and how?
You need a certificate, pick on of these types;
- A verified, supported certificate
- A self-signed certificate
A verified, supported certificate
- Using OpenSSL:
openssl req -newkey rsa:2048 -keyout yourprivatekey.key -out yoursigningrequest.csr
---- Generating a 2048 bit RSA private keywriting new private key to yourprivatekey.key Enter PEM pass phrase: enter a password for your key here Verifying - Enter PEM pass phrase: confirm the entered password ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value,If you enter '.', the field will be left blank.----- Country Name (2 letter code) [AU]: State or Province Name (full name) [Some-State]: Locality Name (eg, city) []: Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []: yourbotdomainname Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: ---- Another example:
- Using Java keytool:
keytool -genkey -alias yourbotdomainname -keyalg RSA -keystore yourkeystore.jks -keysize 2048
--- Enter keystore password: Re-enter new password: What is your first and last name? [Unknown]: yourbotdomainname What is the name of your organizational unit? [Unknown]: What is the name of your organization? [Unknown]: What is the name of your City or Locality? [Unknown]: What is the name of your State or Province? [Unknown]: What is the two-letter country code for this unit? [Unknown]: Is CN=test.telegram.org, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct? [no]: yes Enter key password for yourbotdomainname (RETURN if same as keystore password): ---This generates the initial keystore, from which you can then create a CSR like this:
keytool -certreq -alias yourbotdomainname -keystore yourkeystore.jks -file yourbotdomainname.csr
--- Enter keystore password: ---To validate your certificate the Common Name (CN) has to match your webhook domain. Example, if you’re using https://www.example.com/example.php as a webhook address, the certificate CN has to be www.example.com.
So you need an exact match of the FQDN you’re setting for the webhookThere is an exception, if you’re using a SAN (Subject Alternative Name) the webhook address can either match the CN of your certificate, OR one of the SANs provided in the certificate. In most cases you’ll be using the CN.
Create your CSR and supply the contents of the file to your CA. Most CA’s are kind enough to give you an example command of the input format they expect.
cat yoursigningrequest.csr or cat yourbotdomainname.csr
Lets you have a look at the CSR we just generated:That doesn’t seem to informative, but we can deduce that the file is in PEM format (ASCII base64 encoded) and contains a certificate signing request. Luckily it is possible to look at the human readable contents of the CSR. Use the following commands to double check if all fields are set correctly.
- Using OpenSSL
openssl req -text -noout -verify -in yoursigningrequest.csr - Using Java keytool
keytool -printcertreq -v -file yourbotdomainname.csr
Verify your CSR and supply it to your CA to get a certificate. We’ll use StartSSL as an example here. StartSSL allows you to set up to 5 names (SAN), Their intermediate certificate is also needed for a webhook to work, which makes for a nice complete example.
Go to the certificates wizard, enter the required hostname(s) for your SSL certificate (this is the CN you’ve also set in the CSR and an optional SAN).
In the example above we’ve chosen to set a CN (test.telegram.org) , but also a SAN (sanexample.telegram.org) The CN given has to match the CN used for generating the CSR.
Set your CN (and optional SAN) and copy the contents of the yoursigningrequest.csr file.Paste the contents, submit and you’re done.
Now you can download the created certificate directly. In the example used above you’ll receive a zip file with several PEM certificates. The root, intermediate and yourdomain certificate.
You need the intermediate and yourdomain to set a webhook with a StartSSL certificate.You can inspect the set of certificates you’ve just downloaded.
- Here are some example commands:
- Using OpenSSL:
openssl x509 -in yourdomain.crt -text -noout - Using Java keytool:
keytool -printcert -v -yourdomain.crt - Using Windows:
StartSSL supplies certificates in PEM format with a .crt extension, on Windows you can view the contents of them with a quick double click. Extract the files or open the “Otherserver.zip” and double click each of the certificates for inspection. The details tab supplies you with extra information.
Make sure you have a correct CN in the Subject -field of the yourdomain -certificate. If you’re using a SAN, make sure that it is listed in the Subject Alternative Name -field.
With your fresh certificates at hand, you can now continue setting your webhook.
A self-signed certificate
- Using OpenSSL:
openssl req -newkey rsa:2048 -sha256 -nodes -keyout YOURPRIVATE.key -x509 -days 365 -out YOURPUBLIC.pem -subj «/C=US/ST=New York/L=Brooklyn/O=Example Brooklyn Company/CN=YOURDOMAIN.EXAMPLE»
You’ll end up with 2 files, a private key and the public certificate file. Use YOURPUBLIC.PEM as input file for setting the webhook. - Using Java keytool:
keytool -genkey -keyalg RSA -alias YOURDOMAIN.EXAMPLE -keystore YOURJKS.jks -storepass YOURPASSWORD -validity 360 -keysize 2048
What is your first and last name? [test.telegram.org]: What is the name of your organizational unit? [Unknown]: What is the name of your organization? [Unknown]: What is the name of your City or Locality? [Unknown]: What is the name of your State or Province? [Unknown]: What is the two-letter country code for this unit? [Unknown]: Is CN=test.telegram.org, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct? [no]: yes[NewRequest] ; At least one value must be set in this section Subject = "CN=DOMAIN.EXAMPLE" KeyLength = 2048 KeyAlgorithm = RSA HashAlgorithm = sha256 ;MachineKeySet = true RequestType = Cert UseExistingKeySet=false ;generates a new private key (for export) Exportable = true ;makes the private key exportable with the PFXA self-signed certificate is generated and installed, to use the certificate for a self-signed webhook you’ll have to export it in PEM format.
- Windows continued:
- You can have a look at the certificates in your store with:
certutil -store -user my - To export the installed certificate in DER format (intermediate step for conversion to PEM):
certutil -user -store -split my SERIALNUMBER YOURDER.der - Now you can convert the certificate to PEM:
certutil -encode YOURDER.der YOURPEM.pem
Remember that only the public certificate is needed as input for the self-signed webhook certificate parameter.
certmgr.msc can also be used as a GUI to export the public part of self-signed certificate to PEM.
After following the above you’ll end up with a nice self-signed certificate. You’ll still have to set the webhook, and handle SSL correctly.
How do I set a webhook for either type?
The setWebhook method is needed for both types. For a verified certificate with a trusted root CA, it’s enough to use the setWebhook method with just the URL parameter.
- A curl example for a verified certificate:
curl -F «url=https:///» https://api.telegram.org/bot/setWebhook
For a self-signed certificate an extra parameter is needed, certificate , with the public certificate in PEM format as data.
- A curl example for a self-signed certificate:
curl -F «url=https:///» -F «certificate=@.pem» https://api.telegram.org/bot/setWebhook
The -F means we’re using the multipart/form-data -type to supply the certificate, the type of the certificate parameter is inputFile. Make sure that you’re supplying the correct type.
Both parameters for the setWebhook method are classed as optional. Calling the method with an empty URL parameter can be used to clear a previously set webhook.
- A curl example to clear a previous webhook :
curl -F «url=» https://api.telegram.org/bot/setWebhook
Keep in mind that the URL parameter starts with https:// when setting a webhook. By default that means we’re knocking at your door on port 443. If you want to use another port (80,88 or 8443), you’ll have to specify the port in the URL parameter.
Setting a verified webhook with an untrusted root
If you already have a verified certificate and our servers don’t trust your root CA, we have an alternative way for you to set a webhook. Instead of using the setWebhook method without the certificate parameter, you can use the self-signed method. Your CA’s root certificate has to be used as an inputFile for the certificate parameter.
- A curl example to supply an untrusted root certificate:
curl -F «url=https://» -F «certificate=@.pem» https://api.telegram.org/bot/setWebhook
Before you can do this, you need the root certificate of your certificate’s CA. Most CA’s supply their root certificates in several different formats (PEM/DER/etc.). Visit your CA’s website, and download the Root certificate indicated for your verified certificate.
You can use these commands to quickly convert a DER formatted root certificate to PEM:
- Using OpenSSL:
openssl x509 -inform der -in root.cer -out root.pem - Using Java keytool:
keytool -import -alias Root -keystore YOURKEYSTORE.JKS -trustcacerts -file ROOTCERT.CER
The root certificate needs to be imported in your keystore first:
keytool -exportcert -alias Root -file -rfc -keystore YOURKEYSTORE.JKS
Once done, set your webhook with the root-pem-file and you’ll be good to go. If you need more pointers, have a look at the self-signed part of this guide.
Supplying an intermediate certificate
Once you’ve crafted your certificate, your CA might present you with a nice bundle. Most bundles contain a root certificate, your public certificate and sometimes an intermediate certificate. StartSSL is one of many CA’s that’ll supply such an intermediate beast. This certificate has to be supplied in the chain of certificates you’re presenting to us when we connect to your server. If an intermediate was used to sign your certificate but isn’t supplied to our servers, we won’t be able to verify the chain of trust and your webhook will not work.
If your webhook isn’t working and you’re wondering if the chain is complete:
- Check with your certificate provider if you need an intermediate certificate.
- Verify your certificate chain.
Search online for Symantec crypto report or Qualys ssl. Both supply tools to verify your setup.Here’s an example of a complete chain, note that in this case 2 intermediate certificates have been supplied.
Even though your browser might not complain when visiting your page, an incomplete chain will not work for your webhook. If your chain is incomplete we have some tips to add them to your current setup:
- Apache:
Add the intermediate certificate to the end of the file configured in the SSLCertificateFile directive of your virtual host configuration. If you’re using an older version than Apache 2.4.8, you may use the SSLCertificateChainFile directive instead. - Nginx:
Add the intermediate certificate to the end of the file configured in the ssl_certificate_key directive of your virtual host configuration. - A quick command for doing this correctly:
cat your_domain_name.pem intermediate.pem >> bundle.pem
Make sure the order is correct, expect failure otherwise. - Java keytool:
keytool -import -trustcacerts -alias intermediate -file intermediate.pem -keystore YOURKEYSTORE.jks
The end result of all this is a complete certificate chain, backed by either a root certificate we trust or, in the case of an untrusted root, a root certificate you’re supplying to us. Make sure to verify your setup again after adding the intermediate, once done, you’re good to go!
Testing your bot with updates
- Update examples
A set of example updates, which comes in handy for testing your bot.- Message with text using curl:
curl --tlsv1.2 -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test", "username":"Test" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test", "username":"Test" >, "text":"/start" > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "id":1111111, "type": "private", "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "text":"/start" > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "id":1111111, "type": "private", "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "forward_from": < "last_name":"Forward Lastname", "id": 222222, "first_name":"Forward Firstname" >, "forward_date":1441645550, "text":"/start" > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "forward_from": < "id": -10000000000, "type": "channel", "title": "Test channel" >, "forward_date":1441645550, "text":"/start" > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "text":"/start", "reply_to_message":< "date":1441645000, "chat":< "last_name":"Reply Lastname", "type": "private", "id":1111112, "first_name":"Reply Firstname", "username":"Testusername" >, "message_id":1334, "text":"Original" > > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "edited_message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "text":"Edited text", "edit_date": 1441646600 > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "text":"Bold and italics", "entities": [ < "type": "italic", "offset": 9, "length": 7 >, < "type": "bold", "offset": 0, "length": 4 >] > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "audio": < "file_id": "AwADBAADbXXXXXXXXXXXGBdhD2l6_XX", "duration": 243, "mime_type": "audio/mpeg", "file_size": 3897500, "title": "Test music file" >> >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "voice": < "file_id": "AwADBAADbXXXXXXXXXXXGBdhD2l6_XX", "duration": 5, "mime_type": "audio/ogg", "file_size": 23000 >> >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "message":< "date":1441645532, "chat":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "message_id":1365, "from":< "last_name":"Test Lastname", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "document": < "file_id": "AwADBAADbXXXXXXXXXXXGBdhD2l6_XX", "file_name": "Testfile.pdf", "mime_type": "application/pdf", "file_size": 536392 >> >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "inline_query":< "id": 134567890097, "from":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "query": "inline query", "offset": "" > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "chosen_inline_result":< "result_id": "12", "from":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "query": "inline query", "inline_message_id": "1234csdbsk4839" > >' "https://YOUR.BOT.URL:YOURPORT/"curl -v -k -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '< "update_id":10000, "callback_query":< "id": "4382bfdwdsb323b2d9", "from":< "last_name":"Test Lastname", "type": "private", "id":1111111, "first_name":"Test Firstname", "username":"Testusername" >, "data": "Data from button callback", "inline_message_id": "1234csdbsk4839" > >' "https://YOUR.BOT.URL:YOURPORT/"Подключения Telegram-бота через webhook
Использование Webhook с модулем python-telegram-bot
Все примеры кода по пакету python-telegram-bot запускают Telegram бот с помощью Updater.start_polling() . Он использует метод Telegram API getUpdates для получения новых сообщений для бота. Это вполне нормально для небольших ботов и тестирования, но если бот популярен и получает/отправляет много трафика, то такой подход может замедлить время отклика бота.
Опрос Telegram сервера через webhook — это полезная технология для автоматизации процесса общения с пользователями. Как правило, этот функционал используется для экономии ресурсов на отправку/получение обновлений как собственного сервера, так и серверов Telegram.
Различие между polling и webhook является:
- Опрос polling (через метод .get_updates ) периодически подключается к серверам Telegram для проверки новых обновлений или отправки обработанных сообщений.
- Webhook — это URL-адрес, который передается API Telegram. Каждый раз, когда приходит новое обновление для бота, сервер Telegram отправляет это обновление на указанный URL. Аналогично происходит отправка сообщений.
Содержание:
- Что необходимо для подключения к Telegram через webhook ;
- Встроенный в библиотеку HTTP-сервер для webhook ;
- Обратный прокси-сервер + встроенный сервер webhook ;
- Использование webhook на Heroku;
- Использование nginx с одним доменом/портом для всех ботов;
- Использование haproxy с одним поддоменом на бота;
- Индивидуальное решение, построенное на потоках.
Что необходимо для подключения к Telegram через webhook .
- Публичный IP-адрес или домен. Обычно это означает, что запуск бота должен осуществляться на VPS сервере.
- Необходим SSL-сертификат. Вся связь с серверами Telegram должна быть зашифрована с помощью HTTPS с использованием SSL. В случае подключения polling , о шифровании трафика заботятся серверы Telegram, но если отправка/получение сообщений идет через Webhook, то шифровании должен заботиться клиент/бот. Есть два способа сделать это:
- Подключить проверенный сертификат, выданный доверенным центром сертификации (CA)
- Самостоятельно создать самоподписанный сертификат SSL. Это проще, и в этом нет никакого недостатка.
Чтобы создать самоподписанный SSL-сертификат с помощью openssl , выполните следующую команду в терминале:
$ openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 3650 -out cert.pemУтилита openssl запросит несколько подробностей. Необходимо убедится, что вы ввели правильное полное доменное имя или IP-адрес! Если у сервера есть домен, то введите полное доменное имя (например, sub.example.com ). Если сервер имеет только IP-адрес, то вместо домена введите его IP-адрес. Если введено неверное полное доменное имя или IP-адрес, то бот не получит никаких обновлений от Telegram, при этом не будет никаких ошибок!
Встроенный HTTP-сервер для webhook .
Библиотека python-telegram-bot поставляет встроенный HTTP-сервер, основанный на http.server.HTTPServer . Реализация HTTPServer , которая плотно интегрирована в модуль расширения telegram.ext и может быть запущен с помощью updater.start_webhook / application.run_webhook . Этот веб-сервер также занимается расшифровкой HTTPS-трафика. Это самый простой способ настроить webhook.
Однако у этого решения есть ограничение. Telegram в настоящее время поддерживает только четыре порта для веб-перехватчиков: 443, 80, 88 и 8443. В результате можно запускать не более четырех ботов на одном домене/IP-адресе.
Если это не проблема, то можно использовать код ниже или аналогичный, чтобы запустить бот с webhook. Адрес прослушивания должен быть либо ‘0.0.0.0’ , либо, если нет разрешения на это, общедоступный IP-адрес сервера. Порт может быть одним из 443, 80, 88 или 8443. Рекомендуется установить секретный токен в параметре secret_token , чтобы никто не мог отправить боту поддельные обновления. Аргументы key и cert должны содержать путь к файлам, которые создали ранее. Аргумент webhook_url должен быть фактическим URL-адресом webhook . При этом необходимо перед URL-адресом webhook использовать протокол https:// , домен или IP-адрес, которые установлен в качестве полного доменного имени сертификата, а также правильный порт и URL-адрес.
application.run_webhook( listen='0.0.0.0', port=8443, secret_token='ASecretTokenIHaveChangedByNow', key='private.key', cert='cert.pem', webhook_url='https://example.com:8443' ) # или updater.start_webhook( listen='0.0.0.0', port=8443, secret_token='ASecretTokenIHaveChangedByNow', key='private.key', cert='cert.pem', webhook_url='https://example.com:8443' )
Обратный прокси-сервер + встроенный сервер webhook .
Чтобы решить эту проблему, можно использовать обратный прокси-сервер, такой как nginx или haproxy , а также можно использовать Heroku .
В этой модели обратный прокси ( nginx ), слушает публичный IP-адрес, принимает все запросы webhook и пересылает их на правильный экземпляр локально запущенных встроенных в python-telegram-bot серверов webhook. Обратный прокси также выполняет завершение SSL, то есть расшифровывает HTTPS-соединение, поэтому серверы webhook получают уже расшифрованный трафик. Эти серверы могут работать на любом порту, а не только на четырех разрешенных Telegram портах, т.к. сервера Telegram напрямую подключается только к обратному прокси-серверу.
В зависимости от того, какой прокси-сервер используется, реализация будет выглядеть немного иначе. Ниже перечислены несколько возможных настроек.
Использование webhook на Heroku.
На Heroku использовать webhook можно на свободном плане, т.к. он будет автоматически управлять временем простоя. Для пользователя Heroku будет настроен обратный прокси и создана среда исполнения. Из этой среды необходимо будет извлечь порт, который бот должен прослушивать. Heroku управляет SSL на стороне прокси-сервера, следовательно не нужно создавать сертификат самостоятельно.
import os TOKEN = "TOKEN" PORT = int(os.environ.get('PORT', '8443')) # добавим обработчики application.run_webhook( listen="0.0.0.0", port=PORT, secret_token='ASecretTokenIHaveChangedByNow', webhook_url="https://.herokuapp.com/" )
Использование nginx с одним доменом/портом для всех ботов
Все боты устанавливают свой URL-адрес на один и тот же домен и порт, но с другим url_path . Встроенный в python-telegram-bot сервер обычно запускается по адресу localhost или 127.0.0.1, порт может быть любым.
Примечание: если нет домена, связанного с сервером, то example.com может быть заменен IP-адресом.
Пример кода для запуска бота:
application.run_webhook( listen='127.0.0.1', port=5000, url_path='1', secret_token='ASecretTokenIHaveChangedByNow', webhook_url='https://example.com/1', cert='cert.pem' )
Пример конфигурации для nginx с двумя настроенными ботами (представлены важные части конфига):
server < listen 443 ssl; server_name example.com; ssl_certificate cert.pem; ssl_certificate_key private.key; location /TOKEN1 < proxy_pass http://127.0.0.1:5000/1/; >location /TOKEN2 < proxy_pass http://127.0.0.1:5001/2/; >>
Использование haproxy с одним поддоменом на бота.
При таком подходе, каждому боту присваивается свой собственный поддомен. Если сервер имеет домен example.com , то можно создать поддомены например: bot1.example.com , bot2.example.com и т. д. Понадобится один сертификат для каждого бота с полным доменным именем, установленным для соответствующего поддомена. Встроенный в python-telegram-bot сервер обычно запускается по адресу localhost или 127.0.0.1, порт может быть любым.
Примечание: Необходимо иметь домен привязанный к IP-адресу сервера.
Пример кода для запуска бота:
application.run_webhook( listen='127.0.0.1', port=5000, secret_token='ASecretTokenIHaveChangedByNow', webhook_url='https://bot1.example.com', cert='cert_bot1.pem') )
Пример конфигурации для haproxy с двумя настроенными ботами (сведен к важным частям конфига) . Опять же: полное доменное имя обоих сертификатов должно соответствовать значению в ssl_fc_sni . Кроме того, файлы .pem представляют собой объединенные файлы private.key и cert.pem .
frontend public-https bind 0.0.0.0:443 ssl crt cert_key_bot1.pem crt cert_key_bot2.pem option httpclose use_backend bot1 if < ssl_fc_sni bot1.example.com >use_backend bot2 if < ssl_fc_sni bot2.example.com >backend bot1 mode http option redispatch server bot1.example.com 127.0.0.1:5000 check inter 1000 backend bot2 mode http option redispatch server bot2.example.com 127.0.0.1:5001 check inter 1000
Индивидуальное решение, построенное на потоках.
Не обязательно использовать встроенный веб-сервер. Если решите пойти этим путем, то не следует использовать класс Updater . Модуль telegram.ext был переработан с учетом этой опции, поэтому все равно можно использовать класс Application , чтобы извлечь выгоду из фильтрации / сортировки сообщений, которые он предоставляет. НО придется проделать некоторую работу вручную.
from telegram import Bot from telegram.ext import Application application = Application.builder().token('TOKEN').build() # Регистрация обработчиков здесь # Получаем `update_queue`, из которого приложение получает обновления для обработки. update_queue = application.update_queue start_fetching_updates(update_queue) # Запускаем приложение async with application: application.start() # и останавливаем, когда срабатывает # какой-либо механизм отключения: application.stop()
Здесь start_fetching_updates — это заполнитель для любого метода, который используется для настройки веб-перехватчика. Важной частью является то, что полученные обновления в update_queue ставятся в очередь. То есть вызывается await update_queue.put(update) , где update — это декодированный объект Update (используйте Update.de_json(json.loads(text), bot ) для декодирования обновления из полученных данных JSON).
Альтернатива: нет длительных задач.
Если BOT не использует длительные задачи, запущенные с помощью application.start() , то это не нужно! Вместо того, чтобы помещать обновления в update_queue , можно напрямую обрабатывать их через application.process_update(update) .
Простой пример пользовательского вебхука.
Простой пример бота, который использует пользовательскую настройку веб-перехватчика и обрабатывает пользовательские обновления.Для пользовательской настройки вебхука используются библиотеки starlette и uvicorn . Эти модули необходимо установить как pip install starlette=0.20.0 uvicorn=0.17.0 .
Обратите внимание, что можно использовать любой другой фреймворк веб-сервера на основе asyncio для пользовательской настройки веб-перехватчика.
- Установите токен бота, URL-адрес, admin chat_id и порт в начале основной функции.
- Также может понадобиться изменить значение listen в конфигурации uvicorn , чтобы оно соответствовало вашей настройке.
- Нажатие Ctrl-C в командной строке остановит бота.
import asyncio import html import logging from dataclasses import dataclass from http import HTTPStatus import uvicorn from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import PlainTextResponse, Response from starlette.routing import Route from telegram import __version__ as TG_VER try: from telegram import __version_info__ except ImportError: __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment] if __version_info__ (20, 0, 0, "alpha", 1): raise RuntimeError( f"Пример несовместим с текущей версией PTB TG_VER>. " f"Чтобы просмотреть TG_VER> версия этого примера, " f"посетите https://docs.python-telegram-bot.org/en/vTG_VER>/examples.html" ) from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) logger = logging.getLogger(__name__) @dataclass class WebhookUpdate: """Класс данных для переноса пользовательского типа обновления""" user_id: int payload: str class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): """ Пользовательский класс CallbackContext, который делает user_data доступным для обновлений типа `WebhookUpdate`. """ @classmethod def from_update( cls, update: object, application: "Application", ) -> "CustomContext": if isinstance(update, WebhookUpdate): return cls(application=application, user_id=update.user_id) return super().from_update(update, application) async def start(update: Update, context: CustomContext) -> None: """Показать сообщение с инструкциями по использованию этого бота.""" url = context.bot_data["url"] payload_url = html.escape(f"url>/submitpayload?user_id=&payload=") text = ( f"Чтобы проверить, все ли еще запущен бот, вызовите url>/healthcheck.\n\n" f"Чтобы опубликовать пользовательское обновление, вызовите payload_url> ." ) await update.message.reply_html(text=text) async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: """Обратный вызов, который обрабатывает пользовательские обновления.""" chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) payloads = context.user_data.setdefault("payloads", []) payloads.append(update.payload) combined_payloads = "\n•
".join(payloads) text = ( f"Пользователь chat_member.user.mention_html()> отправил новую полезную нагрузку. " f"На данный момент они отправили следующее: \n\n• combined_payloads> " ) await context.bot.send_message( chat_id=context.bot_data["admin_chat_id"], text=text, parse_mode=ParseMode.HTML ) async def main() -> None: """Настройки приложения и пользовательского веб-сервера.""" url = "https://domain.tld" admin_chat_id = 123456 port = 8000 context_types = ContextTypes(context=CustomContext) # Здесь устанавливаем для `updater` значение `None`, потому что нужно, чтобы пользовательский # сервер обрабатывал обновления, и, следовательно, не нужен экземпляр `Updater`. application = ( Application.builder().token("TOKEN").updater(None).context_types(context_types).build() ) # сохраним значения в `bot_data` таким образом, чтобы можно было легко получить # к ним доступ при обратных вызовах application.bot_data["url"] = url application.bot_data["admin_chat_id"] = admin_chat_id # регистрируем обработчики application.add_handler(CommandHandler("start", start)) application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) # Передать настройки вебхука в телеграмм await application.bot.set_webhook(url=f"url>/telegram") # Поднимаем веб-сервер async def telegram(request: Request) -> Response: """Обрабатываем входящие обновления Telegram, помещая их в `update_queue`""" await application.update_queue.put( Update.de_json(data=await request.json(), bot=application.bot) ) return Response() async def custom_updates(request: Request) -> PlainTextResponse: """ Обрабатываем входящие обновления веб-перехватчика, также помещая их в очередь `update_queue`, если необходимые параметры были переданы правильно. """ try: user_id = int(request.query_params["user_id"]) payload = request.query_params["payload"] except KeyError: return PlainTextResponse( status_code=HTTPStatus.BAD_REQUEST, content="Передайте как `user_id`, так и полезную " "нагрузку в качестве параметров запроса.", ) except ValueError: return PlainTextResponse( status_code=HTTPStatus.BAD_REQUEST, content="`user_id` должен быть строкой!", ) await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) return PlainTextResponse("Спасибо за отправку! Оно пересылается.") async def health(_: Request) -> PlainTextResponse: """Для конечной точки работоспособности отвечаем текстовым сообщением.""" return PlainTextResponse(content="Бот по-прежнему работает нормально :)") starlette_app = Starlette( routes=[ Route("/telegram", telegram, methods=["POST"]), Route("/healthcheck", health, methods=["GET"]), Route("/submitpayload", custom_updates, methods=["POST", "GET"]), ] ) webserver = uvicorn.Server( config=uvicorn.Config( app=starlette_app, port=port, use_colors=False, host="127.0.0.1", ) ) # Запуск приложения и веб-сервера вместе async with application: await application.start() await webserver.serve() await application.stop() if __name__ == "__main__": asyncio.run(main())- КРАТКИЙ ОБЗОР МАТЕРИАЛА.
- Переход на асинхронный python-telegram-bot версии 20.x
- Чистый интерфейс Python для Telegram Bot API
- Команды и оповещения @BotFather в Telegram
- Обработка сообщений модулем python-telegram-bot
- Фильтры сообщений модуля python-telegram-bot
- Хранение временных данных модулем python-telegram-bot
- Настройки по умолчанию модуля python-telegram-bot
- Планировщик сообщений модуля python-telegram-bot
- Форматирование и отправка сообщений в python-telegram-bot
- Работа с файлами/media, модуль python-telegram-bot
- Меню из кнопок, модуль python-telegram-bot
- Объект CallbackContext модуля python-telegram-bot
- Подключения Telegram-бота через webhook
- Обработка исключений модуля python-telegram-bot
- Создание Inline-бота, модуль python-telegram-bot
- Работа с опросами в модуле python-telegram-bot
- Создание разговоров ConversationHandler в python-telegram-bot
- Перезапуск телеграмм-бота в случае ошибки
- Декоратор-обработчик сообщений в python-telegram-bot
- Авторизация на сайте через Telegram Passport
- Ведение публикаций в Telegram-канале с python-telegram-bot
- UTF коды emoji/эмодзи для отправки в Telegram из Python
Telegram-бот, webhook и 50 строк кода
Как, опять? Ещё один туториал, пережёвывающий официальную документацию от Telegram, подумали вы? Да, но нет! Это скорее рассуждения на тему того, как построить функциональный бот-сервис используя Python3.5+, asyncio и aiohttp. Тем интереснее, что заголовок на самом деле лукавит…
Так в чём же лукавство заголовка? Во-первых, кода не 50 строк, а всего 39, а во-вторых, и бот не такой сложный, просто эхо-бот. Но, как мне кажется, этого достаточно, чтобы поверить в то, что сделать свой собственный бот-сервис не столь сложно, как может показаться.
Telegram-bot в 39 строк кода
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()Далее, в нескольких словах, что для чего и как сделать лучше из того, что уже есть.
1. Что используем
- во-первых, Python 3.5+. Почему именно 3.5+, потому что asyncio [2] и потому что сахарные async, await etc;
- во-вторых, aiohttp. Так как сервис на вебхуках, то он одновременно и HTTP-сервер и HTTP-клиент, а что для этого использовать, как не aiohttp [3];
- в-третьих, почему webhook, а не long polling? Если не планируется изначально бот-рассыльщик, то интерактивность является его основной функцией. Выскажу своё мнение, что для этой задачи, бот в роли HTTP-сервера подходит лучше, чем в роли клиента. Да, и отдадим часть работы (доставку сообщений) сервисам Telegram.
2. Как используем
Сервер
Состояние библиотеки aiohttp на текущий момент таково, что с её использованием можно построить полноценный web-сервер в Джанго-стиле [4].
Для standalone-сервиса вся мощь не пригодится, поэтому создание сервера ограничивается несколькими строками.
async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) return appN.B. Обратите внимание, что здесь мы определяем роутинг и задаём обработчик входящих сообщений handler.
И стартуем веб-сервер:
app = loop.run_until_complete(init_app(loop)) web.run_app(app, host='0.0.0.0', port=23456)Клиент
Для отправки сообщения используем метод sendMessage из Telegram API, для этого необходимо отправить на оформленный должным образом URL POST-запрос с параметрами в виде JSON-объекта. И это мы делаем с помощью 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)N.B. Обратите внимание, что в случае успешной обработки входящего сообщения и удачной отправки «эха», обработчик возвращает пустой ответ со статусом HTTP 200. Если этого не сделать, сервисы Telegram продолжат в течение какого-то времени «дёргать» запросами хук, либо пока не получат в ответ 200, либо пока не истечёт определённое для сообщения время.
3. Что можно улучшить
Совершенству нет предела, пара идей, как сделать сервис функциональней.
Используем middleware
Допустим, возникла необходимость фильтровать входящие сообщения. Препроцессинг сообщений можно сделать на специальных веб-обработчиках, в терминах aiohtttp — это middlewares [5].
Пример, определяем мидлварь для игнора сообщений от пользователей из черного списка:
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И добавляем обработчик при инициализации web-приложения:
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Мысли по поводу обработки входящих сообщений
Если бот будет сложнее, чем репитер-попугай, то можно предложить следующую иерархию объектов Api → Conversation → CustomConversation.
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'])Наследуя от Conversation и переопределяя _handler получаем кастомные обработчики, в зависимости от функциональности бота — погодный, финансовый etc.
И наш сервис превращается в ферму:
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)4. Реальный мир
Регистрация webhook
Создаём data.json:
И вызываем соответствующий метод API любым доступным способом, например:
curl -X POST -d @data.json -H "Content-Type: application/json" "https://api.telegram.org/botYOURBOTTOKEN/setWebhook"N.B. Ваш домен, хук на который вы устанавливаете, должен резолвится, иначе метод setWebhook не отработает.
Используем прокси-сервер
Как говорит документация: ports currently supported for Webhooks: 443, 80, 88, 8443.
Как же быть в случае self-hosted, когда необходимые порты уже скорее всего заняты веб-сервером, да и соединение по HTTPS мы в нашем сервисе не настроили?
Ответ простой, запуск сервиса на любом доступном локальном интерфейсе и использование реверс-прокси, и лучше nginx здесь сложно найти что-то другое, пусть он возьмёт на себя задачу организации HTTPS-соединения и переадресацию запросов нашему сервису.
Заключение
Надеюсь, что работа с ботом через вебхуки не показалась сильно сложнее long polling, как по мне так даже проще, гибче и прозрачнее. Дополнительные расходы на организацию сервера не должны пугать настоящего ботовода.
Пусть ваши идеи находят достойный инструмент для реализации.
Полезное:
- Telegram Bot API
- 18.5. asyncio — Asynchronous I/O, event loop, coroutines and tasks
- aiohttp: Asynchronous HTTP Client/Server
- aiohttp: Server Tutorial
- aiohttp: Server Usage — Middlewares
Пишем чат-бот для Telegram на Python, используя webhook и минимум внешних библиотек
Меня зовут Андрей Устьянцев, я ведущий аналитик направления Big Data Лиги Цифровой Экономики, и в этой статье я расскажу, как писал чат‑бот в Telegram на webhook. Если вы знаете, что это такое, и подготовка не вызывает интереса — можете сразу переходить к разделу «Очень кратко». С остальными поделюсь всеми необходимыми шагами.
Почему webhook
Чат‑бот в Telegram может работать в одном из двух режимов.
Один из них называется polling — это когда код, непосредственно реализующий механику бота, опрашивает сервера Telegram с определенной периодичностью («не появилось ли чего новенького»). А если обнаружена активность в чате — реализуется определенная механика взаимодействия (общения).
Большинство материалов в интернете посвящено описанию того, как создавать ботов именно на этой механике, но мне (частное мнение без претензии на истину в последней инстанции) такой подход не очень нравится. Вот почему:
- не вижу смысла постоянно «дергать» сервера Telegram почем зря;
- таймаут, установленный для опроса всех чатов, в которых «общается» бот — это суть задержка в коммуникациях с человеком, который «общается» с ботом.
Второй режим работы ботов, webhook, подразумевает, что Telegram сам вызывает обработчик события/сообщения, когда в боте происходит какая‑то активность. Другими словами, код, реализующий механику бота, срабатывает по инициативе человека, который «общается» с ботом. Самый главный плюс от такого режима работы — ответ бота на действие человека происходит мгновенно: человек написал что‑то боту, Telegram тут же вызвал webhook, написанный код сразу «ответил» человеку.
Лирическое отступление: на тему «что лучше — polling или webhook» спорить можно бесконечно долго. Критерий истины: все зависит от решаемой задачи и от user‑story.
Для задачи, которую решал я, лучше подходит режим webhook: заранее не известно, когда кто‑то напишет боту, но тот должен в любой момент быть готовым поддержать диалог.
При чем тут «минимум» внешних библиотек?
Мое личное убеждение — использовать «чужие» библиотеки по минимуму при написании кода. Особенно в процессе изучения языка программирования. Это отнюдь не означает, что надо впадать в крайности и писать вообще все с нуля — везде возможен разумный компромисс.
Ниже я покажу, что можно написать аккуратный, короткий и читаемый код на Python и без использования специализированных библиотек именно для чат‑ботов (особенно работающих в режиме webhook).
Пошаговая инструкция: как и с чего начать
Расскажу, как я писал код на Python, с какими трудностями столкнулся по пути и как их решал.
Ремарка: код написан в процессе изучения Python, поэтому некоторые моменты могут показаться неоптимальными — буду рад конструктивной критике и предложениям в комментариях к статье.
Итак, что необходимо для работы чат-бота в режиме webhook?
Ссылка на официальную документацию Telegam, где описана работа чат‑ботов в режиме webhook: https://core.telegram.org/bots/api#setwebhook
1. Бот. Я не буду здесь подробно расписывать, как зарегистрировать бота — подробных мануалов достаточно и в интернете, и на этом портале.
2. Сервер для размещения кода, обрабатывающего механику бота — тот самый webhook, которого будет вызывать Telegram при активностях в боте.
Важные моменты (требования Telegram к таким серверам):
- Сервер должен работать круглосуточно. Telegram периодически проверяет служебными запросами наличие и работоспособность webhook.
- Важно, чтобы на сервере был установлен SSL‑сертификат, обеспечивающий (подтверждающий) безопасное соединение с серверами Telegram.
Ну и где брать такой сервер?
Первое — создать свой сервер:
- настроить собственный компьютер под управлением Linux в режиме сервера,
- получить, купить «фирменный» SSL-сертификат или сформировать самостоятельно такой, который Telegram примет как достоверный.
Недостатки такого решения:
- Необходимы дополнительные компетенции по «поднятию» сервера (обычно это еще и на основе Linux) и созданию SSL-сертификата.
Хотя пошаговых инструкций и статей на эту тему достаточно.
- Нужен постоянный IP-адрес подключения к интернету.
Если компьютер подключен к интернету из домашней сети — скорее всего, у вас динамический IP-адрес, который точно не подойдет.
- Надо держать компьютер постоянно включенным в 220В.
Мне в рамках этой задачи круглосуточно гудящий под столом системный блок не нужен.
- Об этом редко пишут, но на поднятый на скорую руку сервер, на который может «постучаться» Telegram, точно будут «ломиться» разные люди, боты, сети в попытке использовать ваш сервер для своих (не всегда хороших) целей.
А это означает, что мало «поднять» сервер, его еще надо и защитить от потенциальных атак из внешних сетей — суровая дополнительная компетенция, скажу я вам.
С профессиональной точки зрения неплохо, конечно, прокачать дополнительные компетенции, но задача состоит в том, чтобы написать на Python код, реализующий механику бота. Причем в процессе изучения самого Python.
Поэтому я выбрал второе решение — облачное.
Идеальный вариант — виртуальный хостинг.
- Круглосуточно работающий сервер Apach, настроенный для запуска приложений на Python.
- Защита от ddos-атак и прочих «пакостей» извне.
- Автоматический выпуск качественного SSL-сертификата.
- Качественный url webhook, соответствующий требованиям Telegram.
- Удобный интерфейс для написания кода на Python прямо из браузера.
Итак, напишем простенького бота, который при любом взаимодействии с ним будет всегда писать в ответ «Ну привет, >»
Очень кратко
- Берем виртуальный хостинг. Подробная инструкция здесь.
- Регистрируем бота.
- Определяем URL-адрес, который сообщим Telegram — что это webhook, соответствующий нашему чат-боту.
- Пишем код на Python.
- «Сообщаем» Telegram адрес webhook нашего бота.
Важно! Рекомендую не менять местами последовательность действий из пунктов 5 и 4. В чем риск? Если Telegram решит проверить работоспособность адреса webhook, а там еще не отлажен код на Python, неправильный ответ Telegram на запрос может привести к блокировке webhook и бота. Снятие блокировки — отдельная неинтересная история.
Разберем код
import time import json import requests def MainProtokol(s,ts = 'Запись'): dt=time.strftime('%d.%m.%Y %H:%M:')+'00' f=open('log.txt','a') f.write(dt+';'+str(ts)+';'+str(s)+'\n') f.close def application(env, start_response): try: content='' token='5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo' if env['PATH_INFO'].lower() == '/tg_bot': # тут вся механика бота именно этот код будет исполняться, когда Telegram будет вызывать webhook wsgi_string=env['wsgi.input'].read() x=wsgi_string.decode('UTF-8') y=x.replace('\n',' ') try: json_string=json.loads(y) except: raise ValueError('Не удалось распарсить в JSON полученную строку') chat_id=json_string['message']['chat']['id'] from_first_name=json_string['message']['from']['first_name'] # имя отправителя # отправка сообщения в чат msg='Ну, привет, '+str(from_first_name) send_message=requests.get('https://api.telegram.org/bot'+token+'/sendMessage?&chat_id='+str(chat_id)+'&text='+str(msg)) if not send_message: raise ValueError('Не удалось отправить текст в бот') elif env['PATH_INFO'] == '/': # случай, когда кто-то просто из браузера зашел на сайт MainProtokol('кто-то просто зашел на сайт') else: # заглушка для обработки ситуации, когда кто-то методом перебора пытается обратиться к какой-то странице сайта raise ValueError('Вызов неизвестного URL :: '+env['PATH_INFO']) start_response('200 OK', [('Content-Type','text/html')]) return [content.encode('utf-8')] except Exception as S: content=str(S) MainProtokol(content,'Ошибка') start_response('200 OK', [('Content-Type','text/html')]) return [content.encode('utf-8')]Нам понадобятся следующие библиотеки:
- time — для работы с функциями даты и времени;
- JSON — для распарсивания JSON-строки, получаемой от Telegram;
- requests — для отправки сообщений в бот.
Примечание: в зависимости от настроек виртуальных серверов по умолчанию и инсталляций Python перечисленные библиотеки могут быть уже предустановлены. Если нет — необходимо их установить командой pip install.
Строка 5: Функция MainProtokol написана для логирования происходящего в коде. Задача этой функции — записывать в текстовый файл то, что в обычных условиях выводится на экран компьютера. Поскольку код выполняется по сути на сервере в момент вызова webhook Telegram, на экран ничего вывести нельзя, потому что экрана как такового нет. Так что текстовый файл — это замена вывода на экран ошибок (если они возникнут при выполнении кода), а также всего, что было бы полезно залогировать.
Строка 12: Функция application — основная у Python, которую вызывает сервер Apach при любом обращении к сайту. И при вызове webhook тоже.
Все данные, которые поступают при обращении к сайту (не важно, открывают ли его в браузере при прямом заходе по имени сайта, или происходит вызов webhook), сохраняются в переменной env (тип — словарь) — суть переменные окружения.
Строка 14: Переменная content в текущем коде — это «заглушка», потому что на экран ничего выводиться не должно. Можно в ней разместить HTML-код, который будет отображается в случае захода на сайт из браузера.
Строка 15: Переменная token — в ней хранится токен бота, полученный при его регистрации.
Строка 17: Проверяем, что вызывается именно webhook. В env['PATH_INFO'] хранится имя страницы, к которой было обращение. Я решил, что у меня страница для коммуникаций с ботами будет называться 'tg_bot'. Настоятельно рекомендую придумывать страницу (адрес) webhook, а не сообщать Telegram просто имя домена – это защитит дополнительно ваш сайт от внешних атак.
Примечание: приведение значения переменной окружения к нижнему регистру функцией lower() — это, можно сказать, мой «пунктик»: так я еще больше уверен, что сравнение строк в одинаковом регистре произойдет корректно.
Строка 19: Получаем данные, переданные из Telegram («от бота») — они хранятся в переменной окружения «wsgi.input» в формате WSGI. Для его дальнейшей обработки применяем метод read().
Строки 24–27: Распарсиваем JSON-строку, полученную от бота.
И вот тут меня ждал подводный камень размерами с булыжник. Метод load библиотеки JSON выдавал ошибку «нарушение структуры JSON». Потратив значительное время и логируя в текстовый файл с использованием функции MainProtokol все, что только можно, я обнаружил следующее:

В JSON-строке, передаваемой из Telegram, совершенно непонятно зачем добавляется спецсимвол «\n» (перевод строки). Причем только в одном месте.
Решение проблемы — перед распарсиванием JSON-строки принудительно заменить в ней «мешающий» спецсимвол на пробел: replace('\n',' ').
Ремарка: по-хорошему надо было бы написать кусок кода или функцию, которая «убирает» из JSON-строки все известные спецсимволы. Но я СТОЛЬКО времени потратил на поиск проблемы, что сил на ее полномасштабное устранение со всеми возможными вариантами последствий впредь у меня уже не осталось.
Итого мы получили словарь в переменной json_string, содержащий все, что передал Telegram от бота. В общем-то, блок строк 17-30 — это то место, где необходимо реализовывать логику «общения» бота. Суть — анализировать полученный JSON и в зависимости от полученных данных программировать поведение бота.
Как разбираться с полученным JSON? По сути это объект message, полное описание которого можно найти в официальной документации вместе с описанием всех вложенных объектов типа chat, user и т. д.
Я пошел другим путем: писал в лог получаемые JSON по разным сценариям взаимодействия с ботом (отправка команды боту, отправка текста боту, отправка картинки), потом смотрел их структуру и реализовывал нужную мне логику.
Ремарка: Telegram передает в webhook только те поля, которые имеют значение. Покажу на примере.
(Исходный код – тут log1 с примерами JSON.txt)
Отправка команды «/start»
Отправка простого текста
- You can have a look at the certificates in your store with:
- Using OpenSSL:
- Using Java keytool:
- Go simple:
