Skip to content

Commit 2192fb8

Browse files
authored
Add EventFactory and HandoffEventNames to botbuilder (#2010)
* first pass at EventFactory * Update EventFactory to latest in dotnet master * update EventFactory tests
1 parent d94630d commit 2192fb8

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed

libraries/botbuilder/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"botframework-connector": "4.1.6",
2828
"botframework-streaming": "4.1.6",
2929
"filenamify": "^4.1.0",
30-
"fs-extra": "^7.0.1"
30+
"fs-extra": "^7.0.1",
31+
"moment-timezone": "^0.5.28"
3132
},
3233
"devDependencies": {
3334
"@types/mocha": "^2.2.47",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { TurnContext } from 'botbuilder-core';
7+
import { Activity, Attachment, ConversationAccount, ConversationReference, Transcript } from 'botframework-schema';
8+
import * as moment from 'moment-timezone';
9+
import { HandoffEventNames } from './handoffEventNames';
10+
11+
/**
12+
* Contains utility methods for creating various event types.
13+
*/
14+
export class EventFactory {
15+
/**
16+
* Create handoff initiation event.
17+
* @param context The context object for the turn.
18+
* @param handoffContext Agent hub-specific context.
19+
* @param transcript Transcript of the conversation.
20+
*/
21+
public static createHandoffInitiation(context: TurnContext, handoffContext: any, transcript?: Transcript): Activity {
22+
if (!context) {
23+
throw new TypeError('EventFactory.createHandoffInitiation(): Missing context.');
24+
}
25+
26+
const handoffEvent = this.createHandoffEvent(HandoffEventNames.InitiateHandoff, handoffContext, context.activity.conversation);
27+
28+
handoffEvent.from = context.activity.from;
29+
handoffEvent.relatesTo = TurnContext.getConversationReference(context.activity) as ConversationReference;
30+
handoffEvent.replyToId = context.activity.id;
31+
handoffEvent.serviceUrl = context.activity.serviceUrl;
32+
handoffEvent.channelId = context.activity.channelId;
33+
34+
if (transcript) {
35+
const attachment: Attachment = {
36+
content: transcript,
37+
contentType: 'application/json',
38+
name: 'Transcript'
39+
};
40+
handoffEvent.attachments.push(attachment);
41+
}
42+
43+
return handoffEvent;
44+
}
45+
46+
/**
47+
* Create handoff status event.
48+
* @param conversation Conversation being handed over.
49+
* @param state State, possible values are: "accepted", "failed", "completed".
50+
* @param message Additional message for failed handoff.
51+
*/
52+
public static createHandoffStatus(conversation: ConversationAccount, state: string, message?: string): Activity {
53+
if (!conversation) {
54+
throw new TypeError('EventFactory.createHandoffStatus(): missing conversation.');
55+
}
56+
57+
if (!state) {
58+
throw new TypeError('EventFactory.createHandoffStatus(): missing state.');
59+
}
60+
61+
const value: any = { state, message };
62+
63+
const handoffEvent = this.createHandoffEvent(HandoffEventNames.HandoffStatus, value, conversation);
64+
65+
return handoffEvent;
66+
}
67+
68+
private static createHandoffEvent(name: string, value: any, conversation: ConversationAccount): Activity {
69+
const handoffEvent: Activity = {} as any;
70+
71+
handoffEvent.name = name;
72+
handoffEvent.value = value;
73+
handoffEvent.id = uuid();
74+
handoffEvent.timestamp = new Date(Date.now());
75+
// The timestamp does not contain the local offset which is a known limitation of Date objects in JavaScript.
76+
// Therefore, the localTimezone is included in the handoffEvent.
77+
handoffEvent.localTimezone = moment.tz.guess();
78+
handoffEvent.conversation = conversation;
79+
handoffEvent.attachments = [];
80+
handoffEvent.entities = [];
81+
82+
return handoffEvent;
83+
}
84+
}
85+
86+
function uuid(): string {
87+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c): string => {
88+
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
89+
return v.toString(16);
90+
});
91+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
export class HandoffEventNames {
7+
8+
public static readonly InitiateHandoff: string = 'handoff.initiate';
9+
10+
public static readonly HandoffStatus: string = 'handoff.status';
11+
}

