@khester/create-dynamics-app 2.6.6 → 2.8.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 +3 -0
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +87 -6
- 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 +8 -0
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +4 -0
- package/dist/scaffold.js.map +1 -1
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js +26 -7
- package/dist/utils/copyTemplate.js.map +1 -1
- package/package.json +2 -2
- package/templates/teams-app/README.md +32 -9
- package/templates/teams-app/{appPackage/manifest.json → _variants/tab.basic/appPackage/manifest.template.json} +6 -5
- package/templates/teams-app/_variants/tab.dashboard/appPackage/manifest.template.json +38 -0
- package/templates/teams-app/_variants/tab.dashboard/auth.html +15 -0
- package/templates/teams-app/_variants/tab.dashboard/env.local.template +3 -0
- package/templates/teams-app/_variants/tab.dashboard/src/App.tsx +49 -0
- package/templates/teams-app/_variants/tab.dashboard/src/auth-bridge.ts +52 -0
- package/templates/teams-app/_variants/tab.dashboard/src/components/Dashboard.tsx +33 -0
- package/templates/teams-app/_variants/tab.dashboard/src/components/widgets/StatsWidget.tsx +25 -0
- package/templates/teams-app/_variants/tab.dashboard/src/components/widgets/TasksWidget.tsx +20 -0
- package/templates/teams-app/_variants/tab.dashboard/src/hooks/useHashRoute.ts +16 -0
- package/templates/teams-app/_variants/tab.dashboard/src/hooks/useTabConfig.ts +70 -0
- package/templates/teams-app/_variants/tab.dashboard/src/hooks/useTeamsAuth.ts +121 -0
- package/templates/teams-app/_variants/tab.dashboard/src/pages/Config.tsx +118 -0
- package/templates/teams-app/_variants/tab.dashboard/src/pages/Remove.tsx +12 -0
- package/templates/teams-app/_variants/tab.dashboard/src/vite-env.d.ts +10 -0
- package/templates/teams-app/_variants/tab.dashboard/vite.config.ts +19 -0
- package/templates/teams-app/_variants/tab.sso/appPackage/manifest.template.json +31 -0
- package/templates/teams-app/_variants/tab.sso/auth.html +15 -0
- package/templates/teams-app/_variants/tab.sso/env.local.template +3 -0
- package/templates/teams-app/_variants/tab.sso/src/App.tsx +57 -0
- package/templates/teams-app/_variants/tab.sso/src/auth-bridge.ts +52 -0
- package/templates/teams-app/_variants/tab.sso/src/hooks/useTeamsAuth.ts +121 -0
- package/templates/teams-app/_variants/tab.sso/src/vite-env.d.ts +10 -0
- package/templates/teams-app/_variants/tab.sso/vite.config.ts +19 -0
- package/templates/teams-app/appPackage/color.png +0 -0
- package/templates/teams-app/appPackage/outline.png +0 -0
- package/templates/teams-app/gitignore +2 -0
- package/templates/teams-app/package.json +1 -1
- package/templates/teams-bot/.env.example +18 -0
- package/templates/teams-bot/README.md +52 -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.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
- /package/templates/teams-app/{src → _variants/tab.basic/src}/App.tsx +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { app, authentication } from '@microsoft/teams-js';
|
|
2
|
+
import { PublicClientApplication } from '@azure/msal-browser';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Auth bridge — loaded by auth.html inside the window Teams opens for
|
|
6
|
+
* `authentication.authenticate()`. MSAL's popup flow is blocked inside the Teams
|
|
7
|
+
* tab iframe, so interactive sign-in happens here: loginRedirect → back to this
|
|
8
|
+
* page → handleRedirectPromise → notifySuccess back to the tab.
|
|
9
|
+
*/
|
|
10
|
+
const setStatus = (msg: string): void => {
|
|
11
|
+
const el = document.getElementById('status');
|
|
12
|
+
if (el) el.textContent = msg;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
(async () => {
|
|
16
|
+
try {
|
|
17
|
+
await app.initialize();
|
|
18
|
+
|
|
19
|
+
const clientId = import.meta.env.VITE_CLIENT_ID ?? '';
|
|
20
|
+
const tenantId = import.meta.env.VITE_TENANT_ID ?? 'common';
|
|
21
|
+
if (!clientId) throw new Error('VITE_CLIENT_ID is not set — copy env.local.template to .env.local');
|
|
22
|
+
|
|
23
|
+
const pca = new PublicClientApplication({
|
|
24
|
+
auth: {
|
|
25
|
+
clientId,
|
|
26
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
27
|
+
redirectUri: `${window.location.origin}/auth.html`,
|
|
28
|
+
},
|
|
29
|
+
cache: { cacheLocation: 'localStorage' },
|
|
30
|
+
});
|
|
31
|
+
await pca.initialize();
|
|
32
|
+
|
|
33
|
+
setStatus('Completing sign-in…');
|
|
34
|
+
const response = await pca.handleRedirectPromise();
|
|
35
|
+
if (response?.account) {
|
|
36
|
+
setStatus(`Signed in as ${response.account.username}`);
|
|
37
|
+
authentication.notifySuccess(response.account.username);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setStatus('Redirecting to sign in…');
|
|
42
|
+
await pca.loginRedirect({ scopes: ['User.Read'] });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
setStatus(`Sign-in failed: ${msg}`);
|
|
46
|
+
try {
|
|
47
|
+
authentication.notifyFailure(msg);
|
|
48
|
+
} catch {
|
|
49
|
+
// Outside a Teams auth window — the status line above is the surface.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { app, authentication } from '@microsoft/teams-js';
|
|
3
|
+
import { PublicClientApplication, type AccountInfo } from '@azure/msal-browser';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Teams-first auth: try Teams SSO (`authentication.getAuthToken()`), fall back to
|
|
7
|
+
* MSAL. Inside the Teams iframe, interactive sign-in must go through the auth
|
|
8
|
+
* bridge (`auth.html`) — `loginPopup` is blocked there; outside Teams (plain
|
|
9
|
+
* browser tab) MSAL redirect works directly.
|
|
10
|
+
*
|
|
11
|
+
* VITE_CLIENT_ID / VITE_TENANT_ID come from `.env.local` (see env.local.template);
|
|
12
|
+
* `teams-app-manager.sh identity` writes them after registering the Entra app.
|
|
13
|
+
*/
|
|
14
|
+
const clientId = import.meta.env.VITE_CLIENT_ID ?? '';
|
|
15
|
+
const tenantId = import.meta.env.VITE_TENANT_ID ?? 'common';
|
|
16
|
+
|
|
17
|
+
const msalInstance = new PublicClientApplication({
|
|
18
|
+
auth: {
|
|
19
|
+
clientId,
|
|
20
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
21
|
+
redirectUri: window.location.origin,
|
|
22
|
+
},
|
|
23
|
+
cache: { cacheLocation: 'localStorage' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export interface AuthState {
|
|
27
|
+
token: string | null;
|
|
28
|
+
account: AccountInfo | null;
|
|
29
|
+
inTeams: boolean;
|
|
30
|
+
loading: boolean;
|
|
31
|
+
error: Error | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useTeamsAuth(): AuthState & { login: () => Promise<void> } {
|
|
35
|
+
const [state, setState] = useState<AuthState>({
|
|
36
|
+
token: null,
|
|
37
|
+
account: null,
|
|
38
|
+
inTeams: false,
|
|
39
|
+
loading: true,
|
|
40
|
+
error: null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const getToken = useCallback(async () => {
|
|
44
|
+
// 1. Teams SSO — works once the app registration exposes api://{host}/{clientId}
|
|
45
|
+
// with the Teams client ids pre-authorized.
|
|
46
|
+
try {
|
|
47
|
+
await app.initialize();
|
|
48
|
+
const ssoToken = await authentication.getAuthToken();
|
|
49
|
+
setState({ token: ssoToken, account: null, inTeams: true, loading: false, error: null });
|
|
50
|
+
return;
|
|
51
|
+
} catch {
|
|
52
|
+
// Not in Teams, or SSO not consented yet — fall through to MSAL.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. MSAL silent — a previous interactive sign-in left an account in the cache.
|
|
56
|
+
try {
|
|
57
|
+
await msalInstance.initialize();
|
|
58
|
+
const redirect = await msalInstance.handleRedirectPromise().catch(() => null);
|
|
59
|
+
const account = redirect?.account ?? msalInstance.getAllAccounts()[0];
|
|
60
|
+
if (account) {
|
|
61
|
+
const result = await msalInstance.acquireTokenSilent({
|
|
62
|
+
scopes: ['User.Read'],
|
|
63
|
+
account,
|
|
64
|
+
});
|
|
65
|
+
setState({
|
|
66
|
+
token: result.accessToken,
|
|
67
|
+
account: result.account,
|
|
68
|
+
inTeams: false,
|
|
69
|
+
loading: false,
|
|
70
|
+
error: null,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setState((prev) => ({ ...prev, loading: false }));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
setState((prev) => ({ ...prev, loading: false, error: error as Error }));
|
|
77
|
+
}
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
/** Interactive sign-in. In Teams → auth bridge popup; in a browser → MSAL redirect. */
|
|
81
|
+
const login = useCallback(async () => {
|
|
82
|
+
if (!clientId) {
|
|
83
|
+
setState((prev) => ({
|
|
84
|
+
...prev,
|
|
85
|
+
error: new Error('VITE_CLIENT_ID is not set — copy env.local.template to .env.local'),
|
|
86
|
+
}));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let inTeams = false;
|
|
90
|
+
try {
|
|
91
|
+
await app.initialize();
|
|
92
|
+
inTeams = true;
|
|
93
|
+
} catch {
|
|
94
|
+
// Plain browser tab.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (inTeams) {
|
|
99
|
+
// Teams opens auth.html in a real window where MSAL's redirect flow is
|
|
100
|
+
// allowed; the bridge calls notifySuccess/notifyFailure when done.
|
|
101
|
+
await authentication.authenticate({
|
|
102
|
+
url: `${window.location.origin}/auth.html`,
|
|
103
|
+
width: 600,
|
|
104
|
+
height: 535,
|
|
105
|
+
});
|
|
106
|
+
await getToken();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await msalInstance.initialize();
|
|
110
|
+
await msalInstance.loginRedirect({ scopes: ['User.Read'] });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
setState((prev) => ({ ...prev, error: error as Error }));
|
|
113
|
+
}
|
|
114
|
+
}, [getToken]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
void getToken();
|
|
118
|
+
}, [getToken]);
|
|
119
|
+
|
|
120
|
+
return { ...state, login };
|
|
121
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
// Teams tabs must be served over HTTPS; `vite --https` or a tunnel in dev.
|
|
8
|
+
server: { port: 3000 },
|
|
9
|
+
build: {
|
|
10
|
+
outDir: 'dist',
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
// MPA: the tab plus the auth bridge Teams opens for interactive sign-in.
|
|
13
|
+
input: {
|
|
14
|
+
main: fileURLToPath(new URL('./index.html', import.meta.url)),
|
|
15
|
+
auth: fileURLToPath(new URL('./auth.html', import.meta.url)),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Bot Framework credentials — written to .env.local by `teams-app-manager.sh identity`
|
|
2
|
+
# (which .env.local overrides; this file documents the full set). Never commit real values.
|
|
3
|
+
MICROSOFT_APP_ID=
|
|
4
|
+
MICROSOFT_APP_PASSWORD=
|
|
5
|
+
MICROSOFT_APP_TENANT_ID=
|
|
6
|
+
# SingleTenant (default) or MultiTenant — must match the Azure Bot resource's app type.
|
|
7
|
+
MICROSOFT_APP_TYPE=SingleTenant
|
|
8
|
+
|
|
9
|
+
# Shared secret required in the X-Notify-Key header by POST /api/notify (notification bots).
|
|
10
|
+
NOTIFY_API_KEY=
|
|
11
|
+
|
|
12
|
+
# Where conversation references persist (notification bots). On Azure App Service use
|
|
13
|
+
# /home/data/conversations.json so subscriptions survive restarts and redeploys.
|
|
14
|
+
CONVERSATION_STORE_PATH=./.data/conversations.json
|
|
15
|
+
|
|
16
|
+
PORT=3978
|
|
17
|
+
# Mock mode runs the bot without any Azure registration (try: npm run dev -- --interactive)
|
|
18
|
+
MOCK_MODE=true
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A Microsoft Teams bot built with the Bot Framework SDK v4 (TypeScript + Express),
|
|
4
|
+
scaffolded by `create-dynamics-app --artifact teams-bot`.
|
|
5
|
+
|
|
6
|
+
## Local development (no Azure needed)
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
cp .env.example .env # MOCK_MODE=true is the default
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev -- --interactive # console chat loop against the bot logic
|
|
12
|
+
npm run dev # or: run the HTTP server on http://localhost:3978
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Mock mode drives the bot through the Bot Framework `TestAdapter` — no app registration,
|
|
16
|
+
no tunnel. The HTTP server exposes:
|
|
17
|
+
|
|
18
|
+
| Endpoint | Purpose |
|
|
19
|
+
|----------|---------|
|
|
20
|
+
| `POST /api/messages` | Bot Framework messaging endpoint |
|
|
21
|
+
| `GET /health` | Health probe (used by the deploy pipeline) |
|
|
22
|
+
| `POST /api/notify` | Proactive notifications (notification bots only; requires the `X-Notify-Key` header) |
|
|
23
|
+
|
|
24
|
+
## Ship it to Azure + Teams
|
|
25
|
+
|
|
26
|
+
The workspace pipeline provisions everything (App Service hosting, Entra app + secret,
|
|
27
|
+
Azure Bot resource, Teams channel, app package):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
teams-app-manager.sh up --project . --sideload
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or run the stages individually: `hosting` → `identity` → `deploy` → `package` → `sideload`.
|
|
34
|
+
The packaged zip lands in `build/` and is uploaded to Teams via the admin center or
|
|
35
|
+
"Upload a custom app". See `msft/05-m365/docs/teams-bot-runbook.md` in the workspace.
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
All settings come from env vars (see `.env.example`). In Azure they are App Service
|
|
40
|
+
app settings; locally they live in `.env` / `.env.local` (the identity stage writes
|
|
41
|
+
credentials to `.env.local`, which takes precedence).
|
|
42
|
+
|
|
43
|
+
Key settings: the Bot Framework credential triple (app id / password / tenant id), the
|
|
44
|
+
app type (SingleTenant default — must match the Azure Bot resource), the notify API key,
|
|
45
|
+
and the conversation-store path (notification bots persist subscriber references there;
|
|
46
|
+
on App Service use `/home/data/conversations.json` so they survive restarts).
|
|
47
|
+
|
|
48
|
+
## Manifest
|
|
49
|
+
|
|
50
|
+
`appPackage/manifest.template.json` keeps two deploy-time placeholders — the Teams app
|
|
51
|
+
id and the bot id — that `teams-app-manager.sh package` renders from the provisioning
|
|
52
|
+
state; everything else is baked at scaffold time.
|
|
@@ -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}} bot", "full": "{{projectName}} — a Microsoft Teams bot that echoes messages and responds to commands" },
|
|
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,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TeamsActivityHandler,
|
|
3
|
+
TurnContext,
|
|
4
|
+
MessageFactory,
|
|
5
|
+
TeamsInfo,
|
|
6
|
+
AdaptiveCardInvokeValue,
|
|
7
|
+
AdaptiveCardInvokeResponse,
|
|
8
|
+
} from 'botbuilder';
|
|
9
|
+
import { createWelcomeCard, createHelpCard, createAboutCard } from './welcomeCard';
|
|
10
|
+
|
|
11
|
+
export class TeamsBot extends TeamsActivityHandler {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
|
|
15
|
+
// Handle incoming messages
|
|
16
|
+
this.onMessage(async (context: TurnContext, next: () => Promise<void>) => {
|
|
17
|
+
await this.handleMessage(context);
|
|
18
|
+
await next();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Handle members added (welcome message)
|
|
22
|
+
this.onMembersAdded(async (context: TurnContext, next: () => Promise<void>) => {
|
|
23
|
+
await this.handleMembersAdded(context);
|
|
24
|
+
await next();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Handle members removed
|
|
28
|
+
this.onMembersRemoved(async (context: TurnContext, next: () => Promise<void>) => {
|
|
29
|
+
await this.handleMembersRemoved(context);
|
|
30
|
+
await next();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async handleMessage(context: TurnContext): Promise<void> {
|
|
35
|
+
// Remove bot mention from message
|
|
36
|
+
const text = this.removeMentionText(context);
|
|
37
|
+
const command = text.toLowerCase().trim();
|
|
38
|
+
|
|
39
|
+
console.log(`Received message: "${text}"`);
|
|
40
|
+
|
|
41
|
+
// Handle commands
|
|
42
|
+
switch (command) {
|
|
43
|
+
case 'help':
|
|
44
|
+
await context.sendActivity({
|
|
45
|
+
attachments: [createHelpCard()],
|
|
46
|
+
});
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case 'about':
|
|
50
|
+
await context.sendActivity({
|
|
51
|
+
attachments: [createAboutCard()],
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
// Echo the message back
|
|
57
|
+
if (command.startsWith('echo ')) {
|
|
58
|
+
const echoText = text.substring(5);
|
|
59
|
+
await context.sendActivity(MessageFactory.text(`Echo: ${echoText}`));
|
|
60
|
+
} else {
|
|
61
|
+
await context.sendActivity(MessageFactory.text(`You said: "${text}"`));
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async handleMembersAdded(context: TurnContext): Promise<void> {
|
|
68
|
+
const membersAdded = context.activity.membersAdded || [];
|
|
69
|
+
const botId = context.activity.recipient.id;
|
|
70
|
+
|
|
71
|
+
for (const member of membersAdded) {
|
|
72
|
+
// Only send welcome when the bot is added, not when users join
|
|
73
|
+
if (member.id !== botId) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get the user's name
|
|
78
|
+
let userName = 'there';
|
|
79
|
+
try {
|
|
80
|
+
const members = await TeamsInfo.getMembers(context);
|
|
81
|
+
if (members && members.length > 0) {
|
|
82
|
+
// Find the user who added the bot (typically the first non-bot member)
|
|
83
|
+
const user = members.find((m) => m.id !== botId);
|
|
84
|
+
if (user) {
|
|
85
|
+
userName = user.name || user.givenName || 'there';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.log('Could not get team members, using default greeting');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Send welcome card
|
|
93
|
+
await context.sendActivity({
|
|
94
|
+
attachments: [createWelcomeCard(userName)],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async handleMembersRemoved(context: TurnContext): Promise<void> {
|
|
100
|
+
const membersRemoved = context.activity.membersRemoved || [];
|
|
101
|
+
|
|
102
|
+
for (const member of membersRemoved) {
|
|
103
|
+
console.log(`Member removed: ${member.name || member.id}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle Adaptive Card submit actions
|
|
108
|
+
protected async onAdaptiveCardInvoke(
|
|
109
|
+
context: TurnContext,
|
|
110
|
+
invokeValue: AdaptiveCardInvokeValue
|
|
111
|
+
): Promise<AdaptiveCardInvokeResponse> {
|
|
112
|
+
const action = invokeValue.action?.data?.action;
|
|
113
|
+
|
|
114
|
+
if (action === 'hello') {
|
|
115
|
+
await context.sendActivity(MessageFactory.text('Hello! How can I help you today?'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
statusCode: 200,
|
|
120
|
+
type: 'application/vnd.microsoft.activity.message',
|
|
121
|
+
value: { message: 'Action processed' },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Helper to remove @mention from message text
|
|
126
|
+
private removeMentionText(context: TurnContext): string {
|
|
127
|
+
let text = context.activity.text || '';
|
|
128
|
+
const mentions = context.activity.entities?.filter(
|
|
129
|
+
(e) => e.type === 'mention'
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (mentions && mentions.length > 0) {
|
|
133
|
+
for (const mention of mentions) {
|
|
134
|
+
if (mention.mentioned?.id === context.activity.recipient.id) {
|
|
135
|
+
text = text.replace(mention.text || '', '').trim();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return text;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -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
|
+
}
|