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

Send proactive messages to Teams users without running a server.

## Key Concepts

**Without a server:**
```typescript
await app.initialize();
await app.send(conversationId, 'Hello!');
```

**With a running server:**
```typescript
await app.start();
// Later, anywhere in your code:
await app.send(conversationId, 'Hello!');
```

> **Note**: Use `app.initialize()` only when you don't need a server. If using `app.start()`, just call `app.send()` directly.
>
> **Important**: Without a server (`app.initialize()`), you can only send messages. You cannot receive incoming messages from users.

## Usage

1. Set up `.env`:
```
BOT_ID=<your-bot-id>
BOT_PASSWORD=<your-bot-password>
```

2. Run:
```bash
npm run dev <CONVERSATION_ID>
```

## Examples

**Send text:**
```typescript
await app.send(conversationId, 'Your message');
```

**Send card:**
```typescript
const card = new AdaptiveCard(
new TextBlock('Title', { size: 'Large' })
);
await app.send(conversationId, card);
```

**Scheduled job (no server):**
```typescript
const app = new App();
await app.initialize();
await app.send(conversationId, 'Reminder!');
```

**From running bot:**
```typescript
const app = new App();
await app.start();

app.on('message', async ({ activity }) => {
await saveConversationId(activity.conversation.id);
});

// Send proactive messages anytime
await app.send(conversationId, 'Update!');
```

## Notes

- Without a server (`app.initialize()`), you can only send messages, not receive them
- Get conversation IDs from previous interactions, installation events, or Graph API
- Your bot must be installed in the conversation
- Be mindful of rate limits
1 change: 1 addition & 0 deletions examples/proactive-messaging/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@microsoft/teams.config/eslint.config');
34 changes: 34 additions & 0 deletions examples/proactive-messaging/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@examples/proactive-messaging",
"version": "0.0.6",
"private": true,
"license": "MIT",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist",
"README.md"
],
"scripts": {
"clean": "npx rimraf ./dist",
"lint": "npx eslint",
"lint:fix": "npx eslint --fix",
"build": "npx tsc",
"start": "node . <CONVERSATION_ID>",
"dev": "tsx -r dotenv/config src/index.ts"
},
"dependencies": {
"@microsoft/teams.api": "2.0.5",
"@microsoft/teams.apps": "2.0.5",
"@microsoft/teams.cards": "2.0.5",
"@microsoft/teams.common": "2.0.5"
},
"devDependencies": {
"@microsoft/teams.config": "2.0.5",
"@types/node": "^22.5.4",
"dotenv": "^16.4.5",
"rimraf": "^6.0.1",
"tsx": "^4.20.6",
"typescript": "^5.4.5"
}
}
72 changes: 72 additions & 0 deletions examples/proactive-messaging/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Proactive Messaging Example
*
* Demonstrates sending messages without running a server using app.initialize().
* Note: If using app.start(), you can call app.send() directly without app.initialize().
*/

import { App } from '@microsoft/teams.apps';
import { ActionSet, AdaptiveCard, OpenUrlAction, TextBlock } from '@microsoft/teams.cards';
import { ConsoleLogger } from '@microsoft/teams.common/logging';

async function sendProactiveMessage(app: App, conversationId: string, message: string) {
console.log(`Sending proactive message to conversation: ${conversationId}`);
console.log(`Message: ${message}`);

const result = await app.send(conversationId, message);

console.log(`✓ Message sent successfully! Activity ID: ${result.id}`);
}

async function sendProactiveCard(app: App, conversationId: string) {
const card = new AdaptiveCard(
new TextBlock('Proactive Notification', { size: 'Large', weight: 'Bolder' }),
new TextBlock('This message was sent proactively without a server running!', { wrap: true }),
new TextBlock('Status: Active • Priority: High • Time: Now', { wrap: true, isSubtle: true }),
new ActionSet(
new OpenUrlAction('https://aka.ms/teams-sdk', { title: 'Learn More' })
)
);

console.log(`Sending proactive card to conversation: ${conversationId}`);

const result = await app.send(conversationId, card);

console.log(`✓ Card sent successfully! Activity ID: ${result.id}`);
}

async function main() {
const conversationId = process.argv[2];

if (!conversationId) {
console.error('Error: Missing conversation ID argument');
console.error('Usage: npm start <CONVERSATION_ID>');
console.error(' npm run dev <CONVERSATION_ID>');
process.exit(1);
}

const app = new App({
logger: new ConsoleLogger('@examples/proactive-messaging', { level: 'info' })
});

// Initialize without starting HTTP server
// Note: If using app.start(), skip this and call app.send() directly
// Without a server, you can only send messages - you cannot receive incoming messages
console.log('Initializing app (without starting server)...');
await app.initialize();
console.log('✓ App initialized\n');

await sendProactiveMessage(
app,
conversationId,
'Hello! This is a proactive message sent without a running server 🚀'
);

await new Promise(resolve => setTimeout(resolve, 2000));

await sendProactiveCard(app, conversationId);

console.log('\n✓ All proactive messages sent successfully!');
}

