diff --git a/.gitignore b/.gitignore index 62397b3..c638939 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ selenium-debug.log .history test-results.xml -config/prod.secret.config.js \ No newline at end of file +config/prod.secret.config.js +config/local.secret.config.js \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index d11ddb9..2917152 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "name": "Launch Program", "type": "node2", "request": "launch", - "program": "${workspaceRoot}\\build\\server.js", + "program": "${workspaceRoot}/build/server.js", "cwd": "${workspaceRoot}" }, { diff --git a/Marketing.md b/Marketing.md new file mode 100644 index 0000000..31ddb93 --- /dev/null +++ b/Marketing.md @@ -0,0 +1,26 @@ +# Marketing Plan # + +## Story Blogs ## +### The Story of Telling ### +http://thestoryoftelling.com/contact/ +hello@thestoryoftelling.com + +### Story Center Blog ### +https://www.storycenter.org/blog/ +info@storycenter.org + +### BookRiot ### +http://bookriot.com/ + +## Game Websites ## +* http://tisch.nyu.edu/itp +* http://rumblegames.com +* https://itch.io/ +* http://reddit.com/r/webgames +* http://indiegamedevelopers.org/ (Post your game channel on Slack) + +## Writing Websites ## +* https://www.reddit.com/r/writing +* https://www.reddit.com/r/write/ +* https://www.reddit.com/r/KeepWriting/ +* https://www.reddit.com/r/writers/ diff --git a/README.md b/README.md index 0fde305..781a4c8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Every player sees in real time what every other player is writing, and can vote The end result is a collaboratively created story. -* [Follow Our Blog](https://medium.com/groupwrite-io) for updates + + +* [Follow Our Blog](https://medium.com/write-io) for updates * [Current Tasks](https://github.com/groupwrite.io/groupwrite.io/projects/1) * [Slack](https://www.hamsterpad.com/chat/writeio) * [A sample story created with groupwrite.io](https://www.facebook.com/ripper234/posts/10153753024424159) @@ -41,6 +43,19 @@ The end result is a collaboratively created story. * [http://localhost:3000/](http://localhost:3000/) * (See also admin screen at [http://localhost:3000/admin](http://localhost:3000/admin)) +### Database setup +The default setup uses the [mongo-in-memory](https://www.npmjs.com/package/mongo-in-memory) database, which cleans on every code edit. If you want persistant data: + +1. [Install a local MongoDB server](https://docs.mongodb.com/manual/administration/install-community/) +2. Create a new local file `config/local.secret.config.js` with + +```js +module.exports = { + // This overrides dev.secret.config.js + mongoConnectionString: 'mongodb://localhost:27017/groupwrite-dev' +} +``` + ## Contribution guidelines ## * We're having weekly coding sessions in Tel Aviv, usually on Thursday evening or Friday noonish. You're welcome to join! @@ -55,11 +70,10 @@ TBD: ## Who do I talk to? ## -For any questions, contact Ron Gross (chiefninjaofficer@gmail.com, +972-52-6558841) - -## Potential P.R venues / Storythons ## +For any questions, contact Ron Gross (ron.gross@gmail.com, +972-52-6558841) -* http://tisch.nyu.edu/itp +## Marketing ## +See [Our Marketing Plan](https://github.com/groupwrite-io/groupwrite.io/blob/master/Marketing.md) ## OKRs ## @@ -90,4 +104,4 @@ npm run unit # run e2e tests npm run e2e -``` \ No newline at end of file +``` diff --git a/api/admin.js b/api/admin.js index deb35a0..35acdc6 100644 --- a/api/admin.js +++ b/api/admin.js @@ -1,3 +1,4 @@ +const secret = require('../config/secret.config') var State = require('./state') module.exports = function (router) { diff --git a/api/api.js b/api/api.js index de4be8e..064844d 100644 --- a/api/api.js +++ b/api/api.js @@ -5,6 +5,8 @@ var session = require('express-session') var secret = require('../config/secret.config') var State = require('./state') +const pjson = require('../package.json'); + // Routes require('./stories')(router) @@ -12,17 +14,14 @@ require('./users')(router) require('./game')(router) require('./admin')(router) -// GET /state +// GET / router.get('/', function (req, res, next) { res.send('groupwrite.io API server') }) -// GET & POST /error (for testing) -router.get('/error', function () { - assert.fail('This returns a 500 error') -}) -router.post('/error', function () { - assert.fail('This returns a 500 error') +// GET /version +router.get('/version', function (req, res, next) { + res.send(pjson.version) }) module.exports = router diff --git a/api/colorPicker.js b/api/colorPicker.js new file mode 100644 index 0000000..8e6d41c --- /dev/null +++ b/api/colorPicker.js @@ -0,0 +1,18 @@ +const _ = require('underscore') +const colorPicker = {} +var _colors = [] + +colorPicker.init = function (colors) { + _colors = colors +} + +colorPicker.getColor = function (takenColors) { + const availableColors = _.difference(_colors, takenColors || []) + if (availableColors.length === 0) { + throw new Error('No colors available.') + } + const randomIndex = _.random(0, availableColors.length - 1) + return availableColors[randomIndex] +} + +module.exports = colorPicker diff --git a/api/game.js b/api/game.js index f5899ca..e8df94e 100644 --- a/api/game.js +++ b/api/game.js @@ -1,5 +1,7 @@ const assert = require('assert') const _ = require('underscore') +var bunyan = require('bunyan') +var log = require('../util/logger').getLogger() const State = require('./state') const server = require('../build/server') @@ -25,6 +27,24 @@ module.exports = function (router) { res.send(true) }) + // POST /submit + router.post('/submit', function (req, res, next) { + var playerId = req.body.playerId + var suggestionDisabled = req.body.suggestionDisabled + if (!playerId) { + res.status(422).send("Missing playerId") + return + } + var player = State.getPlayerById(playerId) + if (player == null) { + res.status(422).send(`Missing player for playerId ${playerId}`) + return + } + player.suggestionSubmitted = true + server.io.emit('server:state') + res.send(true) + }) + // POST /removeVote router.post('/removevote', function (req, res, next) { var voterId = req.body.voterId @@ -64,12 +84,16 @@ module.exports = function (router) { // https://github.com/groupwrite-io/groupwrite.io/issues/53 player.votedForId = votedForId - if (State.updateStory(player)) { - let game = State.findGameByPlayerId(player.id) - assert(game) + let game = State.findGameByPlayerId(player.id) + assert(game) + if (State.updateTitle(player)) { + State.roundOver(game) + server.io.emit('server:title-round-over') + } else if (State.updateStory(player)) { + State.roundOver(game) if (_.last(game.story.contributions).text === 'The End') { - console.log('Game finished, saving story') - let story = new Story({ contributions: game.story.contributions }) + log.info(`Game finished, saving story with players ${game.players}`) + let story = new Story({ contributions: game.story.contributions, title: game.story.title, players: game.players }) story.save().then(() => { game.story.id = story._id // TODO - convert this to 'server:game-over' instead of detecting on client diff --git a/api/model/story.js b/api/model/story.js index ba4348f..79abbe5 100644 --- a/api/model/story.js +++ b/api/model/story.js @@ -7,7 +7,9 @@ var mongoose = require('mongoose') var storySchema = new Schema({ date: { type: Date, default: Date.now }, - contributions: [] + contributions: [], + title: {}, + players: [] }); module.exports = mongoose.model('Story', storySchema) \ No newline at end of file diff --git a/api/state.js b/api/state.js index fe13a28..0850b1e 100644 --- a/api/state.js +++ b/api/state.js @@ -1,6 +1,11 @@ var uuid = require('uuid/v4') -var values = require('object.values'); -var config = require('../config/server.config'); +var values = require('object.values') +var config = require('../config/server.config') +var bunyan = require('bunyan') +var log = require('../util/logger').getLogger() + +var colorPicker = require('./colorPicker') +colorPicker.init(['blue', 'red', 'green']) var State = {} @@ -15,7 +20,7 @@ State.gameToStr = function (game) { for (var playerId of game.playerIds) { result += playerId } - return result; + return result } State.addPlayer = function (player) { @@ -29,23 +34,33 @@ State.addPlayer = function (player) { startTime: Date.now(), id, playerIds: State.queue, + players: Object.values(State.players).filter(p => State.queue.includes(p.id)), story: { - contributions: [] + contributions: [], + title: {} } } State.games[game.id] = game - console.log(`Created game ${State.gameToStr(game)}`) + log.info(`Created game ${State.gameToStr(game)}`) + + // Apply colors + const selectedColors = [] + game.playerIds.forEach(playerId => { + const player = State.getPlayerById(playerId) + player.color = colorPicker.getColor(selectedColors) + selectedColors.push(player.color) + }) // Clear queue State.queue = [] } - console.log(`Player logged in: Added ${player.nickname}, ${player.id} to player array of length ${State.players.length}`) + log.info(`Player logged in: Added ${player.nickname}, ${player.id} to player array of length ${State.players.length}`) } State.removePlayer = function (playerId) { if (!State.players[playerId]) { - console.log(config.noPlayerFoundMessage(playerId)) + log.warn(config.noPlayerFoundMessage(playerId)) return } @@ -53,12 +68,12 @@ State.removePlayer = function (playerId) { delete State[playerId] State.queue = State.queue.filter((qPlayerId) => qPlayerId !== playerId) - console.log(`Player quit: ${player.nickname}, ${player.id}`) + log.info(`Player quit: ${player.nickname}, ${player.id}`) } State.getPlayerById = function (playerId) { if (!State.players[playerId]) { - console.log(config.noPlayerFoundMessage(playerId)) + log.warn(config.noPlayerFoundMessage(playerId)) return } @@ -67,8 +82,6 @@ State.getPlayerById = function (playerId) { // Returns the state as seen by a particular player State.getStateByPlayerId = function (playerId) { - let filteredState = {} - // Find current game for the player let game = values(State.games).find((g) => g.playerIds.includes(playerId)) @@ -101,7 +114,7 @@ State.getAdminState = function () { State.getPlayerById = function (playerId) { if (!State.players[playerId]) { - console.log(config.noPlayerFoundMessage(playerId)) + log.warn(config.noPlayerFoundMessage(playerId)) return null } return State.players[playerId] @@ -125,10 +138,17 @@ State.findRoundWinner = function (game) { } } } - return null } +State.roundOver = function (game) { + // reset suggestionSubmitted + for (let playerId of game.playerIds) { + let player = State.players[playerId] + player.suggestionSubmitted = false + } +} + State.findGameByPlayerId = function (playerId) { for (let gameId in State.games) { let game = State.games[gameId] @@ -142,27 +162,30 @@ State.findGameByPlayerId = function (playerId) { /** * Update the current story by votes (and in the future, check if a player has finalized their suggestion) - * + * * Returns true if the story has been updated */ State.updateStory = function (player) { let game = State.findGameByPlayerId(player.id) if (!game) { - console.log(`No current game for player ${player.Id}`) + log.warn(`No current game for player ${player.Id}`) return false } // Check if a player has majority vote - let roundWinner = State.findRoundWinner(game); + let roundWinner = State.findRoundWinner(game) if (!roundWinner) { return false } - console.log(`Round over in game ${game.id}, winner=${roundWinner.id}. Appending to ongoing story: ${roundWinner.suggestion}`) + log.info(`Round over in game ${game.id}, winner=${roundWinner.id}. Appending to ongoing story: ${roundWinner.suggestion}`) let contribution = { playerId: roundWinner.id, - text: roundWinner.suggestion + text: roundWinner.suggestion, + color: roundWinner.color } + + // game.story.title = contribution game.story.contributions.push(contribution) // Clear votes @@ -175,6 +198,53 @@ State.updateStory = function (player) { return true } +/** + * Test for first round (no contributions) and set new story title + * + * Returns true if the title has been updated + */ + +State.updateTitle = function (player) { + log.info('updating title') + + let game = State.findGameByPlayerId(player.id) + if (!game) { + log.warn(`No current game for player ${player.Id}`) + return false + } + + // Check if story has a title + if (game.story.title.text) { + return false + } + + // Check if a player has majority vote + let roundWinner = State.findRoundWinner(game) + if (!roundWinner) { + return false + } + + // Check if this isn't the first round + if (game.story.contribution) { + return false + } + + log.info(`Round over in game ${game.id}, winner=${roundWinner.id}. Appending to ongoing story: ${roundWinner.suggestion}`) + let title = { + playerId: roundWinner.id, + text: roundWinner.suggestion + } + game.story.title = title + + // Clear votes and suggestions + for (let playerId of game.playerIds) { + State.players[playerId].votedForId = null + State.players[playerId].suggestion = '' + } + + return true +} + State.clearAll() -module.exports = State; +module.exports = State diff --git a/api/stories.js b/api/stories.js index 55e95a4..f53097b 100644 --- a/api/stories.js +++ b/api/stories.js @@ -1,4 +1,3 @@ -var State = require('./state') var Story = require('./model/story') module.exports = function (router) { diff --git a/api/users.js b/api/users.js index 9d6cbb7..2497be9 100644 --- a/api/users.js +++ b/api/users.js @@ -1,5 +1,7 @@ var State = require('./state') var server = require('../build/server') +var bunyan = require('bunyan') +var log = require('../util/logger').getLogger() module.exports = function (router) { // POST /user/login @@ -21,7 +23,7 @@ module.exports = function (router) { // return // } req.session.playerId = playerId - console.log(`/user/login Saved playerId ${playerId} on session ${req.session.id}`) + log.info(`/user/login Saved playerId ${playerId} on session ${req.session.id}`) State.addPlayer({ id: playerId, nickname diff --git a/build/dev-server.js b/build/dev-server.js index 91d0c51..5b34558 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -11,6 +11,7 @@ var webpackConfig = process.env.NODE_ENV === 'testing' var expressWinston = require('express-winston'); var winston = require('winston'); // for transports.Console var api = require('../api/api'); +var log = require('../util/logger').getLogger() var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); @@ -25,19 +26,19 @@ mongoose.Promise = global.Promise mongoose.plugin(autoIncrement.mongoosePlugin); if (secret.mongoConnectionString === 'mongo-in-memory') { - console.log('Using dev mongo-in-memory') + log.info('Using dev mongo-in-memory') const MongoInMemory = require('mongo-in-memory'); var mongoPort = 8000; var mongoServerInstance = new MongoInMemory(mongoPort); //DEFAULT PORT is 27017 mongoServerInstance.start((error, config) => { if (error) { - console.error(error); + log.error(error); } else { //callback when server has started successfully - console.log("HOST " + config.host); - console.log("PORT " + config.port); + log.info("HOST " + config.host); + log.info("PORT " + config.port); var mongouri = mongoServerInstance.getMongouri("groupwrite-prod"); mongoose.connect(mongouri).then(() => { @@ -55,14 +56,22 @@ if (secret.mongoConnectionString === 'mongo-in-memory') { // } // ) }).catch((err) => { - console.log(err) + log.error(err) process.exit() }) } }) } else { - console.log('Connecting to mongoose') - mongoose.connect(secret.mongoConnectionString) + log.info('Connecting to mongodb') + mongoose.connect(secret.mongoConnectionString).then(() => { + log.info('Successfully connected to mongodb') + mongoose.connection.db.collection('startups').save({ + 'date': Date.now() + }) + }).catch((err) => { + log.error(err) + process.exit() + }) } var app = express() @@ -128,7 +137,7 @@ var port = process.env.PORT || config.dev.port app.set('port', port) var uri = 'http://localhost:' + port devMiddleware.waitUntilValid(function () { - console.log('> Listening at ' + uri + '\n') + log.info('> Listening at ' + uri + '\n') }) // Register handlebars templates @@ -175,9 +184,14 @@ app.use(function (err, req, res, next) { res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page - console.error(err.stack) + log.error(err.stack) res.status(err.status || 500); res.render('error'); }); +if (secret.bugsnagId) { + var bugsnag = require("bugsnag") + bugsnag.register(secret.bugsnagId) +} + module.exports = app; \ No newline at end of file diff --git a/build/server.js b/build/server.js index d31b35f..23af8c6 100644 --- a/build/server.js +++ b/build/server.js @@ -6,7 +6,8 @@ var app = require('./dev-server.js') var debug = require('debug')('writing.io:server'); var http = require('http'); var assert = require('assert') -var opn = require('opn') +var bunyan = require('bunyan') +var log = require('../util/logger').getLogger() /** * Create HTTP server. @@ -23,25 +24,25 @@ io.use(sharedsession(app.session, { autoSave: true })); -console.log("Starting socket.io"); +log.info("Starting socket.io"); io.on('connection', function (socket) { var session = socket.handshake.session - console.log(`a user connected, sessionId=${session.id}`); + log.info(`a user connected, sessionId=${session.id}`); socket.on('disconnect', function () { if (session.playerId) { var playerId = session.playerId var nickname = session.nickname - console.log(`socket disconnected, sessionId=${session.id}, playerId=${playerId}, nickname=${nickname}`); + log.info(`socket disconnected, sessionId=${session.id}, playerId=${playerId}, nickname=${nickname}`); State.removePlayer(playerId) io.emit('server:state') } else { - console.log(`socket disconnected, sessionId=${session.id}`); + log.info(`socket disconnected, sessionId=${session.id}`); } }); }); -console.log('Successfully Started socket.io'); +log.info('Successfully Started socket.io'); /** * Listen on provided port, on all network interfaces. @@ -49,17 +50,13 @@ console.log('Successfully Started socket.io'); var port = app.get('port') server.listen(port, function (err) { if (err) { - console.log(err) + log.error(err) return } - // when env is testing, don't need open it - if (process.env.NODE_ENV !== 'testing') { - var uri = 'http://localhost:' + port - opn(uri) - } + log.info(`Go to http://localhost:${port}`) }) -console.log(`Server listening on port ${port}`) +log.info(`Started server listening on port ${port}`) server.on('error', onError); server.on('listening', onListening); diff --git a/config/dev.secret.config.js b/config/dev.secret.config.js index 6b80106..fa85f50 100644 --- a/config/dev.secret.config.js +++ b/config/dev.secret.config.js @@ -1,6 +1,5 @@ module.exports = { - // This should be encrypted - // https://github.com/groupwrite-io/groupwrite.io/issues/37 + // Dev versions only, the production versions are encrypted in prod.secret.config.js.enc adminKey: 'nalkFaoKsjd78', sessionSecret: 'my-secret', mongoConnectionString: 'mongo-in-memory' diff --git a/config/prod.secret.config.js.enc b/config/prod.secret.config.js.enc index e619627..a21f9ce 100644 Binary files a/config/prod.secret.config.js.enc and b/config/prod.secret.config.js.enc differ diff --git a/config/secret.config.js b/config/secret.config.js index de334a9..5528094 100644 --- a/config/secret.config.js +++ b/config/secret.config.js @@ -1,14 +1,28 @@ +// Read standard config +let config; const mode = process.env.mode - +console.log(`Loading secret config, mode='${mode}'`) switch (mode) { case 'prod': - module.exports = require('./prod.secret.config') - break; + config = require('./prod.secret.config') + break case 'dev': default: - module.exports = require('./dev.secret.config') - break; + config = require('./dev.secret.config') + break +} + +// Local file overrides if it exists +const fs = require('fs') +const path = require('path') +const localConfigFile = path.join(__dirname, '/local.secret.config.js') +if (fs.existsSync(localConfigFile)) { + console.log('Overriding with local config file') + var localConfig = require(localConfigFile) + Object.assign(config, localConfig) +} else { + console.log('Not overriding') } -console.log(`Secret config loaded for mode '${mode}'`) \ No newline at end of file +module.exports = config diff --git a/index.html b/index.html index fa414fa..454a066 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,17 @@ - +
-
In order to find your story in the future, just click here.
+Please take 2 minutes to complete this short survey. It would mean the world to us!