Skip to content

Commit

Permalink
IRC Federation: RFC2813 implementation (ngIRCd) (#10113)
Browse files Browse the repository at this point in the history
* Initial progress

* Direct messaging complete

* Handle net-splits

* Cleaned up logging

* more cleanup, better log messages

* Added support for IRC users to create rooms and invite RC users

* Keep rooms in sync

* IRC user can kick RC user

* Working on transcription of coffescript to ecmascript code and fitting on the codebase.

* Adds settings section for config the IRC Server bridge.

* Working handles for direct messages

* Working handles for direct messages

* Working handles for direct messages

* Working handles for direct messages

* Working handles for direct messages

* Working on RC server connection to a local IRC Network

* first version, using a RFC2813 implementation

* Fixing lint errors

* Fixed partial username

* Fixed problems with scope

* removed parser name

* Added a button to reset the IRC connection

* Adjusted messages

* Fixed IRC federation for new users

* Ignore eslint about control character on regex

* Adjusted settings strings
  • Loading branch information
alansikora authored and rodrigok committed Jun 20, 2018
1 parent 3ebb78d commit 17a63ec
Show file tree
Hide file tree
Showing 37 changed files with 2,500 additions and 525 deletions.
2 changes: 1 addition & 1 deletion .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ rocketchat:importer-slack@0.0.1
rocketchat:importer-slack-users@1.0.0
rocketchat:integrations@0.0.1
rocketchat:internal-hubot@0.0.1
rocketchat:irc@0.0.2
rocketchat:irc@0.0.1
rocketchat:issuelinks@0.0.1
rocketchat:katex@0.0.1
rocketchat:lazy-load@0.0.1
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
"poplib": "^0.1.7",
"prom-client": "^11.0.0",
"querystring": "^0.2.0",
"queue-fifo": "^0.2.4",
"redis": "^2.8.0",
"semver": "^5.5.0",
"sharp": "^0.20.3",
Expand Down
6 changes: 6 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,8 @@
"Condensed": "Condensed",
"Computer": "Computer",
"Confirm_password": "Confirm your password",
"Connection_Closed" : "Connection closed",
"Connection_Reset" : "Connection reset",
"Consulting": "Consulting",
"Consumer_Goods": "Consumer Goods",
"Contains_Security_Fixes": "Contains Security Fixes",
Expand Down Expand Up @@ -1392,6 +1394,9 @@
"IRC_Channel_Users_End": "End of output of the NAMES command.",
"IRC_Description": "Internet Relay Chat (IRC) is a text-based group communication tool. Users join uniquely named channels, or rooms, for open discussion. IRC also supports private messages between individual users and file sharing capabilities. This package integrates these layers of functionality with Rocket.Chat.",
"IRC_Enabled": "Attempt to integrate IRC support. Changing this value requires restarting Rocket.Chat.",
"IRC_Enabled_Alert": "IRC Support is a work in progress. Use on a production system is not recommended at this time.",
"IRC_Federation": "IRC Federation",
"IRC_Federation_Disabled": "IRC Federation is disabled.",
"IRC_Hostname": "The IRC host server to connect to.",
"IRC_Login_Fail": "Output upon a failed connection to the IRC server.",
"IRC_Login_Success": "Output upon a successful connection to the IRC server.",
Expand Down Expand Up @@ -2058,6 +2063,7 @@
"Reset": "Reset",
"Reset_password": "Reset password",
"Reset_section_settings": "Reset Section Settings",
"Reset_Connection" : "Reset Connection",
"Restart": "Restart",
"Restart_the_server": "Restart the server",
"Retail": "Retail",
Expand Down
1 change: 1 addition & 0 deletions packages/rocketchat-irc/.npm/package/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
7 changes: 7 additions & 0 deletions packages/rocketchat-irc/.npm/package/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.

You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.
20 changes: 20 additions & 0 deletions packages/rocketchat-irc/.npm/package/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions packages/rocketchat-irc/package.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
Package.describe({
name: 'rocketchat:irc',
version: '0.0.2',
summary: 'RocketChat libraries',
version: '0.0.1',
summary: 'RocketChat support for federating with IRC servers as a leaf node',
git: ''
});

Package.onUse(function(api) {
api.use([
'ecmascript',
'underscore',
'rocketchat:lib'
]);

api.addFiles([
'server/settings.js',
'server/server.js'
], 'server');
api.addFiles('server/irc.js', 'server');
api.addFiles('server/methods/resetIrcConnection.js', 'server');
api.addFiles('server/irc-settings.js', 'server');
});

api.export(['Irc'], ['server']);
Npm.depends({
'queue-fifo': '0.2.4'
});
138 changes: 138 additions & 0 deletions packages/rocketchat-irc/server/irc-bridge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import Queue from 'queue-fifo';
import * as servers from '../servers';
import * as peerCommandHandlers from './peerHandlers';
import * as localCommandHandlers from './localHandlers';

class Bridge {
constructor(config) {
// General
this.config = config;

// Workaround for Rocket.Chat callbacks being called multiple times
this.loggedInUsers = [];

// Server
const Server = servers[this.config.server.protocol];

this.server = new Server(this.config);

this.setupPeerHandlers();
this.setupLocalHandlers();

// Command queue
this.queue = new Queue();
this.queueTimeout = 5;
}

init() {
this.loggedInUsers = [];
this.server.register();

this.server.on('registered', () => {
this.logQueue('Starting...');

this.runQueue();
});
}

stop() {
this.server.disconnect();
}

/**
* Log helper
*/
log(message) {
console.log(`[irc][bridge] ${ message }`);
}

logQueue(message) {
console.log(`[irc][bridge][queue] ${ message }`);
}

/**
*
*
* Queue
*
*
*/
onMessageReceived(from, command, ...parameters) {
this.queue.enqueue({ from, command, parameters });
}

async runQueue() {
// If it is empty, skip and keep the queue going
if (this.queue.isEmpty()) {
return setTimeout(this.runQueue.bind(this), this.queueTimeout);
}

// Get the command
const item = this.queue.dequeue();

this.logQueue(`Processing "${ item.command }" command from "${ item.from }"`);

// Handle the command accordingly
switch (item.from) {
case 'local':
if (!localCommandHandlers[item.command]) {
throw new Error(`Could not find handler for local:${ item.command }`);
}

await localCommandHandlers[item.command].apply(this, item.parameters);
break;
case 'peer':
if (!peerCommandHandlers[item.command]) {
throw new Error(`Could not find handler for peer:${ item.command }`);
}

await peerCommandHandlers[item.command].apply(this, item.parameters);
break;
}

// Keep the queue going
setTimeout(this.runQueue.bind(this), this.queueTimeout);
}

/**
*
*
* Peer
*
*
*/
setupPeerHandlers() {
this.server.on('peerCommand', (cmd) => {
this.onMessageReceived('peer', cmd.identifier, cmd.args);
});
}

/**
*
*
* Local
*
*
*/
setupLocalHandlers() {
// Auth
RocketChat.callbacks.add('afterValidateLogin', this.onMessageReceived.bind(this, 'local', 'onLogin'), RocketChat.callbacks.priority.LOW, 'irc-on-login');
RocketChat.callbacks.add('afterCreateUser', this.onMessageReceived.bind(this, 'local', 'onCreateUser'), RocketChat.callbacks.priority.LOW, 'irc-on-create-user');
// Joining rooms or channels
RocketChat.callbacks.add('afterCreateChannel', this.onMessageReceived.bind(this, 'local', 'onCreateRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-create-channel');
RocketChat.callbacks.add('afterCreateRoom', this.onMessageReceived.bind(this, 'local', 'onCreateRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-create-room');
RocketChat.callbacks.add('afterJoinRoom', this.onMessageReceived.bind(this, 'local', 'onJoinRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-join-room');
// Leaving rooms or channels
RocketChat.callbacks.add('afterLeaveRoom', this.onMessageReceived.bind(this, 'local', 'onLeaveRoom'), RocketChat.callbacks.priority.LOW, 'irc-on-leave-room');
// Chatting
RocketChat.callbacks.add('afterSaveMessage', this.onMessageReceived.bind(this, 'local', 'onSaveMessage'), RocketChat.callbacks.priority.LOW, 'irc-on-save-message');
// Leaving
RocketChat.callbacks.add('afterLogoutCleanUp', this.onMessageReceived.bind(this, 'local', 'onLogout'), RocketChat.callbacks.priority.LOW, 'irc-on-logout');
}

sendCommand(command, parameters) {
this.server.emit('onReceiveFromLocal', command, parameters);
}
}

export default Bridge;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import onCreateRoom from './onCreateRoom';
import onJoinRoom from './onJoinRoom';
import onLeaveRoom from './onLeaveRoom';
import onLogin from './onLogin';
import onLogout from './onLogout';
import onSaveMessage from './onSaveMessage';
import onCreateUser from './onCreateUser';

export { onCreateRoom, onJoinRoom, onLeaveRoom, onLogin, onLogout, onSaveMessage, onCreateUser };
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function handleOnCreateRoom(user, room) {
if (!room.usernames) {
return this.log(`Room ${ room.name } does not have a valid list of usernames`);
}

for (const username of room.usernames) {
const user = RocketChat.models.Users.findOne({ username });

if (user.profile.irc.fromIRC) {
this.sendCommand('joinChannel', { room, user });
} else {
this.sendCommand('joinedChannel', { room, user });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default function handleOnCreateUser(newUser) {
if (!newUser) {
return this.log('Invalid handleOnCreateUser call');
}
if (!newUser.username) {
return this.log('Invalid handleOnCreateUser call (Missing username)');
}
if (this.loggedInUsers.indexOf(newUser._id) !== -1) {
return this.log('Duplicate handleOnCreateUser call');
}

this.loggedInUsers.push(newUser._id);

Meteor.users.update({ _id: newUser._id }, {
$set: {
'profile.irc.fromIRC': false,
'profile.irc.username': `${ newUser.username }-rkt`,
'profile.irc.nick': `${ newUser.username }-rkt`,
'profile.irc.hostname': 'rocket.chat'
}
});

const user = RocketChat.models.Users.findOne({
_id: newUser._id
});

this.sendCommand('registerUser', user);

const rooms = RocketChat.models.Rooms.findWithUsername(user.username).fetch();

rooms.forEach(room => this.sendCommand('joinedChannel', { room, user }));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function handleOnJoinRoom(user, room) {
this.sendCommand('joinedChannel', { room, user });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function handleOnLeaveRoom(user, room) {
this.sendCommand('leftChannel', { room, user });
}
32 changes: 32 additions & 0 deletions packages/rocketchat-irc/server/irc-bridge/localHandlers/onLogin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default function handleOnLogin(login) {
if (login.user === null) {
return this.log('Invalid handleOnLogin call');
}
if (!login.user.username) {
return this.log('Invalid handleOnLogin call (Missing username)');
}
if (this.loggedInUsers.indexOf(login.user._id) !== -1) {
return this.log('Duplicate handleOnLogin call');
}

this.loggedInUsers.push(login.user._id);

Meteor.users.update({ _id: login.user._id }, {
$set: {
'profile.irc.fromIRC': false,
'profile.irc.username': `${ login.user.username }-rkt`,
'profile.irc.nick': `${ login.user.username }-rkt`,
'profile.irc.hostname': 'rocket.chat'
}
});

const user = RocketChat.models.Users.findOne({
_id: login.user._id
});

this.sendCommand('registerUser', user);

const rooms = RocketChat.models.Rooms.findWithUsername(user.username).fetch();

rooms.forEach(room => this.sendCommand('joinedChannel', { room, user }));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import _ from 'underscore';

export default function handleOnLogout(user) {
this.loggedInUsers = _.without(this.loggedInUsers, user._id);

this.sendCommand('disconnected', { user });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default function handleOnSaveMessage(message, to) {
let toIdentification = '';
// Direct message
if (to.t === 'd') {
const subscriptions = RocketChat.models.Subscriptions.findByRoomId(to._id);
subscriptions.forEach((subscription) => {
if (subscription.u.username !== to.username) {
const userData = RocketChat.models.Users.findOne({ username: subscription.u.username });
if (userData) {
if (userData.profile && userData.profile.irc && userData.profile.irc.nick) {
toIdentification = userData.profile.irc.nick;
} else {
toIdentification = userData.username;
}
} else {
toIdentification = subscription.u.username;
}
}
});

if (!toIdentification) {
console.error('[irc][server] Target user not found');
return;
}
} else {
toIdentification = `#${ to.name }`;
}

const user = RocketChat.models.Users.findOne({ _id: message.u._id });

this.sendCommand('sentMessage', { to: toIdentification, user, message: message.msg });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function handleQUIT(args) {
const user = RocketChat.models.Users.findOne({
'profile.irc.nick': args.nick
});

Meteor.users.update({ _id: user._id }, {
$set: {
status: 'offline'
}
});

RocketChat.models.Rooms.removeUsernameFromAll(user.username);
}
Loading

0 comments on commit 17a63ec

Please sign in to comment.