Skip to content

Commit 8562148

Browse files
axelsrzjohnataylor
authored andcommitted
Axsuarez/oauth prompt skills (#498)
* Skill layer working, oauth prompt and testing pending * pylint: Skill layer working, oauth prompt and testing pending * Updating minor skills PRs to match C# * Removing accidental changes in samples 1. and 13. * Adding custom exception for channel service handler * Skills error handler * Skills error handler * pylint: Solved conflicts w/master * pylint: Solved conflicts w/master * OAuthPrompt working as expected in skill child
1 parent 1ce4c0c commit 8562148

File tree

14 files changed

+412
-10
lines changed

14 files changed

+412
-10
lines changed

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
247247
Channels.ms_teams == context.activity.channel_id
248248
and context.activity.conversation is not None
249249
and not context.activity.conversation.tenant_id
250+
and context.activity.channel_data
250251
):
251252
teams_channel_data = context.activity.channel_data
252253
if teams_channel_data.get("tenant", {}).get("id", None):

libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
TokenResponse,
2525
)
2626
from botframework.connector import Channels
27+
from botframework.connector.auth import ClaimsIdentity, SkillValidation
2728
from .prompt_options import PromptOptions
2829
from .oauth_prompt_settings import OAuthPromptSettings
2930
from .prompt_validator_context import PromptValidatorContext
@@ -115,7 +116,7 @@ async def begin_dialog(
115116
if output is not None:
116117
return await dialog_context.end_dialog(output)
117118

118-
await self.send_oauth_card(dialog_context.context, options.prompt)
119+
await self._send_oauth_card(dialog_context.context, options.prompt)
119120
return Dialog.end_of_turn
120121

121122
async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
@@ -132,6 +133,8 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
132133

133134
if state["state"].get("attemptCount") is None:
134135
state["state"]["attemptCount"] = 1
136+
else:
137+
state["state"]["attemptCount"] += 1
135138

136139
# Validate the return value
137140
is_valid = False
@@ -142,7 +145,6 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
142145
recognized,
143146
state["state"],
144147
state["options"],
145-
state["state"]["attemptCount"],
146148
)
147149
)
148150
elif recognized.succeeded:
@@ -188,7 +190,7 @@ async def sign_out_user(self, context: TurnContext):
188190

189191
return await adapter.sign_out_user(context, self._settings.connection_name)
190192

