2
3
Amazon API Gateway
Web Application
1
Comunicación a tiempo real
5
Los Hombres Lobo
4
Alexa Skill
6
Among us
CÓDIGO
❌ Esperar a una interactuación del usuario para informarle del estado del juego
✅ Mantener a los jugadores informados a tiempo real
Alexa no puede recibir información a tiempo real... 👎🏻
Pero sí podemos hacer que los usuarios la reciban 👍🏻
❌ Polling HTTP unidireccional
GET
GET
✅ WebSocket bidireccional
CONNECT
joinRoom
WS
...
HTTP
getRoom
HTTP
startGame
HTTP
⚠️
1
2
3
Launch
+
Login
Start
2
3
1
getRoom
HTTP
WS
joinRoom
HTTP
startGame
joinRoom
HTTP
joinRoomSuccess / newPlayer
WS
startGame
WS
startGame
WS
# serverless.yml
service: my-api-rest
custom:
ROOMS_TABLE: "playing_rooms"
provider:
name: aws
runtime: nodejs12.x
stage: dev
region: eu-west-1
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "*"
environment:
ROOMS_TABLE: ${self:custom.ROOMS_TABLE}
functions:
app:
handler: app.handler
events:
- http: ANY /
- http: 'ANY {proxy+}'
resources:
Resources:
RoomsDynamoDBTable:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
-
AttributeName: room
AttributeType: S
KeySchema:
-
AttributeName: room
KeyType: HASH
BillingMode: PAY_PER_REQUEST
TableName: ${self:custom.ROOMS_TABLE}
ROOMS_TABLE
{
room: "WGFTPH",
players: [
{
name: "Pablo",
role: "impostor"
}
...
]
}
// app.js
const serverless = require('serverless-http');
const express = require('express');
const routes = require('./routes/index');
const app = express();
app.use(express.json({ strict: false }));
app.use('/', routes);
module.exports.handler = serverless(app);
// routes/index.js
const express = require('express');
const router = express.Router();
const alexaController = require('../controllers/alexa_controller');
const webController = require('../controllers/web_controller');
router.route('/room')
.post(alexaController.getRoom);
router.route('/join/:room')
.put(webController.joinRoom);
router.route('/start/:room')
.post(alexaController.startGame);
getRoom
startGame
joinRoom
// controllers/alexa_controller.js
const AWS = require('aws-sdk');
const db = process.env.ROOMS_TABLE;
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.getRoom = async (req, res) => {
const room = roomGenerator();
const params = {
TableName: db,
Item: {
room: room,
players: [],
},
};
try {
await dynamoDb.put(params).promise();
res.status(200).json({ room });
} catch (error) {
res.status(500).json({ error });
}
}
getRoom
{
room: "WGFTPH",
players: []
}
// controllers/web_controller.js
const AWS = require('aws-sdk');
const db = process.env.ROOMS_TABLE;
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.joinRoom = async (req, res) => {
const { room } = req.params;
const { name } = req.body;
const params = {
TableName: db,
Key: {
room: room,
},
UpdateExpression: 'SET #players = list_append(#players, :attrValue)',
ExpressionAttributeNames: {
'#players': 'players',
},
ExpressionAttributeValues: {
':attrValue': [
{
name: name,
role: '',
},
],
},
};
try {
await dynamoDb.update(params).promise();
res.status(200).json({ room, name, players });
} catch (error) {
res.status(500).json({ error });
}
}
joinRoom
{
room: "WGFTPH",
players: [
{
name: "Pablo",
role: ""
}
...
]
}
// controllers/alexa_controller.js
const AWS = require('aws-sdk');
const db = process.env.ROOMS_TABLE;
const dynamoDb = new AWS.DynamoDB.DocumentClient();
const WebSocket = require('ws');
const WS_URL = 'wss://6a412fvzju.execute-api.eu-west-1.amazonaws.com/dev';
let ws = new WebSocket(WS_URL);
module.exports.startGame = async (req, res) => {
const { room } = req.params;
let params = {
TableName: db,
Key: {
room: room,
},
};
try {
const result = await dynamoDb.get(params).promise();
let { players } = result.Item;
players = rolesGenerator(players);
params = {
TableName: db,
Key: {
room: room,
},
UpdateExpression: 'SET #players=:players',
ExpressionAttributeNames: {
'#players': 'players',
},
ExpressionAttributeValues: {
':players': players,
},
};
await dynamoDb.update(params).promise();
await ws.send(
JSON.stringify({
action: 'startGame',
room,
players,
})
);
res.status(200).json({ players });
} catch (error) {
res.status(500).json({ error });
}
}
startGame
{
room: "WGFTPH",
players: [
{
name: "Pablo",
role: "impostor"
}
...
]
}
➡️ WSS startGame
# serverless.yml
service: my-api-ws
custom:
CONNECTIONS_TABLE: "connections"
provider:
name: aws
runtime: nodejs12.x
stage: dev
region: eu-west-1
websocketsApiName: my-api-ws
websocketApiRouteSelectionExpression: $request.body.action
iamRoleStatements:
- Effect: Allow
Action:
- "execute-api:ManageConnections"
Resource:
- "arn:aws:execute-api:*:*:**/@connections/*"
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "*"
environment:
CONNECTIONS_TABLE: ${self:custom.CONNECTIONS_TABLE}
functions:
connectionHandler:
handler: app.connection
events:
- websocket:
route: $connect
- websocket:
route: $disconnect
joinRoomHandler:
handler: app.joinRoom
events:
- websocket:
route: joinRoom
startGameHandler:
handler: app.startGame
events:
- websocket:
route: startGame
defaultHandler:
handler: app.default
events:
- websocket:
route: $default
resources:
Resources:
ConnectionsDynamoDBTable:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
-
AttributeName: connectionId
AttributeType: S
KeySchema:
-
AttributeName: connectionId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
TableName: ${self:custom.CONNECTIONS_TABLE}
websocketApiRouteSelectionExpression
CONNECTIONS_TABLE
{
connectionId: "YfYBXeeqiGYCEXg=",
room: "WGFTPH"
}
# serverless.yml
service: my-api-ws
custom:
CONNECTIONS_TABLE: "connections"
provider:
name: aws
runtime: nodejs12.x
stage: dev
region: eu-west-1
websocketsApiName: my-api-ws
websocketApiRouteSelectionExpression: $request.body.action
iamRoleStatements:
- Effect: Allow
Action:
- "execute-api:ManageConnections"
Resource:
- "arn:aws:execute-api:*:*:**/@connections/*"
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "*"
environment:
CONNECTIONS_TABLE: ${self:custom.CONNECTIONS_TABLE}
functions:
connectionHandler:
handler: app.connection
events:
- websocket:
route: $connect
- websocket:
route: $disconnect
joinRoomHandler:
handler: app.joinRoom
events:
- websocket:
route: joinRoom
startGameHandler:
handler: app.startGame
events:
- websocket:
route: startGame
defaultHandler:
handler: app.default
events:
- websocket:
route: $default
resources:
Resources:
ConnectionsDynamoDBTable:
Type: 'AWS::DynamoDB::Table'
Properties:
AttributeDefinitions:
-
AttributeName: connectionId
AttributeType: S
KeySchema:
-
AttributeName: connectionId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
TableName: ${self:custom.CONNECTIONS_TABLE}
websocketApiRouteSelectionExpression
$connect
$disconnect
$default
CONNECTIONS_TABLE
{
connectionId: "YfYBXeeqiGYCEXg=",
room: "WGFTPH"
}
// app.js
const dynamo = require('db_utils.js');
module.exports.connection = async (event, context, callback) => {
const { connectionId } = event.requestContext;
if (event.requestContext.eventType === 'CONNECT') {
try {
await dynamo.addConnection(connectionId);
callback(null, { statusCode: 200, body: 'OK' });
} catch(error) {
callback(null, { statusCode: 500, body: JSON.stringify(error) });
}
} else if (event.requestContext.eventType === 'DISCONNECT') {
try {
await dynamo.deleteConnection(connectionId);
callback(null, { statusCode: 200, body: 'OK' });
} catch(error) {
callback(null, { statusCode: 500, body: JSON.stringify(error) });
}
}
}
$connect
{
connectionId: "YfYBXeeqiGYCEXg=",
room: ""
}
$disconnect
// app.js
const dynamo = require('db_utils.js');
const ws = require('ws_utils.js');
const axios = require('axios');
const HTTP_URL = 'https://wjky5nj124.execute-api.eu-west-1.amazonaws.com/dev';
module.exports.joinRoom = async (event, context, callback) => {
const { connectionId } = event.requestContext;
const body = JSON.parse(event.body);
const { room, name } = body;
try {
await dynamo.join(connectionId, room);
await axios.put(`${HTTP_URL}/join/${room}`, { name });
await ws.sendMessageToSocket(connectionId, 'joinRoomSuccess', event);
await ws.sendMessageToRoom(room, 'newPlayer', event);
} catch(error) {
callback({ statusCode: 500, body: JSON.stringify(error) });
}
}
joinRoom
{
connectionId: "YfYBXeeqiGYCEXg=",
room: "WGFTPH"
}
➡️ WSS joinRoomSuccess
➡️ WSS newPlayer
➡️ HTTP /join/:room
// app.js
const ws = require('ws_utils.js');
module.exports.startGame = async (event, context, callback) => {
const body = JSON.parse(event.body);
const { room } = body.room;
try {
await ws.sendMessageToRoom(room, 'startGame', event);
} catch(error) {
callback({ statusCode: 500, body: JSON.stringify(error) });
}
}
startGame
➡️ WSS startGame
// ws_utils.js
const dynamo = require('db_utils.js');
const AWS = require('aws-sdk');
const send = async (connectionId, action, event) => {
const body = JSON.parse(event.body);
body.action = action;
const { domainName, stage } = event.requestContext;
const ws = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: `${domainName}/${stage}`;
});
const params = {
ConnectionId: connectionId,
Data: JSON.stringify(body)
};
return await ws.postToConnection(params).promise();
}
module.exports.sendMessageToSocket = async (connectionId, action, event) => {
return await send(connectionId, action, event);
}
module.exports.sendMessageToRoom = async (room, action, event) => {
const players = await dynamo.getPlayersInRoom(room);
return await Promise.all(players.Items.map(async (item) => {
return await send(item.connectionId, action, event);
}));
}
// db_utils.js
const AWS = require('aws-sdk');
const db = process.env.CONNECTIONS_TABLE;
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.addConnection = async (connectionId) => {
const params = {
TableName: db,
Item : {
connectionId: connectionId,
room: ''
}
};
return await dynamoDb.put(params).promise();
}
module.exports.deleteConnection = async (connectionId) => {
const params = {
TableName: db,
Key : {
connectionId: connectionId
}
};
return await dynamoDb.delete(params).promise();
}
module.exports.join = async (connectionId, room) => {
const params = {
TableName: db,
Key : {
connectionId: connectionId
},
UpdateExpression: 'SET room=:room',
ExpressionAttributeValues: {
':room': room
},
ReturnValues: 'ALL_NEW'
};
return await dynamoDb.update(params).promise();
}
module.exports.getPlayersInRoom = async (room) => {
const params = {
TableName: db,
FilterExpression: 'room=:room',
ExpressionAttributeValues: {
':room': room
},
ProjectionExpression: 'connectionId'
};
return await dynamoDb.scan(params).promise();
}
const WS_URL = 'wss://6a412fvzju.execute-api.eu-west-1.amazonaws.com/dev';
const ws = new WebSocket(WS_URL);
HTML5 ➡️ WebSocket API
ws.onopen = () => {
ws.send(JSON.stringify(
{
action: 'joinRoom',
room: [ROOM],
name: [PLAYER_NAME]
}
));
}
Envío de mensajes 📨
onopen + send
Recepción de mensajes 📨
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.action) {
case 'joinRoomSuccess':
localStorage.setItem('room', data.room);
localStorage.setItem('name', data.name);
console.log(`You've been successfully registered in room: ${data.room}`);
break;
case 'newPlayer':
console.log(`New player: ${data.name} in room: ${data.room}`);
break;
case 'startGame':
const name = localStorage.getItem('name');
const { role } = data.players.find((player) => player.name === name);
localStorage.setItem('role', role);
console.log(`Ready to play! You are a ${role}`);
break;
default:
break;
}
};
onmessage
Cerrar la conexión
ws.close()
const Alexa = require('ask-sdk-core');
const axios = require('axios');
const HTTP_URL = 'https://wjky5nj124.execute-api.eu-west-1.amazonaws.com/dev';
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'
},
async handle(handlerInput) {
const requestAttributes = handlerInput.attributesManager.getRequestAttributes();
const result = await axios.post(`${HTTP_URL}/room`, null);
const { room } = result;
const speechText = requestAttributes.t('WELCOME_MSG', { room });
const repromptText = requestAttributes.t('WELCOME_REPROMPT_MSG');
handlerInput.attributesManager.setSessionAttributes({ room });
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(repromptText)
.getResponse()
}
}
const Alexa = require('ask-sdk-core');
const axios = require('axios');
const HTTP_URL = 'https://wjky5nj124.execute-api.eu-west-1.amazonaws.com/dev';
const StartIntentHandler = {
canHandle(handlerInput) {
return (
Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StartIntent'
)
},
async handle(handlerInput) {
const requestAttributes = handlerInput.attributesManager.getRequestAttributes();
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
const speechText = requestAttributes.t('START_MSG');
const repromptText = requestAttributes.t('START_REPROMPT_MSG');
const { room } = sessionAttributes
const result = await axios.post(`${HTTP_URL}/start/${room}`, null);
// Add Dynamic Entities
const { players } = result;
let updateEntitiesDirective = {
type: 'Dialog.UpdateDynamicEntities',
updateBehavior: 'REPLACE',
types: [
{
name: 'PlayerName',
values: [] // we fill this array with the entities below
}
]
};
players.forEach((player) => updateEntitiesDirective.types[0].values.push(
{
id: player.name.replace(/\s/gi, "_"),
name: {
value: player.name
}
}
));
handlerInput.responseBuilder.addDirective(updateEntitiesDirective);
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(repromptText)
.getResponse()
}
}
✅ Entrar en la sala
❌ Matar a alguien
✅ Desvelar víctima
✅ Votar
✅ Fin del juego
➡️ "Alexa, ha sido Pablo"
➡️ "Alexa, ¿quién ha muerto?"
➡️ "Alexa, ya estamos dentro"
➡️ "Alexa, mato a Cristina"
✅ Entrar en la sala
⚠️ Hacer misiones
❌ Matar a alguien
❌ Sabotear
✅ Reportar cuerpo
✅ Pulsar botón de alarma
✅ Votar
❌ Fin del juego
➡️ "Alexa, acabo de escanearme", "Alexa, ¿cómo van las misiones?"
➡️ "Alexa, ha sido el verde"
➡️ "Alexa, llamada de emergencia"
➡️ "Alexa, he visto al amarillo muerto"
➡️ "Alexa, sabotea el oxígeno"
➡️ "Alexa, he matado al amarillo en armería"
➡️ "Alexa, ya estamos dentro"
Hacer misiones
Matar
Entrar en la nave
Sabotear
Reportar
Reportar
Alarma
Votar