├── twx ├── mtproto │ ├── __init__.py │ ├── crypt.py │ ├── prime.py │ ├── mtproto.py │ └── rpc.py ├── __init__.py └── twx.py ├── setup.cfg ├── docs ├── twx │ ├── index.rst │ ├── twx.rst │ └── botapi │ │ └── botapi.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── README.rst ├── .gitignore ├── LICENSE.txt └── setup.py /twx/mtproto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [upload_docs] 3 | upload-dir=docs/_build/html 4 | -------------------------------------------------------------------------------- /docs/twx/index.rst: -------------------------------------------------------------------------------- 1 | TWX Modules 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | twx 8 | botapi/botapi 9 | 10 | -------------------------------------------------------------------------------- /docs/twx/twx.rst: -------------------------------------------------------------------------------- 1 | :mod:`twx` --- Telegram APIs Abstraction Layer 2 | ============================================== 3 | 4 | .. automodule:: twx 5 | 6 | -------------------------------------------------------------------------------- /twx/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | """ 4 | Expand the tex namespace 5 | """ 6 | if sys.version_info[0] == 3 and sys.version_info[1] >=2: 7 | from pkgutil import extend_path 8 | __path__ = extend_path(__path__, __name__) 9 | 10 | """ 11 | Only import twx modules on supported pytohn versions. Type hinting breaks testing 2.7 against twx.botapi 12 | """ 13 | if sys.version_info[0] == 3 and sys.version_info[1] >=4: 14 | from . twx import * 15 | 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. TWX documentation master file, created by 2 | sphinx-quickstart on Sat Jun 27 15:07:02 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | TWX: Telegram Bot API and MTProto Client and Abstraction Layer 7 | ============================================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | 14 | twx/index 15 | 16 | 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | TWX: Abstraction Layer Over Telegram's Bot API and MTProto Chat Potocols 3 | ######################################################################## 4 | 5 | :contributions: Please join https://github.com/datamachine/twx 6 | :issues: Please use https://github.com/datamachine/twx/issues 7 | :Python version supported: 3.4 8 | 9 | **TWX** is a python interface for the Telegram bot API. It supports 10 | making synchronous and asynchronous calls and converts the response 11 | into a usable native python object. 12 | 13 | Support for the MTProto API is in the works, but considered pre-alpha right now. 14 | 15 | ======= 16 | Install 17 | ======= 18 | 19 | For stable: 20 | 21 | ``pip install twx`` 22 | 23 | For dev: 24 | 25 | ``pip install -i https://testpypi.python.org/pypi twx`` 26 | 27 | 28 | .. note:: 29 | 30 | If you are only looking for the Telegram Bot API Client, it can be be found at: https://github.com/datamachine/twx.botapi 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # virtual environments 9 | .venv* 10 | 11 | # OSX Stuff 12 | .DS_Store 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | 4 | revision = None 5 | 6 | # must match PEP 440 7 | _MAJOR_VERSION = 0 8 | _MINOR_VERSION = 5 9 | _MICRO_VERSION = None 10 | _PRE_RELEASE_TYPE = 'a' # a | b | rc 11 | _PRE_RELEASE_VERSION = 5 12 | _DEV_RELEASE_VERSION = None 13 | 14 | version = '{}.{}'.format(_MAJOR_VERSION, _MINOR_VERSION) 15 | revision = None 16 | 17 | if _MICRO_VERSION is not None: 18 | version += '.{}'.format(_MICRO_VERSION) 19 | 20 | if _PRE_RELEASE_TYPE is not None and _PRE_RELEASE_VERSION is not None: 21 | version += '{}{}'.format(_PRE_RELEASE_TYPE, _PRE_RELEASE_VERSION) 22 | 23 | if _DEV_RELEASE_VERSION is not None: 24 | version += '.dev{}'.format(_DEV_RELEASE_VERSION) 25 | revision = 'master' 26 | else: 27 | revision = version 28 | 29 | download_url = 'https://github.com/datamachine/twx/archive/{}.tar.gz'.format(revision) 30 | 31 | print(version) 32 | print(download_url) 33 | 34 | setup( 35 | name = 'twx', 36 | packages = ['twx'], 37 | version = version, 38 | description = "Abstraction Layer Over Telegram's Bot API and MTProto Chat Potocols", 39 | long_description = open("README.rst").read(), 40 | author = 'Vince Castellano, Phillip Lopo', 41 | author_email = 'surye80@gmail.com, philliplopo@gmail.com', 42 | keywords = ['datamachine', 'telex', 'telegram', 'bot', 'api', 'rpc'], 43 | url = 'https://github.com/datamachine/twx', 44 | download_url = download_url, 45 | install_requires=['requests', 'twx.botapi'], 46 | platforms = ['Linux', 'Unix', 'MacOsX', 'Windows'], 47 | classifiers = [ 48 | 'Development Status :: 4 - Beta', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: MIT License', 51 | 'Operating System :: OS Independent', 52 | 'Programming Language :: Python :: 3 :: Only', 53 | 'Programming Language :: Python :: 3.4', 54 | 'Topic :: Communications :: Chat', 55 | 'Topic :: Communications :: File Sharing' 56 | ] 57 | ) 58 | -------------------------------------------------------------------------------- /twx/mtproto/crypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Sammy Pfeiffer 3 | # Author: Anton Grigoryev 4 | # This file implements the AES 256 IGE cipher 5 | # working in Python 2.7 and Python 3.4 (other versions untested) 6 | # as it's needed for the implementation of Telegram API 7 | # It's based on PyCryto 8 | 9 | from __future__ import print_function 10 | from Crypto.Util.strxor import strxor 11 | from Crypto.Cipher import AES 12 | 13 | # AES 256 IGE part 14 | 15 | def ige_encrypt(message, key, iv): 16 | return _ige(message, key, iv, operation="encrypt") 17 | 18 | def ige_decrypt(message, key, iv): 19 | return _ige(message, key, iv, operation="decrypt") 20 | 21 | def _ige(message, key, iv, operation="decrypt"): 22 | """Given a key, given an iv, and message 23 | do whatever operation asked in the operation field. 24 | Operation will be checked for: "decrypt" and "encrypt" strings. 25 | Returns the message encrypted/decrypted. 26 | message must be a multiple by 16 bytes (for division in 16 byte blocks) 27 | key must be 32 byte 28 | iv must be 32 byte (it's not internally used in AES 256 ECB, but it's 29 | needed for IGE)""" 30 | message = bytes(message) 31 | if len(key) != 32: 32 | raise ValueError("key must be 32 bytes long (was " + 33 | str(len(key)) + " bytes)") 34 | if len(iv) != 32: 35 | raise ValueError("iv must be 32 bytes long (was " + 36 | str(len(iv)) + " bytes)") 37 | 38 | cipher = AES.new(key, AES.MODE_ECB, iv) 39 | blocksize = cipher.block_size 40 | 41 | if len(message) % blocksize != 0: 42 | raise ValueError("message must be a multiple of 16 bytes (try adding " + 43 | str(16 - len(message) % 16) + " bytes of padding)") 44 | 45 | ivp = iv[0:blocksize] 46 | ivp2 = iv[blocksize:] 47 | 48 | ciphered = bytes() 49 | 50 | for i in range(0, len(message), blocksize): 51 | indata = message[i:i+blocksize] 52 | if operation == "decrypt": 53 | xored = strxor(indata, ivp2) 54 | decrypt_xored = cipher.decrypt(xored) 55 | outdata = strxor(decrypt_xored, ivp) 56 | ivp = indata 57 | ivp2 = outdata 58 | elif operation == "encrypt": 59 | xored = strxor(indata, ivp) 60 | encrypt_xored = cipher.encrypt(xored) 61 | outdata = strxor(encrypt_xored, ivp2) 62 | ivp = outdata 63 | ivp2 = indata 64 | else: 65 | raise ValueError("operation must be either 'decrypt' or 'encrypt'") 66 | ciphered += outdata 67 | return ciphered -------------------------------------------------------------------------------- /docs/twx/botapi/botapi.rst: -------------------------------------------------------------------------------- 1 | :mod:`twx.botapi` --- Unofficial Telegram Bot API Client 2 | ======================================================== 3 | 4 | .. automodule:: twx.botapi 5 | 6 | .. py:currentmodule:: twx.botapi 7 | 8 | Telegram Bot API Types 9 | ---------------------- 10 | 11 | User 12 | ^^^^ 13 | 14 | .. autoclass:: User 15 | 16 | GroupChat 17 | ^^^^^^^^^ 18 | 19 | .. autoclass:: GroupChat 20 | 21 | Message 22 | ^^^^^^^ 23 | 24 | .. autoclass:: Message 25 | 26 | PhotoSize 27 | ^^^^^^^^^ 28 | 29 | .. autoclass:: PhotoSize 30 | 31 | Audio 32 | ^^^^^ 33 | 34 | .. autoclass:: Audio 35 | 36 | Document 37 | ^^^^^^^^ 38 | 39 | .. autoclass:: Document 40 | 41 | Sticker 42 | ^^^^^^^ 43 | 44 | .. autoclass:: Sticker 45 | 46 | Video 47 | ^^^^^ 48 | 49 | .. autoclass:: Video 50 | 51 | Contact 52 | ^^^^^^^ 53 | 54 | .. autoclass:: Contact 55 | 56 | Location 57 | ^^^^^^^^ 58 | 59 | .. autoclass:: Location 60 | 61 | Update 62 | ^^^^^^ 63 | 64 | .. autoclass:: Update 65 | 66 | InputFile 67 | ^^^^^^^^^ 68 | 69 | .. autoclass:: InputFile 70 | 71 | UserProfilePhotos 72 | ^^^^^^^^^^^^^^^^^ 73 | 74 | .. autoclass:: UserProfilePhotos 75 | 76 | ReplyKeyboardMarkup 77 | ^^^^^^^^^^^^^^^^^^^ 78 | 79 | .. autoclass:: ReplyKeyboardMarkup 80 | 81 | ReplyKeyboardHide 82 | ^^^^^^^^^^^^^^^^^ 83 | 84 | .. autoclass:: ReplyKeyboardHide 85 | 86 | ForceReply 87 | ^^^^^^^^^^ 88 | 89 | .. autoclass:: ForceReply 90 | 91 | 92 | Additional Types 93 | ---------------- 94 | 95 | Error 96 | ^^^^^ 97 | 98 | .. autoclass:: Error 99 | 100 | Telegram Bot API Methods 101 | ------------------------ 102 | 103 | get_me 104 | ^^^^^^ 105 | 106 | .. autofunction:: get_me(*, request_args=None, **kwargs) 107 | 108 | send_message 109 | ^^^^^^^^^^^^ 110 | 111 | .. autofunction:: send_message(chat_id: int, text: str, disable_web_page_preview: bool=None, reply_to_message_id: int=None, reply_markup: ReplyMarkup=None, *, request_args=None, **kwargs) 112 | 113 | forward_message 114 | ^^^^^^^^^^^^^^^ 115 | 116 | .. autofunction:: forward_message(chat_id, from_chat_id, message_id, *, request_args=None, **kwargs) 117 | 118 | send_photo 119 | ^^^^^^^^^^ 120 | 121 | .. autofunction:: send_photo(chat_id: int, photo: InputFile, caption: str=None, reply_to_message_id: int=None, reply_markup: ReplyMarkup=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 122 | 123 | send_audio 124 | ^^^^^^^^^^ 125 | 126 | .. autofunction:: send_audio(chat_id: int, audio: InputFile, reply_to_message_id: int=None, reply_markup: ReplyKeyboardMarkup=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 127 | 128 | send_document 129 | ^^^^^^^^^^^^^ 130 | 131 | .. autofunction:: send_document(chat_id: int, document: InputFile, reply_to_message_id: int=None, reply_markup: ReplyKeyboardMarkup=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 132 | 133 | send_sticker 134 | ^^^^^^^^^^^^ 135 | 136 | .. autofunction:: send_sticker(chat_id: int, sticker: InputFile, reply_to_message_id: int=None, reply_markup: ReplyKeyboardMarkup=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 137 | 138 | send_video 139 | ^^^^^^^^^^ 140 | 141 | .. autofunction:: send_video(chat_id: int, video: InputFile, reply_to_message_id: int=None, reply_markup: ReplyKeyboardMarkup=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 142 | 143 | send_location 144 | ^^^^^^^^^^^^^ 145 | 146 | .. autofunction:: send_location(chat_id: int, latitude: float, longitude: float, reply_to_message_id: int=None, reply_markup: ReplyKeyboardMarkup=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 147 | 148 | send_chat_action 149 | ^^^^^^^^^^^^^^^^ 150 | 151 | .. autofunction:: send_chat_action(chat_id: int, action: ChatAction, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 152 | 153 | get_user_profile_photos 154 | ^^^^^^^^^^^^^^^^^^^^^^^ 155 | 156 | .. autofunction:: get_user_profile_photos(user_id: int, offset: int=None, limit: int=None, *, request_args: dict=None, **kwargs) 157 | 158 | get_updates 159 | ^^^^^^^^^^^ 160 | 161 | .. autofunction:: get_updates(offset: int=None, limit: int=None, timeout: int=None, *, request_args, **kwargs) 162 | 163 | set_webhook 164 | ^^^^^^^^^^^ 165 | 166 | .. autofunction:: set_webhook(url: str=None, *, request_args=None, **kwargs) -> TelegramBotRPCRequest 167 | -------------------------------------------------------------------------------- /twx/mtproto/prime.py: -------------------------------------------------------------------------------- 1 | # NOTICE!!! This is copied from https://stackoverflow.com/questions/4643647/fast-prime-factorization-module 2 | 3 | import random 4 | 5 | def primesbelow(N): 6 | # http://stackoverflow.com/questions/2068372/fastest-way-to-list-all-primes-below-n-in-python/3035188#3035188 7 | #""" Input N>=6, Returns a list of primes, 2 <= p < N """ 8 | correction = N % 6 > 1 9 | N = {0:N, 1:N-1, 2:N+4, 3:N+3, 4:N+2, 5:N+1}[N%6] 10 | sieve = [True] * (N // 3) 11 | sieve[0] = False 12 | for i in range(int(N ** .5) // 3 + 1): 13 | if sieve[i]: 14 | k = (3 * i + 1) | 1 15 | sieve[k*k // 3::2*k] = [False] * ((N//6 - (k*k)//6 - 1)//k + 1) 16 | sieve[(k*k + 4*k - 2*k*(i%2)) // 3::2*k] = [False] * ((N // 6 - (k*k + 4*k - 2*k*(i%2))//6 - 1) // k + 1) 17 | return [2, 3] + [(3 * i + 1) | 1 for i in range(1, N//3 - correction) if sieve[i]] 18 | 19 | smallprimeset = set(primesbelow(100000)) 20 | _smallprimeset = 100000 21 | 22 | def isprime(n, precision=7): 23 | # http://en.wikipedia.org/wiki/Miller-Rabin_primality_test#Algorithm_and_running_time 24 | if n == 1 or n % 2 == 0: 25 | return False 26 | elif n < 1: 27 | raise ValueError("Out of bounds, first argument must be > 0") 28 | elif n < _smallprimeset: 29 | return n in smallprimeset 30 | 31 | 32 | d = n - 1 33 | s = 0 34 | while d % 2 == 0: 35 | d //= 2 36 | s += 1 37 | 38 | for repeat in range(precision): 39 | a = random.randrange(2, n - 2) 40 | x = pow(a, d, n) 41 | 42 | if x == 1 or x == n - 1: continue 43 | 44 | for r in range(s - 1): 45 | x = pow(x, 2, n) 46 | if x == 1: return False 47 | if x == n - 1: break 48 | else: return False 49 | 50 | return True 51 | 52 | # https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/ 53 | def pollard_brent(n): 54 | if n % 2 == 0: return 2 55 | if n % 3 == 0: return 3 56 | 57 | y, c, m = random.randint(1, n-1), random.randint(1, n-1), random.randint(1, n-1) 58 | g, r, q = 1, 1, 1 59 | while g == 1: 60 | x = y 61 | for i in range(r): 62 | y = (pow(y, 2, n) + c) % n 63 | 64 | k = 0 65 | while k < r and g==1: 66 | ys = y 67 | for i in range(min(m, r-k)): 68 | y = (pow(y, 2, n) + c) % n 69 | q = q * abs(x-y) % n 70 | g = gcd(q, n) 71 | k += m 72 | r *= 2 73 | if g == n: 74 | while True: 75 | ys = (pow(ys, 2, n) + c) % n 76 | g = gcd(abs(x - ys), n) 77 | if g > 1: 78 | break 79 | 80 | return g 81 | 82 | smallprimes = primesbelow(10000) # might seem low, but 1000*1000 = 1000000, so this will fully factor every composite < 1000000 83 | def primefactors(n, sort=False): 84 | factors = [] 85 | 86 | limit = int(n ** .5) + 1 87 | for checker in smallprimes: 88 | if checker > limit: break 89 | while n % checker == 0: 90 | factors.append(checker) 91 | n //= checker 92 | limit = int(n ** .5) + 1 93 | if checker > limit: break 94 | 95 | if n < 2: return factors 96 | 97 | while n > 1: 98 | if isprime(n): 99 | factors.append(n) 100 | break 101 | factor = pollard_brent(n) # trial division did not fully factor, switch to pollard-brent 102 | factors.extend(primefactors(factor)) # recurse to factor the not necessarily prime factor returned by pollard-brent 103 | n //= factor 104 | 105 | if sort: factors.sort() 106 | 107 | return factors 108 | 109 | def factorization(n): 110 | factors = {} 111 | for p1 in primefactors(n): 112 | try: 113 | factors[p1] += 1 114 | except KeyError: 115 | factors[p1] = 1 116 | return factors 117 | 118 | totients = {} 119 | def totient(n): 120 | if n == 0: return 1 121 | 122 | try: return totients[n] 123 | except KeyError: pass 124 | 125 | tot = 1 126 | for p, exp in factorization(n).items(): 127 | tot *= (p - 1) * p ** (exp - 1) 128 | 129 | totients[n] = tot 130 | return tot 131 | 132 | def gcd(a, b): 133 | if a == b: return a 134 | while b > 0: a, b = b, a % b 135 | return a 136 | 137 | def lcm(a, b): 138 | return abs(a * b) // gcd(a, b) 139 | 140 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TWX.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TWX.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/TWX" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TWX" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\TWX.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\TWX.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # TWX documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jun 27 15:07:02 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.ifconfig', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx.ext.napoleon' 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'TWX' 58 | copyright = '2015, Vince Castellano, Phillip Lopo' 59 | author = 'Vince Castellano, Phillip Lopo' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '1.0' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '1.0b3' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | #today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | #today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ['_build'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | #default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | #add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | #add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | #show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | #modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | #keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = True 113 | 114 | 115 | # -- Options for HTML output ---------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. See the documentation for 118 | # a list of builtin themes. 119 | html_theme = 'alabaster' 120 | 121 | # Theme options are theme-specific and customize the look and feel of a theme 122 | # further. For a list of options available for each theme, see the 123 | # documentation. 124 | #html_theme_options = {} 125 | html_theme_options = { 126 | 'github_user': 'datamachine', 127 | 'github_repo': 'twx', 128 | 'description': 'Telegram Bot API and MTProto Clients', 129 | 'github_banner': True, 130 | 'github_button': True, 131 | 'show_powered_by': False, 132 | #'link': '#0088cc', 133 | #'sidebar_link': '#0088cc', 134 | #'anchor': '#0088cc', 135 | 'gray_1': '#0088cc', 136 | 'gray_2': '#ecf3f8', 137 | #'gray_3': '#0088cc', 138 | #'pre_bg': '#ecf3f8', 139 | #'font_family': "'Lucida Grande', 'Lucida Sans Unicode', Arial, Helvetica, Verdana, sans-serif", 140 | #'head_font_family': "'Lucida Grande', 'Lucida Sans Unicode', Arial, Helvetica, Verdana, sans-serif" 141 | } 142 | 143 | # Add any paths that contain custom themes here, relative to this directory. 144 | #html_theme_path = [] 145 | 146 | # The name for this set of Sphinx documents. If None, it defaults to 147 | # " v documentation". 148 | #html_title = None 149 | 150 | # A shorter title for the navigation bar. Default is the same as html_title. 151 | #html_short_title = None 152 | 153 | # The name of an image file (relative to this directory) to place at the top 154 | # of the sidebar. 155 | #html_logo = None 156 | 157 | # The name of an image file (within the static path) to use as favicon of the 158 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 159 | # pixels large. 160 | #html_favicon = None 161 | 162 | # Add any paths that contain custom static files (such as style sheets) here, 163 | # relative to this directory. They are copied after the builtin static files, 164 | # so a file named "default.css" will overwrite the builtin "default.css". 165 | html_static_path = ['_static'] 166 | 167 | # Add any extra paths that contain custom files (such as robots.txt or 168 | # .htaccess) here, relative to this directory. These files are copied 169 | # directly to the root of the documentation. 170 | #html_extra_path = [] 171 | 172 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 173 | # using the given strftime format. 174 | #html_last_updated_fmt = '%b %d, %Y' 175 | 176 | # If true, SmartyPants will be used to convert quotes and dashes to 177 | # typographically correct entities. 178 | #html_use_smartypants = True 179 | 180 | # Custom sidebar templates, maps document names to template names. 181 | #html_sidebars = {} 182 | 183 | # Additional templates that should be rendered to pages, maps page names to 184 | # template names. 185 | #html_additional_pages = {} 186 | 187 | # If false, no module index is generated. 188 | #html_domain_indices = True 189 | 190 | # If false, no index is generated. 191 | #html_use_index = True 192 | 193 | # If true, the index is split into individual pages for each letter. 194 | #html_split_index = False 195 | 196 | # If true, links to the reST sources are added to the pages. 197 | #html_show_sourcelink = True 198 | 199 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 200 | #html_show_sphinx = True 201 | 202 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 203 | #html_show_copyright = True 204 | 205 | # If true, an OpenSearch description file will be output, and all pages will 206 | # contain a tag referring to it. The value of this option must be the 207 | # base URL from which the finished HTML is served. 208 | #html_use_opensearch = '' 209 | 210 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 211 | #html_file_suffix = None 212 | 213 | # Language to be used for generating the HTML full-text search index. 214 | # Sphinx supports the following languages: 215 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 216 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 217 | #html_search_language = 'en' 218 | 219 | # A dictionary with options for the search language support, empty by default. 220 | # Now only 'ja' uses this config value 221 | #html_search_options = {'type': 'default'} 222 | 223 | # The name of a javascript file (relative to the configuration directory) that 224 | # implements a search results scorer. If empty, the default will be used. 225 | #html_search_scorer = 'scorer.js' 226 | 227 | # Output file base name for HTML help builder. 228 | htmlhelp_basename = 'TWXdoc' 229 | 230 | # -- Options for LaTeX output --------------------------------------------- 231 | 232 | latex_elements = { 233 | # The paper size ('letterpaper' or 'a4paper'). 234 | #'papersize': 'letterpaper', 235 | 236 | # The font size ('10pt', '11pt' or '12pt'). 237 | #'pointsize': '10pt', 238 | 239 | # Additional stuff for the LaTeX preamble. 240 | #'preamble': '', 241 | 242 | # Latex figure (float) alignment 243 | #'figure_align': 'htbp', 244 | } 245 | 246 | # Grouping the document tree into LaTeX files. List of tuples 247 | # (source start file, target name, title, 248 | # author, documentclass [howto, manual, or own class]). 249 | latex_documents = [ 250 | (master_doc, 'TWX.tex', 'TWX Documentation', 251 | 'Vince Castellano, Phillip Lopo', 'manual'), 252 | ] 253 | 254 | # The name of an image file (relative to this directory) to place at the top of 255 | # the title page. 256 | #latex_logo = None 257 | 258 | # For "manual" documents, if this is true, then toplevel headings are parts, 259 | # not chapters. 260 | #latex_use_parts = False 261 | 262 | # If true, show page references after internal links. 263 | #latex_show_pagerefs = False 264 | 265 | # If true, show URL addresses after external links. 266 | #latex_show_urls = False 267 | 268 | # Documents to append as an appendix to all manuals. 269 | #latex_appendices = [] 270 | 271 | # If false, no module index is generated. 272 | #latex_domain_indices = True 273 | 274 | 275 | # -- Options for manual page output --------------------------------------- 276 | 277 | # One entry per manual page. List of tuples 278 | # (source start file, name, description, authors, manual section). 279 | man_pages = [ 280 | (master_doc, 'twx', 'TWX Documentation', 281 | [author], 1) 282 | ] 283 | 284 | # If true, show URL addresses after external links. 285 | #man_show_urls = False 286 | 287 | 288 | # -- Options for Texinfo output ------------------------------------------- 289 | 290 | # Grouping the document tree into Texinfo files. List of tuples 291 | # (source start file, target name, title, author, 292 | # dir menu entry, description, category) 293 | texinfo_documents = [ 294 | (master_doc, 'TWX', 'TWX Documentation', 295 | author, 'TWX', 'One line description of project.', 296 | 'Miscellaneous'), 297 | ] 298 | 299 | # Documents to append as an appendix to all manuals. 300 | #texinfo_appendices = [] 301 | 302 | # If false, no module index is generated. 303 | #texinfo_domain_indices = True 304 | 305 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 306 | #texinfo_show_urls = 'footnote' 307 | 308 | # If true, do not generate a @detailmenu in the "Top" node's menu. 309 | #texinfo_no_detailmenu = False 310 | 311 | 312 | # Example configuration for intersphinx: refer to the Python standard library. 313 | intersphinx_mapping = {'https://docs.python.org/': None} 314 | -------------------------------------------------------------------------------- /twx/mtproto/mtproto.py: -------------------------------------------------------------------------------- 1 | import os 2 | from socket import socket 3 | import struct 4 | from binascii import crc32 as original_crc32 5 | from time import time 6 | 7 | from Crypto.Hash import SHA 8 | from Crypto.PublicKey import RSA 9 | from Crypto.Util.strxor import strxor 10 | from Crypto.Util.number import long_to_bytes, bytes_to_long 11 | 12 | from twx.mtproto import rpc 13 | from twx.mtproto import crypt 14 | from twx.mtproto import prime 15 | 16 | from hexdump import hexdump 17 | 18 | 19 | def crc32(data): 20 | return original_crc32(data) & 0xffffffff 21 | 22 | class MTProto: 23 | def __init__(self, api_secret, api_id): 24 | self.api_secret = api_secret 25 | self.api_id = api_id 26 | self.dc = Datacenter(0, Datacenter.DCs_test[1], 443) 27 | 28 | class Datacenter: 29 | DATA_VERSION = 4 30 | 31 | DCs = [ 32 | "149.154.175.50", 33 | "149.154.167.51", 34 | "149.154.175.100", 35 | "149.154.167.91", 36 | "149.154.171.5", 37 | ] 38 | 39 | DCs_ipv6 = [ 40 | "2001:b28:f23d:f001::a", 41 | "2001:67c:4e8:f002::a", 42 | "2001:b28:f23d:f003::a", 43 | "2001:67c:4e8:f004::a", 44 | "2001:b28:f23f:f005::a", 45 | ] 46 | 47 | DCs_test = [ 48 | "149.154.175.10", 49 | "149.154.167.40", 50 | "149.154.175.117", 51 | ] 52 | 53 | DCs_test_ipv6 = [ 54 | "2001:b28:f23d:f001::e", 55 | "2001:67c:4e8:f002::e", 56 | "2001:b28:f23d:f003::e", 57 | ] 58 | 59 | def __init__(self, dc_id, ipaddr, port): 60 | self.ipaddr = ipaddr 61 | self.port = port 62 | self.datacenter_id = dc_id 63 | self.auth_server_salt_set = [] 64 | self.sock = socket() 65 | self.sock.connect((ipaddr, port)) 66 | self.sock.settimeout(5.0) 67 | self.message_queue = [] 68 | 69 | self.last_msg_id = 0 70 | self.timedelta = 0 71 | self.number = 0 72 | 73 | self.authorized = False 74 | self.auth_key = None 75 | self.auth_key_id = None 76 | self.server_salt = None 77 | self.server_time = None 78 | 79 | self.MAX_RETRY = 5 80 | self.AUTH_MAX_RETRY = 5 81 | 82 | # TODO: Pass this in 83 | self.rsa_key = """-----BEGIN RSA PUBLIC KEY----- 84 | MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 85 | lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS 86 | an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw 87 | Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+ 88 | 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n 89 | Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 90 | -----END RSA PUBLIC KEY-----""" 91 | 92 | # Handshake 93 | self.create_auth_key() 94 | print(self.auth_key) 95 | 96 | def create_auth_key(self): 97 | rand_nonce = os.urandom(16) 98 | req_pq = rpc.req_pq(rand_nonce).get_bytes() 99 | self.send_message(req_pq) 100 | resPQ = rpc.resPQ(self.recv_message()) 101 | assert rand_nonce == resPQ.nonce 102 | 103 | public_key_fingerprint = resPQ.server_public_key_fingerprints[0] 104 | pq = bytes_to_long(resPQ.pq) 105 | 106 | [p, q] = prime.primefactors(pq) 107 | (p, q) = (q, p) if p > q else (p, q) 108 | assert p*q == pq and p < q 109 | 110 | print("Factorization %d = %d * %d" % (pq, p, q)) 111 | 112 | p_bytes = long_to_bytes(p) 113 | q_bytes = long_to_bytes(q) 114 | key = RSA.importKey(self.rsa_key) 115 | new_nonce = os.urandom(32) 116 | 117 | p_q_inner_data = rpc.p_q_inner_data(pq=resPQ.pq, p=p_bytes, q=q_bytes, 118 | server_nonce=resPQ.server_nonce, 119 | nonce=resPQ.nonce, 120 | new_nonce=new_nonce) 121 | 122 | data = p_q_inner_data.get_bytes() 123 | assert p_q_inner_data.nonce == resPQ.nonce 124 | 125 | sha_digest = SHA.new(data).digest() 126 | random_bytes = os.urandom(255-len(data)-len(sha_digest)) 127 | to_encrypt = sha_digest + data + random_bytes 128 | encrypted_data = key.encrypt(to_encrypt, 0)[0] 129 | 130 | req_DH_params = rpc.req_DH_params(p=p_bytes, q=q_bytes, 131 | nonce=resPQ.nonce, 132 | server_nonce=resPQ.server_nonce, 133 | public_key_fingerprint=public_key_fingerprint, 134 | encrypted_data=encrypted_data) 135 | data = req_DH_params.get_bytes() 136 | 137 | self.send_message(data) 138 | data = self.recv_message(debug=False) 139 | 140 | server_DH_params = rpc.server_DH_params(data) 141 | assert resPQ.nonce == server_DH_params.nonce 142 | assert resPQ.server_nonce == server_DH_params.server_nonce 143 | 144 | encrypted_answer = server_DH_params.encrypted_answer 145 | 146 | tmp_aes_key = SHA.new(new_nonce + resPQ.server_nonce).digest() + SHA.new(resPQ.server_nonce + new_nonce).digest()[0:12] 147 | tmp_aes_iv = SHA.new(resPQ.server_nonce + new_nonce).digest()[12:20] + SHA.new(new_nonce + new_nonce).digest() + new_nonce[0:4] 148 | 149 | answer_with_hash = crypt.ige_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) 150 | 151 | answer_hash = answer_with_hash[:20] 152 | answer = answer_with_hash[20:] 153 | 154 | server_DH_inner_data = rpc.server_DH_inner_data(answer) 155 | assert resPQ.nonce == server_DH_inner_data.nonce 156 | assert resPQ.server_nonce == server_DH_inner_data.server_nonce 157 | 158 | dh_prime_str = server_DH_inner_data.dh_prime 159 | g = server_DH_inner_data.g 160 | g_a_str = server_DH_inner_data.g_a 161 | server_time = server_DH_inner_data.server_time 162 | self.timedelta = server_time - time() 163 | 164 | dh_prime = bytes_to_long(dh_prime_str) 165 | g_a = bytes_to_long(g_a_str) 166 | 167 | assert prime.isprime(dh_prime) 168 | retry_id = 0 169 | b_str = os.urandom(256) 170 | b = bytes_to_long(b_str) 171 | g_b = pow(g, b, dh_prime) 172 | 173 | g_b_str = long_to_bytes(g_b) 174 | 175 | client_DH_inner_data = rpc.client_DH_inner_data(nonce=resPQ.nonce, 176 | server_nonce=resPQ.server_nonce, 177 | retry_id=retry_id, 178 | g_b=g_b_str) 179 | 180 | data = client_DH_inner_data.get_bytes() 181 | 182 | data_with_sha = SHA.new(data).digest()+data 183 | data_with_sha_padded = data_with_sha + os.urandom(-len(data_with_sha) % 16) 184 | encrypted_data = crypt.ige_encrypt(data_with_sha_padded, tmp_aes_key, tmp_aes_iv) 185 | 186 | for i in range(1, self.AUTH_MAX_RETRY): # retry when dh_gen_retry or dh_gen_fail 187 | set_client_DH_params = rpc.set_client_DH_params(nonce=resPQ.nonce, 188 | server_nonce=resPQ.server_nonce, 189 | encrypted_data=encrypted_data) 190 | self.send_message(set_client_DH_params.get_bytes()) 191 | Set_client_DH_params_answer = rpc.set_client_DH_params_answer(self.recv_message()) 192 | 193 | # print Set_client_DH_params_answer 194 | auth_key = pow(g_a, b, dh_prime) 195 | auth_key_str = long_to_bytes(auth_key) 196 | auth_key_sha = SHA.new(auth_key_str).digest() 197 | auth_key_aux_hash = auth_key_sha[:8] 198 | 199 | new_nonce_hash1 = SHA.new(new_nonce+b'\x01'+auth_key_aux_hash).digest()[-16:] 200 | new_nonce_hash2 = SHA.new(new_nonce+b'\x02'+auth_key_aux_hash).digest()[-16:] 201 | new_nonce_hash3 = SHA.new(new_nonce+b'\x03'+auth_key_aux_hash).digest()[-16:] 202 | 203 | assert Set_client_DH_params_answer.nonce == resPQ.nonce 204 | assert Set_client_DH_params_answer.server_nonce == resPQ.server_nonce 205 | 206 | if Set_client_DH_params_answer.status == 'ok': 207 | assert Set_client_DH_params_answer.new_nonce_hash == new_nonce_hash1 208 | print("Diffie Hellman key exchange processed successfully") 209 | 210 | self.server_salt = strxor(new_nonce[0:8], resPQ.server_nonce[0:8]) 211 | self.auth_key = auth_key_str 212 | self.auth_key_id = auth_key_sha[-8:] 213 | print("Auth key generated") 214 | return "Auth Ok" 215 | elif Set_client_DH_params_answer.status == 'retry': 216 | assert Set_client_DH_params_answer.new_nonce_hash == new_nonce_hash2 217 | print ("Retry Auth") 218 | elif Set_client_DH_params_answer.status == 'fail': 219 | assert Set_client_DH_params_answer.new_nonce_hash == new_nonce_hash3 220 | print("Auth Failed") 221 | raise Exception("Auth Failed") 222 | else: 223 | raise Exception("Response Error") 224 | 225 | def generate_message_id(self): 226 | msg_id = int(time() * 2**32) 227 | if self.last_msg_id > msg_id: 228 | msg_id = self.last_msg_id + 1 229 | while msg_id % 4 is not 0: 230 | msg_id += 1 231 | 232 | return msg_id 233 | 234 | def send_message(self, message_data): 235 | message_id = self.generate_message_id() 236 | message = (b'\x00\x00\x00\x00\x00\x00\x00\x00' + 237 | struct.pack('iissss", initConnection.constructor, self.api_id, 41 | self.device_model, self.system_version, 42 | self.app_version, self.lang_code) + self.query.get_bytes() 43 | 44 | def set_params(self, data): 45 | (self.api_id, self.device_model, 46 | self.system_version, self.app_version, 47 | self.lang_code) = struct.unpack(">issss", data) 48 | 49 | class invokeWithLayer(TLObject): 50 | constructor = 0xda9b0d0d 51 | 52 | def __init__(self): 53 | self.layer = 27 54 | self.query = None 55 | 56 | def get_bytes(self): 57 | return struct.pack(">ii", invokeWithLayer.constructor, self.layer) + self.query.get_bytes() 58 | 59 | class checkPhone(TLObject): 60 | constructor = 0x6fe51dfb 61 | 62 | def __init__(self, phone_number=None): 63 | self.response_class = checkedPhone.__class__ 64 | self.phone_number = phone_number 65 | 66 | def get_bytes(self): 67 | return struct.pack(">is", checkPhone.constructor, self.phone_number) 68 | 69 | def set_params(self, data): 70 | (self.phone_number) = struct.unpack(">s", data) 71 | 72 | class checkedPhone(TLObject): 73 | constructor = 0x811ea28e 74 | 75 | def __init__(self): 76 | self.phone_registered = False 77 | self.phone_invited = False 78 | 79 | def get_bytes(self): 80 | return struct.pack(">i??", checkedPhone.constructor, self.phone_registered, self.phone_invited) 81 | 82 | def set_params(self, data): 83 | (self.phone_registered, self.phone_invited) = struct.unpack(">??", data) 84 | 85 | class req_pq(TLObject): 86 | constructor = 0x60469778 87 | 88 | def __init__(self, nonce): 89 | self.nonce = nonce 90 | 91 | def get_bytes(self): 92 | return struct.pack(" callable: 266 | return self._on_msg_receive(msg) 267 | 268 | @on_msg_receive.setter 269 | def on_msg_receive(self, on_success: callable): 270 | self._on_msg_receive = on_success 271 | 272 | @property 273 | def on_user_update(self) -> callable: 274 | return self._on_user_update 275 | 276 | @on_user_update.setter 277 | def on_user_update(self, on_success): 278 | self._on_user_update = on_success 279 | 280 | @property 281 | def on_chat_update(self) -> callable: 282 | return self._on_chat_update 283 | 284 | @on_chat_update.setter 285 | def on_chat_update(self, on_success): 286 | self._on_chat_update = on_success 287 | 288 | @property 289 | def on_secret_chat_update(self) -> callable: 290 | return self._on_secret_chat_update 291 | 292 | @on_secret_chat_update.setter 293 | def on_secret_chat_update(self, on_success): 294 | self._on_secret_chat_update = on_success 295 | 296 | @abstractmethod 297 | def send_message(self, peer: Peer, text: str, reply: int=None, link_preview: bool=None, on_success: callable=None): 298 | """ 299 | Send message to peer. 300 | :param peer: Peer to send message to. 301 | :param text: Text to send. 302 | :param reply: Message object or message_id to reply to. 303 | :param link_preview: Whether or not to show the link preview for this message 304 | :param on_success: Callback to call when call is complete. 305 | """ 306 | pass 307 | 308 | @abstractmethod 309 | def forward_message(self, peer: Peer, message: Message): 310 | """ 311 | Use this method to forward messages of any kind. 312 | 313 | :param peer: Peer to send forwarded message too. 314 | :param message: Message to be forwarded. 315 | 316 | :returns: On success, the sent Message is returned. 317 | """ 318 | pass 319 | 320 | @abstractmethod 321 | def send_photo(self, peer: Peer, photo: str, caption: str=None, reply: int=None, on_success: callable=None, 322 | reply_markup: botapi.ReplyMarkup=None): 323 | """ 324 | Send photo to peer. 325 | :param peer: Peer to send message to. 326 | :param photo: File path to photo to send. 327 | :param caption: Caption for photo 328 | :param reply: Message object or message_id to reply to. 329 | :param on_success: Callback to call when call is complete. 330 | 331 | :type reply: int or Message 332 | """ 333 | pass 334 | 335 | @abstractmethod 336 | def send_audio(self, peer: Peer, audio: str, reply: int=None, on_success: callable=None, 337 | reply_markup: botapi.ReplyMarkup=None): 338 | """ 339 | Send audio clip to peer. 340 | :param peer: Peer to send message to. 341 | :param audio: File path to audio to send. 342 | :param reply: Message object or message_id to reply to. 343 | :param on_success: Callback to call when call is complete. 344 | 345 | :type reply: int or Message 346 | """ 347 | pass 348 | 349 | @abstractmethod 350 | def send_document(self, peer: Peer, document: str, reply: int=None, on_success: callable=None, 351 | reply_markup: botapi.ReplyMarkup=None): 352 | """ 353 | Send document to peer. 354 | :param peer: Peer to send message to. 355 | :param document: File path to document to send. 356 | :param reply: Message object or message_id to reply to. 357 | :param on_success: Callback to call when call is complete. 358 | 359 | :type reply: int or Message 360 | """ 361 | pass 362 | 363 | @abstractmethod 364 | def send_sticker(self, peer: Peer, sticker: str, reply: int=None, on_success: callable=None, 365 | reply_markup: botapi.ReplyMarkup=None): 366 | """ 367 | Send sticker to peer. 368 | :param peer: Peer to send message to. 369 | :param sticker: File path to sticker to send. 370 | :param reply: Message object or message_id to reply to. 371 | :param on_success: Callback to call when call is complete. 372 | 373 | :type reply: int or Message 374 | """ 375 | pass 376 | 377 | @abstractmethod 378 | def send_video(self, peer: Peer, video: str, reply: int=None, on_success: callable=None, 379 | reply_markup: botapi.ReplyMarkup=None): 380 | """ 381 | Send video to peer. 382 | :param peer: Peer to send message to. 383 | :param video: File path to video to send. 384 | :param reply: Message object or message_id to reply to. 385 | :param on_success: Callback to call when call is complete. 386 | 387 | :type reply: int or Message 388 | """ 389 | pass 390 | 391 | @abstractmethod 392 | def send_location(self, peer: Peer, latitude: float, longitude: float, reply: int=None, on_success: callable=None, 393 | reply_markup: botapi.ReplyMarkup=None): 394 | """ 395 | Send location to peer. 396 | :param peer: Peer to send message to. 397 | :param latitude: Latitude of the location. 398 | :param longitude: Longitude of the location. 399 | :param reply: Message object or message_id to reply to. 400 | :param on_success: Callback to call when call is complete. 401 | 402 | :type reply: int or Message 403 | """ 404 | pass 405 | 406 | @abstractmethod 407 | def send_chat_action(self, peer: Peer, action: botapi.ChatAction, on_success: callable=None): 408 | """ 409 | Send status to peer. 410 | :param peer: Peer to send status to. 411 | :param action: Type of action to send to peer. 412 | :param on_success: Callback to call when call is complete. 413 | 414 | """ 415 | pass 416 | 417 | @abstractmethod 418 | def get_user_profile_photos(self, peer: Peer, on_success: callable, offset: int=None, limit: int=None): 419 | pass 420 | 421 | @abstractmethod 422 | def get_contact_list(self, on_success: callable=None): 423 | """ 424 | Retrieve contact list. 425 | :param on_success: Callback to call when server returns of contacts. If not specified, 426 | just load the local version 427 | """ 428 | pass 429 | 430 | @abstractmethod 431 | def add_contact(self, phone_number: str, first_name: str, last_name: str=None, on_success: callable=None): 432 | """ 433 | Add contact by phone number and name (last_name is optional). 434 | :param phone: Valid phone number for contact. 435 | :param first_name: First name to use. 436 | :param last_name: Last name to use. Optional. 437 | :param on_success: Callback to call when adding, will contain success status and the current contact list. 438 | """ 439 | pass 440 | 441 | @abstractmethod 442 | def del_contact(self, peer: Peer, on_success: callable=None): 443 | """ 444 | Delete peer from contact list 445 | :param peer: Peer to delete 446 | :param on_success: Callback to call when deleting, will contain success status and the current contact list. 447 | """ 448 | pass 449 | 450 | @abstractmethod 451 | def message_search(self, text: str, on_success: callable, peer: Peer=None, 452 | min_date: datetime=None, max_date: datetime=None, 453 | max_id: int=None, offset: int=0, limit: int=255): 454 | """ 455 | Search for messages. 456 | :param text: Text to search for in messages 457 | :param on_success: Callback to call containing all the matching messages. 458 | :param peer: Peer to search, if None, search all dialogs. 459 | :param min_date: Start search from this datetime. 460 | :param max_date: Send search at this datetime. 461 | :param max_id: Don't return any messages after this Message or message_id. 462 | :param offset: Number of messages to skip. 463 | :param limit: How many messages to return. 464 | :return: 465 | """ 466 | pass 467 | 468 | @abstractmethod 469 | def set_profile_photo(self, file_path: str, on_success: callable=None): 470 | """ 471 | Sets the profile photo for the bot. 472 | :param file_path: Path to image file 473 | :param on_success: Callback to call with the status 474 | """ 475 | pass 476 | 477 | @abstractmethod 478 | def create_secret_chat(self, user: User, on_success: callable): 479 | """ 480 | Create a secret chat with the user. 481 | :param user: User to start secret chat with. 482 | :param on_success: Will return the chat and meta information. 483 | """ 484 | pass 485 | 486 | @abstractmethod 487 | def create_group_chat(self, user_list: list, name: str, on_success: callable=None): 488 | """ 489 | Create a new group with the specified list of users, must be at least 2 users. 490 | :param user_list: List of Peers. 491 | :param name: Name of group. 492 | :param on_success: 493 | """ 494 | pass 495 | 496 | @abstractmethod 497 | def status_online(self): 498 | """ 499 | Sets bot's status to online. 500 | """ 501 | pass 502 | 503 | @abstractmethod 504 | def status_offline(self): 505 | """ 506 | Sets bot's status to offline. 507 | """ 508 | pass 509 | 510 | 511 | def get_mimetype(file_path): 512 | return mimetypes.guess_type(file_path, strict=False)[0] or 'application/octet-stream' 513 | 514 | class TWXBotApi(TWX): 515 | class UpdateThread(Thread): 516 | def __init__(self, twx): 517 | super().__init__() 518 | self.twx = twx 519 | self.update_offset = 0 520 | 521 | def run(self): 522 | while True: 523 | botapi.get_updates(offset=self.update_offset, timeout=300, 524 | on_success=self.new_updates, **self.twx.request_args).run().wait() 525 | 526 | def new_updates(self, updates): 527 | for update in updates: 528 | self.twx.process_update(update) 529 | self.update_offset = update.update_id + 1 530 | 531 | def __init__(self, token): 532 | super().__init__() 533 | 534 | self._bot_user = botapi.User(None, None, None, None) 535 | self.update_loop = TWXBotApi.UpdateThread(self) 536 | 537 | self.request_args = dict( 538 | token=token, 539 | request_method=botapi.RequestMethod.POST 540 | ) 541 | 542 | self.update_bot_info() 543 | 544 | def start(self): 545 | """ 546 | Starts the long polling update loop. 547 | """ 548 | self.update_loop.start() 549 | 550 | def process_update(self, update: botapi.Update): 551 | try: 552 | print(update) 553 | except Exception: 554 | import sys 555 | print(update.__str__().encode().decode(sys.stdout.encoding)) 556 | msg = update.message 557 | if msg: 558 | if any([msg.text, msg.audio, msg.document, msg.photo, msg.video, msg.sticker, msg.location, msg.contact]): 559 | self._on_msg_receive(msg=self._to_twx_msg(msg)) 560 | 561 | @property 562 | def bot_id(self): 563 | return self._bot_user.id 564 | 565 | def update_bot_info(self): 566 | self._bot_user = botapi.get_me(**self.request_args).run().wait() 567 | 568 | def send_message(self, peer: Peer, text: str, reply: int=None, link_preview: bool=None, 569 | on_success: callable=None, reply_markup: botapi.ReplyMarkup=None): 570 | """ 571 | Send message to peer. 572 | :param peer: Peer to send message to. 573 | :param text: Text to send. 574 | :param reply: Message object or message_id to reply to. 575 | :param link_preview: Whether or not to show the link preview for this message 576 | :param on_success: Callback to call when call is complete. 577 | 578 | :type reply: int or Message 579 | """ 580 | if isinstance(reply, Message): 581 | reply = reply.id 582 | 583 | botapi.send_message(chat_id=peer.id, text=text, disable_web_page_preview=not link_preview, 584 | reply_to_message_id=reply, on_success=on_success, reply_markup=reply_markup, 585 | **self.request_args).run() 586 | 587 | def forward_message(self, peer: Peer, message: Message, on_success: callable=None): 588 | """ 589 | Use this method to forward messages of any kind. 590 | 591 | :param peer: Peer to send forwarded message too. 592 | :param message: Message to be forwarded. 593 | :param on_success: Callback to call when call is complete. 594 | 595 | :returns: On success, the sent Message is returned. 596 | """ 597 | botapi.forward_message(peer.id, message.sender.id, message.id, **self.request_args).run() 598 | 599 | def send_photo(self, peer: Peer, photo: str, caption: str=None, reply: int=None, on_success: callable=None, 600 | reply_markup: botapi.ReplyMarkup=None): 601 | """ 602 | Send photo to peer. 603 | :param peer: Peer to send message to. 604 | :param photo: File path to photo to send. 605 | :param caption: Caption for photo 606 | :param reply: Message object or message_id to reply to. 607 | :param on_success: Callback to call when call is complete. 608 | 609 | :type reply: int or Message 610 | """ 611 | if isinstance(reply, Message): 612 | reply = reply.id 613 | 614 | photo = botapi.InputFile('photo', botapi.InputFileInfo(photo, open(photo, 'rb'), get_mimetype(photo))) 615 | 616 | botapi.send_photo(chat_id=peer.id, photo=photo, caption=caption, reply_to_message_id=reply, on_success=on_success, 617 | reply_markup=reply_markup, **self.request_args).run() 618 | 619 | def send_audio(self, peer: Peer, audio: str, reply: int=None, on_success: callable=None, 620 | reply_markup: botapi.ReplyMarkup=None): 621 | """ 622 | Send audio clip to peer. 623 | :param peer: Peer to send message to. 624 | :param audio: File path to audio to send. 625 | :param reply: Message object or message_id to reply to. 626 | :param on_success: Callback to call when call is complete. 627 | 628 | :type reply: int or Message 629 | """ 630 | if isinstance(reply, Message): 631 | reply = reply.id 632 | 633 | audio = botapi.InputFile('audio', botapi.InputFileInfo(audio, open(audio, 'rb'), get_mimetype(audio))) 634 | 635 | botapi.send_audio(chat_id=peer.id, audio=audio, reply_to_message_id=reply, on_success=on_success, 636 | reply_markup=reply_markup, **self.request_args).run() 637 | 638 | def send_document(self, peer: Peer, document: str, reply: int=None, on_success: callable=None, 639 | reply_markup: botapi.ReplyMarkup=None): 640 | """ 641 | Send document to peer. 642 | :param peer: Peer to send message to. 643 | :param document: File path to document to send. 644 | :param reply: Message object or message_id to reply to. 645 | :param on_success: Callback to call when call is complete. 646 | 647 | :type reply: int or Message 648 | """ 649 | if isinstance(reply, Message): 650 | reply = reply.id 651 | 652 | document = botapi.InputFile('document', botapi.InputFileInfo(document, open(document, 'rb'), 653 | get_mimetype(document))) 654 | 655 | botapi.send_document(chat_id=peer.id, document=document, reply_to_message_id=reply, on_success=on_success, 656 | reply_markup=reply_markup, **self.request_args).run() 657 | 658 | def send_sticker(self, peer: Peer, sticker: str, reply: int=None, on_success: callable=None, 659 | reply_markup: botapi.ReplyMarkup=None): 660 | """ 661 | Send sticker to peer. 662 | :param peer: Peer to send message to. 663 | :param sticker: File path to sticker to send. 664 | :param reply: Message object or message_id to reply to. 665 | :param on_success: Callback to call when call is complete. 666 | 667 | :type reply: int or Message 668 | """ 669 | if isinstance(reply, Message): 670 | reply = reply.id 671 | 672 | sticker = botapi.InputFile('sticker', botapi.InputFileInfo(sticker, open(sticker, 'rb'), 673 | get_mimetype(sticker))) 674 | 675 | botapi.send_sticker(chat_id=peer.id, sticker=sticker, reply_to_message_id=reply, on_success=on_success, 676 | reply_markup=reply_markup, **self.request_args).run() 677 | 678 | def send_video(self, peer: Peer, video: str, reply: int=None, 679 | on_success: callable=None, reply_markup: botapi.ReplyMarkup=None): 680 | """ 681 | Send video to peer. 682 | :param peer: Peer to send message to. 683 | :param video: File path to video to send. 684 | :param reply: Message object or message_id to reply to. 685 | :param on_success: Callback to call when call is complete. 686 | 687 | :type reply: int or Message 688 | """ 689 | if isinstance(reply, Message): 690 | reply = reply.id 691 | 692 | video = botapi.InputFile('video', botapi.InputFileInfo(video, open(video, 'rb'), 693 | get_mimetype(video))) 694 | 695 | botapi.send_video(chat_id=peer.id, video=video, reply_to_message_id=reply, on_success=on_success, 696 | reply_markup=reply_markup, **self.request_args).run() 697 | 698 | def send_location(self, peer: Peer, latitude: float, longitude: float, reply: int=None, 699 | on_success: callable=None, reply_markup: botapi.ReplyMarkup=None): 700 | """ 701 | Send location to peer. 702 | :param peer: Peer to send message to. 703 | :param latitude: Latitude of the location. 704 | :param longitude: Longitude of the location. 705 | :param reply: Message object or message_id to reply to. 706 | :param on_success: Callback to call when call is complete. 707 | 708 | :type reply: int or Message 709 | """ 710 | if isinstance(reply, Message): 711 | reply = reply.id 712 | 713 | botapi.send_location(chat_id=peer.id, latitude=latitude, longitude=longitude, 714 | reply_to_message_id=reply, on_success=on_success, reply_markup=reply_markup, 715 | **self.request_args).run() 716 | 717 | def send_chat_action(self, peer: Peer, action: botapi.ChatAction, on_success: callable=None): 718 | """ 719 | Send status to peer. 720 | :param peer: Peer to send status to. 721 | :param action: Type of action to send to peer. 722 | :param on_success: Callback to call when call is complete. 723 | 724 | """ 725 | botapi.send_chat_action(chat_id=peer.id, action=action, on_success=on_success, **self.request_args).run() 726 | 727 | def get_user_profile_photos(self, user: User, on_success: callable, offset: int=None, limit: int=None): 728 | # """ 729 | # Get user profile photos. 730 | # :param user: User to get profile photos. 731 | # :param on_success: Callback with the requested photos 732 | # :param offset: Sequential number of the first photo to be returned. By default, all photos are returned. 733 | # :param limit: Limits the number of photos to be retrieved. Values between 1—100 are accepted. Defaults to 100. 734 | # """ 735 | 736 | botapi.get_user_profile_photos(user_id=user.id, on_success=on_success, offset=offset, limit=limit, 737 | **self.request_args).run() 738 | 739 | # region Unsupported in botapi 740 | def get_contact_list(self, on_success=None): 741 | """ 742 | Unsupported in the Bot API 743 | """ 744 | raise TWXUnsupportedMethod() 745 | 746 | def add_contact(self, phone, first_name, last_name=None, on_success=None): 747 | """ 748 | Unsupported in the Bot API 749 | """ 750 | raise TWXUnsupportedMethod() 751 | 752 | def del_contact(self, peer, on_success=None): 753 | """ 754 | Unsupported in the Bot API 755 | """ 756 | raise TWXUnsupportedMethod() 757 | 758 | def message_search(self, text, on_success, peer=None, min_date=None, max_date=None, max_id=None, offset=0, limit=255): 759 | """ 760 | Unsupported in the Bot API 761 | """ 762 | raise TWXUnsupportedMethod() 763 | 764 | def set_profile_photo(self, file_path: str, on_success: callable=None): 765 | """ 766 | Unsupported in the Bot API 767 | """ 768 | raise TWXUnsupportedMethod() 769 | 770 | def create_secret_chat(self, user, on_success=None): 771 | """ 772 | Unsupported in the Bot API 773 | """ 774 | raise TWXUnsupportedMethod() 775 | 776 | def create_group_chat(self, user_list, name, on_success=None): 777 | """ 778 | Unsupported in the Bot API 779 | """ 780 | raise TWXUnsupportedMethod() 781 | 782 | def status_online(self): 783 | """ 784 | Unsupported in the Bot API 785 | """ 786 | raise TWXUnsupportedMethod() 787 | 788 | def status_offline(self): 789 | """ 790 | Unsupported in the Bot API 791 | """ 792 | raise TWXUnsupportedMethod() 793 | # endregion 794 | 795 | def _to_twx_msg(self, msg: botapi.Message): 796 | twx_msg = Message() 797 | twx_msg.id = msg.message_id 798 | twx_msg.mention = self._bot_user.username and msg.text and \ 799 | '@{}'.format(self._bot_user.username.lower()) in msg.text.lower() 800 | twx_msg.out = False # BotApi will never include it's own messages. 801 | twx_msg.unread = False # BotApi has no read/unread 802 | twx_msg.service = any([msg.new_chat_member, msg.left_chat_member, msg.new_chat_title, 803 | msg.new_chat_photo, msg.delete_chat_photo, msg.group_chat_created]) 804 | twx_msg.sender = self._to_twx_peer(msg.sender) 805 | twx_msg.receiver = self._to_twx_peer(msg.chat) 806 | twx_msg.text = msg.text 807 | twx_msg.media = {} 808 | twx_msg.date = datetime.fromtimestamp(msg.date) 809 | twx_msg.fwd_src = self._to_twx_peer(msg.forward_from) if msg.forward_from else None 810 | twx_msg.fwd_date = datetime.fromtimestamp(msg.forward_date) if msg.forward_date else None 811 | twx_msg.reply = self._to_twx_msg(msg.reply_to_message) if msg.reply_to_message else None 812 | twx_msg.action = None # TODO: Implement service messages 813 | 814 | return twx_msg 815 | 816 | def _to_twx_peer(self, peer): 817 | user = isinstance(peer, botapi.User) 818 | if user: 819 | twx_peer = User(self, peer.id) 820 | twx_peer.first_name = peer.first_name 821 | twx_peer.last_name = peer.last_name 822 | twx_peer.username = peer.username 823 | else: 824 | twx_peer = Group(self, peer.id) 825 | twx_peer.title = peer.title 826 | 827 | return twx_peer 828 | 829 | 830 | 831 | class TWXUnsupportedMethod(Exception): 832 | pass 833 | 834 | class TWXException(Exception): 835 | pass 836 | 837 | TWX.register(TWXBotApi) 838 | --------------------------------------------------------------------------------