• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-js / 8895810760

30 Apr 2024 01:38PM UTC coverage: 84.426% (-0.005%) from 84.431%
8895810760

Pull #4656

github

web-flow
Merge ac207de5d into de098675c
Pull Request #4656: handel the error by using try catch for processActivity returns a pro…

9997 of 13132 branches covered (76.13%)

Branch coverage included in aggregate %.

0 of 11 new or added lines in 1 file covered. (0.0%)

5 existing lines in 1 file now uncovered.

20426 of 22903 relevant lines covered (89.18%)

7456.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.65
/libraries/botbuilder-core/src/cloudAdapterBase.ts
1
// Copyright (c) Microsoft Corporation.
2
// Licensed under the MIT License.
3

4
import { BotAdapter } from './botAdapter';
2✔
5
import { BotCallbackHandlerKey, TurnContext } from './turnContext';
2✔
6
import { INVOKE_RESPONSE_KEY } from './activityHandlerBase';
2✔
7
import { delay } from 'botbuilder-stdlib';
2✔
8
import { v4 as uuid } from 'uuid';
2✔
9

10
import {
2✔
11
    AuthenticateRequestResult,
12
    AuthenticationConstants,
13
    BotFrameworkAuthentication,
14
    ClaimsIdentity,
15
    ConnectorClient,
16
    ConnectorFactory,
17
    UserTokenClient,
18
} from 'botframework-connector';
19

20
import {
2✔
21
    Activity,
22
    ActivityEventNames,
23
    ActivityEx,
24
    ActivityTypes,
25
    Channels,
26
    ConversationParameters,
27
    ConversationReference,
28
    DeliveryModes,
29
    InvokeResponse,
30
    ResourceResponse,
31
    StatusCodes,
32
} from 'botframework-schema';
33

34
/**
35
 * An adapter that implements the Bot Framework Protocol and can be hosted in different cloud environments both public and private.
36
 */
