Skip to content

Commit

Permalink
[JS] feat: add helper functions to to get Teams channels, members, an…
Browse files Browse the repository at this point in the history
…d details (#1949)

## Linked issues

closes: #minor

## Details

Provide a list of your changes here. If you are fixing a bug, please
provide steps to reproduce the bug.

#### Change details

> We have encapsulated three APIs from
[`TeamsInfo`](https://github.com/microsoft/botbuilder-js/blob/main/libraries/botbuilder/src/teamsInfo.ts)
class of `botbuilder` library: `getTeamChannels`, `getTeamDetails`, and
`getPagedMember`, to facilitate easier proactive message sending. These
three APIs have been wrapped as an initial sample. If this approach is
deemed acceptable, we intend to extend this encapsulation to include
additional APIs provided by `TeamsInfo`, such as
`sendMessageToListOfUsers`, `sendMessageToAllUsersInTenant`, and more.

PM Contact: @MuyangAmigo 

**code snippets**:
To use the APIs:
```ts
for (const reference of conversationReferences) {
  await app.sendProactiveActivity(
    reference,
    "Proactive message from RestifyNotiBot!"
  );

  if (reference.conversation?.conversationType === "channel") {
    const details = await app.getTeamDetails(reference);
    const teamMembers = await app.getPagedMembers(reference);
    console.log(details, teamMembers.members.length);
  }
}
```

**screenshots**:
Results are printed as belows, furthur

![image](https://github.com/user-attachments/assets/a0317b50-dff4-429a-89dd-832cfe95d3f4)

## Attestation Checklist

- [ ] 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 (updating the
doc strings in the code is sufficient)
- 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

### Additional information

> Feel free to add other relevant information below

---------

Co-authored-by: Yiqing Zhao <[email protected]>
Co-authored-by: Corina <[email protected]>
  • Loading branch information
3 people authored Dec 3, 2024
1 parent cd919c0 commit a8cc2c2
Show file tree
Hide file tree
Showing 2 changed files with 453 additions and 9 deletions.
308 changes: 307 additions & 1 deletion js/packages/teams-ai/src/Application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import {
MessageReactionTypes,
TestAdapter,
O365ConnectorCardActionQuery,
FileConsentCardResponse
FileConsentCardResponse,
TurnContext,
TeamsInfo,
ConversationReference,
TeamDetails,
ChannelInfo,
TeamsChannelAccount,
TeamsPagedMembersResult
} from 'botbuilder';

import {
Expand Down Expand Up @@ -920,4 +927,303 @@ describe('Application', () => {
});
});
});

describe('getTeamChannels', () => {
let app = new Application();
let stubContext: sinon.SinonStubbedInstance<TurnContext>;
const returnedChannels: ChannelInfo[] = [{ id: 'testChannelId', name: 'testName' }];

beforeEach(() => {
app = new Application({ adapter: new TeamsAdapter() });
stubContext = sandbox.createStubInstance(TurnContext);
const stubAdapter = sandbox.createStubInstance(CloudAdapter);
(
stubAdapter.continueConversationAsync as unknown as sinon.SinonStub<
[string, Partial<ConversationReference>, (context: TurnContext) => Promise<void>],
Promise<void>
>
).callsFake(async (fakeBotAppId, ref, logic) => {
await logic(stubContext);
});
sandbox.stub(app, 'adapter').get(() => stubAdapter);
sandbox.stub(TeamsInfo, 'getTeamChannels').resolves(returnedChannels);
});

it('should return empty array if conversationType is not channel', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'personal',
id: 'testChannelId',
name: 'testName'
}
});
const continueConversationAsyncStub = sandbox.stub(testAdapter, 'continueConversationAsync').resolves();

const channels = await app.getTeamChannels(new TurnContext(testAdapter, {}));

assert.equal(channels.length, 0);
assert(continueConversationAsyncStub.notCalled);
});

it('should return channel array if conversationType is channel with defined teamId', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});
sandbox.stub(TurnContext.prototype, 'activity').get(() => {
return {
channelData: {
team: {
id: 'testId'
}
}
};
});

const channels = await app.getTeamChannels({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.deepEqual(channels, returnedChannels);
});

