memo/server.js

901 lines
22 KiB
JavaScript

const http = require('http')
const express = require('express')
const compression = require('compression')
const sanitizer = require('sanitizer')
const socketIO = require('socket.io')
const reload = require('reload')
const conf = require('./config.js').server
const { headerBarUrl, logoUrl } = require('./config.js')
const app = express()
const router = express.Router()
app.use(compression())
app.use(conf.baseurl, router)
router.use(express.static(`${__dirname}/node_modules`))
router.use(express.static(`${__dirname}/client`))
const server = http.createServer(app)
const io = socketIO(server, { // Use the renamed socketIO require
path: conf.baseurl === '/' ? '' : `${conf.baseurl}/socket.io`,
cookie: false
})
/** ************
LOCAL INCLUDES
************* */
const rooms = require('./lib/rooms.js')
let data = require('./lib/data.js').db
/** ************
GLOBALS
************* */
// Map of sids to user_names
const sids_to_user_names = []
// Reload code here
reload(app)
.then((reloadReturned) => {
// reloadReturned is documented in the returns API in the README
// Reload started, start web server
server.listen(conf.port, () => {
console.log(`Web server available on http://127.0.0.1:${conf.port}`)
})
})
.catch((err) => {
console.error(
'Reload could not start, could not start server/sample app',
err
)
})
/** ************
SETUP Socket.IO
************* */
/** ************
ROUTES
************* */
router.get('/', (req, res) => {
// console.log(req.header('host'));
url = req.header('host') + req.baseUrl
const { connected } = io.sockets
clientsCount = Object.keys(connected).length
res.render('home.jade', {
url,
headerBarUrl,
logoUrl,
connected: clientsCount,
home: true
})
})
router.get('/demo', (req, res) => {
url = req.header('host') + req.baseUrl
res.render('index.jade', {
pageTitle: 'Memo - demo',
headerBarUrl,
logoUrl,
url,
demo: true
})
})
router.get('/:id', (req, res) => {
url = req.header('host') + req.baseUrl
res.render('index.jade', {
pageTitle: `Memo - ${req.params.id}`,
headerBarUrl,
logoUrl,
url
})
})
router.get('/stats', (req, res) => {
console.log('TODO: stats')
})
/** ************
SOCKET.I0
************* */
// sanitizes text
function scrub(text) {
if (typeof text != 'undefined' && text !== null) {
// clip the string if it is too long
if (text.length > 65535) {
text = text.substr(0, 65535)
}
return sanitizer.sanitize(text)
}
return null
}
io.sockets.on('connection', (client) => {
client.on('message', (message) => {
// console.log(message.action + " -- " + sys.inspect(message.data) );
let clean_data = {}
let clean_message = {}
let message_out = {}
if (!message.action) return
switch (message.action) {
case 'initializeMe':
initClient(client)
break
case 'joinRoom':
joinRoom(client, message.data, (clients) => {
client.json.send({ action: 'roomAccept', data: '' })
})
break
case 'editBoardMetas':
const id = scrub(message.data.id)
const prop = scrub(message.data.prop)
const value = scrub(message.data.value)
const clean_data = { id, prop, value }
getRoom(client, (room) => {
const boardMeta = {
prop,
value
}
// Enregistre les metas dans Redis
db.createBoardMetas(room, id, boardMeta)
})
const message_out = {
action: 'editBoardMetas',
data: clean_data
}
broadcastToRoom(client, message_out)
break
case 'moveCard':
// report to all other browsers
message_out = {
action: message.action,
data: {
id: scrub(message.data.id),
position: {
left: scrub(message.data.position.left),
top: scrub(message.data.position.top)
}
}
}
broadcastToRoom(client, message_out)
// console.log("-----" + message.data.id);
// console.log(JSON.stringify(message.data));
getRoom(client, (room) => {
db.cardSetXY(
room,
message.data.id,
message.data.position.left,
message.data.position.top
)
})
break
case 'createCard':
data = message.data
clean_data = {}
clean_data.text = scrub(data.text)
clean_data.id = scrub(data.id)
clean_data.x = scrub(data.x)
clean_data.y = scrub(data.y)
clean_data.rot = scrub(data.rot)
clean_data.colour = scrub(data.colour)
getRoom(client, (room) => {
createCard(
room,
clean_data.id,
clean_data.text,
clean_data.x,
clean_data.y,
clean_data.rot,
clean_data.colour
)
})
message_out = {
action: 'createCard',
data: clean_data
}
// report to all other browsers
broadcastToRoom(client, message_out)
break
case 'editCard':
clean_data = {}
clean_data.value = scrub(message.data.value)
clean_data.id = scrub(message.data.id)
// send update to database
getRoom(client, (room) => {
db.cardEdit(room, clean_data.id, clean_data.value)
})
message_out = {
action: 'editCard',
data: clean_data
}
broadcastToRoom(client, message_out)
break
case 'deleteCard':
clean_message = {
action: 'deleteCard',
data: { id: scrub(message.data.id) }
}
getRoom(client, (room) => {
db.deleteCard(room, clean_message.data.id)
})
// report to all other browsers
broadcastToRoom(client, clean_message)
break
case 'createColumn':
clean_message = { data: scrub(message.data) }
getRoom(client, (room) => {
db.createColumn(room, clean_message.data, () => {})
})
broadcastToRoom(client, clean_message)
break
case 'deleteColumn':
getRoom(client, (room) => {
db.deleteColumn(room)
})
broadcastToRoom(client, { action: 'deleteColumn' })
break
case 'updateColumns':
var columns = message.data
if (!(columns instanceof Array)) break
var clean_columns = []
for (const i in columns) {
clean_columns[i] = scrub(columns[i])
}
getRoom(client, (room) => {
db.setColumns(room, clean_columns)
})
broadcastToRoom(client, {
action: 'updateColumns',
data: clean_columns
})
break
case 'changeTheme':
clean_message = {}
clean_message.data = scrub(message.data)
getRoom(client, (room) => {
db.setTheme(room, clean_message.data)
})
clean_message.action = 'changeTheme'
broadcastToRoom(client, clean_message)
break
case 'setUserName':
clean_message = {}
clean_message.data = scrub(message.data)
setUserName(client, clean_message.data)
var msg = {}
msg.action = 'nameChangeAnnounce'
msg.data = { sid: client.id, user_name: clean_message.data }
broadcastToRoom(client, msg)
break
case 'addSticker':
var cardId = scrub(message.data.cardId)
var stickerId = scrub(message.data.stickerId)
getRoom(client, (room) => {
db.addSticker(room, cardId, stickerId)
})
broadcastToRoom(client, {
action: 'addSticker',
data: { cardId, stickerId }
})
break
case 'setBoardSize':
var size = {}
size.width = scrub(message.data.width)
size.height = scrub(message.data.height)
getRoom(client, (room) => {
db.setBoardSize(room, size)
})
broadcastToRoom(client, { action: 'setBoardSize', data: size })
break
case 'exportTxt':
exportBoard('txt', client, message.data)
break
case 'exportCsv':
exportBoard('csv', client, message.data)
break
case 'exportJson':
exportJson(client, message.data)
break
case 'importJson':
importJson(client, message.data)
break
case 'createRevision':
createRevision(client, message.data)
break
case 'deleteRevision':
deleteRevision(client, message.data)
break
case 'exportRevision':
exportRevision(client, message.data)
break
default:
// console.log('unknown action');
break
}
})
client.on('disconnect', () => {
leaveRoom(client)
})
// tell all others that someone has connected
// client.broadcast('someone has connected');
})
/** ************
FUNCTIONS
************* */
function initClient(client) {
// console.log ('initClient Started');
getRoom(client, (room) => {
db.getAllCards(room, (cards) => {
client.json.send({
action: 'initCards',
data: cards
})
})
db.getAllColumns(room, (columns) => {
client.json.send({
action: 'initColumns',
data: columns
})
})
db.getRevisions(room, (revisions) => {
client.json.send({
action: 'initRevisions',
data: revisions !== null ? Object.keys(revisions) : []
})
})
db.getTheme(room, (theme) => {
if (theme === null) theme = 'bigcards'
client.json.send({
action: 'changeTheme',
data: theme
})
})
db.getBoardSize(room, (size) => {
if (size !== null) {
client.json.send({
action: 'setBoardSize',
data: size
})
}
})
db.getBoardMetas(room, (metas) => {
if (metas) {
for (const id in metas) {
const meta = metas[id]
client.json.send({
action: 'editBoardMetas',
data: {
id,
prop: meta.prop,
value: meta.value
}
})
}
}
})
roommates_clients = rooms.room_clients(room)
roommates = []
let j = 0
for (const i in roommates_clients) {
if (roommates_clients[i].id != client.id) {
roommates[j] = {
sid: roommates_clients[i].id,
user_name: sids_to_user_names[roommates_clients[i].id]
}
j++
}
}
// console.log('initialusers: ' + roommates);
client.json.send({
action: 'initialUsers',
data: roommates
})
})
}
function joinRoom(client, room, successFunction) {
const msg = {}
msg.action = 'join-announce'
msg.data = { sid: client.id, user_name: client.user_name }
rooms.add_to_room_and_announce(client, room, msg)
successFunction()
}
function leaveRoom(client) {
// console.log (client.id + ' just left');
const msg = {}
msg.action = 'leave-announce'
msg.data = { sid: client.id }
rooms.remove_from_all_rooms_and_announce(client, msg)
delete sids_to_user_names[client.id]
}
function broadcastToRoom(client, message) {
rooms.broadcast_to_roommates(client, message)
}
// ----------------CARD FUNCTIONS
function createCard(room, id, text, x, y, rot, colour) {
const card = {
id,
colour,
rot,
x,
y,
text,
sticker: null
}
db.createCard(room, id, card)
}
function roundRand(max) {
return Math.floor(Math.random() * max)
}
// ------------ROOM STUFF
// Get Room name for the given Session ID
function getRoom(client, callback) {
room = rooms.get_room(client)
// console.log( 'client: ' + client.id + " is in " + room);
callback(room)
}
function setUserName(client, name) {
client.user_name = name
sids_to_user_names[client.id] = name
// console.log('sids to user names: ');
console.dir(sids_to_user_names)
}
function cleanAndInitializeDemoRoom() {
// DUMMY DATA
db.clearRoom('/demo', () => {
db.createColumn('/demo', 'Pas commencé')
db.createColumn('/demo', 'Commencé')
db.createColumn('/demo', 'En test')
db.createColumn('/demo', 'Validation')
db.createColumn('/demo', 'Terminé')
createCard(
'/demo',
'card1',
"Salut, c'est fun",
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'yellow'
)
createCard(
'/demo',
'card2',
"Salut, c'est une nouvelle histoire.",
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'white'
)
createCard(
'/demo',
'card3',
'.',
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'blue'
)
createCard(
'/demo',
'card4',
'.',
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'green'
)
createCard(
'/demo',
'card5',
"Salut, c'est fun",
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'yellow'
)
createCard(
'/demo',
'card6',
"Salut, c'est un nouveau mémo.",
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'yellow'
)
createCard(
'/demo',
'card7',
'.',
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'blue'
)
createCard(
'/demo',
'card8',
'.',
roundRand(600),
roundRand(300),
Math.random() * 10 - 5,
'green'
)
})
}
// Export board in txt or csv
function exportBoard(format, client, data) {
const result = []
getRoom(client, (room) => {
db.getAllCards(room, (cards) => {
db.getAllColumns(room, (columns) => {
const text = []
const cols = {}
if (columns.length > 0) {
for (var i = 0; i < columns.length; i++) {
cols[columns[i]] = []
for (var j = 0; j < cards.length; j++) {
if (i === 0) {
if (cards[j].x < (i + 1) * data) {
cols[columns[i]].push(cards[j])
}
} else if (i + 1 === columns.length) {
if (cards[j].x >= i * data) {
cols[columns[i]].push(cards[j])
}
} else if (
cards[j].x >= i * data
&& cards[j].x < (i + 1) * data
) {
cols[columns[i]].push(cards[j])
}
}
cols[columns[i]].sort((a, b) => {
if (a.y === b.y) {
return a.x - b.x
}
return a.y - b.y
})
}
if (format === 'txt') {
for (var i = 0; i < columns.length; i++) {
if (i === 0) {
text.push(`# ${columns[i]}`)
} else {
text.push(`\n# ${columns[i]}`)
}
for (var j = 0; j < cols[columns[i]].length; j++) {
text.push(`- ${cols[columns[i]][j].text}`)
}
}
} else if (format === 'csv') {
let max = 0
let line = []
const patt_vuln = new RegExp('^[=+-@]')
for (var i = 0; i < columns.length; i++) {
if (cols[columns[i]].length > max) {
max = cols[columns[i]].length
}
var val = columns[i].replace(/"/g, '""')
if (patt_vuln.test(val)) {
// prevent CSV Formula Injection
var val = `'${val}`
}
line.push(`"${val}"`)
}
text.push(line.join(','))
for (var j = 0; j < max; j++) {
line = []
for (var i = 0; i < columns.length; i++) {
var val = cols[columns[i]][j] !== undefined
? cols[columns[i]][j].text.replace(/"/g, '""')
: ''
if (patt_vuln.test(val)) {
// prevent CSV Formula Injection
var val = `'${val}`
}
line.push(`"${val}"`)
}
text.push(line.join(','))
}
}
} else {
for (var j = 0; j < cards.length; j++) {
if (format === 'txt') {
text.push(`- ${cards[j].text}`)
} else if (format === 'csv') {
text.push(`"${cards[j].text.replace(/"/g, '""')}"\n`)
}
}
}
let result
if (format === 'txt' || format === 'csv') {
result = text.join('\n')
} else if (format === 'json') {
result = JSON.stringify(cols)
}
client.json.send({
action: 'export',
data: {
filename: `${room.replace('/', '')}.${format}`,
text: result
}
})
})
})
})
}
// Export board in json, suitable for import
function exportJson(client, data) {
let result = []
getRoom(client, (room) => {
db.getAllCards(room, (cards) => {
db.getAllColumns(room, (columns) => {
db.getTheme(room, (theme) => {
db.getBoardSize(room, (size) => {
if (theme === null) theme = 'bigcards'
if (size === null) { size = { width: data.width, height: data.height } }
result = JSON.stringify({
cards,
columns,
theme,
size
})
client.json.send({
action: 'export',
data: {
filename: `${room.replace('/', '')}.json`,
text: result
}
})
})
})
})
})
})
}
// Import board from json
function importJson(client, data) {
getRoom(client, (room) => {
db.clearRoom(room, () => {
db.getAllCards(room, (cards) => {
for (var i = 0; i < cards.length; i++) {
db.deleteCard(room, cards[i].id)
}
cards = data.cards
const cards2 = []
for (var i = 0; i < cards.length; i++) {
const card = cards[i]
if (
card.id !== undefined
&& card.colour !== undefined
&& card.rot !== undefined
&& card.x !== undefined
&& card.y !== undefined
&& card.text !== undefined
&& card.sticker !== undefined
) {
const c = {
id: card.id,
colour: card.colour,
rot: card.rot,
x: card.x,
y: card.y,
text: scrub(card.text),
sticker: card.sticker
}
db.createCard(room, c.id, c)
cards2.push(c)
}
}
const msg = { action: 'initCards', data: cards2 }
broadcastToRoom(client, msg)
client.json.send(msg)
})
db.getAllColumns(room, (columns) => {
for (var i = 0; i < columns.length; i++) {
db.deleteColumn(room)
}
columns = data.columns
const columns2 = []
for (var i = 0; i < columns.length; i++) {
const column = scrub(columns[i])
if (typeof column === 'string') {
db.createColumn(room, column)
columns2.push(column)
}
}
msg = { action: 'initColumns', data: columns2 }
broadcastToRoom(client, msg)
client.json.send(msg)
})
let { size } = data
if (size.width !== undefined && size.height !== undefined) {
size = { width: scrub(size.width), height: scrub(size.height) }
db.setBoardSize(room, size)
msg = { action: 'setBoardSize', data: size }
broadcastToRoom(client, msg)
client.json.send(msg)
}
data.theme = scrub(data.theme)
if (data.theme === 'smallcards' || data.theme === 'bigcards') {
db.setTheme(room, data.theme)
msg = { action: 'changeTheme', data: data.theme }
broadcastToRoom(client, msg)
client.json.send(msg)
}
})
})
}
//
function createRevision(client, data) {
let result = []
getRoom(client, (room) => {
db.getAllCards(room, (cards) => {
db.getAllColumns(room, (columns) => {
db.getTheme(room, (theme) => {
db.getBoardSize(room, (size) => {
if (theme === null) theme = 'bigcards'
if (size === null) { size = { width: data.width, height: data.height } }
result = {
cards,
columns,
theme,
size
}
const timestamp = Date.now()
db.getRevisions(room, (revisions) => {
if (revisions === null) revisions = {}
revisions[`${timestamp}`] = result
db.setRevisions(room, revisions)
msg = { action: 'addRevision', data: timestamp }
broadcastToRoom(client, msg)
client.json.send(msg)
})
})
})
})
})
})
}
function deleteRevision(client, timestamp) {
getRoom(client, (room) => {
db.getRevisions(room, (revisions) => {
if (revisions !== null && revisions[`${timestamp}`] !== undefined) {
delete revisions[`${timestamp}`]
db.setRevisions(room, revisions)
}
msg = { action: 'deleteRevision', data: timestamp }
broadcastToRoom(client, msg)
client.json.send(msg)
})
})
}
function exportRevision(client, timestamp) {
getRoom(client, (room) => {
db.getRevisions(room, (revisions) => {
if (revisions !== null && revisions[`${timestamp}`] !== undefined) {
client.json.send({
action: 'export',
data: {
filename: `${room.replace('/', '')}-${timestamp}.json`,
text: JSON.stringify(revisions[`${timestamp}`])
}
})
} else {
client.json.send({
action: 'message',
data: `Unable to find revision ${timestamp}.`
})
}
})
})
}
/** ************
SETUP DATABASE ON FIRST RUN
************* */
// (runs only once on startup)
var db = new data(() => {
cleanAndInitializeDemoRoom()
})