├── .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 |
--------------------------------------------------------------------------------