From d28ba96125b2d77d9fe8d2fd76b55ff00fca99a3 Mon Sep 17 00:00:00 2001 From: haosdent Date: Mon, 22 Jun 2015 17:45:36 +0800 Subject: [PATCH] IRC prototype. --- .meteor/packages | 1 + .meteor/versions | 1 + packages/rocketchat-irc/Readme.md | 1 + packages/rocketchat-irc/irc.server.coffee | 165 ++++++++++++++++++++++ packages/rocketchat-irc/package.js | 21 +++ server/lib/accounts.coffee | 3 + server/methods/joinRoom.coffee | 5 + server/methods/leaveRoom.coffee | 4 + server/methods/receiveMessage.coffee | 126 +++++++++++++++++ server/methods/registerUser.coffee | 3 +- 10 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 packages/rocketchat-irc/Readme.md create mode 100644 packages/rocketchat-irc/irc.server.coffee create mode 100644 packages/rocketchat-irc/package.js create mode 100644 server/methods/receiveMessage.coffee diff --git a/.meteor/packages b/.meteor/packages index 4408a9331845..a92d512458c2 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -49,6 +49,7 @@ rocketchat:markdown rocketchat:me rocketchat:mentions rocketchat:oembed +rocketchat:irc tap:i18n tmeasday:crypto-md5 tmeasday:errors diff --git a/.meteor/versions b/.meteor/versions index 8b609d629e1d..bbc3fcff7b1d 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -103,6 +103,7 @@ rocketchat:markdown@0.0.1 rocketchat:me@0.0.1 rocketchat:mentions@0.0.1 rocketchat:oembed@0.0.1 +rocketchat:irc@0.0.1 ronhoffmann:webcomponentjs@1.0.3 routepolicy@1.0.5 service-configuration@1.0.4 diff --git a/packages/rocketchat-irc/Readme.md b/packages/rocketchat-irc/Readme.md new file mode 100644 index 000000000000..5073cb244414 --- /dev/null +++ b/packages/rocketchat-irc/Readme.md @@ -0,0 +1 @@ +Move to [rocketchat-irc](https://github.com/haosdent/Rocket.Chat/tree/irc/packages/rocketchat-irc). \ No newline at end of file diff --git a/packages/rocketchat-irc/irc.server.coffee b/packages/rocketchat-irc/irc.server.coffee new file mode 100644 index 000000000000..fecb9e7ed568 --- /dev/null +++ b/packages/rocketchat-irc/irc.server.coffee @@ -0,0 +1,165 @@ +net = Npm.require('net') + +ircClientMap = {} + +bind = (f) -> + g = Meteor.bindEnvironment (self, args...) -> f.apply(self, args) + (args...) -> g @, args... + +async = (f, args...) -> + Meteor.wrapAsync(f)(args...) + +class IrcClient + constructor: (@loginReq) -> + @user = @loginReq.user + ircClientMap[@user._id] = this + @ircPort = 6667 + @ircHost = 'irc.freenode.net' + @msgBuf = [] + @isConnected = false + @socket = new net.Socket + @socket.setNoDelay + @socket.setEncoding 'utf-8' + @onConnect = bind @onConnect + @onReceiveRawMessage = bind @onReceiveRawMessage + @socket.on 'data', @onReceiveRawMessage + @socket.on 'close', @onClose + @receiveMessageRegex = /^:(\S+)!~\S+ PRIVMSG (\S+) :(.+)$/ + @memberListRegex = /^:\S+ \d+ \S+ = #(\S+) :(.*)$/ + @initRoomList() + + connect: (@loginCb) => + @socket.connect @ircPort, @ircHost, @onConnect + + onConnect: () => + console.log @user.username, 'connect success.' + @socket.write "NICK #{@user.username}\r\n" + @socket.write "USER #{@user.username} 0 * :Real Name\r\n" + # message order could not make sure here + @isConnected = true + @socket.write msg for msg in @msgBuf + @loginCb null, @loginReq + + onClose: (data) => + console.log @user.username, 'connection close.' + + onReceiveRawMessage: (data) => + data = data.toString().split('\n') + for line in data + line = line.trim() + console.log "[#{@ircHost}:#{@ircPort}]:", line + if line.indexOf('PING') == 0 + @socket.write line.replace('PING :', 'PONG ') + continue + + matchResult = @receiveMessageRegex.exec line + if matchResult + @onReceiveMessage matchResult[1], matchResult[2], matchResult[3] + continue + + matchResult = @memberListRegex.exec line + if matchResult + @onReceiveMemberList matchResult[1], matchResult[2].split ' ' + continue + + onReceiveMessage: (name, target, content) -> + console.log '[irc] onReceiveMessage -> '.yellow, 'sourceUserName:', name, 'target:', target, 'content:', content + if target[0] == '#' + room = ChatRoom.findOne {name: target.substring 1} + else + room = ChatRoom.findOne {usernames: { $all: [target, name]}, t: 'd'}, { fields: { usernames: 1, t: 1 } } + + sourceUser = Meteor.users.findOne {username: message.u.username}, fields: username: 1 + unless sourceUser + Meteor.call 'registerUser', + email: '', + password: name, + name: name + Meteor.call 'receiveMessage', + u: + username: name + rid: room._id + msg: content + + onReceiveMemberList: (room, members) -> + console.log '[irc] onReceiveMemberList -> '.yellow, 'room:', room, 'members:', members + + sendRawMessage: (msg) -> + console.log '[irc] sendRawMessage -> '.yellow, msg + if @isConnected + @socket.write msg + else + @msgBuf.push msg + + sendMessage: (room, message) -> + console.log '[irc] sendMessage -> '.yellow, 'userName:', message.u.username, 'arguments:', arguments + target = '' + if room.t == 'c' + target = "##{room.name}" + else if room.t == 'd' + for name in room.usernames + if message.u.username != name + target = name + break + msg = "PRIVMSG #{target} :#{message.msg}\r\n" + @sendRawMessage msg + + initRoomList: () -> + roomsCursor = ChatRoom.find {usernames: { $in: [@user.username]}, t: 'c'}, { fields: { name: 1 }} + rooms = roomsCursor.fetch() + for room in rooms + @joinRoom(room) + + joinRoom: (room) -> + msg = "JOIN ##{room.name}\r\n" + @sendRawMessage msg + msg = "NAMES ##{room.name}\r\n" + @sendRawMessage msg + + leaveRoom: (room) -> + msg = "PART ##{room.name}\r\n" + @sendRawMessage msg + + +IrcClient.getByUid = (uid) -> + return ircClientMap[uid] + +IrcClient.create = (login) -> + unless login.user._id of ircClientMap + ircClient = new IrcClient login + return async ircClient.connect + return login + + +class IrcLoginer + constructor: (login) -> + console.log '[irc] validateLogin -> '.yellow, login + return IrcClient.create login + + +class IrcSender + constructor: (message) -> + room = ChatRoom.findOne message.rid, { fields: { name: 1, usernames: 1, t: 1 } } + ircClient = IrcClient.getByUid message.u._id + ircClient.sendMessage room, message + return message + + +class IrcRoomJoiner + constructor: (user, room) -> + ircClient = IrcClient.getByUid user._id + ircClient.joinRoom room + return room + + +class IrcRoomLeaver + constructor: (user, room) -> + ircClient = IrcClient.getByUid user._id + ircClient.leaveRoom room + return room + + +RocketChat.callbacks.add 'beforeValidateLogin', IrcLoginer, RocketChat.callbacks.priority.LOW +RocketChat.callbacks.add 'beforeSaveMessage', IrcSender, RocketChat.callbacks.priority.LOW +RocketChat.callbacks.add 'beforeJoinRoom', IrcRoomJoiner, RocketChat.callbacks.priority.LOW +RocketChat.callbacks.add 'beforeLeaveRoom', IrcRoomLeaver, RocketChat.callbacks.priority.LOW \ No newline at end of file diff --git a/packages/rocketchat-irc/package.js b/packages/rocketchat-irc/package.js new file mode 100644 index 000000000000..624c3682b634 --- /dev/null +++ b/packages/rocketchat-irc/package.js @@ -0,0 +1,21 @@ +Package.describe({ + name: 'rocketchat:irc', + version: '0.0.1', + summary: 'RocketChat libraries', + git: '' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.0'); + + api.use([ + 'coffeescript', + 'rocketchat:lib@0.0.1' + ]); + + api.addFiles('irc.server.coffee', 'server'); + + api.export(['Irc'], ['server']); +}); + +Package.onTest(function(api) {}); diff --git a/server/lib/accounts.coffee b/server/lib/accounts.coffee index 59f2643c671e..f52fefb6077b 100644 --- a/server/lib/accounts.coffee +++ b/server/lib/accounts.coffee @@ -46,6 +46,7 @@ Accounts.onCreateUser (options, user) -> Accounts.validateLoginAttempt (login) -> + login = RocketChat.callbacks.run 'beforeValidateLogin', login if login.allowed isnt true return login.allowed @@ -58,5 +59,7 @@ Accounts.validateLoginAttempt (login) -> return false Meteor.users.update {_id: login.user._id}, {$set: {lastLogin: new Date}} + Meteor.defer -> + RocketChat.callbacks.run 'afterValidateLogin', login return true diff --git a/server/methods/joinRoom.coffee b/server/methods/joinRoom.coffee index 6e1ad51320e9..b511fe9332b5 100644 --- a/server/methods/joinRoom.coffee +++ b/server/methods/joinRoom.coffee @@ -8,11 +8,14 @@ Meteor.methods # verify if user is already in room # if room.usernames.indexOf(user.username) is -1 + console.log '[methods] joinRoom -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments now = new Date() user = Meteor.users.findOne Meteor.userId() + RocketChat.callbacks.run 'beforeJoinRoom', user, room + update = $push: usernames: @@ -42,4 +45,6 @@ Meteor.methods _id: user._id username: user.username + RocketChat.callbacks.run 'afterJoinRoom', user, room + return true diff --git a/server/methods/leaveRoom.coffee b/server/methods/leaveRoom.coffee index aa8eb29026f8..b15e7cc242c3 100644 --- a/server/methods/leaveRoom.coffee +++ b/server/methods/leaveRoom.coffee @@ -8,6 +8,8 @@ Meteor.methods room = ChatRoom.findOne rid + RocketChat.callbacks.run 'beforeLeaveRoom', Meteor.user(), room + update = $pull: usernames: Meteor.user().username @@ -45,3 +47,5 @@ Meteor.methods ChatSubscription.remove { rid: rid, 'u._id': Meteor.userId() } ChatRoom.update rid, update + + RocketChat.callbacks.run 'afterLeaveRoom', Meteor.user(), room diff --git a/server/methods/receiveMessage.coffee b/server/methods/receiveMessage.coffee new file mode 100644 index 000000000000..3be6b1d67454 --- /dev/null +++ b/server/methods/receiveMessage.coffee @@ -0,0 +1,126 @@ +Meteor.methods + receiveMessage: (message) -> + if message.u?._id? + else if message.u?.username? + message.u = Meteor.users.findOne {username: message.u.username}, fields: username: 1 + else + return false + + room = ChatRoom.findOne message.rid, { fields: { usernames: 1, t: 1 } } + + if not room + return false + + console.log '[methods] receiveMessage -> '.green, 'userId:', message.u._id, 'arguments:', arguments + + message.ts = new Date() + + if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]+)?\??([-\+=&!:;%@\/\.\,\w]+)?#?([\w]+)?)?/g + message.urls = urls + + message = RocketChat.callbacks.run 'beforeReceiveMessage', message + + ### + Defer other updated as their return is not interesting to the user + ### + Meteor.defer -> + + ### + Update all the room activity tracker fields + ### + ChatRoom.update + # only subscriptions to the same room + rid: message.rid + , + # update the last message timestamp + $set: + lm: message.ts + # increate the messages counter + $inc: + msgs: 1 + + + # increment unread couter if direct messages + if room.t is 'd' + ### + Update the other subscriptions + ### + ChatSubscription.update + # only subscriptions to the same room + rid: message.rid + # not the msg owner + 'u._id': + $ne: message.u._id + , + $set: + # alert de user + alert: true + # open the room for the user + open: true + # increment unread couter + $inc: + unread: 1 + + else + message.mentions?.forEach (mention) -> + console.log mention + ### + Update all other subscriptions of mentioned users to alert their owners and incrementing + the unread counter for mentions and direct messages + ### + ChatSubscription.update + # only subscriptions to the same room + rid: message.rid + # the mentioned user + 'u._id': mention._id + , + $set: + # alert de user + alert: true + # open the room for the user + open: true + # increment unread couter + $inc: + unread: 1 + + ### + Update all other subscriptions to alert their owners but witout incrementing + the unread counter, as it is only for mentions and direct messages + ### + ChatSubscription.update + # only subscriptions to the same room + rid: message.rid + # only the ones that have not been alerted yet + alert: false + # not the msg owner + 'u._id': + $ne: message.u._id + , + $set: + # alert de user + alert: true + # open the room for the user + open: true + , + # make sure we alert all matching subscription + multi: true + + ### + Save the message. If there was already a typing record, update it. + ### + ChatMessage.upsert + rid: message.rid + t: 't' + $and: [{ 'u._id': message.u._id }] + , + $set: message + $unset: + t: 1 + expireAt: 1 + + Meteor.defer -> + + message._id = Random.id() + RocketChat.callbacks.run 'afterReceiveMessage', message + + diff --git a/server/methods/registerUser.coffee b/server/methods/registerUser.coffee index 18722744719d..e7596a3a04be 100644 --- a/server/methods/registerUser.coffee +++ b/server/methods/registerUser.coffee @@ -10,4 +10,5 @@ Meteor.methods $set: name: formData.name - Accounts.sendVerificationEmail(userId, userData.email); + if userData.email + Accounts.sendVerificationEmail(userId, userData.email);