Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
Channels.ms_teams == context.activity.channel_id
and context.activity.conversation is not None
and not context.activity.conversation.tenant_id
and context.activity.channel_data
):
teams_channel_data = context.activity.channel_data
if teams_channel_data.get("tenant", {}).get("id", None):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
TokenResponse,
)
from botframework.connector import Channels
from botframework.connector.auth import ClaimsIdentity, SkillValidation
from .prompt_options import PromptOptions
from .oauth_prompt_settings import OAuthPromptSettings
from .prompt_validator_context import PromptValidatorContext
Expand Down Expand Up @@ -115,7 +116,7 @@ async def begin_dialog(
if output is not None:
return await dialog_context.end_dialog(output)

await self.send_oauth_card(dialog_context.context, options.prompt)
await self._send_oauth_card(dialog_context.context, options.prompt)
return Dialog.end_of_turn

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

if state["state"].get("attemptCount") is None:
state["state"]["attemptCount"] = 1
else:
state["state"]["attemptCount"] += 1

# Validate the return value
is_valid = False
Expand All @@ -142,7 +145,6 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
recognized,
state["state"],
state["options"],
state["state"]["attemptCount"],
)
)
elif recognized.succeeded:
Expand Down Expand Up @@ -188,7 +190,7 @@ async def sign_out_user(self, context: TurnContext):

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