libraries/botbuilder/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export { BotFrameworkHttpClient } from './botFrameworkHttpClient';
1414
export { ChannelServiceHandler } from './channelServiceHandler';
1515
export { ChannelServiceRoutes, RouteHandler, WebServer } from './channelServiceRoutes';
1616
export * from './fileTranscriptStore';
17+
export { HandoffEventNames } from './handoffEventNames';
18+
export { EventFactory } from './eventFactory';
1719
export * from './inspectionMiddleware';
1820
export {
1921
WebRequest,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const { equal, ok: assert, strictEqual } = require('assert');
2+
const moment = require('moment-timezone');
3+
4+
const {
5+
ActivityTypes,
6+
EventFactory,
7+
HandoffEventNames,
8+
MessageFactory,
9+
TestAdapter,
10+
TurnContext
11+
} = require('../');
12+
13+
describe('EventFactory', function() {
14+
this.timeout(5000);
15+
16+
describe('createHandoffInitiation', () => {
17+
it('should succeed', () => {
18+
const adapter = new TestAdapter(async context => { /* no op */ });
19+
const fromId = 'test';
20+
const activity = {
21+
type: ActivityTypes.Message,
22+
text: '',
23+
conversation: {},
24+
recipient: {},
25+
from: { id: fromId },
26+
channelId: 'testchannel',
27+
serviceUrl: 'http://myservice'
28+
};
29+
30+
const context = new TurnContext(adapter, activity);
31+
const transcript = { activities: [ MessageFactory.text('hello') ] };
32+
33+
equal(transcript.activities[0].channelId, undefined);
34+
equal(transcript.activities[0].serviceUrl, undefined);
35+
equal(transcript.activities[0].conversation, undefined);
36+
37+
const skillValue = 'any';
38+
const handoffEvent = EventFactory.createHandoffInitiation(context, { skill: skillValue }, transcript);
39+
40+
strictEqual(handoffEvent.name, HandoffEventNames.InitiateHandoff);
41+
const skill = handoffEvent.value && handoffEvent.value.skill;
42+
strictEqual(skill, skillValue);
43+
strictEqual(handoffEvent.from.id, fromId);
44+
});
45+
46+
it('should throw if turnContext is falsy', () => {
47+
try {
48+
EventFactory.createHandoffInitiation(null, 'some text');
49+
} catch (e) {
50+
strictEqual(e.message, 'EventFactory.createHandoffInitiation(): Missing context.');
51+
}
52+
});
53+
});
54+
55+
describe('createHandoffStatus', () => {
56+
it('should succeed', () => {
57+
const state = 'failed';
58+
const message = 'timed out';
59+
const handoffEvent = EventFactory.createHandoffStatus({}, state, message);
60+
61+
strictEqual(handoffEvent.name, HandoffEventNames.HandoffStatus);
62+
63+
const stateFormEvent = handoffEvent.value && handoffEvent.value.state;
64+
strictEqual(stateFormEvent, state);
65+
66+
const messageFormEvent = handoffEvent.value && handoffEvent.value.message;
67+
strictEqual(messageFormEvent, message);
68+
69+
const status = JSON.stringify(handoffEvent.value);
70+
strictEqual(status, `{\"state\":\"${ state }\",\"message\":\"${ message }\"}`);
71+
assert(handoffEvent.attachments, 'handoffEvent.attachments should not be undefined.');
72+
assert(handoffEvent.id, 'handoffEvent.id should not be undefined.');
73+
strictEqual(handoffEvent.localTimezone, moment.tz.guess());
74+
});
75+
76+
it('should succeed with no message', () => {
77+
const state = 'failed';
78+
const handoffEvent = EventFactory.createHandoffStatus({}, state);
79+
80+
const stateFormEvent = handoffEvent.value && handoffEvent.value.state;
81+
strictEqual(state, stateFormEvent);
82+
83+
const messageFormEvent = handoffEvent.value && handoffEvent.value.message;
84+
strictEqual(undefined, messageFormEvent);
85+
});
86+
87+
it('should throw if conversation is falsy', () => {
88+
try {
89+
EventFactory.createHandoffStatus(null, 'some text');
90+
} catch (e) {
91+
strictEqual(e.message, 'EventFactory.createHandoffStatus(): missing conversation.');
92+
}
93+
});
94+
95+
it('should throw if state is falsy', () => {
96+
try {
97+
EventFactory.createHandoffStatus({}, null);
98+
} catch (e) {
99+
strictEqual(e.message, 'EventFactory.createHandoffStatus(): missing state.');
100+
}
101+
});
102+
});
103+
});

0 commit comments

Comments
 (0)