Skip to content

Commit e1ed389

Browse files
committed
Add support for receiving callbacks
1 parent 954a119 commit e1ed389

File tree

8 files changed

+218
-2
lines changed

8 files changed

+218
-2
lines changed

botogram/bot.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import requests.exceptions
2626

2727
from . import api
28+
from . import callbacks
2829
from . import objects
2930
from . import runner
3031
from . import defaults
@@ -87,6 +88,7 @@ def __init__(self, api_connection):
8788
messages.process_channel_post)
8889
self.register_update_processor("edited_channel_post",
8990
messages.process_channel_post_edited)
91+
self.register_update_processor("callback_query", callbacks.process)
9092

9193
self._bot_id = str(uuid.uuid4())
9294

@@ -178,6 +180,13 @@ def __(func):
178180
return func
179181
return __
180182

183+
def callback(self, name):
184+
"""Register a new callback"""
185+
def __(func):
186+
self._main_component.add_callback(name, func)
187+
return func
188+
return __
189+
181190
def timer(self, interval):
182191
"""Register a new timer"""
183192
def __(func):

botogram/callbacks.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright (c) 2015-2017 The Botogram Authors (see AUTHORS)
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
20+
21+
def parse_callback_data(data):
22+
"""Parse the callback data generated by botogram and return it"""
23+
if "\0" in data:
24+
name, custom = data.split("\0", 1)
25+
return name, custom
26+
else:
27+
return data, None
28+
29+
30+
def process(bot, chains, update):
31+
"""Process a callback sent to the bot"""
32+
for hook in chains["callbacks"]:
33+
bot.logger.debug("Processing update #%s with the hook %s" %
34+
(update.update_id, hook.name))
35+
36+
result = hook.call(bot, update)
37+
if result is True:
38+
bot.logger.debug("Update #%s was just processed by the %s hook" %
39+
(update.update_id, hook.name))
40+
return
41+
42+
bot.logger.debug("No hook actually processed the #%s update." %
43+
update.update_id)

botogram/components.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __new__(cls, *args, **kwargs):
3737
self = super(Component, cls).__new__(cls)
3838

3939
self.__commands = {}
40+
self.__callbacks = {}
4041
self.__processors = []
4142
self.__no_commands = []
4243
self.__before_processors = []
@@ -132,6 +133,19 @@ def add_command(self, name, func, hidden=False, order=0, _from_main=False):
132133
command = commands.Command(hook)
133134
self.__commands[name] = command
134135

136+
def add_callback(self, name, func):
137+
"""Add a new callback"""
138+
if name in self.__callbacks:
139+
raise NameError("The callback %s already exists" % name)
140+
141+
if not callable(func):
142+
raise ValueError("A callback must be callable")
143+
144+
hook = hooks.CallbackHook(func, self, {
145+
"name": name,
146+
})
147+
self.__callbacks[name] = hook
148+
135149
def add_timer(self, interval, func):
136150
"""Register a new timer"""
137151
if not callable(func):
@@ -212,7 +226,11 @@ def _get_chains(self):
212226
"chat_unavalable_hooks": [self.__chat_unavailable_hooks],
213227
"messages_edited": [self.__messages_edited_hooks],
214228
"channel_post": [self.__channel_post_hooks],
215-
"channel_post_edited": [self.__channel_post_edited_hooks]
229+
"channel_post_edited": [self.__channel_post_edited_hooks],
230+
"callbacks": [[
231+
self.__callbacks[name]
232+
for name in sorted(self.__callbacks.keys())
233+
]],
216234
}
217235

218236
def _get_commands(self):

botogram/frozenbot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def command(self, name, hidden=False):
124124
"""Register a new command"""
125125
raise FrozenBotError("Can't add commands to a bot at runtime")
126126

127+
def callback(self, name, hidden=False):
128+
"""Register a new callback"""
129+
raise FrozenBotError("Can't add callbacks to a bot at runtime")
130+
127131
def timer(self, interval):
128132
"""Register a new timer"""
129133
raise FrozenBotError("Can't add timers to a bot at runtime")

