├── .gitignore ├── setup.py ├── README.md ├── Dockerfile ├── tgcall.py └── tgvoip.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.so 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | c_ext = Extension("tgvoip", ["tgvoip.cpp",], libraries=['tgvoip',]) 4 | 5 | setup( 6 | name='pytgvoip', 7 | ext_modules=[c_ext], 8 | ) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python based Telegram VOIP calls 2 | 3 | ## The easy way: Build with Docker 4 | ``` 5 | docker build -t pytgvoip . 6 | ``` 7 | 8 | ## Or, build manually 9 | 10 | ### Build and Install libtgvoip 11 | ``` 12 | git clone https://github.com/gabomdq/libtgvoip.git 13 | cd libtgvoip 14 | ./configure 15 | make 16 | make install 17 | ``` 18 | 19 | ### Build tdlib 20 | ``` 21 | git clone https://github.com/tdlib/td.git 22 | cd td 23 | mkdir build 24 | cd build 25 | cmake -DCMAKE_BUILD_TYPE=Release .. 26 | cmake --build . 27 | ``` 28 | 29 | ### Build and Install tdlib Python wrapper 30 | ``` 31 | git clone https://github.com/gabomdq/python-telegram.git 32 | cd python-telegram 33 | ``` 34 | (Before installing!) Copy libtdjson.so from the previous step to python-telegram/telegram/lib/linux 35 | ``` 36 | python3 setup.py install --user 37 | ``` 38 | 39 | ### Install this extension 40 | ``` 41 | python3 setup.py install --user 42 | ``` 43 | 44 | You need to register an app on Telegram's website, retrieve the API id and hash. 45 | Then you can call someone. 46 | ``` 47 | python3 tgcall.py api_id api_hash phone user_id dbkey 48 | ``` 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | 3 | RUN apk add --update \ 4 | ca-certificates \ 5 | musl \ 6 | build-base \ 7 | python3 \ 8 | python3-dev \ 9 | bash \ 10 | git \ 11 | libxml2-dev \ 12 | libxslt-dev \ 13 | openssl-dev \ 14 | opus-dev \ 15 | pulseaudio-dev \ 16 | alsa-lib-dev \ 17 | cmake \ 18 | gperf \ 19 | && pip3.6 install --upgrade pip \ 20 | && rm /var/cache/apk/* 21 | 22 | # make us compatible with manylinux wheels and create some useful symlinks that are expected to exist 23 | RUN echo "manylinux1_compatible = True" > /usr/lib/python3.6/_manylinux.py \ 24 | && cd /usr/bin \ 25 | && ln -sf easy_install-3.6 easy_install \ 26 | && ln -sf idle3.6 idle \ 27 | && ln -sf pydoc3.6 pydoc \ 28 | && ln -sf python3.6 python \ 29 | && ln -sf python-config3.6 python-config \ 30 | && ln -sf pip3.6 pip \ 31 | && ln -sf /usr/include/locale.h /usr/include/xlocale.h 32 | 33 | WORKDIR /usr/src 34 | 35 | # Build libtgvoip 36 | RUN set -ex; git clone https://github.com/gabomdq/libtgvoip.git 37 | RUN set -ex; ./configure && make -j4 && make install 38 | 39 | # Build tdlib 40 | WORKDIR /usr/src 41 | RUN set -ex; git clone https://github.com/tdlib/td.git 42 | WORKDIR /usr/src/td 43 | RUN set -ex; mkdir build 44 | WORKDIR /usr/src/td/build 45 | RUN set -ex; cmake -DCMAKE_BUILD_TYPE=Release .. 46 | RUN set -ex; cmake --build . -- -j4 47 | 48 | # python-telegram 49 | WORKDIR /usr/src 50 | RUN git clone https://github.com/gabomdq/python-telegram.git 51 | RUN mkdir -p python-telegram/telegram/lib/linux && cp td/build/libtdjson.so python-telegram/telegram/lib/linux 52 | RUN set -ex; cd python-telegram;python setup.py install 53 | 54 | # pytgvoip 55 | WORKDIR /usr/src 56 | RUN git clone https://github.com/gabomdq/pytgvoip.git 57 | RUN cd pytgvoip;python setup.py install 58 | 59 | CMD python 60 | 61 | 62 | ARG VCS_REF 63 | ARG VCS_URL 64 | ARG BUILD_DATE 65 | LABEL org.label-schema.vcs-ref=${VCS_REF} \ 66 | org.label-schema.vcs-url=${VCS_URL} \ 67 | org.label-schema.build-date=${BUILD_DATE} 68 | -------------------------------------------------------------------------------- /tgcall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Telegram VOIP calls from python example 3 | # Author Gabriel Jacobo https://mdqinc.com 4 | 5 | import logging 6 | import argparse 7 | import os 8 | import json 9 | import base64 10 | from telegram.client import Telegram 11 | from tgvoip import call 12 | 13 | 14 | def setup_voip(data): 15 | # state['config'] is passed as a string, convert to object 16 | data['state']['config'] = json.loads(data['state']['config']) 17 | # encryption key is base64 encoded 18 | data['state']['encryption_key'] = base64.decodebytes(data['state']['encryption_key'].encode('utf-8')) 19 | # peer_tag is base64 encoded 20 | for conn in data['state']['connections']: 21 | conn['peer_tag'] = base64.decodebytes(conn['peer_tag'].encode('utf-8')) 22 | call(data) 23 | 24 | def handler(msg): 25 | #print ("UPDATE >>>", msg) 26 | if msg['@type'] == 'updateCall': 27 | data = msg['call'] 28 | if data['id'] == outgoing['id'] and data['state']['@type'] == 'callStateReady': 29 | setup_voip(data) 30 | 31 | 32 | if __name__ == '__main__': 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps 35 | parser.add_argument('api_hash', help='API hash') 36 | parser.add_argument('phone', help='Phone nr originating call') 37 | parser.add_argument('user_id', help='User ID to call') 38 | parser.add_argument('dbkey', help='Database encryption key') 39 | args = parser.parse_args() 40 | 41 | tg = Telegram(api_id=args.api_id, 42 | api_hash=args.api_hash, 43 | phone=args.phone, 44 | td_verbosity=5, 45 | files_directory = os.path.expanduser("~/.telegram/" + args.phone), 46 | database_encryption_key=args.dbkey) 47 | tg.login() 48 | 49 | # if this is the first run, library needs to preload all chats 50 | # otherwise the message will not be sent 51 | r = tg.get_chats() 52 | r.wait() 53 | 54 | 55 | r = tg.call_method('createCall', {'user_id': args.user_id, 'protocol': {'udp_p2p': True, 'udp_reflector': True, 'min_layer': 65, 'max_layer': 65} }) 56 | r.wait() 57 | outgoing = r.update 58 | 59 | tg.add_handler(handler) 60 | tg.idle() # blocking waiting for CTRL+C 61 | 62 | 63 | -------------------------------------------------------------------------------- /tgvoip.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* tgvoip.cpp - Python wrapper for libtgvoip 3 | version 0.1 August 8th, 2018 4 | 5 | Copyright (C) 2018 Gabriel Jacobo https://mdqinc.com 6 | 7 | This software is provided 'as-is', without any express or implied 8 | warranty. In no event will the authors be held liable for any damages 9 | arising from the use of this software. 10 | Permission is granted to anyone to use this software for any purpose, 11 | including commercial applications, and to alter it and redistribute it 12 | freely, subject to the following restrictions: 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | */ 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "tgvoip/VoIPController.h" 28 | #include "tgvoip/VoIPServerConfig.h" 29 | 30 | 31 | /* Available functions */ 32 | static PyObject *tgvoip_call_start(PyObject *self, PyObject *args); 33 | static PyObject *tgvoip_call_stop(PyObject *self, PyObject *args); 34 | 35 | /* Module specification */ 36 | static PyMethodDef module_methods[] = { 37 | {"call_start", (PyCFunction) tgvoip_call_start, METH_VARARGS, "Establish a call"}, 38 | {"call_stop", (PyCFunction) tgvoip_call_stop, METH_NOARGS, "Terminate a call"}, 39 | {NULL, NULL, 0, NULL} 40 | }; 41 | 42 | static struct PyModuleDef tgvoip_module = 43 | { 44 | PyModuleDef_HEAD_INIT, 45 | "tgvoip", /* name of module */ 46 | "libtgvoip wrapper for Python 3", 47 | -1, 48 | module_methods 49 | }; 50 | 51 | /* Initialize the module */ 52 | PyMODINIT_FUNC PyInit_tgvoip(void) 53 | { 54 | return PyModule_Create(&tgvoip_module); 55 | } 56 | 57 | 58 | /* Telegram Voip Calling Implementation */ 59 | using namespace std; 60 | using namespace tgvoip; 61 | 62 | static VoIPController::Config config { 63 | /*init_timeout*/30.0, 64 | /*recv_timeout*/30.0, 65 | /*data_saving*/DATA_SAVING_NEVER, 66 | /*enableAEC*/true, 67 | /*enableNS*/true, 68 | /*enableAGC*/true, 69 | /*enableCallUpgrade*/false, 70 | }; 71 | 72 | static VoIPController *cnt = NULL; 73 | static bool call_active = false; 74 | 75 | static bool check_type(PyObject *o, const char *type) 76 | { 77 | PyObject *t = PyDict_GetItemString(o, "@type"); 78 | if (t == NULL) { 79 | return false; 80 | } 81 | 82 | t = PyObject_Str(t); 83 | bool ret = strcmp(PyUnicode_AsUTF8(t), type) == 0; 84 | Py_DECREF(t); 85 | return ret; 86 | } 87 | 88 | static PyObject *tgvoip_call_start(PyObject *self, PyObject *args) 89 | { 90 | PyObject *call = NULL; 91 | if (!PyArg_UnpackTuple(args, "tgvoip_call", 1, 1, &call)) { 92 | return NULL; 93 | } 94 | 95 | if (!PyDict_Check(call)) { 96 | PyErr_SetString(PyExc_RuntimeError, "Parameter has to be a dict with call information"); 97 | return NULL; 98 | } 99 | 100 | // Verify the "@type" property 101 | if (!check_type(call, "call")) { 102 | PyErr_SetString(PyExc_RuntimeError, "Parameter does not have @type == call"); 103 | return NULL; 104 | } 105 | 106 | // Retrieve the state dict 107 | PyObject *state = PyDict_GetItemString(call, "state"); 108 | if (state == NULL) { 109 | PyErr_SetString(PyExc_RuntimeError, "No state entry found"); 110 | return NULL; 111 | } 112 | 113 | if (!check_type(state, "callStateReady")) { 114 | PyErr_SetString(PyExc_RuntimeError, "state does not have @type == callStateReady"); 115 | return NULL; 116 | } 117 | 118 | // Build the endpoints list 119 | PyObject *connections = PyDict_GetItemString(state, "connections"); 120 | if( !PyList_Check(connections)) { 121 | PyErr_SetString(PyExc_RuntimeError, "Connections element is not a list"); 122 | return NULL; 123 | } 124 | 125 | vector endpoints; 126 | uint32_t num_connections = PyList_Size(connections); 127 | for(uint32_t i = 0; i < num_connections; i++) { 128 | PyObject *conn = PyList_GetItem(connections, i); 129 | 130 | if (!PyDict_Check(conn)) { 131 | continue; 132 | } 133 | 134 | if (!check_type(conn, "callConnection")) { 135 | continue; 136 | } 137 | 138 | 139 | PyObject *conn_type = PyDict_GetItemString(conn, "@type"); 140 | if (conn_type == NULL) { 141 | continue; 142 | } 143 | 144 | conn_type = PyObject_Str(conn_type); 145 | if (strcmp(PyUnicode_AsUTF8(conn_type), "callConnection") != 0) { 146 | Py_DECREF(conn_type); 147 | continue; 148 | } 149 | Py_DECREF(conn_type); 150 | 151 | PyObject *conn_id = PyDict_GetItemString(conn, "id"); 152 | if (conn_id == NULL) { 153 | continue; 154 | } 155 | conn_id = PyLong_FromUnicodeObject(conn_id, 10); 156 | int64_t id = (int64_t) PyLong_AsLongLong(conn_id); 157 | Py_DECREF(conn_id); 158 | 159 | PyObject *conn_port = PyDict_GetItemString(conn, "port"); 160 | if (conn_port == NULL) { 161 | continue; 162 | } 163 | uint16_t port = (uint16_t) PyLong_AsLong(conn_port); 164 | 165 | PyObject *conn_ip = PyDict_GetItemString(conn, "ip"); 166 | if (conn_ip == NULL) { 167 | continue; 168 | } 169 | conn_ip = PyObject_Str(conn_ip); 170 | IPv4Address address = IPv4Address(string(PyUnicode_AsUTF8(conn_ip))); 171 | Py_DECREF(conn_ip); 172 | 173 | PyObject *conn_ipv6 = PyDict_GetItemString(conn, "ipv6"); 174 | if (conn_ipv6 == NULL) { 175 | continue; 176 | } 177 | conn_ipv6 = PyObject_Str(conn_ipv6); 178 | IPv6Address v6address = IPv6Address(string(PyUnicode_AsUTF8(conn_ipv6))); 179 | Py_DECREF(conn_ipv6); 180 | 181 | PyObject *peer_tag = PyDict_GetItemString(conn, "peer_tag"); 182 | if (peer_tag == NULL) { 183 | continue; 184 | } 185 | 186 | endpoints.push_back(Endpoint(id, port, address, v6address, Endpoint::TYPE_UDP_RELAY, (unsigned char *) PyBytes_AsString(peer_tag))); 187 | } 188 | 189 | // Build the server config 190 | PyObject *state_config = PyDict_GetItemString(state, "config"); 191 | if( !PyDict_Check(state_config)) { 192 | PyErr_SetString(PyExc_RuntimeError, "state[config] element is not a dict"); 193 | return NULL; 194 | } 195 | map server_conf; 196 | PyObject *key, *value; 197 | Py_ssize_t pos = 0; 198 | 199 | while (PyDict_Next(state_config, &pos, &key, &value)) { 200 | PyObject *k = PyObject_Str(key); 201 | PyObject *v = PyObject_Str(value); 202 | server_conf[string(PyUnicode_AsUTF8(k))] = string(PyUnicode_AsUTF8(v)); 203 | Py_DECREF(k); 204 | Py_DECREF(v); 205 | } 206 | // Encryption key 207 | PyObject *encription_key = PyDict_GetItemString(state, "encryption_key"); 208 | if (encription_key == NULL) { 209 | PyErr_SetString(PyExc_RuntimeError, "No state[encryption_key] entry found"); 210 | return NULL; 211 | } 212 | 213 | // Outgoing 214 | PyObject *is_outgoing = PyDict_GetItemString(call, "is_outgoing"); 215 | if (is_outgoing == NULL) { 216 | PyErr_SetString(PyExc_RuntimeError, "No call[is_outgoing] entry found"); 217 | return NULL; 218 | } 219 | 220 | // Protocol 221 | PyObject *protocol = PyDict_GetItemString(state, "protocol"); 222 | if( !PyDict_Check(protocol)) { 223 | PyErr_SetString(PyExc_RuntimeError, "No adequate state[protocol] entry found"); 224 | return NULL; 225 | } 226 | 227 | PyObject *udp_p2p = PyDict_GetItemString(protocol, "udp_p2p"); 228 | bool allow_p2p = false; 229 | if (udp_p2p != NULL) { 230 | allow_p2p = PyObject_IsTrue(udp_p2p); 231 | } 232 | 233 | PyObject *protocol_max_layer = PyDict_GetItemString(protocol, "max_layer"); 234 | int32_t max_layer = 65; 235 | if (protocol_max_layer != NULL) { 236 | max_layer = PyLong_AsLong(protocol_max_layer); 237 | } 238 | 239 | ServerConfig::GetSharedInstance()->Update(server_conf); 240 | if (cnt == NULL) { 241 | cnt = new VoIPController(); 242 | } 243 | cnt->SetConfig(config); 244 | cnt->SetEncryptionKey(PyBytes_AsString(encription_key), PyObject_IsTrue(is_outgoing)); 245 | cnt->SetRemoteEndpoints(endpoints, allow_p2p, max_layer); 246 | cnt->Start(); 247 | cnt->Connect(); 248 | call_active = true; 249 | Py_INCREF(Py_True); 250 | return Py_True; 251 | } 252 | 253 | 254 | static PyObject *tgvoip_call_stop(PyObject *self, PyObject *args) 255 | { 256 | if (!call_active) { 257 | Py_INCREF(Py_False); 258 | return Py_False; 259 | } 260 | cnt->Stop(); 261 | // Deleting is not strictly required but it works around a bug when using the same UDP port multiple times 262 | delete cnt; 263 | cnt = NULL; 264 | call_active = false; 265 | Py_INCREF(Py_True); 266 | return Py_True; 267 | 268 | } 269 | --------------------------------------------------------------------------------