{"id":115,"date":"2018-08-10T09:36:11","date_gmt":"2018-08-10T12:36:11","guid":{"rendered":"http:\/\/mdqinc.com\/blog\/?p=115"},"modified":"2018-08-10T09:58:22","modified_gmt":"2018-08-10T12:58:22","slug":"telegram-voip-calls-using-python","status":"publish","type":"post","link":"https:\/\/mdqinc.com\/blog\/2018\/08\/telegram-voip-calls-using-python\/","title":{"rendered":"Telegram VOIP calls using Python"},"content":{"rendered":"<p>I&#8217;ve been experimenting with a Home Assistant based automation setup at home. It started as a mini greenhouse control temperature control and monitor system, and quickly evolved to a modest size system with a few devices. Using a combination of ESP8266s \/ ESP32s, Micropython and Arduino Framework code, I managed to connect my home alarm, a couple of doors and the HVAC system with relative ease and I once again felt the joy of unburdened programming &#8230;until I got to the doorbell.<\/p>\n<p>That&#8217;s when, with my mind fueled by the home automation craze, I thought: &#8220;Hey, it&#8217;d be cool to be able to listen in on my doorbell from anywhere&#8221; (It seemed like a good idea at the time&#8230; you had to be there). Getting the sound to the Raspberry Pi that acts as the controller of the system would be relatively simple&#8230;but how to send the audio to my phone?<\/p>\n<p>It turns out that there aren&#8217;t that many cross platforms that run on Linux (and ARM Linux at that!) and can do VOIP calls to Android or iOS. So I needed something that was easy to use, ran on ARM Linux and Android, preferably was open source, had security features and could be controller via scripting. Long story short: I didn&#8217;t find anything suitable, but! Telegram was almost a perfect fit, minus the easy to use part.<\/p>\n<p>I had been using Telegram for quite some time for group messaging (and loving it), and I was marginally aware that their clients were open source and supported voice. So if I could somehow run their Linux client on the Raspberry Pi, I would be set. However, their desktop\u00a0 is not scripting friendly (or documented, at all), and their CLI client is mostly a test platform for APIs and doesn&#8217;t support voice calling (and it&#8217;s not documented, at all). Further, while they provide code for all the parts of the process, the examples and documentation are scattered through the internets and difficult to stumble across.<\/p>\n<p>So, here&#8217;s how I did it.<\/p>\n<p>Telegram has different\u00a0APIs\u00a0that allow you\u00a0to talk to their servers and make calls: There&#8217;s the Telegram proper API, <a href=\"https:\/\/github.com\/tdlib\/td\">tdlib<\/a> (a &#8220;simplified&#8221; wrapper library around the Telegram API), the bots API (a subset of the Telegram API is available for bots), and then <a href=\"https:\/\/github.com\/grishka\/libtgvoip\">tgvoip<\/a> which does the VOIP part.<\/p>\n<p>If you want to make calls, you have to<a href=\"https:\/\/core.telegram.org\/api\/obtaining_api_id\"> get an app id,<\/a> this would be like your custom Telegram client that &#8220;humans&#8221; can use, as opposed to bots. Login to apps is only allowed via a Telegram registered phone number and bots can not access VOIP calls\u00a0(at least at the time of this writing).<\/p>\n<p>The first piece of the puzzle is tdlib. If you look at Telegram&#8217;s core API, it&#8217;s complex. tdlib simplifies the interaction with this asynchronous API by providing <strong>another<\/strong> asynchronous API that manages all the low level implementation details (such as computing encryption key hashes, decrypting the database, etc). There are a few python wrappers for tdlib, the best one that I found in my opinion was <a href=\"https:\/\/github.com\/alexander-akhmetov\/python-telegram\">python-telegram<\/a> which I <a href=\"https:\/\/github.com\/gabomdq\/python-telegram\">forked<\/a> here with a couple minor fixes to be able to receive all of tdlibs messages in the user app, and to control tdlib&#8217;s verbosity level which is a good source of information when debugging issues. I recommend reading python-telegram&#8217;s <a href=\"http:\/\/python-telegram.readthedocs.io\/en\/latest\/tutorial.html\">tutorial<\/a> to get an idea of how the wrapper works.<\/p>\n<p>tdlib offers two methods of usage. The first one is by <a href=\"https:\/\/github.com\/tdlib\/td\/blob\/master\/example\/cpp\/td_example.cpp\">linking directly<\/a> to its low level functions, the other one is via a (yes, yet another) JSON API called tdjson which is what the Python wrapper presented above uses.<\/p>\n<p>Going beyond the basic tutorial, I started looking for VOIP tutorials, and came up empty. So going through\u00a0<a href=\"https:\/\/core.telegram.org\/tdlib\/docs\/index.html\">tdlib&#8217;s API<\/a>\u00a0list of functions, a few that seemed interesting showed up: createCall, acceptCall, etc. I decided to give those a go, and actually got my phone to ring!<\/p>\n<p>Sadly, that&#8217;s all it did. Because I was missing the second piece of the puzzle: tgvoip. This is the library that actually does the UDP or TCP connection, encryption, Opus encoding and decoding, etc. You have to glue this to tdlib (somehow!) in order to have fully working Telegram VOIP calls.<\/p>\n<p>tgvoip is C++ based, and gluing it to Python requires a C module. Luckily for you, I&#8217;ve made such a thing and published it here as <a href=\"https:\/\/github.com\/gabomdq\/pytgvoip\">pytgvoip<\/a>. It even includes a Dockerfile for you docker crazed kids (in my day we installed dependencies by hand! BY HAND!), and I included a quick and dirty example of how to use:<\/p>\n<pre class=\"brush:python\">#!\/usr\/bin\/env python3\r\n# Telegram VOIP calls from python example\r\n# Author Gabriel Jacobo https:\/\/mdqinc.com\r\n\r\nimport logging\r\nimport argparse\r\nimport os\r\nimport json\r\nimport base64\r\nfrom telegram.client import Telegram\r\nfrom tgvoip import call\r\n\r\n\r\ndef setup_voip(data):\r\n    # state['config'] is passed as a string, convert to object\r\n    data['state']['config'] = json.loads(data['state']['config'])\r\n    # encryption key is base64 encoded\r\n    data['state']['encryption_key'] = base64.decodebytes(data['state']['encryption_key'].encode('utf-8'))\r\n    # peer_tag is base64 encoded\r\n    for conn in data['state']['connections']:\r\n        conn['peer_tag'] = base64.decodebytes(conn['peer_tag'].encode('utf-8'))\r\n    call(data)\r\n\r\ndef handler(msg):\r\n    #print (\"UPDATE &gt;&gt;&gt;\", msg)\r\n    if msg['@type'] == 'updateCall':\r\n        data = msg['call']\r\n        if data['id'] == outgoing['id'] and data['state']['@type'] == 'callStateReady':\r\n            setup_voip(data)\r\n\r\n\r\nif __name__ == '__main__':\r\n    parser = argparse.ArgumentParser()\r\n    parser.add_argument('api_id', help='API id')  # https:\/\/my.telegram.org\/apps\r\n    parser.add_argument('api_hash', help='API hash')\r\n    parser.add_argument('phone', help='Phone nr originating call')\r\n    parser.add_argument('user_id', help='User ID to call')\r\n    parser.add_argument('dbkey', help='Database encryption key')\r\n    args = parser.parse_args()\r\n\r\n    tg = Telegram(api_id=args.api_id,\r\n                api_hash=args.api_hash,\r\n                phone=args.phone,\r\n                td_verbosity=5,\r\n                files_directory = os.path.expanduser(\"~\/.telegram\/\" + args.phone),\r\n                database_encryption_key=args.dbkey)\r\n    tg.login()\r\n\r\n    # if this is the first run, library needs to preload all chats\r\n    # otherwise the message will not be sent\r\n    r = tg.get_chats()\r\n    r.wait()\r\n\r\n\r\n    r = tg.call_method('createCall', {'user_id': args.user_id, 'protocol': {'udp_p2p': True, 'udp_reflector': True, 'min_layer': 65, 'max_layer': 65} })\r\n    r.wait()\r\n    outgoing = r.update\r\n\r\n    tg.add_handler(handler)\r\n    tg.idle()  # blocking waiting for CTRL+C\r\n<\/pre>\n<p>Essentially what this does is use tdlib to issue a <strong>createCall<\/strong> to a Telegram user_id (getting the user_id from the phone number is its own thing so I won&#8217;t explain it here but Google&#8217;s your friend). tdlib will initiate and negotiate the call for you (that&#8217;s when the other phone starts ringing!) and eventually send a <strong>updateCall<\/strong> callback to your handler with a <strong>callStateReady<\/strong> state. This means the other user picked up the call, and now we have to pass the call information to tgvoip, and eventually manage the disconnection (not shown here).<\/p>\n<p>So I had calls to my phone from my Linux desktop script finally working. But if you remember my initial goal, I was still nowhere near it&#8230;how to get there will be the topic of my next post.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ve been experimenting with a Home Assistant based automation setup at home. It started as a mini greenhouse control temperature control and monitor system, and quickly evolved to a modest size system with a few devices. Using a combination of ESP8266s \/ ESP32s, Micropython and Arduino Framework code, I managed to connect my home alarm, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[13,4],"tags":[],"_links":{"self":[{"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/posts\/115"}],"collection":[{"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/comments?post=115"}],"version-history":[{"count":6,"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/posts\/115\/revisions"}],"predecessor-version":[{"id":125,"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/posts\/115\/revisions\/125"}],"wp:attachment":[{"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/media?parent=115"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/categories?post=115"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mdqinc.com\/blog\/wp-json\/wp\/v2\/tags?post=115"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}