async def send_oauth_card(
async def _send_oauth_card(
self, context: TurnContext, prompt: Union[Activity, str] = None
):
if not isinstance(prompt, Activity):
Expand All @@ -198,11 +200,32 @@ async def send_oauth_card(

prompt.attachments = prompt.attachments or []

if self._channel_suppports_oauth_card(context.activity.channel_id):
if OAuthPrompt._channel_suppports_oauth_card(context.activity.channel_id):
if not any(
att.content_type == CardFactory.content_types.oauth_card
for att in prompt.attachments
):
link = None
card_action_type = ActionTypes.signin
bot_identity: ClaimsIdentity = context.turn_state.get("BotIdentity")

# check if it's from streaming connection
if not context.activity.service_url.startswith("http"):
if not hasattr(context.adapter, "get_oauth_sign_in_link"):
raise Exception(
"OAuthPrompt: get_oauth_sign_in_link() not supported by the current adapter"
)
link = await context.adapter.get_oauth_sign_in_link(
context, self._settings.connection_name
)
elif bot_identity and SkillValidation.is_skill_claim(
bot_identity.claims
):
link = await context.adapter.get_oauth_sign_in_link(
context, self._settings.connection_name
)
card_action_type = ActionTypes.open_url

prompt.attachments.append(
CardFactory.oauth_card(
OAuthCard(
Expand All @@ -212,7 +235,8 @@ async def send_oauth_card(
CardAction(
title=self._settings.title,
text=self._settings.text,
type=ActionTypes.signin,
type=card_action_type,
value=link,
)
],
)
Expand Down Expand Up @@ -251,9 +275,9 @@ async def send_oauth_card(

async def _recognize_token(self, context: TurnContext) -> PromptRecognizerResult:
token = None
if self._is_token_response_event(context):
if OAuthPrompt._is_token_response_event(context):
token = context.activity.value
elif self._is_teams_verification_invoke(context):
elif OAuthPrompt._is_teams_verification_invoke(context):
code = context.activity.value.state
try:
token = await self.get_user_token(context, code)
Expand All @@ -280,22 +304,25 @@ async def _recognize_token(self, context: TurnContext) -> PromptRecognizerResult
else PromptRecognizerResult()
)

def _is_token_response_event(self, context: TurnContext) -> bool:
@staticmethod
def _is_token_response_event(context: TurnContext) -> bool:
activity = context.activity

return (
activity.type == ActivityTypes.event and activity.name == "tokens/response"
)

def _is_teams_verification_invoke(self, context: TurnContext) -> bool:
@staticmethod
def _is_teams_verification_invoke(context: TurnContext) -> bool:
activity = context.activity

return (
activity.type == ActivityTypes.invoke
and activity.name == "signin/verifyState"
)

def _channel_suppports_oauth_card(self, channel_id: str) -> bool:
@staticmethod
def _channel_suppports_oauth_card(channel_id: str) -> bool:
if channel_id in [
Channels.ms_teams,
Channels.cortana,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# EchoBot

Bot Framework v4 echo bot sample.

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.

## Running the sample
- Clone the repository
```bash
git clone https://github.com/Microsoft/botbuilder-python.git
```
- Activate your desired virtual environment
- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
- In the terminal, type `pip install -r requirements.txt`
- In the terminal, type `python app.py`

## Testing the bot using Bot Framework Emulator
[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.

- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)

### Connect to bot using Bot Framework Emulator
- Launch Bot Framework Emulator
- Paste this URL in the emulator window - http://localhost:3978/api/messages

## Further reading

- [Bot Framework Documentation](https://docs.botframework.com)
- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import sys
import traceback
from datetime import datetime

from aiohttp import web
from aiohttp.web import Request, Response
from botbuilder.core import (
BotFrameworkAdapterSettings,
ConversationState,
MemoryStorage,
UserState,
TurnContext,
BotFrameworkAdapter,
)
from botbuilder.schema import Activity, ActivityTypes

from bots import AuthBot
from dialogs import MainDialog
from config import DefaultConfig

CONFIG = DefaultConfig()

# Create adapter.
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
ADAPTER = BotFrameworkAdapter(SETTINGS)

STORAGE = MemoryStorage()

CONVERSATION_STATE = ConversationState(STORAGE)
USER_STATE = UserState(STORAGE)


# Catch-all for errors.
async def on_error(context: TurnContext, error: Exception):
# This check writes out errors to console log .vs. app insights.
# NOTE: In production environment, you should consider logging this to Azure
# application insights.
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
traceback.print_exc()

# Send a message to the user
await context.send_activity("The bot encountered an error or bug.")
await context.send_activity(
"To continue to run this bot, please fix the bot source code."
)
# Send a trace activity if we're talking to the Bot Framework Emulator
if context.activity.channel_id == "emulator":
# Create a trace activity that contains the error object
trace_activity = Activity(
label="TurnError",
name="on_turn_error Trace",
timestamp=datetime.utcnow(),
type=ActivityTypes.trace,
value=f"{error}",
value_type="https://www.botframework.com/schemas/error",
)
# Send a trace activity, which will be displayed in Bot Framework Emulator
await context.send_activity(trace_activity)


ADAPTER.on_turn_error = on_error

DIALOG = MainDialog(CONFIG)


# Listen for incoming requests on /api/messages
async def messages(req: Request) -> Response:
# Create the Bot
bot = AuthBot(CONVERSATION_STATE, USER_STATE, DIALOG)

# Main bot message handler.
if "application/json" in req.headers["Content-Type"]:
body = await req.json()
else:
return Response(status=415)

activity = Activity().deserialize(body)
auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""

try:
await ADAPTER.process_activity(activity, auth_header, bot.on_turn)
return Response(status=201)
except Exception as exception:
raise exception


APP = web.Application()
APP.router.add_post("/api/messages", messages)

if __name__ == "__main__":
try:
web.run_app(APP, host="localhost", port=CONFIG.PORT)
except Exception as error:
raise error
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from .dialog_bot import DialogBot
from .auth_bot import AuthBot

__all__ = ["DialogBot", "AuthBot"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import List

from botbuilder.core import MessageFactory, TurnContext
from botbuilder.schema import ActivityTypes, ChannelAccount

from helpers.dialog_helper import DialogHelper
from bots import DialogBot


class AuthBot(DialogBot):
async def on_turn(self, turn_context: TurnContext):
if turn_context.activity.type == ActivityTypes.invoke:
await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.conversation_state.create_property("DialogState")
)
else:
await super().on_turn(turn_context)

async def on_members_added_activity(
self, members_added: List[ChannelAccount], turn_context: TurnContext
):
for member in members_added:
if member.id != turn_context.activity.recipient.id:
await turn_context.send_activity(
MessageFactory.text("Hello and welcome!")
)

async def on_token_response_event(
self, turn_context: TurnContext
):
print("on token: Running dialog with Message Activity.")

return await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.conversation_state.create_property("DialogState")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext
from botbuilder.dialogs import Dialog

from helpers.dialog_helper import DialogHelper


class DialogBot(ActivityHandler):
def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog):
self.conversation_state = conversation_state
self._user_state = user_state
self.dialog = dialog

async def on_turn(self, turn_context: TurnContext):
await super().on_turn(turn_context)

await self.conversation_state.save_changes(turn_context, False)
await self._user_state.save_changes(turn_context, False)

async def on_message_activity(self, turn_context: TurnContext):
print("on message: Running dialog with Message Activity.")

return await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.conversation_state.create_property("DialogState")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os

""" Bot Configuration """


class DefaultConfig:
""" Bot Configuration """

PORT = 3978
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
CONNECTION_NAME = ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .logout_dialog import LogoutDialog
from .main_dialog import MainDialog

__all__ = [
"LogoutDialog",
"MainDialog"
]
Loading