Repository: alexander-akhmetov/python-telegram

Issue:
Random error while decrypting database
Regular class initialization
```python
client = Telegram(
    api_id=self.api_id,
    api_hash=self.api_hash,
    library_path=self.library_path,
    phone='+XXXXXXXXXXXX'',
    database_encryption_key='XXXXXXXXXXX',
    use_test_dc=True,
    files_directory=self.files_directory,
    use_message_database=False,
    login=True,
)
```
Randomly causes an error
```
Telegram error: {'@type': 'error', 'code': 401, 'message': 'Database encryption key is needed: call checkDatabaseEncryptionKey first', '@extra': {'request_id': 'updateAuthorizationState'}}
```

Next time you start everything can work fine

<details><summary>Full log</summary>
<p>

```python
INFO:telegram.tdjson:Using shared library "/app/telegram_data/vendor/tdlib/libtdjson.so"
INFO:telegram.client:[Telegram.td_listener] started
INFO:telegram.worker:[SimpleWorker] started
INFO:telegram.client:[login] Login process has been started
INFO:telegram.client:[login] current authorization state: None
INFO:telegram.client:Setting tdlib initial params: files_dir=/app/telegram_data/data/ test_dc=True
DEBUG:telegram.tdjson:[me ==>] Sent b'{"@type": "setTdlibParameters", "parameters": {"use_test_dc": true, "api_id": API_ID, "api_hash": "API_HASH", "device_model": "python-telegram", "system_version": "unknown", "application_version": "0.7.0", "system_language_code": "en", "database_directory": "/app/telegram_data/data/database", "use_message_database": false, "files_directory": "/app/telegram_data/data/files"}, "@extra": {"request_id": "updateAuthorizationState"}}'
DEBUG:telegram.tdjson:[me <==] Received {'@type': 'updateAuthorizationState', 'authorization_state': {'@type': 'authorizationStateWaitTdlibParameters'}}
DEBUG:telegram.tdjson:[me <==] Received {'@type': 'updateAuthorizationState', 'authorization_state': {'@type': 'authorizationStateWaitEncryptionKey', 'is_encrypted': True}}
DEBUG:telegram.client:async_result has not been found in by request_id=updateAuthorizationState
DEBUG:telegram.tdjson:[me <==] Received {'@type': 'ok', '@extra': {'request_id': 'updateAuthorizationState'}}
DEBUG:telegram.client:async_result has not been found in by request_id=updateAuthorizationState
INFO:telegram.client:[login] current authorization state: authorizationStateWaitTdlibParameters
INFO:telegram.client:Setting tdlib initial params: files_dir=/app/telegram_data/data/ test_dc=True
DEBUG:telegram.tdjson:[me ==>] Sent b'{"@type": "setTdlibParameters", "parameters": {"use_test_dc": true, "api_id": API_ID, "api_hash": "API_HASH", "device_model": "python-telegram", "system_version": "unknown", "application_version": "0.7.0", "system_language_code": "en", "database_directory": "/app/telegram_data/data/database", "use_message_database": false, "files_directory": "/app/telegram_data/data/files"}, "@extra": {"request_id": "updateAuthorizationState"}}'
DEBUG:telegram.tdjson:[me <==] Received {'@type': 'error', 'code': 401, 'message': 'Database encryption key is needed: call checkDatabaseEncryptionKey first', '@extra': {'request_id': 'updateAuthorizationState'}}
Traceback (most recent call last):
  File "manage.py", line 15, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 316, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 353, in execute
    output = self.handle(*args, **options)
  File "/app/telegram_data/management/commands/test.py", line 38, in handle
    login=True,
  File "/usr/local/lib/python3.7/site-packages/telegram/client.py", line 90, in __init__
    self.login()
  File "/usr/local/lib/python3.7/site-packages/telegram/client.py", line 339, in login
    result.wait(raise_exc=True)
  File "/usr/local/lib/python3.7/site-packages/telegram/utils.py", line 39, in wait
    raise RuntimeError(f'Telegram error: {self.error_info}')
RuntimeError: Telegram error: {'@type': 'error', 'code': 401, 'message': 'Database encryption key is needed: call checkDatabaseEncryptionKey first', '@extra': {'request_id': 'updateAuthorizationState'}}
```
</p>
</details>

