Skip to content

Commit

Permalink
[JS] feat: #243, #245 Task Module config fetch/submit, file consent, …
Browse files Browse the repository at this point in the history
…and actionable message invokes (#821)

## Linked issues

closes: #243, #245

## Details
- Add handling for 'config/fetch' and 'config/submit'
- Add handling for 'actionableMessage/executeAction`
- Add handling for 'fileConsent/invoke'


---
- Rename & refactor usage of `MessagingExtensionInvokeNames` to
`MessageExtensionInvokeNames`
- Have unit test coverage ignore export files

## Attestation Checklist


# Unit Tests TBD in later PR
- [x] My code follows the style guidelines of this project

- I have checked for/fixed spelling, linting, and other errors
- I have commented my code for clarity
- I have made corresponding changes to the documentation (we use
[TypeDoc](https://typedoc.org/) to document our code)
- My changes generate no new warnings
- I have added tests that validates my changes, and provides sufficient
test coverage. I have tested with:
  - Local testing
  - E2E testing in Teams
- New and existing unit tests pass locally with my changes

---------

Co-authored-by: Corina Gum <>
  • Loading branch information
corinagum committed Nov 21, 2023
1 parent 99f4354 commit bd25f7e
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 67 deletions.
9 changes: 8 additions & 1 deletion js/.nycrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
{
"extension": [".ts"],
"include": ["packages/**/src/**/*.ts", "lib/**/*.js"],
"exclude": ["**/node_modules/**", "**/tests/**", "**/coverage/**", "**/*.d.ts", "**/*.spec.ts"],
"exclude": [
"**/node_modules/**",
"**/tests/**",
"**/coverage/**",
"**/*.d.ts",
"**/*.spec.ts",
"packages/**/src/index.ts"
],
"reporter": ["html", "text"],
"all": true,
"cache": true
Expand Down
17 changes: 17 additions & 0 deletions js/packages/teams-ai/src/Application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,16 @@ describe('Application', () => {
});
});
}

it(`should throw an error when event is not known case`, () => {
assert.throws(
() =>
mockApp.messageEventUpdate('test' as any, async (_context, _state) => {
assert.fail('should not be called');
}),
new Error(`Invalid TeamsMessageEvent type: test`)
);
});
});

describe('messageUpdate', () => {
Expand Down Expand Up @@ -429,5 +439,12 @@ describe('Application', () => {
});
});
}

it('should throw an error when handler is not a function', () => {
assert.throws(
() => mockApp.messageEventUpdate('editMessage', 1 as any),
new Error(`MessageUpdate 'handler' for editMessage is number. Type of 'handler' must be a function.`)
);
});
});
});
77 changes: 72 additions & 5 deletions js/packages/teams-ai/src/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
ActivityTypes,
BotAdapter,
ConversationReference,
FileConsentCardResponse,
O365ConnectorCardActionQuery,
ResourceResponse,
Storage,
TurnContext
Expand Down Expand Up @@ -516,6 +518,71 @@ export class Application<TState extends TurnState = TurnState> {
return this;
}

/**
* Registers a handler to process when a file consent card is accepted by the user.
* @param {(context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise<void>} handler Function to call when the route is triggered.
* @returns {this} The application instance for chaining purposes.
*/
public fileConsentAccept(
handler: (context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise<void>
): this {
const selector = (context: TurnContext): Promise<boolean> => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.name === 'fileConsent/invoke' &&
context.activity.value?.action === 'accept'
);
};
const handlerWrapper = (context: TurnContext, state: TState) => {
return handler(context, state, context.activity.value as FileConsentCardResponse);
};
this.addRoute(selector, handlerWrapper);
return this;
}

/**
* Registers a handler to process when a file consent card is declined by the user.
* @param {(context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise<void>} handler Function to call when the route is triggered.
* @returns {this} The application instance for chaining purposes.
*/
public fileConsentDecline(
handler: (context: TurnContext, state: TState, fileConsentResponse: FileConsentCardResponse) => Promise<void>
): this {
const selector = (context: TurnContext): Promise<boolean> => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.name === 'fileConsent/invoke' &&
context.activity.value?.action === 'decline'
);
};
const handlerWrapper = (context: TurnContext, state: TState) => {
return handler(context, state, context.activity.value as FileConsentCardResponse);
};
this.addRoute(selector, handlerWrapper);
return this;
}

/**
* Registers a handler to process when a O365 Connector Card Action activity is received from the user.
* @param {(context: TurnContext, state: TState, query: O365ConnectorCardActionQuery) => Promise<void>} handler Function to call when the route is triggered.
* @returns {this} The application instance for chaining purposes.
*/
public O365ConnectorCardAction(
handler: (context: TurnContext, state: TState, query: O365ConnectorCardActionQuery) => Promise<void>
): this {
const selector = (context: TurnContext): Promise<boolean> => {
return Promise.resolve(
context.activity.type === ActivityTypes.Invoke &&
context.activity.name === 'actionableMessage/executeAction'
);
};
const handlerWrapper = (context: TurnContext, state: TState) => {
return handler(context, state, context.activity.value as O365ConnectorCardActionQuery);
};
this.addRoute(selector, handlerWrapper);
return this;
}

