Skip to content

Commit

Permalink
feat: added on_callback; updated debug backend; updated reply; update…
Browse files Browse the repository at this point in the history
…d docs and tests (closes #58)
  • Loading branch information
michaelkryukov committed Aug 23, 2021
1 parent 583b4f7 commit f2271a3
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 12 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
> Changes to public API are marked as `^`. Possible changes
> to public API are marked as `^?`.
- v5.1.0
- (VKontakte) Added decorator `@plugin.vk.on_callbacks` (closes #58).
- (VKontakte) Changed API version from `5.122` to `5.131`.
- Added requests capturing to debug backend.
- Updated `reply` to raise if no `default_target_id` found.

- v5.0.5
- ^(VKontakte) Attachments with type `image` now returns largest image.
- Added support for expect_sender for telegram 'callback_query'.
Expand Down
4 changes: 4 additions & 0 deletions kutana/backends/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, messages=None, on_complete=None, save_replies=True, **kwargs)
self.answers_count = 0

self.responses = []
self.requests = []

self.on_complete = on_complete

Expand Down Expand Up @@ -60,6 +61,9 @@ async def execute_send(self, target_id, message, attachments, kwargs):

self.check_if_complete()

async def execute_request(self, method, kwargs):
self.requests.append((method, kwargs))

def check_if_complete(self):
if self.messages or not self.on_complete:
return
Expand Down
10 changes: 5 additions & 5 deletions kutana/backends/vkontakte/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(
token,
session=None,
requests_per_second=19,
api_version="5.122",
api_version="5.131",
api_url="https://api.vk.com",
**kwargs,
):
Expand All @@ -53,10 +53,10 @@ def __init__(

self.default_updates_settings = dict(
message_new=1, message_reply=0, message_allow=0,
message_deny=0, message_edit=0, photo_new=0, audio_new=0,
video_new=0, wall_reply_new=0, wall_reply_edit=0,
wall_reply_delete=0, wall_reply_restore=0, wall_post_new=0,
wall_repost=0, board_post_new=0, board_post_edit=0,
message_deny=0, message_edit=0, message_event=1,
photo_new=0, audio_new=0, video_new=0, wall_reply_new=0,
wall_reply_edit=0, wall_reply_delete=0, wall_reply_restore=0,
wall_post_new=0, wall_repost=0, board_post_new=0, board_post_edit=0,
board_post_restore=0, board_post_delete=0, photo_comment_new=0,
photo_comment_edit=0, photo_comment_delete=0,
photo_comment_restore=0, video_comment_new=0,
Expand Down
83 changes: 83 additions & 0 deletions kutana/backends/vkontakte/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ def _to_hashable(self, obj):
return tuple(
(k, self._to_hashable(v)) for k, v in sorted(obj.items())
)

if isinstance(obj, list):
return tuple(self._to_hashable(o) for o in obj)

return obj

def _get_keys(self, update, ctx):
Expand All @@ -64,6 +66,23 @@ def add_handler(self, handler, key):
return super().add_handler(handler, self._to_hashable(key))


class CallbackPayloadRouter(PayloadRouter):
def _get_keys(self, update, ctx):
if ctx.backend.get_identity() != "vkontakte":
return

if update.type != UpdateType.UPD or update.raw["type"] != "message_event":
return

payload = update.raw["object"]["payload"]

if isinstance(payload, dict):
for key_set in self.possible_key_sets:
yield self._to_hashable(pick(payload, key_set))
else:
yield self._to_hashable(payload)


class ActionMessageRouter(MapRouter):
__slots__ = ()

Expand Down Expand Up @@ -143,6 +162,70 @@ async def wrapper(update, ctx):

return decorator

def on_callbacks(
self,
payloads,
priority=0,
router_priority=None,
):
"""
Decorator for registering coroutine to be called when
incoming update is message event and payload have specified
content. Excessive fields in objects are ignored. Use
strings and numbers for exact matching.
Context is automatically populated with following values:
- payload
- message_event (object with fields event_id, user_id and peer_id)
- conversation_message_id
- sendMessageEventAnswer (helper method for "messages.sendMessageEventAnswer")
Currently wildcards are not supported. You can always use objects in your
buttons, and then "@plugin.vk.on_callbacks([{}])" will catch all callbacks,
because provided snippet only checks that payload is dict.
See :class:`kutana.plugin.Plugin.on_commands` for details
about 'priority' and 'router_priority'.
"""

def decorator(func):
async def wrapper(update, ctx):
obj = update.raw["object"]

ctx.payload = obj.get("payload")

ctx.message_event = {
"event_id": obj["event_id"],
"user_id": obj["user_id"],
"peer_id": obj["peer_id"],
}

ctx.conversation_message_id = obj.get("conversation_message_id")

async def send_message_event_answer(event_data, **kwargs):
return await ctx.request("messages.sendMessageEventAnswer", **{
"event_data": json.dumps(event_data),
**ctx.message_event,
**kwargs,
})

ctx.send_message_event_answer = send_message_event_answer

return await func(update, ctx)

for payload in payloads:
self.plugin._add_handler_for_router(
CallbackPayloadRouter,
handler=Handler(wrapper, priority),
handler_key=payload,
router_priority=router_priority,
)

return func

return decorator

def on_message_actions(
self,
action_types,
Expand Down
3 changes: 3 additions & 0 deletions kutana/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ async def reply(self, message, attachments=(), **kwargs):
If message is too long - it will be splitted.
"""

if self.default_target_id is None:
raise RuntimeError("Target for reply is not found")

return await self.send_message(
self.default_target_id,
message,
Expand Down
6 changes: 3 additions & 3 deletions kutana/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ def _get_or_add_router(self, router_cls, priority=None):

def _add_handler_for_router(self, router, handler, handler_key=None, router_priority=None):
router = self._get_or_add_router(router, priority=router_priority)
if handler_key:
router.add_handler(handler, handler_key)
else:
if handler_key is None:
router.add_handler(handler)
else:
router.add_handler(handler, handler_key)

@property
def storage(self):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import setuptools


VERSION = "5.0.5"
VERSION = "5.1.0.dev0"


with open("README.md", "r") as fh:
Expand Down
12 changes: 11 additions & 1 deletion tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ def test_reply_non_str():
ctx = Context(backend=MagicMock(execute_send=CoroutineMock()))
ctx.default_target_id = 1
asyncio.get_event_loop().run_until_complete(ctx.reply(123))
ctx.backend.execute_send.assert_called_with(1, '123', (), {})
ctx.backend.execute_send.assert_called_with(1, "123", (), {})


def test_reply_no_default_target():
ctx = Context(backend=MagicMock(execute_send=CoroutineMock()))
ctx.default_target_id = None

with pytest.raises(RuntimeError):
asyncio.get_event_loop().run_until_complete(ctx.reply("hey"))

ctx.backend.execute_send.assert_not_called()


def test_context():
Expand Down
1 change: 0 additions & 1 deletion tests/test_vkontakte.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,6 @@ async def _get_response(self, method, kwargs={}):
async def __(message, ctx):
assert ctx.resolve_screen_name
assert ctx.reply

await ctx.reply(message.text, attachments=message.attachments)

app.add_plugin(echo_plugin)
Expand Down
3 changes: 3 additions & 0 deletions tests/test_vkontakte_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@
".echo": {'type': 'message_new', 'object': {"message": {'date': 1569715006, 'from_id': 87641997, 'id': 47101, 'out': 0, 'peer_id': 87641997, 'text': '.echo', 'conversation_message_id': 42164, 'fwd_messages': [], 'important': False, 'random_id': 0, 'attachments': [], 'is_hidden': False}, 'group_id': 342296307}},
".echo chat": {'type': 'message_new', 'object': {"message": {'date': 1569715006, 'from_id': 87641997, 'id': 47101, 'out': 0, 'peer_id': 2000000000 + 87641997, 'text': '[sdffff23f23|Спасибо 2] .echo chat [michaelkrukov|Михаил]', 'conversation_message_id': 42164, 'fwd_messages': [], 'important': False, 'random_id': 0, 'attachments': [], 'is_hidden': False}, 'group_id': 342296307}},
".echo wa": {'type': 'message_new', 'object': {"message": {'date': 1569715006, 'from_id': 87641997, 'id': 47101, 'out': 0, 'peer_id': 87641997, 'text': '.echo wa', 'conversation_message_id': 42164, 'fwd_messages': [], 'important': False, 'random_id': 0, 'attachments': [ATTACHMENTS["image"]], 'is_hidden': False}, 'group_id': 342296307}},
"inline_callback_1": {'type': 'message_event', 'object': {'user_id': 87641997, 'peer_id': 87641997, 'event_id': '3159dc190b1g', 'payload': {'val': 1}, 'conversation_message_id': 42164}, 'group_id': 1, 'event_id': '728189fc2e495db59e4b169g881b6666c205ee71'},
"inline_callback_2": {'type': 'message_event', 'object': {'user_id': 87641997, 'peer_id': 87641997, 'event_id': '3159dc190b1g', 'payload': {'val': 2}, 'conversation_message_id': 42164}, 'group_id': 1, 'event_id': '728189fc2e495db59e4b169g881b6666c205ee71'},
"inline_callback_val": {'type': 'message_event', 'object': {'user_id': 87641997, 'peer_id': 87641997, 'event_id': '3159dc190b1g', 'payload': 'val', 'conversation_message_id': 42164}, 'group_id': 1, 'event_id': '728189fc2e495db59e4b169g881b6666c205ee71'},
}
73 changes: 72 additions & 1 deletion tests/test_vkontakte_extensions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import pytest
from kutana import (
Plugin, Message, Update, UpdateType, Attachment, HandlerResponse as hr,
Plugin, Message, Update, UpdateType, HandlerResponse as hr,
)
from test_vkontakte_data import MESSAGES
from testing_tools import make_kutana_no_run


Expand Down Expand Up @@ -178,3 +179,73 @@ async def __(msg, ctx):
assert len(debug.answers[1]) == 2
assert debug.answers[1][0] == ("hello", (), {})
assert debug.answers[1][1] == ("sup", (), {})


def test_on_callback():
app, debug, hu = make_kutana_no_run(backend_source="vkontakte")

pl = Plugin("")

@pl.vk.on_callbacks([{"val": 1}])
async def __(upd, ctx):
await ctx.send_message_event_answer({
"type": "show_snackbar",
"text": "hey hey hey",
})

@pl.vk.on_callbacks(['val'])
async def __(upd, ctx):
await ctx.send_message_event_answer({
"type": "show_snackbar",
"text": "val val val",
})

app.add_plugin(pl)

hu(Update(MESSAGES["inline_callback_1"], UpdateType.UPD))
hu(Update(MESSAGES["inline_callback_2"], UpdateType.UPD))
hu(Update(MESSAGES["inline_callback_val"], UpdateType.UPD))
hu(Message(make_message_update({"val": 1}), UpdateType.MSG, "hey3", (), 1, 0, 0, 0))

assert debug.requests == [
("messages.sendMessageEventAnswer", {
'event_data': '{"type": "show_snackbar", "text": "hey hey hey"}',
'event_id': '3159dc190b1g',
'user_id': 87641997,
'peer_id': 87641997,
}),
("messages.sendMessageEventAnswer", {
'event_data': '{"type": "show_snackbar", "text": "val val val"}',
'event_id': '3159dc190b1g',
'user_id': 87641997,
'peer_id': 87641997,
}),
]


def test_ignore_non_vkontakte():
app, debug, hu = make_kutana_no_run()

pl = Plugin("")

@pl.vk.on_payloads([{"command": "echo"}])
async def __(msg, ctx):
await ctx.reply(ctx.payload["text"])

@pl.vk.on_callbacks([{"val": 1}])
async def __(upd, ctx):
await ctx.send_message_event_answer({
"type": "show_snackbar",
"text": "hey hey hey",
})

app.add_plugin(pl)

raw1 = make_message_update('{"command": "echo", "text": "hello"}')
raw2 = MESSAGES["inline_callback_1"]

hu(Message(raw1, UpdateType.MSG, "hey1", (), 1, 0, 0, 0))
hu(Update(raw2, UpdateType.UPD))

assert not debug.answers
assert not debug.requests

0 comments on commit f2271a3

Please sign in to comment.