@khester/create-dynamics-app 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +49 -0
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/artifacts/types.d.ts +1 -1
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts +1 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +2 -0
- package/dist/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/templates/teams-app/_variants/tab.basic/appPackage/manifest.template.json +2 -2
- package/templates/teams-app/_variants/tab.dashboard/appPackage/manifest.template.json +2 -2
- package/templates/teams-app/_variants/tab.sso/appPackage/manifest.template.json +2 -2
- package/templates/teams-bot/.env.example +19 -0
- package/templates/teams-bot/README.md +60 -0
- package/templates/teams-bot/_variants/bot.echo/appPackage/manifest.template.json +25 -0
- package/templates/teams-bot/_variants/bot.echo/src/bot/TeamsBot.ts +142 -0
- package/templates/teams-bot/_variants/bot.echo/src/bot/welcomeCard.ts +134 -0
- package/templates/teams-bot/_variants/bot.echo/src/index.ts +59 -0
- package/templates/teams-bot/_variants/bot.echo/src/mock/mockMode.ts +104 -0
- package/templates/teams-bot/_variants/bot.message-extension/appPackage/manifest.template.json +56 -0
- package/templates/teams-bot/_variants/bot.message-extension/src/bot/MessageExtensionBot.ts +114 -0
- package/templates/teams-bot/_variants/bot.message-extension/src/index.ts +51 -0
- package/templates/teams-bot/_variants/bot.message-extension/src/mock/mockData.ts +30 -0
- package/templates/teams-bot/_variants/bot.notification/appPackage/manifest.template.json +25 -0
- package/templates/teams-bot/_variants/bot.notification/src/bot/NotificationBot.ts +54 -0
- package/templates/teams-bot/_variants/bot.notification/src/index.ts +136 -0
- package/templates/teams-bot/_variants/bot.notification/src/mock/mockMode.ts +55 -0
- package/templates/teams-bot/_variants/bot.notification/src/storage/conversationStore.ts +84 -0
- package/templates/teams-bot/appPackage/color.png +0 -0
- package/templates/teams-bot/appPackage/outline.png +0 -0
- package/templates/teams-bot/gitignore +8 -0
- package/templates/teams-bot/nodemon.json +6 -0
- package/templates/teams-bot/package.json +24 -0
- package/templates/teams-bot/src/adapters/botAdapter.ts +45 -0
- package/templates/teams-bot/src/config.ts +34 -0
- package/templates/teams-bot/tsconfig.json +17 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CardFactory, Attachment } from 'botbuilder';
|
|
2
|
+
|
|
3
|
+
export function createWelcomeCard(userName: string): Attachment {
|
|
4
|
+
const card = {
|
|
5
|
+
type: 'AdaptiveCard',
|
|
6
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
7
|
+
version: '1.4',
|
|
8
|
+
body: [
|
|
9
|
+
{
|
|
10
|
+
type: 'TextBlock',
|
|
11
|
+
size: 'Large',
|
|
12
|
+
weight: 'Bolder',
|
|
13
|
+
text: `Welcome, ${userName}!`,
|
|
14
|
+
wrap: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'TextBlock',
|
|
18
|
+
text: "I'm a Teams bot built with Bot Framework. Here's what I can do:",
|
|
19
|
+
wrap: true,
|
|
20
|
+
spacing: 'Medium',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'FactSet',
|
|
24
|
+
facts: [
|
|
25
|
+
{ title: 'Echo', value: 'I repeat what you say' },
|
|
26
|
+
{ title: 'Help', value: 'Type "help" for commands' },
|
|
27
|
+
{ title: 'About', value: 'Type "about" for info' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'TextBlock',
|
|
32
|
+
text: 'Try sending me a message!',
|
|
33
|
+
wrap: true,
|
|
34
|
+
spacing: 'Medium',
|
|
35
|
+
color: 'Accent',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
actions: [
|
|
39
|
+
{
|
|
40
|
+
type: 'Action.Submit',
|
|
41
|
+
title: 'Say Hello',
|
|
42
|
+
data: { action: 'hello' },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return CardFactory.adaptiveCard(card);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createHelpCard(): Attachment {
|
|
51
|
+
const card = {
|
|
52
|
+
type: 'AdaptiveCard',
|
|
53
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
54
|
+
version: '1.4',
|
|
55
|
+
body: [
|
|
56
|
+
{
|
|
57
|
+
type: 'TextBlock',
|
|
58
|
+
size: 'Medium',
|
|
59
|
+
weight: 'Bolder',
|
|
60
|
+
text: 'Available Commands',
|
|
61
|
+
wrap: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'Container',
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
type: 'TextBlock',
|
|
68
|
+
text: '**help** - Show this help message',
|
|
69
|
+
wrap: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'TextBlock',
|
|
73
|
+
text: '**about** - Learn about this bot',
|
|
74
|
+
wrap: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'TextBlock',
|
|
78
|
+
text: '**echo [message]** - Echo your message',
|
|
79
|
+
wrap: true,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'TextBlock',
|
|
83
|
+
text: 'Or just type anything and I will echo it back!',
|
|
84
|
+
wrap: true,
|
|
85
|
+
spacing: 'Medium',
|
|
86
|
+
isSubtle: true,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return CardFactory.adaptiveCard(card);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createAboutCard(): Attachment {
|
|
97
|
+
const card = {
|
|
98
|
+
type: 'AdaptiveCard',
|
|
99
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
100
|
+
version: '1.4',
|
|
101
|
+
body: [
|
|
102
|
+
{
|
|
103
|
+
type: 'TextBlock',
|
|
104
|
+
size: 'Medium',
|
|
105
|
+
weight: 'Bolder',
|
|
106
|
+
text: 'About This Bot',
|
|
107
|
+
wrap: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'FactSet',
|
|
111
|
+
facts: [
|
|
112
|
+
{ title: 'Name', value: '{{projectName}}' },
|
|
113
|
+
{ title: 'Framework', value: 'Bot Framework SDK v4' },
|
|
114
|
+
{ title: 'Scaffold', value: 'create-dynamics-app (teams-bot)' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'TextBlock',
|
|
119
|
+
text: 'This bot demonstrates the fundamentals of building a Teams bot using the Bot Framework SDK.',
|
|
120
|
+
wrap: true,
|
|
121
|
+
spacing: 'Medium',
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
actions: [
|
|
125
|
+
{
|
|
126
|
+
type: 'Action.OpenUrl',
|
|
127
|
+
title: 'View Documentation',
|
|
128
|
+
url: 'https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return CardFactory.adaptiveCard(card);
|
|
134
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import express, { Request, Response } from 'express';
|
|
2
|
+
import { config, validateConfig } from './config';
|
|
3
|
+
import { createBotAdapter } from './adapters/botAdapter';
|
|
4
|
+
import { TeamsBot } from './bot/TeamsBot';
|
|
5
|
+
import { startInteractiveTest } from './mock/mockMode';
|
|
6
|
+
|
|
7
|
+
validateConfig();
|
|
8
|
+
|
|
9
|
+
// Interactive mock mode: console chat loop, no server / no Azure.
|
|
10
|
+
if (process.argv.includes('--interactive')) {
|
|
11
|
+
startInteractiveTest();
|
|
12
|
+
} else {
|
|
13
|
+
const app = express();
|
|
14
|
+
app.use(express.json());
|
|
15
|
+
|
|
16
|
+
const adapter = createBotAdapter();
|
|
17
|
+
const bot = new TeamsBot();
|
|
18
|
+
|
|
19
|
+
// Health check endpoint (the deploy pipeline gates on this)
|
|
20
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
21
|
+
res.json({
|
|
22
|
+
status: 'healthy',
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
mockMode: config.mockMode,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Bot messaging endpoint
|
|
29
|
+
app.post('/api/messages', async (req: Request, res: Response) => {
|
|
30
|
+
try {
|
|
31
|
+
await adapter.process(req, res, (context) => bot.run(context));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error processing message:', error);
|
|
34
|
+
res.status(500).send('Internal Server Error');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
39
|
+
res.json({
|
|
40
|
+
name: '{{projectName}}',
|
|
41
|
+
description: 'Microsoft Teams echo bot (Bot Framework SDK v4)',
|
|
42
|
+
endpoints: { messaging: '/api/messages', health: '/health' },
|
|
43
|
+
mockMode: config.mockMode,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.listen(config.port, () => {
|
|
48
|
+
console.log(`\nBot server is running!`);
|
|
49
|
+
console.log(` Local: http://localhost:${config.port}`);
|
|
50
|
+
console.log(` Health: http://localhost:${config.port}/health`);
|
|
51
|
+
console.log(` Mode: ${config.mockMode ? 'Mock (development)' : 'Production'}`);
|
|
52
|
+
console.log(`\n Messaging endpoint: /api/messages`);
|
|
53
|
+
|
|
54
|
+
if (config.mockMode) {
|
|
55
|
+
console.log(`\nTip: run with --interactive for console testing:`);
|
|
56
|
+
console.log(` npm run dev -- --interactive`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { TestAdapter, Activity, ConversationReference, TurnContext } from 'botbuilder';
|
|
2
|
+
import { TeamsBot } from '../bot/TeamsBot';
|
|
3
|
+
|
|
4
|
+
// Create a test adapter for mock mode testing
|
|
5
|
+
export function createMockAdapter(): TestAdapter {
|
|
6
|
+
const adapter = new TestAdapter(async (context: TurnContext) => {
|
|
7
|
+
const bot = new TeamsBot();
|
|
8
|
+
await bot.run(context);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return adapter;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Simulate a Teams conversation for testing
|
|
15
|
+
export async function runMockConversation(
|
|
16
|
+
userMessage: string,
|
|
17
|
+
callback?: (response: Partial<Activity>[]) => void
|
|
18
|
+
): Promise<Partial<Activity>[]> {
|
|
19
|
+
const adapter = createMockAdapter();
|
|
20
|
+
const responses: Partial<Activity>[] = [];
|
|
21
|
+
|
|
22
|
+
await adapter.send(userMessage).assertReply((activity) => {
|
|
23
|
+
responses.push(activity);
|
|
24
|
+
callback?.([activity]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return responses;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create a mock conversation reference for proactive messaging tests
|
|
31
|
+
export function createMockConversationReference(
|
|
32
|
+
userId: string = 'user-1'
|
|
33
|
+
): Partial<ConversationReference> {
|
|
34
|
+
return {
|
|
35
|
+
activityId: 'mock-activity-id',
|
|
36
|
+
user: {
|
|
37
|
+
id: userId,
|
|
38
|
+
name: 'Mock User',
|
|
39
|
+
},
|
|
40
|
+
bot: {
|
|
41
|
+
id: 'mock-bot-id',
|
|
42
|
+
name: 'Mock Bot',
|
|
43
|
+
},
|
|
44
|
+
conversation: {
|
|
45
|
+
id: 'mock-conversation-id',
|
|
46
|
+
isGroup: false,
|
|
47
|
+
conversationType: 'personal',
|
|
48
|
+
tenantId: 'mock-tenant-id',
|
|
49
|
+
name: '',
|
|
50
|
+
},
|
|
51
|
+
channelId: 'msteams',
|
|
52
|
+
serviceUrl: 'https://smba.trafficmanager.net/teams/',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Interactive console testing
|
|
57
|
+
export async function startInteractiveTest(): Promise<void> {
|
|
58
|
+
const readline = await import('readline');
|
|
59
|
+
const rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log('\n=== Bot Mock Mode Interactive Test ===');
|
|
65
|
+
console.log('Type messages to test the bot. Type "exit" to quit.\n');
|
|
66
|
+
|
|
67
|
+
const adapter = createMockAdapter();
|
|
68
|
+
|
|
69
|
+
const askQuestion = (): void => {
|
|
70
|
+
rl.question('You: ', async (input) => {
|
|
71
|
+
if (input.toLowerCase() === 'exit') {
|
|
72
|
+
console.log('Goodbye!');
|
|
73
|
+
rl.close();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await adapter.send(input).assertReply((activity) => {
|
|
80
|
+
if (activity.text) {
|
|
81
|
+
console.log(`Bot: ${activity.text}`);
|
|
82
|
+
}
|
|
83
|
+
if (activity.attachments && activity.attachments.length > 0) {
|
|
84
|
+
console.log('Bot: [Adaptive Card]');
|
|
85
|
+
const card = activity.attachments[0].content;
|
|
86
|
+
if (card.body) {
|
|
87
|
+
for (const block of card.body) {
|
|
88
|
+
if (block.type === 'TextBlock' && block.text) {
|
|
89
|
+
console.log(` ${block.text}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
console.log('Bot: [No response]');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
askQuestion();
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
askQuestion();
|
|
104
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
|
|
3
|
+
"manifestVersion": "1.19",
|
|
4
|
+
"id": "{{TEAMS_APP_ID}}",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"developer": {
|
|
7
|
+
"name": "Contoso",
|
|
8
|
+
"websiteUrl": "https://example.com",
|
|
9
|
+
"privacyUrl": "https://example.com/privacy",
|
|
10
|
+
"termsOfUseUrl": "https://example.com/terms"
|
|
11
|
+
},
|
|
12
|
+
"name": { "short": "{{projectName}}", "full": "{{projectName}}" },
|
|
13
|
+
"description": { "short": "{{projectName}} message extension", "full": "{{projectName}} — a Microsoft Teams message extension: search sample data from the compose box, create items via a task module, and unfurl links" },
|
|
14
|
+
"icons": { "color": "color.png", "outline": "outline.png" },
|
|
15
|
+
"accentColor": "#6264A7",
|
|
16
|
+
"composeExtensions": [
|
|
17
|
+
{
|
|
18
|
+
"botId": "{{BOT_ID}}",
|
|
19
|
+
"canUpdateConfiguration": false,
|
|
20
|
+
"commands": [
|
|
21
|
+
{
|
|
22
|
+
"id": "searchCommand",
|
|
23
|
+
"type": "query",
|
|
24
|
+
"title": "Search",
|
|
25
|
+
"description": "Search sample data",
|
|
26
|
+
"initialRun": true,
|
|
27
|
+
"context": ["commandBox", "compose"],
|
|
28
|
+
"parameters": [
|
|
29
|
+
{
|
|
30
|
+
"name": "query",
|
|
31
|
+
"title": "Search query",
|
|
32
|
+
"description": "Enter search terms"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "createCommand",
|
|
38
|
+
"type": "action",
|
|
39
|
+
"title": "Create",
|
|
40
|
+
"description": "Create a new item",
|
|
41
|
+
"fetchTask": true,
|
|
42
|
+
"context": ["commandBox", "compose"]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"messageHandlers": [
|
|
46
|
+
{
|
|
47
|
+
"type": "link",
|
|
48
|
+
"value": {
|
|
49
|
+
"domains": ["*.example.com"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"permissions": ["identity"]
|
|
56
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TeamsActivityHandler,
|
|
3
|
+
TurnContext,
|
|
4
|
+
MessagingExtensionQuery,
|
|
5
|
+
MessagingExtensionResponse,
|
|
6
|
+
MessagingExtensionAction,
|
|
7
|
+
MessagingExtensionActionResponse,
|
|
8
|
+
CardFactory,
|
|
9
|
+
TaskModuleResponse,
|
|
10
|
+
} from 'botbuilder';
|
|
11
|
+
import { searchProducts, mockProducts, IProduct } from '../mock/mockData';
|
|
12
|
+
|
|
13
|
+
export class MessageExtensionBot extends TeamsActivityHandler {
|
|
14
|
+
// Handle search-based message extension
|
|
15
|
+
protected async handleTeamsMessagingExtensionQuery(
|
|
16
|
+
context: TurnContext,
|
|
17
|
+
query: MessagingExtensionQuery
|
|
18
|
+
): Promise<MessagingExtensionResponse> {
|
|
19
|
+
const searchQuery = query.parameters?.[0]?.value || '';
|
|
20
|
+
const results = searchQuery ? searchProducts(searchQuery) : mockProducts.slice(0, 5);
|
|
21
|
+
|
|
22
|
+
const attachments = results.map((product) => this.createProductCard(product));
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
composeExtension: {
|
|
26
|
+
type: 'result',
|
|
27
|
+
attachmentLayout: 'list',
|
|
28
|
+
attachments,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle action-based message extension (fetch task)
|
|
34
|
+
protected async handleTeamsMessagingExtensionFetchTask(
|
|
35
|
+
_context: TurnContext,
|
|
36
|
+
_action: MessagingExtensionAction
|
|
37
|
+
): Promise<TaskModuleResponse> {
|
|
38
|
+
return {
|
|
39
|
+
task: {
|
|
40
|
+
type: 'continue',
|
|
41
|
+
value: {
|
|
42
|
+
title: 'Create New Product',
|
|
43
|
+
height: 400,
|
|
44
|
+
width: 400,
|
|
45
|
+
card: CardFactory.adaptiveCard({
|
|
46
|
+
type: 'AdaptiveCard',
|
|
47
|
+
version: '1.4',
|
|
48
|
+
body: [
|
|
49
|
+
{ type: 'TextBlock', text: 'Create Product', weight: 'Bolder', size: 'Medium' },
|
|
50
|
+
{ type: 'Input.Text', id: 'name', placeholder: 'Product name', label: 'Name' },
|
|
51
|
+
{ type: 'Input.Text', id: 'description', placeholder: 'Description', label: 'Description', isMultiline: true },
|
|
52
|
+
{ type: 'Input.Number', id: 'price', placeholder: '0', label: 'Price ($)' },
|
|
53
|
+
],
|
|
54
|
+
actions: [{ type: 'Action.Submit', title: 'Create' }],
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle action-based message extension (submit action)
|
|
62
|
+
protected async handleTeamsMessagingExtensionSubmitAction(
|
|
63
|
+
_context: TurnContext,
|
|
64
|
+
action: MessagingExtensionAction
|
|
65
|
+
): Promise<MessagingExtensionActionResponse> {
|
|
66
|
+
const data = action.data;
|
|
67
|
+
const product: IProduct = {
|
|
68
|
+
id: Date.now().toString(),
|
|
69
|
+
name: data.name || 'New Product',
|
|
70
|
+
description: data.description || '',
|
|
71
|
+
// Adaptive Card Input.Number submits its value as a string — coerce it.
|
|
72
|
+
price: Number(data.price) || 0,
|
|
73
|
+
category: 'New',
|
|
74
|
+
imageUrl: 'https://via.placeholder.com/150',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
composeExtension: {
|
|
79
|
+
type: 'result',
|
|
80
|
+
attachmentLayout: 'list',
|
|
81
|
+
attachments: [this.createProductCard(product)],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle link unfurling
|
|
87
|
+
protected async handleTeamsAppBasedLinkQuery(
|
|
88
|
+
_context: TurnContext,
|
|
89
|
+
_query: { url: string }
|
|
90
|
+
): Promise<MessagingExtensionResponse> {
|
|
91
|
+
const product = mockProducts[0];
|
|
92
|
+
return {
|
|
93
|
+
composeExtension: {
|
|
94
|
+
type: 'result',
|
|
95
|
+
attachmentLayout: 'list',
|
|
96
|
+
attachments: [this.createProductCard(product)],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private createProductCard(product: IProduct) {
|
|
102
|
+
const card = CardFactory.thumbnailCard(
|
|
103
|
+
product.name,
|
|
104
|
+
`$${product.price} | ${product.category}`,
|
|
105
|
+
[product.imageUrl],
|
|
106
|
+
[],
|
|
107
|
+
{ subtitle: product.description }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const preview = CardFactory.thumbnailCard(product.name, product.description, [product.imageUrl]);
|
|
111
|
+
|
|
112
|
+
return { ...card, preview };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import express, { Request, Response } from 'express';
|
|
2
|
+
import { config, validateConfig } from './config';
|
|
3
|
+
import { createBotAdapter } from './adapters/botAdapter';
|
|
4
|
+
import { MessageExtensionBot } from './bot/MessageExtensionBot';
|
|
5
|
+
|
|
6
|
+
validateConfig();
|
|
7
|
+
|
|
8
|
+
// A message extension is invoked from the Teams compose box (search / action /
|
|
9
|
+
// link unfurling), not a console — so there is no --interactive mock loop here.
|
|
10
|
+
const app = express();
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
const adapter = createBotAdapter();
|
|
14
|
+
const bot = new MessageExtensionBot();
|
|
15
|
+
|
|
16
|
+
// Health check endpoint (the deploy pipeline gates on this)
|
|
17
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
18
|
+
res.json({
|
|
19
|
+
status: 'healthy',
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
mockMode: config.mockMode,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Bot messaging endpoint — message-extension queries/actions/link-queries arrive here
|
|
26
|
+
app.post('/api/messages', async (req: Request, res: Response) => {
|
|
27
|
+
try {
|
|
28
|
+
await adapter.process(req, res, (context) => bot.run(context));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error processing message:', error);
|
|
31
|
+
res.status(500).send('Internal Server Error');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
36
|
+
res.json({
|
|
37
|
+
name: '{{projectName}}',
|
|
38
|
+
description: 'Microsoft Teams message extension (Bot Framework SDK v4)',
|
|
39
|
+
endpoints: { messaging: '/api/messages', health: '/health' },
|
|
40
|
+
mockMode: config.mockMode,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.listen(config.port, () => {
|
|
45
|
+
console.log(`\nMessage-extension server is running!`);
|
|
46
|
+
console.log(` Local: http://localhost:${config.port}`);
|
|
47
|
+
console.log(` Health: http://localhost:${config.port}/health`);
|
|
48
|
+
console.log(` Mode: ${config.mockMode ? 'Mock (development)' : 'Production'}`);
|
|
49
|
+
console.log(`\n Messaging endpoint: /api/messages`);
|
|
50
|
+
console.log(` Invoke it from the Teams compose box (search / action / link unfurl).`);
|
|
51
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface IProduct {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
price: number;
|
|
6
|
+
category: string;
|
|
7
|
+
imageUrl: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Sample data backing the search / action / link-unfurling responses. Swap this
|
|
11
|
+
// for a live Dataverse query (app-only token) per the teams-message-extension runbook.
|
|
12
|
+
export const mockProducts: IProduct[] = [
|
|
13
|
+
{ id: '1', name: 'Laptop Pro', description: 'High-performance laptop', price: 1299, category: 'Electronics', imageUrl: 'https://via.placeholder.com/150' },
|
|
14
|
+
{ id: '2', name: 'Wireless Mouse', description: 'Ergonomic wireless mouse', price: 49, category: 'Electronics', imageUrl: 'https://via.placeholder.com/150' },
|
|
15
|
+
{ id: '3', name: 'Mechanical Keyboard', description: 'RGB mechanical keyboard', price: 129, category: 'Electronics', imageUrl: 'https://via.placeholder.com/150' },
|
|
16
|
+
{ id: '4', name: 'USB-C Hub', description: '7-in-1 USB-C hub', price: 79, category: 'Electronics', imageUrl: 'https://via.placeholder.com/150' },
|
|
17
|
+
{ id: '5', name: 'Monitor Stand', description: 'Adjustable monitor stand', price: 89, category: 'Office', imageUrl: 'https://via.placeholder.com/150' },
|
|
18
|
+
{ id: '6', name: 'Desk Lamp', description: 'LED desk lamp', price: 45, category: 'Office', imageUrl: 'https://via.placeholder.com/150' },
|
|
19
|
+
{ id: '7', name: 'Webcam HD', description: '1080p webcam', price: 99, category: 'Electronics', imageUrl: 'https://via.placeholder.com/150' },
|
|
20
|
+
{ id: '8', name: 'Headphones', description: 'Noise-canceling headphones', price: 299, category: 'Electronics', imageUrl: 'https://via.placeholder.com/150' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function searchProducts(query: string): IProduct[] {
|
|
24
|
+
const q = query.toLowerCase();
|
|
25
|
+
return mockProducts.filter(p =>
|
|
26
|
+
p.name.toLowerCase().includes(q) ||
|
|
27
|
+
p.description.toLowerCase().includes(q) ||
|
|
28
|
+
p.category.toLowerCase().includes(q)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
|
|
3
|
+
"manifestVersion": "1.19",
|
|
4
|
+
"id": "{{TEAMS_APP_ID}}",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"developer": {
|
|
7
|
+
"name": "Contoso",
|
|
8
|
+
"websiteUrl": "https://example.com",
|
|
9
|
+
"privacyUrl": "https://example.com/privacy",
|
|
10
|
+
"termsOfUseUrl": "https://example.com/terms"
|
|
11
|
+
},
|
|
12
|
+
"name": { "short": "{{projectName}}", "full": "{{projectName}}" },
|
|
13
|
+
"description": { "short": "{{projectName}} notifications", "full": "{{projectName}} — a Microsoft Teams bot that delivers proactive notifications; message it 'subscribe' to opt in" },
|
|
14
|
+
"icons": { "color": "color.png", "outline": "outline.png" },
|
|
15
|
+
"accentColor": "#6264A7",
|
|
16
|
+
"bots": [
|
|
17
|
+
{
|
|
18
|
+
"botId": "{{BOT_ID}}",
|
|
19
|
+
"scopes": ["personal", "team", "groupChat"],
|
|
20
|
+
"supportsFiles": false,
|
|
21
|
+
"isNotificationOnly": false
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"permissions": ["identity"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TeamsActivityHandler, TurnContext, MessageFactory, CardFactory, Attachment } from 'botbuilder';
|
|
2
|
+
import { IConversationStore } from '../storage/conversationStore';
|
|
3
|
+
|
|
4
|
+
export class NotificationBot extends TeamsActivityHandler {
|
|
5
|
+
constructor(private conversationStore: IConversationStore) {
|
|
6
|
+
super();
|
|
7
|
+
|
|
8
|
+
// Capture the conversation reference whenever we hear from a user — this is
|
|
9
|
+
// what proactive messaging replays later via continueConversationAsync.
|
|
10
|
+
this.onConversationUpdate(async (context: TurnContext, next: () => Promise<void>) => {
|
|
11
|
+
await this.saveConversationReference(context);
|
|
12
|
+
await next();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
this.onMessage(async (context: TurnContext, next: () => Promise<void>) => {
|
|
16
|
+
await this.saveConversationReference(context);
|
|
17
|
+
const text = context.activity.text?.toLowerCase().trim() || '';
|
|
18
|
+
|
|
19
|
+
if (text === 'subscribe') {
|
|
20
|
+
await context.sendActivity(MessageFactory.text('You are subscribed to notifications!'));
|
|
21
|
+
} else if (text === 'unsubscribe') {
|
|
22
|
+
const userId = context.activity.from.id;
|
|
23
|
+
await this.conversationStore.delete(userId);
|
|
24
|
+
await context.sendActivity(MessageFactory.text('You have been unsubscribed.'));
|
|
25
|
+
} else {
|
|
26
|
+
await context.sendActivity(
|
|
27
|
+
MessageFactory.text('Commands: **subscribe**, **unsubscribe**. Notifications arrive via POST /api/notify.')
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
await next();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async saveConversationReference(context: TurnContext): Promise<void> {
|
|
35
|
+
const reference = TurnContext.getConversationReference(context.activity);
|
|
36
|
+
const userId = context.activity.from?.id;
|
|
37
|
+
if (userId) {
|
|
38
|
+
await this.conversationStore.save(userId, reference);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static createNotificationCard(title: string, message: string): Attachment {
|
|
43
|
+
return CardFactory.adaptiveCard({
|
|
44
|
+
type: 'AdaptiveCard',
|
|
45
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
46
|
+
version: '1.4',
|
|
47
|
+
body: [
|
|
48
|
+
{ type: 'TextBlock', text: title, weight: 'Bolder', size: 'Medium', wrap: true },
|
|
49
|
+
{ type: 'TextBlock', text: message, wrap: true },
|
|
50
|
+
{ type: 'TextBlock', text: new Date().toLocaleString(), size: 'Small', isSubtle: true },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|