mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-11-21 22:56:38 +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>`_
|
- `Shelomentsev D <https://github.com/shelomentsevd>`_
|
||||||
- `Simon Schürrle <https://github.com/SitiSchu>`_
|
- `Simon Schürrle <https://github.com/SitiSchu>`_
|
||||||
- `sooyhwang <https://github.com/sooyhwang>`_
|
- `sooyhwang <https://github.com/sooyhwang>`_
|
||||||
|
- `syntx <https://github.com/syntx>`_
|
||||||
- `thodnev <https://github.com/thodnev>`_
|
- `thodnev <https://github.com/thodnev>`_
|
||||||
- `Trainer Jono <https://github.com/Tr-Jono>`_
|
- `Trainer Jono <https://github.com/Tr-Jono>`_
|
||||||
- `Valentijn <https://github.com/Faalentijn>`_
|
- `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)
|
### [`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!
|
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)
|
### [`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.
|
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
|
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
|
||||||
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
|
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:
|
Attributes:
|
||||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||||
trigger the start of the conversation.
|
trigger the start of the conversation.
|
||||||
|
@ -88,6 +102,9 @@ class ConversationHandler(Handler):
|
||||||
persistence
|
persistence
|
||||||
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
|
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`
|
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:
|
Args:
|
||||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||||
|
@ -119,6 +136,9 @@ class ConversationHandler(Handler):
|
||||||
persistence
|
persistence
|
||||||
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
|
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`
|
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:
|
Raises:
|
||||||
ValueError
|
ValueError
|
||||||
|
@ -142,7 +162,8 @@ class ConversationHandler(Handler):
|
||||||
per_message=False,
|
per_message=False,
|
||||||
conversation_timeout=None,
|
conversation_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
persistent=False):
|
persistent=False,
|
||||||
|
map_to_parent=None):
|
||||||
|
|
||||||
self.entry_points = entry_points
|
self.entry_points = entry_points
|
||||||
self.states = states
|
self.states = states
|
||||||
|
@ -160,6 +181,7 @@ class ConversationHandler(Handler):
|
||||||
self.persistence = None
|
self.persistence = None
|
||||||
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
|
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
|
||||||
Set by dispatcher"""
|
Set by dispatcher"""
|
||||||
|
self.map_to_parent = map_to_parent
|
||||||
|
|
||||||
self.timeout_jobs = dict()
|
self.timeout_jobs = dict()
|
||||||
self.conversations = dict()
|
self.conversations = dict()
|
||||||
|
@ -328,7 +350,11 @@ class ConversationHandler(Handler):
|
||||||
self._trigger_timeout, self.conversation_timeout,
|
self._trigger_timeout, self.conversation_timeout,
|
||||||
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
|
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):
|
def update_state(self, new_state, key):
|
||||||
if new_state == self.END:
|
if new_state == self.END:
|
||||||
|
|
|
@ -43,6 +43,10 @@ class TestConversationHandler(object):
|
||||||
# and then we can start coding!
|
# and then we can start coding!
|
||||||
END, THIRSTY, BREWING, DRINKING, CODING = range(-1, 4)
|
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
|
current_state, entry_points, states, fallbacks = None, None, None, None
|
||||||
group = Chat(0, Chat.GROUP)
|
group = Chat(0, Chat.GROUP)
|
||||||
second_group = Chat(1, Chat.GROUP)
|
second_group = Chat(1, Chat.GROUP)
|
||||||
|
@ -69,6 +73,43 @@ class TestConversationHandler(object):
|
||||||
self.fallbacks = [CommandHandler('eat', self.start)]
|
self.fallbacks = [CommandHandler('eat', self.start)]
|
||||||
self.is_timeout = False
|
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
|
# State handlers
|
||||||
def _set_state(self, update, state):
|
def _set_state(self, update, state):
|
||||||
self.current_state[update.message.from_user.id] = state
|
self.current_state[update.message.from_user.id] = state
|
||||||
|
@ -103,6 +144,23 @@ class TestConversationHandler(object):
|
||||||
def passout2(self, bot, update):
|
def passout2(self, bot, update):
|
||||||
self.is_timeout = True
|
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
|
# Tests
|
||||||
def test_per_all_false(self):
|
def test_per_all_false(self):
|
||||||
with pytest.raises(ValueError, match="can't all be 'False'"):
|
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,"
|
"If 'per_chat=True', 'InlineQueryHandler' can not be used,"
|
||||||
" since inline queries have no chat context."
|
" 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