Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user id in start conversation #316

Merged
merged 5 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions __tests__/happy.replaceActivityFromId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'dotenv/config';

import onErrorResumeNext from 'on-error-resume-next';

import { timeouts } from './constants.json';
import * as createDirectLine from './setup/createDirectLine';
import postActivity from './setup/postActivity';
import waitForBotToRespond from './setup/waitForBotToRespond';

describe('Happy path', () => {
let unsubscribes;

beforeEach(() => unsubscribes = []);
afterEach(() => unsubscribes.forEach(fn => onErrorResumeNext(fn)));

describe('should receive the welcome message from bot', () => {
let directLine;

describe('using REST', () => {
beforeEach(() => jest.setTimeout(timeouts.rest));

test('with secret', async () => {
directLine = await createDirectLine.forREST({ token: false });
});
});

describe('using Web Socket', () => {
beforeEach(() => jest.setTimeout(timeouts.webSocket));

test('with secret', async () => {
directLine = await createDirectLine.forWebSocket({ token: false });
});
});

afterEach(async () => {
// If directLine object is undefined, that means the test is failing.
if (!directLine) { return; }

unsubscribes.push(directLine.end.bind(directLine));

directLine.setUserId('u_test');

await Promise.all([
postActivity(directLine, { text: 'Hello, World!', type: 'message' }),
waitForBotToRespond(directLine, ({ from }) => from.id === 'u_test')
]);
});
});
});
45 changes: 45 additions & 0 deletions __tests__/happy.userIdOnStartConversation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dotenv/config';

import onErrorResumeNext from 'on-error-resume-next';

import { timeouts } from './constants.json';
import * as createDirectLine from './setup/createDirectLine';
import waitForBotToRespond from './setup/waitForBotToRespond';

describe('Happy path', () => {
let unsubscribes;

beforeEach(() => unsubscribes = []);
afterEach(() => unsubscribes.forEach(fn => onErrorResumeNext(fn)));

describe('should receive the welcome message from bot', () => {
let directLine;

describe('using REST', () => {
beforeEach(() => jest.setTimeout(timeouts.rest));

test('with secret', async () => {
directLine = await createDirectLine.forREST({ token: false });
});
});

describe('using Web Socket', () => {
beforeEach(() => jest.setTimeout(timeouts.webSocket));

test('with secret', async () => {
directLine = await createDirectLine.forWebSocket({ token: false });
});
});

afterEach(async () => {
// If directLine object is undefined, that means the test is failing.
if (!directLine) { return; }

unsubscribes.push(directLine.end.bind(directLine));

directLine.setUserId('u_test');

await waitForBotToRespond(directLine, ({ text }) => text === 'Welcome');
});
});
});
44 changes: 44 additions & 0 deletions __tests__/unhappy.setUserIdAfterConnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'dotenv/config';

import onErrorResumeNext from 'on-error-resume-next';

import { timeouts } from './constants.json';
import * as createDirectLine from './setup/createDirectLine';
import waitForConnected from './setup/waitForConnected';

describe('Unhappy path', () => {
let unsubscribes;

beforeEach(() => unsubscribes = []);
afterEach(() => unsubscribes.forEach(fn => onErrorResumeNext(fn)));

describe('should receive the welcome message from bot', () => {
let directLine;

describe('using REST', () => {
beforeEach(() => jest.setTimeout(timeouts.rest));

test('with secret', async () => {
directLine = await createDirectLine.forREST({ token: false });
});
});

describe('using Web Socket', () => {
beforeEach(() => jest.setTimeout(timeouts.webSocket));

test('with secret', async () => {
directLine = await createDirectLine.forWebSocket({ token: false });
});
});

afterEach(async () => {
// If directLine object is undefined, that means the test is failing.
if (!directLine) { return; }

unsubscribes.push(directLine.end.bind(directLine));
unsubscribes.push(await waitForConnected(directLine));

expect(() => directLine.setUserId('e_test')).toThrowError('DirectLineJS: It is connected, we cannot set user id.');
});
});
});
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"botframework-streaming": "4.10.3",
"core-js": "3.6.4",
"cross-fetch": "3.0.4",
"jwt-decode": "3.1.2",
"rxjs": "5.5.10",
"url-search-params-polyfill": "8.0.0"
},
Expand All @@ -42,6 +43,7 @@
"@babel/preset-env": "^7.6.0",
"@babel/preset-typescript": "^7.6.0",
"@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.5.0",
"@types/node": "^12.7.4",
"@types/p-defer": "^2.0.0",
"babel-jest": "^24.9.0",
Expand Down
37 changes: 37 additions & 0 deletions src/directLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IScheduler } from 'rxjs/Scheduler';
import { Subscriber } from 'rxjs/Subscriber';
import { Subscription } from 'rxjs/Subscription';
import { async as AsyncScheduler } from 'rxjs/scheduler/async';
import jwtDecode, { JwtPayload, InvalidTokenError } from 'jwt-decode';

import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/combineLatest';
Expand Down Expand Up @@ -471,6 +472,7 @@ export class DirectLine implements IBotConnection {
private retries: number;

private localeOnStartConversation: string;
private userIdOnStartConversation: string;

private pollingInterval: number = 1000; //ms

Expand Down Expand Up @@ -630,6 +632,9 @@ export class DirectLine implements IBotConnection {
const body = this.conversationId
? undefined
: {
user: {
id: this.userIdOnStartConversation
},
locale: this.localeOnStartConversation
};
return this.services.ajax({
Expand Down Expand Up @@ -749,6 +754,11 @@ export class DirectLine implements IBotConnection {
}

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

setUserId(userId: string) {
if (this.connectionStatus$.getValue() === ConnectionStatus.Online) {
throw new Error('DirectLineJS: It is connected, we cannot set user id.');
timenick marked this conversation as resolved.
Show resolved Hide resolved
}

const userIdFromToken = this.parseToken(this.token);
if (userIdFromToken) {
return console.warn('DirectLineJS: user id is already set in token, will ignore this user id.');
}

if (/^dl_/u.test(userId)) {
return console.warn('DirectLineJS: user id prefixed with "dl_" is reserved and must be embedded into the Direct Line token to prevent forgery.');
}

this.userIdOnStartConversation = userId;
}

private parseToken(token: string) {
try {
const { user } = jwtDecode<JwtPayload>(token) as { [key: string]: any; };
return user;
} catch (e) {
if (e instanceof InvalidTokenError) {
return undefined;
}
}
}

}