Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
},
"root": true
}

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ experimental/generator-dotnet-yeoman/node_modules
# Allow the root-level /packages/ directory or it gets confused with NuGet directories
!/[Pp]ackages/*

# Allow the packages directory under tests
!/tests/unit/[Pp]ackages/*

# Ignore correct yarn files (no zero install support)
.yarn/*
!.yarn/patches
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"packages/*",
"packages/*/js",
"skills/declarative/*",
"testing/*"
"tests/**/js",
"!tests/functional"
],
"scripts": {
"lint": "yarn workspaces foreach run lint"
Expand Down
17 changes: 10 additions & 7 deletions packages/Teams/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,24 @@
"botbuilder-dialogs-adaptive-runtime-core": "~4.14.0-preview",
"botbuilder-dialogs-declarative": "~4.14.0-preview",
"botbuilder-stdlib": "~4.14.0-internal",
"botframework-connector": "~4.14.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/lodash": "^4.14.168",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"adaptive-expressions": "4.14.0",
"botbuilder": "4.14.0",
"botbuilder-dialogs": "4.14.0",
"botbuilder-dialogs-adaptive": "~4.14.0-preview",
"botbuilder-dialogs-adaptive-runtime-core": "~4.14.0-preview",
"botbuilder-dialogs-declarative": "~4.14.0-preview",
"botbuilder-stdlib": "~4.14.0-internal",
"eslint": "latest",
"botbuilder-dialogs-adaptive": "4.14.0-preview",
"botbuilder-dialogs-adaptive-runtime-core": "4.14.0-preview",
"botbuilder-dialogs-declarative": "4.14.0-preview",
"botbuilder-stdlib": "4.14.0-internal",
"botframework-connector": "4.14.0",
"eslint": "^7.30.0",
"eslint-plugin-prettier": "latest",
"lodash": "^4.17.21",
"rimraf": "^3.0.2",
"typescript": "^4.0.5"
Expand Down
32 changes: 28 additions & 4 deletions packages/Teams/js/src/actions/actionHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export function getValue<T>(
* BotFrameworkAdapter and TestAdapter contain them, so we just need to make sure that dc.context.adapter contains
* an adapter with the appropriate auth methods.
*/
export interface HasAuthMethods {
getUserToken: typeof BotFrameworkAdapter.prototype.getUserToken;
getSignInLink: typeof BotFrameworkAdapter.prototype.getSignInLink;
}
export type HasAuthMethods = Pick<
BotFrameworkAdapter,
'getUserToken' | 'getSignInLink'
>;

/**
* Test to assert val has required auth methods.
Expand All @@ -55,3 +55,27 @@ export const testAdapterHasAuthMethods: Test<HasAuthMethods> = (
tests.isFunc((val as BotFrameworkAdapter).getSignInLink))
);
};

/**
* This is similar to HasAuthMethods, but purely for testing since TestAdapter does not have
* createConnectorClient, but BotFrameworkAdapter does.
*/
export type HasCreateConnectorClientMethod = Pick<
BotFrameworkAdapter,
'createConnectorClient'
>;

/**
* Test to assert val has required createConnectorClient method.
*
* @param {any} val Usually context.adapter.
* @returns {Assertion} Asserts that val has required createConnectorClient method.
*/
export const testAdapterHasCreateConnectorClientMethod: Test<HasCreateConnectorClientMethod> = (
val: unknown
): val is HasCreateConnectorClientMethod => {
return (
val instanceof BotFrameworkAdapter ||
tests.isFunc((val as BotFrameworkAdapter).createConnectorClient)
);
};
58 changes: 46 additions & 12 deletions packages/Teams/js/src/actions/baseAuthResponseDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
// Licensed under the MIT License.

import isEmpty from 'lodash/isEmpty';
import { ActionTypes, Activity, CardAction, TokenResponse } from 'botbuilder';
import {
ActionTypes,
Activity,
CardAction,
CloudAdapterBase,
TokenResponse,
} from 'botbuilder';
import {
BoolExpressionConverter,
Expression,
Expand All @@ -22,6 +28,7 @@ import {
BaseTeamsCacheInfoResponseDialog,
BaseTeamsCacheInfoResponseDialogConfiguration,
} from './baseTeamsCacheInfoResponseDialog';
import { UserTokenClient } from 'botframework-connector';

export interface BaseAuthResponseDialogConfiguration
extends BaseTeamsCacheInfoResponseDialogConfiguration {
Expand Down Expand Up @@ -98,8 +105,8 @@ export abstract class BaseAuthResponseDialog
if (tokenResponse) {
// We have the token, so the user is already signed in.
// Similar to OAuthInput, just return the token in the property.
if (this.property != null) {
dc.state.setValue(this.property?.getValue(dc.state), tokenResponse);
if (this.property) {
dc.state.setValue(this.property.getValue(dc.state), tokenResponse);
}

// End the dialog and return the token response.
Expand All @@ -125,19 +132,32 @@ export abstract class BaseAuthResponseDialog
* @param {CardAction} _cardAction CardAction with a valid Sign In Link.
* @returns {Partial<Activity>} Promise representing the InvokeResponse Activity specific to this type of auth dialog.
*/
protected createOAuthInvokeResponseActivityFromCardAction(
protected abstract createOAuthInvokeResponseActivityFromCardAction(
_dc: DialogContext,
_cardAction: CardAction
): Partial<Activity> {
throw new Error('NotImplemented');
}
): Partial<Activity>;

private async createOAuthInvokeResponseActivityFromTitleAndConnectionName(
dc: DialogContext,
title?: string,
connectionName?: string
title: string,
connectionName: string
): Promise<Partial<Activity>> {
// TODO: Switch to using Cloud OAuth after this PR gets merged: https://github.com/microsoft/botbuilder-js/pull/3149
const userTokenClient = dc.context.turnState.get<UserTokenClient>(
(dc.context.adapter as CloudAdapterBase).UserTokenClientKey
);
if (userTokenClient) {
const signInResource = await userTokenClient.getSignInResource(
connectionName,
dc.context.activity,
''
);
return this.createOAuthInvokeResponseActivityFromTitleAndSignInLink(
dc,
title,
signInResource.signInLink as string
);
}

if (!testAdapterHasAuthMethods(dc.context.adapter)) {
throw new Error('Auth is not supported by the current adapter.');
} else if (!title || !connectionName) {
Expand Down Expand Up @@ -176,13 +196,27 @@ export abstract class BaseAuthResponseDialog
dc: DialogContext,
connectionName: string
): Promise<TokenResponse> {
// TODO: Switch to using Cloud OAuth after this PR gets merged: https://github.com/microsoft/botbuilder-js/pull/3149
// When the Bot Service Auth flow completes, the action.State will contain a magic code used for verification.
const state = dc.context.activity.value?.state;

const userTokenClient = dc.context.turnState.get<UserTokenClient>(
(dc.context.adapter as CloudAdapterBase).UserTokenClientKey
);
if (userTokenClient) {
return userTokenClient.getUserToken(
dc.context.activity.from?.id,
connectionName,
dc.context.activity.channelId,
state
);
}

if (!testAdapterHasAuthMethods(dc.context.adapter)) {
throw new Error('Auth is not supported by the current adapter.');
}

// When the Bot Service Auth flow completes, the action.State will contain a magic code used for verification.
const magicCode = !isEmpty(dc.context.activity?.value)
const magicCode = !isEmpty(dc.context.activity.value)
? dc.context.activity.value.state
: undefined;

Expand Down
76 changes: 69 additions & 7 deletions packages/Teams/js/src/actions/sendMessageToTeamsChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
StringExpression,
StringExpressionConverter,
} from 'adaptive-expressions';
import { Activity, Channels, TeamsInfo } from 'botbuilder';
import {
Activity,
Channels,
CloudAdapterBase,
ConversationReference,
TeamsInfo,
} from 'botbuilder';
import {
Converter,
ConverterFactory,
Expand All @@ -19,8 +25,17 @@ import {
DialogTurnResult,
TemplateInterface,
} from 'botbuilder-dialogs';
import {
AuthenticationConstants,
ClaimsIdentity,
ConnectorClient,
JwtTokenValidation,
} from 'botframework-connector';
import { ActivityTemplateConverter } from 'botbuilder-dialogs-adaptive/lib/converters';
import { getValue } from './actionHelpers';
import {
getValue,
testAdapterHasCreateConnectorClientMethod,
} from './actionHelpers';

export interface SendMessageToTeamsChannelConfiguration
extends DialogConfiguration {
Expand Down Expand Up @@ -124,11 +139,58 @@ export class SendMessageToTeamsChannel
getValue(dc, this.teamsChannelId) ??
dc.context?.activity.channelData?.channel?.id;

const result = await TeamsInfo.sendMessageToTeamsChannel(
dc.context,
activity,
teamsChannelId
);
let result: [ConversationReference, string];

// Check for legacy adapter
if (testAdapterHasCreateConnectorClientMethod(dc.context.adapter)) {
const credentials = dc.context.turnState.get<ConnectorClient>(
dc.context.adapter.ConnectorClientKey
)?.credentials;
if (!credentials) {
throw new Error(
'Missing credentials as MicrosoftAppCredentials in ConnectorClient from TurnState'
);
}

result = await TeamsInfo.sendMessageToTeamsChannel(
dc.context,
activity,
teamsChannelId
);
} else if (dc.context.adapter instanceof CloudAdapterBase) {
const botIdentity = dc.context.turnState.get<ClaimsIdentity>(
dc.context.adapter.BotIdentityKey
);

let appId: string | undefined;
if (botIdentity) {
// 'version' is sometimes empty, which will result in no id returned from GetAppIdFromClaims.
appId = JwtTokenValidation.getAppIdFromClaims(botIdentity.claims);

if (!appId) {
appId = botIdentity.claims.find(
(claim) => claim.type === AuthenticationConstants.AudienceClaim
)?.value;
}

if (!appId) {
throw new Error('Missing AppIdClaim in ClaimsIdentity');
}
} else {
throw new Error(
'Missing dc.context.adapter.BotIdentityKey in TurnContext TurnState'
);
}

result = await TeamsInfo.sendMessageToTeamsChannel(
dc.context,
activity,
teamsChannelId,
appId
);
} else {
throw new Error('The adapter does not support SendMessageToTeamsChannel');
}

if (this.conversationReferenceProperty != null) {
dc.state.setValue(
Expand Down
25 changes: 12 additions & 13 deletions packages/Teams/js/src/actions/sendTabCardResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import {
BoolExpression,
BoolExpressionConverter,
StringExpressionConverter,
} from 'adaptive-expressions';
Expand All @@ -16,47 +17,45 @@ import {
import {
Converter,
ConverterFactory,
Dialog,
DialogContext,
DialogStateManager,
DialogTurnResult,
TemplateInterface,
} from 'botbuilder-dialogs';
import { ActivityTemplateConverter } from 'botbuilder-dialogs-adaptive/lib/converters';
import {
BaseAuthResponseDialog,
BaseAuthResponseDialogConfiguration,
} from './baseAuthResponseDialog';

export interface SendTabCardResponseConfiguration
extends BaseAuthResponseDialogConfiguration {
export interface SendTabCardResponseConfiguration extends Dialog {
cards?: TemplateInterface<Activity, DialogStateManager>;
disabled?: boolean | string | BoolExpression;
}

/**
* Send a Card Tab response to the user.
*/
export class SendTabCardResponse
extends BaseAuthResponseDialog
implements BaseAuthResponseDialogConfiguration {
export class SendTabCardResponse extends Dialog {
/**
* Class identifier.
*/
public static $kind = 'Teams.SendTabCardResponse';

/**
* Gets or sets an optional expression which if is true will disable this action.
*/
public disabled?: BoolExpression;

/**
* Template for the activity expression containing Adaptive Cards to send.
*/
public cards?: TemplateInterface<Activity, DialogStateManager>;

public getConverter(
property: keyof BaseAuthResponseDialogConfiguration | string
property: keyof Dialog | string
): Converter | ConverterFactory {
switch (property) {
case 'disabled':
return new BoolExpressionConverter();
case 'property':
case 'connectionName':
case 'title':
return new StringExpressionConverter();
case 'cards':
return new ActivityTemplateConverter();
Expand Down Expand Up @@ -114,7 +113,7 @@ export class SendTabCardResponse
*/
protected onComputeId(): string {
return `SendTabCardResponse[\
${this.title?.toString() ?? ''}\
${this.cards?.toString() ?? ''}\
]`;
}

Expand Down
Loading