summaryrefslogtreecommitdiffstats
path: root/telepot/api.py
blob: 3ffa788dff7ca0935e8ca6d04b60a1caf8fe5bfa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import urllib3
import logging
import json
import re
import os

from . import exception, _isstring

# Suppress InsecurePlatformWarning
urllib3.disable_warnings()


_default_pool_params = dict(num_pools=3, maxsize=10, retries=3, timeout=30)
_onetime_pool_params = dict(num_pools=1, maxsize=1, retries=3, timeout=30)

_pools = {
    'default': urllib3.PoolManager(**_default_pool_params),
}

_onetime_pool_spec = (urllib3.PoolManager, _onetime_pool_params)


def set_proxy(url, basic_auth=None):
    """
    Access Bot API through a proxy.

    :param url: proxy URL
    :param basic_auth: 2-tuple ``('username', 'password')``
    """
    global _pools, _onetime_pool_spec
    if not url:
        _pools['default'] = urllib3.PoolManager(**_default_pool_params)
        _onetime_pool_spec = (urllib3.PoolManager, _onetime_pool_params)
    elif basic_auth:
        h = urllib3.make_headers(proxy_basic_auth=':'.join(basic_auth))
        _pools['default'] = urllib3.ProxyManager(url, proxy_headers=h, **_default_pool_params)
        _onetime_pool_spec = (urllib3.ProxyManager, dict(proxy_url=url, proxy_headers=h, **_onetime_pool_params))
    else:
        _pools['default'] = urllib3.ProxyManager(url, **_default_pool_params)
        _onetime_pool_spec = (urllib3.ProxyManager, dict(proxy_url=url, **_onetime_pool_params))

def _create_onetime_pool():
    cls, kw = _onetime_pool_spec
    return cls(**kw)

def _methodurl(req, **user_kw):
    token, method, params, files = req
    return 'https://api.telegram.org/bot%s/%s' % (token, method)

def _which_pool(req, **user_kw):
    token, method, params, files = req
    return None if files else 'default'

def _guess_filename(obj):
    name = getattr(obj, 'name', None)
    if name and _isstring(name) and name[0] != '<' and name[-1] != '>':
        return os.path.basename(name)

def _filetuple(key, f):
    if not isinstance(f, tuple):
        return (_guess_filename(f) or key, f.read())
    elif len(f) == 1:
        return (_guess_filename(f[0]) or key, f[0].read())
    elif len(f) == 2:
        return (f[0], f[1].read())
    elif len(f) == 3:
        return (f[0], f[1].read(), f[2])
    else:
        raise ValueError()

import sys
PY_3 = sys.version_info.major >= 3
def _fix_type(v):
    if isinstance(v, float if PY_3 else (long, float)):
        return str(v)
    else:
        return v

def _compose_fields(req, **user_kw):
    token, method, params, files = req

    fields = {k:_fix_type(v) for k,v in params.items()} if params is not None else {}
    if files:
        fields.update({k:_filetuple(k,v) for k,v in files.items()})

    return fields

def _default_timeout(req, **user_kw):
    name = _which_pool(req, **user_kw)
    if name is None:
        return _onetime_pool_spec[1]['timeout']
    else:
        return _pools[name].connection_pool_kw['timeout']

def _compose_kwargs(req, **user_kw):
    token, method, params, files = req
    kw = {}

    if not params and not files:
        kw['encode_multipart'] = False

    if method == 'getUpdates' and params and 'timeout' in params:
        # Ensure HTTP timeout is longer than getUpdates timeout
        kw['timeout'] = params['timeout'] + _default_timeout(req, **user_kw)
    elif files:
        # Disable timeout if uploading files. For some reason, the larger the file,
        # the longer it takes for the server to respond (after upload is finished).
        # It is unclear how long timeout should be.
        kw['timeout'] = None

    # Let user-supplied arguments override
    kw.update(user_kw)
    return kw

def _transform(req, **user_kw):
    kwargs = _compose_kwargs(req, **user_kw)

    fields = _compose_fields(req, **user_kw)

    url = _methodurl(req, **user_kw)

    name = _which_pool(req, **user_kw)

    if name is None:
        pool = _create_onetime_pool()
    else:
        pool = _pools[name]

    return pool.request_encode_body, ('POST', url, fields), kwargs

def _parse(response):
    try:
        text = response.data.decode('utf-8')
        data = json.loads(text)
    except ValueError:  # No JSON object could be decoded
        raise exception.BadHTTPResponse(response.status, text, response)

    if data['ok']:
        return data['result']
    else:
        description, error_code = data['description'], data['error_code']

        # Look for specific error ...
        for e in exception.TelegramError.__subclasses__():
            n = len(e.DESCRIPTION_PATTERNS)
            if any(map(re.search, e.DESCRIPTION_PATTERNS, n*[description], n*[re.IGNORECASE])):
                raise e(description, error_code, data)

        # ... or raise generic error
        raise exception.TelegramError(description, error_code, data)

def request(req, **user_kw):
    fn, args, kwargs = _transform(req, **user_kw)
    r = fn(*args, **kwargs)  # `fn` must be thread-safe
    return _parse(r)

def _fileurl(req):
    token, path = req
    return 'https://api.telegram.org/file/bot%s/%s' % (token, path)

def download(req, **user_kw):
    pool = _create_onetime_pool()
    r = pool.request('GET', _fileurl(req), **user_kw)
    return r