UNPKG

17.8 kBPlain TextView Raw
1/**
2 * @module botkit
3 */
4/**
5 * Copyright (c) Microsoft Corporation. All rights reserved.
6 * Licensed under the MIT License.
7 */
8import { Botkit, BotkitMessage } from './core';
9import { Activity, ConversationAccount, ConversationReference, ConversationParameters, TurnContext } from 'botbuilder';
10import { DialogTurnResult, Dialog } from 'botbuilder-dialogs';
11
12/**
13 * A base class for a `bot` instance, an object that contains the information and functionality for taking action in response to an incoming message.
14 * Note that adapters are likely to extend this class with additional platform-specific methods - refer to the adapter documentation for these extensions.
15 */
16export class BotWorker {
17 private _controller: Botkit;
18 private _config: any;
19
20 /**
21 * Create a new BotWorker instance. Do not call this directly - instead, use [controller.spawn()](#spawn).
22 * @param controller A pointer to the main Botkit controller
23 * @param config An object typically containing { dialogContext, reference, context, activity }
24 */
25 public constructor(controller: Botkit, config) {
26 this._controller = controller;
27 this._config = {
28 ...config
29 };
30 }
31
32 /**
33 * Get a reference to the main Botkit controller.
34 */
35 public get controller(): Botkit {
36 return this._controller;
37 }
38
39 /**
40 * Get a value from the BotWorker's configuration.
41 *
42 * ```javascript
43 * let original_context = bot.getConfig('context');
44 * await original_context.sendActivity('send directly using the adapter instead of Botkit');
45 * ```
46 *
47 * @param {string} key The name of a value stored in the configuration
48 * @returns {any} The value stored in the configuration (or null if absent)
49 */
50 public getConfig(key?: string): any {
51 if (key) {
52 return this._config[key];
53 } else {
54 return this._config;
55 }
56 }
57
58 /**
59 * Send a message using whatever context the `bot` was spawned in or set using [changeContext()](#changecontext) --
60 * or more likely, one of the platform-specific helpers like
61 * [startPrivateConversation()](../reference/slack.md#startprivateconversation) (Slack),
62 * [startConversationWithUser()](../reference/twilio-sms.md#startconversationwithuser) (Twilio SMS),
63 * and [startConversationWithUser()](../reference/facebook.md#startconversationwithuser) (Facebook Messenger).
64 * Be sure to check the platform documentation for others - most adapters include at least one.
65 *
66 * Simple use in event handler (acts the same as bot.reply)
67 * ```javascript
68 * controller.on('event', async(bot, message) => {
69 *
70 * await bot.say('I received an event!');
71 *
72 * });
73 * ```
74 *
75 * Use with a freshly spawned bot and bot.changeContext:
76 * ```javascript
77 * let bot = controller.spawn(OPTIONS);
78 * bot.changeContext(REFERENCE);
79 * bot.say('ALERT! I have some news.');
80 * ```
81 *
82 * Use with multi-field message object:
83 * ```javascript
84 * controller.on('event', async(bot, message) => {
85 * bot.say({
86 * text: 'I heard an event',
87 * attachments: [
88 * title: message.type,
89 * text: `The message was of type ${ message.type }`,
90 * // ...
91 * ]
92 * });
93 * });
94 * ```
95 *
96 * @param message A string containing the text of a reply, or more fully formed message object
97 * @returns Return value will contain the results of the send action, typically `{id: <id of message>}`
98 */
99 public async say(message: Partial<BotkitMessage> | string): Promise<any> {
100 return new Promise((resolve, reject) => {
101 const activity = this.ensureMessageFormat(message);
102
103 this._controller.middleware.send.run(this, activity, async (err, bot, activity) => {
104 if (err) {
105 return reject(err);
106 }
107 resolve(await this.getConfig('context').sendActivity(activity));
108 });
109 });
110 };
111
112 /**
113 * Reply to an incoming message.
114 * Message will be sent using the context of the source message, which may in some cases be different than the context used to spawn the bot.
115 *
116 * Note that like [bot.say()](#say), `reply()` can take a string or a message object.
117 *
118 * ```javascript
119 * controller.on('event', async(bot, message) => {
120 *
121 * await bot.reply(message, 'I received an event and am replying to it.');
122 *
123 * });
124 * ```
125 *
126 * @param src An incoming message, usually passed in to a handler function
127 * @param resp A string containing the text of a reply, or more fully formed message object
128 * @returns Return value will contain the results of the send action, typically `{id: <id of message>}`
129 */
130 public async reply(src: Partial<BotkitMessage>, resp: Partial<BotkitMessage> | string): Promise<any> {
131 let activity = this.ensureMessageFormat(resp);
132
133 // Get conversation reference from src
134 const reference = TurnContext.getConversationReference(src.incoming_message);
135
136 activity = TurnContext.applyConversationReference(activity, reference);
137
138 return this.say(activity);
139 }
140
141 /**
142 * Begin a pre-defined dialog by specifying its id. The dialog will be started in the same context (same user, same channel) in which the original incoming message was received.
143 * [See "Using Dialogs" in the core documentation.](../index.md#using-dialogs)
144 *
145 * ```javascript
146 * controller.hears('hello', 'message', async(bot, message) => {
147 * await bot.beginDialog(GREETINGS_DIALOG);
148 * });
149 * ```
150 * @param id id of dialog
151 * @param options object containing options to be passed into the dialog
152 */
153 public async beginDialog(id: string, options?: any): Promise<void> {
154 if (this._config.dialogContext) {
155 await this._config.dialogContext.beginDialog(id + ':botkit-wrapper', {
156 user: this.getConfig('context').activity.from.id,
157 channel: this.getConfig('context').activity.conversation.id,
158 ...options
159 });
160
161 // make sure we save the state change caused by the dialog.
162 // this may also get saved again at end of turn
163 await this._controller.saveState(this);
164 } else {
165 throw new Error('Call to beginDialog on a bot that did not receive a dialogContext during spawn');
166 }
167 }
168
169 /**
170 * Cancel any and all active dialogs for the current user/context.
171 */
172 public async cancelAllDialogs(): Promise<DialogTurnResult> {
173 if (this._config.dialogContext) {
174 return this._config.dialogContext.cancelAllDialogs();
175 }
176 }
177
178 /**
179 * Get a reference to the active dialog
180 * @returns a reference to the active dialog or undefined if no dialog is active
181 */
182 public getActiveDialog(): Dialog | undefined {
183 return this.getConfig('dialogContext').activeDialog;
184 }
185
186 /**
187 * Check if any dialog is active or not
188 * @returns true if there is an active dialog, otherwise false
189 */
190 public hasActiveDialog(): boolean {
191 return !!this.getActiveDialog();
192 }
193
194 /**
195 * Check to see if a given dialog is currently active in the stack
196 * @param id The id of a dialog to look for in the dialog stack
197 * @returns true if dialog with id is located anywhere in the dialog stack
198 */
199 public isDialogActive(id: string): boolean {
200 if (this.getConfig('dialogContext').stack.length) {
201 return (this.getConfig('dialogContext').stack.filter((d) => d.id === id).length > 0);
202 }
203 return false;
204 }
205
206 /**
207 * Replace any active dialogs with a new a pre-defined dialog by specifying its id. The dialog will be started in the same context (same user, same channel) in which the original incoming message was received.
208 * [See "Using Dialogs" in the core documentation.](../index.md#using-dialogs)
209 *
210 * ```javascript
211 * controller.hears('hello', 'message', async(bot, message) => {
212 * await bot.replaceDialog(GREETINGS_DIALOG);
213 * });
214 * ```
215 * @param id id of dialog
216 * @param options object containing options to be passed into the dialog
217 */
218 public async replaceDialog(id: string, options?: any): Promise<void> {
219 if (this._config.dialogContext) {
220 await this._config.dialogContext.replaceDialog(id + ':botkit-wrapper', {
221 user: this.getConfig('context').activity.from.id,
222 channel: this.getConfig('context').activity.conversation.id,
223 ...options
224 });
225
226 // make sure we save the state change caused by the dialog.
227 // this may also get saved again at end of turn
228 await this._controller.saveState(this);
229 } else {
230 throw new Error('Call to beginDialog on a bot that did not receive a dialogContext during spawn');
231 }
232 }
233
234 /**
235 * Alter the context in which a bot instance will send messages.
236 * Use this method to create or adjust a bot instance so that it can send messages to a predefined user/channel combination.
237 *
238 * ```javascript
239 * // get the reference field and store it.
240 * const saved_reference = message.reference;
241 *
242 * // later on...
243 * let bot = await controller.spawn();
244 * bot.changeContext(saved_reference);
245 * bot.say('Hello!');
246 * ```
247 *
248 * @param reference A [ConversationReference](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/conversationreference?view=botbuilder-ts-latest), most likely captured from an incoming message and stored for use in proactive messaging scenarios.
249 */
250 public async changeContext(reference: Partial<ConversationReference>): Promise<BotWorker> {
251 // change context of outbound activities to use this new address
252 this._config.reference = reference;
253
254 // Create an activity using this reference
255 const activity = TurnContext.applyConversationReference(
256 { type: 'message' },
257 reference,
258 true
259 );
260
261 // create a turn context
262 const turnContext = new TurnContext(this.getConfig('adapter'), activity as Activity);
263
264 // create a new dialogContext so beginDialog works.
265 const dialogContext = await this._controller.dialogSet.createContext(turnContext);
266
267 this._config.context = turnContext;
268 this._config.dialogContext = dialogContext;
269 this._config.activity = activity;
270
271 return this;
272 }
273
274 public async startConversationWithUser(reference: any): Promise<void> {
275 // this code is mostly copied from BotFrameworkAdapter.createConversation
276
277 if (!reference.serviceUrl) { throw new Error('bot.startConversationWithUser(): missing serviceUrl.'); }
278
279 // Create conversation
280 const parameters: ConversationParameters = { bot: reference.bot, members: [reference.user], isGroup: false, activity: null, channelData: null };
281 const client = this.getConfig('adapter').createConnectorClient(reference.serviceUrl);
282
283 // Mix in the tenant ID if specified. This is required for MS Teams.
284 if (reference.conversation && reference.conversation.tenantId) {
285 // Putting tenantId in channelData is a temporary solution while we wait for the Teams API to be updated
286 parameters.channelData = { tenant: { id: reference.conversation.tenantId } };
287
288 // Permanent solution is to put tenantId in parameters.tenantId
289 parameters.tenantId = reference.conversation.tenantId;
290 }
291
292 const response = await client.conversations.createConversation(parameters);
293
294 // Initialize request and copy over new conversation ID and updated serviceUrl.
295 const request: Partial<Activity> = TurnContext.applyConversationReference(
296 { type: 'event', name: 'createConversation' },
297 reference,
298 true
299 );
300
301 const conversation: ConversationAccount = {
302 // fallback to existing conversation id because Emulator will respond without a response.id AND needs to stay in same channel.
303 // This should be fixed by Emulator. https://github.com/microsoft/BotFramework-Emulator/issues/2097
304 id: response.id || reference.conversation.id,
305 isGroup: false,
306 conversationType: null,
307 tenantId: null,
308 name: null
309 };
310 request.conversation = conversation;
311
312 if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; }
313
314 // Create context and run middleware
315 const turnContext: TurnContext = this.getConfig('adapter').createContext(request);
316
317 // create a new dialogContext so beginDialog works.
318 const dialogContext = await this._controller.dialogSet.createContext(turnContext);
319
320 this._config.context = turnContext;
321 this._config.dialogContext = dialogContext;
322 this._config.activity = request;
323 }
324
325 /**
326 * Take a crudely-formed Botkit message with any sort of field (may just be a string, may be a partial message object)
327 * and map it into a beautiful BotFramework Activity.
328 * Any fields not found in the Activity definition will be moved to activity.channelData.
329 * @params message a string or partial outgoing message object
330 * @returns a properly formed Activity object
331 */
332 public ensureMessageFormat(message: Partial<BotkitMessage> | string): Partial<Activity> {
333 if (typeof (message) === 'string') {
334 return {
335 type: 'message',
336 text: message,
337 channelData: {}
338 };
339 } else {
340 // set up a base message activity
341 // https://docs.microsoft.com/en-us/javascript/api/botframework-schema/activity?view=botbuilder-ts-latest
342 const activity: Partial<Activity> = {
343 type: message.type || 'message',
344 text: message.text,
345
346 action: message.action,
347 attachmentLayout: message.attachmentLayout,
348 attachments: message.attachments,
349
350 channelData: {
351 ...message.channelData
352 },
353 channelId: message.channelId,
354 code: message.code,
355 conversation: message.conversation,
356
357 deliveryMode: message.deliveryMode,
358 entities: message.entities,
359 expiration: message.expiration,
360 from: message.from,
361 historyDisclosed: message.historyDisclosed,
362 id: message.id,
363 importance: message.importance,
364 inputHint: message.inputHint,
365 label: message.label,
366 listenFor: message.listenFor,
367 locale: message.locale,
368 localTimestamp: message.localTimestamp,
369 localTimezone: message.localTimezone,
370 membersAdded: message.membersAdded,
371 membersRemoved: message.membersRemoved,
372 name: message.name,
373 reactionsAdded: message.reactionsAdded,
374 reactionsRemoved: message.reactionsRemoved,
375 recipient: message.recipient,
376 relatesTo: message.relatesTo,
377 replyToId: message.replyToId,
378 semanticAction: message.semanticAction,
379 serviceUrl: message.serviceUrl,
380 speak: message.speak,
381 suggestedActions: message.suggestedActions,
382 summary: message.summary,
383 textFormat: message.textFormat,
384 textHighlights: message.textHighlights,
385 timestamp: message.timestamp,
386 topicName: message.topicName,
387 value: message.value,
388 valueType: message.valueType
389 };
390
391 // Now, copy any additional fields not in the activity into channelData
392 // This way, any fields added by the developer to the root object
393 // end up in the approved channelData location.
394 for (const key in message) {
395 if (key !== 'channelData' && !Object.prototype.hasOwnProperty.call(activity, key)) {
396 activity.channelData[key] = message[key];
397 }
398 }
399 return activity;
400 }
401 }
402
403 /**
404 * Set the http response status code for this turn
405 *
406 * ```javascript
407 * controller.on('event', async(bot, message) => {
408 * // respond with a 500 error code for some reason!
409 * bot.httpStatus(500);
410 * });
411 * ```
412 *
413 * @param status {number} a valid http status code like 200 202 301 500 etc
414 */
415 public httpStatus(status: number): void {
416 this.getConfig('context').turnState.set('httpStatus', status);
417 }
418
419 /**
420 * Set the http response body for this turn.
421 * Use this to define the response value when the platform requires a synchronous response to the incoming webhook.
422 *
423 * Example handling of a /slash command from Slack:
424 * ```javascript
425 * controller.on('slash_command', async(bot, message) => {
426 * bot.httpBody('This is a reply to the slash command.');
427 * })
428 * ```
429 *
430 * @param body (any) a value that will be returned as the http response body
431 */
432 public httpBody(body: any): void {
433 this.getConfig('context').turnState.set('httpBody', body);
434 }
435}
436
\No newline at end of file