Teams Providers¶
Microsoft Teams integration via the Bot Framework SDK. Supports inbound webhook parsing and proactive outbound messaging through stored conversation references.
Quick start¶
from roomkit import (
RoomKit,
BotFrameworkTeamsProvider,
TeamsConfig,
parse_teams_webhook,
)
from roomkit.channels import TeamsChannel
config = TeamsConfig(
app_id="YOUR_APP_ID",
app_password="YOUR_APP_PASSWORD",
tenant_id="YOUR_TENANT_ID", # or "common" for multi-tenant
)
provider = BotFrameworkTeamsProvider(config)
kit = RoomKit()
kit.register_channel(TeamsChannel("teams-main", provider=provider))
Configuration¶
TeamsConfig holds the Azure Bot registration credentials:
from roomkit import TeamsConfig
config = TeamsConfig(
app_id="your-azure-app-id", # Azure AD application (client) ID
app_password="your-client-secret", # Azure AD client secret
tenant_id="common", # "common" for multi-tenant (default)
)
| Parameter | Type | Default | Description |
|---|---|---|---|
app_id |
str |
required | Azure AD application (client) ID |
app_password |
SecretStr |
required | Azure AD client secret |
tenant_id |
str |
"common" |
Azure AD tenant ID ("common" for multi-tenant bots) |
Single-tenant apps
For single-tenant Azure AD apps, you must set tenant_id to your actual tenant ID (a UUID).
When tenant_id is not "common", the provider automatically passes it as channel_auth_tenant
to the Bot Framework SDK, which is required for token acquisition to succeed.
Webhook integration¶
Bot Framework sends one Activity per HTTP POST to your messaging endpoint. Use parse_teams_webhook() to convert message activities into InboundMessage, and parse_teams_activity() / is_bot_added() to handle lifecycle events:
from aiohttp import web
from roomkit import is_bot_added, parse_teams_activity, parse_teams_webhook
async def handle_messages(request: web.Request) -> web.Response:
payload = await request.json()
# Always save the conversation reference (needed for proactive sends)
await provider.save_conversation_reference(payload)
activity = parse_teams_activity(payload)
# Handle bot installation
if is_bot_added(payload):
print(f"Bot added to {activity['conversation_type']} conversation")
return web.Response(status=200)
# Handle regular messages
if activity["activity_type"] == "message":
messages = parse_teams_webhook(payload, channel_id="teams-main")
for inbound in messages:
result = await kit.process_inbound(inbound)
return web.Response(status=200)
FastAPI variant:
from fastapi import FastAPI, Request
from roomkit import is_bot_added, parse_teams_activity, parse_teams_webhook
app = FastAPI()
@app.post("/api/messages")
async def handle_messages(request: Request):
payload = await request.json()
await provider.save_conversation_reference(payload)
if is_bot_added(payload):
return {"status": "bot_added"}
activity = parse_teams_activity(payload)
if activity["activity_type"] == "message":
for inbound in parse_teams_webhook(payload, channel_id="teams-main"):
await kit.process_inbound(inbound)
return {"status": "ok"}
Webhook metadata¶
Each parsed InboundMessage includes metadata extracted from the Activity:
| Key | Type | Description |
|---|---|---|
sender_name |
str |
Display name of the sender |
conversation_id |
str |
Teams conversation ID (used as to for outbound) |
conversation_type |
str |
"personal", "groupChat", or "channel" |
is_group |
bool |
Whether the message is from a group/channel chat |
bot_mentioned |
bool |
Whether the bot was @mentioned (always True in personal chats) |
service_url |
str |
Bot Framework service URL for this conversation |
tenant_id |
str |
Azure AD tenant ID of the sender |
Mention stripping¶
In group chats and channels, users @mention the bot to address it. The webhook parser automatically strips <at>BotName</at> tags from the message text, so your hooks receive clean text:
Activity parsing helpers¶
parse_teams_activity() extracts common fields from any Bot Framework Activity (not just messages). Useful for handling lifecycle events like conversationUpdate:
from roomkit import parse_teams_activity
activity = parse_teams_activity(payload)
# Returns: {
# "activity_type": "message" | "conversationUpdate" | ...,
# "conversation_id": "19:abc123@thread.v2",
# "conversation_type": "personal" | "groupChat" | "channel",
# "is_group": True/False,
# "service_url": "https://smba.trafficmanager.net/amer/",
# "tenant_id": "your-tenant-id",
# "sender_id": "...",
# "sender_name": "...",
# "bot_id": "...",
# "members_added": [...],
# "members_removed": [...],
# }
Bot installation detection¶
is_bot_added() checks if a conversationUpdate Activity indicates the bot was added to a conversation:
from roomkit import is_bot_added
if is_bot_added(payload):
conv_type = parse_teams_activity(payload)["conversation_type"]
print(f"Bot installed in {conv_type} chat")
You can optionally pass a bot_id parameter. If not provided, it uses payload["recipient"]["id"].
Conversation references¶
Teams requires conversation references for proactive (bot-initiated) messaging. When a user messages the bot, you capture the reference from the inbound Activity. Later, you use that reference to send messages back.
BotFrameworkTeamsProvider manages this automatically with a pluggable store:
# Capture reference from inbound webhook
await provider.save_conversation_reference(activity_dict)
# Later: send proactively using the conversation ID as `to`
result = await provider.send(event, to="conversation-id-here")
Custom conversation store¶
The default InMemoryConversationReferenceStore works for single-process bots. For production, implement ConversationReferenceStore with a persistent backend:
from roomkit import ConversationReferenceStore
class RedisConversationStore(ConversationReferenceStore):
async def save(self, conversation_id, reference):
await self.redis.set(f"teams:ref:{conversation_id}", json.dumps(reference))
async def get(self, conversation_id):
data = await self.redis.get(f"teams:ref:{conversation_id}")
return json.loads(data) if data else None
async def delete(self, conversation_id):
await self.redis.delete(f"teams:ref:{conversation_id}")
async def list_all(self):
# Return all stored references
...
provider = BotFrameworkTeamsProvider(
config,
conversation_store=RedisConversationStore(redis),
)
Proactive channel messaging¶
To message a Teams channel the bot has been installed in — even if no user has messaged the bot in that channel yet — use create_channel_conversation():
# Create a conversation in a Teams channel
conv_id = await provider.create_channel_conversation(
service_url="https://smba.trafficmanager.net/amer/",
channel_id="19:abc123@thread.tacv2",
tenant_id="your-tenant-id", # optional, falls back to config
)
# Now you can send messages using the returned conversation ID
result = await provider.send(event, to=conv_id)
The service_url and channel_id are available from a prior conversationUpdate Activity (captured via parse_teams_activity()).
Channel capabilities¶
| Feature | Supported |
|---|---|
| Text messages | Yes |
| Rich text (HTML) | Yes |
| Threading | Yes |
| Reactions | Yes |
| Read receipts | Yes |
| Max message length | 28,000 characters |
| Media attachments | Not yet |
| Adaptive Cards | Not yet |
Installation¶
This installs botbuilder-core>=4.14. The BotFrameworkTeamsProvider constructor raises a helpful ImportError if the dependency is missing.
Testing with Bot Framework Emulator¶
You can test locally without Azure credentials using the Bot Framework Emulator:
- Leave
app_idandapp_passwordempty for local testing - Run your bot on
http://localhost:3978/api/messages - Connect the Emulator to that URL
# Local testing config (no auth)
config = TeamsConfig(app_id="", app_password="")
provider = BotFrameworkTeamsProvider(config)
Testing with MockTeamsProvider¶
For unit tests, use MockTeamsProvider which records all sent messages:
from roomkit import MockTeamsProvider
mock = MockTeamsProvider()
result = await mock.send(event, to="conv-123")
assert result.success
assert len(mock.sent) == 1
assert mock.sent[0]["to"] == "conv-123"
API reference¶
TeamsProvider ¶
Bases: ABC
Microsoft Teams delivery provider.
send
abstractmethod
async
¶
Send a Microsoft Teams message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
RoomEvent
|
The room event containing the message content. |
required |
to
|
str
|
Recipient Teams conversation ID. |
required |
Returns:
| Type | Description |
|---|---|
ProviderResult
|
Result with provider-specific delivery metadata. |
verify_signature ¶
Verify that a webhook payload was signed by the Bot Framework.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
bytes
|
Raw request body bytes. |
required |
signature
|
str
|
Value of the |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the signature is valid, False otherwise. |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If the provider does not support signature verification. |
BotFrameworkTeamsProvider ¶
Bases: TeamsProvider
Send messages via the Microsoft Bot Framework SDK.
save_conversation_reference
async
¶
Extract and store a ConversationReference from an inbound Activity dict.
create_personal_conversation
async
¶
Create a 1:1 personal conversation with a user and store its reference.
Use this to proactively message a Teams user by their AAD/Teams ID, even if the user has never messaged the bot. The bot must be installed in the user's tenant for this to succeed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
service_url
|
str
|
The Bot Framework service URL for the tenant
(e.g. |
required |
user_id
|
str
|
The AAD object ID or Teams user ID of the target user
(e.g. |
required |
tenant_id
|
str | None
|
Azure AD tenant ID. Falls back to
:attr: |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The conversation ID for the newly created 1:1 conversation. |
str
|
This ID can be used as the |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the conversation could not be created. |
create_channel_conversation
async
¶
Create a conversation in a Teams channel and store its reference.
Use this to proactively message a Teams channel the bot has been installed in, even if no user has messaged the bot in that channel yet.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
service_url
|
str
|
The Bot Framework service URL for the team
(e.g. |
required |
channel_id
|
str
|
The Teams channel ID
(e.g. |
required |
tenant_id
|
str | None
|
Azure AD tenant ID. Falls back to
:attr: |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The conversation ID for the newly created conversation. |
str
|
This ID can be used as the |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the conversation could not be created. |
verify_signature ¶
Verify a Bot Framework JWT bearer token.
Validates the JWT from the Authorization: Bearer <token> header
using Microsoft's OpenID Connect metadata and signing keys.
Requires the PyJWT and cryptography packages::
pip install PyJWT cryptography
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
bytes
|
Raw request body bytes (unused — Bot Framework signs the token, not the payload). |
required |
signature
|
str
|
The full |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the JWT is valid (signature, issuer, audience, expiry). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required dependencies are missing. |
MockTeamsProvider ¶
TeamsConfig ¶
Bases: BaseModel
Microsoft Teams Bot Framework configuration.
Supports two authentication modes:
Client secret (password)::
TeamsConfig(app_id="...", app_password="...")
Certificate-based::
TeamsConfig(
app_id="...",
certificate_thumbprint="AB01CD...",
certificate_private_key="-----BEGIN RSA PRIVATE KEY-----\n...",
)
Exactly one mode must be provided.
uses_certificate_auth
property
¶
Return True if configured for certificate-based authentication.
parse_teams_webhook ¶
Convert a Bot Framework Activity payload into InboundMessages.
Bot Framework sends one Activity per HTTP POST. Only type="message"
activities with non-empty text are converted. <at>BotName</at>
mention tags are stripped from group chat messages.
parse_teams_activity ¶
Extract common fields from a Bot Framework Activity payload.
Returns a dict with activity_type, conversation_id,
conversation_type, conversation_name, is_group,
service_url, tenant_id, sender_id, sender_name,
bot_id, members_added, and members_removed. Useful for
handling lifecycle events (conversationUpdate) alongside message
parsing.
is_bot_added ¶
Check if a conversationUpdate Activity indicates the bot was added.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict[str, Any]
|
Raw Bot Framework Activity dict. |
required |
bot_id
|
str | None
|
The bot's AAD ID. If None, falls back to
|
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
in |
ConversationReferenceStore ¶
Bases: ABC
Abstract store for Bot Framework ConversationReference dicts.
InMemoryConversationReferenceStore ¶
parse_teams_reactions ¶
Parse reaction events from a messageReaction Activity.
Teams sends messageReaction activities when a user adds or removes
a reaction (like, heart, laugh, etc.) on a message. This helper
normalises both reactionsAdded and reactionsRemoved into a flat
list of dicts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict[str, Any]
|
Raw Bot Framework Activity dict. |
required |
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
A list of dicts, each with keys |
list[dict[str, Any]]
|
|
list[dict[str, Any]]
|
|
list[dict[str, Any]]
|
is not a |