/**
* Dispatches an incoming activity to a handler registered with the application.
* @remarks
Expand Down Expand Up @@ -1045,9 +1112,9 @@ function createConversationUpdateSelector(event: ConversationUpdateEvents): Rout
return (context: TurnContext) => {
return Promise.resolve(
context?.activity?.type == ActivityTypes.ConversationUpdate &&
context?.activity?.channelData?.eventType == event &&
context?.activity?.channelData?.channel &&
context.activity.channelData?.team
context?.activity?.channelData?.eventType == event &&
context?.activity?.channelData?.channel &&
context.activity.channelData?.team
);
};
case 'membersAdded':
Expand Down Expand Up @@ -1198,8 +1265,8 @@ function createMessageReactionSelector(event: MessageReactionEvents): RouteSelec
return (context: TurnContext) => {
return Promise.resolve(
context?.activity?.type == ActivityTypes.MessageReaction &&
Array.isArray(context?.activity?.reactionsRemoved) &&
context.activity.reactionsRemoved.length > 0
Array.isArray(context?.activity?.reactionsRemoved) &&
context.activity.reactionsRemoved.length > 0
);
};
}
Expand Down
74 changes: 43 additions & 31 deletions js/packages/teams-ai/src/MessageExtensions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ import {
TurnContext
} from 'botbuilder';

const {
ANONYMOUS_QUERY_LINK_INVOKE,
FETCH_TASK_INVOKE,
QUERY_INVOKE,
QUERY_LINK_INVOKE,
SELECT_ITEM_INVOKE,
SUBMIT_ACTION_INVOKE,
QUERY_SETTING_URL,
CONFIGURE_SETTINGS,
QUERY_CARD_BUTTON_CLICKED
} = MessageExtensionsInvokeNames;

describe('MessageExtensions', () => {
const adapter = new TestAdapter();
let mockApp: Application;
Expand All @@ -22,9 +34,9 @@ describe('MessageExtensions', () => {
assert.equal(mockApp.messageExtensions instanceof MessageExtensions, true);
});

describe(`${MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE}`, () => {
describe(`${ANONYMOUS_QUERY_LINK_INVOKE}`, () => {
it('should return InvokeResponse with status code 200 with an unfurled link in the response', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE, {
const activity = createTestInvoke(ANONYMOUS_QUERY_LINK_INVOKE, {
url: 'https://www.youtube.com/watch?v=971YIvosuUk&ab_channel=MicrosoftDeveloper'
});
activity.channelId = Channels.Msteams;
Expand Down Expand Up @@ -82,9 +94,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.CONFIGURE_SETTINGS}`, () => {
describe(`${CONFIGURE_SETTINGS}`, () => {
it('should return InvokeResponse with status code 200 with the configure setting invoke name', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.CONFIGURE_SETTINGS, { theme: 'dark' });
const activity = createTestInvoke(CONFIGURE_SETTINGS, { theme: 'dark' });
activity.channelId = Channels.Msteams;

mockApp.messageExtensions.configureSettings(async (context: TurnContext, _state, value) => {
Expand All @@ -100,9 +112,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.FETCH_TASK_INVOKE}`, () => {
describe(`${FETCH_TASK_INVOKE}`, () => {
it('should return InvokeResponse with status code 200 with the task invoke card', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'showTaskModule'
});
activity.channelId = Channels.Msteams;
Expand Down Expand Up @@ -157,7 +169,7 @@ describe('MessageExtensions', () => {
});

it('should return InvokeResponse with status code 200 with a string message', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'showMessage'
});
activity.channelId = Channels.Msteams;
Expand All @@ -183,22 +195,22 @@ describe('MessageExtensions', () => {

it('should call the same handler among an array of commandIds', async () => {
// commandId: ['showTaskModule', 'show', 'show task module']
const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'showTaskModule'
});
activity.channelId = Channels.Msteams;
const regexp = new RegExp(/show$/, 'i');
const activity2 = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity2 = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'Show'
});
activity2.channelId = Channels.Msteams;

const activity3 = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity3 = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'show task module'
});
activity3.channelId = Channels.Msteams;

const activity4 = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity4 = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'show task'
});
activity4.channelId = Channels.Msteams;
Expand Down Expand Up @@ -245,15 +257,15 @@ describe('MessageExtensions', () => {

it('should throw an error when the routeSelector routes incorrectly', async () => {
// Incorrect invoke
const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, {
const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, {
commandId: 'Create',
botActivityPreview: [1],
botMessagePreviewAction: 'edit'
});

mockApp.messageExtensions.fetchTask(
async (context) => {
return context.activity.name === MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE;
return context.activity.name === SUBMIT_ACTION_INVOKE;
},
async (context: TurnContext, _state) => {
assert.fail('should not have reached this point');
Expand All @@ -268,9 +280,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE}`, () => {
describe(`${SUBMIT_ACTION_INVOKE}`, () => {
it('should return InvokeResponse with status code 200 with the submit action invoke name for submitAction', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, {
const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, {
commandId: 'giveKudos',
commandContext: 'compose',
context: {
Expand Down Expand Up @@ -342,7 +354,7 @@ describe('MessageExtensions', () => {
});

it('should return InvokeResponse with status code 200 with the submit action invoke name for botMessagePreviewSend', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, {
const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, {
commandId: 'Create Preview',
botActivityPreview: [1],
botMessagePreviewAction: 'send'
Expand All @@ -365,7 +377,7 @@ describe('MessageExtensions', () => {
});

it('should return InvokeResponse with status code 200 with the submit action invoke name for botMessagePreviewEdit', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, {
const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, {
commandId: 'Create Preview',
botActivityPreview: [1],
botMessagePreviewAction: 'edit'
Expand All @@ -390,14 +402,14 @@ describe('MessageExtensions', () => {

it('should call the same handler among an array of commandIds for botMessagePreviewSend', async () => {
// commandId: ['create preview', 'preview']
const activity = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, {
const activity = createTestInvoke(SUBMIT_ACTION_INVOKE, {
commandId: 'create preview',
botActivityPreview: ['create preview'],
botMessagePreviewAction: 'send'
});
activity.channelId = Channels.Msteams;

const activity2 = createTestInvoke(MessageExtensionsInvokeNames.SUBMIT_ACTION_INVOKE, {
const activity2 = createTestInvoke(SUBMIT_ACTION_INVOKE, {
commandId: 'preview',
botActivityPreview: ['preview'],
botMessagePreviewAction: 'send'
Expand Down Expand Up @@ -426,7 +438,7 @@ describe('MessageExtensions', () => {

it('should throw an error when the routeSelector routes incorrectly for botMessagePreviewSend', async () => {
// Incorrect invoke
const activity = createTestInvoke(MessageExtensionsInvokeNames.FETCH_TASK_INVOKE, {
const activity = createTestInvoke(FETCH_TASK_INVOKE, {
commandId: 'Create',
botActivityPreview: [1],
botMessagePreviewAction: 'edit'
Expand All @@ -435,7 +447,7 @@ describe('MessageExtensions', () => {

mockApp.messageExtensions.botMessagePreviewSend(
async (context) => {
return context.activity.name === MessageExtensionsInvokeNames.FETCH_TASK_INVOKE;
return context.activity.name === FETCH_TASK_INVOKE;
},
async (context: TurnContext, _state, previewActivity) => {
assert.fail('should not have reached this point');
Expand All @@ -452,9 +464,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.QUERY_INVOKE}`, () => {
describe(`${QUERY_INVOKE}`, () => {
it('should return InvokeResponse with status code 200 with the query invoke name', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_INVOKE, { commandId: 'showQuery' });
const activity = createTestInvoke(QUERY_INVOKE, { commandId: 'showQuery' });
activity.channelId = Channels.Msteams;

interface MyParams {}
Expand Down Expand Up @@ -485,9 +497,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.QUERY_CARD_BUTTON_CLICKED}`, () => {
describe(`${QUERY_CARD_BUTTON_CLICKED}`, () => {
it('should return InvokeResponse with status code 200 with the query card button clicked invoke name', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_CARD_BUTTON_CLICKED, {
const activity = createTestInvoke(QUERY_CARD_BUTTON_CLICKED, {
title: 'Query button',
displayText: 'Yes',
value: 'Yes'
Expand All @@ -509,9 +521,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.QUERY_LINK_INVOKE}`, () => {
describe(`${QUERY_LINK_INVOKE}`, () => {
it('should return InvokeResponse with status code 200 with an unfurled link in the response', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_LINK_INVOKE, {
const activity = createTestInvoke(QUERY_LINK_INVOKE, {
url: 'https://www.youtube.com/watch?v=971YIvosuUk&ab_channel=MicrosoftDeveloper'
});
activity.channelId = Channels.Msteams;
Expand Down Expand Up @@ -570,9 +582,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.QUERY_SETTING_URL}`, async () => {
describe(`${QUERY_SETTING_URL}`, async () => {
it('should return InvokeResponse with status code 200 when querySettingUrl is invoked', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.QUERY_SETTING_URL, {});
const activity = createTestInvoke(QUERY_SETTING_URL, {});
activity.channelId = Channels.Msteams;

mockApp.messageExtensions.queryUrlSetting(async (context: TurnContext, _state) => {
Expand All @@ -590,9 +602,9 @@ describe('MessageExtensions', () => {
});
});

describe(`${MessageExtensionsInvokeNames.SELECT_ITEM_INVOKE}`, () => {
describe(`${SELECT_ITEM_INVOKE}`, () => {
it('should return InvokeResponse with status code 200 with selected item in the response', async () => {
const activity = createTestInvoke(MessageExtensionsInvokeNames.SELECT_ITEM_INVOKE, {
const activity = createTestInvoke(SELECT_ITEM_INVOKE, {
attachmentLayout: 'list',
attachments: [
{
Expand Down

0 comments on commit bd25f7e

Please sign in to comment.