191-
async def send_oauth_card(
193+
async def _send_oauth_card(
192194
self, context: TurnContext, prompt: Union[Activity, str] = None
193195
):
194196
if not isinstance(prompt, Activity):
@@ -198,11 +200,32 @@ async def send_oauth_card(
198200

199201
prompt.attachments = prompt.attachments or []
200202

201-
if self._channel_suppports_oauth_card(context.activity.channel_id):
203+
if OAuthPrompt._channel_suppports_oauth_card(context.activity.channel_id):
202204
if not any(
203205
att.content_type == CardFactory.content_types.oauth_card
204206
for att in prompt.attachments
205207
):
208+
link = None
209+
card_action_type = ActionTypes.signin
210+
bot_identity: ClaimsIdentity = context.turn_state.get("BotIdentity")
211+
212+
# check if it's from streaming connection
213+
if not context.activity.service_url.startswith("http"):
214+
if not hasattr(context.adapter, "get_oauth_sign_in_link"):
215+
raise Exception(
216+
"OAuthPrompt: get_oauth_sign_in_link() not supported by the current adapter"
217+
)
218+
link = await context.adapter.get_oauth_sign_in_link(
219+
context, self._settings.connection_name
220+
)
221+
elif bot_identity and SkillValidation.is_skill_claim(
222+
bot_identity.claims
223+
):
224+
link = await context.adapter.get_oauth_sign_in_link(
225+
context, self._settings.connection_name
226+
)
227+
card_action_type = ActionTypes.open_url
228+
206229
prompt.attachments.append(
207230
CardFactory.oauth_card(
208231
OAuthCard(
@@ -212,7 +235,8 @@ async def send_oauth_card(
212235
CardAction(
213236
title=self._settings.title,
214237
text=self._settings.text,
215-
type=ActionTypes.signin,
238+
type=card_action_type,
239+
value=link,
216240
)
217241
],
218242
)
@@ -251,9 +275,9 @@ async def send_oauth_card(
251275

252276
async def _recognize_token(self, context: TurnContext) -> PromptRecognizerResult:
253277
token = None
254-
if self._is_token_response_event(context):
278+
if OAuthPrompt._is_token_response_event(context):
255279
token = context.activity.value
256-
elif self._is_teams_verification_invoke(context):
280+
elif OAuthPrompt._is_teams_verification_invoke(context):
257281
code = context.activity.value.state
258282
try:
259283
token = await self.get_user_token(context, code)
@@ -280,22 +304,25 @@ async def _recognize_token(self, context: TurnContext) -> PromptRecognizerResult
280304
else PromptRecognizerResult()
281305
)
282306

283-
def _is_token_response_event(self, context: TurnContext) -> bool:
307+
@staticmethod
308+
def _is_token_response_event(context: TurnContext) -> bool:
284309
activity = context.activity
285310

286311
return (
287312
activity.type == ActivityTypes.event and activity.name == "tokens/response"
288313
)
289314

290-
def _is_teams_verification_invoke(self, context: TurnContext) -> bool:
315+
@staticmethod
316+
def _is_teams_verification_invoke(context: TurnContext) -> bool:
291317
activity = context.activity
292318

293319
return (
294320
activity.type == ActivityTypes.invoke
295321
and activity.name == "signin/verifyState"
296322
)
297323

298-
def _channel_suppports_oauth_card(self, channel_id: str) -> bool:
324+
@staticmethod
325+
def _channel_suppports_oauth_card(channel_id: str) -> bool:
299326
if channel_id in [
300327
Channels.ms_teams,
301328
Channels.cortana,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# EchoBot
2+
3+
Bot Framework v4 echo bot sample.
4+
5+
This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
6+
7+
## Running the sample
8+
- Clone the repository
9+
```bash
10+
git clone https://github.com/Microsoft/botbuilder-python.git
11+
```
12+
- Activate your desired virtual environment
13+
- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
14+
- In the terminal, type `pip install -r requirements.txt`
15+
- In the terminal, type `python app.py`
16+
17+
## Testing the bot using Bot Framework Emulator
18+
[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
19+
20+
- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
21+
22+
### Connect to bot using Bot Framework Emulator
23+
- Launch Bot Framework Emulator
24+
- Paste this URL in the emulator window - http://localhost:3978/api/messages
25+
26+
## Further reading
27+
28+
- [Bot Framework Documentation](https://docs.botframework.com)
29+
- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
30+
- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import sys
5+
import traceback
6+
from datetime import datetime
7+
8+
from aiohttp import web
9+
from aiohttp.web import Request, Response
10+
from botbuilder.core import (
11+
BotFrameworkAdapterSettings,
12+
ConversationState,
13+
MemoryStorage,
14+
UserState,
15+
TurnContext,
16+
BotFrameworkAdapter,
17+
)
18+
from botbuilder.schema import Activity, ActivityTypes
19+
20+
from bots import AuthBot
21+
from dialogs import MainDialog
22+
from config import DefaultConfig
23+
24+
CONFIG = DefaultConfig()
25+
26+
# Create adapter.
27+
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
28+
SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
29+
ADAPTER = BotFrameworkAdapter(SETTINGS)
30+
31+
STORAGE = MemoryStorage()
32+
33+
CONVERSATION_STATE = ConversationState(STORAGE)
34+
USER_STATE = UserState(STORAGE)
35+
36+
37+
# Catch-all for errors.
38+
async def on_error(context: TurnContext, error: Exception):
39+
# This check writes out errors to console log .vs. app insights.
40+
# NOTE: In production environment, you should consider logging this to Azure
41+
# application insights.
42+
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
43+
traceback.print_exc()
44+
45+
# Send a message to the user
46+
await context.send_activity("The bot encountered an error or bug.")
47+
await context.send_activity(
48+
"To continue to run this bot, please fix the bot source code."
49+
)
50+
# Send a trace activity if we're talking to the Bot Framework Emulator
51+
if context.activity.channel_id == "emulator":
52+
# Create a trace activity that contains the error object
53+
trace_activity = Activity(
54+
label="TurnError",
55+
name="on_turn_error Trace",
56+
timestamp=datetime.utcnow(),
57+
type=ActivityTypes.trace,
58+
value=f"{error}",
59+
value_type="https://www.botframework.com/schemas/error",
60+
)
61+
# Send a trace activity, which will be displayed in Bot Framework Emulator
62+
await context.send_activity(trace_activity)
63+
64+
65+
ADAPTER.on_turn_error = on_error
66+
67+
DIALOG = MainDialog(CONFIG)
68+
69+
70+
# Listen for incoming requests on /api/messages
71+
async def messages(req: Request) -> Response:
72+
# Create the Bot
73+
bot = AuthBot(CONVERSATION_STATE, USER_STATE, DIALOG)
74+
75+
# Main bot message handler.
76+
if "application/json" in req.headers["Content-Type"]:
77+
body = await req.json()
78+
else:
79+
return Response(status=415)
80+
81+
activity = Activity().deserialize(body)
82+
auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
83+
84+
try:
85+
await ADAPTER.process_activity(activity, auth_header, bot.on_turn)
86+
return Response(status=201)
87+
except Exception as exception:
88+
raise exception
89+
90+
91+
APP = web.Application()
92+
APP.router.add_post("/api/messages", messages)
93+
94+
if __name__ == "__main__":
95+
try:
96+
web.run_app(APP, host="localhost", port=CONFIG.PORT)
97+
except Exception as error:
98+
raise error
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from .dialog_bot import DialogBot
4+
from .auth_bot import AuthBot
5+
6+
__all__ = ["DialogBot", "AuthBot"]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import List
5+
6+
from botbuilder.core import MessageFactory, TurnContext
7+
from botbuilder.schema import ActivityTypes, ChannelAccount
8+
9+
from helpers.dialog_helper import DialogHelper
10+
from bots import DialogBot
11+
12+
13+
class AuthBot(DialogBot):
14+
async def on_turn(self, turn_context: TurnContext):
15+
if turn_context.activity.type == ActivityTypes.invoke:
16+
await DialogHelper.run_dialog(
17+
self.dialog,
18+
turn_context,
19+
self.conversation_state.create_property("DialogState")
20+
)
21+
else:
22+
await super().on_turn(turn_context)
23+
24+
async def on_members_added_activity(
25+
self, members_added: List[ChannelAccount], turn_context: TurnContext
26+
):
27+
for member in members_added:
28+
if member.id != turn_context.activity.recipient.id:
29+
await turn_context.send_activity(
30+
MessageFactory.text("Hello and welcome!")
31+
)
32+
33+
async def on_token_response_event(
34+
self, turn_context: TurnContext
35+
):
36+
print("on token: Running dialog with Message Activity.")
37+
38+
return await DialogHelper.run_dialog(
39+
self.dialog,
40+
turn_context,
41+
self.conversation_state.create_property("DialogState")
42+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext
5+
from botbuilder.dialogs import Dialog
6+
7+
from helpers.dialog_helper import DialogHelper
8+
9+
10+
class DialogBot(ActivityHandler):
11+
def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog):
12+
self.conversation_state = conversation_state
13+
self._user_state = user_state
14+
self.dialog = dialog
15+
16+
async def on_turn(self, turn_context: TurnContext):
17+
await super().on_turn(turn_context)
18+
19+
await self.conversation_state.save_changes(turn_context, False)
20+
await self._user_state.save_changes(turn_context, False)
21+
22+
async def on_message_activity(self, turn_context: TurnContext):
23+
print("on message: Running dialog with Message Activity.")
24+
25+
return await DialogHelper.run_dialog(
26+
self.dialog,
27+
turn_context,
28+
self.conversation_state.create_property("DialogState")
29+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License.
4+
5+
import os
6+
7+
""" Bot Configuration """
8+
9+
10+
class DefaultConfig:
11+
""" Bot Configuration """
12+
13+
PORT = 3978
14+
APP_ID = os.environ.get("MicrosoftAppId", "")
15+
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
16+
CONNECTION_NAME = ""
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .logout_dialog import LogoutDialog
2+
from .main_dialog import MainDialog
3+
4+
__all__ = [
5+
"LogoutDialog",
6+
"MainDialog"
7+
]

0 commit comments

Comments
 (0)