-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Arbitrary callback data #1844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Arbitrary callback data #1844
Conversation
5caf64e
to
ca2fbb5
Compare
IISC this doesn't handle inline keyboards attached to inline result messages yet. If we revisit this PR, that's one issue to think about |
We just discussed this and will be going forward with it. A few notes:
sketch for a LRU data class: class LRUDict:
data = {}
maxsize = 1000
def put(self, key, object):
if len(self.data) == maxsize:
self.data.pop(next(iter(self.data))) # dicts are only guaranteed to be ordered since py3.7, so maybe use OrderedDict instead …
self.data[key] = object Edit: Couple with linked list to make efficient |
@Bibo-Joshi Ordered dict is not LRU. Though, writing this, I think that if the intention is to immediately remove items when they're accessed then an Ordered Dict would be enough. (Though it might be limiting if we change the design) |
without tests or pre-commit for now # Conflicts: # telegram/bot.py # telegram/callbackquery.py # telegram/error.py # telegram/ext/basepersistence.py # telegram/ext/callbackqueryhandler.py # telegram/ext/dictpersistence.py # telegram/ext/dispatcher.py # telegram/ext/picklepersistence.py # telegram/ext/updater.py # telegram/utils/helpers.py # telegram/utils/webhookhandler.py # tests/test_bot.py # tests/test_callbackquery.py # tests/test_helpers.py # tests/test_persistence.py # tests/test_updater.py
Merging master was surprisingly easy :D |
After internal discussion, using the uuid should be enough, so the latest commit removes the signing procedure.
Edit I: We could think of making Edit II: just skipping CQs with invalid data is a bad idea. Users might wanna reply with a prompt telling them that the button no longer works. Rather set the data attribute to the InvalidCallbackData exception or something like that. Edit III: Inline messages. We don't get the keyboard back when buttons from those are pressed. Idea: instead of making the LRU cache per button, make it per keyboard. we have space for 2 uuids in 64chars anyway. So once we get a CQ, we know all the other buttons/objects that we sent along with it and can make use of Edit I. Ofc when answering an inline query with multiple results that have an inline keyboard, we don't know which one actually gets sent, but that's what the LRU cache is good for. |
# Conflicts: # telegram/callbackquery.py
Thanks for the thorough review @harshil21, you found a lot of things to improve 😃 So far I've gone through your comments and marked all as resolved that where either clear or where you reacted with 👍 to my comments. I will get to the TODO section soonish. Regarding the other comments:
Unfortunately this doesn't allow for the entries not to expire :/
Upsie, read that too late :D already merged that
stay tuned 🎶 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there. Relax, I am just a little warning for the maintainers to release directly after merging your PR, otherwise we have broken examples and people might get confused :)
I added an example that can be reviewed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay example looks good. However, I noticed one thing while running the example - When a user spam clicks a button (say '1'), it becomes an instance of InvalidCallbackData
, and it fails. Of course when you remove the drop_callback_data
line, this doesn't happen anymore. But maybe such behaviour should be at least documented somewhere?
Also can we remove the cast()
in the example and ask mypy to ignore them? makes it a little complicated for beginners.
Ah, yes, that's right. In fact it could be considered a feature, see #2539 :D Anyway, I've updated the wiki above as follows:
Note that I also scraped the sentence about
I'm not too sure about this, tbh. If we drop # Get the data from the callback_data.
number, number_list = cast(Tuple[int, List[int]], query.data) because if you use type hinting, you'll have to do that cast in any case when using arbitrary callback data, so I think the example should showcase it. I could live with dropping the other two casts, though …. (edit: I did) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
# Conflicts: # docs/source/telegram.ext.utils.types.rst # telegram/ext/__init__.py # telegram/ext/basepersistence.py # telegram/ext/callbackcontext.py # telegram/ext/conversationhandler.py # telegram/ext/dictpersistence.py # telegram/ext/dispatcher.py # telegram/ext/picklepersistence.py # telegram/ext/updater.py # telegram/ext/utils/types.py # telegram/utils/types.py # tests/test_persistence.py # tests/test_slots.py
This description is quite outdated, please see the discussion below
ToDo
Persistence.store_callback_data
default toTrue
and makeBP.get/update_callback_data
abstract methods. Currently they're not b/c backwards compatibility ….. versionadded:: directives
Defaults
fromtg.Bot
completey and move them totg.ext.Bot
. We may have to think about how to handle signatures withDefaultVaule
, then, i.e. if_insert_defaults
should stay intg.Bot
or should be moved totg.ext.Bot
.This PR adds the functionality to
callback_data
toInlineKeyboardButtons
pattern
argument forCallbackQueryHandler
may now be a callable accepting thecallback_data
as argument.callback_data
received in an update is the same as the sent oneThis is opt-in, so we're not forcing anyone to use it.
Closes #709
How does it work
Passing arbitrary objects as
callback_data
This is achieved by replacing them by their
id()
and keeping a dictionary that maps the ids to the objects. I decided to useid()
overuuid
or similar becauseid()
is unique, so that's covered already andOf course, this ties in with persistence. As for the roles PR #1789: Adding stuff to persistence should be reserverd for major features. IMHO, this is one of them.
Signing the data
The callback data sent by the bot will read
signature data
.signature
is generated usinghmac
from the standard library, where the key consists of the bots token and username and the message consits of thedata
and thechat_id
, the message was sent to.Upon receiving an update,
CallbackQuery.de_json
will check, that the data is valid by checking the signature. Malicious updates will be discarded and logged.Generating the signature and validation done by methods added to the
helpers
module.Users can decide, not to validate the data. This can be useful, when the token has been revoked and they want the old buttons to still work.
Wiki Page: Callback Data
Arbitrary objects as
callback_data
Telegrams Bot API only accepts strings with length up to 64 bytes as
callback_data
forInlineKeyboardButtons
, which sometimes is quite a limitation.With PTB you are able, to pass any object as
callback_data
. This is achieved by storing the object and passing a unique id to Telegram. When aCallbackQuery
is recieved, the id in thecallback_data
is replaced with the stored object. To use this feature, set the parameterarbitrary_callback_data
for yourUpdater
orBot
instance toTrue
.This means two thigs for you:
If you don't use persistence, buttons won't work after restarting your bot, as the stored updates are lost. More precicely, the
callback_data
you will recieve isNone
. If you don't need persistence otherwise, you can setstore_callback_data
toTrue
and all the others toFalse
.When using the
CallbackQueryHandler
, thepattern
argument can now be eithercallback_data
is in fact a stringcallback_data
as only argument. You can perform any kinds of tests on thecallback_data
and returnTrue
oreFalse
accordingslySecurity of InlineKeyboardButtons
Callback updates are not sent by Telegram, but by the client. This means that they can be manipulated by a user. (While Telegram inofficially does try to prevent this, they don't want Bot devs to rely on them doing the security checks).
Most of the time, this is not really a problem, since
callback_data
often just isYes
,No
, etc. However, if the callback data is something likedelete message_id 123
, the malicious user could delete any message sent by the bot.To increase security of callbacks, PTB signs outgoing callback data. The signature not only makes sure that the
callback_data
is valid, but also that the update comes from the chat, the message with theInlineKeyboardButton
was sent in. Incoming updates with an invalid signature will be rejected.The signature is based on your bots token and username. This means that old buttons will no longer be accepted, if you revoke your token. If you need to revoke your token, e.g. during developement, and want the buttons to still work, you can pass
to either your
Updater
orBot
instance. Make sure to switch it toTrue
after some time.