Skip to content

Commit d287f5e

Browse files
timenickcompulim
andauthored
Support user id in start conversation (#316)
* validate and pass userid to directline channel api * Add test case and required dependency * fixed comments and added tests * Update directLine.ts Co-authored-by: William Wong <compulim@users.noreply.github.com>
1 parent c6ecb71 commit d287f5e

6 files changed

Lines changed: 191 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'dotenv/config';
2+
3+
import onErrorResumeNext from 'on-error-resume-next';
4+
5+
import { timeouts } from './constants.json';
6+
import * as createDirectLine from './setup/createDirectLine';
7+
import postActivity from './setup/postActivity';
8+
import waitForBotToRespond from './setup/waitForBotToRespond';
9+
10+
describe('Happy path', () => {
11+
let unsubscribes;
12+
13+
beforeEach(() => unsubscribes = []);
14+
afterEach(() => unsubscribes.forEach(fn => onErrorResumeNext(fn)));
15+
16+
describe('should receive the welcome message from bot', () => {
17+
let directLine;
18+
19+
describe('using REST', () => {
20+
beforeEach(() => jest.setTimeout(timeouts.rest));
21+
22+
test('with secret', async () => {
23+
directLine = await createDirectLine.forREST({ token: false });
24+
});
25+
});
26+
27+
describe('using Web Socket', () => {
28+
beforeEach(() => jest.setTimeout(timeouts.webSocket));
29+
30+
test('with secret', async () => {
31+
directLine = await createDirectLine.forWebSocket({ token: false });
32+
});
33+
});
34+
35+
afterEach(async () => {
36+
// If directLine object is undefined, that means the test is failing.
37+
if (!directLine) { return; }
38+
39+
unsubscribes.push(directLine.end.bind(directLine));
40+
41+
directLine.setUserId('u_test');
42+
43+
await Promise.all([
44+
postActivity(directLine, { text: 'Hello, World!', type: 'message' }),
45+
waitForBotToRespond(directLine, ({ from }) => from.id === 'u_test')
46+
]);
47+
});
48+
});
49+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'dotenv/config';
2+
3+
import onErrorResumeNext from 'on-error-resume-next';
4+
5+
import { timeouts } from './constants.json';
6+
import * as createDirectLine from './setup/createDirectLine';
7+
import waitForBotToRespond from './setup/waitForBotToRespond';
8+
9+
describe('Happy path', () => {
10+
let unsubscribes;
11+
12+
beforeEach(() => unsubscribes = []);
13+
afterEach(() => unsubscribes.forEach(fn => onErrorResumeNext(fn)));
14+
15+
describe('should receive the welcome message from bot', () => {
16+
let directLine;
17+
18+
describe('using REST', () => {
19+
beforeEach(() => jest.setTimeout(timeouts.rest));
20+
21+
test('with secret', async () => {
22+
directLine = await createDirectLine.forREST({ token: false });
23+
});
24+
});
25+
26+
describe('using Web Socket', () => {
27+
beforeEach(() => jest.setTimeout(timeouts.webSocket));
28+
29+
test('with secret', async () => {
30+
directLine = await createDirectLine.forWebSocket({ token: false });
31+
});
32+
});
33+
34+
afterEach(async () => {
35+
// If directLine object is undefined, that means the test is failing.
36+
if (!directLine) { return; }
37+
38+
unsubscribes.push(directLine.end.bind(directLine));
39+
40+
directLine.setUserId('u_test');
41+
42+
await waitForBotToRespond(directLine, ({ text }) => text === 'Welcome');
43+
});
44+
});
45+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'dotenv/config';
2+
3+
import onErrorResumeNext from 'on-error-resume-next';
4+
5+
import { timeouts } from './constants.json';
6+
import * as createDirectLine from './setup/createDirectLine';
7+
import waitForConnected from './setup/waitForConnected';
8+
9+
describe('Unhappy path', () => {
10+
let unsubscribes;
11+
12+
beforeEach(() => unsubscribes = []);
13+
afterEach(() => unsubscribes.forEach(fn => onErrorResumeNext(fn)));
14+
15+
describe('should receive the welcome message from bot', () => {
16+
let directLine;
17+
18+
describe('using REST', () => {
19+
beforeEach(() => jest.setTimeout(timeouts.rest));
20+
21+
test('with secret', async () => {
22+
directLine = await createDirectLine.forREST({ token: false });
23+
});
24+
});
25+
26+
describe('using Web Socket', () => {
27+
beforeEach(() => jest.setTimeout(timeouts.webSocket));
28+
29+
test('with secret', async () => {
30+
directLine = await createDirectLine.forWebSocket({ token: false });
31+
});
32+
});
33+
34+
afterEach(async () => {
35+
// If directLine object is undefined, that means the test is failing.
36+
if (!directLine) { return; }
37+
38+
unsubscribes.push(directLine.end.bind(directLine));
39+
unsubscribes.push(await waitForConnected(directLine));
40+
41+
expect(() => directLine.setUserId('e_test')).toThrowError('DirectLineJS: It is connected, we cannot set user id.');
42+
});
43+
});
44+
});

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"botframework-streaming": "4.10.3",
3131
"core-js": "3.6.4",
3232
"cross-fetch": "3.0.4",
33+
"jwt-decode": "3.1.2",
3334
"rxjs": "5.5.10",
3435
"url-search-params-polyfill": "8.0.0"
3536
},
@@ -42,6 +43,7 @@
4243
"@babel/preset-env": "^7.6.0",
4344
"@babel/preset-typescript": "^7.6.0",
4445
"@types/jest": "^24.0.18",
46+
"@types/jsonwebtoken": "^8.5.0",
4547
"@types/node": "^12.7.4",
4648
"@types/p-defer": "^2.0.0",
4749
"babel-jest": "^24.9.0",

src/directLine.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IScheduler } from 'rxjs/Scheduler';
99
import { Subscriber } from 'rxjs/Subscriber';
1010
import { Subscription } from 'rxjs/Subscription';
1111
import { async as AsyncScheduler } from 'rxjs/scheduler/async';
12+
import jwtDecode, { JwtPayload, InvalidTokenError } from 'jwt-decode';
1213

1314
import 'rxjs/add/operator/catch';
1415
import 'rxjs/add/operator/combineLatest';
@@ -471,6 +472,7 @@ export class DirectLine implements IBotConnection {
471472
private retries: number;
472473

473474
private localeOnStartConversation: string;
475+
private userIdOnStartConversation: string;
474476

475477
private pollingInterval: number = 1000; //ms
476478

@@ -630,6 +632,9 @@ export class DirectLine implements IBotConnection {
630632
const body = this.conversationId
631633
? undefined
632634
: {
635+
user: {
636+
id: this.userIdOnStartConversation
637+
},
633638
locale: this.localeOnStartConversation
634639
};
635640
return this.services.ajax({
@@ -749,6 +754,11 @@ export class DirectLine implements IBotConnection {
749754
}
750755

751756
postActivity(activity: Activity) {
757+
// If user id is set, check if it match activity.from.id and always override it in activity
758+
if (this.userIdOnStartConversation && activity.from && activity.from.id !== this.userIdOnStartConversation) {
759+
console.warn('DirectLineJS: Activity.from.id does not match with user id, ignoring activity.from.id');
760+
activity.from.id = this.userIdOnStartConversation;
761+
}
752762
// Use postMessageWithAttachments for messages with attachments that are local files (e.g. an image to upload)
753763
// Technically we could use it for *all* activities, but postActivity is much lighter weight
754764
// So, since WebChat is partially a reference implementation of Direct Line, we implement both.
@@ -1032,5 +1042,32 @@ export class DirectLine implements IBotConnection {
10321042
return `${DIRECT_LINE_VERSION} (${clientAgent} ${process.env.npm_package_version})`;
10331043
}
10341044

1045+
setUserId(userId: string) {
1046+
if (this.connectionStatus$.getValue() === ConnectionStatus.Online) {
1047+
throw new Error('DirectLineJS: It is connected, we cannot set user id.');
1048+
}
1049+
1050+
const userIdFromToken = this.parseToken(this.token);
1051+
if (userIdFromToken) {
1052+
return console.warn('DirectLineJS: user id is already set in token, will ignore this user id.');
1053+
}
1054+
1055+
if (/^dl_/u.test(userId)) {
1056+
return console.warn('DirectLineJS: user id prefixed with "dl_" is reserved and must be embedded into the Direct Line token to prevent forgery.');
1057+
}
1058+
1059+
this.userIdOnStartConversation = userId;
1060+
}
1061+
1062+
private parseToken(token: string) {
1063+
try {
1064+
const { user } = jwtDecode<JwtPayload>(token) as { [key: string]: any; };
1065+
return user;
1066+
} catch (e) {
1067+
if (e instanceof InvalidTokenError) {
1068+
return undefined;
1069+
}
1070+
}
1071+
}
10351072

10361073
}

0 commit comments

Comments
 (0)