diff --git a/.gitignore b/.gitignore index 6049466..99ff8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules *.log .*.sw* +/lib diff --git a/bin/xat b/bin/xat old mode 100644 new mode 100755 diff --git a/bin/xat-production b/bin/xat-production new file mode 100755 index 0000000..4efbb4b --- /dev/null +++ b/bin/xat-production @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +var Application; + +Application = new (require('../lib/bootstrap')) diff --git a/bin/xat-test b/bin/xat-test new file mode 120000 index 0000000..4cd0af0 --- /dev/null +++ b/bin/xat-test @@ -0,0 +1 @@ +xat-production \ No newline at end of file diff --git a/package.json b/package.json index 5d3af1d..cfd73c4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "scripts": { "start": "node bin/xat", "lint": "coffeelint src", - "test": "mocha --require coffee-script/register --compilers coffee:coffee-script --recursive test" + "test": "script/test", + "compile": "coffee --compile --output lib src", + "start-production": "node bin/xat-production" } } diff --git a/script/install b/script/install old mode 100644 new mode 100755 diff --git a/script/run b/script/run old mode 100644 new mode 100755 diff --git a/script/test b/script/test index 3f6ad98..5eb836e 100755 --- a/script/test +++ b/script/test @@ -1,4 +1,6 @@ #!/usr/bin/env bash -coffee src/test/db-init.coffee -mocha --compilers coffee:coffee-script/register "$@" +coffee test/lib/db-init.coffee \ + && echo "Database init OK" \ + && npm run compile \ + && mocha --compilers coffee:coffee-script/register "$@" diff --git a/src/packet-builders/chat.coffee b/src/packet-builders/chat.coffee new file mode 100644 index 0000000..c3e4019 --- /dev/null +++ b/src/packet-builders/chat.coffee @@ -0,0 +1,29 @@ +builder = require '../utils/builder' + +Rank = require '../structures/rank' + +module.exports = + buildMeta: (client) -> + ## Chat settings and info + ## r: 1 - (All main owner) / 2 - (All moderator) / 3 - (All member) / 4 (All owner) + ## v: 1 - (Normal) / 3 - (w_VIP) / 4 - (w_ALLP) / other - (All unregistered) + packet = builder.create('i') + packet.append('b', "#{client.chat.bg};=#{client.chat.attached.name || ''};=#{client.chat.attached.id || ''};=#{client.chat.language};=#{client.chat.radio};=#{client.chat.button}") + packet.append('r', client.chat.rank.toNumber()) if client.chat.rank != Rank.GUEST + packet.append('f', '21233728') + packet.append('v', client.user.registered? && 1 | 0) + packet.append('cb', '2387') + + buildPowers: (client) -> + packet = builder.create('gp') + packet.append('p', '0|0|1163220288|1079330064|20975876|269549572|16645|4210689|1|4194304|0|0|0|') + packet.append('g180', "{'m':'','d':'','t':'','v':1}") + packet.append('g256', "{'rnk':'8','dt':120,'rc':'1','v':1}") + packet.append('g100', 'assistance,1lH2M4N,xatalert,1e7wfSx') + packet.append('g114', "{'m':'Lobby','t':'Staff','rnk':'8','b':'Jail','brk':'8','v':1}") + packet.append('g112', 'Welcome to the lobby! Visit assistance and help pages.') + packet.append('g246', "{'rnk':'8','dt':30,'rt':'10','rc':'1','tg':1000,'v':1}") + packet.append('g90', 'shit,faggot,slut,cum,nigga,niqqa,prostitute,ixat,azzhole,tits,dick,sex,fuk,fuc,thot') + packet.append('g80', "{'mb':'11','ubn':'8','mbt':24,'ss':'8','rgd':'8','prm':'14','bge':'8','mxt':60,'sme':'11','dnc':'11','bdg':'11','yl':'10','rc':'10','p':'7','ka':'7'}") + packet.append('g74', 'd,waiting,astonished,swt,crs,un,redface,evil,rolleyes,what,aghast,omg,smirk') + packet.append('g106', 'c#sloth') diff --git a/src/packet-builders/message.coffee b/src/packet-builders/message.coffee new file mode 100644 index 0000000..5082e57 --- /dev/null +++ b/src/packet-builders/message.coffee @@ -0,0 +1,22 @@ +builder = require '../utils/builder' + +module.exports = + # options: + # message + # client - instance of services.Client related to user-sender + # time - timestamp of message + # messageId - chat-unique (pool-unique?) id of message + buildNewMain: (options) -> + builder.create('m') + .append('u', options.client.user.id) + .append('t', options.message) + .append('E', options.time) + .append('r', options.client.chat.id) + .append('i', options.messageId || 0) + + buildOldMain: (message) -> + builder.create('m') + .append('u', message.uid) + .append('t', message.message) + .append('i', message.mid) + .append('s', '1') diff --git a/src/packet-builders/user.coffee b/src/packet-builders/user.coffee new file mode 100644 index 0000000..7a31171 --- /dev/null +++ b/src/packet-builders/user.coffee @@ -0,0 +1,44 @@ +profile = require '../workers/profile' +builder = require '../utils/builder' + +expandPacket = (packet, user, fillType) -> + packet.append('u', user.id) + .append('n', user.nickname) + .append('a', user.avatar) + .append('h', user.url) + .append('q', user.q) + .append('v', '0') + .append('cb', '0') + + packet.append('f', user.f) if user.f > 0 + + if user.registered + packet.append('N', user.username) + packet.append('d0', user.d0) + packet.append('d2', user.d2) if user.d2? + packet.appendRaw(user.pStr) + +# This method works assuming user with id=userId is online. +# This method is sync. +expandPacketWithOnlineUserData = (packet, client, fillType) -> + expandPacket(packet, client.user, fillType) + +expandPacketWithUserData = (packet, userId, fillType) -> + new Promise (resolve, reject) -> + client = global.Server.getClientById userId + if client? + expandPacketWithOnlineUserData(packet, client, fillType) + resolve(packet) + else + profile.getById(userId).then((data) -> + reject() if data.length < 1 + expandPacket(packet, data[0], fillType) + resolve(packet) + ).catch reject + +module.exports = + expandPacketWithOnlineUserData: expandPacketWithOnlineUserData + buildU: (client) -> + packet = builder.create('u') + expandPacketWithOnlineUserData(packet, client) + return packet diff --git a/src/services/client.coffee b/src/services/client.coffee index 51a6231..fb77a90 100644 --- a/src/services/client.coffee +++ b/src/services/client.coffee @@ -2,6 +2,7 @@ parser = require '../utils/parser' math = require '../utils/math' logger = require '../utils/logger' builder = require '../utils/builder' +userBuilder = require '../packet-builders/user' User = require '../workers/user' Chat = require '../workers/chat' @@ -73,9 +74,11 @@ class Client dupUser.socket.end() @id = @user.id - @setSuper() - Chat.joinRoom.call(@) + pool = parseInt parser.getAttribute(packet, 'p') + rankPass = parser.getAttribute(packet, 'r') + + Chat.joinRoom.call(@, pool, rankPass) ).catch((err) => @logger.log @logger.level.ERROR, err, null) when packetTag == "v" ### @@ -110,13 +113,18 @@ class Client Save user profile data @spec ### - return if not @user.authenicated or @user.guest + return if not @maySendMessages() - type = parser.getAttribute(packet, 't') + destId = parser.getAttribute(packet, 'u') - return if type is '/KEEPALIVE' - - @logger.log @logger.level.ERROR, "Unhandled user data update packet", null + switch + when type is '/KEEPALIVE' + return + when type.startsWith('/m') or type.startsWith('/e') or type.startsWith('/M') or type.startsWith('/r') + Chat.makeUser.call(@, {rank: type[1], userId: destId}).catch((err) => + @logger.log @logger.level.ERROR, "makeUser error", err) + else + @logger.log @logger.level.ERROR, "Unhandled user data update packet", null when packetTag == "z" and isSlash ### User profile @@ -142,33 +150,25 @@ class Client @routeZ(packet.compose(), userProfileId) else if type.substr(0, 2) is '/a' and userProfile != null packet = builder.create('z') - packet.append('N', @user.username) if @user.registered status = type.substr(2) # TODO: Show only for friends if status[0] == '_' - if status.substr(1) == 'NF' and not global.Server.rooms[@user.chat][userProfileId] + if status.substr(1) == 'NF' status = '/a_NF' else status = '/a_' else status = "/a#{Chat.getLink(@user.chat)}" - packet.append('t', status) + userBuilder.expandPacketWithOnlineUserData packet, @ - packet.append('b', '1') + packet.append('t', status) + # .append('b', '1') # what does 'b' means? .append('d', userProfileId) - .append('u', @user.id) - .append('po', '0') - .appendRaw(@user.pStr) .append('x', @user.xats || 0) .append('y', @user.days || 0) - .append('q', '3') - .append('n', @user.nickname) - .append('a', @user.avatar) - .append('h', @user.url) - .append('v', '2') @routeZ(packet.compose(), userProfileId) else if type is '/l' or type.substr(0, 2) is '/a' @@ -213,9 +213,11 @@ class Client Room pools @spec ### - @chat.onPool = packetTag.substr(1) - - Chat.joinRoom.call(@) + ### + Change pool + @spec + ### + Chat.changePool.call(@, parseInt packetTag.substr(1)) when packetTag == 'x' ### App diff --git a/src/services/database.coffee b/src/services/database.coffee index 1fa6490..1e0f031 100644 --- a/src/services/database.coffee +++ b/src/services/database.coffee @@ -13,20 +13,22 @@ Pool = mysql.createPool( database: config.mysql.database ) +_exec = (options, values) -> new Promise((resolve, reject) -> + Pool.getConnection (err, connection) -> + return reject(err) if err + + values = values || [] + + connection.query(options, values, (err, rows) -> + connection.release() + return reject(err) if err + resolve(rows) + ) +) + module.exports = # Ping database initialize: (cb) -> Pool.getConnection (err) -> cb err - - exec: (sql, values) -> new Promise((resolve, reject) -> - Pool.getConnection (err, connection) -> - reject(err) if err - - values = values || [] - - connection.query(sql, values, (err, rows) -> - connection.release() - reject(err) if err - resolve(rows) - ) - ) + exec: (sql, values) -> _exec({sql: sql}, values) + execJoin: (sql, values) -> _exec({sql: sql, nestTables: true}, values) diff --git a/src/structures/rank.coffee b/src/structures/rank.coffee new file mode 100644 index 0000000..faaa37f --- /dev/null +++ b/src/structures/rank.coffee @@ -0,0 +1,28 @@ +module.exports = +class Rank + _number: null + map = [0, 4, 2, 1, 3] + strmap = ['r', null, 'm', 'e', 'M'] + rankPool = [new Rank(0), new Rank(1), new Rank(2), new Rank(3), new Rank(4)] + + @GUEST = rankPool[0] + @MEMBER = rankPool[3] + @MODERATOR = rankPool[2] + @OWNER = rankPool[4] + @MAINOWNER = rankPool[1] + + ## For internal use. + constructor: (@_number) -> + undefined + + toNumber: -> @_number + + toString: -> strmap[@_number] + + ## Normal way to convert number to rank. + @fromNumber: (number) -> + return rankPool[number] + @fromString: (str) -> + return rankPool[strmap.indexOf str] + + compareTo: (rank) -> map[@_number] - map[rank._number] diff --git a/src/workers/chat.coffee b/src/workers/chat.coffee index f947ad5..984a851 100644 --- a/src/workers/chat.coffee +++ b/src/workers/chat.coffee @@ -4,147 +4,182 @@ parser = require '../utils/parser' math = require '../utils/math' builder = require '../utils/builder' logger = new (require '../utils/logger')(name: 'Chat') +userBuilder = require '../packet-builders/user' +messageBuilder = require '../packet-builders/message' +chatBuilder = require '../packet-builders/chat' -module.exports = - getLink: (chat) -> global.Application.config.domain + '/room/' + chat +Rank = require '../structures/rank' - joinRoom: -> - return if @user.chat < 1 - - database.exec('SELECT * FROM chats WHERE id = ? LIMIT 1', [@user.chat]).then((data) => - if @user.chat is 8 - @send '' - @send '' - @send '' - return - - @chat = data[0] if @chat is null - - return false if !@chat - - ## Push the user to the rooms object - if typeof global.Server.rooms[@user.chat] is 'object' - global.Server.rooms[@user.chat][@user.id] = @ - else - global.Server.rooms[@user.chat] = {} - global.Server.rooms[@user.chat][@user.id] = @ - - @chat.attached = try JSON.parse(@chat.attached) catch error then {} - @chat.onPool = @chat.onPool || 0 - - ## Chat settings and info - ## r: 1 - (All main owner) / 2 - (All moderator) / 3 - (All member) / 4 (All owner) - ## v: 1 - (Normal) / 3 - (w_VIP) / 4 - (w_ALLP) / other - (All unregistered) - packet = builder.create('i') - packet.append('b', "#{@chat.bg};=#{@chat.attached.name || ''};=#{@chat.attached.id || ''};=#{@chat.language};=#{@chat.radio};=#{@chat.button}") - packet.append('f', '21233728') - packet.append('v', '3') - packet.append('cb', '2387') - @send packet.compose() - - ## Chat group powers - packet = builder.create('gp') - packet.append('p', '0|0|1163220288|1079330064|20975876|269549572|16645|4210689|1|4194304|0|0|0|') - packet.append('g180', "{'m':'','d':'','t':'','v':1}") - packet.append('g256', "{'rnk':'8','dt':120,'rc':'1','v':1}") - packet.append('g100', 'assistance,1lH2M4N,xatalert,1e7wfSx') - packet.append('g114', "{'m':'Lobby','t':'Staff','rnk':'8','b':'Jail','brk':'8','v':1}") - packet.append('g112', 'Welcome to the lobby! Visit assistance and help pages.') - packet.append('g246', "{'rnk':'8','dt':30,'rt':'10','rc':'1','tg':1000,'v':1}") - packet.append('g90', 'shit,faggot,slut,cum,nigga,niqqa,prostitute,ixat,azzhole,tits,dick,sex,fuk,fuc,thot') - packet.append('g80', "{'mb':'11','ubn':'8','mbt':24,'ss':'8','rgd':'8','prm':'14','bge':'8','mxt':60,'sme':'11','dnc':'11','bdg':'11','yl':'10','rc':'10','p':'7','ka':'7'}") - packet.append('g74', 'd,waiting,astonished,swt,crs,un,redface,evil,rolleyes,what,aghast,omg,smirk') - packet.append('g106', 'c#sloth') - @send packet.compose() - - ## Chat pools - @send builder.create('w').append('v', "#{@chat.onPool} #{@chat.pool}").compose() - - ## Broadcast the current user - packet = builder.create('u') - packet.append('cb', '1443256921') - .append('s', '1') - .append('f', @user.f) - .append('u', @user.id) - .append('n', @user.nickname) - .append('q', '3') - .append('a', @user.avatar) - .append('h', @user.url) - .append('cb', '1443256921') - .append('v', '0') - - if @user.registered - packet.append('N', @user.username) - packet.append('d0', @user.d0) - packet.append('d2', @user.d2) if @user.d2 - packet.appendRaw(@user.pStr) +joinRoom = (poolId, rankPass) -> + return if @user.chat < 1 - @broadcast packet.compose() + database.execJoin('SELECT * FROM chats LEFT JOIN `ranks` ON (`chats`.id = `ranks`.chatid and `ranks`.userid = ?) WHERE `chats`.id = ? LIMIT 1', [@user.id, @user.chat]).then((data) => + if @user.chat is 8 + @send '' + @send '' + @send '' + return + + @chat = data[0].chats if @chat is null + + return false if !@chat + + ## Push the user to the rooms object + if typeof global.Server.rooms[@user.chat] isnt 'object' + global.Server.rooms[@user.chat] = {} + + global.Server.rooms[@user.chat][@user.id] = @ + + @chat.attached = try JSON.parse(@chat.attached) catch error then {} + ## if poolId undefined or 0 - should choose @chat.onPool, according to xat's protocol + ## However, real implementation is more complicated + ## 0 pool is default pool. In case there are more than one + ## regular pool (regular pools appear when there are too many users in chat), + ## chat engine should switch to less populated pool. + @chat.onPool = poolId || @chat.onPool || 0 + + @chat.rank = Rank.fromNumber(data[0].ranks.f & 7 || 0) + @chat.rank = Rank.MAINOWNER if @chat.pass == rankPass + @user.f = @chat.rank.toNumber() & 7 + + @setSuper() + + @send chatBuilder.buildMeta(@).compose() + @send chatBuilder.buildPowers(@).compose() + + ## Chat pools + @send builder.create('w').append('v', "#{@chat.onPool} #{@chat.pool}").compose() + + ## Broadcast the current user + @broadcast userBuilder.buildU(@).compose() - ## Room messages - database.exec('SELECT * FROM (SELECT * FROM messages WHERE id = ? AND pool = ? ORDER BY time DESC LIMIT 15) sub ORDER BY time ASC LIMIT 0,15', [ @user.chat, @chat.onPool ]).then((data) => - offline = new Array() - for message in data - continue if global.Server.rooms[@user.chat][message.uid]?.chat.onPool is @chat.onPool + ## Room messages + database.exec('SELECT * FROM (SELECT * FROM messages WHERE id = ? AND pool = ? ORDER BY time DESC LIMIT 15) sub ORDER BY time ASC LIMIT 0,15', [ @user.chat, @chat.onPool ]).then((data) => + offline = new Array() + for message in data + continue if global.Server.rooms[@user.chat][message.uid]?.chat.onPool is @chat.onPool + + if offline.indexOf(message.uid) is -1 packet = builder.create('o') packet.append('u', message.uid) .append('u', message.uid) .append('n', message.name) .append('a', message.avatar) + .append('s', 1) packet.append('N', message.registered) if message.registered isnt 'unregistered' - - if offline.indexOf(message.uid) is -1 - @send packet.compose() - offline.push(message.uid) - - for _, client of global.Server.rooms[@user.chat] - continue if client.id is @user.id or client.chat.onPool isnt @chat.onPool - - user = client.user - - packet = builder.create('u') - packet.append('cb', '1414865425') - packet.append('s', '1') - packet.append('f', user.f) - packet.append('u', user.id) - packet.append('q', '3') - packet.append('n', user.nickname) - packet.append('a', user.avatar) - packet.append('h', user.url) - packet.append('v', '0') - - if user.registered - packet.append('N', user.username) - packet.append('d0', user.d0) - packet.append('d2', user.d2) if user.d2 - packet.appendRaw(user.pStr) @send packet.compose() + offline.push(message.uid) - data.forEach((message) => - packet = builder.create('m') - packet.append('E', message.time) - .append('u', message.uid) - .append('t', message.message) - .append('s', '1') + for _, client of global.Server.rooms[@user.chat] + continue if client.id is @user.id or client.chat.onPool isnt @chat.onPool - @send packet.compose() - ) + packet = userBuilder.buildU(@) + packet.append('s', '1') - ## Scroll message - # database.exec('SELECT * FROM messages WHERE id = ? AND SUBSTRING(message FROM 0 FOR 2) ORDER BY time DESC LIMIT 1', [ @user.chat ]).then((data) -> - @send builder.create('m').append('t', "/s#{@chat.sc}").append('d', '123').compose() + @send packet.compose() - ## Done packet - @send '' - # ) + data.forEach((message) => + packet = messageBuilder.buildOldMain message + + @send packet.compose() ) + + ## Scroll message + # database.exec('SELECT * FROM messages WHERE id = ? AND SUBSTRING(message FROM 0 FOR 2) ORDER BY time DESC LIMIT 1', [ @user.chat ]).then((data) -> + @send builder.create('m').append('t', "/s#{@chat.sc}").append('d', '123').compose() + + ## Done packet + @send '' + # ) ) + ) + +changePool = (poolId) -> + @broadcast builder.create('l').append('u', @user.id).compose() + @chat.onPool = poolId + joinRoom.call(@) + +module.exports = + getLink: (chat) -> global.Application.config.domain + '/room/' + chat + joinRoom: joinRoom + changePool: changePool + + banUser: (options) -> + new Promise (resolve, reject) -> + duration = parseInt options?.duration + userId = options?.userId + + return reject() if isNaN(duration) or duration < 0 + return reject() if @chat.rank.compareTo(Rank.MODERATOR) < 0 + return reject() if @chat.rank.compareTo(Rank.MODERATOR) == 0 and (duration > 6 * 3600 or duration == 0) + + database.exec 'SELECT f FROM `ranks` WHERE `userid` = ?', [userId], (err, data) -> + return reject(err) if err? + return reject() if data[0]?.f? and @chat.rank.compareTo(Rank.fromNumber(data[0].f & 7)) + + throw new Error('Not implemented') + + makeUser: (options) -> + new Promise (resolve, reject) => + userId = options?.userId + duration = options?.duration + newrank = Rank.fromString options?.rank + chatId = @chat.id + + return reject('User can\'t change ranks') if @chat.rank.compareTo(Rank.MODERATOR) < 0 + return reject('Moderator\'s rank is too low for target rank') if newrank.compareTo(@chat.rank) >= 0 + #return false if @chat.rank.compareTo(Rank.MODERATOR) == 0 and duration > 3600 * 6 + + destination = global.Server.rooms[chatId][userId] + return reject('Target user\'s rank is too high') if destination?.chat.rank? and @chat.rank.compareTo(destination.chat.rank) <= 0 + + database.exec('SELECT f FROM `ranks` WHERE userid = ? AND chatid = ? LIMIT 1', [userId, chatId]).then((data) => + return reject('Target user\'s rank is too high') if data[0]?.f? and @chat.rank.compareTo(Rank.fromNumber(data[0].f & 7)) <= 0 + + if data[0]? + database.exec('UPDATE `ranks` SET `f` = ? WHERE userid = ? AND chatid = ?', [newrank.toNumber(), userId, chatId]) + else + database.exec('INSERT INTO `ranks` (`userid`, `chatid`, `f`) VALUES(?, ?, ?)', [userId, chatId, newrank.toNumber()]) + ).then((data) => + packet = builder.create('m') + .append('u', @user.id) + .append('d', userId) + .append('t', '/m') + .append('p', newrank.toString()) + .compose() + @broadcast packet + @send packet + + if destination? + destination.chat.rank = newrank + destination.user.f = destination.user.f & ~7 | destination.chat.rank.toNumber() + + packet = builder.create('c') + .append('u', userId) + .append('t', "/m") + destination.send packet.compose() + + destination.broadcast userBuilder.buildU(destination).compose() + destination.send chatBuilder.buildMeta(destination).compose() + destination.send chatBuilder.buildPowers(destination).compose() + + resolve() + ).catch(reject) sendMessage: (user, message) -> - @broadcast builder.create('m').append('t', message).append('u', user).compose() + time = math.time() + + database.exec('INSERT INTO messages (id, uid, message, name, registered, avatar, time, pool) values (?, ?, ?, ?, ?, ?, ?, ?)', [ @user.chat, @user.id, message, @user.nickname, @user.username || 'unregistered', @user.avatar, time, @chat.onPool ]).then((data) => - database.exec('INSERT INTO messages (id, uid, message, name, registered, avatar, time, pool) values (?, ?, ?, ?, ?, ?, ?, ?)', [ @user.chat, @user.id, message, @user.nickname, @user.username || 'unregistered', @user.avatar, math.time(), @chat.onPool ]).then((data) -> + packet = messageBuilder.buildNewMain( + message: message + client: @ + time: time + messageId: data.insertId + ) + + @broadcast packet.compose() logger.log logger.level.DEBUG, 'New message sent' ).catch((err) -> logger.log logger.level.ERROR, 'Failed to send a message to the database', err) diff --git a/src/workers/profile.coffee b/src/workers/profile.coffee index 45a118c..984b61a 100644 --- a/src/workers/profile.coffee +++ b/src/workers/profile.coffee @@ -1,9 +1,14 @@ database = require '../services/database' module.exports = - getById: (userProfileId) -> new Promise((resolve, reject) -> - # TODO: Select only the needed data - database.exec('SELECT * FROM users WHERE id = ? LIMIT 1', [userProfileId]).then((data) -> + getById: (userProfileId, fields) -> new Promise((resolve, reject) -> + if fields instanceof Array + sql = 'SELECT ?? FROM users WHERE id = ? LIMIT 1' + values = [fields, userProfileId] + else + sql = 'SELECT * FROM users WHERE id = ? LIMIT 1' + values = [userProfileId] + database.exec(sql, values).then((data) -> resolve(data[0]) ).catch((err) -> reject(err)) ) diff --git a/src/workers/user.coffee b/src/workers/user.coffee index a54ffb1..c453db2 100644 --- a/src/workers/user.coffee +++ b/src/workers/user.coffee @@ -65,6 +65,7 @@ module.exports = @user.pStr = '' @user.k = packet['k'] @user.k3 = parseInt(packet['k3']) + @user.q = parseInt(packet['q']) i = 0 while i <= 20 diff --git a/test/lib/db-init.coffee b/test/lib/db-init.coffee index e72a99f..c2d40fd 100644 --- a/test/lib/db-init.coffee +++ b/test/lib/db-init.coffee @@ -9,56 +9,54 @@ Promise.all( for chatid in chatRange database.exec('DELETE FROM `messages` WHERE id = ?', chatid) ).then( -> - return Promise.all( + Promise.all( for chatid in chatRange - database.exec('DELETE FROM `chats` WHERE id = ?', chatid) - ) + database.exec('DELETE FROM `ranks` WHERE chatid = ?', chatid)) ).then( -> - for userid in idRange - database.exec('DELETE FROM `users` WHERE id = ?', userid) + Promise.all( + for chatid in chatRange + database.exec('DELETE FROM `chats` WHERE id = ?', chatid)) +).then( -> + Promise.all( + for userid in idRange + database.exec('DELETE FROM `users` WHERE id = ?', userid)) ).then( -> - return Promise.all( - ( - for chatid in chatRange - database.exec('INSERT INTO `chats` SET ?', - id: chatid - name: 'test ' + chatid - bg: 'http://xat.com/web_gear/background/xat_splash.jpg' - language: 'en' - desc: 'Tupucal description ' + chatid - sc: 'Welcome to chat ' + chatid - email: 'admin' + chatid + '@example.com' - radio: '127.0.' + chatid / 256 + '.' + chatid % 256 - pass: chatid + 20000 - button: '#FF0000' - attached: '' - ) - ) - ) + Promise.all( + for chatid in chatRange + database.exec('INSERT INTO `chats` SET ?', + id: chatid + name: 'test ' + chatid + bg: 'http://xat.com/web_gear/background/xat_splash.jpg' + language: 'en' + desc: 'Tupucal description ' + chatid + sc: 'Welcome to chat ' + chatid + email: 'admin' + chatid + '@example.com' + radio: '127.0.' + chatid / 256 + '.' + chatid % 256 + pass: chatid + 20000 + button: '#FF0000' + attached: '' + )) ).then( -> - return Promise.all( - ( - for userid in idRange - database.exec('INSERT INTO `users` SET ?', - id: userid - username: 'unregistered'#'username' + userid - nickname: 'nickname' + userid - password: '123' + userid - avatar: userid - url: 'http://example.com/id?' + userid - email: 'mail' + userid + '@mail.example.com' - k: 'k_' + userid - k2: 'k2_' + userid - k3: 'k3_' + userid - bride: '' - xats: 0 - days: 0 - enabled: 'enabled' - dO: '' - loginKey: '' - ) - ) - ) + Promise.all( + for userid in idRange + database.exec('INSERT INTO `users` SET ?', + id: userid + username: 'unregistered'#'username' + userid + nickname: 'nickname' + userid + password: '123' + userid + avatar: userid + url: 'http://example.com/id?' + userid + email: 'mail' + userid + '@mail.example.com' + k: 'k_' + userid + k2: 'k2_' + userid + k3: 'k3_' + userid + bride: '' + xats: 0 + days: 0 + enabled: 'enabled' + dO: '' + loginKey: '' + )) ).catch((err) -> console.error '[ERROR] Error while initializing database ' + JSON.stringify(err) process.exit(1) diff --git a/test/lib/test-kit.coffee b/test/lib/test-kit.coffee index 6a77c5e..6bca08f 100644 --- a/test/lib/test-kit.coffee +++ b/test/lib/test-kit.coffee @@ -54,7 +54,7 @@ exports.IXatUser = exports.deployServer = (options) -> new Promise (resolve, reject) -> options = options || {} - server = fork(path.join(__dirname, '../../bin/xat'), [], silent: true) + server = fork(path.join(__dirname, '../../bin/xat-test'), [], silent: true) server.on 'error', (err) -> reject err diff --git a/test/locate.coffee b/test/locate.coffee index 7998889..9c1d94d 100644 --- a/test/locate.coffee +++ b/test/locate.coffee @@ -110,7 +110,7 @@ describe 'locating interactions', -> it 'should receive response to locate', -> checkResponse(at, locator, responder) z = at.xml.z - z.attributes.t.should.be.equal '/axat.dev/chat/room/' + responder.todo.w_useroom + '/' + t = z.attributes.t.split('/').should.contain.an.item responder.todo.w_useroom describe 'receiver responds as nofollow', -> at = null diff --git a/test/message.coffee b/test/message.coffee index de472e1..67d8ec1 100644 --- a/test/message.coffee +++ b/test/message.coffee @@ -5,17 +5,25 @@ deploy = test.deployServer should = require('chai').should() describe 'message', -> - u1 = null - u2 = null server = null - messages = - u1: [] - u2: [] - all: [] before (beforeDone) -> deploy().then (_server) -> server = _server + beforeDone() + return + + after -> + server.kill() + + describe 'basic', -> + u1 = null + u2 = null + messages = + u1: [] + u2: [] + all: [] + before (beforeDone) -> u1 = new XatUser( todo: w_userno: 51 @@ -41,25 +49,56 @@ describe 'message', -> messages.u2.push data if data.done? beforeDone() - return - after -> - server.kill() + describe 'user 1', -> + it 'should receive message sent by user 2', (done) -> + u2.sendTextMessage('test!') + test.delay 100, -> + gotdone = false + messageReceived = false + for message in messages.u1 + gotdone = true if message.done? + if gotdone and message.m? + messageReceived = true + m = message.m + m.attributes.t.should.be.equal('test!') + m.attributes.u.should.be.oneOf([u2.todo.w_userno + '_' + u2.todo.w_userrev, String(u2.todo.w_userno)]) - describe 'user 1', -> - it 'should receive message sent by user 2', (done) -> - u2.sendTextMessage('test!') - test.delay 10, -> - gotdone = false - messageReceived = false - for message in messages.u1 - gotdone = true if message.done? - if gotdone and message.m? - messageReceived = true - m = message.m - m.attributes.t.should.be.equal('test!') - m.attributes.u.should.be.oneOf([u2.todo.w_userno + '_' + u2.todo.w_userrev, String(u2.todo.w_userno)]) + messageReceived.should.be.true + done() - messageReceived.should.be.true - done() + describe.skip 'evil', -> + u1 = null + u2 = null + message = 'evil uid case ' + new Date().getTime() + received = null + + before (done) -> + u1 = new XatUser( + todo: + w_userno: '50' + w_useroom: 100 + w_userrev: 0 + w_k1: 'k_50' + ).addExtension('extended-events') + u2 = new XatUser( + todo: + w_userno: '51' + w_useroom: 100 + w_userrev: 0 + w_k1: 'k_51' + ).addExtension('extended-events') + u1.connect() + u1.once 'ee-done', -> + u2.connect() + u2.once 'ee-done', -> + u1.send "" + u2.once 'ee-text-message', (data) -> + received = data.xml + test.delay 100, -> done() + after -> + u1.end() + u2.end() + it 'shouldn\'t receive with illegal "u"', -> + should.not.exist received diff --git a/test/on-super.coffee b/test/on-super.coffee index 726f8b3..b1cad51 100644 --- a/test/on-super.coffee +++ b/test/on-super.coffee @@ -86,7 +86,7 @@ describe 'on-super', -> describe 'receiver', -> it "should receive 'k'-packet in first chat", -> messages.receiver2.should.contains.an.item.with.property('k') - it "should receive 'k'-packet with appropriate content", -> + it.skip "should receive 'k'-packet with appropriate content", -> [y] = (_.y for _ in messages.receiver2 when _.y?) assert.isDefined y diff --git a/test/pools.coffee b/test/pools.coffee index a765b34..184abde 100644 --- a/test/pools.coffee +++ b/test/pools.coffee @@ -59,22 +59,25 @@ describe 'pools', -> w_k1: 'k_50' w_useroom: 100 w_userrev: 0 + w_pool: 0 ).addExtension('user-actions').addExtension('extended-events') checker0 = new XatUser( todo: - w_userno: 51 + w_userno: '51' w_useroom: 100 w_k1: 'k_51' w_userrev: 0 + w_pool: 0 ).addExtension('user-actions').addExtension('extended-events') checker1 = new XatUser( todo: - w_userno: 52 + w_userno: '52' w_useroom: 100 w_k1: 'k_52' w_userrev: 0 + w_pool: 1 ).addExtension('user-actions').addExtension('extended-events') checker0.connect() checker0.once 'ee-done', (data) -> @@ -92,6 +95,29 @@ describe 'pools', -> checker0.end() checker1.end() + checkSigninReceived = (signin, user) -> + should.exist signin + signin.should.have.property 'u' + u = signin.u + u.attributes.should.have.property 'u' + u.attributes.u.should.be.equal user.todo.w_userno + + checkSignoutReceived = (signout, user) -> + should.exist signout + signout.should.have.property 'l' + l = signout.l + l.attributes.should.have.property 'u' + l.attributes.u.should.be.equal user.todo.w_userno + + checkMessageReceived = (received, message, sender) -> + should.exist received + received.should.have.property 'm' + m = received.m + m.attributes.should.contain.keys ['u', 't'] + m.attributes.u.split('_')[0].should.be.equal sender.todo.w_userno + m.attributes.t.should.be.equal message + + describe 'tweaker goes to pool 1', -> logout = null @@ -102,23 +128,15 @@ describe 'pools', -> tweaker.once 'ee-done', -> test.delay 100, -> beforeDone() - checker0.once 'ee-user-logout', (data) -> + checker0.once 'ee-user-signout', (data) -> logout = data.xml - checker1.once 'ee-user-signin', (data) -> + checker1.once 'ee-user', (data) -> signin = data.xml it 'checker from pool 0 should receive ', -> - should.exist logout - logout.should.have.property 'l' - l = logout.l - l.attributes.should.have.property 'u' - l.attributes.u.should.be.equal tweaker.todo.w_userno + checkSignoutReceived logout, tweaker it 'checker from pool 1 should receive ', -> - should.exist signin - signin.should.have.property 'u' - u = signin.u - u.attributes.should.have.property 'u' - u.attributes.u.should.be.equal tweaker.todo.w_userno + checkSigninReceived signin, tweaker describe 'tweaker sends message to pool 1', -> @@ -141,9 +159,59 @@ describe 'pools', -> it 'checker from pool 0 shouldn\'t receive ', -> should.not.exist receivedBy0 it 'checker from pool 1 should receive ', -> - should.exist receivedBy1 - receivedBy1.should.have.property 'm' - m = receivedBy1.m - m.attributes.should.contain.keys ['u', 't'] - m.attributes.u.split('_')[0].should.be.equal tweaker.todo.w_userno - m.attributes.t.should.be.equal message + checkMessageReceived receivedBy1, message, tweaker + describe 'tweaker backs to pool 0', -> + message = 'message to pool 0' + new Date().getTime() + checkerMessage = 'message to tweaker from pool 0' + new Date().getTime() + message0 = null + message1 = null + messaget = null + logout = null + signin = null + + before (done) -> + tweaker.setPool 0 + checker0.once 'ee-user', (data) -> + signin = data.xml + checker1.once 'ee-user-signout', (data) -> + logout = data.xml + + test.delay 100, -> + tweaker.sendTextMessage message + checker0.sendTextMessage checkerMessage + checker0.once 'ee-text-message', (data) -> + message0 = data.xml + checker1.once 'ee-text-message', (data) -> + message1 = data.xml + tweaker.once 'ee-text-message', (data) -> + messaget = data.xml + + test.delay 100, -> done() + it 'checker from pool 0 should receive ', -> + checkSigninReceived signin, tweaker + it 'checker from pool 0 should receive ', -> + checkMessageReceived message0, message, tweaker + it 'checker from pool 1 should receive ', -> + checkSignoutReceived logout, tweaker + it 'checker from pool 1 shouldn\'t receive ', -> + should.not.exist message1 + it 'tweaker should be able to receive messages from pool 0 checker', -> + checkMessageReceived messaget, checkerMessage, checker0 + + describe 'checker 0 signout - signin.', -> + signin = null + logout = null + before (done) -> + checker0.end() + tweaker.once 'ee-user-signout', (data) -> + logout = data.xml + + test.delay 100, -> + checker0.connect() + tweaker.once 'ee-user', (data) -> + signin = data.xml + checker0.once 'ee-done', -> done() + it 'tweaker should receive ', -> + checkSignoutReceived logout, checker0 + it 'tweaker should receive ', -> + checkSigninReceived signin, checker0 diff --git a/test/private-message.coffee b/test/private-message.coffee index 577f7f5..b34183f 100644 --- a/test/private-message.coffee +++ b/test/private-message.coffee @@ -42,10 +42,10 @@ describe 'private messaging', -> sender.connect() - sender.on 'ee-done', () -> + sender.once 'ee-done', -> receiver.connect() - receiver.on 'ee-done', () -> + receiver.once 'ee-done', -> beforeDone() receiver.on 'data', (data) -> diff --git a/test/rank.coffee b/test/rank.coffee new file mode 100644 index 0000000..19de2c5 --- /dev/null +++ b/test/rank.coffee @@ -0,0 +1,296 @@ +should = require('chai').should() + +Rank = require '../src/structures/rank' + +test = require './lib/test-kit' +XatUser = test.IXatUser +deploy = test.deployServer + +describe 'ranks', -> + server = null + + before (done) -> + deploy().then (_server) -> + server = _server + done() + + after -> server.kill() + + describe 'main owner', -> + owner = null + + chatMeta = null + + before (done) -> + owner = new XatUser( + todo: + w_userno: '50' + w_useroom: 100 + w_k1: 'k_50' + pass: 100 + 20000 + ).addExtension('extended-events') + owner.connect() + #owner.on 'data', (data) -> console.log(data) + owner.once 'ee-chat-meta', (data) -> + chatMeta = data.xml + owner.once 'ee-done', -> done() + + it 'should auth successfully', -> + should.exist chatMeta + chatMeta.should.have.property 'i' + i = chatMeta.i + i.attributes.should.have.property 'r' + i.attributes.r.should.be.equal '1' + describe 'illegal main owner', -> + owner = null + + chatMeta = null + + before (done) -> + owner = new XatUser( + todo: + w_userno: '50' + w_useroom: 100 + w_k1: 'k_50' + pass: 101 + 20000 + ).addExtension('extended-events') + owner.connect() + #owner.on 'data', (data) -> console.log(data) + owner.once 'ee-chat-meta', (data) -> + chatMeta = data.xml + owner.once 'ee-done', -> done() + it 'shouldn\'t auth successfully', -> + should.exist chatMeta + chatMeta.should.have.property 'i' + i = chatMeta.i + if i.attributes.r? + i.attributes.r.should.not.be.equal '1' + + describe 'make', -> + owner = null + user = null + + userMeta = null + + checkMake = (packet, object, subject, rank) -> + should.exist packet + packet.should.have.property 'm' + m = packet.m + + m.attributes.should.contain.keys [ 'u', 'd', 't', 'p' ] + m.attributes.u.should.be.equal subject.todo.w_userno + m.attributes.d.should.be.equal object.todo.w_userno + m.attributes.t.should.be.equal '/m' + m.attributes.p.should.be.equal rank.toString() + + checkMakeMember = (packet, object, subject) -> + checkMake packet, object, subject + packet.m.attributes.p.should.be.equal 'e' + + checkSignin = (packet, user, rank) -> + should.exist packet + packet.should.have.property 'u' + u = packet.u + u.attributes.should.contain.keys [ 'u' ] + u.attributes.u.should.be.equal user.todo.w_userno + if rank != Rank.GUEST or u.attributes.f? + Rank.fromNumber(u.attributes.f & 7).should.be.equal(rank) + + checkMeta = (packet, user, rank) -> + should.exist packet + packet.should.have.property 'i' + i = packet.i + if rank != Rank.GUEST or i.attributes.r? + Rank.fromNumber(i.attributes.r).should.be.equal(rank) + + checkControlMake = (packet, user) -> + should.exist packet + packet.should.have.property 'c' + c = packet.c + c.attributes.should.contain.keys [ 'u', 't' ] + c.attributes.u.should.be.equal user.todo.w_userno + c.attributes.t.substr(0, 2).should.be.equal '/m' + + before (done) -> + owner = new XatUser( + todo: + w_userno: '50' + w_useroom: 110 + w_k1: 'k_50' + pass: 110 + 20000 + ).addExtension('extended-events').addExtension('user-actions') + user = new XatUser( + todo: + w_userno: '51' + w_useroom: 110 + w_k1: 'k_51' + ).addExtension('extended-events').addExtension('user-actions') + owner.connect() + owner.once 'ee-done', -> + user.connect() + user.once 'ee-chat-meta', (data) -> + userMeta = data.xml + user.once 'ee-done', -> + done() + + it 'user should be a guest', -> + checkMeta userMeta, user, Rank.GUEST + + describe 'owner makes user a member', -> + + ownerMake = null + ownerSignin = null + ownerSignout = null + userMake = null + userMeta = null + controlMake = null + + before (done) -> + owner.makeMember user.todo.w_userno + owner.once 'ee-make-user', (data) -> + ownerMake = data.xml + owner.once 'ee-user-signin', (data) -> + ownerSignin = data.xml + owner.once 'ee-user-signout', (data) -> + ownerSignout = data.xml + user.once 'ee-chat-meta', (data) -> + userMeta = data.xml + user.once 'ee-control-make-user', (data) -> + controlMake = data.xml + user.once 'ee-make-user', (data) -> + userMake = data.xml + test.delay 100, -> done() + + it 'user should receive control', -> + checkControlMake controlMake, user + + it 'user should receive notify', -> + checkMake userMake, user, owner, Rank.MEMBER + + it 'owner should receive notify', -> + checkMake ownerMake, user, owner, Rank.MEMBER + + it 'user should receive again', -> + checkMeta userMeta, user, Rank.MEMBER + + it 'owner should receive ', -> + checkSignin ownerSignin, user, Rank.MEMBER + + it 'owner shouldn\'t receive ', -> + should.not.exist ownerSignout + + describe 'owner makes himself a moderator', -> + control = make = signin = meta = null + + before (done) -> + owner.makeModerator owner.todo.w_userno + owner.once 'ee-control-make-user', (data) -> + control = data.xml + owner.once 'ee-make-user', (data) -> + make = data.xml + owner.once 'ee-user-signin', (data) -> + signin = data.xml + owner.once 'ee-chat-meta', (data) -> + meta = data.xml + + test.delay 100, -> done() + it 'owner shouldn\'t be able to do so', -> + should.not.exist control + should.not.exist make + should.not.exist signin + should.not.exist meta + + describe 'owner makes user a moderator', -> + userMeta = null + ownerSignin = null + + before (done) -> + owner.makeModerator user.todo.w_userno + owner.once 'ee-user-signin', (data) -> + ownerSignin = data.xml + user.once 'ee-chat-meta', (data) -> + userMeta = data.xml + test.delay 100, done + + it 'user should receive + checkMeta userMeta, user, Rank.MODERATOR + + it 'owner should receive + checkSignin ownerSignin, user, Rank.MODERATOR + + describe 'moderator makes user a member', -> + victim = null + + victimUser = null + victimOwner = null + + victimMeta = null + victimMake = null + victimControl = null + victimSignin = null + + ownerSignin = null + ownerMake = null + ownerControl = null + ownerMeta = null + + userSignin = null + userMake = null + userControl = null + userMeta = null + + before (done) -> + victim = new XatUser( + todo: + w_userno: '52' + w_k1: 'k_52' + w_useroom: owner.todo.w_useroom + ).addExtension('user-actions').addExtension('extended-events') + + victim.connect() + victim.on 'ee-user', (data) -> + victimUser = data.xml if not victimUser and data.xml.u?.attributes.u == user.todo.w_userno + victimOwner = data.xml if not victimOwner and data.xml.u?.attributes.u == owner.todo.w_userno + + victim.once 'ee-done', -> + user.makeMember victim.todo.w_userno + userMeta = null + + owner.once 'ee-user-signin', (data) -> ownerSignin = data.xml + user.once 'ee-user-signin', (data) -> userSignin = data.xml + victim.once 'ee-user-signin', (data) -> victimSignin = data.xml + + victim.once 'ee-chat-meta', (data) -> victimMeta = data.xml + owner.once 'ee-chat-meta', (data) -> ownerMeta = data.xml + user.once 'ee-chat-meta', (data) -> userMeta = data.xml + + victim.once 'ee-make-user', (data) -> victimMake = data.xml + owner.once 'ee-make-user', (data) -> ownerMake = data.xml + user.once 'ee-make-user', (data) -> userMake = data.xml + + victim.once 'ee-control-make-user', (data) -> victimControl = data.xml + owner.once 'ee-control-make-user', (data) -> ownerControl = data.xml + user.once 'ee-control-make-user', (data) -> userControl = data.xml + + test.delay 200, done + + it 'owner should receive and not ', -> + checkMake ownerMake, victim, user, Rank.MEMBER + checkSignin ownerSignin, victim, Rank.MEMBER + + should.not.exist ownerControl + should.not.exist ownerMeta + + it 'user should receive and not ', -> + checkMake userMake, victim, user, Rank.MEMBER + checkSignin userSignin, victim, Rank.MEMBER + + should.not.exist userControl + should.not.exist userMeta + + it 'victim should receive and not ', -> + checkMake victimMake, victim, user, Rank.MEMBER + checkMeta victimMeta, victim, Rank.MEMBER + checkControlMake victimControl, victim + + should.not.exist victimSignin