Initial commit of Twilio Server Starter for Python

This commit is contained in:
Jeffrey Linwood
2016-11-14 10:41:18 -06:00
parent 5430cce36c
commit 49b3c6d012
20 changed files with 1259 additions and 2 deletions

18
.env.example Normal file
View File

@@ -0,0 +1,18 @@
# Required for all uses
TWILIO_ACCOUNT_SID=
TWILIO_API_KEY=
TWILIO_API_SECRET=
# Required for Video
TWILIO_CONFIGURATION_SID=
# Required for IP Messaging
TWILIO_IPM_SERVICE_SID=
# Required for Notify
TWILIO_NOTIFICATION_SERVICE_SID=
TWILIO_APN_CREDENTIAL_SID=
TWILIO_GCM_CREDENTIAL_SID=
# Required for Sync
TWILIO_SYNC_SERVICE_SID=

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.env
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View File

@@ -1,2 +1,92 @@
# sdk-starter-python
Demo application showcasing Twilio API usage in Python
# Twilio SDK Starter Application for Python
This sample project demonstrates how to use Twilio APIs in a Python web
application. Once the app is up and running, check out [the home page](http://localhost:5000)
to see which demos you can run. You'll find examples for [IP Messaging](https://www.twilio.com/ip-messaging),
[Video](https://www.twilio.com/video), [Sync](https://www.twilio.com/sync), and more.
Let's get started!
## Configure the sample application
To run the application, you'll need to gather your Twilio account credentials and configure them
in a file named `.env`. To create this file from an example template, do the following in your
Terminal.
```bash
cp .env.example .env
```
Open `.env` in your favorite text editor and configure the following values.
### Configure account information
Every sample in the demo requires some basic credentials from your Twilio account. Configure these first.
| Config Value | Description |
| :------------- |:------------- |
`TWILIO_ACCOUNT_SID` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console).
`TWILIO_API_KEY` | Used to authenticate - [generate one here](https://www.twilio.com/console/video/dev-tools/api-keys).
`TWILIO_API_SECRET` | Used to authenticate - [just like the above, you'll get one here](https://www.twilio.com/console/video/dev-tools/api-keys).
#### A Note on API Keys
When you generate an API key pair at the URLs above, your API Secret will only be shown once -
make sure to save this information in a secure location, or possibly your `~/.bash_profile`.
### Configure product-specific settings
Depending on which demos you'd like to run, you'll need to configure a few more values in your
`.env` file.
| Config Value | Product Demo | Description |
| :------------- |:------------- |:------------- |
`TWILIO_IPM_SERVICE_SID` | IP Messaging | Like a database for your IP Messaging data - [generate one in the console here](https://www.twilio.com/console/ip-messaging/services)
`TWILIO_CONFIGURATION_SID` | Video | Identifier for a set of config properties for your video application - [find yours here](https://www.twilio.com/console/video/profiles)
`TWILIO_SYNC_SERVICE_SID` | Sync (Preview) | Like a database for your Sync data - generate one with the curl command below.
`TWILIO_NOTIFICATION_SERVICE_SID` | Notify (Preview) | You will need to create a Notify service - [generate one here](https://www.twilio.com/console/notify/services)
`TWILIO_APN_CREDENTIAL_SID` | Notify (Preview) | Adds iOS notification ability to your app - [generate one here](https://www.twilio.com/console/notify/credentials). You'll need to provision your APN push credentials to generate this. See [this](https://www.twilio.com/docs/api/ip-messaging/guides/push-notifications-ios) guide on how to do that. (Optional)
`TWILIO_GCM_CREDENTIAL_SID` | Notify (Preview) |Adds Android/GCM notification ability to your app - [generate one here](https://www.twilio.com/console/notify/credentials). You'll need to provision your GCM push credentials to generate this. See [this](https://www.twilio.com/docs/api/ip-messaging/guides/push-notifications-android) guide on how to do that (Optional)
#### Temporary: Generating a Sync Service Instance
During the Sync developer preview, you will need to generate Sync service
instances via API until the Console GUI is available. Using the API key pair you
generated above, generate a service instance via REST API with this curl command:
```bash
curl -X POST https://preview.twilio.com/Sync/Services \
-d 'FriendlyName=MySyncServiceInstance' \
-u 'SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:your_api_secret'
```
## Run the sample application
This application uses the lightweight [Flask Framework](http://flask.pocoo.org/).
We need to set up your Python environment. Install `virtualenv` via `pip`:
```bash
pip install virtualenv
```
Next, we need to install our depenedencies:
```bash
virtualenv venv
source venv/bin/activate
pip install -r requirements.txt
```
Now we should be all set! Run the application using the `python` command.
```bash
python app.py
```
Your application should now be running at [http://localhost:5000](http://localhost:5000). When you're finished, deactivate your virtual environment using `deactivate`.
![Home Screen](https://cloud.githubusercontent.com/assets/809856/19532947/673cc7d6-9603-11e6-9a7c-13c0f9ab33b7.png)
## License
MIT

148
app.py Normal file
View File

@@ -0,0 +1,148 @@
import os
from flask import Flask, jsonify, request
from faker import Factory
from twilio.rest import Client
from twilio.jwt.access_token import AccessToken, SyncGrant, ConversationsGrant, IpMessagingGrant
from dotenv import load_dotenv, find_dotenv
from os.path import join, dirname
app = Flask(__name__)
fake = Factory.create()
dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.route('/video/')
def video():
return app.send_static_file('video/index.html')
@app.route('/sync/')
def sync():
return app.send_static_file('sync/index.html')
@app.route('/notify/')
def notify():
return app.send_static_file('notify/index.html')
@app.route('/ipmessaging/')
def ipmessaging():
return app.send_static_file('ipmessaging/index.html')
# Basic health check - check environment variables have been configured
# correctly
@app.route('/config')
def config():
return jsonify(
TWILIO_ACCOUNT_SID=os.environ['TWILIO_ACCOUNT_SID'],
TWILIO_NOTIFICATION_SERVICE_SID=os.environ['TWILIO_NOTIFICATION_SERVICE_SID'],
TWILIO_APN_CREDENTIAL_SID=os.environ['TWILIO_APN_CREDENTIAL_SID'],
TWILIO_GCM_CREDENTIAL_SID=os.environ['TWILIO_GCM_CREDENTIAL_SID'],
TWILIO_API_KEY=os.environ['TWILIO_API_KEY'],
TWILIO_API_SECRET=bool(os.environ['TWILIO_API_SECRET']),
TWILIO_IPM_SERVICE_SID=os.environ['TWILIO_IPM_SERVICE_SID'],
TWILIO_SYNC_SERVICE_SID=os.environ['TWILIO_SYNC_SERVICE_SID'],
TWILIO_CONFIGURATION_SID=os.environ['TWILIO_CONFIGURATION_SID']
)
@app.route('/token')
def token():
# get credentials for environment variables
account_sid = os.environ['TWILIO_ACCOUNT_SID']
api_key = os.environ['TWILIO_API_KEY']
api_secret = os.environ['TWILIO_API_SECRET']
sync_service_sid = os.environ['TWILIO_SYNC_SERVICE_SID']
configuration_profile_sid = os.environ['TWILIO_CONFIGURATION_SID']
ipm_service_sid = os.environ['TWILIO_IPM_SERVICE_SID']
# create a randomly generated username for the client
identity = fake.user_name()
# Create a unique endpoint ID for the
device_id = request.args.get('device')
endpoint = "TwilioAppDemo:{0}:{1}".format(identity, device_id)
# Create access token with credentials
token = AccessToken(account_sid, api_key, api_secret, identity)
# Create a Sync grant and add to token
if sync_service_sid:
sync_grant = SyncGrant(endpoint_id=endpoint, service_sid=sync_service_sid)
token.add_grant(sync_grant)
# Create a Video grant and add to token
if configuration_profile_sid:
video_grant = ConversationsGrant(configuration_profile_sid=configuration_profile_sid)
token.add_grant(video_grant)
# Create an IP Messaging grant and add to token
if ipm_service_sid:
ipm_grant = IpMessagingGrant(endpoint_id=endpoint, service_sid=ipm_service_sid)
token.add_grant(ipm_grant)
# Return token info as JSON
return jsonify(identity=identity, token=token.to_jwt())
# Notify - create a device binding from a POST HTTP request
@app.route('/register', methods=['POST'])
def register():
# get credentials for environment variables
account_sid = os.environ['TWILIO_ACCOUNT_SID']
api_key = os.environ['TWILIO_API_KEY']
api_secret = os.environ['TWILIO_API_SECRET']
service_sid = os.environ['TWILIO_NOTIFICATION_SERVICE_SID']
# Initialize the Twilio client
client = Client(api_key, api_secret, account_sid)
# Body content
content = request.get_json()
# Get a reference to the notification service
service = client.notify.v1.services(service_sid)
# Create the binding
binding = service.bindings.create(
endpoint=content["endpoint"],
identity=content["identity"],
binding_type=content["BindingType"],
address=content["Address"])
print binding
# Return success message
return jsonify(message="Binding created!")
# Notify - send a notification from a POST HTTP request
@app.route('/send-notification', methods=['POST'])
def send_notification():
# get credentials for environment variables
account_sid = os.environ['TWILIO_ACCOUNT_SID']
api_key = os.environ['TWILIO_API_KEY']
api_secret = os.environ['TWILIO_API_SECRET']
service_sid = os.environ['TWILIO_NOTIFICATION_SERVICE_SID']
# Initialize the Twilio client
client = Client(api_key, api_secret, account_sid)
service = client.notify.v1.services(service_sid)
# Create a notification for a given identity
identity = request.form.get('identity')
notification = service.notifications.create(
identity=identity,
body='Hello ' + identity + '!'
)
return jsonify(message="Notification created!")
@app.route('/<path:path>')
def static_file(path):
return app.send_static_file(path)
if __name__ == '__main__':
app.run(debug=True)

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
Flask==0.10.1
fake-factory==0.5.3
twilio==6.0.0rc12
python-dotenv==0.6.0

72
static/config-check.js Normal file
View File

@@ -0,0 +1,72 @@
$(function() {
$.get('/config', function(response) {
configureField(response, 'TWILIO_ACCOUNT_SID','twilioAccountSID',false);
configureField(response, 'TWILIO_API_KEY','twilioAPIKey',false);
configureField(response, 'TWILIO_API_SECRET','twilioAPISecret',true);
configureField(response, 'TWILIO_CONFIGURATION_SID','twilioConfigurationSID',false);
configureField(response, 'TWILIO_NOTIFICATION_SERVICE_SID','twilioNotificationServiceSID',false);
configureField(response, 'TWILIO_APN_CREDENTIAL_SID','twilioAPNCredentialSID',false);
configureField(response, 'TWILIO_GCM_CREDENTIAL_SID','twilioGCMCredentialSID',false);
configureField(response, 'TWILIO_IPM_SERVICE_SID','twilioIPMServiceSID',false);
configureField(response, 'TWILIO_SYNC_SERVICE_SID','twilioSyncServiceSID',false);
//configure individual product buttons
if (response.TWILIO_ACCOUNT_SID && response.TWILIO_ACCOUNT_SID != '' &&
response.TWILIO_API_KEY && response.TWILIO_API_KEY != '' && response.TWILIO_API_SECRET) {
if (response.TWILIO_CONFIGURATION_SID && response.TWILIO_CONFIGURATION_SID != '') {
$('#videoDemoButton').addClass('btn-success');
} else {
$('#videoDemoButton').addClass('btn-danger');
}
if (response.TWILIO_IPM_SERVICE_SID && response.TWILIO_IPM_SERVICE_SID != '') {
$('#ipmDemoButton').addClass('btn-success');
} else {
$('#ipmDemoButton').addClass('btn-danger');
}
if (response.TWILIO_SYNC_SERVICE_SID && response.TWILIO_SYNC_SERVICE_SID != '') {
$('#syncDemoButton').addClass('btn-success');
} else {
$('#syncDemoButton').addClass('btn-danger');
}
if (response.TWILIO_NOTIFICATION_SERVICE_SID && response.TWILIO_NOTIFICATION_SERVICE_SID != '') {
$('#notifyDemoButton').addClass('btn-success');
} else {
$('#notifyDemoButton').addClass('btn-danger');
}
}
else {
$('#videoDemoButton').addClass('btn-danger');
$('#ipmDemoButton').addClass('btn-danger');
$('#syncDemoButton').addClass('btn-danger');
$('#notifyDemoButton').addClass('btn-danger');
}
});
var configureField = function(response, keyName,elementId,masked) {
if (masked) {
if (response[keyName]) {
$('#' + elementId).html('Configured properly');
$('#' + elementId).addClass('set');
} else {
$('#' + elementId).html('Not configured in .env');
$('#' + elementId).addClass('unset');
}
} else {
if (response[keyName] && response[keyName] != '') {
$('#' + elementId).html(response[keyName]);
$('#' + elementId).addClass('set');
} else {
$('#' + elementId).html('Not configured in .env');
$('#' + elementId).addClass('unset');
}
}
};
});

7
static/index.css Normal file
View File

@@ -0,0 +1,7 @@
.config-value.set {
color:seagreen;
}
.config-value.unset {
color:darkred;
}

74
static/index.html Normal file
View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Twilio Server Starter Kit</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="container">
<h1>Twilio Server Starter Kit Environment Setup</h1>
<h2>Account Information</h2>
<table class="table table-striped">
<tr>
<td class="config-key">TWILIO_ACCOUNT_SID</td>
<td class="config-value" id="twilioAccountSID"></td>
</tr>
<tr>
<td class="config-key">TWILIO_API_KEY</td>
<td class="config-value" id="twilioAPIKey"></td>
</tr>
<tr>
<td class="config-key">TWILIO_API_SECRET</td>
<td class="config-value" id="twilioAPISecret"></td>
</tr>
</table>
<h2>Products</h2>
<table class="table table-striped">
<tr>
<td class="config-product">Video</td>
<td class="config-key">TWILIO_CONFIGURATION_SID</td>
<td class="config-value" id="twilioConfigurationSID"></td>
</tr>
<tr>
<td class="config-product">Notify</td>
<td class="config-key">TWILIO_NOTIFICATION_SERVICE_SID</td>
<td class="config-value" id="twilioNotificationServiceSID"></td>
</tr>
<tr>
<td class="config-product">Notify</td>
<td class="config-key">TWILIO_APN_CREDENTIAL_SID</td>
<td class="config-value" id="twilioAPNCredentialSID"></td>
</tr>
<tr>
<td class="config-product">Notify</td>
<td class="config-key">TWILIO_GCM_CREDENTIAL_SID</td>
<td class="config-value" id="twilioGCMCredentialSID"></td>
</tr>
<tr>
<td class="config-product">IP Messaging</td>
<td class="config-key">TWILIO_IPM_SERVICE_SID</td>
<td class="config-value" id="twilioIPMServiceSID"></td>
</tr>
<tr>
<td class="config-product">Sync</td>
<td class="config-key">TWILIO_SYNC_SERVICE_SID</td>
<td class="config-value" id="twilioSyncServiceSID"></td>
</tr>
</table>
<h1>Demos</h1>
<a id="videoDemoButton" class="btn btn-lg" href="/video/">Video</a>
<a id="syncDemoButton" class="btn btn-lg" href="/sync/">Sync</a>
<a id="notifyDemoButton" class="btn btn-lg" href="/notify/">Notify</a>
<a id="ipmDemoButton" class="btn btn-lg" href="/ipmessaging/">IP Messaging</a>
</div> <!-- container -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="/config-check.js"></script>
</body>
</script>

90
static/ipmessaging/index.css Executable file
View File

@@ -0,0 +1,90 @@
* {
box-sizing:border-box;
}
html, body {
padding:0;
margin:0;
height:100%;
width:100%;
color:#dedede;
background-color: #849091;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
header {
width:100%;
position:absolute;
text-align:center;
bottom:20px;
}
header a, header a:visited {
font-size:18px;
color:#dedede;
text-decoration:none;
}
header a:hover {
text-decoration:underline;
}
section {
height:70%;
background-color:#2B2B2A;
}
section input {
display:block;
height:52px;
width:800px;
margin:10px auto;
outline:none;
background-color:transparent;
border:none;
border-bottom:1px solid #2B2B2A;
padding:0;
font-size:42px;
color:#eee;
}
#messages {
background-color:#232323;
padding:10px;
height:100%;
width:800px;
margin:0 auto;
overflow-y:auto;
}
#messages p {
margin:5px 0;
padding:0;
}
.info {
margin:5px 0;
font-style:italic;
}
.message-container {
margin:5px 0;
color:#fff;
}
.message-container .username {
display:inline-block;
margin-right:5px;
font-weight:bold;
color:#849091;
}
.me, .username.me {
font-weight:bold;
color:cyan;
}
.message-container .username.me {
display:inline-block;
margin-right:5px;
}

27
static/ipmessaging/index.html Executable file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Twilio IP Messaging Quickstart</title>
<link rel="shortcut icon" href="//www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<header>
<a href="https://www.twilio.com/docs/api/ip-messaging/guides/quickstart-js"
target="_blank">Read the getting started guide
<i class="fa fa-fw fa-external-link"></i>
</a>
</header>
<section>
<div id="messages"></div>
<input id="chat-input" type="text" placeholder="say anything" autofocus/>
</section>
<script src="//media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
<script src="//media.twiliocdn.com/sdk/js/ip-messaging/v0.10/twilio-ip-messaging.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="index.js"></script>
</body>
</html>

108
static/ipmessaging/index.js Executable file
View File

@@ -0,0 +1,108 @@
$(function() {
// Get handle to the chat div
var $chatWindow = $('#messages');
// Manages the state of our access token we got from the server
var accessManager;
// Our interface to the IP Messaging service
var messagingClient;
// A handle to the "general" chat channel - the one and only channel we
// will have in this sample app
var generalChannel;
// The server will assign the client a random username - store that value
// here
var username;
// Helper function to print info messages to the chat window
function print(infoMessage, asHtml) {
var $msg = $('<div class="info">');
if (asHtml) {
$msg.html(infoMessage);
} else {
$msg.text(infoMessage);
}
$chatWindow.append($msg);
}
// Helper function to print chat message to the chat window
function printMessage(fromUser, message) {
var $user = $('<span class="username">').text(fromUser + ':');
if (fromUser === username) {
$user.addClass('me');
}
var $message = $('<span class="message">').text(message);
var $container = $('<div class="message-container">');
$container.append($user).append($message);
$chatWindow.append($container);
$chatWindow.scrollTop($chatWindow[0].scrollHeight);
}
// Alert the user they have been assigned a random username
print('Logging in...');
// Get an access token for the current user, passing a username (identity)
// and a device ID - for browser-based apps, we'll always just use the
// value "browser"
$.getJSON('/token', {
device: 'browser'
}, function(data) {
// Alert the user they have been assigned a random username
username = data.identity;
print('You have been assigned a random username of: '
+ '<span class="me">' + username + '</span>', true);
// Initialize the IP messaging client
accessManager = new Twilio.AccessManager(data.token);
messagingClient = new Twilio.IPMessaging.Client(accessManager);
// Get the general chat channel, which is where all the messages are
// sent in this simple application
print('Attempting to join "general" chat channel...');
var promise = messagingClient.getChannelByUniqueName('general');
promise.then(function(channel) {
generalChannel = channel;
if (!generalChannel) {
// If it doesn't exist, let's create it
messagingClient.createChannel({
uniqueName: 'general',
friendlyName: 'General Chat Channel'
}).then(function(channel) {
console.log('Created general channel:');
console.log(channel);
generalChannel = channel;
setupChannel();
});
} else {
console.log('Found general channel:');
console.log(generalChannel);
setupChannel();
}
});
});
// Set up channel after it has been found
function setupChannel() {
// Join the general channel
generalChannel.join().then(function(channel) {
print('Joined channel as '
+ '<span class="me">' + username + '</span>.', true);
});
// Listen for new messages sent to the channel
generalChannel.on('messageAdded', function(message) {
printMessage(message.author, message.body);
});
}
// Send a new message to the general channel
var $input = $('#chat-input');
$input.on('keydown', function(e) {
if (e.keyCode == 13) {
generalChannel.sendMessage($input.val())
$input.val('');
}
});
});

35
static/notify/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Hello App! - Notify Quickstart</title>
<link rel="shortcut icon" href="//www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="notify.css">
</head>
<body>
<header>
<a href="https://www.twilio.com/docs/api/notifications"
target="_blank">
Read the Twilio Notify guide
<i class="fa fa-fw fa-external-link"></i>
</a>
</header>
<section>
<h1>Send Notification</h1>
<input type="text" id="identityInput" size="30"/>
<p/>
<input type="submit" id="sendNotificationButton" value="Send Notification"/>
<div id="message">
Welcome to Notify!
</div>
<p>After you set up a notification binding, go ahead and send a notification to that identity!</p>
</section>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="notify.js"></script>
</body>
</html>

40
static/notify/notify.css Normal file
View File

@@ -0,0 +1,40 @@
* {
box-sizing:border-box;
}
html, body {
padding:0;
margin:0;
height:100%;
width:100%;
color:#dedede;
background-color: #849091;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
header {
width:100%;
position:absolute;
text-align:center;
bottom:20px;
}
header a, header a:visited {
font-size:18px;
color:#dedede;
text-decoration:none;
}
header a:hover {
text-decoration:underline;
}
section {
background-color:#2B2B2A;
text-align:center;
padding:16px;
}
#message {
padding:6px;
}

12
static/notify/notify.js Normal file
View File

@@ -0,0 +1,12 @@
$(function() {
$('#sendNotificationButton').on('click', function() {
$.post('/send-notification', {
identity: $('#identityInput').val()
}, function(response) {
$('#identityInput').val('');
$('#message').html(response.message);
console.log(response);
});
});
});

63
static/sync/index.css Executable file
View File

@@ -0,0 +1,63 @@
* {
box-sizing:border-box;
}
html, body {
padding:0;
margin:0;
height:100%;
width:100%;
color:#dedede;
background-color: #849091;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
header {
width:100%;
position:absolute;
text-align:center;
bottom:20px;
}
header a, header a:visited {
font-size:18px;
color:#dedede;
text-decoration:none;
}
header a:hover {
text-decoration:underline;
}
section {
background-color:#2B2B2A;
text-align:center;
padding:16px;
}
button:hover {
cursor:pointer;
background-color:#000;
color:#fff;
}
#message {
padding:6px;
}
#board {
width: 33%;
margin-left: auto;
margin-right: auto;
}
#board .board-row {
width: 100%;
padding-bottom: 3px;
}
#board .board-row button {
width: 30%;
height: 100px;
font-size: 50px;
}

51
static/sync/index.html Executable file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Tic-Tac-Twilio - Sync Quickstart</title>
<link rel="shortcut icon" href="//www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<header>
<a href="https://www.twilio.com/docs/api/sync/quickstart-js"
target="_blank">
Read the getting started guide
<i class="fa fa-fw fa-external-link"></i>
</a>
</header>
<section>
<h1>Tic-Tac-Twilio</h1>
<div id="board">
<div class="board-row">
<button type="button" data-row="0" data-col="0" disabled>&nbsp;</button>
<button type="button" data-row="0" data-col="1" disabled>&nbsp;</button>
<button type="button" data-row="0" data-col="2" disabled>&nbsp;</button>
</div>
<div class="board-row">
<button type="button" data-row="1" data-col="0" disabled>&nbsp;</button>
<button type="button" data-row="1" data-col="1" disabled>&nbsp;</button>
<button type="button" data-row="1" data-col="2" disabled>&nbsp;</button>
</div>
<div class="board-row">
<button type="button" data-row="2" data-col="0" disabled>&nbsp;</button>
<button type="button" data-row="2" data-col="1" disabled>&nbsp;</button>
<button type="button" data-row="2" data-col="2" disabled>&nbsp;</button>
</div>
</div>
<div id="message">
Welcome! Initializing Sync...
</div>
<p>Open this page in a few tabs to test!</p>
</section>
<script src="//media.twiliocdn.com/sdk/js/sync/v0.3/twilio-sync.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="index.js"></script>
</body>
</html>

123
static/sync/index.js Executable file
View File

@@ -0,0 +1,123 @@
$(function () {
//We'll use message to tell the user what's happening
var $message = $('#message');
//Get handle to the game board buttons
var $buttons = $('#board .board-row button');
//Manages the state of our access token we got from the server
var accessManager;
//Our interface to the Sync service
var syncClient;
//We're going to use a single Sync document, our simplest
//synchronisation primitive, for this demo
var syncDoc;
//Get an access token for the current user, passing a device ID
//In browser-based apps, every tab is like its own unique device
//synchronizing state -- so we'll use a random UUID to identify
//this tab.
$.getJSON('/token', {
device: getDeviceId()
}, function (tokenResponse) {
//Initialize the Sync client
syncClient = new Twilio.Sync.Client(tokenResponse.token);
//Let's pop a message on the screen to show that Sync is ready
$message.html('Sync initialized!');
//Now that Sync is active, lets enable our game board
$buttons.attr('disabled', false);
//This code will create and/or open a Sync document
//Note the use of promises
syncClient.document('SyncGame').then(function(doc) {
//Lets store it in our global variable
syncDoc = doc;
//Initialize game board UI to current state (if it exists)
var data = syncDoc.get();
if (data.board) {
updateUserInterface(data);
}
//Let's subscribe to changes on this document, so when something
//changes on this document, we can trigger our UI to update
syncDoc.on('updated', updateUserInterface);
});
});
//Whenever a board button is clicked:
$buttons.on('click', function (e) {
//Toggle the value: X, O, or empty
toggleCellValue($(e.target));
//Update the document
var data = readGameBoardFromUserInterface();
//Send updated document to Sync
//This should trigger "updated" events on other clients
syncDoc.set(data);
});
//Toggle the value: X, O, or empty (&nbsp; for UI)
function toggleCellValue($cell) {
var cellValue = $cell.html();
if (cellValue === 'X') {
$cell.html('O');
} else if (cellValue === 'O') {
$cell.html('&nbsp;');
} else {
$cell.html('X');
}
}
//Generate random UUID to identify this browser tab
//For a more robust solution consider a library like
//fingerprintjs2: https://github.com/Valve/fingerprintjs2
function getDeviceId() {
return 'browser-' +
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
//Read the state of the UI and create a new document
function readGameBoardFromUserInterface() {
var board = [
['', '', ''],
['', '', ''],
['', '', '']
];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var selector = '[data-row="' + row + '"]' +
'[data-col="' + col + '"]';
board[row][col] = $(selector).html().replace('&nbsp;', '');
}
}
return {board: board};
}
//Update the buttons on the board to match our document
function updateUserInterface(data) {
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var selector = '[data-row="' + row + '"]' +
'[data-col="' + col + '"]';
var cellValue = data.board[row][col];
$(selector).html(cellValue === '' ? '&nbsp;' : cellValue);
}
}
}
});

29
static/video/index.html Executable file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Twilio Video - Video Quickstart</title>
<link rel="stylesheet" href="site.css">
</head>
<body>
<div id="remote-media"></div>
<div id="controls">
<div id="preview">
<p class="instructions">Hello Beautiful</p>
<div id="local-media"></div>
<button id="button-preview">Preview My Camera</button>
</div>
<div id="room-controls">
<p class="instructions">Room Name:</p>
<input id="room-name" type="text" placeholder="Enter a room name" />
<button id="button-join">Join Room</button>
<button id="button-leave">Leave Room</button>
</div>
<div id="log"></div>
</div>
<script src="//media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
<script src="//media.twiliocdn.com/sdk/js/video/releases/1.0.0-beta2/twilio-video.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="quickstart.js"></script>
</body>
</html>

120
static/video/quickstart.js Executable file
View File

@@ -0,0 +1,120 @@
var videoClient;
var activeRoom;
var previewMedia;
var identity;
var roomName;
// Check for WebRTC
if (!navigator.webkitGetUserMedia && !navigator.mozGetUserMedia) {
alert('WebRTC is not available in your browser.');
}
// When we are about to transition away from this page, disconnect
// from the room, if joined.
window.addEventListener('beforeunload', leaveRoomIfJoined);
$.getJSON('/token', function (data) {
identity = data.identity;
// Create a Conversations Client and connect to Twilio
videoClient = new Twilio.Video.Client(data.token);
document.getElementById('room-controls').style.display = 'block';
// Bind button to join room
document.getElementById('button-join').onclick = function () {
roomName = document.getElementById('room-name').value;
if (roomName) {
log("Joining room '" + roomName + "'...");
videoClient.connect({ to: roomName}).then(roomJoined,
function(error) {
log('Could not connect to Twilio: ' + error.message);
});
} else {
alert('Please enter a room name.');
}
};
// Bind button to leave room
document.getElementById('button-leave').onclick = function () {
log('Leaving room...');
activeRoom.disconnect();
};
});
// Successfully connected!
function roomJoined(room) {
activeRoom = room;
log("Joined as '" + identity + "'");
document.getElementById('button-join').style.display = 'none';
document.getElementById('button-leave').style.display = 'inline';
// Draw local video, if not already previewing
if (!previewMedia) {
room.localParticipant.media.attach('#local-media');
}
room.participants.forEach(function(participant) {
log("Already in Room: '" + participant.identity + "'");
participant.media.attach('#remote-media');
});
// When a participant joins, draw their video on screen
room.on('participantConnected', function (participant) {
log("Joining: '" + participant.identity + "'");
participant.media.attach('#remote-media');
participant.on('disconnected', function (participant) {
log("Participant '" + participant.identity + "' left the room");
});
});
// When a participant disconnects, note in log
room.on('participantDisconnected', function (participant) {
log("Participant '" + participant.identity + "' left the room");
participant.media.detach();
});
// When the conversation ends, stop capturing local video
// Also remove media for all remote participants
room.on('disconnected', function () {
log('Left');
room.localParticipant.media.detach();
room.participants.forEach(function(participant) {
participant.media.detach();
});
activeRoom = null;
document.getElementById('button-join').style.display = 'inline';
document.getElementById('button-leave').style.display = 'none';
});
}
// Local video preview
document.getElementById('button-preview').onclick = function () {
if (!previewMedia) {
previewMedia = new Twilio.Video.LocalMedia();
Twilio.Video.getUserMedia().then(
function (mediaStream) {
previewMedia.addStream(mediaStream);
previewMedia.attach('#local-media');
},
function (error) {
console.error('Unable to access local media', error);
log('Unable to access Camera and Microphone');
});
};
};
// Activity log
function log(message) {
var logDiv = document.getElementById('log');
logDiv.innerHTML += '<p>&gt;&nbsp;' + message + '</p>';
logDiv.scrollTop = logDiv.scrollHeight;
}
function leaveRoomIfJoined() {
if (activeRoom) {
activeRoom.disconnect();
}
}

144
static/video/site.css Executable file
View File

@@ -0,0 +1,144 @@
@import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono);
body,
p {
padding: 0;
margin: 0;
}
body {
background: #272726;
}
div#remote-media {
height: 43%;
width: 100%;
background-color: #fff;
text-align: center;
margin: auto;
}
div#remote-media video {
border: 1px solid #272726;
margin: 3em 2em;
height: 70%;
max-width: 27% !important;
background-color: #272726;
background-repeat: no-repeat;
}
div#controls {
padding: 3em;
max-width: 1200px;
margin: 0 auto;
}
div#controls div {
float: left;
}
div#controls div#room-controls,
div#controls div#preview {
width: 16em;
margin: 0 1.5em;
text-align: center;
}
div#controls p.instructions {
text-align: left;
margin-bottom: 1em;
font-family: Helvetica-LightOblique, Helvetica, sans-serif;
font-style: oblique;
font-size: 1.25em;
color: #777776;
}
div#controls button {
width: 15em;
height: 2.5em;
margin-top: 1.75em;
border-radius: 1em;
font-family: "Helvetica Light", Helvetica, sans-serif;
font-size: .8em;
font-weight: lighter;
outline: 0;
}
div#controls div#room-controls input {
font-family: Helvetica-LightOblique, Helvetica, sans-serif;
font-style: oblique;
font-size: 1em;
}
div#controls button:active {
position: relative;
top: 1px;
}
div#controls div#preview div#local-media {
width: 270px;
height: 202px;
border: 1px solid #cececc;
box-sizing: border-box;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjgwcHgiIGhlaWdodD0iODBweCIgdmlld0JveD0iMCAwIDgwIDgwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjEgKDEyMDAyKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5GaWxsIDUxICsgRmlsbCA1MjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPgogICAgICAgIDxnIGlkPSJjdW1tYWNrIiBza2V0Y2g6dHlwZT0iTVNMYXllckdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTU5LjAwMDAwMCwgLTE3NDYuMDAwMDAwKSIgZmlsbD0iI0ZGRkZGRiI+CiAgICAgICAgICAgIDxnIGlkPSJGaWxsLTUxLSstRmlsbC01MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTU5LjAwMDAwMCwgMTc0Ni4wMDAwMDApIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0zOS42ODYsMC43MyBDMTcuODUsMC43MyAwLjA4NSwxOC41IDAuMDg1LDQwLjMzIEMwLjA4NSw2Mi4xNyAxNy44NSw3OS45MyAzOS42ODYsNzkuOTMgQzYxLjUyMiw3OS45MyA3OS4yODcsNjIuMTcgNzkuMjg3LDQwLjMzIEM3OS4yODcsMTguNSA2MS41MjIsMC43MyAzOS42ODYsMC43MyBMMzkuNjg2LDAuNzMgWiBNMzkuNjg2LDEuNzMgQzYxLjAwNSwxLjczIDc4LjI4NywxOS4wMiA3OC4yODcsNDAuMzMgQzc4LjI4Nyw2MS42NSA2MS4wMDUsNzguOTMgMzkuNjg2LDc4LjkzIEMxOC4zNjcsNzguOTMgMS4wODUsNjEuNjUgMS4wODUsNDAuMzMgQzEuMDg1LDE5LjAyIDE4LjM2NywxLjczIDM5LjY4NiwxLjczIEwzOS42ODYsMS43MyBaIiBpZD0iRmlsbC01MSI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTQ3Ljk2LDUzLjMzNSBMNDcuOTYsNTIuODM1IEwyMC4wOTMsNTIuODM1IEwyMC4wOTMsMjcuODI1IEw0Ny40NiwyNy44MjUgTDQ3LjQ2LDM4LjI1NSBMNTkuMjc5LDMwLjgwNSBMNTkuMjc5LDQ5Ljg1NSBMNDcuNDYsNDIuNDA1IEw0Ny40Niw1My4zMzUgTDQ3Ljk2LDUzLjMzNSBMNDcuOTYsNTIuODM1IEw0Ny45Niw1My4zMzUgTDQ4LjQ2LDUzLjMzNSBMNDguNDYsNDQuMjE1IEw2MC4yNzksNTEuNjY1IEw2MC4yNzksMjguOTk1IEw0OC40NiwzNi40NDUgTDQ4LjQ2LDI2LjgyNSBMMTkuMDkzLDI2LjgyNSBMMTkuMDkzLDUzLjgzNSBMNDguNDYsNTMuODM1IEw0OC40Niw1My4zMzUgTDQ3Ljk2LDUzLjMzNSIgaWQ9IkZpbGwtNTIiPjwvcGF0aD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+);
background-position: center;
background-repeat: no-repeat;
margin: 0 auto;
}
div#controls div#preview div#local-media video {
max-width: 100%;
max-height: 100%;
border: none;
}
div#controls div#preview button#button-preview {
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjE3cHgiIGhlaWdodD0iMTJweCIgdmlld0JveD0iMCAwIDE3IDEyIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjEgKDEyMDAyKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5GaWxsIDM0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9ImN1bW1hY2siIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjUuMDAwMDAwLCAtMTkwOS4wMDAwMDApIiBmaWxsPSIjMEEwQjA5Ij4KICAgICAgICAgICAgPHBhdGggZD0iTTEzNi40NzEsMTkxOS44NyBMMTM2LjQ3MSwxOTE5LjYyIEwxMjUuNzY3LDE5MTkuNjIgTDEyNS43NjcsMTkxMC4wOCBMMTM2LjIyMSwxOTEwLjA4IEwxMzYuMjIxLDE5MTQuMTUgTDE0MC43ODUsMTkxMS4yNyBMMTQwLjc4NSwxOTE4LjQyIEwxMzYuMjIxLDE5MTUuNTUgTDEzNi4yMjEsMTkxOS44NyBMMTM2LjQ3MSwxOTE5Ljg3IEwxMzYuNDcxLDE5MTkuNjIgTDEzNi40NzEsMTkxOS44NyBMMTM2LjcyMSwxOTE5Ljg3IEwxMzYuNzIxLDE5MTYuNDUgTDE0MS4yODUsMTkxOS4zMyBMMTQxLjI4NSwxOTEwLjM3IEwxMzYuNzIxLDE5MTMuMjQgTDEzNi43MjEsMTkwOS41OCBMMTI1LjI2NywxOTA5LjU4IEwxMjUuMjY3LDE5MjAuMTIgTDEzNi43MjEsMTkyMC4xMiBMMTM2LjcyMSwxOTE5Ljg3IEwxMzYuNDcxLDE5MTkuODciIGlkPSJGaWxsLTM0IiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4=)1em center no-repeat #fff;
border: none;
padding-left: 1.5em;
}
div#controls div#log {
border: 1px solid #686865;
}
div#controls div#room-controls {
display: none;
}
div#controls div#room-controls input {
width: 100%;
height: 2.5em;
padding: .5em;
display: block;
}
div#controls div#room-controls button {
color: #fff;
background: 0 0;
border: 1px solid #686865;
}
div#controls div#room-controls button#button-leave {
display: none;
}
div#controls div#log {
width: 35%;
height: 9.5em;
margin-top: 2.75em;
text-align: left;
padding: 1.5em;
float: right;
overflow-y: scroll;
}
div#controls div#log p {
color: #686865;
font-family: 'Share Tech Mono', 'Courier New', Courier, fixed-width;
font-size: 1.25em;
line-height: 1.25em;
margin-left: 1em;
text-indent: -1.25em;
width: 90%;
}