Skip to content

Commit ec70a70

Browse files
Eric Dahlvangjohnataylor
authored andcommitted
TeamsInfo and TeamsConnectorClient updates (#462)
* add teams_info and updates * add roster scenario to test teamsinfo and connector calls * fix get_mentions * add teams_info and updates * add roster scenario to test teamsinfo and connector calls * fix get_mentions * fixing linting
1 parent f46a715 commit ec70a70

File tree

16 files changed

+386
-9
lines changed

16 files changed

+386
-9
lines changed

libraries/botbuilder-core/botbuilder/core/teams/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,9 @@
66
# --------------------------------------------------------------------------
77

88
from .teams_activity_handler import TeamsActivityHandler
9+
from .teams_info import TeamsInfo
910

10-
__all__ = ["TeamsActivityHandler"]
11+
__all__ = [
12+
"TeamsActivityHandler",
13+
"TeamsInfo",
14+
]

libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ async def on_teams_members_added_dispatch_activity( # pylint: disable=unused-ar
358358
"""
359359
team_accounts_added = []
360360
for member in members_added:
361+
# TODO: fix this
361362
new_account_json = member.serialize()
362363
if "additional_properties" in new_account_json:
363364
del new_account_json["additional_properties"]
@@ -385,6 +386,7 @@ async def on_teams_members_removed_dispatch_activity( # pylint: disable=unused-
385386
):
386387
teams_members_removed = []
387388
for member in members_removed:
389+
# TODO: fix this
388390
new_account_json = member.serialize()
389391
if "additional_properties" in new_account_json:
390392
del new_account_json["additional_properties"]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import List
5+
from botbuilder.core.turn_context import TurnContext
6+
from botbuilder.schema.teams import (
7+
ChannelInfo,
8+
TeamDetails,
9+
TeamsChannelData,
10+
TeamsChannelAccount,
11+
)
12+
from botframework.connector.aio import ConnectorClient
13+
from botframework.connector.teams.teams_connector_client import TeamsConnectorClient
14+
15+
16+
class TeamsInfo:
17+
@staticmethod
18+
def get_team_details(turn_context: TurnContext, team_id: str = "") -> TeamDetails:
19+
if not team_id:
20+
team_id = TeamsInfo.get_team_id(turn_context)
21+
22+
if not team_id:
23+
raise TypeError(
24+
"TeamsInfo.get_team_details: method is only valid within the scope of MS Teams Team."
25+
)
26+
27+
return TeamsInfo.get_teams_connector_client(
28+
turn_context
29+
).teams.get_team_details(team_id)
30+
31+
@staticmethod
32+
def get_team_channels(
33+
turn_context: TurnContext, team_id: str = ""
34+
) -> List[ChannelInfo]:
35+
if not team_id:
36+
team_id = TeamsInfo.get_team_id(turn_context)
37+
38+
if not team_id:
39+
raise TypeError(
40+
"TeamsInfo.get_team_channels: method is only valid within the scope of MS Teams Team."
41+
)
42+
43+
return (
44+
TeamsInfo.get_teams_connector_client(turn_context)
45+
.teams.get_teams_channels(team_id)
46+
.conversations
47+
)
48+
49+
@staticmethod
50+
async def get_team_members(turn_context: TurnContext, team_id: str = ""):
51+
if not team_id:
52+
team_id = TeamsInfo.get_team_id(turn_context)
53+
54+
if not team_id:
55+
raise TypeError(
56+
"TeamsInfo.get_team_members: method is only valid within the scope of MS Teams Team."
57+
)
58+
59+
return await TeamsInfo._get_members(
60+
TeamsInfo._get_connector_client(turn_context),
61+
turn_context.activity.conversation.id,
62+
)
63+
64+
@staticmethod
65+
async def get_members(turn_context: TurnContext) -> List[TeamsChannelAccount]:
66+
team_id = TeamsInfo.get_team_id(turn_context)
67+
if not team_id:
68+
conversation_id = turn_context.activity.conversation.id
69+
return await TeamsInfo._get_members(
70+
TeamsInfo._get_connector_client(turn_context), conversation_id
71+
)
72+
73+
return await TeamsInfo.get_team_members(turn_context, team_id)
74+
75+
@staticmethod
76+
def get_teams_connector_client(turn_context: TurnContext) -> TeamsConnectorClient:
77+
connector_client = TeamsInfo._get_connector_client(turn_context)
78+
return TeamsConnectorClient(
79+
connector_client.config.credentials, turn_context.activity.service_url
80+
)
81+
82+
# TODO: should have access to adapter's credentials
83+
# return TeamsConnectorClient(turn_context.adapter._credentials, turn_context.activity.service_url)
84+
85+
@staticmethod
86+
def get_team_id(turn_context: TurnContext):
87+
channel_data = TeamsChannelData(**turn_context.activity.channel_data)
88+
if channel_data.team:
89+
# urllib.parse.quote_plus(
90+
return channel_data.team["id"]
91+
return ""
92+
93+
@staticmethod
94+
def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
95+
return turn_context.adapter.create_connector_client(
96+
turn_context.activity.service_url
97+
)
98+
99+
@staticmethod
100+
async def _get_members(
101+
connector_client: ConnectorClient, conversation_id: str
102+
) -> List[TeamsChannelAccount]:
103+
if connector_client is None:
104+
raise TypeError("TeamsInfo._get_members.connector_client: cannot be None.")
105+
106+
if not conversation_id:
107+
raise TypeError("TeamsInfo._get_members.conversation_id: cannot be empty.")
108+
109+
teams_members = []
110+
members = await connector_client.conversations.get_conversation_members(
111+
conversation_id
112+
)
113+
114+
for member in members:
115+
new_account_json = member.serialize()
116+
teams_members.append(TeamsChannelAccount(**new_account_json))
117+
118+
return teams_members

libraries/botbuilder-core/botbuilder/core/turn_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,5 @@ def get_mentions(activity: Activity) -> List[Mention]:
377377
for entity in activity.entities:
378378
if entity.type.lower() == "mention":
379379
result.append(entity)
380+
380381
return result

libraries/botframework-connector/botframework/connector/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
# --------------------------------------------------------------------------
1111

1212
from botbuilder.schema import *
13+
from botbuilder.schema.teams import *

libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from msrest.pipeline import ClientRawResponse
1313
from msrest.exceptions import HttpOperationError
1414

15-
from .. import models
15+
from ... import models
1616

1717

1818
class TeamsOperations(object):
@@ -34,7 +34,7 @@ def __init__(self, client, config, serializer, deserializer):
3434

3535
self.config = config
3636

37-
def fetch_channel_list(
37+
def get_teams_channels(
3838
self, team_id, custom_headers=None, raw=False, **operation_config
3939
):
4040
"""Fetches channel list for a given team.
@@ -55,7 +55,7 @@ def fetch_channel_list(
5555
:class:`HttpOperationError<msrest.exceptions.HttpOperationError>`
5656
"""
5757
# Construct URL
58-
url = self.fetch_channel_list.metadata["url"]
58+
url = self.get_teams_channels.metadata["url"]
5959
path_format_arguments = {
6060
"teamId": self._serialize.url("team_id", team_id, "str")
6161
}
@@ -88,9 +88,9 @@ def fetch_channel_list(
8888

8989
return deserialized
9090

91-
fetch_channel_list.metadata = {"url": "/v3/teams/{teamId}/conversations"}
91+
get_teams_channels.metadata = {"url": "/v3/teams/{teamId}/conversations"}
9292

93-
def fetch_team_details(
93+
def get_team_details(
9494
self, team_id, custom_headers=None, raw=False, **operation_config
9595
):
9696
"""Fetches details related to a team.
@@ -111,7 +111,7 @@ def fetch_team_details(
111111
:class:`HttpOperationError<msrest.exceptions.HttpOperationError>`
112112
"""
113113
# Construct URL
114-
url = self.fetch_team_details.metadata["url"]
114+
url = self.get_team_details.metadata["url"]
115115
path_format_arguments = {
116116
"teamId": self._serialize.url("team_id", team_id, "str")
117117
}
@@ -144,4 +144,4 @@ def fetch_team_details(
144144

145145
return deserialized
146146

147-
fetch_team_details.metadata = {"url": "/v3/teams/{teamId}"}
147+
get_team_details.metadata = {"url": "/v3/teams/{teamId}"}

libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from msrest.service_client import SDKClient
1313
from msrest import Configuration, Serializer, Deserializer
14-
from botbuilder.schema import models
14+
from .. import models
1515
from .version import VERSION
1616
from .operations.teams_operations import TeamsOperations
1717

scenarios/roster/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# RosterBot
2+
3+
Bot Framework v4 teams roster 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\roster` 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)

scenarios/roster/app.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import asyncio
5+
import sys
6+
from datetime import datetime
7+
from types import MethodType
8+
9+
from flask import Flask, request, Response
10+
from botbuilder.core import (
11+
BotFrameworkAdapterSettings,
12+
TurnContext,
13+
BotFrameworkAdapter,
14+
)
15+
from botbuilder.schema import Activity, ActivityTypes
16+
17+
from bots import RosterBot
18+
19+
# Create the loop and Flask app
20+
LOOP = asyncio.get_event_loop()
21+
APP = Flask(__name__, instance_relative_config=True)
22+
APP.config.from_object("config.DefaultConfig")
23+
24+
# Create adapter.
25+
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
26+
SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
27+
ADAPTER = BotFrameworkAdapter(SETTINGS)
28+
29+
30+
# Catch-all for errors.
31+
async def on_error( # pylint: disable=unused-argument
32+
self, context: TurnContext, error: Exception
33+
):
34+
# This check writes out errors to console log .vs. app insights.
35+
# NOTE: In production environment, you should consider logging this to Azure
36+
# application insights.
37+
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
38+
39+
# Send a message to the user
40+
await context.send_activity("The bot encountered an error or bug.")
41+
await context.send_activity(
42+
"To continue to run this bot, please fix the bot source code."
43+
)
44+
# Send a trace activity if we're talking to the Bot Framework Emulator
45+
if context.activity.channel_id == "emulator":
46+
# Create a trace activity that contains the error object
47+
trace_activity = Activity(
48+
label="TurnError",
49+
name="on_turn_error Trace",
50+
timestamp=datetime.utcnow(),
51+
type=ActivityTypes.trace,
52+
value=f"{error}",
53+
value_type="https://www.botframework.com/schemas/error",
54+
)
55+
# Send a trace activity, which will be displayed in Bot Framework Emulator
56+
await context.send_activity(trace_activity)
57+
58+
59+
ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
60+
61+
# Create the Bot
62+
BOT = RosterBot()
63+
64+
# Listen for incoming requests on /api/messages.s
65+
@APP.route("/api/messages", methods=["POST"])
66+
def messages():
67+
# Main bot message handler.
68+
if "application/json" in request.headers["Content-Type"]:
69+
body = request.json
70+
else:
71+
return Response(status=415)
72+
73+
activity = Activity().deserialize(body)
74+
auth_header = (
75+
request.headers["Authorization"] if "Authorization" in request.headers else ""
76+
)
77+
78+
try:
79+
task = LOOP.create_task(
80+
ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
81+
)
82+
LOOP.run_until_complete(task)
83+
return Response(status=201)
84+
except Exception as exception:
85+
raise exception
86+
87+
88+
if __name__ == "__main__":
89+
try:
90+
APP.run(debug=False, port=APP.config["PORT"]) # nosec debug
91+
except Exception as exception:
92+
raise exception

scenarios/roster/bots/__init__.py

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+
4+
from .roster_bot import RosterBot
5+
6+
__all__ = ["RosterBot"]

0 commit comments

Comments
 (0)