Skip to content

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 7 commits into from
Nov 13, 2019
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
10 changes: 10 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/Teams/__init__.py
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"]
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")
Copy link
Member

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.

Copy link
Contributor Author

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

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.
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 libraries/botbuilder-core/tests/teams/conversation-update/app.py
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
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"]
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
Loading