Skip to content

Commit

Permalink
Merge pull request #64 from twilio-labs/VIDEO-3729-add-conversations-…
Browse files Browse the repository at this point in the history
…support

Video 3729 add conversations support
  • Loading branch information
timmydoza authored Mar 5, 2021
2 parents 6153d12 + 3c1b7ac commit 497492f
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 55 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.8.0

### New Features

- 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.

### Maintenence

- Upgraded @twilio/cli-core from 5.15.1 to 5.17.0
- Upgraded lodash from 4.17.20 to 4.17.21

## 0.7.1

### Maintenence
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2020 Twilio Inc.
Copyright 2021 Twilio Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,21 @@ Each request is verified using a passcode generated at deploy time. Passcodes re

### Token

Returns a Programmable Video Access token.
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.

```shell
POST /token
```

#### Parameters

| Name | Type | Description |
| --------------- | --------- | -------------------------------------------------------------------------------------- |
| `passcode` | `string` | **Required**. The application passcode. |
| `user_identity` | `string` | **Required**. The user's identity. |
| `room_name` | `string` | A room name that will be used to create a token scoped to connecting to only one room. |
| `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. |
| Name | Type | Description |
| --------------------- | --------- | -------------------------------------------------------------------------------------- |
| `passcode` | `string` | **Required**. The application passcode. |
| `user_identity` | `string` | **Required**. The user's identity. |
| `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. |
| `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. |
| `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`. |

#### Success Responses

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@twilio-labs/plugin-rtc",
"version": "0.7.2",
"version": "0.8.0",
"description": "A Twilio-CLI plugin for real-time communication apps",
"main": "index.js",
"publishConfig": {
Expand Down
6 changes: 5 additions & 1 deletion src/commands/rtc/apps/video/delete.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
const { findApp } = require('../../../../helpers');
const { findApp, findConversationsService } = require('../../../../helpers');
const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands;

class DeleteCommand extends TwilioClientCommand {
async run() {
await super.run();
const appInfo = await findApp.call(this);
const conversatsionsServiceInfo = await findConversationsService.call(this);

if (appInfo) {
await this.twilioClient.serverless.services(appInfo.sid).remove();
if (conversatsionsServiceInfo) {
await this.twilioClient.conversations.services(conversatsionsServiceInfo.sid).remove();
}
console.log('Successfully deleted app.');
} else {
console.log('There is no app to delete.');
Expand Down
22 changes: 22 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ async function displayAppInfo() {
);
}

async function findConversationsService() {
const services = await this.twilioClient.conversations.services.list();
return services.find(service => service.friendlyName.includes(APP_NAME));
}

async function getConversationsServiceSID() {
const exisitingConversationsService = await findConversationsService.call(this);

if (exisitingConversationsService) {
return exisitingConversationsService.sid;
}

const service = await this.twilioClient.conversations.services.create({
friendlyName: `${APP_NAME}-conversations-service`,
});
return service.sid;
}

async function deploy() {
const assets = this.flags['app-directory'] ? await getAssets(this.flags['app-directory']) : [];
const { functions } = await getListOfFunctionsAndAssets(__dirname, {
Expand Down Expand Up @@ -181,13 +199,16 @@ TWILIO_API_SECRET = the secret for the API Key`);

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

const conversationServiceSid = await getConversationsServiceSID.call(this);

