mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-11-25 00:27:46 +01:00
parent
2cc9aac7dc
commit
aadb6df271
6 changed files with 557 additions and 2 deletions
|
@ -72,6 +72,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
|||
- `Shelomentsev D <https://github.com/shelomentsevd>`_
|
||||
- `Simon Schürrle <https://github.com/SitiSchu>`_
|
||||
- `sooyhwang <https://github.com/sooyhwang>`_
|
||||
- `syntx <https://github.com/syntx>`_
|
||||
- `thodnev <https://github.com/thodnev>`_
|
||||
- `Trainer Jono <https://github.com/Tr-Jono>`_
|
||||
- `Valentijn <https://github.com/Faalentijn>`_
|
||||
|
|
|
@ -16,6 +16,9 @@ A common task for a bot is to ask information from the user. In v5.0 of this lib
|
|||
### [`conversationbot2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.py)
|
||||
A more complex example of a bot that uses the `ConversationHandler`. It is also more confusing. Good thing there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.png) for this one, too!
|
||||
|
||||
### [`nestedconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.py)
|
||||
A even more complex example of a bot that uses the nested `ConversationHandler`s. While it's certainly not that complex that you couldn't built it without nested `ConversationHanldler`s, it gives a good impression on how to work with them. Of course, there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.png) for this example, too!
|
||||
|
||||
### [`inlinekeyboard.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard.py)
|
||||
This example sheds some light on inline keyboards, callback queries and message editing.
|
||||
|
||||
|
|
BIN
examples/nestedconversationbot.png
Normal file
BIN
examples/nestedconversationbot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 492 KiB |
362
examples/nestedconversationbot.py
Normal file
362
examples/nestedconversationbot.py
Normal file
|
@ -0,0 +1,362 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
Example of a bot-user conversation using nested ConversationHandlers.
|
||||
Send /start to initiate the conversation.
|
||||
Press Ctrl-C on the command line or send a signal to the process to stop the
|
||||
bot.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from telegram import (InlineKeyboardMarkup, InlineKeyboardButton)
|
||||
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
|
||||
ConversationHandler, CallbackQueryHandler)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# State definitions for top level conversation
|
||||
SELECTING_ACTION, ADDING_MEMBER, ADDING_SELF, DESCRIBING_SELF = map(chr, range(4))
|
||||
# State definitions for second level conversation
|
||||
SELECTING_LEVEL, SELECTING_GENDER = map(chr, range(4, 6))
|
||||
# State definitions for descriptions conversation
|
||||
SELECTING_FEATURE, TYPING = map(chr, range(6, 8))
|
||||
# Meta states
|
||||
STOPPING, SHOWING = map(chr, range(8, 10))
|
||||
# Shortcut for ConversationHandler.END
|
||||
END = ConversationHandler.END
|
||||
|
||||
# Different constants for this example
|
||||
(PARENTS, CHILDREN, SELF, GENDER, MALE, FEMALE, AGE, NAME, START_OVER, FEATURES,
|
||||
CURRENT_FEATURE, CURRENT_LEVEL) = map(chr, range(10, 22))
|
||||
|
||||
|
||||
# Helper
|
||||
def _name_switcher(level):
|
||||
if level == PARENTS:
|
||||
return ('Father', 'Mother')
|
||||
elif level == CHILDREN:
|
||||
return ('Brother', 'Sister')
|
||||
|
||||
|
||||
# Top level conversation callbacks
|
||||
def start(update, context):
|
||||
"""Select an action: Adding parent/child or show data."""
|
||||
text = 'You may add a familiy member, yourself show the gathered data or end the ' \
|
||||
'conversation. To abort, simply type /stop.'
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Add family member', callback_data=str(ADDING_MEMBER)),
|
||||
InlineKeyboardButton(text='Add yourself', callback_data=str(ADDING_SELF))
|
||||
], [
|
||||
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
|
||||
InlineKeyboardButton(text='Done', callback_data=str(END))
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
# If we're starting over we don't need do send a new message
|
||||
if context.user_data.get(START_OVER):
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
else:
|
||||
update.message.reply_text('Hi, I\'m FamiliyBot and here to help you gather information'
|
||||
'about your family.')
|
||||
update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
|
||||
context.user_data[START_OVER] = False
|
||||
return SELECTING_ACTION
|
||||
|
||||
|
||||
def adding_self(update, context):
|
||||
"""Add information about youself."""
|
||||
context.user_data[CURRENT_LEVEL] = SELF
|
||||
text = 'Okay, please tell me about yourself.'
|
||||
button = InlineKeyboardButton(text='Add info', callback_data=str(MALE))
|
||||
keyboard = InlineKeyboardMarkup.from_button(button)
|
||||
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return DESCRIBING_SELF
|
||||
|
||||
|
||||
def show_data(update, context):
|
||||
"""Pretty print gathered data."""
|
||||
def prettyprint(user_data, level):
|
||||
people = user_data.get(level)
|
||||
if not people:
|
||||
return '\nNo information yet.'
|
||||
|
||||
text = ''
|
||||
if level == SELF:
|
||||
for person in user_data[level]:
|
||||
text += '\nName: {0}, Age: {1}'.format(person.get(NAME, '-'), person.get(AGE, '-'))
|
||||
else:
|
||||
male, female = _name_switcher(level)
|
||||
|
||||
for person in user_data[level]:
|
||||
gender = female if person[GENDER] == FEMALE else male
|
||||
text += '\n{0}: Name: {1}, Age: {2}'.format(gender, person.get(NAME, '-'),
|
||||
person.get(AGE, '-'))
|
||||
return text
|
||||
|
||||
ud = context.user_data
|
||||
text = 'Yourself:' + prettyprint(ud, SELF)
|
||||
text += '\n\nParents:' + prettyprint(ud, PARENTS)
|
||||
text += '\n\nChildren:' + prettyprint(ud, CHILDREN)
|
||||
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Back', callback_data=str(END))
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
ud[START_OVER] = True
|
||||
|
||||
return SHOWING
|
||||
|
||||
|
||||
def stop(update, context):
|
||||
"""End Conversation by command."""
|
||||
update.message.reply_text('Okay, bye.')
|
||||
|
||||
return END
|
||||
|
||||
|
||||
def end(update, context):
|
||||
"""End conversation from InlineKeyboardButton."""
|
||||
text = 'See you around!'
|
||||
update.callback_query.edit_message_text(text=text)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
# Second level conversation callbacks
|
||||
def select_level(update, context):
|
||||
"""Choose to add a parent or a child."""
|
||||
text = 'You may add a parent or a child. Also you can show the gathered data or go back.'
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Add parent', callback_data=str(PARENTS)),
|
||||
InlineKeyboardButton(text='Add child', callback_data=str(CHILDREN))
|
||||
], [
|
||||
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
|
||||
InlineKeyboardButton(text='Back', callback_data=str(END))
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return SELECTING_LEVEL
|
||||
|
||||
|
||||
def select_gender(update, context):
|
||||
"""Choose to add mother or father."""
|
||||
level = update.callback_query.data
|
||||
context.user_data[CURRENT_LEVEL] = level
|
||||
|
||||
text = 'Please choose, whom to add.'
|
||||
|
||||
male, female = _name_switcher(level)
|
||||
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Add ' + male, callback_data=str(MALE)),
|
||||
InlineKeyboardButton(text='Add ' + female, callback_data=str(FEMALE))
|
||||
], [
|
||||
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
|
||||
InlineKeyboardButton(text='Back', callback_data=str(END))
|
||||
]]
|
||||
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return SELECTING_GENDER
|
||||
|
||||
|
||||
def end_second_level(update, context):
|
||||
"""Return to top level conversation."""
|
||||
context.user_data[START_OVER] = True
|
||||
start(update, context)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
# Third level callbacks
|
||||
def select_feature(update, context):
|
||||
"""Select a feature to update for the person."""
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Name', callback_data=str(NAME)),
|
||||
InlineKeyboardButton(text='Age', callback_data=str(AGE)),
|
||||
InlineKeyboardButton(text='Done', callback_data=str(END)),
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
# If we collect features for a new person, clear the cache and save the gender
|
||||
if not context.user_data.get(START_OVER):
|
||||
context.user_data[FEATURES] = {GENDER: update.callback_query.data}
|
||||
text = 'Please select a feature to update.'
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
# But after we do that, we need to send a new message
|
||||
else:
|
||||
text = 'Got it! Please select a feature to update.'
|
||||
update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
|
||||
context.user_data[START_OVER] = False
|
||||
return SELECTING_FEATURE
|
||||
|
||||
|
||||
def ask_for_input(update, context):
|
||||
"""Prompt user to input data for selected feature."""
|
||||
context.user_data[CURRENT_FEATURE] = update.callback_query.data
|
||||
text = 'Okay, tell me.'
|
||||
update.callback_query.edit_message_text(text=text)
|
||||
|
||||
return TYPING
|
||||
|
||||
|
||||
def save_input(update, context):
|
||||
"""Save input for feature and return to feature selection."""
|
||||
ud = context.user_data
|
||||
ud[FEATURES][ud[CURRENT_FEATURE]] = update.message.text
|
||||
|
||||
ud[START_OVER] = True
|
||||
|
||||
return select_feature(update, context)
|
||||
|
||||
|
||||
def end_describing(update, context):
|
||||
"""End gathering of features and return to parent conversation."""
|
||||
ud = context.user_data
|
||||
level = ud[CURRENT_LEVEL]
|
||||
if not ud.get(level):
|
||||
ud[level] = []
|
||||
ud[level].append(ud[FEATURES])
|
||||
|
||||
# Print upper level menu
|
||||
if level == SELF:
|
||||
ud[START_OVER] = True
|
||||
start(update, context)
|
||||
else:
|
||||
select_level(update, context)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
def stop_nested(update, context):
|
||||
"""Completely end conversation from within nested conversation."""
|
||||
update.message.reply_text('Okay, bye.')
|
||||
|
||||
return STOPPING
|
||||
|
||||
|
||||
# Error handler
|
||||
def error(update, context):
|
||||
"""Log Errors caused by Updates."""
|
||||
logger.warning('Update "%s" caused error "%s"', update, context.error)
|
||||
|
||||
|
||||
def main():
|
||||
# Create the Updater and pass it your bot's token.
|
||||
# Make sure to set use_context=True to use the new context based callbacks
|
||||
# Post version 12 this will no longer be necessary
|
||||
updater = Updater("TOKEN", use_context=True)
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dp = updater.dispatcher
|
||||
|
||||
# Set up third level ConversationHandler (collecting features)
|
||||
description_conv = ConversationHandler(
|
||||
entry_points=[CallbackQueryHandler(select_feature,
|
||||
pattern='^' + str(MALE) + '$|^' + str(FEMALE) + '$')],
|
||||
|
||||
states={
|
||||
SELECTING_FEATURE: [CallbackQueryHandler(ask_for_input,
|
||||
pattern='^(?!' + str(END) + ').*$')],
|
||||
TYPING: [MessageHandler(Filters.text, save_input)],
|
||||
},
|
||||
|
||||
fallbacks=[
|
||||
CallbackQueryHandler(end_describing, pattern='^' + str(END) + '$'),
|
||||
CommandHandler('stop', stop_nested)
|
||||
],
|
||||
|
||||
map_to_parent={
|
||||
# Return to second level menu
|
||||
END: SELECTING_LEVEL,
|
||||
# End conversation alltogether
|
||||
STOPPING: STOPPING,
|
||||
}
|
||||
)
|
||||
|
||||
# Set up second level ConversationHandler (adding a person)
|
||||
add_member_conv = ConversationHandler(
|
||||
entry_points=[CallbackQueryHandler(select_level,
|
||||
pattern='^' + str(ADDING_MEMBER) + '$')],
|
||||
|
||||
states={
|
||||
SELECTING_LEVEL: [CallbackQueryHandler(select_gender,
|
||||
pattern='^{0}$|^{1}$'.format(str(PARENTS),
|
||||
str(CHILDREN)))],
|
||||
SELECTING_GENDER: [description_conv]
|
||||
},
|
||||
|
||||
fallbacks=[
|
||||
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
|
||||
CallbackQueryHandler(end_second_level, pattern='^' + str(END) + '$'),
|
||||
CommandHandler('stop', stop_nested)
|
||||
],
|
||||
|
||||
map_to_parent={
|
||||
# After showing data return to top level menu
|
||||
SHOWING: SHOWING,
|
||||
# Return to top level menu
|
||||
END: SELECTING_ACTION,
|
||||
# End conversation alltogether
|
||||
STOPPING: END,
|
||||
}
|
||||
)
|
||||
|
||||
# Set up top level ConversationHandler (selecting action)
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('start', start)],
|
||||
|
||||
states={
|
||||
SHOWING: [CallbackQueryHandler(start, pattern='^' + str(END) + '$')],
|
||||
SELECTING_ACTION: [
|
||||
add_member_conv,
|
||||
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
|
||||
CallbackQueryHandler(adding_self, pattern='^' + str(ADDING_SELF) + '$'),
|
||||
CallbackQueryHandler(end, pattern='^' + str(END) + '$'),
|
||||
],
|
||||
DESCRIBING_SELF: [description_conv],
|
||||
},
|
||||
|
||||
fallbacks=[CommandHandler('stop', stop)],
|
||||
)
|
||||
# Because the states of the third level conversation map to the ones of the
|
||||
# second level conversation, we need to be a bit hacky about that:
|
||||
conv_handler.states[SELECTING_LEVEL] = conv_handler.states[SELECTING_ACTION]
|
||||
conv_handler.states[STOPPING] = conv_handler.entry_points
|
||||
|
||||
dp.add_handler(conv_handler)
|
||||
|
||||
# log all errors
|
||||
dp.add_error_handler(error)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -64,6 +64,20 @@ class ConversationHandler(Handler):
|
|||
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
|
||||
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
|
||||
|
||||
Note:
|
||||
In each of the described collections of handlers, a handler may in turn be a
|
||||
:class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should
|
||||
have the attribute :attr:`map_to_parent` which allows to return to the parent conversation
|
||||
at specified states within the nested conversation.
|
||||
|
||||
Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states`
|
||||
attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents
|
||||
states to continue the parent conversation after this has ended or even map a state to
|
||||
:attr:`END` to end the *parent* conversation from within the nested one. For an example on
|
||||
nested :class:`ConversationHandler` s, see our `examples`_.
|
||||
|
||||
.. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples
|
||||
|
||||
Attributes:
|
||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||
trigger the start of the conversation.
|
||||
|
@ -88,6 +102,9 @@ class ConversationHandler(Handler):
|
|||
persistence
|
||||
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
|
||||
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
|
||||
map_to_parent (Dict[:obj:`object`, :obj:`object`]): Optional. A :obj:`dict` that can be
|
||||
used to instruct a nested conversationhandler to transition into a mapped state on
|
||||
its parent conversationhandler in place of a specified nested state.
|
||||
|
||||
Args:
|
||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||
|
@ -119,6 +136,9 @@ class ConversationHandler(Handler):
|
|||
persistence
|
||||
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
|
||||
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
|
||||
map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
|
||||
used to instruct a nested conversationhandler to transition into a mapped state on
|
||||
its parent conversationhandler in place of a specified nested state.
|
||||
|
||||
Raises:
|
||||
ValueError
|
||||
|
@ -142,7 +162,8 @@ class ConversationHandler(Handler):
|
|||
per_message=False,
|
||||
conversation_timeout=None,
|
||||
name=None,
|
||||
persistent=False):
|
||||
persistent=False,
|
||||
map_to_parent=None):
|
||||
|
||||
self.entry_points = entry_points
|
||||
self.states = states
|
||||
|
@ -160,6 +181,7 @@ class ConversationHandler(Handler):
|
|||
self.persistence = None
|
||||
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
|
||||
Set by dispatcher"""
|
||||
self.map_to_parent = map_to_parent
|
||||
|
||||
self.timeout_jobs = dict()
|
||||
self.conversations = dict()
|
||||
|
@ -328,7 +350,11 @@ class ConversationHandler(Handler):
|
|||
self._trigger_timeout, self.conversation_timeout,
|
||||
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
|
||||
|
||||
self.update_state(new_state, conversation_key)
|
||||
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
|
||||
self.update_state(self.END, conversation_key)
|
||||
return self.map_to_parent.get(new_state)
|
||||
else:
|
||||
self.update_state(new_state, conversation_key)
|
||||
|
||||
def update_state(self, new_state, key):
|
||||
if new_state == self.END:
|
||||
|
|
|
@ -43,6 +43,10 @@ class TestConversationHandler(object):
|
|||
# and then we can start coding!
|
||||
END, THIRSTY, BREWING, DRINKING, CODING = range(-1, 4)
|
||||
|
||||
# Drinking state definitions (nested)
|
||||
# At first we're holding the cup. Then we sip coffee, and last we swallow it
|
||||
HOLDING, SIPPING, SWALLOWING, REPLENISHING, STOPPING = map(chr, range(ord('a'), ord('f')))
|
||||
|
||||
current_state, entry_points, states, fallbacks = None, None, None, None
|
||||
group = Chat(0, Chat.GROUP)
|
||||
second_group = Chat(1, Chat.GROUP)
|
||||
|
@ -69,6 +73,43 @@ class TestConversationHandler(object):
|
|||
self.fallbacks = [CommandHandler('eat', self.start)]
|
||||
self.is_timeout = False
|
||||
|
||||
# for nesting tests
|
||||
self.nested_states = {
|
||||
self.THIRSTY: [CommandHandler('brew', self.brew), CommandHandler('wait', self.start)],
|
||||
self.BREWING: [CommandHandler('pourCoffee', self.drink)],
|
||||
self.CODING: [
|
||||
CommandHandler('keepCoding', self.code),
|
||||
CommandHandler('gettingThirsty', self.start),
|
||||
CommandHandler('drinkMore', self.drink)
|
||||
],
|
||||
}
|
||||
self.drinking_entry_points = [CommandHandler('hold', self.hold)]
|
||||
self.drinking_states = {
|
||||
self.HOLDING: [CommandHandler('sip', self.sip)],
|
||||
self.SIPPING: [CommandHandler('swallow', self.swallow)],
|
||||
self.SWALLOWING: [CommandHandler('hold', self.hold)]
|
||||
}
|
||||
self.drinking_fallbacks = [CommandHandler('replenish', self.replenish),
|
||||
CommandHandler('stop', self.stop),
|
||||
CommandHandler('end', self.end),
|
||||
CommandHandler('startCoding', self.code),
|
||||
CommandHandler('drinkMore', self.drink)]
|
||||
self.drinking_entry_points.extend(self.drinking_fallbacks)
|
||||
|
||||
# Map nested states to parent states:
|
||||
self.drinking_map_to_parent = {
|
||||
# Option 1 - Map a fictional internal state to an external parent state
|
||||
self.REPLENISHING: self.BREWING,
|
||||
# Option 2 - Map a fictional internal state to the END state on the parent
|
||||
self.STOPPING: self.END,
|
||||
# Option 3 - Map the internal END state to an external parent state
|
||||
self.END: self.CODING,
|
||||
# Option 4 - Map an external state to the same external parent state
|
||||
self.CODING: self.CODING,
|
||||
# Option 5 - Map an external state to the internal entry point
|
||||
self.DRINKING: self.DRINKING
|
||||
}
|
||||
|
||||
# State handlers
|
||||
def _set_state(self, update, state):
|
||||
self.current_state[update.message.from_user.id] = state
|
||||
|
@ -103,6 +144,23 @@ class TestConversationHandler(object):
|
|||
def passout2(self, bot, update):
|
||||
self.is_timeout = True
|
||||
|
||||
# Drinking actions (nested)
|
||||
|
||||
def hold(self, bot, update):
|
||||
return self._set_state(update, self.HOLDING)
|
||||
|
||||
def sip(self, bot, update):
|
||||
return self._set_state(update, self.SIPPING)
|
||||
|
||||
def swallow(self, bot, update):
|
||||
return self._set_state(update, self.SWALLOWING)
|
||||
|
||||
def replenish(self, bot, update):
|
||||
return self._set_state(update, self.REPLENISHING)
|
||||
|
||||
def stop(self, bot, update):
|
||||
return self._set_state(update, self.STOPPING)
|
||||
|
||||
# Tests
|
||||
def test_per_all_false(self):
|
||||
with pytest.raises(ValueError, match="can't all be 'False'"):
|
||||
|
@ -609,3 +667,108 @@ class TestConversationHandler(object):
|
|||
"If 'per_chat=True', 'InlineQueryHandler' can not be used,"
|
||||
" since inline queries have no chat context."
|
||||
)
|
||||
|
||||
def test_nested_conversation_handler(self, dp, bot, user1, user2):
|
||||
self.nested_states[self.DRINKING] = [ConversationHandler(
|
||||
entry_points=self.drinking_entry_points,
|
||||
states=self.drinking_states,
|
||||
fallbacks=self.drinking_fallbacks,
|
||||
map_to_parent=self.drinking_map_to_parent)]
|
||||
handler = ConversationHandler(entry_points=self.entry_points,
|
||||
states=self.nested_states,
|
||||
fallbacks=self.fallbacks)
|
||||
dp.add_handler(handler)
|
||||
|
||||
# User one, starts the state machine.
|
||||
message = Message(0, user1, None, self.group, text='/start', bot=bot,
|
||||
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
|
||||
offset=0, length=len('/start'))])
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.THIRSTY
|
||||
|
||||
# The user is thirsty and wants to brew coffee.
|
||||
message.text = '/brew'
|
||||
message.entities[0].length = len('/brew')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.BREWING
|
||||
|
||||
# Lets pour some coffee.
|
||||
message.text = '/pourCoffee'
|
||||
message.entities[0].length = len('/pourCoffee')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user is holding the cup
|
||||
message.text = '/hold'
|
||||
message.entities[0].length = len('/hold')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.HOLDING
|
||||
|
||||
# The user is sipping coffee
|
||||
message.text = '/sip'
|
||||
message.entities[0].length = len('/sip')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.SIPPING
|
||||
|
||||
# The user is swallowing
|
||||
message.text = '/swallow'
|
||||
message.entities[0].length = len('/swallow')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.SWALLOWING
|
||||
|
||||
# The user is holding the cup again
|
||||
message.text = '/hold'
|
||||
message.entities[0].length = len('/hold')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.HOLDING
|
||||
|
||||
# The user wants to replenish the coffee supply
|
||||
message.text = '/replenish'
|
||||
message.entities[0].length = len('/replenish')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.REPLENISHING
|
||||
assert handler.conversations[(0, user1.id)] == self.BREWING
|
||||
|
||||
# The user wants to drink their coffee again
|
||||
message.text = '/pourCoffee'
|
||||
message.entities[0].length = len('/pourCoffee')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user is now ready to start coding
|
||||
message.text = '/startCoding'
|
||||
message.entities[0].length = len('/startCoding')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.CODING
|
||||
|
||||
# The user decides it's time to drink again
|
||||
message.text = '/drinkMore'
|
||||
message.entities[0].length = len('/drinkMore')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user is holding their cup
|
||||
message.text = '/hold'
|
||||
message.entities[0].length = len('/hold')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.HOLDING
|
||||
|
||||
# The user wants to end with the drinking and go back to coding
|
||||
message.text = '/end'
|
||||
message.entities[0].length = len('/end')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.END
|
||||
assert handler.conversations[(0, user1.id)] == self.CODING
|
||||
|
||||
# The user wants to drink once more
|
||||
message.text = '/drinkMore'
|
||||
message.entities[0].length = len('/drinkMore')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user wants to stop altogether
|
||||
message.text = '/stop'
|
||||
message.entities[0].length = len('/stop')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.STOPPING
|
||||
assert handler.conversations.get((0, user1.id)) is None
|
||||
|
|
Loading…
Reference in a new issue