Skip to content

Commit 497492f

Browse files
author
timmydoza
authored
Merge pull request #64 from twilio-labs/VIDEO-3729-add-conversations-support
Video 3729 add conversations support
2 parents 6153d12 + 3c1b7ac commit 497492f

File tree

11 files changed

+457
-55
lines changed

11 files changed

+457
-55
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 0.8.0
4+
5+
### New Features
6+
7+
- Added support for Twilio Conversations. When an application requests a token from the `/token` endpoint, a `create_conversation=true` parameter can now be specified. When this parameter is present, the `/token` endpoint will create a conversation that is associated with the room and add the participant to it. This allows video application to use [Twilio Conversations](https://www.twilio.com/conversations-api) for in-room chat.
8+
9+
### Maintenence
10+
11+
- Upgraded @twilio/cli-core from 5.15.1 to 5.17.0
12+
- Upgraded lodash from 4.17.20 to 4.17.21
13+
314
## 0.7.1
415

516
### Maintenence

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright 2020 Twilio Inc.
189+
Copyright 2021 Twilio Inc.
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,21 @@ Each request is verified using a passcode generated at deploy time. Passcodes re
8282

8383
### Token
8484

85-
Returns a Programmable Video Access token.
85+
This endpoint returns a Programmable Video Access token. When `create_room` is true, it will create a room, and when `create_conversation` is true, it will create a [Twilio Conversation](https://www.twilio.com/docs/conversations/api/conversation-resource) associated with the room. This token is used by the above mentioned Video Apps to connect to a video room and a conversation.
8686

8787
```shell
8888
POST /token
8989
```
9090

9191
#### Parameters
9292

93-
| Name | Type | Description |
94-
| --------------- | --------- | -------------------------------------------------------------------------------------- |
95-
| `passcode` | `string` | **Required**. The application passcode. |
96-
| `user_identity` | `string` | **Required**. The user's identity. |
97-
| `room_name` | `string` | A room name that will be used to create a token scoped to connecting to only one room. |
98-
| `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. |
93+
| Name | Type | Description |
94+
| --------------------- | --------- | -------------------------------------------------------------------------------------- |
95+
| `passcode` | `string` | **Required**. The application passcode. |
96+
| `user_identity` | `string` | **Required**. The user's identity. |
97+
| `room_name` | `string` | **Required when `create_room` is `true`** A room name that will be used to create a token scoped to connecting to only one room. |
98+
| `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. |
99+
| `create_conversation` | `boolean` | (default: `false`) When true, a [Twilio Conversation](https://www.twilio.com/docs/conversations/api/conversation-resource) will be created (if it doesn't already exist) and a participant will be added to it when a token is requested. `create_room` must also be `true`. |
99100

100101
#### Success Responses
101102

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@twilio-labs/plugin-rtc",
3-
"version": "0.7.2",
3+
"version": "0.8.0",
44
"description": "A Twilio-CLI plugin for real-time communication apps",
55
"main": "index.js",
66
"publishConfig": {

src/commands/rtc/apps/video/delete.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
const { findApp } = require('../../../../helpers');
1+
const { findApp, findConversationsService } = require('../../../../helpers');
22
const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands;
33

44
class DeleteCommand extends TwilioClientCommand {
55
async run() {
66
await super.run();
77
const appInfo = await findApp.call(this);
8+
const conversatsionsServiceInfo = await findConversationsService.call(this);
89

910
if (appInfo) {
1011
await this.twilioClient.serverless.services(appInfo.sid).remove();
12+
if (conversatsionsServiceInfo) {
13+
await this.twilioClient.conversations.services(conversatsionsServiceInfo.sid).remove();
14+
}
1115
console.log('Successfully deleted app.');
1216
} else {
1317
console.log('There is no app to delete.');

src/helpers.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ async function displayAppInfo() {
143143
);
144144
}
145145

146+
async function findConversationsService() {
147+
const services = await this.twilioClient.conversations.services.list();
148+
return services.find(service => service.friendlyName.includes(APP_NAME));
149+
}
150+
151+
async function getConversationsServiceSID() {
152+
const exisitingConversationsService = await findConversationsService.call(this);
153+
154+
if (exisitingConversationsService) {
155+
return exisitingConversationsService.sid;
156+
}
157+
158+
const service = await this.twilioClient.conversations.services.create({
159+
friendlyName: `${APP_NAME}-conversations-service`,
160+
});
161+
return service.sid;
162+
}
163+
146164
async function deploy() {
147165
const assets = this.flags['app-directory'] ? await getAssets(this.flags['app-directory']) : [];
148166
const { functions } = await getListOfFunctionsAndAssets(__dirname, {
@@ -181,13 +199,16 @@ TWILIO_API_SECRET = the secret for the API Key`);
181199

182200
cli.action.start('deploying app');
183201

202+
const conversationServiceSid = await getConversationsServiceSID.call(this);
203+
184204
const deployOptions = {
185205
env: {
186206
TWILIO_API_KEY_SID: this.twilioClient.username,
187207
TWILIO_API_KEY_SECRET: this.twilioClient.password,
188208
API_PASSCODE: pin,
189209
API_PASSCODE_EXPIRY: expiryTime,
190210
ROOM_TYPE: this.flags['room-type'],
211+
CONVERSATIONS_SERVICE_SID: conversationServiceSid,
191212
},
192213
pkgJson: {
193214
dependencies: {
@@ -220,6 +241,7 @@ module.exports = {
220241
deploy,
221242
displayAppInfo,
222243
findApp,
244+
findConversationsService,
223245
getAssets,
224246
getMiddleware,
225247
getAppInfo,

src/serverless/functions/token.js

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33

44
const AccessToken = Twilio.jwt.AccessToken;
55
const VideoGrant = AccessToken.VideoGrant;
6+
const ChatGrant = AccessToken.ChatGrant;
67
const MAX_ALLOWED_SESSION_DURATION = 14400;
78

89
module.exports.handler = async (context, event, callback) => {
9-
const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE } = context;
10+
const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE, CONVERSATIONS_SERVICE_SID } = context;
1011

1112
const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path);
1213
authHandler(context, event, callback);
1314

14-
const { user_identity, room_name, create_room = true } = event;
15+
const { user_identity, room_name, create_room = true, create_conversation = false } = event;
1516

1617
let response = new Twilio.Response();
1718
response.appendHeader('Content-Type', 'application/json');
@@ -27,6 +28,17 @@ module.exports.handler = async (context, event, callback) => {
2728
return callback(null, response);
2829
}
2930

31+
if (typeof create_conversation !== 'boolean') {
32+
response.setStatusCode(400);
33+
response.setBody({
34+
error: {
35+
message: 'invalid parameter',
36+
explanation: 'A boolean value must be provided for the create_conversation parameter',
37+
},
38+
});
39+
return callback(null, response);
40+
}
41+
3042
if (!user_identity) {
3143
response.setStatusCode(400);
3244
response.setBody({
@@ -38,14 +50,29 @@ module.exports.handler = async (context, event, callback) => {
3850
return callback(null, response);
3951
}
4052

53+
if (!room_name && create_room) {
54+
response.setStatusCode(400);
55+
response.setBody({
56+
error: {
57+
message: 'missing room_name',
58+
explanation: 'The room_name parameter is missing. room_name is required when create_room is true.',
59+
},
60+
});
61+
return callback(null, response);
62+
}
63+
4164
if (create_room) {
4265
const client = context.getTwilioClient();
66+
let room;
4367

4468
try {
45-
await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE });
69+
// See if a room already exists
70+
room = await client.video.rooms(room_name).fetch();
4671
} catch (e) {
47-
// Ignore 53113 error (room already exists). See: https://www.twilio.com/docs/api/errors/53113
48-
if (e.code !== 53113) {
72+
try {
73+
// If room doesn't exist, create it
74+
room = await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE });
75+
} catch (e) {
4976
response.setStatusCode(500);
5077
response.setBody({
5178
error: {
@@ -56,14 +83,65 @@ module.exports.handler = async (context, event, callback) => {
5683
return callback(null, response);
5784
}
5885
}
86+
87+
if (create_conversation) {
88+
const conversationsClient = client.conversations.services(CONVERSATIONS_SERVICE_SID);
89+
90+
try {
91+
// See if conversation already exists
92+
await conversationsClient.conversations(room.sid).fetch();
93+
} catch (e) {
94+
try {
95+
// If conversation doesn't exist, create it.
96+
await conversationsClient.conversations.create({ uniqueName: room.sid });
97+
} catch (e) {
98+
response.setStatusCode(500);
99+
response.setBody({
100+
error: {
101+
message: 'error creating conversation',
102+
explanation: 'Something went wrong when creating a conversation.',
103+
},
104+
});
105+
return callback(null, response);
106+
}
107+
}
108+
109+
try {
110+
// Add participant to conversation
111+
await conversationsClient.conversations(room.sid).participants.create({ identity: user_identity });
112+
} catch (e) {
113+
// Ignore "Participant already exists" error (50433)
114+
if (e.code !== 50433) {
115+
response.setStatusCode(500);
116+
response.setBody({
117+
error: {
118+
message: 'error creating conversation participant',
119+
explanation: 'Something went wrong when creating a conversation participant.',
120+
},
121+
});
122+
return callback(null, response);
123+
}
124+
}
125+
}
59126
}
60127

128+
// Create token
61129
const token = new AccessToken(ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, {
62130
ttl: MAX_ALLOWED_SESSION_DURATION,
63131
});
132+
133+
// Add participant's identity to token
64134
token.identity = user_identity;
135+
136+
// Add video grant to token
65137
const videoGrant = new VideoGrant({ room: room_name });
66138
token.addGrant(videoGrant);
139+
140+
// Add chat grant to token
141+
const chatGrant = new ChatGrant({ serviceSid: CONVERSATIONS_SERVICE_SID });
142+
token.addGrant(chatGrant);
143+
144+
// Return token
67145
response.setStatusCode(200);
68146
response.setBody({ token: token.toJwt(), room_type: ROOM_TYPE });
69147
return callback(null, response);

test/e2e/e2e.test.js

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,53 @@ describe('the RTC Twilio-CLI Plugin', () => {
133133
const { body } = await superagent
134134
.post(`${URL}/token`)
135135
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' });
136-
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } });
136+
expect(jwt.decode(body.token).grants).toEqual(
137+
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
138+
);
137139
expect(body.room_type).toEqual('group');
138140

139141
const room = await twilioClient.video.rooms(ROOM_NAME).fetch();
140142
expect(room.type).toEqual('group');
141143
});
142144

145+
it('should return a video token with a valid Chat Grant and add the participant to the conversation', async () => {
146+
const ROOM_NAME = nanoid();
147+
const { body } = await superagent
148+
.post(`${URL}/token`)
149+
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_conversation: true });
150+
151+
const conversationServiceSid = jwt.decode(body.token).grants.chat.service_sid;
152+
153+
const room = await twilioClient.video.rooms(ROOM_NAME).fetch();
154+
155+
// Find the deployed conversations service
156+
const deployedConversationsServices = await twilioClient.conversations.services.list();
157+
const deployedConversationsService = deployedConversationsServices.find(
158+
service => (service.sid = conversationServiceSid)
159+
);
160+
161+
// Find the conversation participant
162+
const conversationParticipants = await twilioClient.conversations
163+
.services(deployedConversationsService.sid)
164+
.conversations(room.sid)
165+
.participants.list();
166+
const conversationParticipant = conversationParticipants.find(
167+
participant => participant.identity === 'test user'
168+
);
169+
170+
expect(deployedConversationsService).toBeDefined();
171+
expect(conversationParticipant).toBeDefined();
172+
});
173+
143174
it('should return a video token without creating a room when the "create_room" flag is false', async () => {
144175
expect.assertions(3);
145176
const ROOM_NAME = nanoid();
146177
const { body } = await superagent
147178
.post(`${URL}/token`)
148179
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_room: false });
149-
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } });
180+
expect(jwt.decode(body.token).grants).toEqual(
181+
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
182+
);
150183
expect(body.room_type).toEqual('group');
151184

152185
try {
@@ -156,6 +189,30 @@ describe('the RTC Twilio-CLI Plugin', () => {
156189
}
157190
});
158191

192+
it('should return a video token without creating a conversation when the "create_conversation" flag is false', async () => {
193+
const ROOM_NAME = nanoid();
194+
const { body } = await superagent
195+
.post(`${URL}/token`)
196+
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_conversation: false });
197+
198+
const conversationServiceSid = jwt.decode(body.token).grants.chat.service_sid;
199+
200+
const room = await twilioClient.video.rooms(ROOM_NAME).fetch();
201+
202+
// Find the deployed conversations service
203+
const deployedConversationsServices = await twilioClient.conversations.services.list();
204+
const deployedConversationsService = deployedConversationsServices.find(
205+
service => (service.sid = conversationServiceSid)
206+
);
207+
208+
const conversationPromise = twilioClient.conversations
209+
.services(deployedConversationsService.sid)
210+
.conversations(room.sid)
211+
.fetch();
212+
213+
expect(conversationPromise).rejects.toEqual(expect.objectContaining({ code: 20404 }));
214+
});
215+
159216
it('should return a 401 error when an incorrect passcode is provided', () => {
160217
superagent
161218
.post(`${URL}/token`)
@@ -261,7 +318,9 @@ describe('the RTC Twilio-CLI Plugin', () => {
261318
const { body } = await superagent
262319
.post(`${URL}/token`)
263320
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' });
264-
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } });
321+
expect(jwt.decode(body.token).grants).toEqual(
322+
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
323+
);
265324
expect(body.room_type).toEqual('go');
266325

267326
const room = await twilioClient.video.rooms(ROOM_NAME).fetch();
@@ -298,7 +357,9 @@ describe('the RTC Twilio-CLI Plugin', () => {
298357
const { body } = await superagent
299358
.post(`${testURL}/token`)
300359
.send({ passcode: updatedPasscode, room_name: 'test-room', user_identity: 'test user' });
301-
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: 'test-room' } });
360+
expect(jwt.decode(body.token).grants).toEqual(
361+
expect.objectContaining({ identity: 'test user', video: { room: 'test-room' } })
362+
);
302363
});
303364
});
304365
});

0 commit comments

Comments
 (0)