-
Notifications
You must be signed in to change notification settings - Fork 276
/
botFrameworkAdapter.ts
811 lines (745 loc) · 39.5 KB
/
botFrameworkAdapter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity, ActivityTypes, BotAdapter, ChannelAccount, ConversationAccount, ConversationParameters, ConversationReference, ConversationsResult, IUserTokenProvider, ResourceResponse, TokenResponse, TurnContext } from 'botbuilder-core';
import { AuthenticationConstants, ChannelValidation, ConnectorClient, EmulatorApiClient, GovernmentConstants, GovernmentChannelValidation, JwtTokenValidation, MicrosoftAppCredentials, SimpleCredentialProvider, TokenApiClient, TokenStatus, TokenApiModels } from 'botframework-connector';
import * as os from 'os';
/**
* Express or Restify Request object.
*/
export interface WebRequest {
body?: any;
headers: any;
on(event: string, ...args: any[]): any;
}
/**
* Express or Restify Response object.
*/
export interface WebResponse {
end(...args: any[]): any;
send(body: any): any;
status(status: number): any;
}
/**
* Settings used to configure a `BotFrameworkAdapter` instance.
*/
export interface BotFrameworkAdapterSettings {
/**
* ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
*/
appId: string;
/**
* Password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
*/
appPassword: string;
/**
* (Optional) The OAuth API Endpoint for your bot to use.
*/
channelAuthTenant?: string;
/**
* (Optional) The OAuth API Endpoint for your bot to use.
*/
oAuthEndpoint?: string;
/**
* (Optional) The Open ID Metadata Endpoint for your bot to use.
*/
openIdMetadata?: string;
/**
* (Optional) The channel service option for this bot to validate connections from Azure or other channel locations
*/
channelService?: string;
}
/**
* Response object expected to be sent in response to an `invoke` activity.
*/
export interface InvokeResponse {
/**
* Status code to return for response.
*/
status: number;
/**
* (Optional) body to return for response.
*/
body?: any;
}
// Retrieve additional information, i.e., host operating system, host OS release, architecture, Node.js version
const ARCHITECTURE: any = os.arch();
const TYPE: any = os.type();
const RELEASE: any = os.release();
const NODE_VERSION: any = process.version;
// tslint:disable-next-line:no-var-requires no-require-imports
const pjson: any = require('../package.json');
const USER_AGENT: string = `Microsoft-BotFramework/3.1 BotBuilder/${ pjson.version } ` +
`(Node.js,Version=${ NODE_VERSION }; ${ TYPE } ${ RELEASE }; ${ ARCHITECTURE })`;
const OAUTH_ENDPOINT = 'https://api.botframework.com';
const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us';
const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse');
/**
* A BotAdapter class that connects your bot to Bot Framework channels and the Emulator.
*
* @remarks
* Use this adapter to connect your bot to the Bot Framework service, through which
* your bot can reach many chat channels like Skype, Slack, and Teams. This adapter
* also allows your bot to work with the Bot Framework Emulator, which simulates
* the Bot Framework service and provides a chat interface for testing and debugging.
*
* The following example shows the typical adapter setup:
*
* ```JavaScript
* const { BotFrameworkAdapter } = require('botbuilder');
*
* const adapter = new BotFrameworkAdapter({
* appId: process.env.MICROSOFT_APP_ID,
* appPassword: process.env.MICROSOFT_APP_PASSWORD
* });
* ```
*/
export class BotFrameworkAdapter extends BotAdapter implements IUserTokenProvider {
protected readonly credentials: MicrosoftAppCredentials;
protected readonly credentialsProvider: SimpleCredentialProvider;
protected readonly settings: BotFrameworkAdapterSettings;
private isEmulatingOAuthCards: boolean;
/**
* Creates a new BotFrameworkAdapter instance.
*
* @remarks
* Settings for this adapter include:
* ```javascript
* {
* "appId": "ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).",
* "appPassword": "Password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).",
* "openIdMetadata": "The Open ID Metadata Endpoint for your bot to use.",
* "oAuthEndpoint": "The OAuth API Endpoint for your bot to use.",
* "channelService": "(Optional) The channel service option for this bot to validate connections from Azure or other channel locations"
* }
* ```
* @param settings (optional) configuration settings for the adapter.
*/
constructor(settings?: Partial<BotFrameworkAdapterSettings>) {
super();
this.settings = { appId: '', appPassword: '', ...settings };
this.credentials = new MicrosoftAppCredentials(this.settings.appId, this.settings.appPassword || '', this.settings.channelAuthTenant);
this.credentialsProvider = new SimpleCredentialProvider(this.credentials.appId, this.credentials.appPassword);
this.isEmulatingOAuthCards = false;
// If no channelService or openIdMetadata values were passed in the settings, check the process' Environment Variables for values.
// These values may be set when a bot is provisioned on Azure and if so are required for the bot to properly work in Public Azure or a National Cloud.
this.settings.channelService = this.settings.channelService || process.env[AuthenticationConstants.ChannelService];
this.settings.openIdMetadata = this.settings.openIdMetadata || process.env[AuthenticationConstants.BotOpenIdMetadataKey];
if (this.settings.openIdMetadata) {
ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
}
if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
this.credentials.oAuthEndpoint = GovernmentConstants.ToChannelFromBotLoginUrl;
this.credentials.oAuthScope = GovernmentConstants.ToChannelFromBotOAuthScope;
}
// Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation)
// This will only occur on actities from teams that include tenant info in channelData but NOT in conversation,
// thus should be future friendly. However, once the the transition is complete. we can remove this.
this.use(async(context, next) => {
if (context.activity.channelId === 'msteams' && context.activity && context.activity.conversation && !context.activity.conversation.tenantId && context.activity.channelData && context.activity.channelData.tenant) {
context.activity.conversation.tenantId = context.activity.channelData.tenant.id;
}
await next();
});
}
/**
* Resume a conversation with a user, possibly after some time has gone by.
*
* @remarks
* This is often referred to as the bot's "Proactive Messaging" flow as it lets the bot proactively
* send messages to a conversation or user without having to reply directly to an incoming message.
* Scenarios like sending notifications or coupons to a user are enabled by this method.
*
* In order to use this method, a ConversationReference must first be extracted from an incoming
* activity. This reference can be stored in a database and used to resume the conversation at a later time.
* The reference can be created from any incoming activity using `TurnContext.getConversationReference(context.activity)`.
*
* The processing steps for this method are very similar to [processActivity()](#processactivity)
* in that a `TurnContext` will be created which is then routed through the adapters middleware
* before calling the passed in logic handler. The key difference is that since an activity
* wasn't actually received from outside, it has to be created by the bot. The created activity will have its address
* related fields populated but will have a `context.activity.type === undefined`.
*
* ```JavaScript
* server.post('/api/notifyUser', async (req, res) => {
* // Lookup previously saved conversation reference
* const reference = await findReference(req.body.refId);
*
* // Proactively notify the user
* if (reference) {
* await adapter.continueConversation(reference, async (context) => {
* await context.sendActivity(req.body.message);
* });
* res.send(200);
* } else {
* res.send(404);
* }
* });
* ```
* @param reference A `ConversationReference` saved during a previous incoming activity.
* @param logic A function handler that will be called to perform the bots logic after the the adapters middleware has been run.
*/
public async continueConversation(reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promise<void>): Promise<void> {
const request: Partial<Activity> = TurnContext.applyConversationReference(
{ type: 'event', name: 'continueConversation' },
reference,
true
);
const context: TurnContext = this.createContext(request);
await this.runMiddleware(context, logic as any);
}
/**
* Starts a new conversation with a user. This is typically used to Direct Message (DM) a member
* of a group.
*
* @remarks
* This function creates a new conversation between the bot and a single user, as specified by
* the ConversationReference passed in. In multi-user chat environments, this typically means
* starting a 1:1 direct message conversation with a single user. If called on a reference
* already representing a 1:1 conversation, the new conversation will continue to be 1:1.
*
* * In order to use this method, a ConversationReference must first be extracted from an incoming
* activity. This reference can be stored in a database and used to resume the conversation at a later time.
* The reference can be created from any incoming activity using `TurnContext.getConversationReference(context.activity)`.
*
* The processing steps for this method are very similar to [processActivity()](#processactivity)
* in that a `TurnContext` will be created which is then routed through the adapters middleware
* before calling the passed in logic handler. The key difference is that since an activity
* wasn't actually received from outside, it has to be created by the bot. The created activity will have its address
* related fields populated but will have a `context.activity.type === undefined`..
*
* ```JavaScript
* // Get group members conversation reference
* const reference = TurnContext.getConversationReference(context.activity);
*
* // Start a new conversation with the user
* await adapter.createConversation(reference, async (ctx) => {
* await ctx.sendActivity(`Hi (in private)`);
* });
* ```
* @param reference A `ConversationReference` of the user to start a new conversation with.
* @param logic A function handler that will be called to perform the bot's logic after the the adapters middleware has been run.
*/
public async createConversation(reference: Partial<ConversationReference>, logic?: (context: TurnContext) => Promise<void>): Promise<void> {
if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.createConversation(): missing serviceUrl.`); }
// Create conversation
const parameters: ConversationParameters = { bot: reference.bot, members: [reference.user], isGroup: false, activity: null, channelData: null };
const client: ConnectorClient = this.createConnectorClient(reference.serviceUrl);
// Mix in the tenant ID if specified. This is required for MS Teams.
if (reference.conversation && reference.conversation.tenantId) {
// Putting tenantId in channelData is a temporary solution while we wait for the Teams API to be updated
parameters.channelData = { tenant: { id: reference.conversation.tenantId } };
// Permanent solution is to put tenantId in parameters.tenantId
parameters.tenantId = reference.conversation.tenantId;
}
const response = await client.conversations.createConversation(parameters);
// Initialize request and copy over new conversation ID and updated serviceUrl.
const request: Partial<Activity> = TurnContext.applyConversationReference(
{ type: 'event', name: 'createConversation' },
reference,
true
);
const conversation: ConversationAccount = {
id: response.id,
isGroup: false,
conversationType: null,
tenantId: null,
name: null,
};
request.conversation = conversation;
if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; }
// Create context and run middleware
const context: TurnContext = this.createContext(request);
await this.runMiddleware(context, logic as any);
}
/**
* Deletes an activity that was previously sent to a channel.
*
* @remarks
* Calling `TurnContext.deleteActivity()` is the preferred way of deleting activities (rather than calling it directly from the adapter), as that
* will ensure that any interested middleware will be notified.
*
* > [!TIP]
* > Note: Not all chat channels support this method. Calling it on an unsupported channel may result in an error.
* @param context Context for the current turn of conversation with the user.
* @param reference Conversation reference information for the activity being deleted.
*/
public async deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void> {
if (!reference.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing serviceUrl`); }
if (!reference.conversation || !reference.conversation.id) {
throw new Error(`BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id`);
}
if (!reference.activityId) { throw new Error(`BotFrameworkAdapter.deleteActivity(): missing activityId`); }
const client: ConnectorClient = this.createConnectorClient(reference.serviceUrl);
await client.conversations.deleteActivity(reference.conversation.id, reference.activityId);
}
/**
* Deletes a member from the current conversation.
*
* @remarks
* Remove a member's identity information from the conversation.
*
* Note that this method does not apply to all channels.
* @param context Context for the current turn of conversation with the user.
* @param memberId ID of the member to delete from the conversation.
*/
public async deleteConversationMember(context: TurnContext, memberId: string): Promise<void> {
if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl`); }
if (!context.activity.conversation || !context.activity.conversation.id) {
throw new Error(`BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id`);
}
const serviceUrl: string = context.activity.serviceUrl;
const conversationId: string = context.activity.conversation.id;
const client: ConnectorClient = this.createConnectorClient(serviceUrl);
await client.conversations.deleteConversationMember(conversationId, memberId);
}
/**
* Lists the members of a given activity as specified in a TurnContext.
*
* @remarks
* Returns an array of ChannelAccount objects representing the users involved in a given activity.
*
* This is different from `getConversationMembers()` in that it will return only those users
* directly involved in the activity, not all members of the conversation.
* @param context Context for the current turn of conversation with the user.
* @param activityId (Optional) activity ID to enumerate. If not specified the current activities ID will be used.
*/
public async getActivityMembers(context: TurnContext, activityId?: string): Promise<ChannelAccount[]> {
if (!activityId) { activityId = context.activity.id; }
if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing serviceUrl`); }
if (!context.activity.conversation || !context.activity.conversation.id) {
throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id`);
}
if (!activityId) {
throw new Error(`BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id`);
}
const serviceUrl: string = context.activity.serviceUrl;
const conversationId: string = context.activity.conversation.id;
const client: ConnectorClient = this.createConnectorClient(serviceUrl);
return await client.conversations.getActivityMembers(conversationId, activityId);
}
/**
* Lists the members of the current conversation as specified in a TurnContext.
*
* @remarks
* Returns an array of ChannelAccount objects representing the users currently involved in the conversation
* in which an activity occured.
*
* This is different from `getActivityMembers()` in that it will return all
* members of the conversation, not just those directly involved in the activity.
* @param context Context for the current turn of conversation with the user.
*/
public async getConversationMembers(context: TurnContext): Promise<ChannelAccount[]> {
if (!context.activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing serviceUrl`); }
if (!context.activity.conversation || !context.activity.conversation.id) {
throw new Error(`BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id`);
}
const serviceUrl: string = context.activity.serviceUrl;
const conversationId: string = context.activity.conversation.id;
const client: ConnectorClient = this.createConnectorClient(serviceUrl);
return await client.conversations.getConversationMembers(conversationId);
}
/**
* Lists the Conversations in which this bot has participated for a given channel server.
*
* @remarks
* The channel server returns results in pages and each page will include a `continuationToken`
* that can be used to fetch the next page of results from the server.
* @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`.
* @param continuationToken (Optional) token used to fetch the next page of results from the channel server. This should be left as `undefined` to retrieve the first page of results.
*/
public async getConversations(contextOrServiceUrl: TurnContext | string, continuationToken?: string): Promise<ConversationsResult> {
const url: string = typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl;
const client: ConnectorClient = this.createConnectorClient(url);
return await client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined);
}
/**
* Retrieves the OAuth token for a user that is in a sign-in flow.
* @param context Context for the current turn of conversation with the user.
* @param connectionName Name of the auth connection to use.
* @param magicCode (Optional) Optional user entered code to validate.
*/
public async getUserToken(context: TurnContext, connectionName: string, magicCode?: string): Promise<TokenResponse> {
if (!context.activity.from || !context.activity.from.id) {
throw new Error(`BotFrameworkAdapter.getUserToken(): missing from or from.id`);
}
if (!connectionName) {
throw new Error('getUserToken() requires a connectionName but none was provided.');
}
this.checkEmulatingOAuthCards(context);
const userId: string = context.activity.from.id;
const url: string = this.oauthApiUrl(context);
const client: TokenApiClient = this.createTokenApiClient(url);
const result: TokenApiModels.UserTokenGetTokenResponse = await client.userToken.getToken(userId, connectionName, { code: magicCode, channelId: context.activity.channelId });
if (!result || !result.token || result._response.status == 404) {
return undefined;
} else {
return result as TokenResponse;
}
}
/**
* Signs the user out with the token server.
* @param context Context for the current turn of conversation with the user.
* @param connectionName Name of the auth connection to use.
* @param userId id of user to sign out.
* @returns A promise that represents the work queued to execute.
*/
public async signOutUser(context: TurnContext, connectionName?: string, userId?: string): Promise<void> {
if (!context.activity.from || !context.activity.from.id) {
throw new Error(`BotFrameworkAdapter.signOutUser(): missing from or from.id`);
}
if (!userId){
userId = context.activity.from.id;
}
this.checkEmulatingOAuthCards(context);
const url: string = this.oauthApiUrl(context);
const client: TokenApiClient = this.createTokenApiClient(url);
await client.userToken.signOut(userId, { connectionName: connectionName, channelId: context.activity.channelId } );
}
/**
* Gets a signin link from the token server that can be sent as part of a SigninCard.
* @param context Context for the current turn of conversation with the user.
* @param connectionName Name of the auth connection to use.
*/
public async getSignInLink(context: TurnContext, connectionName: string): Promise<string> {
this.checkEmulatingOAuthCards(context);
const conversation: Partial<ConversationReference> = TurnContext.getConversationReference(context.activity);
const url: string = this.oauthApiUrl(context);
const client: TokenApiClient = this.createTokenApiClient(url);
const state: any = {
ConnectionName: connectionName,
Conversation: conversation,
MsAppId: (client.credentials as MicrosoftAppCredentials).appId
};
const finalState: string = Buffer.from(JSON.stringify(state)).toString('base64');
return (await client.botSignIn.getSignInUrl(finalState, { channelId: context.activity.channelId }))._response.bodyAsText;
}
/**
* Retrieves the token status for each configured connection for the given user.
* @param context Context for the current turn of conversation with the user.
* @param userId The user Id for which token status is retrieved.
* @param includeFilter Optional comma seperated list of connection's to include. Blank will return token status for all configured connections.
* @returns Array of TokenStatus
* */
public async getTokenStatus(context: TurnContext, userId?: string, includeFilter?: string ): Promise<TokenStatus[]>
{
if (!userId && (!context.activity.from || !context.activity.from.id)) {
throw new Error(`BotFrameworkAdapter.getTokenStatus(): missing from or from.id`);
}
this.checkEmulatingOAuthCards(context);
userId = userId || context.activity.from.id;
const url: string = this.oauthApiUrl(context);
const client: TokenApiClient = this.createTokenApiClient(url);
return (await client.userToken.getTokenStatus(userId, {channelId: context.activity.channelId, include: includeFilter}))._response.parsedBody;
}
/**
* Signs the user out with the token server.
* @param context Context for the current turn of conversation with the user.
* @param connectionName Name of the auth connection to use.
*/
public async getAadTokens(context: TurnContext, connectionName: string, resourceUrls: string[]): Promise<{
[propertyName: string]: TokenResponse;
}> {
if (!context.activity.from || !context.activity.from.id) {
throw new Error(`BotFrameworkAdapter.getAadTokens(): missing from or from.id`);
}
this.checkEmulatingOAuthCards(context);
const userId: string = context.activity.from.id;
const url: string = this.oauthApiUrl(context);
const client: TokenApiClient = this.createTokenApiClient(url);
return (await client.userToken.getAadTokens(userId, connectionName, { resourceUrls: resourceUrls }, { channelId: context.activity.channelId }))._response.parsedBody as {[propertyName: string]: TokenResponse };
}
/**
* Tells the token service to emulate the sending of OAuthCards for a channel.
* @param contextOrServiceUrl The URL of the emulator.
* @param emulate If `true` the emulator will emulate the sending of OAuthCards.
*/
public async emulateOAuthCards(contextOrServiceUrl: TurnContext | string, emulate: boolean): Promise<void> {
this.isEmulatingOAuthCards = emulate;
const url: string = this.oauthApiUrl(contextOrServiceUrl);
await EmulatorApiClient.emulateOAuthCards(this.credentials as MicrosoftAppCredentials, url, emulate);
}
/**
* Processes an incoming request received by the bots web server into a TurnContext.
*
* @remarks
* This method is the main way a bot receives incoming messages.
*
* This method takes a raw incoming request object from a webserver and processes it into a
* normalized TurnContext that can be used by the bot. This includes any messages sent from a
* user and is the method that drives what is often referred to as the bot's "Reactive Messaging"
* flow.
*
* The following steps will be taken to process the activity:
*
* - The identity of the sender will be verified to be either the Emulator or a valid Microsoft
* server. The bots `appId` and `appPassword` will be used during this process and the request
* will be rejected if the senders identity can't be verified.
* - The activity will be parsed from the body of the incoming request. An error will be returned
* if the activity can't be parsed.
* - A `TurnContext` instance will be created for the received activity and wrapped with a
* [Revocable Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable).
* - The context will be routed through any middleware registered with the adapter using
* [use()](#use). Middleware is executed in the order in which it's added and any middleware
* can intercept or prevent further routing of the context by simply not calling the passed
* in `next()` function. This is called the "Leading Edge" of the request; middleware will
* get a second chance to run on the "Trailing Edge" of the request after the bots logic has run.
* - Assuming the context hasn't been intercepted by a piece of middleware, the context will be
* passed to the logic handler passed in. The bot may perform additional routing or
* processing at this time. Returning a promise (or providing an `async` handler) will cause the
* adapter to wait for any asynchronous operations to complete.
* - Once the bot's logic completes, the promise chain set up by the middleware stack will be resolved,
* giving middleware a second chance to run on the "Trailing Edge" of the request.
* - After the middleware stack's promise chain has been fully resolved the context object will be
* `revoked()` and any future calls to the context will result in a `TypeError: Cannot perform
* 'set' on a proxy that has been revoked` being thrown.
*
* > [!TIP]
* > Note: If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked`
* > appearing in your bot's console output, the likely cause is that an async function was used
* > without using the `await` keyword. Make sure all async functions use await!
*
* ```JavaScript
* server.post('/api/messages', (req, res) => {
* // Route received request to adapter for processing
* adapter.processActivity(req, res, async (context) => {
* // Process any messages received
* if (context.activity.type === ActivityTypes.Message) {
* await context.sendActivity(`Hello World`);
* }
* });
* });
* ```
* @param req An Express or Restify style Request object.
* @param res An Express or Restify style Response object.
* @param logic A function handler that will be called to perform the bots logic after the received activity has been pre-processed by the adapter and routed through any middleware for processing.
*/
public async processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise<any>): Promise<void> {
let body: any;
let status: number;
try {
// Parse body of request
status = 400;
const request = await parseRequest(req);
// Authenticate the incoming request
status = 401;
const authHeader: string = req.headers.authorization || req.headers.Authorization || '';
await this.authenticateRequest(request, authHeader);
// Process received activity
status = 500;
const context: TurnContext = this.createContext(request);
await this.runMiddleware(context, logic);
// Retrieve cached invoke response.
if (request.type === ActivityTypes.Invoke) {
const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY);
if (invokeResponse && invokeResponse.value) {
const value: InvokeResponse = invokeResponse.value;
status = value.status;
body = value.body;
} else {
status = 501;
}
} else {
status = 200;
}
} catch (err) {
body = err.toString();
}
// Return status
res.status(status);
if (body) { res.send(body); }
res.end();
// Check for an error
if (status >= 400) {
console.warn(`BotFrameworkAdapter.processActivity(): ${ status } ERROR - ${ body.toString() }`);
throw new Error(body.toString());
}
}
/**
* Sends a set of outgoing activities to the appropriate channel server.
*
* @remarks
* The activities will be sent one after another in the order in which they're received. A response object will be returned for each
* sent activity. For `message` activities this will contain the id of the delivered message.
*
* Instead of calling these methods directly on the adapter, calling `TurnContext.sendActivities()` or `TurnContext.sendActivity()`
* is the preferred way of sending activities as that will ensure that outgoing activities have been properly addressed
* and that any interested middleware has been notified.
*
* The primary scenario for calling this method directly is when you want to explicitly bypass
* going through any middleware. For instance, sending the `typing` activity might
* be a good reason to call this method directly as those activities are unlikely to require
* handling by middleware.
* @param context Context for the current turn of conversation with the user.
* @param activities List of activities to send.
*/
public async sendActivities(context: TurnContext, activities: Partial<Activity>[]): Promise<ResourceResponse[]> {
const responses: ResourceResponse[] = [];
for (let i = 0; i < activities.length; i++) {
const activity: Partial<Activity> = activities[i];
switch (activity.type) {
case 'delay':
await delay(typeof activity.value === 'number' ? activity.value : 1000);
responses.push({} as ResourceResponse);
break;
case 'invokeResponse':
// Cache response to context object. This will be retrieved when turn completes.
context.turnState.set(INVOKE_RESPONSE_KEY, activity);
responses.push({} as ResourceResponse);
break;
default:
if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.sendActivity(): missing serviceUrl.`); }
if (!activity.conversation || !activity.conversation.id) {
throw new Error(`BotFrameworkAdapter.sendActivity(): missing conversation id.`);
}
const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl);
if (activity.type === 'trace' && activity.channelId !== 'emulator') {
// Just eat activity
responses.push({} as ResourceResponse);
} else if (activity.replyToId) {
responses.push(await client.conversations.replyToActivity(
activity.conversation.id,
activity.replyToId,
activity as Activity
));
} else {
responses.push(await client.conversations.sendToConversation(
activity.conversation.id,
activity as Activity
));
}
break;
}
}
return responses;
}
/**
* Replaces an activity that was previously sent to a channel with an updated version.
*
* @remarks
* Calling `TurnContext.updateActivity()` is the preferred way of updating activities (rather than calling it directly from the adapter) as that
* will ensure that any interested middleware has been notified.
*
* It should be noted that not all channels support this feature.
* @param context Context for the current turn of conversation with the user.
* @param activity New activity to replace a current activity with.
*/
public async updateActivity(context: TurnContext, activity: Partial<Activity>): Promise<void> {
if (!activity.serviceUrl) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing serviceUrl`); }
if (!activity.conversation || !activity.conversation.id) {
throw new Error(`BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id`);
}
if (!activity.id) { throw new Error(`BotFrameworkAdapter.updateActivity(): missing activity.id`); }
const client: ConnectorClient = this.createConnectorClient(activity.serviceUrl);
await client.conversations.updateActivity(
activity.conversation.id,
activity.id,
activity as Activity
);
}
/**
* Allows for the overriding of authentication in unit tests.
* @param request Received request.
* @param authHeader Received authentication header.
*/
protected async authenticateRequest(request: Partial<Activity>, authHeader: string): Promise<void> {
const claims = await JwtTokenValidation.authenticateRequest(
request as Activity, authHeader,
this.credentialsProvider,
this.settings.channelService
);
if (!claims.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); }
}
/**
* Allows for mocking of the connector client in unit tests.
* @param serviceUrl Clients service url.
*/
protected createConnectorClient(serviceUrl: string): ConnectorClient {
const client: ConnectorClient = new ConnectorClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} );
return client;
}
/**
* Allows for mocking of the OAuth API Client in unit tests.
* @param serviceUrl Clients service url.
*/
protected createTokenApiClient(serviceUrl: string): TokenApiClient {
const client = new TokenApiClient(this.credentials, { baseUri: serviceUrl, userAgent: USER_AGENT} );
return client;
}
/**
* Allows for mocking of the OAuth Api URL in unit tests.
* @param contextOrServiceUrl The URL of the channel server to query or a TurnContext. This can be retrieved from `context.activity.serviceUrl`.
*/
protected oauthApiUrl(contextOrServiceUrl: TurnContext | string): string {
return this.isEmulatingOAuthCards ?
(typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl) :
(this.settings.oAuthEndpoint ? this.settings.oAuthEndpoint :
JwtTokenValidation.isGovernment(this.settings.channelService) ?
US_GOV_OAUTH_ENDPOINT : OAUTH_ENDPOINT);
}
/**
* Allows for mocking of toggling the emulating OAuthCards in unit tests.
* @param context The TurnContext
*/
protected checkEmulatingOAuthCards(context: TurnContext): void {
if (!this.isEmulatingOAuthCards &&
context.activity.channelId === 'emulator' &&
(!this.credentials.appId || !this.credentials.appPassword)) {
this.isEmulatingOAuthCards = true;
}
}
/**
* Allows for the overriding of the context object in unit tests and derived adapters.
* @param request Received request.
*/
protected createContext(request: Partial<Activity>): TurnContext {
return new TurnContext(this as any, request);
}
}
/**
* Handle incoming webhooks from the botframework
* @private
* @param req incoming web request
*/
function parseRequest(req: WebRequest): Promise<Activity> {
return new Promise((resolve: any, reject: any): void => {
function returnActivity(activity: Activity): void {
if (typeof activity !== 'object') { throw new Error(`BotFrameworkAdapter.parseRequest(): invalid request body.`); }
if (typeof activity.type !== 'string') { throw new Error(`BotFrameworkAdapter.parseRequest(): missing activity type.`); }
if (typeof activity.timestamp === 'string') { activity.timestamp = new Date(activity.timestamp); }
if (typeof activity.localTimestamp === 'string') { activity.localTimestamp = new Date(activity.localTimestamp); }
if (typeof activity.expiration === 'string') { activity.expiration = new Date(activity.expiration); }
resolve(activity);
}
if (req.body) {
try {
returnActivity(req.body);
} catch (err) {
reject(err);
}
} else {
let requestData = '';
req.on('data', (chunk: string) => {
requestData += chunk;
});
req.on('end', () => {
try {
req.body = JSON.parse(requestData);
returnActivity(req.body);
} catch (err) {
reject(err);
}
});
}
});
}
function delay(timeout: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}