├── .gitignore ├── README.md ├── full_demo.py ├── markup_demo.py ├── persistent_demo.py ├── setup.py ├── telegram_dialog ├── __init__.py ├── bot.py ├── items.py └── tools.py └── text_demo.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/* 3 | dist/* 4 | python_telegram_dialog_bot.egg-info/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-telegram-dialog-bot 2 | Simple API for Python bots with complicated dialogs. 3 | 4 | # Install 5 | 6 | python3 -m virtualenv venv 7 | source venv/bin/activate 8 | python3 setup.py install 9 | 10 | # Use 11 | 12 | See [full_demo.py](https://github.com/Saluev/python-telegram-dialog-bot/blob/master/full_demo.py) for Python 3 usage example and [persistent_demo.py](https://github.com/Saluev/python-telegram-dialog-bot/blob/master/persistent_demo.py) for [Stackless Python](https://bitbucket.org/stackless-dev/stackless) usage example. 13 | -------------------------------------------------------------------------------- /full_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | from telegram_dialog import * 5 | 6 | 7 | @requires_personal_chat("Простите, только в личном чате.") 8 | def dialog(start_message): 9 | answer = yield "Здравствуйте! Меня забыли наградить именем, а как зовут вас?" 10 | # убираем ведущие знаки пунктуации, оставляем только 11 | # первую компоненту имени, пишем её с заглавной буквы 12 | name = answer.text.rstrip(".!").split()[0].capitalize() 13 | likes_python = yield from ask_yes_or_no("Приятно познакомиться, %s. Вам нравится Питон?" % name) 14 | if likes_python: 15 | answer = yield from discuss_good_python(name) 16 | else: 17 | answer = yield from discuss_bad_python(name) 18 | 19 | 20 | def ask_yes_or_no(question): 21 | """Спросить вопрос и дождаться ответа, содержащего «да» или «нет». 22 | 23 | Возвращает: 24 | bool 25 | """ 26 | answer = yield (question, ["Да.", "Нет."]) 27 | while not ("да" in answer.text.lower() or "нет" in answer.text.lower()): 28 | answer = yield HTML("Так да или нет?") 29 | return "да" in answer.text.lower() 30 | 31 | 32 | def discuss_good_python(name): 33 | answer = yield "Мы с вами, %s, поразительно похожи! Что вам нравится в нём больше всего?" % name 34 | likes_article = yield from ask_yes_or_no("Ага. А как вам, кстати, статья на Хабре? Понравилась?") 35 | if likes_article: 36 | answer = yield "Чудно!" 37 | else: 38 | answer = yield "Жалко." 39 | return answer 40 | 41 | 42 | def discuss_bad_python(name): 43 | answer = yield "Ай-яй-яй. %s, фу таким быть! Что именно вам так не нравится?" % name 44 | likes_article = yield from ask_yes_or_no( 45 | "Ваша позиция имеет право на существование. Статья " 46 | "на Хабре вам, надо полагать, тоже не понравилась?") 47 | if likes_article: 48 | answer = yield "Ну и ладно." 49 | else: 50 | answer = yield ( 51 | "Что «нет»? «Нет, не понравилась» или «нет, понравилась»?", 52 | ["Нет, не понравилась!", "Нет, понравилась!"] 53 | ) 54 | answer = yield "Спокойно, это у меня юмор такой." 55 | return answer 56 | 57 | 58 | if __name__ == "__main__": 59 | dialog_bot = DialogBot(sys.argv[1], dialog) 60 | dialog_bot.start() 61 | -------------------------------------------------------------------------------- /markup_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import collections 3 | import sys 4 | 5 | from telegram.ext import Filters 6 | from telegram.ext import MessageHandler 7 | from telegram.ext import Updater 8 | 9 | 10 | class Message(object): 11 | def __init__(self, text, **options): 12 | self.text = text 13 | self.options = options 14 | 15 | 16 | class Markdown(Message): 17 | def __init__(self, text, **options): 18 | super(Markdown, self).__init__(text, parse_mode="Markdown", **options) 19 | 20 | 21 | class HTML(Message): 22 | def __init__(self, text, **options): 23 | super(HTML, self).__init__(text, parse_mode="HTML", **options) 24 | 25 | 26 | def dialog(): 27 | answer = yield "Здравствуйте! Меня забыли наградить именем, а как зовут вас?" 28 | # убираем ведущие знаки пунктуации, оставляем только 29 | # первую компоненту имени, пишем её с заглавной буквы 30 | name = answer.text.rstrip(".!").split()[0].capitalize() 31 | likes_python = yield from ask_yes_or_no("Приятно познакомиться, %s. Вам нравится Питон?" % name) 32 | if likes_python: 33 | answer = yield from discuss_good_python(name) 34 | else: 35 | answer = yield from discuss_bad_python(name) 36 | 37 | 38 | def ask_yes_or_no(question): 39 | """Спросить вопрос и дождаться ответа, содержащего «да» или «нет». 40 | 41 | Возвращает: 42 | bool 43 | """ 44 | answer = yield question 45 | while not ("да" in answer.text.lower() or "нет" in answer.text.lower()): 46 | answer = yield HTML("Так да или нет?") 47 | return "да" in answer.text.lower() 48 | 49 | 50 | def discuss_good_python(name): 51 | answer = yield "Мы с вами, %s, поразительно похожи! Что вам нравится в нём больше всего?" % name 52 | likes_article = yield from ask_yes_or_no("Ага. А как вам, кстати, статья на Хабре? Понравилась?") 53 | if likes_article: 54 | answer = yield "Чудно!" 55 | else: 56 | answer = yield "Жалко." 57 | return answer 58 | 59 | 60 | def discuss_bad_python(name): 61 | answer = yield "Ай-яй-яй. %s, фу таким быть! Что именно вам так не нравится?" % name 62 | likes_article = yield from ask_yes_or_no( 63 | "Ваша позиция имеет право на существование. Статья " 64 | "на Хабре вам, надо полагать, тоже не понравилась?") 65 | if likes_article: 66 | answer = yield "Ну и ладно." 67 | else: 68 | answer = yield "Что «нет»? «Нет, не понравилась» или «нет, понравилась»?" 69 | answer = yield "Спокойно, это у меня юмор такой." 70 | return answer 71 | 72 | 73 | class DialogBot(object): 74 | 75 | def __init__(self, token, generator): 76 | self.updater = Updater(token=token) # заводим апдейтера 77 | handler = MessageHandler(Filters.text | Filters.command, self.handle_message) 78 | self.updater.dispatcher.add_handler(handler) # ставим обработчик всех текстовых сообщений 79 | self.handlers = collections.defaultdict(generator) # заводим мапу "id чата -> генератор" 80 | 81 | def start(self): 82 | self.updater.start_polling() 83 | 84 | def handle_message(self, bot, update): 85 | print("Received", update.message) 86 | chat_id = update.message.chat_id 87 | if update.message.text == "/start": 88 | # если передана команда /start, начинаем всё с начала -- для 89 | # этого удаляем состояние текущего чатика, если оно есть 90 | self.handlers.pop(chat_id, None) 91 | if chat_id in self.handlers: 92 | # если диалог уже начат, то надо использовать .send(), чтобы 93 | # передать в генератор ответ пользователя 94 | try: 95 | answer = self.handlers[chat_id].send(update.message) 96 | except StopIteration: 97 | # если при этом генератор закончился -- что делать, начинаем общение с начала 98 | del self.handlers[chat_id] 99 | # (повторно вызванный, этот метод будет думать, что пользователь с нами впервые) 100 | return self.handle_message(bot, update) 101 | else: 102 | # диалог только начинается. defaultdict запустит новый генератор для этого 103 | # чатика, а мы должны будем извлечь первое сообщение с помощью .next() 104 | # (.send() срабатывает только после первого yield) 105 | answer = next(self.handlers[chat_id]) 106 | # отправляем полученный ответ пользователю 107 | print("Answer: %r" % answer) 108 | self._send_answer(bot, chat_id, answer) 109 | 110 | def _send_answer(self, bot, chat_id, answer): 111 | if isinstance(answer, str): 112 | answer = Message(answer) 113 | bot.sendMessage(chat_id=chat_id, text=answer.text, **answer.options) 114 | 115 | 116 | if __name__ == "__main__": 117 | dialog_bot = DialogBot(sys.argv[1], dialog) 118 | dialog_bot.start() 119 | -------------------------------------------------------------------------------- /persistent_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pickle 4 | import sys 5 | import time 6 | 7 | try: 8 | import stackless 9 | except ImportError: 10 | raise SystemExit("Stackless Python is required to run this demo.") 11 | 12 | from telegram_dialog import * 13 | 14 | 15 | @requires_personal_chat("Простите, только в личном чате.") 16 | def dialog(start_message): 17 | answer = yield "Здравствуйте! Меня забыли наградить именем, а как зовут вас?" 18 | # убираем ведущие знаки пунктуации, оставляем только 19 | # первую компоненту имени, пишем её с заглавной буквы 20 | name = answer.text.rstrip(".!").split()[0].capitalize() 21 | likes_python = yield from ask_yes_or_no("Приятно познакомиться, %s. Вам нравится Питон?" % name) 22 | if likes_python: 23 | answer = yield from discuss_good_python(name) 24 | else: 25 | answer = yield from discuss_bad_python(name) 26 | 27 | 28 | def ask_yes_or_no(question): 29 | """Спросить вопрос и дождаться ответа, содержащего «да» или «нет». 30 | 31 | Возвращает: 32 | bool 33 | """ 34 | answer = yield (question, ["Да.", "Нет."]) 35 | while not ("да" in answer.text.lower() or "нет" in answer.text.lower()): 36 | answer = yield HTML("Так да или нет?") 37 | return "да" in answer.text.lower() 38 | 39 | 40 | def discuss_good_python(name): 41 | answer = yield "Мы с вами, %s, поразительно похожи! Что вам нравится в нём больше всего?" % name 42 | likes_article = yield from ask_yes_or_no("Ага. А как вам, кстати, статья на Хабре? Понравилась?") 43 | if likes_article: 44 | answer = yield "Чудно!" 45 | else: 46 | answer = yield "Жалко." 47 | return answer 48 | 49 | 50 | def discuss_bad_python(name): 51 | answer = yield "Ай-яй-яй. %s, фу таким быть! Что именно вам так не нравится?" % name 52 | likes_article = yield from ask_yes_or_no( 53 | "Ваша позиция имеет право на существование. Статья " 54 | "на Хабре вам, надо полагать, тоже не понравилась?") 55 | if likes_article: 56 | answer = yield "Ну и ладно." 57 | else: 58 | answer = yield ( 59 | "Что «нет»? «Нет, не понравилась» или «нет, понравилась»?", 60 | ["Нет, не понравилась!", "Нет, понравилась!"] 61 | ) 62 | answer = yield "Спокойно, это у меня юмор такой." 63 | return answer 64 | 65 | 66 | if __name__ == "__main__": 67 | try: 68 | with open("handlers.pickle", "rb") as f: 69 | handlers = pickle.load(f) 70 | except IOError: 71 | handlers = None 72 | dialog_bot = DialogBot(sys.argv[1], dialog, handlers) 73 | dialog_bot.start() 74 | while True: 75 | try: 76 | time.sleep(1) 77 | except: 78 | with open("handlers.pickle", "wb") as f: 79 | pickle.dump(dialog_bot.handlers, f) 80 | os._exit(0) 81 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="python-telegram-dialog-bot", 5 | version="0.0.1", 6 | description= 7 | "Simple Python package for creating " 8 | "Telegram bots with complex dialogs.", 9 | url="https://github.com/saluev/python-telegram-dialog-bot", 10 | 11 | author="Tigran Saluev", 12 | author_email="tigran@saluev.com", 13 | 14 | license="MIT", 15 | 16 | classifiers=[ 17 | 'Development Status :: 3 - Alpha', 18 | 19 | # Indicate who your project is intended for 20 | 'Intended Audience :: Developers', 21 | 'Topic :: Software Development :: Libraries', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 24 | 'License :: OSI Approved :: MIT License', 25 | 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Programming Language :: Python :: 3.4', 29 | 'Programming Language :: Python :: 3.5', 30 | ], 31 | 32 | packages=["telegram_dialog"], 33 | 34 | install_requires=['python-telegram-bot'], 35 | ) 36 | -------------------------------------------------------------------------------- /telegram_dialog/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot import DialogBot 2 | from .items import * 3 | from .tools import * 4 | -------------------------------------------------------------------------------- /telegram_dialog/bot.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import collections.abc 3 | import copy 4 | 5 | from telegram import ReplyKeyboardMarkup 6 | from telegram import ReplyMarkup 7 | from telegram.ext import Filters 8 | from telegram.ext import InlineQueryHandler 9 | from telegram.ext import MessageHandler 10 | from telegram.ext import Updater 11 | 12 | from .items import * 13 | 14 | 15 | class DialogBot(object): 16 | def __init__(self, token, generator, handlers=None): 17 | self.updater = Updater(token=token) 18 | message_handler = MessageHandler(Filters.text | Filters.command, self.handle_message) 19 | inline_query_handler = InlineQueryHandler(self.handle_inline_query) 20 | self.updater.dispatcher.add_handler(message_handler) 21 | self.updater.dispatcher.add_handler(inline_query_handler) 22 | self.generator = generator 23 | self.handlers = handlers or {} 24 | self.last_message_ids = {} 25 | 26 | def start(self): 27 | self.updater.start_polling() 28 | 29 | def stop(self): 30 | self.updater.stop() 31 | 32 | def handle_message(self, bot, update, **kwargs): 33 | print("Received", update.message) 34 | chat_id = update.message.chat_id 35 | if update.message.text == "/start": 36 | self.handlers.pop(chat_id, None) 37 | self.apply_handler(bot, chat_id, update.message) 38 | 39 | def handle_inline_query(self, bot, update, **kwargs): 40 | inline_query = update.inline_query 41 | print("Received inline query", inline_query) 42 | user_id = inline_query.from_user.id 43 | just_started, handler = self.get_handler(user_id) 44 | results = list(handler.inline_query(inline_query)) if hasattr(handler, "inline_query") else [] 45 | if just_started: 46 | del self.handlers[user_id] 47 | return bot.answerInlineQuery(inline_query.id, results) 48 | 49 | def get_handler(self, chat_id, *args, **kwargs): 50 | if chat_id not in self.handlers: 51 | result = self.handlers[chat_id] = self.generator(*args, **kwargs) 52 | return True, result 53 | return False, self.handlers[chat_id] 54 | 55 | def apply_handler(self, bot, chat_id, message=None): 56 | just_started, handler = self.get_handler(chat_id, message) 57 | if just_started: 58 | answer = next(handler) 59 | else: 60 | try: 61 | answer = handler.send(message) 62 | except StopIteration: 63 | del self.handlers[chat_id] 64 | return self.apply_handler(bot, chat_id, message) 65 | self.send_answer(bot, chat_id, answer) 66 | 67 | def send_answer(self, bot, chat_id, answer): 68 | print("Sending answer %r to %s" % (answer, chat_id)) 69 | if isinstance(answer, collections.abc.Iterable) and not isinstance(answer, str): 70 | # мы получили несколько объектов -- сперва каждый надо обработать 71 | answer = list(map(self._convert_answer_part, answer)) 72 | else: 73 | # мы получили один объект -- сводим к более общей задаче 74 | answer = [self._convert_answer_part(answer)] 75 | # перед тем, как отправить очередное сообщение, идём вперёд в поисках 76 | # «довесков» -- клавиатуры там или в перспективе ещё чего-нибудь 77 | current_message = last_message = None 78 | for part in answer: 79 | if isinstance(part, Message): 80 | if current_message is not None: 81 | # сообщение, которое мы встретили раньше, пора бы отправить. 82 | # поскольку не все объекты исчерпаны, пусть это сообщение 83 | # не вызывает звоночек (если не указано обратное) 84 | current_message = copy.deepcopy(current_message) 85 | current_message.options.setdefault("disable_notification", True) 86 | self._send_or_edit(bot, chat_id, current_message) 87 | current_message = part 88 | if isinstance(part, ReplyMarkup): 89 | # ага, а вот и довесок! добавляем текущему сообщению. 90 | # нет сообщения -- ну извините, это ошибка. 91 | current_message.options["reply_markup"] = part 92 | # надо не забыть отправить последнее встреченное сообщение. 93 | if current_message is not None: 94 | self._send_or_edit(bot, chat_id, current_message) 95 | 96 | def _send_or_edit(self, bot, chat_id, message): 97 | if isinstance(message, EditLast): 98 | bot.editMessageText(text=message.text, chat_id=chat_id, message_id=self.last_message_ids[chat_id], 99 | **message.options) 100 | else: 101 | print("Sending message: %r" % message.text) 102 | self.last_message_ids[chat_id] = bot.sendMessage(chat_id=chat_id, text=message.text, **message.options) 103 | 104 | def _convert_answer_part(self, answer_part): 105 | if isinstance(answer_part, str): 106 | return Message(answer_part) 107 | if isinstance(answer_part, (collections.abc.Iterable, Keyboard)): 108 | # клавиатура? 109 | resize_keyboard = False 110 | one_time_keyboard = True 111 | 112 | if isinstance(answer_part, collections.abc.Iterable): 113 | answer_part = list(answer_part) 114 | else: 115 | one_time_keyboard = answer_part.one_time_keyboard 116 | resize_keyboard = answer_part.resize_keyboard 117 | answer_part = answer_part.markup 118 | 119 | if isinstance(answer_part[0], str): 120 | # она! оформляем как горизонтальный ряд кнопок. 121 | # кстати, все наши клавиатуры одноразовые -- нам пока хватит. 122 | return ReplyKeyboardMarkup([answer_part], one_time_keyboard=one_time_keyboard, 123 | resize_keyboard=resize_keyboard) 124 | elif isinstance(answer_part[0], collections.abc.Iterable): 125 | # двумерная клавиатура? 126 | answer_part = list(map(list, answer_part)) 127 | if isinstance(answer_part[0][0], str): 128 | # она! 129 | return ReplyKeyboardMarkup(answer_part, one_time_keyboard=one_time_keyboard, 130 | resize_keyboard=resize_keyboard) 131 | if isinstance(answer_part, Inline): 132 | return answer_part.convert() 133 | return answer_part 134 | -------------------------------------------------------------------------------- /telegram_dialog/items.py: -------------------------------------------------------------------------------- 1 | from telegram import InlineKeyboardButton 2 | from telegram import InlineKeyboardMarkup 3 | 4 | 5 | class Message(object): 6 | def __init__(self, text, **options): 7 | self.text = text 8 | self.options = options 9 | 10 | 11 | class Markdown(Message): 12 | def __init__(self, text, **options): 13 | super(Markdown, self).__init__(text, parse_mode="Markdown", **options) 14 | 15 | def __repr__(self): 16 | options = dict(self.options) 17 | options.pop("parse_mode") 18 | options = (", " + repr(options)) if options else "" 19 | return "Markdown(%r%s)" % (self.text, options) 20 | 21 | 22 | class HTML(Message): 23 | def __init__(self, text, **options): 24 | super(HTML, self).__init__(text, parse_mode="HTML", **options) 25 | 26 | def __repr__(self): 27 | options = dict(self.options) 28 | options.pop("parse_mode") 29 | options = (", " + repr(options)) if options else "" 30 | return "HTML(%r%s)" % (self.text, options) 31 | 32 | 33 | class EditLast(Message): 34 | pass 35 | 36 | 37 | class Button(object): 38 | def __init__(self, text, **kwargs): 39 | self.text = text 40 | self.options = kwargs 41 | 42 | def convert(self): 43 | return InlineKeyboardButton(text=self.text, **self.options) 44 | 45 | 46 | class Inline(object): 47 | def __init__(self, keyboard): 48 | self.keyboard = keyboard 49 | 50 | def convert(self): 51 | print(self.keyboard) 52 | keyboard = [ 53 | [ 54 | (button if isinstance(button, Button) else Button(button)).convert() 55 | for button in row 56 | ] 57 | for row in self.keyboard 58 | ] 59 | return InlineKeyboardMarkup(keyboard) 60 | 61 | 62 | class Keyboard(object): 63 | def __init__(self, markup, one_time_keyboard=True, resize_keyboard=False): 64 | self.markup = markup 65 | self.one_time_keyboard = one_time_keyboard 66 | self.resize_keyboard = resize_keyboard 67 | -------------------------------------------------------------------------------- /telegram_dialog/tools.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from telegram_dialog import Keyboard 4 | 5 | 6 | def require_choice(caption, keyboard, question=None): 7 | """ 8 | Requires user to make a choice of given set of options. 9 | 10 | Args: 11 | caption (String) - message to send with table of choices 12 | table (Union[Iterable[String], Iterable[Iterable[String]]]) - choices 13 | question (Optional[String]) - message to send when wrong answer given 14 | 15 | Returns: 16 | Union[Integral, Tuple[Integral, Integral]] - index of chosen option 17 | String - text of chosen option 18 | """ 19 | markup = keyboard 20 | if isinstance(keyboard, Keyboard): 21 | markup = keyboard.markup 22 | 23 | if not isinstance(markup[0], str): 24 | choices = sum(markup, []) 25 | else: 26 | choices = markup 27 | 28 | question = question or caption 29 | answer = yield ((caption, keyboard) if caption else keyboard) 30 | while answer.text not in choices: 31 | answer = yield (question) 32 | if not isinstance(markup[0], str): 33 | return next( 34 | (row_idx, row.index(answer.text)) 35 | for row_idx, row in enumerate(markup) 36 | if answer.text in row 37 | ), answer 38 | else: 39 | return markup.index(answer.text), answer 40 | 41 | 42 | def requires_personal_chat(error_message): 43 | """ 44 | Make dialog generator work in personal chats only. 45 | 46 | Args: 47 | error_message (String) - message to send when addressed in group chat 48 | 49 | Returns: 50 | Decorator 51 | """ 52 | 53 | def decorator(func): 54 | @functools.wraps(func) 55 | def result_func(message): 56 | chat_id = message.chat_id 57 | sender_id = message.from_user.id 58 | if chat_id != sender_id: 59 | yield error_message 60 | return 61 | result = yield from func(message) 62 | return result 63 | 64 | return result_func 65 | 66 | return decorator 67 | 68 | 69 | def dialog(func): 70 | return DialogGenerator(func) 71 | 72 | 73 | class DialogGenerator(object): 74 | def __init__(self, dialog_generator): 75 | self.dialog_generator = dialog_generator 76 | self.inline_generator = None 77 | 78 | def __call__(self, *args, **kwargs): 79 | return Dialog( 80 | self.dialog_generator(*args, **kwargs), 81 | self.inline_generator) 82 | 83 | def inline(self, inline): 84 | self.inline_generator = inline 85 | 86 | 87 | class Dialog(object): 88 | def __init__(self, dialog, inline_generator=None): 89 | self.dialog = dialog 90 | self.inline_generator = inline_generator 91 | 92 | def __next__(self): 93 | return next(self.dialog) 94 | 95 | def send(self, *args, **kwargs): 96 | return self.dialog.send(*args, **kwargs) 97 | 98 | def inline_query(self, inline_query): 99 | if self.inline_generator is None: 100 | return 101 | inline = self.inline_generator(inline_query) 102 | result = yield from inline 103 | return result 104 | -------------------------------------------------------------------------------- /text_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import collections 3 | import sys 4 | 5 | from telegram.ext import Filters 6 | from telegram.ext import MessageHandler 7 | from telegram.ext import Updater 8 | 9 | 10 | def dialog(): 11 | answer = yield "Здравствуйте! Меня забыли наградить именем, а как зовут вас?" 12 | # убираем ведущие знаки пунктуации, оставляем только 13 | # первую компоненту имени, пишем её с заглавной буквы 14 | name = answer.text.rstrip(".!").split()[0].capitalize() 15 | likes_python = yield from ask_yes_or_no("Приятно познакомиться, %s. Вам нравится Питон?" % name) 16 | if likes_python: 17 | answer = yield from discuss_good_python(name) 18 | else: 19 | answer = yield from discuss_bad_python(name) 20 | 21 | 22 | def ask_yes_or_no(question): 23 | """Спросить вопрос и дождаться ответа, содержащего «да» или «нет». 24 | 25 | Возвращает: 26 | bool 27 | """ 28 | answer = yield question 29 | while not ("да" in answer.text.lower() or "нет" in answer.text.lower()): 30 | answer = yield "Так да или нет?" 31 | return "да" in answer.text.lower() 32 | 33 | 34 | def discuss_good_python(name): 35 | answer = yield "Мы с вами, %s, поразительно похожи! Что вам нравится в нём больше всего?" % name 36 | likes_article = yield from ask_yes_or_no("Ага. А как вам, кстати, статья на Хабре? Понравилась?") 37 | if likes_article: 38 | answer = yield "Чудно!" 39 | else: 40 | answer = yield "Жалко." 41 | return answer 42 | 43 | 44 | def discuss_bad_python(name): 45 | answer = yield "Ай-яй-яй. %s, фу таким быть! Что именно вам так не нравится?" % name 46 | likes_article = yield from ask_yes_or_no( 47 | "Ваша позиция имеет право на существование. Статья " 48 | "на Хабре вам, надо полагать, тоже не понравилась?") 49 | if likes_article: 50 | answer = yield "Ну и ладно." 51 | else: 52 | answer = yield "Что «нет»? «Нет, не понравилась» или «нет, понравилась»?" 53 | answer = yield "Спокойно, это у меня юмор такой." 54 | return answer 55 | 56 | 57 | class DialogBot(object): 58 | 59 | def __init__(self, token, generator): 60 | self.updater = Updater(token=token) # заводим апдейтера 61 | handler = MessageHandler(Filters.text | Filters.command, self.handle_message) 62 | self.updater.dispatcher.add_handler(handler) # ставим обработчик всех текстовых сообщений 63 | self.handlers = collections.defaultdict(generator) # заводим мапу "id чата -> генератор" 64 | 65 | def start(self): 66 | self.updater.start_polling() 67 | 68 | def handle_message(self, bot, update): 69 | print("Received", update.message) 70 | chat_id = update.message.chat_id 71 | if update.message.text == "/start": 72 | # если передана команда /start, начинаем всё с начала -- для 73 | # этого удаляем состояние текущего чатика, если оно есть 74 | self.handlers.pop(chat_id, None) 75 | if chat_id in self.handlers: 76 | # если диалог уже начат, то надо использовать .send(), чтобы 77 | # передать в генератор ответ пользователя 78 | try: 79 | answer = self.handlers[chat_id].send(update.message) 80 | except StopIteration: 81 | # если при этом генератор закончился -- что делать, начинаем общение с начала 82 | del self.handlers[chat_id] 83 | # (повторно вызванный, этот метод будет думать, что пользователь с нами впервые) 84 | return self.handle_message(bot, update) 85 | else: 86 | # диалог только начинается. defaultdict запустит новый генератор для этого 87 | # чатика, а мы должны будем извлечь первое сообщение с помощью .next() 88 | # (.send() срабатывает только после первого yield) 89 | answer = next(self.handlers[chat_id]) 90 | # отправляем полученный ответ пользователю 91 | print("Answer: %r" % answer) 92 | bot.sendMessage(chat_id=chat_id, text=answer) 93 | 94 | 95 | if __name__ == "__main__": 96 | dialog_bot = DialogBot(sys.argv[1], dialog) 97 | dialog_bot.start() 98 | --------------------------------------------------------------------------------