Skip to content

Commit 4397903

Browse files
Eldinniejsmnbom
authored andcommitted
* BasePersistence * basic construct * Keep working * Continue work Add tests for Basepersistence * Finish up BasePersistence and implementation * PickelPersistence and start tests * Finishing up * Oops, left in some typings * Compatibilty issues regarding py2 solved For Py2 compatibility * increasing coverage * Small changes due to CR * All persistence tests in one file * add DictPersistence * Last changes per CR * forgot change * changes per CR * call update_* only with relevant data As discussed with @jsmnbom * Add conversationbot Example * should not have committed API-key
1 parent b9f56ca commit 4397903

17 files changed

Lines changed: 1677 additions & 9 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
telegram.ext.BasePersistence
2+
============================
3+
4+
.. autoclass:: telegram.ext.BasePersistence
5+
:members:
6+
:show-inheritance:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
telegram.ext.DictPersistence
2+
============================
3+
4+
.. autoclass:: telegram.ext.DictPersistence
5+
:members:
6+
:show-inheritance:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
telegram.ext.PicklePersistence
2+
==============================
3+
4+
.. autoclass:: telegram.ext.PicklePersistence
5+
:members:
6+
:show-inheritance:

docs/source/telegram.ext.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,12 @@ Handlers
2929
telegram.ext.stringcommandhandler
3030
telegram.ext.stringregexhandler
3131
telegram.ext.typehandler
32+
33+
Persistence
34+
-----------
35+
36+
.. toctree::
37+
38+
telegram.ext.basepersistence
39+
telegram.ext.picklepersistence
40+
telegram.ext.dictpersistence

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@ A basic example of an [inline bot](https://core.telegram.org/bots/inline). Don't
2525
### [`paymentbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/paymentbot.py)
2626
A basic example of a bot that can accept payments. Don't forget to enable and configure payments with [@BotFather](https://telegram.me/BotFather).
2727

28+
### [`persistentconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/persistentconversationbot.py)
29+
A basic example of a bot store conversation state and user_data over multiple restarts.
30+
2831
## Pure API
2932
The [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) example uses only the pure, "bare-metal" API wrapper.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Simple Bot to reply to Telegram messages
5+
# This program is dedicated to the public domain under the CC0 license.
6+
"""
7+
This Bot uses the Updater class to handle the bot.
8+
9+
First, a few callback functions are defined. Then, those functions are passed to
10+
the Dispatcher and registered at their respective places.
11+
Then, the bot is started and runs until we press Ctrl-C on the command line.
12+
13+
Usage:
14+
Example of a bot-user conversation using ConversationHandler.
15+
Send /start to initiate the conversation.
16+
Press Ctrl-C on the command line or send a signal to the process to stop the
17+
bot.
18+
"""
19+
20+
from telegram import ReplyKeyboardMarkup
21+
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, RegexHandler,
22+
ConversationHandler, PicklePersistence)
23+
24+
import logging
25+
26+
# Enable logging
27+
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
28+
level=logging.INFO)
29+
30+
logger = logging.getLogger(__name__)
31+
32+
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)
33+
34+
reply_keyboard = [['Age', 'Favourite colour'],
35+
['Number of siblings', 'Something else...'],
36+
['Done']]
37+
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
38+
39+
40+
def facts_to_str(user_data):
41+
facts = list()
42+
43+
for key, value in user_data.items():
44+
facts.append('{} - {}'.format(key, value))
45+
46+
return "\n".join(facts).join(['\n', '\n'])
47+
48+
49+
def start(bot, update, user_data):
50+
reply_text = "Hi! My name is Doctor Botter."
51+
if user_data:
52+
reply_text += " You already told me your {}. Why don't you tell me something more " \
53+
"about yourself? Or change enything I " \
54+
"already know.".format(", ".join(user_data.keys()))
55+
else:
56+
reply_text += " I will hold a more complex conversation with you. Why don't you tell me " \
57+
"something about yourself?"
58+
update.message.reply_text(reply_text, reply_markup=markup)
59+
60+
return CHOOSING
61+
62+
63+
def regular_choice(bot, update, user_data):
64+
text = update.message.text
65+
user_data['choice'] = text
66+
if user_data.get(text):
67+
reply_text = 'Your {}, I already know the following ' \
68+
'about that: {}'.format(text.lower(), user_data[text.lower()])
69+
else:
70+
reply_text = 'Your {}? Yes, I would love to hear about that!'.format(text.lower())
71+
update.message.reply_text(reply_text)
72+
73+
return TYPING_REPLY
74+
75+
76+
def custom_choice(bot, update):
77+
update.message.reply_text('Alright, please send me the category first, '
78+
'for example "Most impressive skill"')
79+
80+
return TYPING_CHOICE
81+
82+
83+
def received_information(bot, update, user_data):
84+
text = update.message.text
85+
category = user_data['choice']
86+
user_data[category] = text.lower()
87+
del user_data['choice']
88+
89+
update.message.reply_text("Neat! Just so you know, this is what you already told me:"
90+
"{}"
91+
"You can tell me more, or change your opinion on "
92+
"something.".format(facts_to_str(user_data)), reply_markup=markup)
93+
94+
return CHOOSING
95+
96+
97+
def show_data(bot, update, user_data):
98+
update.message.reply_text("This is what you already told me:"
99+
"{}".format(facts_to_str(user_data)))
100+
101+
102+
def done(bot, update, user_data):
103+
if 'choice' in user_data:
104+
del user_data['choice']
105+
106+
update.message.reply_text("I learned these facts about you:"
107+
"{}"
108+
"Until next time!".format(facts_to_str(user_data)))
109+
return ConversationHandler.END
110+
111+
112+
def error(bot, update, error):
113+
"""Log Errors caused by Updates."""
114+
logger.warning('Update "%s" caused error "%s"', update, error)
115+
116+
117+
def main():
118+
# Create the Updater and pass it your bot's token.
119+
pp = PicklePersistence(filename='conversationbot')
120+
updater = Updater("TOKEN", persistence=pp)
121+
122+
# Get the dispatcher to register handlers
123+
dp = updater.dispatcher
124+
125+
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
126+
conv_handler = ConversationHandler(
127+
entry_points=[CommandHandler('start', start, pass_user_data=True)],
128+
129+
states={
130+
CHOOSING: [RegexHandler('^(Age|Favourite colour|Number of siblings)$',
131+
regular_choice,
132+
pass_user_data=True),
133+
RegexHandler('^Something else...$',
134+
custom_choice),
135+
],
136+
137+
TYPING_CHOICE: [MessageHandler(Filters.text,
138+
regular_choice,
139+
pass_user_data=True),
140+
],
141+
142+
TYPING_REPLY: [MessageHandler(Filters.text,
143+
received_information,
144+
pass_user_data=True),
145+
],
146+
},
147+
148+
fallbacks=[RegexHandler('^Done$', done, pass_user_data=True)],
149+
name="my_conversation",
150+
persistent=True
151+
)
152+
153+
dp.add_handler(conv_handler)
154+
155+
show_data_handler = CommandHandler('show_data', show_data, pass_user_data=True)
156+
dp.add_handler(show_data_handler)
157+
# log all errors
158+
dp.add_error_handler(error)
159+
160+
# Start the Bot
161+
updater.start_polling()
162+
163+
# Run the bot until you press Ctrl-C or the process receives SIGINT,
164+
# SIGTERM or SIGABRT. This should be used most of the time, since
165+
# start_polling() is non-blocking and will stop the bot gracefully.
166+
updater.idle()
167+
168+
169+
if __name__ == '__main__':
170+
main()

