-
Notifications
You must be signed in to change notification settings - Fork 294
Conversation Update Scenario #431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
140372d
adding teams activity handler, team info, and teams channel account c…
Virtual-Josh 21876ca
adding conversation update scenario
Virtual-Josh 96f262d
fixing linting issues
Virtual-Josh 2b3f2bf
updating classes to use standard attrs
Virtual-Josh a5c2ef7
cleaning up PR feedback
Virtual-Josh e4d6fd1
adding line
Virtual-Josh f8f3ea5
adding another blank line
Virtual-Josh 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
10 changes: 10 additions & 0 deletions
10
libraries/botbuilder-core/botbuilder/core/Teams/__init__.py
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 |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# -------------------------------------------------------------------------- | ||
|
||
from .teams_activity_handler import TeamsActivityHandler | ||
|
||
__all__ = ["TeamsActivityHandler"] |
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 |
---|---|---|
@@ -0,0 +1,188 @@ | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. | ||
|
||
from http import HTTPStatus | ||
from botbuilder.schema import ActivityTypes, ChannelAccount | ||
from botbuilder.core.turn_context import TurnContext | ||
from botbuilder.core import ActivityHandler, MessageFactory, InvokeResponse | ||
from botbuilder.schema.teams import ( | ||
TeamInfo, | ||
ChannelInfo, | ||
TeamsChannelData, | ||
TeamsChannelAccount, | ||
) | ||
from botframework.connector import Channels | ||
|
||
class TeamsActivityHandler(ActivityHandler): | ||
async def on_turn(self, turn_context: TurnContext): | ||
if turn_context is None: | ||
raise TypeError("ActivityHandler.on_turn(): turn_context cannot be None.") | ||
|
||
if hasattr(turn_context, "activity") and turn_context.activity is None: | ||
raise TypeError( | ||
"ActivityHandler.on_turn(): turn_context must have a non-None activity." | ||
) | ||
|
||
if ( | ||
hasattr(turn_context.activity, "type") | ||
and turn_context.activity.type is None | ||
): | ||
raise TypeError( | ||
"ActivityHandler.on_turn(): turn_context activity must have a non-None type." | ||
) | ||
|
||
if turn_context.activity.type == ActivityTypes.invoke: | ||
invoke_response = await self.on_invoke_activity(turn_context) | ||
else: | ||
await super().on_turn(turn_context) | ||
|
||
async def on_invoke_activity(self, turn_context: TurnContext): | ||
try: | ||
if ( | ||
not turn_context.activity.name | ||
and turn_context.activity.channel_id == Channels.Msteams | ||
): | ||
return # await on_teams_card_action_invoke_activity(turn_context) | ||
|
||
turn_context.send_activity(MessageFactory.text("working")) | ||
except: | ||
return | ||
|
||
async def on_conversation_update_activity(self, turn_context: TurnContext): | ||
if turn_context.activity.channel_id == Channels.ms_teams: | ||
channel_data = TeamsChannelData(**turn_context.activity.channel_data) | ||
|
||
if turn_context.activity.members_added: | ||
return await self.on_teams_members_added_dispatch_activity( | ||
turn_context.activity.members_added, channel_data.team, turn_context | ||
) | ||
|
||
if turn_context.activity.members_removed: | ||
return await self.on_teams_members_removed_dispatch_activity( | ||
turn_context.activity.members_removed, | ||
channel_data.team, | ||
turn_context, | ||
) | ||
|
||
if channel_data: | ||
if channel_data.event_type == "channelCreated": | ||
return await self.on_teams_channel_created_activity( | ||
channel_data.channel, channel_data.team, turn_context | ||
) | ||
if channel_data.event_type == "channelDeleted": | ||
return await self.on_teams_channel_deleted_activity( | ||
channel_data.channel, channel_data.team, turn_context | ||
) | ||
if channel_data.event_type == "channelRenamed": | ||
return await self.on_teams_channel_renamed_activity( | ||
channel_data.channel, channel_data.team, turn_context) | ||
if channel_data.event_type == "teamRenamed": | ||
return await self.on_teams_team_renamed_activity( | ||
channel_data.team, turn_context | ||
) | ||
return await super().on_conversation_update_activity(turn_context) | ||
|
||
return await super().on_conversation_update_activity(turn_context) | ||
|
||
async def on_teams_channel_created_activity( # pylint: disable=unused-argument | ||
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return | ||
|
||
async def on_teams_team_renamed_activity( # pylint: disable=unused-argument | ||
self, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return | ||
|
||
async def on_teams_members_added_dispatch_activity( # pylint: disable=unused-argument | ||
self, | ||
members_added: [ChannelAccount], | ||
team_info: TeamInfo, | ||
turn_context: TurnContext, | ||
): | ||
""" | ||
team_members = {} | ||
team_members_added = [] | ||
for member in members_added: | ||
if member.additional_properties != {}: | ||
team_members_added.append(TeamsChannelAccount(member)) | ||
else: | ||
if team_members == {}: | ||
result = await TeamsInfo.get_members_async(turn_context) | ||
team_members = { i.id : i for i in result } | ||
|
||
if member.id in team_members: | ||
team_members_added.append(member) | ||
else: | ||
newTeamsChannelAccount = TeamsChannelAccount( | ||
id=member.id, | ||
name = member.name, | ||
aad_object_id = member.aad_object_id, | ||
role = member.role | ||
) | ||
team_members_added.append(newTeamsChannelAccount) | ||
|
||
return await self.on_teams_members_added_activity(teams_members_added, team_info, turn_context) | ||
""" | ||
for member in members_added: | ||
new_account_json = member.__dict__ | ||
del new_account_json["additional_properties"] | ||
member = TeamsChannelAccount(**new_account_json) | ||
return await self.on_teams_members_added_activity(members_added, turn_context) | ||
|
||
async def on_teams_members_added_activity( | ||
self, teams_members_added: [TeamsChannelAccount], turn_context: TurnContext | ||
): | ||
for member in teams_members_added: | ||
member = ChannelAccount(member) | ||
return super().on_members_added_activity(teams_members_added, turn_context) | ||
|
||
async def on_teams_members_removed_dispatch_activity( # pylint: disable=unused-argument | ||
self, | ||
members_removed: [ChannelAccount], | ||
team_info: TeamInfo, | ||
turn_context: TurnContext, | ||
): | ||
teams_members_removed = [] | ||
for member in members_removed: | ||
new_account_json = member.__dict__ | ||
del new_account_json["additional_properties"] | ||
teams_members_removed.append(TeamsChannelAccount(**new_account_json)) | ||
|
||
return await self.on_teams_members_removed_activity( | ||
teams_members_removed, turn_context | ||
) | ||
|
||
async def on_teams_members_removed_activity( | ||
self, teams_members_removed: [TeamsChannelAccount], turn_context: TurnContext | ||
): | ||
members_removed = [ChannelAccount(i) for i in teams_members_removed] | ||
return super().on_members_removed_activity(members_removed, turn_context) | ||
|
||
async def on_teams_channel_deleted_activity( # pylint: disable=unused-argument | ||
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return # Task.CompleteTask | ||
|
||
async def on_teams_channel_renamed_activity( # pylint: disable=unused-argument | ||
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return # Task.CompleteTask | ||
|
||
async def on_teams_team_reanamed_async( # pylint: disable=unused-argument | ||
self, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return # Task.CompleteTask | ||
|
||
@staticmethod | ||
def _create_invoke_response(body: object = None) -> InvokeResponse: | ||
return InvokeResponse(status=int(HTTPStatus.OK), body=body) | ||
|
||
class _InvokeResponseException(Exception): | ||
def __init__(self, status_code: HTTPStatus, body: object = None): | ||
super().__init__() | ||
self._status_code = status_code | ||
self._body = body | ||
|
||
def create_invoke_response(self) -> InvokeResponse: | ||
return InvokeResponse(status=int(self._status_code), body=self._body) |
Empty file.
30 changes: 30 additions & 0 deletions
30
libraries/botbuilder-core/tests/teams/conversation-update/README.md
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 |
---|---|---|
@@ -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) |
92 changes: 92 additions & 0 deletions
92
libraries/botbuilder-core/tests/teams/conversation-update/app.py
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 |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. | ||
|
||
import asyncio | ||
import sys | ||
from datetime import datetime | ||
from types import MethodType | ||
|
||
from flask import Flask, request, Response | ||
from botbuilder.core import ( | ||
BotFrameworkAdapterSettings, | ||
TurnContext, | ||
BotFrameworkAdapter, | ||
) | ||
from botbuilder.schema import Activity, ActivityTypes | ||
|
||
from bots import ConversationUpdateBot | ||
|
||
# Create the loop and Flask app | ||
LOOP = asyncio.get_event_loop() | ||
APP = Flask(__name__, instance_relative_config=True) | ||
APP.config.from_object("config.DefaultConfig") | ||
|
||
# Create adapter. | ||
# See https://aka.ms/about-bot-adapter to learn more about how bots work. | ||
SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) | ||
ADAPTER = BotFrameworkAdapter(SETTINGS) | ||
|
||
|
||
# Catch-all for errors. | ||
async def on_error( # pylint: disable=unused-argument | ||
self, 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) | ||
|
||
# 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 = MethodType(on_error, ADAPTER) | ||
|
||
# Create the Bot | ||
BOT = ConversationUpdateBot() | ||
|
||
# Listen for incoming requests on /api/messages.s | ||
@APP.route("/api/messages", methods=["POST"]) | ||
def messages(): | ||
# Main bot message handler. | ||
if "application/json" in request.headers["Content-Type"]: | ||
body = request.json | ||
else: | ||
return Response(status=415) | ||
|
||
activity = Activity().deserialize(body) | ||
auth_header = ( | ||
request.headers["Authorization"] if "Authorization" in request.headers else "" | ||
) | ||
|
||
try: | ||
task = LOOP.create_task( | ||
ADAPTER.process_activity(activity, auth_header, BOT.on_turn) | ||
) | ||
LOOP.run_until_complete(task) | ||
return Response(status=201) | ||
except Exception as exception: | ||
raise exception | ||
|
||
|
||
if __name__ == "__main__": | ||
try: | ||
APP.run(debug=False, port=APP.config["PORT"]) # nosec debug | ||
except Exception as exception: | ||
raise exception |
6 changes: 6 additions & 0 deletions
6
libraries/botbuilder-core/tests/teams/conversation-update/bots/__init__.py
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 |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. | ||
|
||
from .conversation_update_bot import ConversationUpdateBot | ||
|
||
__all__ = ["ConversationUpdateBot"] |
56 changes: 56 additions & 0 deletions
56
libraries/botbuilder-core/tests/teams/conversation-update/bots/conversation_update_bot.py
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 |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. | ||
|
||
from botbuilder.core import MessageFactory, TurnContext | ||
from botbuilder.core.teams import TeamsActivityHandler | ||
from botbuilder.schema.teams import ChannelInfo, TeamInfo, TeamsChannelAccount | ||
|
||
|
||
class ConversationUpdateBot(TeamsActivityHandler): | ||
async def on_teams_channel_created_activity( | ||
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return await turn_context.send_activity( | ||
MessageFactory.text( | ||
f"The new channel is {channel_info.name}. The channel id is {channel_info.id}" | ||
) | ||
) | ||
|
||
async def on_teams_channel_deleted_activity( | ||
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return await turn_context.send_activity( | ||
MessageFactory.text(f"The deleted channel is {channel_info.name}") | ||
) | ||
|
||
async def on_teams_channel_renamed_activity( | ||
self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return await turn_context.send_activity( | ||
MessageFactory.text(f"The new channel name is {channel_info.name}") | ||
) | ||
|
||
async def on_teams_team_renamed_activity( | ||
self, team_info: TeamInfo, turn_context: TurnContext | ||
): | ||
return await turn_context.send_activity( | ||
MessageFactory.text(f"The new team name is {team_info.name}") | ||
) | ||
|
||
async def on_teams_members_added_activity( | ||
self, teams_members_added: [TeamsChannelAccount], turn_context: TurnContext | ||
): | ||
for member in teams_members_added: | ||
await turn_context.send_activity( | ||
MessageFactory.text(f"Welcome your new team member {member.id}") | ||
) | ||
return | ||
|
||
async def on_teams_members_removed_activity( | ||
self, teams_members_removed: [TeamsChannelAccount], turn_context: TurnContext | ||
): | ||
for member in teams_members_removed: | ||
await turn_context.send_activity( | ||
MessageFactory.text(f"Say goodbye to your team member {member.id}") | ||
) | ||
return |
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.
I'm curious about the logic here: if the activity doesn't have a type attribute at all... should it pass? Because with this conditional is not gonna raise an error until it gets to line 34 and you try to access turn_context.activity.type
I believe that (and correct me if you had other intention) you're trying to validate that turn_context.activity.type exists and is not None, you can achieve that by
if not getattr(turn_context.activity, "type", None): ...(raise your error)
I don't remember if you did a similar validation in your past PR but please take a look to make sure.
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.
Will resolve this in a different PR