From 57655d3be31d579aae4a28d3ce164bca08947bed Mon Sep 17 00:00:00 2001 From: Marc Gunn Date: Tue, 6 Oct 2015 23:54:01 -0700 Subject: [PATCH 1/6] define game api --- .editorconfig | 6 +- app/api/game.yaml | 163 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 app/api/game.yaml diff --git a/.editorconfig b/.editorconfig index 9e32b41..291897e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,4 +9,8 @@ trim_trailing_whitespace = true insert_final_newline = true [*.md] -trim_trailing_whitespace = false \ No newline at end of file +trim_trailing_whitespace = false + +[*.yaml] +indent_style = spaces +indent_size = 2 diff --git a/app/api/game.yaml b/app/api/game.yaml new file mode 100644 index 0000000..bb100b1 --- /dev/null +++ b/app/api/game.yaml @@ -0,0 +1,163 @@ +swagger: '2.0' +info: + version: 0.1.0 + title: 'Torwolf Games API' +paths: + /games: + get: + description: Finds games + parameters: + - name: offset + in: query + description: The offset at which the result set should begin. + required: false + type: integer + default: 0 + - name: limit + in: query + description: Result will contain at most limit items + required: false + type: integer + default: 20 + - name: phase + in: query + description: Games in the result set should be in the provided list of comma separated phases. + required: false + type: string + enum: ["FORMING", "STARTED", "COMPLETED"] + - name: name + in: query + description: Games should match the provided text search string + required: false + type: string + responses: + 200: + description: Successful response + schema: + $ref: '#/definitions/ResultSet' + # TODO: error middleware + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + post: + description: Creates a game + parameters: + - name: game + in: body + description: Game to create + required: true + schema: + $ref: '#/definitions/Game' + responses: + 200: + description: Successful response + schema: + $ref: '#/definitions/Game' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /games/{id}: + get: + description: Gets a game + parameters: + - name: id + in: path + description: Id of the game to get + required: true + type: number + format: i32 + responses: + 200: + description: Successful response + schema: + $ref: '#/definitions/Game' + # TODO: error middleware + 404: + description: Game was not found + schema: + $ref: '#/definitions/Error' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + +definitions: + Game: + required: + - name + - description + properties: + name: + type: string + description: The name of this game. + description: + type: string + description: A description of what to expect in this game. + victor: + type: string + description: Which side won the game. + enum: ['GOVERNMENT', 'REBELLION'] + phase: + type: string + description: Which phase the game is currently in. + enum: ['FORMING', 'STARTED', 'COMPLETED'] + startedAt: + type: string + format: date-time + description: When this game entered the 'STARTED' phase. + completedAt: + type: string + format: date-time + description: When this game entered the 'COMPLETED' phase. + createdAt: + type: string + format: date-time + description: When this game was created. + updatedAt: + type: string + format: date-time + description: When this game was last updated. + id: + type: integer + description: A unique identifier for this game. + example: + name: Example Game + description: A really good example + victor: REBELLION + phase: COMPLETED + startedAt: 2015-10-04 05:05:09.877 +00:00 + completedAt: 2015-10-04 05:05:09.877 +00:00 + createdAt: 2015-10-04 05:05:09.877 +00:00 + updatedAt: 2015-10-04 05:05:09.877 +00:00 + id: 39 + + ResultSet: + type: object + required: + - start + - totalCount + - results + properties: + start: + type: integer + format: int32 + totalCount: + type: integer + format: i32 + results: + type: array + items: + $ref: '#/definitions/Game' + + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string From 43d8fe70bd9900d712664e10601b25b195bf2cbc Mon Sep 17 00:00:00 2001 From: Marc Gunn Date: Tue, 6 Oct 2015 23:54:35 -0700 Subject: [PATCH 2/6] tests --- test/integration/gameTest.js | 163 +++++++++++++++++++++++++++++++++++ test/integration/userTest.js | 13 ++- 2 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 test/integration/gameTest.js diff --git a/test/integration/gameTest.js b/test/integration/gameTest.js new file mode 100644 index 0000000..6a24d1e --- /dev/null +++ b/test/integration/gameTest.js @@ -0,0 +1,163 @@ +var should = require('chai').should(); +var request = require('supertest'); +var moment = require('moment'); +var async = require('async'); +var _ = require('lodash'); + +var url = 'http://localhost:3000'; + +if (!global.hasOwnProperty('testApp')) { + global.testApp = require('../../server'); +} +var app = global.testApp; + +describe('Game routes', function() { + var game = undefined; + var gameTemplate = undefined; + var agent = request.agent(app.app); + + var assertGame = function(newGame, expectedGame) { + newGame.name.should.equal(expectedGame.name); + newGame.description.should.equal(expectedGame.description); + newGame.id.should.exist; + should.not.exist(newGame.victor); + newGame.phase.should.equal('FORMING'); + should.not.exist(newGame.completedAt); + should.not.exist(newGame.startedAt); + newGame.createdAt.should.exist; + moment(newGame.createdAt).format.should.not.equal('Invalid date'); + newGame.updatedAt.should.exist; + moment(newGame.updatedAt).format.should.not.equal('Invalid date'); + } + + var assertGameById = function(id, expectedGame, agent, cb) { + agent + .get('/games/' + id ) + .end(function(err, response) { + if (err) { + return cb(err); + } + response.statusCode.should.equal(200); + var game = response.body; + assertGame(game, expectedGame); + cb() + }); + } + + beforeEach(function(done) { + gameTemplate = { + name: 'Bowser\s Kingdom', + description: 'Kingdom of the Koopa' + }; + var user = { + username: 'dry bones' + Math.random(), + password: 'bowser sucks', + email: 'drybones' + Math.random() + '@koopakingdom.com' + }; + + async.waterfall([ + function(cb) { + agent + .post('/users') + .send(_.cloneDeep(user)) + .end(cb); + }, + function(response, cb) { + agent + .post('/login') + .send({email: user.email, password: user.password}) + .end(cb); + }, + function(response, cb) { + async.times(25, function(n, next) { + gameTemplate = _.cloneDeep(gameTemplate); + gameTemplate.name = 'Game ' + n; + gameTemplate.phase = (n % 2 == 0 ? 'FORMING' : 'STARTED') + agent + .post('/games') + .send(gameTemplate) + .end( function(err, response) { + if (err) { + return next(err); + } + next(); + }); + }, function(err) { + if (err) { + return cb(err); + } + cb(); + }); + } + ], done); + }); + + it('Should create games', function (done) { + agent + .post('/games') + .send(_.cloneDeep(gameTemplate)) + .end( function(err, response) { + if (err) { + return done(err); + } + response.statusCode.should.equal(200); + var game = response.body; + assertGame(game, gameTemplate); + done(); + }); + }); + + it('Should filter by phase', function(done) { + agent + .get('/games?phase=FORMING,COMPLETED') + .end( function(err, response) { + if (err) { + return done(err); + } + response.statusCode.should.equal(200); + var resultSet = response.body; + resultSet.start.should.equal(0); + resultSet.totalCount.should.exist; + resultSet.results.should.have.length(20); + resultSet.results.forEach(function(game) { + game.phase.should.equal('FORMING'); + }); + done(); + }); + }); + + it('Should page', function(done) { + agent + .get('/games?offset=3&limit=10') + .end( function(err, response) { + if (err) { + return done(err); + } + response.statusCode.should.equal(200); + var resultSet = response.body; + resultSet.start.should.equal(3); + resultSet.totalCount.should.exist; + resultSet.results.should.have.length(10); + done(); + }); + }); + + it('Should filter by name', function(done) { + agent + .get('/games?name=Ga%2515') + .end( function(err, response) { + if (err) { + return done(err); + } + response.statusCode.should.equal(200); + var resultSet = response.body; + resultSet.start.should.equal(0); + resultSet.totalCount.should.exist; + resultSet.results.should.have.length(20); + resultSet.results.forEach(function(game) { + game.name.should.equal('Game ' + 15); + }); + done(); + }); + }); +}); diff --git a/test/integration/userTest.js b/test/integration/userTest.js index f9ab04a..cd1e43a 100644 --- a/test/integration/userTest.js +++ b/test/integration/userTest.js @@ -5,7 +5,10 @@ var async = require('async'); var _ = require('lodash'); var url = 'http://localhost:3000'; -var app = require('../../server'); +if (!global.hasOwnProperty('testApp')) { + global.testApp = require('../../server'); +} +var app = global.testApp; describe('User routes', function() { var user = undefined; @@ -56,10 +59,6 @@ describe('User routes', function() { }); }); - after(function() { - app.server.close(); - }); - it('Should create users', function (done) { var userTemplate = { username: 'dry bones' + Math.random(), @@ -117,7 +116,7 @@ describe('User routes', function() { .post('/login') .send(_.cloneDeep(userTemplate)) .end(cb); - }, + }, function(response, cb) { updatedUser = { username: 'dry bones' + Math.random(), @@ -151,4 +150,4 @@ describe('User routes', function() { done(); }); }); -}); \ No newline at end of file +}); From 6446d4a29dae3c865497b5feb5885612c54299f8 Mon Sep 17 00:00:00 2001 From: Marc Gunn Date: Tue, 6 Oct 2015 23:55:01 -0700 Subject: [PATCH 3/6] game API --- app/controllers/game.server.controller.js | 65 +++++++++++++++++++++++ app/models/game.js | 23 ++++++-- app/repositories/game.js | 44 +++++++++++++++ app/routes/game.server.routes.js | 13 +++++ 4 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 app/controllers/game.server.controller.js create mode 100644 app/repositories/game.js create mode 100644 app/routes/game.server.routes.js diff --git a/app/controllers/game.server.controller.js b/app/controllers/game.server.controller.js new file mode 100644 index 0000000..2d61cb9 --- /dev/null +++ b/app/controllers/game.server.controller.js @@ -0,0 +1,65 @@ +var gameRepository = require('../repositories/game'); +var async = require('async'); + +/** + * Module dependencies. + */ + +exports.create = function(req, res, next) { + 'use strict'; + gameRepository.create(req.body, function(err, game) { + if (err) { + return next(err); + } + return res.json(game); + }); +}; + +exports.find = function(req, res, next) { + 'use strict'; + var options = { + offset: req.query.offset || 0, + limit: req.query.limit || 20, + where: { } + }; + if (req.query.name) { + var queryString = '%' + req.query.name + '%'; + options.where.name = { + $like: queryString + }; + } + if (req.query.phase) { + var phases = req.query.phase.split(','); + options.where.phase = { + $in: phases + }; + } + + async.parallel([ + function(cb) { + gameRepository.find(options, cb); + }, + function(cb) { + gameRepository.count(options, cb); + } + ], function(err, results) { + if (err) { + return next(err); + } + return res.json({ + start: parseInt(options.offset), + totalCount: results[1], + results: results[0] + }); + }); +}; + +exports.get = function(req, res, next) { + 'use strict'; + gameRepository.get(req.params.id, function(err, game) { + if (err) { + return next(err); + } + return res.json(game); + }); +}; diff --git a/app/models/game.js b/app/models/game.js index ea47a6b..5b31047 100644 --- a/app/models/game.js +++ b/app/models/game.js @@ -4,8 +4,7 @@ var Sequelize = require('sequelize'); var schema = { name: { type: Sequelize.STRING, - allowNull: false, - unique: true + allowNull: false }, description: { type: Sequelize.TEXT, @@ -21,20 +20,36 @@ var schema = { }, phase: { type: Sequelize.ENUM('FORMING', 'STARTED', 'COMPLETED'), + defaultValue: 'FORMING', allowNull: false }, victor: { type: Sequelize.ENUM('REBELLION', 'GOVERNMENT') - } + }, + createdAt: { + type: Sequelize.DATE, + field: 'created_at' + }, + updatedAt: { + type: Sequelize.DATE, + field: 'updated_at' + } }; var options = { + createdAt: 'createdAt', + updatedAt: 'updatedAt', underscored: true, + timestamps: true, tableName: 'game', indexes: [{ name: 'phase_index', method: 'BTREE', fields: ['phase'] + }, { + name: 'name_index', + method: 'BTREE', + fields: ['name'] }] }; @@ -44,4 +59,4 @@ module.exports = function(sequelize, DataTypes) { Game.hasMany(User); return Game; }; -//#JSCOVERAGE_ENDIF \ No newline at end of file +//#JSCOVERAGE_ENDIF diff --git a/app/repositories/game.js b/app/repositories/game.js new file mode 100644 index 0000000..229a164 --- /dev/null +++ b/app/repositories/game.js @@ -0,0 +1,44 @@ +var sequelize = global.database.sequelize; +var Game = sequelize.import(__dirname + '/../models/game'); + +module.exports = { + get: function(id, cb) { + Game.findById(id) + .then(function(game) { + if (!game) { + return cb(new Error("Could not find game with id " + id)); + } else { + return cb(null, game); + } + }).catch(function(error) { + return cb(error); + }); + }, + + create: function (game, cb) { + Game.create(game) + .then(function(game) { + return cb(null, game); + }).catch(function(error) { + return cb(error); + }); + }, + + find: function (options, cb) { + Game.findAll(options) + .then(function(games) { + return cb(null, games); + }).catch(function(error) { + return cb(error); + }); + }, + + count: function (options, cb) { + Game.count(options) + .then(function(count) { + cb(null, count); + }).catch(function(error) { + return cb(error); + }); + } +}; diff --git a/app/routes/game.server.routes.js b/app/routes/game.server.routes.js new file mode 100644 index 0000000..bf4f9b0 --- /dev/null +++ b/app/routes/game.server.routes.js @@ -0,0 +1,13 @@ +var controller = require('../controllers/game.server.controller'); +var express = require('express'); +var gameRouter = express.Router(); +var ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn; + +module.exports = function (app) { + 'use strict'; + // user routing + gameRouter.post('/', ensureLoggedIn('/login'), controller.create); + gameRouter.get('/:id', ensureLoggedIn('/login'), controller.get); + gameRouter.get('/', ensureLoggedIn('/login'), controller.find); + app.use('/games', gameRouter); +}; From 6246bf9423ee0bc5870bf70c38e68c404ede51ba Mon Sep 17 00:00:00 2001 From: Marc Gunn Date: Wed, 7 Oct 2015 00:03:46 -0700 Subject: [PATCH 4/6] fix indentation --- app/models/game.js | 30 +++++++++++++++--------------- test/integration/gameTest.js | 12 ++++++------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/models/game.js b/app/models/game.js index 5b31047..05a9fe4 100644 --- a/app/models/game.js +++ b/app/models/game.js @@ -27,13 +27,13 @@ var schema = { type: Sequelize.ENUM('REBELLION', 'GOVERNMENT') }, createdAt: { - type: Sequelize.DATE, - field: 'created_at' - }, - updatedAt: { - type: Sequelize.DATE, - field: 'updated_at' - } + type: Sequelize.DATE, + field: 'created_at' + }, + updatedAt: { + type: Sequelize.DATE, + field: 'updated_at' + } }; var options = { @@ -43,14 +43,14 @@ var options = { timestamps: true, tableName: 'game', indexes: [{ - name: 'phase_index', - method: 'BTREE', - fields: ['phase'] - }, { - name: 'name_index', - method: 'BTREE', - fields: ['name'] - }] + name: 'phase_index', + method: 'BTREE', + fields: ['phase'] + }, { + name: 'name_index', + method: 'BTREE', + fields: ['name'] + }] }; module.exports = function(sequelize, DataTypes) { diff --git a/test/integration/gameTest.js b/test/integration/gameTest.js index 6a24d1e..ea7fd54 100644 --- a/test/integration/gameTest.js +++ b/test/integration/gameTest.js @@ -57,7 +57,7 @@ describe('Game routes', function() { async.waterfall([ function(cb) { - agent + agent .post('/users') .send(_.cloneDeep(user)) .end(cb); @@ -73,7 +73,7 @@ describe('Game routes', function() { gameTemplate = _.cloneDeep(gameTemplate); gameTemplate.name = 'Game ' + n; gameTemplate.phase = (n % 2 == 0 ? 'FORMING' : 'STARTED') - agent + agent .post('/games') .send(gameTemplate) .end( function(err, response) { @@ -93,7 +93,7 @@ describe('Game routes', function() { }); it('Should create games', function (done) { - agent + agent .post('/games') .send(_.cloneDeep(gameTemplate)) .end( function(err, response) { @@ -108,7 +108,7 @@ describe('Game routes', function() { }); it('Should filter by phase', function(done) { - agent + agent .get('/games?phase=FORMING,COMPLETED') .end( function(err, response) { if (err) { @@ -127,7 +127,7 @@ describe('Game routes', function() { }); it('Should page', function(done) { - agent + agent .get('/games?offset=3&limit=10') .end( function(err, response) { if (err) { @@ -143,7 +143,7 @@ describe('Game routes', function() { }); it('Should filter by name', function(done) { - agent + agent .get('/games?name=Ga%2515') .end( function(err, response) { if (err) { From e84ef308be6128c55bda313ef5c830661232ada2 Mon Sep 17 00:00:00 2001 From: Marc Gunn Date: Wed, 7 Oct 2015 00:58:06 -0700 Subject: [PATCH 5/6] split up message types between outbound and inbound to server --- message-types.js | 86 ++++++++++++++++++++++---------- payloads.js | 127 ++++++++++++++++++++--------------------------- 2 files changed, 112 insertions(+), 101 deletions(-) diff --git a/message-types.js b/message-types.js index 8e92dc2..494b9a3 100644 --- a/message-types.js +++ b/message-types.js @@ -3,45 +3,77 @@ if(typeof(window) != "undefined") { } else { } -exports.EMAIL_REGISTER = "register"; // A new email account has been created -exports.EMAIL_SEND = "send"; // An email has been sent +exports.EMAIL_REGISTER = "register"; // Create a new email account +exports.EMAIL_REGISTERED = "registered"; // A new email account has been created +exports.EMAIL_SEND = "send"; // Send an email +exports.EMAIL_SENT = "sent"; // An email has been sent -exports.GENERAL_ACTIVATE = "activate"; // Activate torwolf? exports.GENERAL_DEACTIVATE = "deactivate"; // Deactivate torwolf? +exports.GENERAL_ACTIVATE = "activate"; // Activate torwolf? exports.GENERAL_ERROR = "error"; // An error has occurred -exports.IRC_CONNECT = "connect"; // A player has connected to IRC -exports.IRC_DISCONNECT = "disconnect" // a player has disconnected from IRC -exports.IRC_MESSAGE = "message"; // An IRC message has been sent -exports.IRC_JOIN = "join"; // A player has joined an IRC channel -exports.IRC_LEAVE = "leave"; // A player has left an IRC channel -exports.IRC_NICK = "switch nick"; // A player has switched nicks +exports.IRC_CONNECT = "connect"; // Connect to IRC +exports.IRC_CONNECTED = "connected"; // A player has connected to IRC +exports.IRC_DISCONNECT = "disconnect" // Disconnect from IRC +exports.IRC_DISCONNECTED = "disconnected" // A player has disconnected from IRC +exports.IRC_MESSAGE = "message"; // Send an IRC message +exports.IRC_MESSAGED = "messaged"; // An IRC message has been sent +exports.IRC_JOIN = "join"; // Join an IRC channel +exports.IRC_JOINED = "joined"; // A player has joined an IRC channel +exports.IRC_LEAVE = "leave"; // Leave an IRC channel +exports.IRC_LEFT = "left"; // A player has left an IRC channel +exports.IRC_SWITCHNICK = "switch nick"; // switch IRC nicks +exports.IRC_NICKSWITCHED = "nick switched"; // A player has switched IRC nicks -exports.LOBBY_CONNECT = "connect" // A player has connected to the lobby -exports.LOBBY_CREATE = "create"; // A new game has been created -exports.LOBBY_JOIN = "join"; // A player has joined the lobby +exports.LOBBY_CONNECT = "connect" // Connect to the lobby +exports.LOBBY_CONNECTED = "connected" // A player has connected to the lobby +exports.LOBBY_CREATE = "create"; // Create a new game +exports.LOBBY_CREATED = "created"; // A new game has been created +exports.LOBBY_JOINED = "joined"; // A player has joined the lobby +exports.LOBBY_JOIN = "join"; // Join a lobby -exports.NEWSPAPER_PUBLISH = "publish"; // A paper has been published +exports.NEWSPAPER_PUBLISH = "publish"; // Publish a paper +exports.NEWSPAPER_PUBLISHED = "published"; // A paper has been published -exports.SNOOPER_INTERCEPT = "intercept"; // A plaintext message has been intercepted -exports.SNOOPER_SSL = "ssl"; // An encrypted message has been intercepted -exports.SNOOPER_TOR = "tor"; // A player has joined or used the tor network -exports.SNOOPER_WIRETAP = "wiretap"; // A wiretap has been put in place +exports.SNOOPER_INTERCEPT = "intercept"; // TODO: what is this message responsible for? +exports.SNOOPER_INTERCEPTED = "intercepted"; // A plaintext message has been intercepted +exports.SNOOPER_SSL = "ssl"; // TODO: what is this message responsible for? +exports.SNOOPER_SSLED = "ssl"; // An encrypted message has been intercepted +exports.SNOOPER_TOR = "tor"; // TODO: what is this message responsible for? +exports.SNOOPER_TORRED = "tored"; // A player has joined or used the tor network +exports.SNOOPER_WIRETAP = "wiretap"; // Put a wiretap into place +exports.SNOOPER_WIRETAPPED = "wiretapped"; // A wiretap has been put in place exports.STORYTELLER_ALLEGIANCE = "allegiance"; // Change allegiance / Set allegiance -exports.STORYTELLER_ANNOUNCEMENT = "announcement"; // Make an announcement +exports.STORYTELLER_ALLEGIANCECHANGE = "allegiance-change"; // An allegiance has been changed +exports.STORYTELLER_ANNOUNCE = "announce"; // Make an announcement +exports.STORYTELLER_ANNOUNCEMENT = "announcement-sent"; // An announcement has been made exports.STORYTELLER_END = "end"; // End the game -exports.STORYTELLER_HEARTBEAT = "heartbeat"; // Announce a heartbeat -exports.STORYTELLER_INVESTIGATE = "investigate"; // look into an issue -exports.STORYTELLER_KILL = "kill"; // A player has been killed -exports.STORYTELLER_JOIN = "join"; // Join a game / someone has joined -exports.STORYTELLER_LEAVE = "leave"; // Leave a game / someone has left -exports.STORYTELLER_ROLE = "setrole"; // Specify role preference / Set role +exports.STORYTELLER_ENDED = "ended"; // The game has ended +exports.STORYTELLER_HEARTBEATPING = "heartbeat-ping"; // Send a heartbeat +exports.STORYTELLER_HEARTBEATPONG = "heartbeat-pong"; // Respond to a heartbeat +exports.STORYTELLER_INVESTIGATED = "investigated"; // An issue has been investigated +exports.STORYTELLER_INVESTIGATE = "investigate"; // Investigate an issue +exports.STORYTELLER_KILL = "kill"; // Kill a player +exports.STORYTELLER_KILLED = "killed"; // A player has been killed +exports.STORYTELLER_JOIN = "join"; // Join a game +exports.STORYTELLER_JOINED = "joined"; // Someone has joined the game +exports.STORYTELLER_LEAVE = "leave"; // Leave a game +exports.STORYTELLER_LEFT = "left"; // Someone has left the game +exports.STORYTELLER_SETROLE = "set-role"; // Set a preferred role +exports.STORYTELLER_ROLESET = "role-set"; // A preferred role has been set exports.STORYTELLER_RUMOR = "rumor"; // Give the person a rumor -exports.STORYTELLER_START = "start"; // The game has started -exports.STORYTELLER_TICK = "tick"; // Trigger a tick / announce a tick +exports.STORYTELLER_RUMORRECEIVED = "rumor-received"; // A rumor has been received +exports.STORYTELLER_START = "start"; // Start the game +exports.STORYTELLER_STARTED = "started"; // The game has started +exports.STORYTELLER_TICK = "tick"; // Trigger a tick +exports.STORYTELLER_TOCK = "tock"; // A tick has occurred exports.TOR_CONNECT = "connect"; // Connect to Tor +exports.TOR_CONNECTED = "connected"; // Connection to Tor complete exports.TOR_DISCONNECT = "disconnect"; // Disconnect from Tor +exports.TOR_DISCONNECTED = "disconnected"; // Disconnection from Tor complete exports.TOR_ROUTE = "route"; // Send a package to be routed through Tor -exports.TOR_EDUCATION = "tor education" // Teach a player about the Tor network \ No newline at end of file +exports.TOR_ROUTED = "routed"; // A package has been routed through Tor +exports.TOR_EDUCATE = "tor-educate" // Teach a player about the Tor network +exports.TOR_EDUCATED = "tor-educated" // A player has been taught about Tor diff --git a/payloads.js b/payloads.js index a9e788f..c489ac7 100644 --- a/payloads.js +++ b/payloads.js @@ -58,7 +58,7 @@ exports.EmailRegisterOutPayload = function(account) { this.account = account; this.getPayload = function() { return { - type: constants.EMAIL_REGISTER, + type: constants.EMAIL_REGISTERED, data: { accountId: this.account.id, address: this.account.address, @@ -70,7 +70,7 @@ exports.EmailRegisterOutPayload = function(account) { exports.EmailSendInPayload = function(message) { this.message = message; - + this.getPayload = function() { return { type: constants.EMAIL_SEND, @@ -89,7 +89,7 @@ exports.EmailSendInPayload = function(message) { exports.EmailSendOutPayload = function(message) { this.message = message; - + this.getPayload = function() { var ccAddresses = []; var toAddresses = []; @@ -97,9 +97,9 @@ exports.EmailSendOutPayload = function(message) { ccAddresses.push(message.cc[x].address); for(var x in message.to) toAddresses.push(message.to[x].address); - + return { - type: constants.EMAIL_SEND, + type: constants.EMAIL_SENT, data: { body: this.message.body, ccAddresses: ccAddresses, @@ -117,7 +117,7 @@ exports.IrcConnectOutPayload = function(message) { this.user = message.user; this.getPayload = function() { return { - type: constants.IRC_CONNECT, + type: constants.IRC_CONNECTED, data: { messageId: this.message.id, playerId: this.user.player.id, @@ -145,7 +145,7 @@ exports.IrcMessageOutPayload = function(message) { this.message = message; this.getPayload = function() { return { - type: constants.IRC_MESSAGE, + type: constants.IRC_MESSAGED, data: { messageId: this.message.id, text: this.message.text, @@ -172,7 +172,7 @@ exports.IrcJoinOutPayload = function(user) { this.user = user; this.getPayload = function() { return { - type: constants.IRC_JOIN, + type: constants.IRC_JOINED, data: { playerId: this.user.player.id, userId: this.user.id, @@ -182,11 +182,11 @@ exports.IrcJoinOutPayload = function(user) { }; }; -exports.IrcNickOutPayload = function(user) { +exports.IrcNickOutPayload = function(user) { this.user = user; this.getPayload = function() { return { - type: constants.IRC_NICK, + type: constants.IRC_NICKSWITCHED, data: { nick: this.user.nick, userId: this.user.id @@ -211,7 +211,7 @@ exports.LobbyConnectOutPayload = function(player) { this.player = player; this.getPayload = function() { return { - type: constants.LOBBY_CONNECT, + type: constants.LOBBY_CONNECTED, data: { playerId: this.player.id, name: this.player.name @@ -240,7 +240,7 @@ exports.LobbyCreateOutPayload = function(game) { this.game = game; this.getPayload = function() { return { - type: constants.LOBBY_CREATE, + type: constants.LOBBY_CREATED, data: { gameId: this.game.id, isPrivate: this.game.isPrivate, @@ -275,7 +275,7 @@ exports.LobbyJoinOutPayload = function(player, game) { this.game = game; this.getPayload = function() { return { - type: constants.LOBBY_JOIN, + type: constants.LOBBY_JOINED, data: { playerId: this.player.id, gameId: this.game.id @@ -287,7 +287,7 @@ exports.LobbyJoinOutPayload = function(player, game) { exports.NewspaperPublishInPayload = function(game) { this.game = game - + this.getPayload = function() { return { type: constants.NEWSPAPER_PUBLISH, @@ -300,14 +300,14 @@ exports.NewspaperPublishInPayload = function(game) { exports.NewspaperPublishOutPayload = function(edition) { this.edition = edition; - + this.getPayload = function() { var rumorIds = []; for(var x in this.edition.rumors) rumorIds.push(this.edition.rumors[x].id); - + return { - type: constants.NEWSPAPER_PUBLISH, + type: constants.NEWSPAPER_PUBLISHED, data: { copy: this.edition.copy, editionId: this.edition.id, @@ -322,7 +322,7 @@ exports.NewspaperPublishOutPayload = function(edition) { exports.SnooperInterceptInPayload = function(interaction) { this.interaction = interaction; - + this.getPayload = function() { return { type: constants.SNOOPER_INTERCEPT, @@ -336,10 +336,10 @@ exports.SnooperInterceptInPayload = function(interaction) { exports.SnooperInterceptOutPayload = function(interaction, player) { this.interaction = interaction; this.player = player; - + this.getPayload = function() { return { - type: constants.SNOOPER_INTERCEPT, + type: constants.SNOOPER_INTERCEPTED, data: { interactionId: this.interaction.id, message: this.interaction.message, @@ -360,10 +360,10 @@ exports.SnooperSslInPayload = function() { exports.SnooperSslOutPayload = function(player, target) { this.player = player; this.target = target; - + this.getPayload = function() { return { - type: constants.SNOOPER_TOR, + type: constants.SNOOPER_SSLED, data: { playerId: this.player.id, target: this.target @@ -382,10 +382,10 @@ exports.SnooperTorInPayload = function() { exports.SnooperTorOutPayload = function(player, state) { this.player = player; this.state = state; - + this.getPayload = function() { return { - type: constants.SNOOPER_TOR, + type: constants.SNOOPER_TORRED, data: { playerId: this.player.id, state: this.state @@ -396,7 +396,7 @@ exports.SnooperTorOutPayload = function(player, state) { exports.SnooperWiretapInPayload = function(player) { this.player = player; - + this.getPayload = function() { return { type: constants.SNOOPER_WIRETAP, @@ -409,10 +409,10 @@ exports.SnooperWiretapInPayload = function(player) { exports.SnooperWiretapOutPayload = function(player) { this.player = player; - + this.getPayload = function() { return { - type: constants.SNOOPER_WIRETAP, + type: constants.SNOOPER_WIRETAPPED, data: { playerId: player.id } @@ -428,10 +428,10 @@ exports.StorytellerAllegianceOutPayload = function(player) { this.player = player; this.getPayload = function() { return { - type: constants.STORYTELLER_ALLEGIANCE, + type: constants.STORYTELLER_ALLEGIANCECHANGE, data: { playerId: this.player.id, - allegiance: this.player.allegiance + allegiance: this.player.allegiance } } } @@ -469,7 +469,7 @@ exports.StorytellerEndOutPayload = function(game) { this.game = game; this.getPayload = function() { return { - type: constants.STORYTELLER_END, + type: constants.STORYTELLER_ENDED, data: { gameId: this.game.id, } @@ -481,10 +481,10 @@ exports.StorytellerEndOutPayload = function(game) { exports.StorytellerHeartbeatInPayload = function(game) { this.game = game; this.count = 0; - + this.getPayload = function() { return { - type: constants.STORYTELLER_HEARTBEAT, + type: constants.STORYTELLER_HEARTBEATPING, data: { gameId: this.game.id, count: this.count @@ -497,7 +497,7 @@ exports.StorytellerHeartbeatOutPayload = function(count) { this.count = count; this.getPayload = function() { return { - type: constants.STORYTELLER_HEARTBEAT, + type: constants.STORYTELLER_HEARTBEATPONG, data: { count: this.count, } @@ -538,7 +538,7 @@ exports.StorytellerJoinOutPayload = function(player) { this.player = player; this.getPayload = function() { return { - type: constants.STORYTELLER_JOIN, + type: constants.STORYTELLER_JOINED, data: { playerId: this.player.id, status: this.player.status, @@ -566,7 +566,7 @@ exports.StorytellerKillOutPayload = function(player) { this.player = player; this.getPayload = function() { return { - type: constants.STORYTELLER_KILL, + type: constants.STORYTELLER_KILLED, data: { playerId: this.player.id, } @@ -581,10 +581,10 @@ exports.StorytellerRoleOutPayload = function(player) { this.player = player; this.getPayload = function() { return { - type: constants.STORYTELLER_ROLE, + type: constants.STORYTELLER_ROLESET, data: { playerId: this.player.id, - role: this.player.role + role: this.player.role } } } @@ -595,7 +595,7 @@ exports.StorytellerRumorInPayload = function(rumor) { this.rumor = rumor; this.sourceId = ""; this.truthStatus = ""; - + this.getPayload = function() { return { type: constants.STORYTELLER_RUMOR, @@ -607,7 +607,7 @@ exports.StorytellerRumorInPayload = function(rumor) { } } } - + } exports.StorytellerRumorOutPayload = function(rumor) { @@ -615,10 +615,10 @@ exports.StorytellerRumorOutPayload = function(rumor) { this.rumor = rumor; this.sourceId = ""; this.truthStatus = ""; - + this.getPayload = function() { return { - type: constants.STORYTELLER_RUMOR, + type: constants.STORYTELLER_RUMORRECEIVED, data: { destinationId: this.destinationId, publicationStatus: this.rumor.publicationStatus, @@ -637,7 +637,7 @@ exports.StorytellerStartInPayload = function(game) { return { type: constants.STORYTELLER_START, data: { - gameId: this.game.id + gameId: this.game.id } } } @@ -648,7 +648,7 @@ exports.StorytellerStartOutPayload = function(game) { exports.StorytellerTickInPayload = function(game) { this.game = game; - + this.getPayload = function() { return { type: constants.STORYTELLER_TICK, @@ -663,7 +663,7 @@ exports.StorytellerTickOutPayload = function(game) { this.game = game; this.getPayload = function() { return { - type: constants.STORYTELLER_TICK, + type: constants.STORYTELLER_TOCK, data: { round: this.game.round } @@ -671,27 +671,6 @@ exports.StorytellerTickOutPayload = function(game) { }; } - -exports.TorBridgeInPayload = function() { - this.getPayload = function() { - return { - type: constants.TOR_BRIDGE, - data: { - } - } - }; -}; - -exports.TorBridgeOutPayload = function() { - this.getPayload = function() { - return { - type: constants.TOR_BRIDGE, - data: { - } - } - }; -} - exports.TorConnectInPayload = function() { this.getPayload = function() { return { @@ -705,7 +684,7 @@ exports.TorConnectInPayload = function() { exports.TorConnectOutPayload = function() { this.getPayload = function() { return { - type: constants.TOR_CONNECT, + type: constants.TOR_CONNECTED, data: { } } @@ -725,7 +704,7 @@ exports.TorDisconnectInPayload = function() { exports.TorDisconnectOutPayload = function() { this.getPayload = function() { return { - type: constants.TOR_DISCONNECT, + type: constants.TOR_DISCONNECTED, data: { } } @@ -735,7 +714,7 @@ exports.TorDisconnectOutPayload = function() { exports.TorRouteInPayload = function(message) { this.message = message; this.bridgeId = ""; - + this.getPayload = function() { return { type: constants.TOR_ROUTE, @@ -750,10 +729,10 @@ exports.TorRouteInPayload = function(message) { exports.TorRouteOutPayload = function(message) { this.message = message; this.bridgeId = ""; - + this.getPayload = function() { return { - type: constants.TOR_ROUTE, + type: constants.TOR_ROUTED, data: { bridgeId: this.bridgeId, message: this.message @@ -765,10 +744,10 @@ exports.TorRouteOutPayload = function(message) { exports.TorEducateOutPayload = function(teacherId, studentId) { this.teacherId = teacherId; this.studentId = studentId; - + this.getPayload = function() { return { - type: constants.TOR_EDUCATION, + type: constants.TOR_EDUCATED, data: { teacherId: this.teacherId, studentId: this.studentId @@ -780,10 +759,10 @@ exports.TorEducateOutPayload = function(teacherId, studentId) { exports.TorEducateInPayload = function(message) { this.teacherId = teacherId; this.studentId = studentId; - + this.getPayload = function() { return { - type: constants.TOR_EDUCATION, + type: constants.TOR_EDUCATE, data: { teacherId: this.teacherId, studentId: this.studentId From 3ea252f7d6b8cd8816a2ccf3dcab1b921ec4d0c4 Mon Sep 17 00:00:00 2001 From: Marc Gunn Date: Thu, 8 Oct 2015 23:50:20 -0700 Subject: [PATCH 6/6] heartbeat support --- app/classes.js | 10 ++ app/handlers/error.js | 11 ++ app/handlers/heartbeat.js | 34 +++++ app/handlers/messageSender.js | 23 +++ app/handlers/router.js | 30 ++++ app/handlers/routingTable.js | 8 + app/lib/gameState.js | 9 ++ constants.js | 19 +++ locales.js | 14 ++ locales/default.js | 251 ++++++++++++++++++++++++++++++++ package.json | 7 +- payloads.js | 4 +- server.js | 34 ++++- test/factories/game.js | 6 + test/factories/user.js | 7 + test/integration/socketsTest.js | 71 +++++++++ 16 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 app/classes.js create mode 100644 app/handlers/error.js create mode 100644 app/handlers/heartbeat.js create mode 100644 app/handlers/messageSender.js create mode 100644 app/handlers/router.js create mode 100644 app/handlers/routingTable.js create mode 100644 app/lib/gameState.js create mode 100644 constants.js create mode 100644 locales.js create mode 100644 locales/default.js create mode 100644 test/factories/game.js create mode 100644 test/factories/user.js create mode 100644 test/integration/socketsTest.js diff --git a/app/classes.js b/app/classes.js new file mode 100644 index 0000000..b8107f7 --- /dev/null +++ b/app/classes.js @@ -0,0 +1,10 @@ +var uuid = require('uuid'); + +exports.Interaction = function() { + this.id = uuid.v4(); + this.isSsl = false; + this.isTor = false; + this.message = null; // The message that initiated this interaction + this.responses = []; // The messages that have been sent as a response to this interaction + this.socket = null; // The socket that initiated this interaction +}; diff --git a/app/handlers/error.js b/app/handlers/error.js new file mode 100644 index 0000000..597d4bc --- /dev/null +++ b/app/handlers/error.js @@ -0,0 +1,11 @@ +var payloads = require('../../payloads'); +var messageTypes = require('../../message-types'); + +// Functions +exports.error = function (message, socket) { + var error = new payloads.ErrorPayload(message); + socket.emit('error', { + payload: error, + type: messageTypes.GENERAL_ERROR + }); +}; diff --git a/app/handlers/heartbeat.js b/app/handlers/heartbeat.js new file mode 100644 index 0000000..e80c9ad --- /dev/null +++ b/app/handlers/heartbeat.js @@ -0,0 +1,34 @@ +var constants = require('../../constants'), + payloads = require('../../payloads'), + locales = require('../../locales'), + error = require('./error'), + messageSender = require('./messageSender'), + gameRepository = require('../repositories/game'), + messageTypes = require('../../message-types'); + +exports.handle = function(data, interaction) { + var socket = interaction.socket; + var count = data.count; + var game = gameRepository.get(data.game.id, function (err, game) { + if (err) { + throw err; + } + + // Did the game end? + if (game.phase === 'COMPLETED') { + return; + } + + // Start the next heartbeat + return setTimeout(function() { + var payload = new payloads.StorytellerHeartbeatOutPayload(++count); + messageSender.send( + payload, + messageTypes.STORYTELLER_HEARTBEATPING, + socket, + interaction); + }, constants.TICK_HEARTBEAT); + }); + +}; + diff --git a/app/handlers/messageSender.js b/app/handlers/messageSender.js new file mode 100644 index 0000000..9ec12ab --- /dev/null +++ b/app/handlers/messageSender.js @@ -0,0 +1,23 @@ +exports.send = function(payload, type, socket, interaction) { + var message = { + payload: payload, + type: type + }; + + // Add to the interaction + if(interaction) { + interaction.responses.push(message); + message.interactionId = interaction.id; + } + + // TODO: Snoop the interaction + if(interaction) { + // var interceptIn = new payloads.SnooperInterceptInPayload(interaction); + // exports.routeMessage( + // constants.COMMUNICATION_TARGET_SNOOPER, + // interceptIn.getPayload(), + // constants.COMMUNICATION_SOCKET_SERVER); + } + + socket.emit('message', message); +}; diff --git a/app/handlers/router.js b/app/handlers/router.js new file mode 100644 index 0000000..88a6c69 --- /dev/null +++ b/app/handlers/router.js @@ -0,0 +1,30 @@ +var classes = require('../classes'), + constants = require('../../constants'), + payloads = require('../../payloads'), + gameState = require('../lib/gameState'), + routingTable = require('./routingTable'), + logger = require('../lib/logger').logger; + +exports.receiveMessage = function(message, socket) { + if(!message.payload || !message.type) { + logger.info("Invalid payload received " + JSON.stringify(message)); + return; // Invalid payload + } + + // Set up metadata about the message + if(gameState.getInteractionById(message.interactionId)) { + logger.info("Duplicate interaction received " + message.interactionId); + return; // Duplicate interaction + } + // Build the interaction + var interaction = new classes.Interaction(); + interaction.id = message.interactionId?message.interactionId:interaction.id; + interaction.message = message; + interaction.isTor = message.isTor?true:false; + interaction.isSsl = message.isSsl?true:false; + interaction.socket = socket; + gameState.storeInteraction(interaction); + message.interactionId = interaction.id; + + routingTable[message.type].handle(message.payload, interaction); +}; diff --git a/app/handlers/routingTable.js b/app/handlers/routingTable.js new file mode 100644 index 0000000..8fb0247 --- /dev/null +++ b/app/handlers/routingTable.js @@ -0,0 +1,8 @@ +var messageTypes = require('../../message-types'); + +var heartbeat = require('./heartbeat'); + +var table = {}; +table[messageTypes.STORYTELLER_HEARTBEATPONG] = heartbeat; + +module.exports = table; diff --git a/app/lib/gameState.js b/app/lib/gameState.js new file mode 100644 index 0000000..c156e73 --- /dev/null +++ b/app/lib/gameState.js @@ -0,0 +1,9 @@ +var interactions = {}; + +exports.getInteractionById = function(interactionId) { + return interactions[interactionId]; +}; + +exports.storeInteraction = function(interaction) { + interactions[interaction.id] = interaction; +}; diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..ec5aced --- /dev/null +++ b/constants.js @@ -0,0 +1,19 @@ +if(typeof(window) != "undefined") { + var exports = window; +} else { +} + +exports.COMMUNICATION_SOCKET_SERVER = "server"; // allows the server to communicate with itself + +exports.TICK_WARNING = 50000; // Number of milliseconds warning to give before ending the turn +exports.TICK_HEARTBEAT = 10000; // How many milliseconds between each heartbeat + +exports.COMMUNICATION_TARGET_EMAIL = "email"; +exports.COMMUNICATION_TARGET_IRC = "irc"; +exports.COMMUNICATION_TARGET_LOBBY = "lobby"; +exports.COMMUNICATION_TARGET_NEWSPAPER = "newspaper"; +exports.COMMUNICATION_TARGET_SNOOPER = "snooper"; +exports.COMMUNICATION_TARGET_STORYTELLER = "storyteller"; +exports.COMMUNICATION_TARGET_TOR = "tor"; + +exports.LOCALE_DEFAULT = "default"; diff --git a/locales.js b/locales.js new file mode 100644 index 0000000..add7ef0 --- /dev/null +++ b/locales.js @@ -0,0 +1,14 @@ +if(typeof(window) != "undefined") { + window.LOCALE = (navigator.language)?navigator.language:navigator.userLanguage; + var script = document.createElement( 'script' ); + script.type = 'text/javascript'; + script.src = '/locales/' + window.LOCALE + '.js'; + $("head").append(script); + + if(!(LOCALE in localization)) + localization[LOCALE] = localization[window.LOCALE_DEFAULT]; +} else { + // Todo: include every file in locales/ dynamically + exports.localization = {}; + exports["default"] = require("./locales/default.js").localization["default"]; +} diff --git a/locales/default.js b/locales/default.js new file mode 100644 index 0000000..b1ecc48 --- /dev/null +++ b/locales/default.js @@ -0,0 +1,251 @@ +if(typeof(window) != "undefined") { + var exports = window; +} else { +} + +(function() { + var locale = "default"; + exports.localization = {}; + exports.localization[locale] = {}; + + exports.localization[locale]["errors"] = { + email: { + ADDRESS_EMPTY: "you cannot create an empty email address", + ADDRESS_TAKEN: "an account by that name already exists" + }, + irc: { + NICKEXISTS: "The nick %s is already taken." + }, + lobby: { + CREATE_NAME_BLANK: "Your game name cannot be blank.", + CREATE_PASSWORD_BLANK: "You cannot have a private game without a password.", + JOIN_ALREADY_IN_GAME: "You are already in a game.", + JOIN_GAME_FULL: "This game is full.", + JOIN_INCORRECT_PASSWORD: "You did not enter the correct password.", + JOIN_NONEXISTANT_GAME: "The game you tried to join doesn't exist." + }, + newspaper: { + }, + snooper: { + WIRETAP_COUNT: "You have used up all your wiretaps.", + WIRETAP_ROLE: "You cannot wiretap other players.", + INTERCEPT_SYSTEM: "Only the system can intercept a message." + }, + storyteller: { + GAMEOVER_SYSTEM: "Only the system can end the game.", + HEARTBEAT_SYSTEM: "Only the system can trigger a heartbeat.", + INVESTIGATE_NOGAME: "You aren't connected to a game.", + INVESTIGATE_NOJOURNALIST: "Only the journalist may investigate rumors.", + INVESTIGATE_NOPLAYER: "You aren't a registered player on the server.", + INVESTIGATE_NORUMOR: "That rumor doesn't exist in this game.", + INVESTIGATE_OLDRUMOR: "That rumor has already been investigated.", + KILL_ILLEGAL: "You aren't allowed to kill people.", + KILL_NOBODY: "The person you are trying to kill does not exist.", + RUMOR_INVALID_RUMOR: "The specified rumor does not exist.", + RUMOR_INVALID_SOURCE: "The specified player does not know that rumor.", + RUMOR_SYSTEM: "Only the system can decide when rumors are spread.", + JOIN_LOBBY: "You can only join games through the lobby.", + START_SYSTEM: "Only the system can start a game.", + TICK_SYSTEM: "Only the system can trigger a tick.", + }, + tor: { + DISABLED: "Your proxy has not been properly configured." + } + }; + + exports.localization[locale]["gui"] = { + lobby: { + CREATE: "Create", + JOIN: "Join", + PASSWORD_PROMPT: "This game is private. What's the password?", + + badges: { + PRIVATE: "Private" + }, + create: { + ISPRIVATE: "Private", + NAME: "Name:", + PASSWORD: "Password" + }, + + LOBBY: "Lobby" + }, + email: { + tabs: { + ACCOUNT: 'Accounts', + ADDRESSES: 'Address Book', + COMPOSE: 'Compose', + INBOX: 'Inbox', + SETTINGS: 'Settings' + }, + + ADDRESS: "Address", + ATTACH_RUMOR: "Attach a rumor", + BCC: "Bcc", + CC: "Cc", + CREATE: "Create Account", + EMAIL: "Email", + FROM: "From", + REMOVE: "Remove", + RUMOR: "Rumor", + SEND: "Send", + SUBJECT: "Subject", + TO: "To", + TOR: "Use Tor" + }, + irc: { + IRC: "IRC", + SEND: "Send", + }, + newspaper: { + NEWSPAPER: "The Wolf Gazette" + }, + player: { + allegiance: { + G: "Government", + N: "Neutral", + R: "Rebellion", + U: "Unknown" + }, + allegianceCode: { + G: "G", + N: "N", + R: "R", + U: "U" + }, + role: { + A: "Activist", + C: "Citizen", + CA: "Citizen (Activist)", + CS: "Citizen (Spy)", + E: "Editor", + J: "Journalist", + S: "Spy", + U: "Unknown" + }, + roleCode: { + A: "A", + C: "C", + CA: "CA", + CS: "CS", + E: "E", + J: "J", + S: "S", + U: "U" + }, + status: { + A: "Alive", + D: "Dead" + }, + + KILL: "Kill", + YOU: "You" + }, + rumor: { + investigationStatus: { + C: "Complete", + I: "Investigating...", + N: "Not Investigating" + }, + investigationStatusCode: { + C: "C", + I: "I", + N: "N" + }, + publicationStatus: { + P: "Published", + U: "Unpublished" + }, + publicationStatusCode: { + P: "P", + U: "U" + }, + truthStatus: { + T: "True", + F: "False", + U: "Unknown" + }, + truthStatusCode: { + T: "T", + F: "F", + U: "U" + }, + + INVESTIGATE: "Investigate" + }, + snooper: { + SNOOPER: "Snooper v1.0" + }, + storyteller: { + PLAYERS: "Players", + RUMORS: "Rumors", + STORYTELLER: "Storyteller" + }, + tor: { + ACTIVATE: "Activate Tor", + DEACTIVATE: "Deactivate Tor", + TOR: "Tor" + } + }; + + exports.localization[locale]["messages"] = { + irc: { + CONNECTING: "Connecting to IRC... ", + CONNECTED: "Connected.", + JOINED: "has joined the channel.", + LEFT: "has left the channel.", + MOTD: "MOTD for irc.torwolf.net: Welcome to the torwolf IRC server! " + + "Thank you to Dry_Bones for hosting this server, and now, " + + "a message from Dry_Bones: Greetings, denizens of the torwolf IRC" + + "server. Today, I want to tell you about a special fan of the Mario " + + "Party series. Not many people purchase our games, let alone pre-order " + + "them, but a certain fan, slifty, pre-ordered Mario Party 9 many months " + + "before it came out! That's the kind of fan dedication that motivates " + + "us to make Mario Party game after Mario Party game. Thanks for your " + + "support, slifty! And thank you everyone for using torwolf IRC.", + SWITCHNICK: "%s is now known as %s.", + }, + snooper: { + ANONYMOUS: "Someone", + DEFAULT: "Intercepted a message from %s to module %s ", + EMAIL_REGISTER: "%s just registered the email address %s", + EMAIL_SEND: "%s just sent an email", + IRC_JOIN: "%s has joined the channel in IRC", + IRC_LEAVE: "%s has left the channel in IRC", + IRC_MESSAGE: "%s has said something in IRC", + }, + storyteller: { + GAMEOVER: "The game is over.", + KILL: "%s walks up to %s, takes out his gun, and shoots. It is not a pretty scene", + NEWS_NOTHING: "The journalist did not publish an article last month.", + NEWS_SOMETHING: "The journalist published an article last month! It is sitting on your doorstep.", + ROUND_BEGIN: "Month %d has begun.", + ROUND_END: "Month %d has ended.", + VICTORY_ACTIVISTS_MEDIA: "With so many secrets about the government revealed, the world has started to pay attention. The battle isn't over, but the media effort has come to a close. The rebels and the journalist have won.", + VICTORY_ACTIVISTS_KILLING: "The murder of an innocent civilian has sparked outrage across the world. As the newsroom continues to issue reports of corruption and crimes against humanity, the government finally begins to face serious pressure. The battle isn't over, but the media effort has come to a close. The rebels and the journalist have won.", + VICTORY_GOVERNMENT_JOURNALIST: "The journalist has been killed. The world is outraged, but with no source of information that outrage is short lived. The government is able to continue forward as planned. The rebels and the journalist have lost.", + VICTORY_GOVERNMENT_ACTIVIST: "The activist has been killed. Some of the world notices, but it is generally overlooked. With no leadership the rebels fall apart. The government doesn't skip a beat. The rebels and the journalist have lost.", + WARNING: "This turn ends in %d %s.", + WARNING_UNIT_SINGULAR: "second", + WARNING_UNIT_PLURAL: "seconds", + }, + newspaper: { + FALSE_HEADLINES: [ + "No Merit to \'%s\'", + "\'%s\' Hoax Debunked" + ], + FALSE_COPY: [ + "It turns out that the popular rumor about \'%s\' isn't true.", + ], + NO_HEADLINE: "Nothing was written about your country this month", + NO_COPY: "Apparently nothing interesting happened in your country worth reporting on.", + TRUE_HEADLINES: [ + "BREAKING: Secret \'%s\' mission revealed", + "Verified reports of government-run \'%s\' project", + ], + TRUE_COPY: [ + "There are serious concerns about the welfare of the people due to the \'%s\' project.", + ] + } + }; +})(); diff --git a/package.json b/package.json index 46006c7..fb06e09 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "passport": "0.3.0", "passport-local": "1.0.0", "async": "1.4.2", - "connect-ensure-login": "0.1.1" + "connect-ensure-login": "0.1.1", + "socket.io": "1.3.7", + "uuid": "2.0.1" }, "devDependencies": { "gulp-mocha": "2.1.3", @@ -46,6 +48,7 @@ "supertest": "1.1.0", "mockery": "1.4.0", "gulp-jshint": "1.11.2", - "gulp-watch": "4.3.5" + "gulp-watch": "4.3.5", + "socket.io-client": "1.3.7" } } diff --git a/payloads.js b/payloads.js index c489ac7..1f2b4c9 100644 --- a/payloads.js +++ b/payloads.js @@ -478,9 +478,9 @@ exports.StorytellerEndOutPayload = function(game) { } -exports.StorytellerHeartbeatInPayload = function(game) { +exports.StorytellerHeartbeatInPayload = function(game, count) { this.game = game; - this.count = 0; + this.count = count; this.getPayload = function() { return { diff --git a/server.js b/server.js index fa32ef4..fb2078a 100644 --- a/server.js +++ b/server.js @@ -8,7 +8,12 @@ var chalk = require('chalk'), logger = require('./app/lib/logger').logger, passport = require('passport'), userRepository = require('./app/repositories/user'), - LocalStrategy = require('passport-local'); + LocalStrategy = require('passport-local'), + constants = require('./constants'), + router = require('./app/handlers/router'), + messageSender = require('./app/handlers/messageSender'), + payloads = require('./payloads'), + messageTypes = require('./message-types'); /** * Main application entry file. @@ -39,11 +44,32 @@ passport.use(new LocalStrategy({ } )); +var server = require('http').createServer(app); +var io = require('socket.io')(server); +io.sockets.on('connection', function(socket) { + socket.locale = constants.LOCALE_DEFAULT; + + var payload = new payloads.StorytellerHeartbeatOutPayload(0); + messageSender.send( + payload, + messageTypes.STORYTELLER_HEARTBEATPING, + socket); + + socket.on('locale', function (locale) { + socket.locale = locale; + }); + + socket.on('message', function (payload) { + logger.debug('Received message ' + JSON.stringify(payload)); + router.receiveMessage(payload, socket); + }); +}); + // Start the app by listening on -var server = app.listen(config.port); +server = server.listen(config.port); // Expose app -exports = module.exports = { +exports = module.exports = { app: app, server: server } @@ -53,4 +79,4 @@ logger.info('--'); logger.info(chalk.green(config.app.title + ' application started')); logger.info(chalk.green('Environment:\t\t\t' + process.env.NODE_ENV)); logger.info(chalk.green('Port:\t\t\t\t' + config.port)); -logger.info('--'); \ No newline at end of file +logger.info('--'); diff --git a/test/factories/game.js b/test/factories/game.js new file mode 100644 index 0000000..fb561b4 --- /dev/null +++ b/test/factories/game.js @@ -0,0 +1,6 @@ +exports.create = function () { + return { + name: 'Bowser\s Kingdom', + description: 'Kingdom of the Koopa' + }; +} diff --git a/test/factories/user.js b/test/factories/user.js new file mode 100644 index 0000000..84c03fb --- /dev/null +++ b/test/factories/user.js @@ -0,0 +1,7 @@ +exports.create = function () { + return { + username: 'dry bones' + Math.random(), + password: 'bowser sucks', + email: 'drybones' + Math.random() + '@koopakingdom.com' + }; +} diff --git a/test/integration/socketsTest.js b/test/integration/socketsTest.js new file mode 100644 index 0000000..fd9443f --- /dev/null +++ b/test/integration/socketsTest.js @@ -0,0 +1,71 @@ +var should = require('chai').should(), + request = require('supertest'), + async = require('async'), + io = require('socket.io-client'), + url = 'http://localhost:3000', + userFactory = require('../factories/user'), + gameFactory = require('../factories/game'), + payloads = require('../../payloads'), + messageTypes = require('../../message-types'), + constants = require('../../constants'); + +if (!global.hasOwnProperty('testApp')) { + global.testApp = require('../../server'); +} + +var app = global.testApp; +var socket; + +describe('Core sockets', function() { + var game = undefined; + var gameTemplate = undefined; + var agent = request.agent(app.app); + + beforeEach(function(done) { + var user = userFactory.create(); + async.waterfall([ + function(cb) { + agent + .post('/users') + .send(user) + .end(cb); + }, + function(response, cb) { + agent + .post('/login') + .send({email: user.email, password: user.password}) + .end(cb); + }, + function(response, cb) { + agent + .post('/games') + .send(gameFactory.create()) + .end(cb); + } + ], function(err, response) { + if (err) { + return done(err); + } + game = response.body; + done(); + }); + }); + + it('Should heartbeat', function (done) { + var expectedCount = 0; + constants.TICK_HEARTBEAT = 300; + var socket = require('socket.io-client')('http://localhost:3000'); + socket.on('message', function(data) { + if (data.payload.count === 3) { + done(); + } + data.payload.count.should.equal(expectedCount); + expectedCount++; + data.type.should.equal(messageTypes.STORYTELLER_HEARTBEATPING); + socket.emit('message', { + payload: new payloads.StorytellerHeartbeatInPayload({id: game.id}, data.payload.count), + type: messageTypes.STORYTELLER_HEARTBEATPONG + }); + }); + }); +});