Skip to content
Open
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
57 changes: 57 additions & 0 deletions examples/proactive-messaging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Proactive Messaging Example

This example demonstrates how to send proactive messages to Teams users without running a server. This is useful for:
- Scheduled notifications
- Alert systems
- Background jobs that need to notify users
- Webhook handlers that send messages

## Key Concepts

- Uses `app.initialize()` instead of `app.start()` (no HTTP server)
- Directly sends messages using `app.send()`
- Requires a conversation ID (from previous interactions or from the Teams API)

## How It Works

The example shows the separation of activity sending from HTTP transport:

1. **Initialize without server**: `await app.initialize()` sets up credentials, token manager, and activity sender without starting the HTTP server
2. **Send messages**: `await app.send(conversation_id, message)` sends messages directly using the ActivitySender
3. **No HTTP server**: Perfect for background jobs, scheduled tasks, or webhook handlers

## Usage

```bash
# Set up your environment variables
export CLIENT_ID=your_app_id
export CLIENT_SECRET=your_app_secret
export TENANT_ID=your_tenant_id

# Run the example with a conversation ID
uv run src/main.py <conversation_id>
```

## Getting a Conversation ID

You need a conversation ID to send proactive messages. You can get this from:

1. **Previous bot interactions**: Store the conversation ID when users first interact with your bot
2. **Teams API**: Use the Microsoft Teams API to create or get conversation references
3. **Testing**: Use an existing bot conversation and extract the conversation ID from the activity

## Example Output

```
Initializing app (without starting server)...
✓ App initialized

Sending proactive message to conversation: 19:...
Message: Hello! This is a proactive message sent without a running server 🚀
✓ Message sent successfully! Activity ID: 1234567890

Sending proactive card to conversation: 19:...
✓ Card sent successfully! Activity ID: 1234567891

✓ All proactive messages sent successfully!
```
13 changes: 13 additions & 0 deletions examples/proactive-messaging/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "proactive-messaging"
version = "0.1.0"
description = "Example showing proactive messaging without running a server"
readme = "README.md"
requires-python = ">=3.12,<3.14"
dependencies = [
"dotenv>=0.9.9",
"microsoft-teams-apps",
]

[tool.uv.sources]
microsoft-teams-apps = { workspace = true }
110 changes: 110 additions & 0 deletions examples/proactive-messaging/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.

Proactive Messaging Example
===========================
This example demonstrates how to send proactive messages to Teams users
without running a server. This is useful for:
- Scheduled notifications
- Alert systems
- Background jobs that need to notify users
- Webhook handlers that send messages

Key points:
- Uses app.initialize() instead of app.start() (no HTTP server)
- Directly sends messages using app.send()
- Requires a conversation ID (from previous interactions or from the Teams API)
"""

import argparse
import asyncio

from microsoft_teams.apps import App
from microsoft_teams.cards import ActionSet, AdaptiveCard, OpenUrlAction, TextBlock


async def send_proactive_message(app: App, conversation_id: str, message: str) -> None:
"""
Send a proactive message to a Teams conversation.

Args:
app: The initialized App instance
conversation_id: The Teams conversation ID to send the message to
message: The message text to send
"""
print(f"Sending proactive message to conversation: {conversation_id}")
print(f"Message: {message}")

# Send the message
result = await app.send(conversation_id, message)

print(f"✓ Message sent successfully! Activity ID: {result.id}")


async def send_proactive_card(app: App, conversation_id: str) -> None:
"""
Send a proactive Adaptive Card to a Teams conversation.

Args:
app: The initialized App instance
conversation_id: The Teams conversation ID to send the card to
"""
# Create an Adaptive Card
card = AdaptiveCard(
schema="http://adaptivecards.io/schemas/adaptive-card.json",
body=[
TextBlock(text="Proactive Notification", size="Large", weight="Bolder"),
TextBlock(text="This message was sent proactively without a server running!", wrap=True),
TextBlock(text="Status: Active • Priority: High • Time: Now", wrap=True, is_subtle=True),
ActionSet(actions=[OpenUrlAction(title="Learn More", url="https://aka.ms/teams-sdk")]),
],
)

print(f"Sending proactive card to conversation: {conversation_id}")

result = await app.send(conversation_id, card)

print(f"✓ Card sent successfully! Activity ID: {result.id}")


async def main():
"""
Main function demonstrating proactive messaging.

In a real application, you would:
1. Store conversation IDs when users first interact with your bot
2. Use those IDs later to send proactive messages
3. Get conversation IDs from the Teams API or from previous interactions
"""
parser = argparse.ArgumentParser(
description="Send proactive messages to a Teams conversation without running a server"
)
parser.add_argument("conversation_id", help="The Teams conversation ID to send messages to")
args = parser.parse_args()

# Create app (no plugins needed for sending only)
app = App()

# Initialize the app without starting the HTTP server
# This sets up credentials, token manager, and activity sender
print("Initializing app (without starting server)...")
await app.initialize()
print("✓ App initialized\n")

# Example 1: Send a simple text message
await send_proactive_message(
app, args.conversation_id, "Hello! This is a proactive message sent without a running server 🚀"
)

# Wait a bit between messages
await asyncio.sleep(2)

# Example 2: Send an Adaptive Card
await send_proactive_card(app, args.conversation_id)

print("\n✓ All proactive messages sent successfully!")


if __name__ == "__main__":
asyncio.run(main())
76 changes: 76 additions & 0 deletions packages/apps/src/microsoft_teams/apps/activity_sender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

from logging import Logger
from typing import Optional

from microsoft_teams.api import (
ActivityParams,
ApiClient,
ConversationReference,
SentActivity,
)
from microsoft_teams.common import Client, ConsoleLogger

from .http_stream import HttpStream
from .plugins.streamer import StreamerProtocol


class ActivitySender:
"""
Handles sending activities to the Bot Framework.
Separate from transport concerns (HTTP, WebSocket, etc.)
"""

def __init__(self, client: Client, logger: Optional[Logger] = None):
"""
Initialize ActivitySender.

