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
6 changes: 5 additions & 1 deletion libraries/botbuilder-core/botbuilder/core/teams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
# --------------------------------------------------------------------------

from .teams_activity_handler import TeamsActivityHandler
from .teams_info import TeamsInfo

__all__ = ["TeamsActivityHandler"]
__all__ = [
"TeamsActivityHandler",
"TeamsInfo",
]
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ async def on_teams_members_added_dispatch_activity( # pylint: disable=unused-ar
"""
team_accounts_added = []
for member in members_added:
# TODO: fix this
new_account_json = member.serialize()
if "additional_properties" in new_account_json:
del new_account_json["additional_properties"]
Expand Down Expand Up @@ -385,6 +386,7 @@ async def on_teams_members_removed_dispatch_activity( # pylint: disable=unused-
):
teams_members_removed = []
for member in members_removed:
# TODO: fix this
new_account_json = member.serialize()
if "additional_properties" in new_account_json:
del new_account_json["additional_properties"]
Expand Down
118 changes: 118 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import List
from botbuilder.core.turn_context import TurnContext
from botbuilder.schema.teams import (
ChannelInfo,
TeamDetails,
TeamsChannelData,
TeamsChannelAccount,
)
from botframework.connector.aio import ConnectorClient
from botframework.connector.teams.teams_connector_client import TeamsConnectorClient


class TeamsInfo:
@staticmethod
def get_team_details(turn_context: TurnContext, team_id: str = "") -> TeamDetails:
if not team_id:
team_id = TeamsInfo.get_team_id(turn_context)

if not team_id:
raise TypeError(
"TeamsInfo.get_team_details: method is only valid within the scope of MS Teams Team."
)

return TeamsInfo.get_teams_connector_client(
turn_context
).teams.get_team_details(team_id)

@staticmethod
def get_team_channels(
turn_context: TurnContext, team_id: str = ""
) -> List[ChannelInfo]:
if not team_id:
team_id = TeamsInfo.get_team_id(turn_context)

if not team_id:
raise TypeError(
"TeamsInfo.get_team_channels: method is only valid within the scope of MS Teams Team."
)

return (
TeamsInfo.get_teams_connector_client(turn_context)
.teams.get_teams_channels(team_id)
.conversations
)

@staticmethod
async def get_team_members(turn_context: TurnContext, team_id: str = ""):
if not team_id:
team_id = TeamsInfo.get_team_id(turn_context)

if not team_id:
raise TypeError(
"TeamsInfo.get_team_members: method is only valid within the scope of MS Teams Team."
)

return await TeamsInfo._get_members(
TeamsInfo._get_connector_client(turn_context),
turn_context.activity.conversation.id,
)

@staticmethod
async def get_members(turn_context: TurnContext) -> List[TeamsChannelAccount]:
team_id = TeamsInfo.get_team_id(turn_context)
if not team_id:
conversation_id = turn_context.activity.conversation.id
return await TeamsInfo._get_members(
TeamsInfo._get_connector_client(turn_context), conversation_id
)

return await TeamsInfo.get_team_members(turn_context, team_id)

@staticmethod
def get_teams_connector_client(turn_context: TurnContext) -> TeamsConnectorClient:
connector_client = TeamsInfo._get_connector_client(turn_context)
return TeamsConnectorClient(
connector_client.config.credentials, turn_context.activity.service_url
)

# TODO: should have access to adapter's credentials
# return TeamsConnectorClient(turn_context.adapter._credentials, turn_context.activity.service_url)

@staticmethod
def get_team_id(turn_context: TurnContext):
channel_data = TeamsChannelData(**turn_context.activity.channel_data)
if channel_data.team:
# urllib.parse.quote_plus(
return channel_data.team["id"]
return ""

@staticmethod
def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
return turn_context.adapter.create_connector_client(
turn_context.activity.service_url
)

@staticmethod
async def _get_members(
connector_client: ConnectorClient, conversation_id: str
) -> List[TeamsChannelAccount]:
if connector_client is None:
raise TypeError("TeamsInfo._get_members.connector_client: cannot be None.")

if not conversation_id:
raise TypeError("TeamsInfo._get_members.conversation_id: cannot be empty.")

teams_members = []
members = await connector_client.conversations.get_conversation_members(
conversation_id
)

for member in members:
new_account_json = member.serialize()
teams_members.append(TeamsChannelAccount(**new_account_json))

return teams_members
1 change: 1 addition & 0 deletions libraries/botbuilder-core/botbuilder/core/turn_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,5 @@ def get_mentions(activity: Activity) -> List[Mention]:
for entity in activity.entities:
if entity.type.lower() == "mention":
result.append(entity)

return result
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
# --------------------------------------------------------------------------

from botbuilder.schema import *
from botbuilder.schema.teams import *
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from msrest.pipeline import ClientRawResponse
from msrest.exceptions import HttpOperationError

from .. import models
from ... import models


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

self.config = config

def fetch_channel_list(
def get_teams_channels(
self, team_id, custom_headers=None, raw=False, **operation_config
):
"""Fetches channel list for a given team.
Expand All @@ -55,7 +55,7 @@ def fetch_channel_list(
:class:`HttpOperationError<msrest.exceptions.HttpOperationError>`
"""
# Construct URL
url = self.fetch_channel_list.metadata["url"]
url = self.get_teams_channels.metadata["url"]
path_format_arguments = {
"teamId": self._serialize.url("team_id", team_id, "str")
}
Expand Down Expand Up @@ -88,9 +88,9 @@ def fetch_channel_list(

return deserialized

fetch_channel_list.metadata = {"url": "/v3/teams/{teamId}/conversations"}
get_teams_channels.metadata = {"url": "/v3/teams/{teamId}/conversations"}

def fetch_team_details(
def get_team_details(
self, team_id, custom_headers=None, raw=False, **operation_config
):
"""Fetches details related to a team.
Expand All @@ -111,7 +111,7 @@ def fetch_team_details(
:class:`HttpOperationError<msrest.exceptions.HttpOperationError>`
"""
# Construct URL
url = self.fetch_team_details.metadata["url"]
url = self.get_team_details.metadata["url"]
path_format_arguments = {
"teamId": self._serialize.url("team_id", team_id, "str")
}
Expand Down Expand Up @@ -144,4 +144,4 @@ def fetch_team_details(

return deserialized

fetch_team_details.metadata = {"url": "/v3/teams/{teamId}"}
get_team_details.metadata = {"url": "/v3/teams/{teamId}"}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from msrest.service_client import SDKClient
from msrest import Configuration, Serializer, Deserializer
from botbuilder.schema import models
from .. import models
from .version import VERSION
from .operations.teams_operations import TeamsOperations

Expand Down
30 changes: 30 additions & 0 deletions scenarios/roster/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# RosterBot

Bot Framework v4 teams roster 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\roster` 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 scenarios/roster/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 RosterBot

# 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 = RosterBot()

# 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 scenarios/roster/bots/__init__.py
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 .roster_bot import RosterBot

__all__ = ["RosterBot"]
Loading