This repository was archived by the owner on Jan 5, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 304
Support oauth and oauth prompt #208
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
10611d1
added some oauth related methods to BotFrameworkAdapter
axelsrz a62e825
resolved conflicts with master
axelsrz 2c28fdf
OAuth functionality in BotFrameworkAdapter completed, testing pending…
axelsrz 39b154c
Oauth functionality added, not fully tested
axelsrz 33ff476
Connector working, OAuth Card not constructed properly
axelsrz e5bb2fb
Merge with master
axelsrz 43a1c1e
Oauth prompt working as expected with unit tests
axelsrz f5767cb
Merge branch 'master' of https://github.com/Microsoft/botbuilder-pyth…
axelsrz 4095f0d
resolved merge conflicts
axelsrz baf3270
export oauthprompt from dialogs directly
axelsrz 1536162
Fixes on styling as requested in PR review
axelsrz a781cb6
changes from dotnet PR #1850
axelsrz 347ced1
Adding missing license headers
axelsrz 1f9d9e9
Merge branch 'master' into axsuarez/support-oauth-and-gov
a40edf6
Merge branch 'master' into axsuarez/support-oauth-and-gov
axelsrz ee691d7
Merge branch 'master' into axsuarez/support-oauth-and-gov
axelsrz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,46 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| # TODO: enable this in the future | ||
| # With python 3.7 the line below will allow to do Postponed Evaluation of Annotations. See PEP 563 | ||
| # from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import inspect | ||
| from datetime import datetime | ||
| from typing import Coroutine, List, Callable | ||
| from typing import Coroutine, Dict, List, Callable, Union | ||
| from copy import copy | ||
| from ..bot_adapter import BotAdapter | ||
| from ..turn_context import TurnContext | ||
| from ..turn_context import TurnContext | ||
| from ..user_token_provider import UserTokenProvider | ||
| from botbuilder.schema import (ActivityTypes, Activity, ConversationAccount, | ||
| ConversationReference, ChannelAccount, ResourceResponse) | ||
| ConversationReference, ChannelAccount, ResourceResponse, | ||
| TokenResponse) | ||
|
|
||
|
|
||
| class UserToken: | ||
| def __init__(self, connection_name: str = None, user_id: str = None, channel_id: str = None, token: str = None): | ||
| self.connection_name = connection_name | ||
| self.user_id = user_id | ||
| self.channel_id = channel_id | ||
| self.token = token | ||
|
|
||
| def equals_key(self, rhs: 'UserToken'): | ||
| return (rhs is not None and | ||
| self.connection_name == rhs.connection_name and | ||
| self.user_id == rhs.user_id and | ||
| self.channel_id == rhs.channel_id) | ||
|
|
||
|
|
||
| class TestAdapter(BotAdapter): | ||
| def __init__(self, logic: Coroutine=None, conversation: ConversationReference=None, send_trace_activity: bool = False): | ||
| class TokenMagicCode: | ||
| def __init__(self, key: UserToken = None, magic_code: str = None): | ||
| self.key = key | ||
| self.magic_code = magic_code | ||
|
|
||
|
|
||
| class TestAdapter(BotAdapter, UserTokenProvider): | ||
| def __init__(self, logic: Coroutine = None, conversation: ConversationReference = None, | ||
| send_trace_activity: bool = False): | ||
| """ | ||
| Creates a new TestAdapter instance. | ||
| :param logic: | ||
|
|
@@ -22,6 +49,8 @@ def __init__(self, logic: Coroutine=None, conversation: ConversationReference=No | |
| super(TestAdapter, self).__init__() | ||
| self.logic = logic | ||
| self._next_id: int = 0 | ||
| self._user_tokens: List[UserToken] = [] | ||
| self._magic_codes: List[TokenMagicCode] = [] | ||
| self.activity_buffer: List[Activity] = [] | ||
| self.updated_activities: List[Activity] = [] | ||
| self.deleted_activities: List[ConversationReference] = [] | ||
|
|
@@ -46,10 +75,12 @@ async def send_activities(self, context, activities: List[Activity]): | |
| :param activities: | ||
| :return: | ||
| """ | ||
|
|
||
| def id_mapper(activity): | ||
| self.activity_buffer.append(activity) | ||
| self._next_id += 1 | ||
| return ResourceResponse(id=str(self._next_id)) | ||
|
|
||
| """This if-else code is temporary until the BotAdapter and Bot/TurnContext are revamped.""" | ||
| if type(activities) == list: | ||
| responses = [id_mapper(activity) for activity in activities] | ||
|
|
@@ -96,7 +127,7 @@ async def receive_activity(self, activity): | |
| """ | ||
| if type(activity) == str: | ||
| activity = Activity(type='message', text=activity) | ||
| # Initialize request | ||
| # Initialize request. | ||
| request = copy(self.template) | ||
|
|
||
| for key, value in vars(activity).items(): | ||
|
|
@@ -108,7 +139,7 @@ async def receive_activity(self, activity): | |
| self._next_id += 1 | ||
| request.id = str(self._next_id) | ||
|
|
||
| # Create context object and run middleware | ||
| # Create context object and run middleware. | ||
| context = TurnContext(self, request) | ||
| return await self.run_pipeline(context, self.logic) | ||
|
|
||
|
|
@@ -152,9 +183,71 @@ async def tests(self, *args): | |
| timeout = arg[3] | ||
| await self.test(arg[0], arg[1], description, timeout) | ||
|
|
||
| def add_user_token(self, connection_name: str, channel_id: str, user_id: str, token: str, magic_code: str = None): | ||
| key = UserToken() | ||
| key.channel_id = channel_id | ||
| key.connection_name = connection_name | ||
| key.user_id = user_id | ||
| key.token = token | ||
|
|
||
| if not magic_code: | ||
| self._user_tokens.append(key) | ||
| else: | ||
| mc = TokenMagicCode() | ||
| mc.key = key | ||
| mc.magic_code = magic_code | ||
| self._magic_codes.append(mc) | ||
|
|
||
| async def get_user_token(self, context: TurnContext, connection_name: str, magic_code: str = None) -> TokenResponse: | ||
| key = UserToken() | ||
| key.channel_id = context.activity.channel_id | ||
| key.connection_name = connection_name | ||
| key.user_id = context.activity.from_property.id | ||
|
|
||
| if magic_code: | ||
| magic_code_record = list(filter(lambda x: key.equals_key(x.key), self._magic_codes)) | ||
| if magic_code_record and len(magic_code_record) > 0 and magic_code_record[0].magic_code == magic_code: | ||
| # Move the token to long term dictionary. | ||
| self.add_user_token(connection_name, key.channel_id, key.user_id, magic_code_record[0].key.token) | ||
|
|
||
| # Remove from the magic code list. | ||
| idx = self._magic_codes.index(magic_code_record[0]) | ||
| self._magic_codes = [self._magic_codes.pop(idx)] | ||
|
|
||
| match = list(filter(lambda x: key.equals_key(x), self._user_tokens)) | ||
|
|
||
| if match and len(match) > 0: | ||
| return TokenResponse( | ||
| connection_name=match[0].connection_name, | ||
| token=match[0].token, | ||
| expiration=None | ||
| ) | ||
| else: | ||
| # Not found. | ||
| return None | ||
|
|
||
| async def sign_out_user(self, context: TurnContext, connection_name: str): | ||
| channel_id = context.activity.channel_id | ||
| user_id = context.activity.from_property.id | ||
|
|
||
| new_records = [] | ||
| for token in self._user_tokens: | ||
| if (token.channel_id != channel_id or | ||
| token.user_id != user_id or | ||
| (connection_name and connection_name != token.connection_name)): | ||
| new_records.append(token) | ||
| self._user_tokens = new_records | ||
|
|
||
| async def get_oauth_sign_in_link(self, context: TurnContext, connection_name: str) -> str: | ||
| return f'https://fake.com/oauthsignin/{connection_name}/{context.activity.channel_id}/{context.activity.from_property.id}' | ||
|
|
||
| async def get_aad_tokens(self, context: TurnContext, connection_name: str, resource_urls: List[str]) -> Dict[ | ||
| str, TokenResponse]: | ||
| return None | ||
|
|
||
|
|
||
| class TestFlow(object): | ||
| def __init__(self, previous, adapter: TestAdapter): | ||
| def __init__(self, previous: Callable, adapter: TestAdapter): | ||
| """ | ||
| INTERNAL: creates a new TestFlow instance. | ||
| :param previous: | ||
|
|
@@ -183,6 +276,7 @@ async def send(self, user_says) -> 'TestFlow': | |
| :param user_says: | ||
| :return: | ||
| """ | ||
|
|
||
| async def new_previous(): | ||
| nonlocal self, user_says | ||
| if callable(self.previous): | ||
|
|
@@ -191,14 +285,16 @@ async def new_previous(): | |
|
|
||
| return TestFlow(await new_previous(), self.adapter) | ||
|
|
||
| async def assert_reply(self, expected, description=None, timeout=None) -> 'TestFlow': | ||
| async def assert_reply(self, expected: Union[str, Activity, Callable[[Activity, str], None]], description=None, | ||
| timeout=None) -> 'TestFlow': | ||
| """ | ||
| Generates an assertion if the bots response doesn't match the expected text/activity. | ||
| :param expected: | ||
| :param description: | ||
| :param timeout: | ||
| :return: | ||
| """ | ||
|
|
||
| def default_inspector(reply, description=None): | ||
| if isinstance(expected, Activity): | ||
| validate_activity(reply, expected) | ||
|
|
@@ -209,7 +305,7 @@ def default_inspector(reply, description=None): | |
| if description is None: | ||
| description = '' | ||
|
|
||
| inspector = expected if type(expected) == 'function' else default_inspector | ||
| inspector = expected if callable(expected) else default_inspector | ||
|
|
||
| async def test_flow_previous(): | ||
| nonlocal timeout | ||
|
|
@@ -222,7 +318,7 @@ async def wait_for_activity(): | |
| nonlocal expected, timeout | ||
| current = datetime.now() | ||
| if (current - start).total_seconds() * 1000 > timeout: | ||
| if type(expected) == Activity: | ||
| if isinstance(expected, Activity): | ||
| expecting = expected.text | ||
| elif callable(expected): | ||
| expecting = inspect.getsourcefile(expected) | ||
|
|
@@ -232,11 +328,17 @@ async def wait_for_activity(): | |
| f'{current - start}ms.') | ||
| elif len(adapter.activity_buffer) > 0: | ||
| reply = adapter.activity_buffer.pop(0) | ||
| inspector(reply, description) | ||
| try: | ||
| await inspector(reply, description) | ||
| except Exception: | ||
| inspector(reply, description) | ||
|
|
||
| else: | ||
| await asyncio.sleep(0.05) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sketchy? |
||
| await wait_for_activity() | ||
|
|
||
| await wait_for_activity() | ||
|
|
||
| return TestFlow(await test_flow_previous(), self.adapter) | ||
|
|
||
|
|
||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
consider a list comprehension?