it('should return channel array if conversationType is channel with defined conversationId and undefined name', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});
sandbox.stub(TurnContext.prototype, 'activity').get(() => {
return {
conversation: {
id: 'teamId'
}
};
});

const channels = await app.getTeamChannels({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.deepEqual(channels, returnedChannels);
});

it('should return empty array if conversationType is channel with defined name', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});
sandbox.stub(TurnContext.prototype, 'activity').get(() => {
return {
conversation: {
name: 'teamName',
id: 'teamId'
}
};
});

const channels = await app.getTeamChannels({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.equal(channels.length, 0);
});
});

describe('getTeamDetails', () => {
let app = new Application();
let stubContext: sinon.SinonStubbedInstance<TurnContext>;
const returnedDetails: TeamDetails = {
id: 'teamId',
name: 'teamName'
};

beforeEach(() => {
app = new Application({ adapter: new TeamsAdapter() });
stubContext = sandbox.createStubInstance(TurnContext);
const stubAdapter = sandbox.createStubInstance(CloudAdapter);
(
stubAdapter.continueConversationAsync as unknown as sinon.SinonStub<
[string, Partial<ConversationReference>, (context: TurnContext) => Promise<void>],
Promise<void>
>
).callsFake(async (fakeBotAppId, ref, logic) => {
await logic(stubContext);
});
sandbox.stub(app, 'adapter').get(() => stubAdapter);
sandbox.stub(TeamsInfo, 'getTeamDetails').resolves(returnedDetails);
});

it('should return undefined details if conversationType is not channel', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'personal',
id: 'testChannelId',
name: 'testName'
}
});
const continueConversationAsyncStub = sandbox.stub(testAdapter, 'continueConversationAsync').resolves();

const details = await app.getTeamDetails(new TurnContext(testAdapter, {}));

assert.equal(details, undefined);
assert(continueConversationAsyncStub.notCalled);
});

it('should return team details if conversationType is channel with defined teamId', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});
sandbox.stub(TurnContext.prototype, 'activity').get(() => {
return {
channelData: {
team: {
id: 'testId'
}
}
};
});

const details = await app.getTeamDetails({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.deepEqual(details, returnedDetails);
});

it('should return team details if conversationType is channel with defined conversationId and undefined name', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});
sandbox.stub(TurnContext.prototype, 'activity').get(() => {
return {
conversation: {
id: 'teamId'
}
};
});

const details = await app.getTeamDetails({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.deepEqual(details, returnedDetails);
});

it('should return undefined if conversationType is channel with defined name', async () => {
sandbox.stub(TurnContext, 'getConversationReference').returns({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});
sandbox.stub(TurnContext.prototype, 'activity').get(() => {
return {
conversation: {
name: 'teamName',
id: 'teamId'
}
};
});

const details = await app.getTeamDetails({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.equal(details, undefined);
});
});

describe('getPagedMembers', () => {
let app = new Application();
let stubContext: sinon.SinonStubbedInstance<TurnContext>;
const returnedPagedMembers: TeamsPagedMembersResult = {
continuationToken: 'token',
members: [{} as TeamsChannelAccount, {} as TeamsChannelAccount]
};

beforeEach(() => {
app = new Application({ adapter: new TeamsAdapter() });
stubContext = sandbox.createStubInstance(TurnContext);
const stubAdapter = sandbox.createStubInstance(CloudAdapter);
(
stubAdapter.continueConversationAsync as unknown as sinon.SinonStub<
[string, Partial<ConversationReference>, (context: TurnContext) => Promise<void>],
Promise<void>
>
).callsFake(async (fakeBotAppId, ref, logic) => {
await logic(stubContext);
});
sandbox.stub(app, 'adapter').get(() => stubAdapter);
sandbox.stub(TeamsInfo, 'getPagedMembers').resolves(returnedPagedMembers);
});

it('should return paged members result', async () => {
const pagedMembers = await app.getPagedMembers({
conversation: {
isGroup: false,
conversationType: 'channel',
id: 'testChannelId',
name: 'testName'
}
});

assert.deepEqual(pagedMembers, returnedPagedMembers);
});
});
});
Loading

0 comments on commit a8cc2c2

Please sign in to comment.