Repository: python-telegram-bot/python-telegram-bot

Issue:
[BUG] context.user_data saved in a conversation is lost upon timeout.
<!--
Thanks for reporting issues of python-telegram-bot!

Use this template to notify us if you found a bug.

To make it easier for us to help you please enter detailed information below.

Please note, we only support the latest version of python-telegram-bot and
master branch. Please make sure to upgrade & recreate the issue on the latest
version prior to opening an issue.
-->
### Steps to reproduce
1. In the entry function of a conversation save some data in context.user_data

2. Set the conversation_timeout of the conversation to some value other than 0. I used it as 10

3. In the "ConversationHandler.TIMEOUT" state define a callback handler and in that try reading the user_data from context.

### Expected behaviour
Should be able to fetch the saved data using context.user_data.get method.

### Actual behaviour
I get the following error: AttributeError: 'NoneType' object has no attribute 'get'. In all other states I am able to read the user_data except in the TIMEOUT state.

### Configuration
**Operating System:**
Windows 10

**Version of Python, python-telegram-bot & dependencies:**
Version of Python 3.7.5
Version of python-telegram-bot 12.4.2




Discussion:
Hi thanks for the catch! Haven't tested it myself, but looking at the code, your description makes sense. I think, we just need to call `CallbackContext.from_update()` in this `__init__` function over [here](https://github.com/python-telegram-bot/python-telegram-bot/blob/28ded6718ecaf2541b2dd25a14bad0a7c3068c7b/telegram/ext/conversationhandler.py#L32).

As a workaround: If I see correctly, the `context` you are getting in the timeout handler should have the attributes `update` and `dispatcher`. You can then use 
```python
chat_data = context.dispatcher.chat_data[context.update.chat.id]
user_data = context.dispatcher.user_data[context.update.from_user.id]
```
to access the data (if the update has a chat/from_user, that is)
Your work around solves my problem for now. Thank you very much.

Resolving patch:
diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py
--- a/telegram/ext/conversationhandler.py
+++ b/telegram/ext/conversationhandler.py
@@ -29,10 +29,11 @@
 
 
 class _ConversationTimeoutContext(object):
-    def __init__(self, conversation_key, update, dispatcher):
+    def __init__(self, conversation_key, update, dispatcher, callback_context):
         self.conversation_key = conversation_key
         self.update = update
         self.dispatcher = dispatcher
+        self.callback_context = callback_context
 
 
 class ConversationHandler(Handler):
@@ -96,8 +97,9 @@ class ConversationHandler(Handler):
         conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`): Optional. When this
             handler is inactive more than this timeout (in seconds), it will be automatically
             ended. If this value is 0 (default), there will be no timeout. When it's triggered, the
-            last received update will be handled by ALL the handler's who's `check_update` method
-            returns True that are in the state :attr:`ConversationHandler.TIMEOUT`.
+            last received update and the corresponding ``context`` will be handled by ALL the
+            handler's who's `check_update` method returns True that are in the state
+            :attr:`ConversationHandler.TIMEOUT`.
         name (:obj:`str`): Optional. The name for this conversationhandler. Required for
             persistence
         persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
@@ -130,8 +132,9 @@ class ConversationHandler(Handler):
         conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this
             handler is inactive more than this timeout (in seconds), it will be automatically
             ended. If this value is 0 or None (default), there will be no timeout. The last
-            received update will be handled by ALL the handler's who's `check_update` method
-            returns True that are in the state :attr:`ConversationHandler.TIMEOUT`.
+            received update and the corresponding ``context`` will be handled by ALL the handler's
+            who's `check_update` method returns True that are in the state
+            :attr:`ConversationHandler.TIMEOUT`.
         name (:obj:`str`, optional): The name for this conversationhandler. Required for
             persistence
         persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
@@ -466,7 +469,8 @@ def handle_update(self, update, dispatcher, check_result, context=None):
                 # Add the new timeout job
                 self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
                     self._trigger_timeout, self.conversation_timeout,
-                    context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
+                    context=_ConversationTimeoutContext(conversation_key, update,
+                                                        dispatcher, context))
 
         if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
             self.update_state(self.END, conversation_key)
@@ -503,9 +507,9 @@ def _trigger_timeout(self, context, job=None):
         callback_context = None
         if isinstance(context, CallbackContext):
             job = context.job
-            callback_context = context
 
         context = job.context
+        callback_context = context.callback_context
 
         with self._timeout_jobs_lock:
             found_job = self.timeout_jobs[context.conversation_key]


Test patch:
diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py
--- a/tests/test_conversationhandler.py
+++ b/tests/test_conversationhandler.py
@@ -546,6 +546,43 @@ def test_conversation_timeout(self, dp, bot, user1):
         dp.job_queue.tick()
         assert handler.conversations.get((self.group.id, user1.id)) is None
 
+    def test_conversation_handler_timeout_update_and_context(self, cdp, bot, user1):
+        context = None
+
+        def start_callback(u, c):
+            nonlocal context, self
+            context = c
+            return self.start(u, c)
+
+        states = self.states
+        timeout_handler = CommandHandler('start', None)
+        states.update({ConversationHandler.TIMEOUT: [timeout_handler]})
+        handler = ConversationHandler(entry_points=[CommandHandler('start', start_callback)],
+                                      states=states, fallbacks=self.fallbacks,
+                                      conversation_timeout=0.5)
+        cdp.add_handler(handler)
+
+        # Start state machine, then reach timeout
+        message = Message(0, user1, None, self.group, text='/start',
+                          entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0,
+                                                  length=len('/start'))],
+                          bot=bot)
+        update = Update(update_id=0, message=message)
+
+        def timeout_callback(u, c):
+            nonlocal update, context, self
+            self.is_timeout = True
+            assert u is update
+            assert c is context
+
+        timeout_handler.callback = timeout_callback
+
+        cdp.process_update(update)
+        sleep(0.5)
+        cdp.job_queue.tick()
+        assert handler.conversations.get((self.group.id, user1.id)) is None
+        assert self.is_timeout
+
     def test_conversation_timeout_keeps_extending(self, dp, bot, user1):
         handler = ConversationHandler(entry_points=self.entry_points, states=self.states,
                                       fallbacks=self.fallbacks, conversation_timeout=0.5)