botogram/hooks.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import re
2121

22+
from .callbacks import parse_callback_data
23+
2224

2325
class Hook:
2426
"""Base class for all the hooks"""
@@ -206,6 +208,31 @@ def _call(self, bot, update):
206208
return True
207209

208210

211+
class CallbackHook(Hook):
212+
"""Underlying hook for @bot.callback"""
213+
214+
def _after_init(self, args):
215+
self._name = "%s:%s" % (self.component.component_name, args["name"])
216+
217+
def _call(self, bot, update):
218+
if not update.callback_query:
219+
return
220+
q = update.callback_query
221+
222+
name, data = parse_callback_data(q._data)
223+
print(name, self._name)
224+
if name != self._name:
225+
return
226+
227+
bot._call(
228+
self.func, self.component_id, query=q, chat=q.message.chat,
229+
message=q.message, data=data,
230+
)
231+
232+
update.callback_query._maybe_send_noop()
233+
return True
234+
235+
209236
class ChatUnavailableHook(Hook):
210237
"""Underlying hook for @bot.chat_unavailable"""
211238

botogram/objects/callbacks.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (c) 2015-2017 The Botogram Authors (see AUTHORS)
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
# DEALINGS IN THE SOFTWARE.
20+
21+
from .base import BaseObject
22+
from .messages import User, Message
23+
from .mixins import _require_api
24+
25+
26+
class CallbackQuery(BaseObject):
27+
"""Telegram API representation of a callback query
28+
29+
https://core.telegram.org/bots/api#callbackquery
30+
"""
31+
32+
required = {
33+
"id": str,
34+
"from": User,
35+
"message": Message,
36+
"chat_instance": str,
37+
}
38+
optional = {
39+
"inline_message_id": str,
40+
"data": str,
41+
"game_short_name": str,
42+
}
43+
replace_keys = {
44+
"from": "sender",
45+
"data": "_data",
46+
}
47+
48+
def __init__(self, *args, **kwargs):
49+
super().__init__(*args, **kwargs)
50+
51+
self._answered = False
52+
53+
@_require_api
54+
def notify(self, text, alert=False, cache=0):
55+
"""Send a notification or an alert to the user"""
56+
self._answered = True
57+
58+
self._api.call("answerCallbackQuery", {
59+
"callback_query_id": self.id,
60+
"text": text,
61+
"show_alert": alert,
62+
"cache_time": cache,
63+
})
64+
65+
@_require_api
66+
def open_url(self, url, cache=0):
67+
"""Tell the user's client to open an URL"""
68+
self._answered = True
69+
70+
self._api.call("answerCallbackQuery", {
71+
"callback_query_id": self.id,
72+
"url": url,
73+
"cache_time": cache,
74+
})
75+
76+
@_require_api
77+
def _maybe_send_noop(self):
78+
"""Internal function to hide the spinner if needed"""
79+
if self._answered:
80+
return
81+
82+
# Only call this if the query wasn't answered before
83+
self._api.call("answerCallbackQuery", {
84+
"callback_query_id": self.id,
85+
"cache_time": 0
86+
})

botogram/objects/updates.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from .base import BaseObject, multiple
2222

23+
from .callbacks import CallbackQuery
2324
from .messages import Message
2425

2526

@@ -36,7 +37,8 @@ class Update(BaseObject):
3637
"message": Message,
3738
"edited_message": Message,
3839
"channel_post": Message,
39-
"edited_channel_post": Message
40+
"edited_channel_post": Message,
41+
"callback_query": CallbackQuery,
4042
}
4143
_check_equality_ = "update_id"
4244

tests/test_callbacks.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright (c) 2015-2017 The Botogram Authors (see AUTHORS)
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
# DEALINGS IN THE SOFTWARE.
20+
21+
from botogram.callbacks import parse_callback_data
22+
23+
24+
def test_parse_callback_data():
25+
assert parse_callback_data("test") == ("test", None)
26+
assert parse_callback_data("test:something") == ("test:something", None)
27+
assert parse_callback_data("test\0wow") == ("test", "wow")

0 commit comments

Comments
 (0)