@microsoft/botbuilder-m365
Version:
M365 extensions for Microsoft BotBuilder, Alpha release.
469 lines (435 loc) • 20.6 kB
text/typescript
/**
* @module botbuilder-m365
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
TurnContext,
TaskModuleTaskInfo,
ActivityTypes,
InvokeResponse,
INVOKE_RESPONSE_KEY,
TaskModuleResponse,
MessagingExtensionResult,
MessagingExtensionActionResponse,
MessagingExtensionParameter,
MessagingExtensionQuery,
Activity
} from 'botbuilder';
import { Application, RouteSelector, Query } from './Application';
import { TurnState } from './TurnState';
const ANONYMOUS_QUERY_LINK_INVOKE_NAME = `composeExtension/anonymousQueryLink`;
const FETCH_TASK_INVOKE_NAME = `composeExtension/fetchTask`;
const QUERY_INVOKE_NAME = `composeExtension/query`;
const QUERY_LINK_INVOKE_NAME = `composeExtension/queryLink`;
const SELECT_ITEM_INVOKE_NAME = `composeExtension/selectItem`;
const SUBMIT_ACTION_INVOKE_NAME = `composeExtension/submitAction`;
export class MessageExtensions<TState extends TurnState> {
private readonly _app: Application<TState>;
public constructor(app: Application<TState>) {
this._app = app;
}
public anonymousQueryLink(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (context: TurnContext, state: TState) => Promise<MessagingExtensionResult>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, ANONYMOUS_QUERY_LINK_INVOKE_NAME);
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== ANONYMOUS_QUERY_LINK_INVOKE_NAME
) {
throw new Error(
`Unexpected MessageExtensions.anonymousQueryLink() triggered for activity type: ${context?.activity?.type}`
);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state);
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response: MessagingExtensionActionResponse = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
},
true
);
});
return this._app;
}
public botMessagePreviewEdit(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (
context: TurnContext,
state: TState,
previewActivity: Partial<Activity>
) => Promise<MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE_NAME, 'edit');
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== SUBMIT_ACTION_INVOKE_NAME ||
context?.activity?.value?.botMessagePreviewAction !== 'edit'
) {
throw new Error(
`Unexpected MessageExtensions.botMessagePreviewEdit() triggered for activity type: ${context?.activity?.type}`
);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context.activity.value?.botActivityPreview[0] ?? {});
await this.returnSubmitActionResponse(context, result);
},
true
);
});
return this._app;
}
public botMessagePreviewSend(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (context: TurnContext, state: TState, previewActivity: Partial<Activity>) => Promise<void>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE_NAME, 'send');
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== SUBMIT_ACTION_INVOKE_NAME ||
context?.activity?.value?.botMessagePreviewAction !== 'send'
) {
throw new Error(
`Unexpected MessageExtensions.botMessagePreviewSend() triggered for activity type: ${context?.activity?.type}`
);
}
// Call handler and then check to see if an invoke response has already been added
await handler(context, state, context.activity.value?.botActivityPreview[0] ?? {});
// Queue up invoke response
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
await context.sendActivity({
value: { body: {}, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
},
true
);
});
return this._app;
}
public fetchTask(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (context: TurnContext, state: TState) => Promise<TaskModuleTaskInfo | string>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, FETCH_TASK_INVOKE_NAME);
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== FETCH_TASK_INVOKE_NAME
) {
throw new Error(
`Unexpected MessageExtensions.fetchTask() triggered for activity type: ${context?.activity?.type}`
);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state);
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
let response: TaskModuleResponse;
if (typeof result == 'string') {
// Return message
response = {
task: {
type: 'message',
value: result
}
};
} else {
// Return card
response = {
task: {
type: 'continue',
value: result
}
};
}
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
},
true
);
});
return this._app;
}
public query<TParams extends Record<string, any> = Record<string, any>>(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (context: TurnContext, state: TState, query: Query<TParams>) => Promise<MessagingExtensionResult>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, QUERY_INVOKE_NAME);
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== QUERY_INVOKE_NAME
) {
throw new Error(
`Unexpected MessageExtensions.query() triggered for activity type: ${context?.activity?.type}`
);
}
// Flatten query options
const meQuery: MessagingExtensionQuery = context?.activity?.value ?? {};
const query: Query<TParams> = {
count: meQuery?.queryOptions?.count ?? 25,
skip: meQuery?.queryOptions?.skip ?? 0,
parameters: {} as TParams
};
// Flatten query parameters
(meQuery.parameters ?? []).forEach((param: MessagingExtensionParameter) => {
if (param.name) {
(query.parameters as any)[param.name] = param.value;
}
});
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, query);
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response: MessagingExtensionActionResponse = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
},
true
);
});
return this._app;
}
public queryLink(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (context: TurnContext, state: TState) => Promise<MessagingExtensionResult>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, QUERY_LINK_INVOKE_NAME);
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== QUERY_LINK_INVOKE_NAME
) {
throw new Error(
`Unexpected MessageExtensions.queryLink() triggered for activity type: ${context?.activity?.type}`
);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state);
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response: MessagingExtensionActionResponse = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
},
true
);
});
return this._app;
}
public selectItem(
handler: (context: TurnContext, state: TState, item: Record<string, any>) => Promise<MessagingExtensionResult>
): Application<TState> {
// Define static route selector
const selector = (context: TurnContext) =>
Promise.resolve(
context?.activity?.type == ActivityTypes.Invoke && context?.activity.name === SELECT_ITEM_INVOKE_NAME
);
// Add route
this._app.addRoute(
selector,
async (context, state) => {
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context?.activity?.value ?? {});
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
const response: MessagingExtensionActionResponse = {
composeExtension: result
};
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
},
true
);
return this._app;
}
public submitAction<TData>(
commandId: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
handler: (
context: TurnContext,
state: TState,
data: TData
) => Promise<MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined>
): Application<TState> {
(Array.isArray(commandId) ? commandId : [commandId]).forEach((cid) => {
const selector = createTaskSelector(cid, SUBMIT_ACTION_INVOKE_NAME);
this._app.addRoute(
selector,
async (context, state) => {
// Insure that we're in an invoke as expected
if (
context?.activity?.type !== ActivityTypes.Invoke ||
context?.activity?.name !== SUBMIT_ACTION_INVOKE_NAME
) {
throw new Error(
`Unexpected MessageExtensions.submitAction() triggered for activity type: ${context?.activity?.type}`
);
}
// Call handler and then check to see if an invoke response has already been added
const result = await handler(context, state, context.activity.value?.data ?? {});
await this.returnSubmitActionResponse(context, result);
},
true
);
});
return this._app;
}
private async returnSubmitActionResponse(
context: TurnContext,
result: MessagingExtensionResult | TaskModuleTaskInfo | string | null | undefined
): Promise<void> {
if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
// Format invoke response
let response: MessagingExtensionActionResponse;
if (typeof result == 'string') {
// Return message
response = {
task: {
type: 'message',
value: result
}
};
} else if (typeof result == 'object') {
if ((result as TaskModuleTaskInfo).card) {
// Return another task module
response = {
task: {
type: 'continue',
value: result as TaskModuleTaskInfo
}
};
} else {
// Return card to user
response = {
composeExtension: result as MessagingExtensionResult
};
}
} else {
// No action taken
response = {
composeExtension: undefined
};
}
// Queue up invoke response
await context.sendActivity({
value: { body: response, status: 200 } as InvokeResponse,
type: ActivityTypes.InvokeResponse
});
}
}
}
/**
*
* @param {string | RegExp | RouteSelector[]} commandId Name of the commandId
* @param {boolean} invokeName Whether or not the commandId a Teams invokable action
* @param {string} botMessagePreviewAction Message Extension preview action 'edit' or 'send'
* @returns {RouteSelector} Route selector function
*/
function createTaskSelector(
commandId: string | RegExp | RouteSelector,
invokeName: string,
botMessagePreviewAction?: 'edit' | 'send'
): RouteSelector {
if (typeof commandId == 'function') {
// Return the passed in selector function
return commandId;
} else if (commandId instanceof RegExp) {
// Return a function that matches the commandId using a RegExp
return (context: TurnContext) => {
const isInvoke = context?.activity?.type == ActivityTypes.Invoke && context?.activity?.name == invokeName;
if (
isInvoke &&
typeof context?.activity?.value?.commandId == 'string' &&
matchesPreviewAction(context.activity, botMessagePreviewAction)
) {
return Promise.resolve(commandId.test(context.activity.value.commandId));
} else {
return Promise.resolve(false);
}
};
} else {
// Return a function that attempts to match commandId
return (context: TurnContext) => {
const isInvoke = context?.activity?.type == ActivityTypes.Invoke && context?.activity?.name == invokeName;
return Promise.resolve(
isInvoke &&
context?.activity?.value?.commandId === commandId &&
matchesPreviewAction(context.activity, botMessagePreviewAction)
);
};
}
}
/**
* Checks if the activity is a bot message preview action.
*
* @param {Activity} activity The activity / communication type that is being checked.
* @param {string} botMessagePreviewAction Name of the preview action
* @returns {boolean} True if the activity is a bot message preview action, false if it is not.
*/
function matchesPreviewAction(activity: Activity, botMessagePreviewAction?: 'edit' | 'send'): boolean {
if (typeof activity?.value?.botMessagePreviewAction == 'string') {
return activity.value.botMessagePreviewAction == botMessagePreviewAction;
} else {
return botMessagePreviewAction == undefined;
}
}