Args:
client: HTTP client with token provider configured
logger: Optional logger instance for debugging. If not provided, creates a default console logger.
"""
self._client = client
self._logger = logger or ConsoleLogger().create_logger("@teams/activity-sender")

async def send(self, activity: ActivityParams, ref: ConversationReference) -> SentActivity:
"""
Send an activity to the Bot Framework.

Args:
activity: The activity to send
ref: The conversation reference

Returns:
The sent activity with id and other server-populated fields
"""
# Create API client for this conversation's service URL
api = ApiClient(service_url=ref.service_url, options=self._client)

# Merge activity with conversation reference
activity.from_ = ref.bot
activity.conversation = ref.conversation

# Decide create vs update
if hasattr(activity, "id") and activity.id:
res = await api.conversations.activities(ref.conversation.id).update(activity.id, activity)
return SentActivity.merge(activity, res)

res = await api.conversations.activities(ref.conversation.id).create(activity)
return SentActivity.merge(activity, res)

def create_stream(self, ref: ConversationReference) -> StreamerProtocol:
"""
Create a new activity stream for real-time updates.

Args:
ref: The conversation reference

Returns:
A new streaming instance
"""
# Create API client for this conversation's service URL
api = ApiClient(ref.service_url, self._client)
return HttpStream(api, ref, self._logger)
51 changes: 43 additions & 8 deletions packages/apps/src/microsoft_teams/apps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from microsoft_teams.cards import AdaptiveCard
from microsoft_teams.common import Client, ClientOptions, ConsoleLogger, EventEmitter, LocalStorage

from .activity_sender import ActivitySender
from .app_events import EventManager
from .app_oauth import OauthHandlers
from .app_plugins import PluginProcessor
Expand Down Expand Up @@ -122,6 +123,12 @@ def __init__(self, **options: Unpack[AppOptions]):

self._port: Optional[int] = None
self._running = False
self._initialized = False

# initialize ActivitySender for sending activities
self.activity_sender = ActivitySender(
self.http_client.clone(ClientOptions(token=self._get_bot_token)), self.log
)

# initialize all event, activity, and plugin processors
self.activity_processor = ActivityProcessor(
Expand All @@ -133,6 +140,7 @@ def __init__(self, **options: Unpack[AppOptions]):
self.http_client,
self._token_manager,
self.options.api_client_settings,
self.activity_sender,
)
self.event_manager = EventManager(self._events)
self.activity_processor.event_manager = self.event_manager
Expand Down Expand Up @@ -187,6 +195,32 @@ def id(self) -> Optional[str]:
return None
return self.credentials.client_id

async def initialize(self) -> None:
"""
Initialize the Teams application without starting the HTTP server.

This method sets up credentials, token manager, activity sender, and plugins,
allowing you to use app.send() for proactive messaging without running a server.
"""
if self._initialized:
self.log.warning("App is already initialized")
return

try:
for plugin in self.plugins:
# Inject the dependencies
self._plugin_processor.inject(plugin)
if hasattr(plugin, "on_init") and callable(plugin.on_init):
await plugin.on_init()

self._initialized = True
self.log.info("Teams app initialized successfully (without HTTP server)")

except Exception as error:
self.log.error(f"Failed to initialize app: {error}")
self._events.emit("error", ErrorEvent(error, context={"method": "initialize"}))
raise

async def start(self, port: Optional[int] = None) -> None:
"""
Start the Teams application and begin serving HTTP requests.
Expand All @@ -204,11 +238,9 @@ async def start(self, port: Optional[int] = None) -> None:
self._port = port or int(os.getenv("PORT", "3978"))

try:
for plugin in self.plugins:
# Inject the dependencies
self._plugin_processor.inject(plugin)
if hasattr(plugin, "on_init") and callable(plugin.on_init):
await plugin.on_init()
# Initialize the app if not already initialized
if not self._initialized:
await self.initialize()

# Set callback and start HTTP plugin
async def on_http_ready() -> None:
Expand Down Expand Up @@ -262,8 +294,11 @@ async def on_http_stopped() -> None:
async def send(self, conversation_id: str, activity: str | ActivityParams | AdaptiveCard):
"""Send an activity proactively."""

if not self._initialized:
raise ValueError("app not initialized - call app.initialize() or app.start() first")

if self.id is None:
raise ValueError("app not started")
raise ValueError("app credentials not configured")

conversation_ref = ConversationReference(
channel_id="msteams",
Expand All @@ -279,7 +314,7 @@ async def send(self, conversation_id: str, activity: str | ActivityParams | Adap
else:
activity = activity

return await self.http.send(activity, conversation_ref)
return await self.activity_sender.send(activity, conversation_ref)

def use(self, middleware: Callable[[ActivityContext[ActivityBase]], Awaitable[None]]) -> None:
"""Add middleware to run on all activities."""
Expand Down Expand Up @@ -471,7 +506,7 @@ async def call_next(r: Request) -> Any:
ctx = FunctionContext(
id=self.id,
api=self.api,
http=self.http,
activity_sender=self.activity_sender,
log=self.log,
data=await r.json(),
**r.state.context.__dict__,
Expand Down
Loading