Skip to content

Commit

Permalink
feat: cloud adapter (#3790)
Browse files Browse the repository at this point in the history
* feat: add additional BotFrameworkAuthentication classes for CloudAdapter (#3698)

* add additional BotFrameworkAuthentication classes for CloudAdapter

* Add Alexa to Channels enum for #3603

* fix export, USER_AGENT and style issue

* make path in Configuration.get optional, apply other PR feedback

* address additional PR feedback, minor cleanup

* update adaptive-runtime.Configuration to reflect contract

* cleanup typing, add unit tests, refactor existing code

* fix linting error in types.ts

* fix: remove Cortana from Channels enum (#3730)

* remove Cortana from Channels enum

* clean up linted choices_* tests

* feat: cloud adapter (#3728)

* port: cloud adapter base

- remove unnecessary emitter type
- streaming syntax issues
- introduce runtypes helpers
- bot framework adapter implements http adapter interface

* add created status code, remove dupe status codes

Not sure how the duplicate status codes piece was introduced, seems unnecessary though.

* add continue conv w/ claims to bot adapter

This enables us to properly pass claims to cloud adapter without plumbing them
all the way through turn context. This is handled in .NET via overloads, however
we can't achieve the same thing due to backwards-compat requirements in BotAdapter.

See .NET overloads:
https://github.com/microsoft/botbuilder-dotnet/blob/3c335046f95deeac50fbb0b48c7c8c42051d4f6d/libraries/Microsoft.Bot.Builder/CloudAdapterBase.cs#L147-L163

* connectory factory create -> audience?

* cloud adapter + associated runtime changes

- properly handle streaming requests
- include retries for named pipes

* cloud skill handler changes

- better http errors for channel service routes
- clean up channel service handler tests

This is essentially a port of microsoft/botbuilder-dotnet#5304

* better docs/comments all around

* plumb errors through to http responses

- req + res in turn context, use in adapter error handler
- fix several errors that should have yielded a 401

* port builtin auth handler logic

This accounts for the no-auth (Emulator) skills scenario. The logic exists in .NET here:
https://github.com/microsoft/botbuilder-dotnet/blob/3c335046f95deeac50fbb0b48c7c8c42051d4f6d/libraries/Microsoft.Bot.Connector/Authentication/BuiltinBotFrameworkAuthentication.cs#L72-L87

* fix anonymous auth handling

The default scenario after creating a bot using Composer is a settings file where `MicrosoftAppId` is set to `""`, aka
the empty string. When Emulator sends requests without an app ID/password, the auth header is missing and thus produces
a null app ID (identity.getClaims returns null). This commit properly accounts for this anonymous auth scenario.

* fix runtime constructor exception

The default runtime config does not set any of the configuration values related to auth. As a result, runtypes checking
fails. This change coerces the runtype into a partial (to match the contructor signature) and assigns the checked value.

Also,

- fix boolean coercian to stringify value
- use import type

* small runtypes cleanup

Also remove unnecessary "as" casting

* remove extra call to isValidAppId

This call does not exist in the .NET counterpart:
https://github.com/microsoft/botbuilder-dotnet/blob/main/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs#L76

* fix dialog context error message issue

* cloud adapter base -> core

Also extract response error handling to integrations as base no longer knows about HTTP specifics

* user token access helper

Also, refactor adaptive oauth input using oauth prompt

* reconcile core bot adapter changes

* handle streaming version request

Note that token handling also does not exist in .NET:
https://github.com/microsoft/botbuilder-dotnet/blob/main/libraries/Microsoft.Bot.Builder/Streaming/StreamingRequestHandler.cs#L517

* support service client credentials factory in runtime

* restore appId validation to pwServiceCredentialFactory.createCredentials()
- remove "only-warn" plugin
- add tests

* quick PR feedback fixes

* remove duplicate call to create connector client

* continueConversationAsync with overloads

* finishing touches

- remaining PR items
- remove BotLogic type
- test:compat fixes

Co-authored-by: stevengum <14935595+stevengum@users.noreply.github.com>

* fix: remaining runtypes -> zod (#3789)

* fix: test:compat

* fix: tests

Co-authored-by: Steven Gum <14935595+stevengum@users.noreply.github.com>
  • Loading branch information
Josh Gummersall and stevengum authored Jun 23, 2021
1 parent 542641d commit 636daf8
Show file tree
Hide file tree
Showing 87 changed files with 4,486 additions and 1,636 deletions.
10 changes: 7 additions & 3 deletions libraries/botbuilder-ai/src/luisRecognizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,13 @@ export interface LuisRecognizerOptionsV2 extends LuisRecognizerOptions {
// This zod type is purely used for type assertions in a scenario where we
// know better than the compiler does that we'll have a value of this type.
// This is just meant to operate as a simple type assertion.
const UnsafeLuisRecognizerUnion: z.ZodType<
LuisRecognizerOptionsV2 | LuisRecognizerOptionsV3 | LuisPredictionOptions
> = z.record(z.unknown());
const UnsafeLuisRecognizerUnion = z.custom<LuisRecognizerOptionsV3 | LuisRecognizerOptionsV2 | LuisPredictionOptions>(
(val: unknown): val is LuisRecognizerOptionsV3 | LuisRecognizerOptionsV2 | LuisPredictionOptions =>
z.record(z.unknown()).check(val),
{
message: 'LuisRecognizerOptionsV3 | LuisRecognizerOptionsV2 | LuisPredictionOptions',
}
);

/**
* Recognize intents in a user utterance using a configured LUIS model.
Expand Down
1 change: 1 addition & 0 deletions libraries/botbuilder-ai/src/luisRecognizerOptionsV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class LuisRecognizerV3 extends LuisRecognizerInternal {
if (
z
.object({ startIndex: z.number(), endIndex: z.number() })
.nonstrict()
.check(childInstance)
) {
const start = childInstance.startIndex;
Expand Down
5 changes: 4 additions & 1 deletion libraries/botbuilder-azure-blobs/src/blobsTranscriptStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ function getConversationPrefix(channelId: string, conversationId: string): strin

// Formats an activity as a blob key
function getBlobKey(activity: Activity): string {
const { timestamp } = z.object({ timestamp: z.instanceof(Date) }).parse(activity);
const { timestamp } = z
.object({ timestamp: z.instanceof(Date) })
.nonstrict()
.parse(activity);

return sanitizeBlobKey(
[activity.channelId, activity.conversation.id, `${formatTicks(timestamp)}-${activity.id}.json`].join('/')
Expand Down
78 changes: 78 additions & 0 deletions libraries/botbuilder-core/etc/botbuilder-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import { AnimationCard } from 'botframework-schema';
import { Assertion } from 'botbuilder-stdlib';
import { Attachment } from 'botframework-schema';
import { AudioCard } from 'botframework-schema';
import { AuthenticateRequestResult } from 'botframework-connector';
import { AuthenticationConfiguration } from 'botframework-connector';
import { BotFrameworkAuthentication } from 'botframework-connector';
import { BotFrameworkClient } from 'botframework-connector';
import { CardAction } from 'botframework-schema';
import { CardImage } from 'botframework-schema';
import { ChannelAccount } from 'botframework-schema';
import { ClaimsIdentity } from 'botframework-connector';
import { Configuration } from 'botbuilder-dialogs-adaptive-runtime-core';
import { ConnectorClientOptions } from 'botframework-connector';
import { ConnectorFactory } from 'botframework-connector';
import { ConversationReference } from 'botframework-schema';
import { HeroCard } from 'botframework-schema';
import { InputHints } from 'botframework-schema';
Expand All @@ -24,16 +30,20 @@ import { MediaUrl } from 'botframework-schema';
import { Mention } from 'botframework-schema';
import { MessageReaction } from 'botframework-schema';
import { O365ConnectorCard } from 'botframework-schema';
import { PasswordServiceClientCredentialFactory } from 'botframework-connector';
import { ReceiptCard } from 'botframework-schema';
import { ResourceResponse } from 'botframework-schema';
import { ServiceClientCredentialsFactory } from 'botframework-connector';
import { ServiceCollection } from 'botbuilder-dialogs-adaptive-runtime-core';
import { SignInUrlResponse } from 'botframework-schema';
import { StatusCodes } from 'botframework-schema';
import { ThumbnailCard } from 'botframework-schema';
import { TokenExchangeRequest } from 'botframework-schema';
import { TokenExchangeResource } from 'botframework-schema';
import { TokenResponse } from 'botframework-schema';
import { UserTokenClient } from 'botframework-connector';
import { VideoCard } from 'botframework-schema';
import * as z from 'zod';

// @public
export class ActivityFactory {
Expand Down Expand Up @@ -135,7 +145,12 @@ export class AutoSaveStateMiddleware implements Middleware {
export abstract class BotAdapter {
// (undocumented)
readonly BotIdentityKey: symbol;
// (undocumented)
readonly ConnectorClientKey: symbol;
abstract continueConversation(reference: Partial<ConversationReference>, logic: (revocableContext: TurnContext) => Promise<void>): Promise<void>;
continueConversationAsync(botAppId: string, reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promise<void>): Promise<void>;
continueConversationAsync(claimsIdentity: ClaimsIdentity, reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promise<void>): Promise<void>;
continueConversationAsync(claimsIdentity: ClaimsIdentity, reference: Partial<ConversationReference>, audience: string, logic: (context: TurnContext) => Promise<void>): Promise<void>;
abstract deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void>;
// (undocumented)
protected middleware: MiddlewareSet;
Expand Down Expand Up @@ -273,12 +288,69 @@ export class CardFactory {
static videoCard(title: string, media: (MediaUrl | string)[], buttons?: (CardAction | string)[], other?: Partial<VideoCard>): Attachment;
}

// @public (undocumented)
export abstract class CloudAdapterBase extends BotAdapter {
constructor(botFrameworkAuthentication: BotFrameworkAuthentication);
// (undocumented)
protected readonly botFrameworkAuthentication: BotFrameworkAuthentication;
// (undocumented)
readonly ConnectorFactoryKey: symbol;
// @deprecated (undocumented)
continueConversation(reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promise<void>): Promise<void>;
// @internal (undocumented)
continueConversationAsync(botAppIdOrClaimsIdentity: string | ClaimsIdentity, reference: Partial<ConversationReference>, logicOrAudience: ((context: TurnContext) => Promise<void>) | string, maybeLogic?: (context: TurnContext) => Promise<void>): Promise<void>;
protected createClaimsIdentity(botAppId?: string): ClaimsIdentity;
// (undocumented)
deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void>;
protected processActivity(authHeader: string, activity: Activity, logic: (context: TurnContext) => Promise<void>): Promise<InvokeResponse | undefined>;
protected processActivity(authenticateRequestResult: AuthenticateRequestResult, activity: Activity, logic: (context: TurnContext) => Promise<void>): Promise<InvokeResponse | undefined>;
protected processProactive(claimsIdentity: ClaimsIdentity, continuationActivity: Partial<Activity>, audience: string | undefined, logic: (context: TurnContext) => Promise<void>): Promise<void>;
// (undocumented)
sendActivities(context: TurnContext, activities: Partial<Activity>[]): Promise<ResourceResponse[]>;
// (undocumented)
updateActivity(context: TurnContext, activity: Partial<Activity>): Promise<ResourceResponse | void>;
// (undocumented)
readonly UserTokenClientKey: symbol;
}

// @public
export class ComponentRegistration {
static add(componentRegistration: ComponentRegistration): void;
static get components(): ComponentRegistration[];
}

// @public
export class ConfigurationBotFrameworkAuthentication extends BotFrameworkAuthentication {
constructor(botFrameworkAuthConfig?: ConfigurationBotFrameworkAuthenticationOptions, credentialsFactory?: ServiceClientCredentialsFactory, authConfiguration?: AuthenticationConfiguration, botFrameworkClientFetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>, connectorClientOptions?: ConnectorClientOptions);
// (undocumented)
authenticateChannelRequest(authHeader: string): Promise<ClaimsIdentity>;
// (undocumented)
authenticateRequest(activity: Activity, authHeader: string): Promise<AuthenticateRequestResult>;
// (undocumented)
authenticateStreamingRequest(authHeader: string, channelIdHeader: string): Promise<AuthenticateRequestResult>;
// (undocumented)
createBotFrameworkClient(): BotFrameworkClient;
// (undocumented)
createConnectorFactory(claimsIdentity: ClaimsIdentity): ConnectorFactory;
// (undocumented)
createUserTokenClient(claimsIdentity: ClaimsIdentity): Promise<UserTokenClient>;
}

// Warning: (ae-forgotten-export) The symbol "TypedOptions" needs to be exported by the entry point index.d.ts
//
// @public
export type ConfigurationBotFrameworkAuthenticationOptions = z.infer<typeof TypedOptions>;

// @public
export class ConfigurationServiceClientCredentialFactory extends PasswordServiceClientCredentialFactory {
constructor(factoryOptions?: ConfigurationServiceClientCredentialFactoryOptions);
}

// Warning: (ae-forgotten-export) The symbol "TypedConfig" needs to be exported by the entry point index.d.ts
//
// @public
export type ConfigurationServiceClientCredentialFactoryOptions = z.infer<typeof TypedConfig>;

// @public
export class ConsoleTranscriptLogger implements TranscriptLogger {
logActivity(activity: Activity): void | Promise<void>;
Expand All @@ -296,6 +368,12 @@ export interface CoreAppCredentials {
signRequest(webResource: CoreWebResource): Promise<CoreWebResource>;
}

// @public
export function createBotFrameworkAuthenticationFromConfiguration(configuration: Configuration, credentialsFactory?: ServiceClientCredentialsFactory, authConfiguration?: AuthenticationConfiguration, botFrameworkClientFetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>, connectorClientOptions?: ConnectorClientOptions): BotFrameworkAuthentication;

// @public
export function createServiceClientCredentialFactoryFromConfiguration(configuration: Configuration): ConfigurationServiceClientCredentialFactory;

// @public
export type DeleteActivityHandler = (context: TurnContext, reference: Partial<ConversationReference>, next: () => Promise<void>) => Promise<void>;

Expand Down
3 changes: 2 additions & 1 deletion libraries/botbuilder-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"botbuilder-stdlib": "4.1.6",
"botframework-connector": "4.1.6",
"botframework-schema": "4.1.6",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"zod": "~1.11.17"
},
"devDependencies": {
"chatdown": "^1.2.4",
Expand Down
14 changes: 7 additions & 7 deletions libraries/botbuilder-core/src/activityHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
* Licensed under the MIT License.
*/

import { ActivityHandlerBase } from './activityHandlerBase';
import { InvokeException } from './invokeException';
import { InvokeResponse } from './invokeResponse';
import { TurnContext } from './turnContext';
import { verifyStateOperationName, tokenExchangeOperationName, tokenResponseEventName } from './signInConstants';

import {
Activity,
AdaptiveCardInvokeResponse,
AdaptiveCardInvokeValue,
MessageReaction,
StatusCodes,
} from 'botframework-schema';

import { ActivityHandlerBase } from './activityHandlerBase';
import { InvokeException } from './invokeException';
import { InvokeResponse } from './invokeResponse';
import { StatusCodes } from './statusCodes';
import { TurnContext } from './turnContext';
import { verifyStateOperationName, tokenExchangeOperationName, tokenResponseEventName } from './signInConstants';

/**
* Describes a bot activity event handler, for use with an [ActivityHandler](xref:botbuilder-core.ActivityHandler) object.
*
Expand Down
2 changes: 1 addition & 1 deletion libraries/botbuilder-core/src/activityHandlerBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import { ActivityTypes, ChannelAccount, MessageReaction, TurnContext } from '.';
import { InvokeResponse } from './invokeResponse';
import { StatusCodes } from './statusCodes';
import { StatusCodes } from 'botframework-schema';

// This key is exported internally so that subclassed ActivityHandlers and BotAdapters will not override any already set InvokeResponses.
export const INVOKE_RESPONSE_KEY = Symbol('invokeResponse');
Expand Down
64 changes: 62 additions & 2 deletions libraries/botbuilder-core/src/botAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ClaimsIdentity } from 'botframework-connector';
import { Activity, ConversationReference, ResourceResponse } from 'botframework-schema';
import { makeRevocable } from './internal';
import { Middleware, MiddlewareHandler, MiddlewareSet } from './middlewareSet';
Expand All @@ -28,9 +29,12 @@ import { TurnContext } from './turnContext';
*/
export abstract class BotAdapter {
protected middleware: MiddlewareSet = new MiddlewareSet();

private turnError: (context: TurnContext, error: Error) => Promise<void>;
public readonly BotIdentityKey: symbol = Symbol('BotIdentity');
public readonly OAuthScopeKey: symbol = Symbol('OAuthScope');

public readonly BotIdentityKey = Symbol('BotIdentity');
public readonly ConnectorClientKey = Symbol('ConnectorClient');
public readonly OAuthScopeKey = Symbol('OAuthScope');

/**
* Asynchronously sends a set of outgoing activities to a channel server.
Expand Down Expand Up @@ -97,6 +101,62 @@ export abstract class BotAdapter {
logic: (revocableContext: TurnContext) => Promise<void>
): Promise<void>;

/**
* Asynchronously resumes a conversation with a user, possibly after some time has gone by.
*
* @param botAppId The application ID of the bot. This parameter is ignored in single tenant the Adapters (Console,Test, etc) but is critical to the BotFrameworkAdapter which is multi-tenant aware.
* @param reference A partial [ConversationReference](xref:botframework-schema.ConversationReference) to the conversation to continue.
* @param logic The asynchronous method to call after the adapter middleware runs.
* @returns a promise representing the async operation
*/
continueConversationAsync(
botAppId: string,
reference: Partial<ConversationReference>,
logic: (context: TurnContext) => Promise<void>
): Promise<void>;

/**
* Asynchronously resumes a conversation with a user, possibly after some time has gone by.
*
* @param claimsIdentity A [ClaimsIdentity](xref:botframework-connector) for the conversation.
* @param reference A partial [ConversationReference](xref:botframework-schema.ConversationReference) to the conversation to continue.
* @param logic The asynchronous method to call after the adapter middleware runs.
* @returns a promise representing the async operation
*/
continueConversationAsync(
claimsIdentity: ClaimsIdentity,
reference: Partial<ConversationReference>,
logic: (context: TurnContext) => Promise<void>
): Promise<void>;

/**
* Asynchronously resumes a conversation with a user, possibly after some time has gone by.
*
* @param claimsIdentity A [ClaimsIdentity](xref:botframework-connector) for the conversation.
* @param reference A partial [ConversationReference](xref:botframework-schema.ConversationReference) to the conversation to continue.
* @param audience A value signifying the recipient of the proactive message.</param>
* @param logic The asynchronous method to call after the adapter middleware runs.
* @returns a promise representing the async operation
*/
continueConversationAsync(
claimsIdentity: ClaimsIdentity,
reference: Partial<ConversationReference>,
audience: string,
logic: (context: TurnContext) => Promise<void>
): Promise<void>;

/**
* @internal
*/
async continueConversationAsync(
botAppIdOrClaimsIdentity: string | ClaimsIdentity,
reference: Partial<ConversationReference>,
logicOrAudience: ((context: TurnContext) => Promise<void>) | string,
maybeLogic?: (context: TurnContext) => Promise<void>
): Promise<void> {
throw new Error('NotImplemented');
}

/**
* Gets or sets an error handler that can catch exceptions in the middleware or application.
*
Expand Down
Loading

0 comments on commit 636daf8

Please sign in to comment.