Comments:

Patch:
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index ce36cc8..a850da7 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -2,6 +2,14 @@
 Changelog
 =========
 
+[unreleased]
+
+- **Incompatible** default path for files is changed. Now the library uses an md5 hash of the phone number or bot token instead of just a phone number.
+  It should not be noticeable for most cases, but if you rely on locally saved files or database, you need to pass the ``files_directory`` parameter to the ``telegram.client.Telegram``.
+- Fixed problem with randomly raised "Database encryption key is needed" errors during login process. (#12)
+- Fixed `stop` method execution. (#8)
+- Added ``examples/bot_login.py`` example.
+
 [0.8.0] - 2019-03-17
 
 - ``telegram.client.Telegram`` now supports any update type with a new method ``add_update_handler(handler_type, func)``
diff --git a/examples/bot_login.py b/examples/bot_login.py
new file mode 100644
index 0000000..5b1767a
--- /dev/null
+++ b/examples/bot_login.py
@@ -0,0 +1,28 @@
+import argparse
+
+from telegram.client import Telegram
+
+
+def bot_get_me(api_id, api_hash, token):
+    tg = Telegram(
+        api_id=api_id,
+        api_hash=api_hash,
+        bot_token=token,
+        database_encryption_key='changeme1234',
+    )
+    # you must call login method before others
+    tg.login()
+
+    result = tg.get_me()
+    result.wait()
+    print(result.update)
+    tg.stop()
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('api_id', help='API id')  # https://my.telegram.org/apps
+    parser.add_argument('api_hash', help='API hash')
+    parser.add_argument('token', help='Bot token')
+    args = parser.parse_args()
+    bot_get_me(args.api_id, args.api_hash, args.token)
diff --git a/telegram/client.py b/telegram/client.py
index 6fc1a1f..0794692 100644
--- a/telegram/client.py
+++ b/telegram/client.py
@@ -1,4 +1,5 @@
 import os
+import hashlib
 import time
 import queue
 import signal
@@ -75,7 +76,13 @@ class Telegram:
         self._database_encryption_key = database_encryption_key
 
         if not files_directory:
-            files_directory = f'/tmp/.tdlib_files/{self.phone}/'
+            hasher = hashlib.md5()
+            hasher.update(
+                (self.phone or self.bot_token).encode('utf-8')  # type: ignore
+            )
+            directory_name = hasher.hexdigest()
+            files_directory = f'/tmp/.tdlib_files/{directory_name}/'
+
         self.files_directory = files_directory
 
         self._authorized = False
@@ -328,15 +335,22 @@ class Telegram:
     def _signal_handler(self, signum, frame):
         self._is_enabled = False
 
+    def get_authorization_state(self):
+        logger.debug('Getting authorization state')
+        data = {'@type': 'getAuthorizationState'}
+
+        return self._send_data(data, result_id='getAuthorizationState')
+
     def login(self):
         """
         Login process (blocking)
 
-        Must be called before any other call. It sends initial params to the tdlib, sets database encryption key, etc.
+        Must be called before any other call.
+        It sends initial params to the tdlib, sets database encryption key, etc.
         """
         authorization_state = None
         actions = {
-            None: self._set_initial_params,
+            None: self.get_authorization_state,
             'authorizationStateWaitTdlibParameters': self._set_initial_params,
             'authorizationStateWaitEncryptionKey': self._send_encryption_key,
             'authorizationStateWaitPhoneNumber': self._send_phone_number_or_bot_token,
@@ -355,7 +369,11 @@ class Telegram:
 
             if result:
                 result.wait(raise_exc=True)
-                authorization_state = result.update['authorization_state']['@type']
+
+                if result.id == 'getAuthorizationState':
+                    authorization_state = result.update['@type']
+                else:
+                    authorization_state = result.update['authorization_state']['@type']
 
     def _set_initial_params(self) -> AsyncResult:
         logger.info(
diff --git a/telegram/tdjson.py b/telegram/tdjson.py
index 2b448c0..122e015 100644
--- a/telegram/tdjson.py
+++ b/telegram/tdjson.py
@@ -111,6 +111,4 @@ class TDJson:
         return result
 
     def stop(self) -> None:
-        self._tdjson._td_json_client_destroy(
-            self.td_json_client
-        )  # pylint: disable=protected-access
+        self._td_json_client_destroy(self.td_json_client)
diff --git a/telegram/utils.py b/telegram/utils.py
index f233fcb..634c817 100644
--- a/telegram/utils.py
+++ b/telegram/utils.py
@@ -3,7 +3,7 @@ import uuid
 from typing import TYPE_CHECKING, Any, Dict, Optional
 
 if TYPE_CHECKING:
-    from telegram.client import Telegram    # noqa  pylint: disable=cyclic-import
+    from telegram.client import Telegram  # noqa  pylint: disable=cyclic-import
 
 
 class AsyncResult:
diff --git a/telegram/worker.py b/telegram/worker.py
index e836579..a2b6a55 100644
--- a/telegram/worker.py
+++ b/telegram/worker.py
@@ -24,7 +24,9 @@ class SimpleWorker(BaseWorker):
     """Simple one-thread worker"""
 
     def run(self) -> None:
-        self._thread = threading.Thread(target=self._run_thread)    # pylint: disable=attribute-defined-outside-init
+        self._thread = threading.Thread(  # pylint: disable=attribute-defined-outside-init
+            target=self._run_thread
+        )
         self._thread.daemon = True
         self._thread.start()
 


Test patch:
diff --git a/tests/test_tdjson.py b/tests/test_tdjson.py
index bb20ee8..fbc4c17 100644
--- a/tests/test_tdjson.py
+++ b/tests/test_tdjson.py
@@ -7,13 +7,13 @@ class Test_get_tdjson_lib_path(object):
         mocked_resource = mocker.Mock()
 
         with mocker.mock_module.patch('telegram.tdjson.platform.system', mocked_system):
-            with mocker.mock_module.patch('telegram.tdjson.pkg_resources.resource_filename',
-                                          mocked_resource):
+            with mocker.mock_module.patch(
+                'telegram.tdjson.pkg_resources.resource_filename', mocked_resource
+            ):
                 _get_tdjson_lib_path()
 
         mocked_resource.assert_called_once_with(
-            'telegram',
-            'lib/darwin/libtdjson.dylib',
+            'telegram', 'lib/darwin/libtdjson.dylib'
         )
 
     def test_for_linux(self, mocker):
@@ -21,39 +21,33 @@ class Test_get_tdjson_lib_path(object):
         mocked_resource = mocker.Mock(return_value='/tmp/')
 
         with mocker.mock_module.patch('telegram.tdjson.platform.system', mocked_system):
-            with mocker.mock_module.patch('telegram.tdjson.pkg_resources.resource_filename',
-                                          mocked_resource):
+            with mocker.mock_module.patch(
+                'telegram.tdjson.pkg_resources.resource_filename', mocked_resource
+            ):
                 _get_tdjson_lib_path()
 
-        mocked_resource.assert_called_once_with(
-            'telegram',
-            'lib/linux/libtdjson.so',
-        )
+        mocked_resource.assert_called_once_with('telegram', 'lib/linux/libtdjson.so')
 
     def test_for_windows(self, mocker):
         mocked_system = mocker.Mock(return_value='Windows')
         mocked_resource = mocker.Mock(return_value='/tmp/')
 
         with mocker.mock_module.patch('telegram.tdjson.platform.system', mocked_system):
-            with mocker.mock_module.patch('telegram.tdjson.pkg_resources.resource_filename',
-                                          mocked_resource):
+            with mocker.mock_module.patch(
+                'telegram.tdjson.pkg_resources.resource_filename', mocked_resource
+            ):
                 _get_tdjson_lib_path()
 
-        mocked_resource.assert_called_once_with(
-            'telegram',
-            'lib/linux/libtdjson.so',
-        )
+        mocked_resource.assert_called_once_with('telegram', 'lib/linux/libtdjson.so')
 
     def test_unknown(self, mocker):
         mocked_system = mocker.Mock(return_value='Unknown')
         mocked_resource = mocker.Mock(return_value='/tmp/')
 
         with mocker.mock_module.patch('telegram.tdjson.platform.system', mocked_system):
-            with mocker.mock_module.patch('telegram.tdjson.pkg_resources.resource_filename',
-                                          mocked_resource):
+            with mocker.mock_module.patch(
+                'telegram.tdjson.pkg_resources.resource_filename', mocked_resource
+            ):
                 _get_tdjson_lib_path()
 
-        mocked_resource.assert_called_once_with(
-            'telegram',
-            'lib/linux/libtdjson.so',
-        )
+        mocked_resource.assert_called_once_with('telegram', 'lib/linux/libtdjson.so')
diff --git a/tests/test_telegram_methods.py b/tests/test_telegram_methods.py
index bbd8d1b..ada7e9e 100644
--- a/tests/test_telegram_methods.py
+++ b/tests/test_telegram_methods.py
@@ -236,6 +236,7 @@ class TestTelegram:
 
     def test_set_initial_params(self, telegram):
         async_result = telegram._set_initial_params()
+        phone_md5 = '69560384b84c896952ef20352fbce705'
 
         exp_data = {
             '@type': 'setTdlibParameters',
@@ -247,9 +248,9 @@ class TestTelegram:
                 'system_version': 'unknown',
                 'application_version': VERSION,
                 'system_language_code': 'en',
-                'database_directory': f'/tmp/.tdlib_files/{PHONE}/database',
+                'database_directory': f'/tmp/.tdlib_files/{phone_md5}/database',
                 'use_message_database': True,
-                'files_directory': f'/tmp/.tdlib_files/{PHONE}/files',
+                'files_directory': f'/tmp/.tdlib_files/{phone_md5}/files',
             },
             '@extra': {'request_id': 'updateAuthorizationState'},
         }
@@ -296,29 +297,35 @@ class TestTelegram__login:
     def test_login_process_with_phone(self, telegram):
         telegram._authorized = False
 
-        def _get_ar(data):
-            ar = AsyncResult(client=telegram)
+        def _get_async_result(data, request_id=None):
+            result = AsyncResult(client=telegram)
 
-            ar.update = data
+            result.update = data
+            result.id = request_id
 
-            return ar
+            return result
 
         # login process chain
-        telegram._set_initial_params = lambda: _get_ar(
+        telegram.get_authorization_state = lambda: _get_async_result(
+            data={'@type': 'authorizationStateWaitEncryptionKey'},
+            request_id='getAuthorizationState',
+        )
+
+        telegram._set_initial_params = lambda: _get_async_result(
             data={
                 'authorization_state': {'@type': 'authorizationStateWaitEncryptionKey'}
             }
         )
-        telegram._send_encryption_key = lambda: _get_ar(
+        telegram._send_encryption_key = lambda: _get_async_result(
             data={'authorization_state': {'@type': 'authorizationStateWaitPhoneNumber'}}
         )
-        telegram._send_phone_number_or_bot_token = lambda: _get_ar(
+        telegram._send_phone_number_or_bot_token = lambda: _get_async_result(
             data={'authorization_state': {'@type': 'authorizationStateWaitCode'}}
         )
-        telegram._send_telegram_code = lambda: _get_ar(
+        telegram._send_telegram_code = lambda: _get_async_result(
             data={'authorization_state': {'@type': 'authorizationStateWaitPassword'}}
         )
-        telegram._send_password = lambda: _get_ar(
+        telegram._send_password = lambda: _get_async_result(
             data={'authorization_state': {'@type': 'authorizationStateReady'}}
         )
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 8ef8e2c..51ea333 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -21,10 +21,7 @@ class TestAsyncResult(object):
 
     def test_parse_update_with_error(self):
         ar = AsyncResult(client=None)
-        update = {
-            '@type': 'error',
-            'some': 'data',
-        }
+        update = {'@type': 'error', 'some': 'data'}
 
         assert ar.error is False
         assert ar.error_info is None
@@ -38,10 +35,7 @@ class TestAsyncResult(object):
 
     def test_parse_update_ok(self):
         ar = AsyncResult(client=None)
-        update = {
-            '@type': 'ok',
-            'some': 'data',
-        }
+        update = {'@type': 'ok', 'some': 'data'}
 
         ar.parse_update(update)
 
@@ -52,10 +46,7 @@ class TestAsyncResult(object):
 
     def test_parse_update(self):
         ar = AsyncResult(client=None)
-        update = {
-            '@type': 'some_type',
-            'some': 'data',
-        }
+        update = {'@type': 'some_type', 'some': 'data'}
 
         ar.parse_update(update)