Skip to content

Commit

Permalink
Merge pull request #6 from jandusek/v1.0
Browse files Browse the repository at this point in the history
V1.0
  • Loading branch information
jandusek authored Dec 8, 2020
2 parents 34ab415 + b007d5e commit 96016ba
Show file tree
Hide file tree
Showing 26 changed files with 22,682 additions and 4,197 deletions.
85 changes: 60 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

## Table of contents

* [Introduction](#introduction)
+ [Notable features](#notable-features)
* [Installation](#installation)
* [ToDo](#todo)
* [Screenshots](#screenshots)
+ [SMS](#sms)
+ [Call](#call)
- [Introduction](#introduction)
- [Notable features](#notable-features)
- [Installation](#installation)
- [Roadmap](#roadmap)
- [Screenshots](#screenshots)
- [SMS](#sms)
- [Call](#call)

## Introduction

Expand All @@ -25,21 +25,21 @@ This repository consists of two parts:

The npm `deploy` command utilizes [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart) to deploy both the frontend and backend to [Twilio Runtime](https://www.twilio.com/docs/runtime/functions-assets-api).


### Notable features

* SMS: Infitinty scolling (older messages get loaded automatically as one scrolls up in a thread)
* SMS: Hovering over message timestamp displays tooltip with additional details of each message including its [SID](https://www.twilio.com/docs/glossary/what-is-a-sid)
* Configurable accent color (see `REACT_APP_ACCENT_COLOR` in `/.env`)

- SMS: Infitinty scolling (older messages get loaded automatically as one scrolls up in a thread)
- SMS: Hovering over message timestamp displays tooltip with additional details of each message including its [SID](https://www.twilio.com/docs/glossary/what-is-a-sid)
- SMS: Unread message count tracking
- Call: Support for both inbound and outbound calls
- Configurable accent color (see `REACT_APP_ACCENT_COLOR` in `/.env`)

## Installation

Before you start, make sure you have [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart) and its [serverless plugin](https://www.twilio.com/docs/twilio-cli/plugins#available-plugins) installed and working.

Then there are couple things that need to be prepared before the installation:

1. Note down your [Account SID and Auth Token](https://www.twilio.com/console)
1. Note down your [Account SID](https://www.twilio.com/console)
2. Create and note down [API key & secret](https://www.twilio.com/console/project/api-keys)
3. Create a [Programmable Chat Service](https://www.twilio.com/console/chat/services) and note down its SID

Expand All @@ -58,20 +58,28 @@ $ cd deploy; npm install; cd .. # install Twilio Runtime dependencies
$ npm run deploy # test deploy your application to get its public URLs
...
Functions:
[protected] https://twilio-phone-client-XXXX-dev.twil.io/callInbound <<< note down the /callInbound URL
[protected] https://twilio-phone-client-XXXX-dev.twil.io/callOutbound <<< note down the /callOutbound URL
[protected] https://twilio-phone-client-XXXX-dev.twil.io/msgInbound <<< note down the /msgInbound URL
https://twilio-phone-client-XXXX-dev.twil.io/getAccessToken
https://twilio-phone-client-XXXX-dev.twil.io/getCapToken
https://twilio-phone-client-XXXX-dev.twil.io/msgOutbound
```

Your phone client will **not work** at this point.

4. Purchase a [Twilio phone number](https://www.twilio.com/console/phone-numbers/incoming) and note it down (if you plan to use both SMS and calls, make sure your phone number has both capabilities). In the phone number's configuration, set the "A Message Comes In" Webhook to the `/msgInbound` URL you have collected above.
4. Purchase a [Twilio phone number](https://www.twilio.com/console/phone-numbers/incoming) and note it down (if you plan to use both SMS and calls, make sure your phone number has both capabilities).

In the phone number's configuration, set the "A Call Comes In" Webhook to the `/callInbound` URL you have collected above.

![A Call Comes In](./screenshots/voice_webhook.png)

Then set the "A Message Comes In" Webhook to the `/msgInbound` URL you have collected above.

![A Message Comes In](./screenshots/msg_webhook.png)

After you hit the Save button, both webhooks will update to a Function, that is expected.

5. Create a new [TwiML App](https://www.twilio.com/console/voice/twiml/apps) and set its Voice Request URL to the `/callOutbound` URL you have noted in the previous step. Save it and then go back to the TwiML App's configuration and note down its SID.

![TwiML APP](./screenshots/twiml_app.png)
Expand All @@ -82,12 +90,12 @@ Your phone client will **not work** at this point.
$ cp deploy/.env.sample deploy/.env
$ vim deploy/.env # fill in information collected in the previous steps
ACCOUNT_SID=ACxxx
AUTH_TOKEN=xxx
SYNC_API_KEY=SKxxx
SYNC_API_SECRET=xxx
API_KEY=SKxxx
API_SECRET=xxx
CHAT_SERVICE_SID=ISxxx
TWILIO_NUMBER=+1xxx
TWIML_APP_SID=APxxx
SECRET=some_password
$ npm run deploy # deploy your application for the 2nd time
...
Expand All @@ -102,16 +110,19 @@ Assets:

**Pro tip:** To run the client in its own resizable window with minimal window chrome, you can use something like this:

macOS: ```/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app="https://twilio-phone-client-XXXX-dev.twil.io/index.html"```
macOS: `/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app="https://twilio-phone-client-XXXX-dev.twil.io/index.html"`

Windows: ```chrome --app="https://twilio-phone-client-XXXX-dev.twil.io/index.html"```
Windows: `chrome --app="https://twilio-phone-client-XXXX-dev.twil.io/index.html"`

## ToDo
## Roadmap

* Add support for inbound calls (currently only outbound calls are supported)
* Add support for Multimedia Messages (MMS) for both inbound and outbound
* Add call history and allow quick redials
* Add ability to delete SMS threads from within the client
- ~~Add basic form of authentication~~
- Add one-click installation option (using Heroku) to eliminate the need for local env and numerous manual installation steps
- ~~Add support for inbound calls~~
- ~~Add unread badges to individual messaging threads and SMS channel overall~~
- ~~Add ability to delete SMS threads from within the client~~
- Add support for Multimedia Messages (MMS) for both inbound and outbound
- Add call history and allow quick redials

## Screenshots

Expand All @@ -122,3 +133,27 @@ Windows: ```chrome --app="https://twilio-phone-client-XXXX-dev.twil.io/index.htm
### Call

![Call](./screenshots/call.jpg)

## Changelog

### v1.0 - First stable version

- This is a breaking change version and reinstallation is recommended (identity used in SDKs and Chat channel member name is now tied to the phone number, API key env variables have been renamed)
- Moved from Capability to Access tokens for Client.js
- Auth Token no longer needed, authentication fully via API keys
- Fixed numerous issues associated with switching between the SMS and Call tabs
- Fixed issue with mute button not working under certain circumstances
- Message compose input field now always retains focus for smoother back and forth messaging experience
- Added support for inbound calls
- Added ability to delete messaging history with a given contact
- Added basic access control using a shared secret

#### Upgrade steps from v0.9

- rename env variables in `deploy/.env` from `SYNC_API_KEY` and `SYNC_API_SECRET` to `API_KEY` and `API_SECRET`
- create a new [Chat Service](https://www.twilio.com/console/chat/services) and update CHAT_SERVICE_SID with its SID in `deploy/.env`
- add a SECRET env variable with some shared secret (effectively a password) to `deploy/.env`

### v0.9 - Proof of concept

- Initial version
6 changes: 3 additions & 3 deletions deploy/.env.sample
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ACCOUNT_SID=ACxxx
AUTH_TOKEN=xxx
SYNC_API_KEY=SKxxx
SYNC_API_SECRET=xxx
API_KEY=SKxxx
API_SECRET=xxx
CHAT_SERVICE_SID=ISxxx
TWILIO_NUMBER=+1xxx
TWIML_APP_SID=APxxx
SECRET=some_password
6 changes: 6 additions & 0 deletions deploy/functions/callInbound.protected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
exports.handler = (context, event, callback) => {
let response = new Twilio.twiml.VoiceResponse();
const dial = response.dial();
dial.client('client' + context.TWILIO_NUMBER);
callback(null, response);
};
59 changes: 39 additions & 20 deletions deploy/functions/getAccessToken.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
const isEnabled = true;
const isEnabled = true; // master switch for programmatic enabling/disabling of the client

exports.handler = function (context, event, callback) {
const AccessToken = Twilio.jwt.AccessToken;
const ChatGrant = AccessToken.ChatGrant;
const VoiceGrant = AccessToken.VoiceGrant;
let response = new Twilio.Response();
response.setHeaders({
"Access-Control-Allow-Origin": "*"
});

// if running locally (i.e. testing), enable any originator for CORS
if (context.path === undefined)
response.setHeaders({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS'
});

// check master switch
if (!isEnabled) {
response.setStatusCode(404);
callback(null, response);
} else {
const token = new AccessToken(
context.ACCOUNT_SID,
context.SYNC_API_KEY,
context.SYNC_API_SECRET,
{ ttl: 3600 } // tokenAboutToExpire event is triggered 3 minutes before expiration to this gives each token ~30s of effective lifetime
);
const chatGrant = new ChatGrant({
serviceSid: context.CHAT_SERVICE_SID,
});
token.addGrant(chatGrant);
token.identity = "build-client";
response.setBody(token.toJwt());
callback(null, response);
response.setBody('This client is currently disabled');
return callback(null, response);
}

};
// check secret
if (event.secret !== context.SECRET) {
response.setStatusCode(401);
response.setBody('Invalid secret');
return callback(null, response);
}

const token = new AccessToken(
context.ACCOUNT_SID,
context.API_KEY,
context.API_SECRET,
{ ttl: 3600 } // tokenAboutToExpire event is triggered 3 minutes before expiration to this gives each token ~57m of effective lifetime
);
const chatGrant = new ChatGrant({
serviceSid: context.CHAT_SERVICE_SID
});
const voiceGrant = new VoiceGrant({
outgoingApplicationSid: process.env.TWIML_APP_SID,
incomingAllow: true
});
token.addGrant(chatGrant);
token.addGrant(voiceGrant);
token.identity = 'client' + context.TWILIO_NUMBER;
response.setBody(token.toJwt());
return callback(null, response);
};
30 changes: 0 additions & 30 deletions deploy/functions/getCapToken.js

This file was deleted.

61 changes: 39 additions & 22 deletions deploy/functions/msgInbound.protected.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,76 @@
const twilio = require('twilio');

const identity = 'build-client';

exports.handler = (context, event, callback) => {
exports.handler = async (context, event, callback) => {
const identity = 'client' + context.TWILIO_NUMBER;
let response = new Twilio.Response();
client = context.getTwilioClient();
const client = new twilio(context.API_KEY, context.API_SECRET, {
accountSid: context.ACCOUNT_SID
});
const chatName = event.From;

function postMessage(chatService) {
chatService.channels(chatName)
.messages
.create({
from: "them",
chatService
.channels(chatName)
.messages.create({
from: 'them',
body: event.Body,
attributes: JSON.stringify({
fromNumber: event.From,
toNumber: event.To,
sid: event.MessageSid
})
// ToDo: Add media handling for MMS
}).then(message => {
})
.then((message) => {
console.log(message);
response.setStatusCode(204);
callback(null, response);
}).catch(e => console.error("error posting message:", e));
})
.catch((e) => console.error('error posting message:', e));
}

const roles = await client.chat
.services(context.CHAT_SERVICE_SID)
.roles.list();
const adminRole = roles.find((role) => role.friendlyName === 'channel admin');

const chatService = client.chat.services(context.CHAT_SERVICE_SID);
console.log(`channel: ${chatName}`);
chatService.channels(chatName)
chatService
.channels(chatName)
.fetch()
.then(channel => {
.then((channel) => {
console.log(`fetched channel: ${channel.uniqueName}`);
postMessage(chatService);
})
.catch(e => {
.catch((e) => {
// if channel doesn't exist, create one
if (e.code === 20404) {
chatService.channels
.create({ uniqueName: chatName })
.then(channel => {
.create({
uniqueName: chatName
})
.then((channel) => {
// add our generic identity as a member of that channel
chatService.channels(chatName).members.create({ identity })
.then(member => {
chatService
.channels(chatName)
.members.create({
identity,
roleSid: adminRole.sid
})
.then((member) => {
console.log(`created channel: ${channel.uniqueName}`);
postMessage(chatService);
})
.catch(e => {
console.error(`error joining member: ${e}`)
.catch((e) => {
console.error(`error joining member: ${e}`);
response.setBody(`error joining member: ${e}`);
response.setStatusCode(500);
callback(null, response);
});
})
.catch(e => {
console.error(`error creating channel: ${e}`)
.catch((e) => {
console.error(`error creating channel: ${e}`);
response.setBody(`error creating channel: ${e}`);
response.setStatusCode(500);
callback(null, response);
Expand All @@ -65,4 +82,4 @@ exports.handler = (context, event, callback) => {
callback(null, response);
}
});
};
};
Loading

0 comments on commit 96016ba

Please sign in to comment.