telegram/ext/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
# along with this program. If not, see [http://www.gnu.org/licenses/].
1919
"""Extensions over the Telegram Bot API to facilitate bot making"""
2020

21+
from .basepersistence import BasePersistence
22+
from .picklepersistence import PicklePersistence
23+
from .dictpersistence import DictPersistence
2124
from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async
2225
from .jobqueue import JobQueue, Job
2326
from .updater import Updater
@@ -43,4 +46,5 @@
4346
'MessageHandler', 'BaseFilter', 'Filters', 'RegexHandler', 'StringCommandHandler',
4447
'StringRegexHandler', 'TypeHandler', 'ConversationHandler',
4548
'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue',
46-
'DispatcherHandlerStop', 'run_async')
49+
'DispatcherHandlerStop', 'run_async', 'BasePersistence', 'PicklePersistence',
50+
'DictPersistence')

telegram/ext/basepersistence.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python
2+
#
3+
# A library that provides a Python interface to the Telegram Bot API
4+
# Copyright (C) 2015-2018
5+
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser Public License
18+
# along with this program. If not, see [http://www.gnu.org/licenses/].
19+
"""This module contains the BasePersistence class."""
20+
21+
22+
class BasePersistence(object):
23+
"""Interface class for adding persistence to your bot.
24+
Subclass this object for different implementations of a persistent bot.
25+
26+
All relevant methods must be overwritten. This means:
27+
28+
* If :attr:`store_chat_data` is ``True`` you must overwrite :meth:`get_chat_data` and
29+
:meth:`update_chat_data`.
30+
* If :attr:`store_user_data` is ``True`` you must overwrite :meth:`get_user_data` and
31+
:meth:`update_user_data`.
32+
* If you want to store conversation data with :class:`telegram.ext.ConversationHandler`, you
33+
must overwrite :meth:`get_conversations` and :meth:`update_conversation`.
34+
* :meth:`flush` will be called when the bot is shutdown.
35+
36+
Attributes:
37+
store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this
38+
persistence class.
39+
store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this
40+
persistence class.
41+
42+
Args:
43+
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
44+
persistence class. Default is ``True``.
45+
store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
46+
persistence class. Default is ``True`` .
47+
"""
48+
49+
def __init__(self, store_user_data=True, store_chat_data=True):
50+
self.store_user_data = store_user_data
51+
self.store_chat_data = store_chat_data
52+
53+
def get_user_data(self):
54+
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
55+
persistence object. It should return the user_data if stored, or an empty
56+
``defaultdict(dict)``.
57+
58+
Returns:
59+
:obj:`defaultdict`: The restored user data.
60+
"""
61+
raise NotImplementedError
62+
63+
def get_chat_data(self):
64+
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
65+
persistence object. It should return the chat_data if stored, or an empty
66+
``defaultdict(dict)``.
67+
68+
Returns:
69+
:obj:`defaultdict`: The restored chat data.
70+
"""
71+
raise NotImplementedError
72+
73+
def get_conversations(self, name):
74+
""""Will be called by :class:`telegram.ext.Dispatcher` when a
75+
:class:`telegram.ext.ConversationHandler` is added if
76+
:attr:`telegram.ext.ConversationHandler.persistent` is ``True``.
77+
It should return the conversations for the handler with `name` or an empty ``dict``
78+
79+
Args:
80+
name (:obj:`str`): The handlers name.
81+
82+
Returns:
83+
:obj:`dict`: The restored conversations for the handler.
84+
"""
85+
raise NotImplementedError
86+
87+
def update_conversation(self, name, key, new_state):
88+
"""Will be called when a :attr:`telegram.ext.ConversationHandler.update_state`
89+
is called. this allows the storeage of the new state in the persistence.
90+
91+
Args:
92+
name (:obj:`str`): The handlers name.
93+
key (:obj:`tuple`): The key the state is changed for.
94+
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
95+
"""
96+
raise NotImplementedError
97+
98+
def update_user_data(self, user_id, data):
99+
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
100+
handled an update.
101+
102+
Args:
103+
user_id (:obj:`int`): The user the data might have been changed for.
104+
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data`[user_id].
105+
"""
106+
raise NotImplementedError
107+
108+
def update_chat_data(self, chat_id, data):
109+
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
110+
handled an update.
111+
112+
Args:
113+
chat_id (:obj:`int`): The chat the data might have been changed for.
114+
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data`[user_id].
115+
"""
116+
raise NotImplementedError
117+
118+
def flush(self):
119+
"""Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
120+
persistence a chance to finish up saving or close a database connection gracefully. If this
121+
is not of any importance just pass will be sufficient.
122+
"""
123+
pass

0 commit comments

Comments
 (0)