const deployOptions = {
env: {
TWILIO_API_KEY_SID: this.twilioClient.username,
TWILIO_API_KEY_SECRET: this.twilioClient.password,
API_PASSCODE: pin,
API_PASSCODE_EXPIRY: expiryTime,
ROOM_TYPE: this.flags['room-type'],
CONVERSATIONS_SERVICE_SID: conversationServiceSid,
},
pkgJson: {
dependencies: {
Expand Down Expand Up @@ -220,6 +241,7 @@ module.exports = {
deploy,
displayAppInfo,
findApp,
findConversationsService,
getAssets,
getMiddleware,
getAppInfo,
Expand Down
88 changes: 83 additions & 5 deletions src/serverless/functions/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@

const AccessToken = Twilio.jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;
const ChatGrant = AccessToken.ChatGrant;
const MAX_ALLOWED_SESSION_DURATION = 14400;

module.exports.handler = async (context, event, callback) => {
const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE } = context;
const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE, CONVERSATIONS_SERVICE_SID } = context;

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

const { user_identity, room_name, create_room = true } = event;
const { user_identity, room_name, create_room = true, create_conversation = false } = event;

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

if (typeof create_conversation !== 'boolean') {
response.setStatusCode(400);
response.setBody({
error: {
message: 'invalid parameter',
explanation: 'A boolean value must be provided for the create_conversation parameter',
},
});
return callback(null, response);
}

if (!user_identity) {
response.setStatusCode(400);
response.setBody({
Expand All @@ -38,14 +50,29 @@ module.exports.handler = async (context, event, callback) => {
return callback(null, response);
}

if (!room_name && create_room) {
response.setStatusCode(400);
response.setBody({
error: {
message: 'missing room_name',
explanation: 'The room_name parameter is missing. room_name is required when create_room is true.',
},
});
return callback(null, response);
}

if (create_room) {
const client = context.getTwilioClient();
let room;

try {
await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE });
// See if a room already exists
room = await client.video.rooms(room_name).fetch();
} catch (e) {
// Ignore 53113 error (room already exists). See: https://www.twilio.com/docs/api/errors/53113
if (e.code !== 53113) {
try {
// If room doesn't exist, create it
room = await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE });
} catch (e) {
response.setStatusCode(500);
response.setBody({
error: {
Expand All @@ -56,14 +83,65 @@ module.exports.handler = async (context, event, callback) => {
return callback(null, response);
}
}

if (create_conversation) {
const conversationsClient = client.conversations.services(CONVERSATIONS_SERVICE_SID);

try {
// See if conversation already exists
await conversationsClient.conversations(room.sid).fetch();
} catch (e) {
try {
// If conversation doesn't exist, create it.
await conversationsClient.conversations.create({ uniqueName: room.sid });
} catch (e) {
response.setStatusCode(500);
response.setBody({
error: {
message: 'error creating conversation',
explanation: 'Something went wrong when creating a conversation.',
},
});
return callback(null, response);
}
}

try {
// Add participant to conversation
await conversationsClient.conversations(room.sid).participants.create({ identity: user_identity });
} catch (e) {
// Ignore "Participant already exists" error (50433)
if (e.code !== 50433) {
response.setStatusCode(500);
response.setBody({
error: {
message: 'error creating conversation participant',
explanation: 'Something went wrong when creating a conversation participant.',
},
});
return callback(null, response);
}
}
}
}

// Create token
const token = new AccessToken(ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, {
ttl: MAX_ALLOWED_SESSION_DURATION,
});

// Add participant's identity to token
token.identity = user_identity;

// Add video grant to token
const videoGrant = new VideoGrant({ room: room_name });
token.addGrant(videoGrant);

// Add chat grant to token
const chatGrant = new ChatGrant({ serviceSid: CONVERSATIONS_SERVICE_SID });
token.addGrant(chatGrant);

// Return token
response.setStatusCode(200);
response.setBody({ token: token.toJwt(), room_type: ROOM_TYPE });
return callback(null, response);
Expand Down
69 changes: 65 additions & 4 deletions test/e2e/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,53 @@ describe('the RTC Twilio-CLI Plugin', () => {
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' });
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } });
expect(jwt.decode(body.token).grants).toEqual(
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
);
expect(body.room_type).toEqual('group');

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

it('should return a video token with a valid Chat Grant and add the participant to the conversation', async () => {
const ROOM_NAME = nanoid();
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_conversation: true });

const conversationServiceSid = jwt.decode(body.token).grants.chat.service_sid;

const room = await twilioClient.video.rooms(ROOM_NAME).fetch();

// Find the deployed conversations service
const deployedConversationsServices = await twilioClient.conversations.services.list();
const deployedConversationsService = deployedConversationsServices.find(
service => (service.sid = conversationServiceSid)
);

// Find the conversation participant
const conversationParticipants = await twilioClient.conversations
.services(deployedConversationsService.sid)
.conversations(room.sid)
.participants.list();
const conversationParticipant = conversationParticipants.find(
participant => participant.identity === 'test user'
);

expect(deployedConversationsService).toBeDefined();
expect(conversationParticipant).toBeDefined();
});

it('should return a video token without creating a room when the "create_room" flag is false', async () => {
expect.assertions(3);
const ROOM_NAME = nanoid();
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_room: false });
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } });
expect(jwt.decode(body.token).grants).toEqual(
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
);
expect(body.room_type).toEqual('group');

try {
Expand All @@ -156,6 +189,30 @@ describe('the RTC Twilio-CLI Plugin', () => {
}
});

it('should return a video token without creating a conversation when the "create_conversation" flag is false', async () => {
const ROOM_NAME = nanoid();
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_conversation: false });

const conversationServiceSid = jwt.decode(body.token).grants.chat.service_sid;

const room = await twilioClient.video.rooms(ROOM_NAME).fetch();

// Find the deployed conversations service
const deployedConversationsServices = await twilioClient.conversations.services.list();
const deployedConversationsService = deployedConversationsServices.find(
service => (service.sid = conversationServiceSid)
);

const conversationPromise = twilioClient.conversations
.services(deployedConversationsService.sid)
.conversations(room.sid)
.fetch();

expect(conversationPromise).rejects.toEqual(expect.objectContaining({ code: 20404 }));
});

it('should return a 401 error when an incorrect passcode is provided', () => {
superagent
.post(`${URL}/token`)
Expand Down Expand Up @@ -261,7 +318,9 @@ describe('the RTC Twilio-CLI Plugin', () => {
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' });
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } });
expect(jwt.decode(body.token).grants).toEqual(
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
);
expect(body.room_type).toEqual('go');

const room = await twilioClient.video.rooms(ROOM_NAME).fetch();
Expand Down Expand Up @@ -298,7 +357,9 @@ describe('the RTC Twilio-CLI Plugin', () => {
const { body } = await superagent
.post(`${testURL}/token`)
.send({ passcode: updatedPasscode, room_name: 'test-room', user_identity: 'test user' });
expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: 'test-room' } });
expect(jwt.decode(body.token).grants).toEqual(
expect.objectContaining({ identity: 'test user', video: { room: 'test-room' } })
);
});
});
});
Expand Down
Loading

0 comments on commit 497492f

Please sign in to comment.