main().catch(console.error);
8 changes: 8 additions & 0 deletions examples/proactive-messaging/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "@microsoft/teams.config/tsconfig.node.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}
18 changes: 18 additions & 0 deletions examples/proactive-messaging/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["//"],
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"],
"cache": false,
"dependsOn": [
"@microsoft/teams.api#build",
"@microsoft/teams.apps#build",
"@microsoft/teams.cards#build",
"@microsoft/teams.common#build",
"@microsoft/teams.dev#build",
"@microsoft/teams.graph#build"
]
}
}
}
46 changes: 46 additions & 0 deletions packages/apps/src/activity-sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ActivityParams, Client, ConversationReference, SentActivity } from '@microsoft/teams.api';
import * as $http from '@microsoft/teams.common/http';
import { ILogger } from '@microsoft/teams.common/logging';

import { HttpStream } from './http-stream';
import { IActivitySender, IStreamer } from './types';

/**
* Handles sending activities to the Bot Framework
* Separate from transport concerns (HTTP, WebSocket, etc.)
*/
export class ActivitySender implements IActivitySender {
constructor(
private client: $http.Client,
private logger: ILogger
) { }

async send(activity: ActivityParams, ref: ConversationReference): Promise<SentActivity> {
// Create API client for this conversation's service URL
const api = new Client(ref.serviceUrl, this.client);

// Merge activity with conversation reference
activity = {
...activity,
from: ref.bot,
conversation: ref.conversation,
};

// Decide create vs update
if (activity.id) {
const res = await api.conversations
.activities(ref.conversation.id)
.update(activity.id, activity);
return { ...activity, ...res };
}

const res = await api.conversations.activities(ref.conversation.id).create(activity);
return { ...activity, ...res };
}

createStream(ref: ConversationReference): IStreamer {
// Create API client for this conversation's service URL
const api = new Client(ref.serviceUrl, this.client);
return new HttpStream(api, ref, this.logger);
}
}
18 changes: 5 additions & 13 deletions packages/apps/src/app.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
IActivitySentEvent,
IErrorEvent,
} from './events';
import { AppEvents, IPlugin, ISender } from './types';
import { AppEvents, IPlugin } from './types';

/**
* subscribe to an event
Expand Down Expand Up @@ -36,34 +36,26 @@ export async function onError<TPlugin extends IPlugin>(

export async function onActivitySent<TPlugin extends IPlugin>(
this: App<TPlugin>,
sender: ISender,
event: IActivitySentEvent
) {
for (const plugin of this.plugins) {
if (plugin.onActivitySent) {
await plugin.onActivitySent({
...event,
sender,
});
await plugin.onActivitySent(event);
}
}

this.events.emit('activity.sent', { ...event, sender });
this.events.emit('activity.sent', event);
}

export async function onActivityResponse<TPlugin extends IPlugin>(
this: App<TPlugin>,
sender: ISender,
event: IActivityResponseEvent
) {
for (const plugin of this.plugins) {
if (plugin.onActivityResponse) {
await plugin.onActivityResponse({
...event,
sender,
});
await plugin.onActivityResponse(event);
}
}

this.events.emit('activity.response', { ...event, sender });
this.events.emit('activity.response', event);
}
6 changes: 2 additions & 4 deletions packages/apps/src/app.plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,10 @@ describe('app.plugin', () => {
await app.start();

// Trigger a message activity by directly calling onActivity (internal API for testing)
const httpPlugin = app.getPlugin('http') as TestHttpPlugin;
const activity = new MessageActivity('test message');

// @ts-expect-error - accessing internal method for testing
await app.onActivity(httpPlugin, {
activity: activity.toInterface(),
await app.onActivity({
body: activity.toInterface(),
token: {
appId: 'test-app-id',
serviceUrl: 'https://test.botframework.com',
Expand Down
10 changes: 5 additions & 5 deletions packages/apps/src/app.plugins.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ILogger } from '@microsoft/teams.common';

import { App } from './app';
import { allIEventKeys, IEvents } from './events';
import { IPlugin, IPluginActivityEvent, IPluginErrorEvent, ISender, PluginName } from './types';
import { allIEventKeys, IActivityEvent, IEvents } from './events';
import { IPlugin, IPluginErrorEvent, PluginName } from './types';
import {
DependencyMetadata,
PLUGIN_DEPENDENCIES_METADATA_KEY,
Expand Down Expand Up @@ -81,11 +81,11 @@ export function inject<TPlugin extends IPlugin>(this: App<TPlugin>, plugin: IPlu

if (name === 'error') {
handler = (event: IPluginErrorEvent) => {
this.onError({ ...event, sender: plugin });
this.onError(event);
};
} else if (name === 'activity') {
handler = (event: IPluginActivityEvent) => {
return this.onActivity(plugin as ISender, event);
handler = (event: IActivityEvent) => {
return this.onActivity(event);
};
} else if (name === 'custom') {
handler = (name: string, event: unknown) => {
Expand Down
Loading