37
export abstract class CloudAdapterBase extends BotAdapter {
2✔
38
    readonly ConnectorFactoryKey = Symbol('ConnectorFactory');
24✔
39
    readonly UserTokenClientKey = Symbol('UserTokenClient');
24✔
40

41
    /**
42
     * Create a new [CloudAdapterBase](xref:botbuilder.CloudAdapterBase) instance.
43
     *
44
     * @param botFrameworkAuthentication A [BotFrameworkAuthentication](xref:botframework-connector.BotFrameworkAuthentication) used for validating and creating tokens.
45
     */
46
    constructor(protected readonly botFrameworkAuthentication: BotFrameworkAuthentication) {
24✔
47
        super();
24✔
48

49
        if (!botFrameworkAuthentication) {
24✔
50
            throw new TypeError('`botFrameworkAuthentication` parameter required');
4✔
51
        }
52
    }
53

54
    /**
55
     * @inheritdoc
56
     */
57
    async sendActivities(context: TurnContext, activities: Partial<Activity>[]): Promise<ResourceResponse[]> {
58
        if (!context) {
16✔
59
            throw new TypeError('`context` parameter required');
2✔
60
        }
61

62
        if (!activities) {
14✔
63
            throw new TypeError('`activities` parameter required');
2✔
64
        }
65

66
        if (!activities.length) {
12✔
67
            throw new Error('Expecting one or more activities, but the array was empty.');
2✔
68
        }
69

70
        const responses: ResourceResponse[] = [];
10✔
71
        for (const activity of activities) {
10✔
72
            delete activity.id;
10✔
73
            let response: ResourceResponse;
74

75
            if (activity.type === 'delay') {
10✔
76
                await delay(typeof activity.value === 'number' ? activity.value : 1000);
2!
77
            } else if (activity.type === ActivityTypes.InvokeResponse) {
8✔
78
                context.turnState.set(INVOKE_RESPONSE_KEY, activity);
2✔
79
            } else if (activity.type === ActivityTypes.Trace && activity.channelId !== Channels.Emulator) {
6!
80
                // no-op
81
            } else {
82
                const connectorClient = context.turnState.get<ConnectorClient>(this.ConnectorClientKey);
6✔
83
                if (!connectorClient) {
6✔
84
                    throw new Error('Unable to extract ConnectorClient from turn context.');
2✔
85
                }
86

87
                if (activity.replyToId) {
4✔
88
                    response = await connectorClient.conversations.replyToActivity(
2✔
89
                        activity.conversation.id,
90
                        activity.replyToId,
91
                        activity
92
                    );
93
                } else {
94
                    response = await connectorClient.conversations.sendToConversation(
2✔
95
                        activity.conversation.id,
96
                        activity
97
                    );
98
                }
99
            }
100

101
            if (!response) {
8✔
102
                response = { id: activity.id ?? '' };
4!
103
            }
104

105
            responses.push(response);
8✔
106
        }
107

108
        return responses;
8✔
109
    }
110

111
    /**
112
     * @inheritdoc
113
     */
114
    async updateActivity(context: TurnContext, activity: Partial<Activity>): Promise<ResourceResponse | void> {
115
        if (!context) {
6✔
116
            throw new TypeError('`context` parameter required');
2✔
117
        }
118

119
        if (!activity) {
4✔
120
            throw new TypeError('`activity` parameter required');
2✔
121
        }
122

123
        const connectorClient = context.turnState.get<ConnectorClient>(this.ConnectorClientKey);
2✔
124
        if (!connectorClient) {
2!
125
            throw new Error('Unable to extract ConnectorClient from turn context.');
×
126
        }
127

128
        const response = await connectorClient.conversations.updateActivity(
2✔
129
            activity.conversation.id,
130
            activity.id,
131
            activity
132
        );
133

134
        return response?.id ? { id: response.id } : undefined;
2!
135
    }
136

137
    /**
138
     * @inheritdoc
139
     */
140
    async deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void> {
141
        if (!context) {
6✔
142
            throw new TypeError('`context` parameter required');
2✔
143
        }
144

145
        if (!reference) {
4✔
146
            throw new TypeError('`reference` parameter required');
2✔
147
        }
148

149
        const connectorClient = context.turnState.get<ConnectorClient>(this.ConnectorClientKey);
2✔
150
        if (!connectorClient) {
2!
151
            throw new Error('Unable to extract ConnectorClient from turn context.');
×
152
        }
153

154
        await connectorClient.conversations.deleteActivity(reference.conversation.id, reference.activityId);
2✔
155
    }
156

157
    /**
158
     * @inheritdoc
159
     *
160
     * @deprecated
161
     */
162
    async continueConversation(
163
        _reference: Partial<ConversationReference>,
164
        _logic: (context: TurnContext) => Promise<void>
165
    ): Promise<void> {
166
        throw new Error(
2✔
167
            '`CloudAdapterBase.continueConversation` is deprecated, please use `CloudAdapterBase.continueConversationAsync`'
168
        );
169
    }
170

171
    /**
172
     * @internal
173
     */
174
    async continueConversationAsync(
175
        botAppIdOrClaimsIdentity: string | ClaimsIdentity,
176
        reference: Partial<ConversationReference>,
177
        logicOrAudience: ((context: TurnContext) => Promise<void>) | string,
178
        maybeLogic?: (context: TurnContext) => Promise<void>
179
    ): Promise<void> {
180
        const botAppId = typeof botAppIdOrClaimsIdentity === 'string' ? botAppIdOrClaimsIdentity : undefined;
6✔
181

182
        const claimsIdentity =
183
            typeof botAppIdOrClaimsIdentity !== 'string'
6✔
184
                ? botAppIdOrClaimsIdentity
6✔
185
                : this.createClaimsIdentity(botAppId);
186

187
        const audience = typeof logicOrAudience === 'string' ? logicOrAudience : undefined;
6!
188

189
        const logic = typeof logicOrAudience === 'function' ? logicOrAudience : maybeLogic;
6✔
190

191
        return this.processProactive(claimsIdentity, ActivityEx.getContinuationActivity(reference), audience, logic);
6✔
192
    }
193

194
    /**
195
     * @inheritdoc
196
     */
197
    async createConversationAsync(
198
        botAppId: string,
199
        channelId: string,
200
        serviceUrl: string,
201
        audience: string,
202
        conversationParameters: ConversationParameters,
203
        logic: (context: TurnContext) => Promise<void>
204
    ): Promise<void> {
205
        if (typeof serviceUrl !== 'string' || !serviceUrl) {
12✔
206
            throw new TypeError('`serviceUrl` must be a non-empty string');
4✔
207
        }
208

209
        if (!conversationParameters) throw new TypeError('`conversationParameters` must be defined');
8✔
210
        if (!logic) throw new TypeError('`logic` must be defined');
6✔
211

212
        // Create a ClaimsIdentity, to create the connector and for adding to the turn context.
213
        const claimsIdentity = this.createClaimsIdentity(botAppId);
4✔
214
        claimsIdentity.claims.push({ type: AuthenticationConstants.ServiceUrlClaim, value: serviceUrl });
4✔
215

216
        // Create the connector factory.
217
        const connectorFactory = this.botFrameworkAuthentication.createConnectorFactory(claimsIdentity);
4✔
218

219
        // Create the connector client to use for outbound requests.
220
        const connectorClient = await connectorFactory.create(serviceUrl, audience);
4✔
221

222
        // Make the actual create conversation call using the connector.
223
        const createConversationResult = await connectorClient.conversations.createConversation(conversationParameters);
4✔
224

225
        // Create the create activity to communicate the results to the application.
226
        const createActivity = this.createCreateActivity(
2✔
227
            createConversationResult.id,
228
            channelId,
229
            serviceUrl,
230
            conversationParameters
231
        );
232

233
        // Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.)
234
        const userTokenClient = await this.botFrameworkAuthentication.createUserTokenClient(claimsIdentity);
2✔
235

236
        // Create a turn context and run the pipeline.
237
        const context = this.createTurnContext(
2✔
238
            createActivity,
239
            claimsIdentity,
240
            undefined,
241
            connectorClient,
242
            userTokenClient,
243
            logic,
244
            connectorFactory
245
        );
246

247
        // Run the pipeline.
248
        await this.runMiddleware(context, logic);
2✔
249
    }
250

251
    private createCreateActivity(
252
        createdConversationId: string | undefined,
253
        channelId: string,
254
        serviceUrl: string,
255
        conversationParameters: ConversationParameters
256
    ): Partial<Activity> {
257
        // Create a conversation update activity to represent the result.
258
        const activity = ActivityEx.createEventActivity();
2✔
259

260
        activity.name = ActivityEventNames.CreateConversation;
2✔
261
        activity.channelId = channelId;
2✔
262
        activity.serviceUrl = serviceUrl;
2✔
263
        activity.id = createdConversationId ?? uuid();
2!
264
        activity.conversation = {
2✔
265
            conversationType: undefined,
266
            id: createdConversationId,
267
            isGroup: conversationParameters.isGroup,
268
            name: undefined,
269
            tenantId: conversationParameters.tenantId,
270
        };
271
        activity.channelData = conversationParameters.channelData;
2✔
272
        activity.recipient = conversationParameters.bot;
2✔
273

274
        return activity;
2✔
275
    }
276

277
    /**
278
     * The implementation for continue conversation.
279
     *
280
     * @param claimsIdentity The [ClaimsIdentity](xref:botframework-connector.ClaimsIdentity) for the conversation.
281
     * @param continuationActivity The continuation [Activity](xref:botframework-schema.Activity) used to create the [TurnContext](xref:botbuilder-core.TurnContext).
282
     * @param audience The audience for the call.
283
     * @param logic The function to call for the resulting bot turn.
284
     * @returns a Promise representing the async operation
285
     */
286
    protected async processProactive(
287
        claimsIdentity: ClaimsIdentity,
288
        continuationActivity: Partial<Activity>,
289
        audience: string | undefined,
290
        logic: (context: TurnContext) => Promise<void>
291
    ): Promise<void> {
292
        // Create the connector factory and  the inbound request, extracting parameters and then create a connector for outbound requests.
293
        const connectorFactory = this.botFrameworkAuthentication.createConnectorFactory(claimsIdentity);
6✔
294

295
        // Create the connector client to use for outbound requests.
296
        const connectorClient = await connectorFactory.create(continuationActivity.serviceUrl, audience);
6✔
297

298
        // Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.)
299
        const userTokenClient = await this.botFrameworkAuthentication.createUserTokenClient(claimsIdentity);
6✔
300

301
        // Create a turn context and run the pipeline.
302
        const context = this.createTurnContext(
6✔
303
            continuationActivity,
304
            claimsIdentity,
305
            audience,
306
            connectorClient,
307
            userTokenClient,
308
            logic,
309
            connectorFactory
310
        );
311

312
        // Run the pipeline.
313
        await this.runMiddleware(context, logic);
6✔
314
    }
315

316
    /**
317
     * The implementation for processing an Activity sent to this bot.
318
     *
319
     * @param authHeader The authorization header from the http request.
320
     * @param activity The [Activity](xref:botframework-schema.Activity) to process.
321
     * @param logic The function to call for the resulting bot turn.
322
     * @returns a Promise resolving to an invoke response, or undefined.
323
     */
324
    protected processActivity(
325
        authHeader: string,
326
        activity: Activity,
327
        logic: (context: TurnContext) => Promise<void>
328
    ): Promise<InvokeResponse | undefined>;
329

330
    /**
331
     * The implementation for processing an Activity sent to this bot.
332
     *
333
     * @param authenticateRequestResult The [AuthenticateRequestResult](xref:botframework-connector.AuthenticateRequestResult) for this turn.
334
     * @param activity The [Activity](xref:botframework-schema.Activity) to process.
335
     * @param logic The function to call for the resulting bot turn.
336
     * @returns a Promise resolving to an invoke response, or undefined.
337
     */
338
    protected processActivity(
339
        authenticateRequestResult: AuthenticateRequestResult,
340
        activity: Activity,
341
        logic: (context: TurnContext) => Promise<void>
342
    ): Promise<InvokeResponse | undefined>;
343

344
    /**
345
     * @internal
346
     */
347
    protected async processActivity(
348
        authHeaderOrAuthenticateRequestResult: string | AuthenticateRequestResult,
349
        activity: Activity,
350
        logic: (context: TurnContext) => Promise<void>
351
    ): Promise<InvokeResponse | undefined> {
NEW
352
        try {
×
353
            // Authenticate the inbound request, extracting parameters and create a ConnectorFactory for creating a Connector for outbound requests.
354
            const authenticateRequestResult =
NEW
355
                typeof authHeaderOrAuthenticateRequestResult === 'string'
×
356
                    ? await this.botFrameworkAuthentication.authenticateRequest(
×
357
                        activity,
358
                        authHeaderOrAuthenticateRequestResult
359
                    )
360
                    : authHeaderOrAuthenticateRequestResult;
361

362
            // Set the callerId on the activity.
NEW
363
            activity.callerId = authenticateRequestResult.callerId;
×
364

365
            // Create the connector client to use for outbound requests.
NEW
366
            const connectorClient = await authenticateRequestResult.connectorFactory?.create(
×
367
                activity.serviceUrl,
368
                authenticateRequestResult.audience
369
            );
370

NEW
371
            if (!connectorClient) {
×
NEW
372
                throw new Error('Unable to extract ConnectorClient from turn context.');
×
373
            }
374

375
            // Create a UserTokenClient instance for the application to use. (For example, it would be used in a sign-in prompt.)
NEW
376
            const userTokenClient = await this.botFrameworkAuthentication.createUserTokenClient(
×
377
                authenticateRequestResult.claimsIdentity
378
            );
379

380
            // Create a turn context and run the pipeline.
NEW
381
            const context = this.createTurnContext(
×
382
                activity,
383
                authenticateRequestResult.claimsIdentity,
384
                authenticateRequestResult.audience,
385
                connectorClient,
386
                userTokenClient,
387
                logic,
388
                authenticateRequestResult.connectorFactory
389
            );
390

391
            // Run the pipeline.
NEW
392
            await this.runMiddleware(context, logic);
×
393

394
            // If there are any results they will have been left on the TurnContext.
NEW
395
            return this.processTurnResults(context);
×
396
        }
397
        catch (err) {
NEW
UNCOV
398
            return Promise.reject(err);
×
399
        }
400
    }
401

402
    /**
403
     * This is a helper to create the ClaimsIdentity structure from an appId that will be added to the TurnContext.
404
     * It is intended for use in proactive and named-pipe scenarios.
405
     *
406
     * @param botAppId The bot's application id.
407
     * @returns a [ClaimsIdentity](xref:botframework-connector.ClaimsIdentity) with the audience and appId claims set to the botAppId.
408
     */
409
    protected createClaimsIdentity(botAppId = ''): ClaimsIdentity {
×
410
        return new ClaimsIdentity([
12✔
411
            {
412
                type: AuthenticationConstants.AudienceClaim,
413
                value: botAppId,
414
            },
415
            {
416
                type: AuthenticationConstants.AppIdClaim,
417
                value: botAppId,
418
            },
419
        ]);
420
    }
421

422
    private createTurnContext(
423
        activity: Partial<Activity>,
424
        claimsIdentity: ClaimsIdentity,
425
        oauthScope: string | undefined,
426
        connectorClient: ConnectorClient,
427
        userTokenClient: UserTokenClient,
428
        logic: (context: TurnContext) => Promise<void>,
429
        connectorFactory: ConnectorFactory
430
    ): TurnContext {
431
        const context = new TurnContext(this, activity);
8✔
432

433
        context.turnState.set(this.BotIdentityKey, claimsIdentity);
8✔
434
        context.turnState.set(this.ConnectorClientKey, connectorClient);
8✔
435
        context.turnState.set(this.UserTokenClientKey, userTokenClient);
8✔
436

437
        context.turnState.set(BotCallbackHandlerKey, logic);
8✔
438

439
        context.turnState.set(this.ConnectorFactoryKey, connectorFactory);
8✔
440
        context.turnState.set(this.OAuthScopeKey, oauthScope);
8✔
441

442
        return context;
8✔
443
    }
444

445
    private processTurnResults(context: TurnContext): InvokeResponse | undefined {
446
        // Handle ExpectedReplies scenarios where all activities have been buffered and sent back at once in an invoke response.
447
        if (context.activity.deliveryMode === DeliveryModes.ExpectReplies) {
×
UNCOV
448
            return {
×
449
                status: StatusCodes.OK,
450
                body: {
451
                    activities: context.bufferedReplyActivities,
452
                },
453
            };
454
        }
455

456
        // Handle Invoke scenarios where the bot will return a specific body and return code.
457
        if (context.activity.type === ActivityTypes.Invoke) {
×
458
            const activityInvokeResponse = context.turnState.get<Activity>(INVOKE_RESPONSE_KEY);
×
459
            if (!activityInvokeResponse) {
×
UNCOV
460
                return { status: StatusCodes.NOT_IMPLEMENTED };
×
461
            }
462

UNCOV
463
            return activityInvokeResponse.value;
×
464
        }
465

466
        // No body to return.
UNCOV
467
        return undefined;
×
468
    }
469
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc