snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
69
apps/meteor/server/bridges/irc/README.md
Normal file
69
apps/meteor/server/bridges/irc/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
##IRC Package for Rocket.Chat
|
||||
|
||||
### Overview
|
||||
|
||||
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.
|
||||
|
||||
This package does not attempt to replace Rocket.Chat functionality nor operate as an IRC server for third-party client support. Rather, it utilizes Slack's similarities to IRC by allowing users to transparently access external IRC servers (e.g. Freenode) through Rocket.Chat's desktop and mobile app frontends.
|
||||
|
||||
|
||||
### Development
|
||||
|
||||
Core functionality is still in its infancy and needs improvement! If your objectives are broader than the stated scope of this project, please consider creating a separate package. Bugs and feature requests should be reported at [github.com/RocketChat/Rocket.Chat/issues](https://github.com/RocketChat/Rocket.Chat/issues)
|
||||
|
||||
The most active conversation on this package can be found in [Issue 216](https://github.com/RocketChat/Rocket.Chat/issues/216).
|
||||
|
||||
|
||||
### Build Notes
|
||||
|
||||
It is strongly encouraged to not enable this package in a production environment. There is currently no strict user account oversight, meaning problematic interoperation with an IRC server may interfere with Rocket.Chat's authentication and room behavior.
|
||||
|
||||
These *nix build notes are provided for development purposes only and do not include instructions on using SSL for encrypted authentication. Additionally, the following must be installed:
|
||||
|
||||
* [Node.js](http://nodejs.org)
|
||||
* [Meteor](http://www.meteor.com)
|
||||
* [mongoDB](http://www.mongodb.com)
|
||||
|
||||
#### Clone the Rocket.Chat repo via Git
|
||||
|
||||
git clone https://github.com/RocketChat/Rocket.Chat.git
|
||||
|
||||
#### Edit the package list
|
||||
|
||||
Change to the top-level directory of your Rocket.Chat repo. Edit .meteor/packages and uncomment the following:
|
||||
|
||||
#rocketchat:irc
|
||||
|
||||
#### Export your environment variables
|
||||
Modify as necessary.
|
||||
|
||||
export ROOT_URL=http://localhost
|
||||
export MONGO_URL=mongodb://localhost:27017/rocketchat
|
||||
export PORT=3000
|
||||
|
||||
#### Compile the development environment
|
||||
|
||||
meteor
|
||||
|
||||
Once all dependencies are met and the compile has completed successfully, you can access your Rocket.Chat build via http://localhost:3000
|
||||
|
||||
You will then need to create a new account, go into the admin settings, choose IRC, enable it, and restart Rocket.Chat. This complexity will decrease once the original codebase is retooled.
|
||||
|
||||
As you use Rocket.Chat, be sure to monitor the console terminal (or Rocket.Chat's web admin log) for IRC errors and activity.
|
||||
|
||||
|
||||
### Changelog
|
||||
|
||||
Please see the [commit history](https://github.com/RocketChat/Rocket.Chat/commits/develop/packages/rocketchat-irc) on GitHub for a full list of changes.
|
||||
|
||||
Major functional changes:
|
||||
|
||||
#### 0.0.2
|
||||
|
||||
* Remapped IRC username and real name to match Rocket.Chat's user accounts
|
||||
* Added base support for config via web
|
||||
* Performed package housecleaning for future growth
|
||||
|
||||
#### 0.0.1
|
||||
|
||||
* Core concept, design, and functionality
|
||||
1
apps/meteor/server/bridges/irc/index.ts
Normal file
1
apps/meteor/server/bridges/irc/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './methods/resetIrcConnection';
|
||||
239
apps/meteor/server/bridges/irc/irc-bridge/index.js
Normal file
239
apps/meteor/server/bridges/irc/irc-bridge/index.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
import moment from 'moment';
|
||||
import Queue from 'queue-fifo';
|
||||
|
||||
import * as localCommandHandlers from './localHandlers';
|
||||
import * as peerCommandHandlers from './peerHandlers';
|
||||
import { withThrottling } from '../../../../lib/utils/highOrderFunctions';
|
||||
import { callbacks } from '../../../lib/callbacks';
|
||||
import { afterLeaveRoomCallback } from '../../../lib/callbacks/afterLeaveRoomCallback';
|
||||
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
|
||||
import { notifyOnSettingChangedById } from '../../../lib/notifyListener';
|
||||
import { updateAuditedBySystem } from '../../../settings/lib/auditedSettingUpdates';
|
||||
import * as servers from '../servers';
|
||||
|
||||
const logger = new Logger('IRC Bridge');
|
||||
const queueLogger = logger.section('Queue');
|
||||
|
||||
let removed = false;
|
||||
const updateLastPing = withThrottling({ wait: 10_000 })(() => {
|
||||
if (removed) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
const updatedValue = await updateAuditedBySystem({
|
||||
reason: 'updateLastPing',
|
||||
})(Settings.updateValueById, 'IRC_Bridge_Last_Ping', new Date(), { upsert: true });
|
||||
if (updatedValue.modifiedCount || updatedValue.upsertedCount) {
|
||||
void notifyOnSettingChangedById('IRC_Bridge_Last_Ping');
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.initTime = new Date();
|
||||
removed = false;
|
||||
this.loggedInUsers = [];
|
||||
|
||||
const lastPing = await Settings.findOneById('IRC_Bridge_Last_Ping');
|
||||
if (lastPing) {
|
||||
if (Math.abs(moment(lastPing.value).diff()) < 1000 * 30) {
|
||||
this.log('Not trying to connect.');
|
||||
this.remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.log('Connecting.');
|
||||
updateLastPing();
|
||||
this.server.register();
|
||||
|
||||
this.server.on('registered', () => {
|
||||
this.logQueue('Starting...');
|
||||
|
||||
this.runQueue();
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.server.disconnect();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.log('Removing current connection.');
|
||||
removed = true;
|
||||
this.server = null;
|
||||
this.removeLocalHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log helper
|
||||
*/
|
||||
log(message) {
|
||||
// TODO logger: debug?
|
||||
logger.info(message);
|
||||
}
|
||||
|
||||
logQueue(message) {
|
||||
// TODO logger: debug?
|
||||
queueLogger.info(message);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Queue
|
||||
*
|
||||
*
|
||||
*/
|
||||
onMessageReceived(from, command, ...parameters) {
|
||||
this.queue.enqueue({ from, command, parameters });
|
||||
}
|
||||
|
||||
async runQueue() {
|
||||
if (!this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastResetTime = Settings.findOneById('IRC_Bridge_Reset_Time');
|
||||
if (lastResetTime && lastResetTime.value > this.initTime) {
|
||||
this.stop();
|
||||
this.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
updateLastPing();
|
||||
|
||||
// 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({ msg: 'Processing command from source', command: item.command, from: item.from });
|
||||
|
||||
// Handle the command accordingly
|
||||
try {
|
||||
// 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;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logQueue(e);
|
||||
}
|
||||
|
||||
// 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
|
||||
callbacks.add('afterValidateLogin', this.onMessageReceived.bind(this, 'local', 'onLogin'), callbacks.priority.LOW, 'irc-on-login');
|
||||
callbacks.add(
|
||||
'afterCreateUser',
|
||||
this.onMessageReceived.bind(this, 'local', 'onCreateUser'),
|
||||
callbacks.priority.LOW,
|
||||
'irc-on-create-user',
|
||||
);
|
||||
// Joining rooms or channels
|
||||
callbacks.add(
|
||||
'afterCreateChannel',
|
||||
this.onMessageReceived.bind(this, 'local', 'onCreateRoom'),
|
||||
callbacks.priority.LOW,
|
||||
'irc-on-create-channel',
|
||||
);
|
||||
callbacks.add(
|
||||
'afterCreateRoom',
|
||||
this.onMessageReceived.bind(this, 'local', 'onCreateRoom'),
|
||||
callbacks.priority.LOW,
|
||||
'irc-on-create-room',
|
||||
);
|
||||
callbacks.add('afterJoinRoom', this.onMessageReceived.bind(this, 'local', 'onJoinRoom'), callbacks.priority.LOW, 'irc-on-join-room');
|
||||
// Leaving rooms or channels
|
||||
afterLeaveRoomCallback.add(this.onMessageReceived.bind(this, 'local', 'onLeaveRoom'), callbacks.priority.LOW, 'irc-on-leave-room');
|
||||
// Chatting
|
||||
callbacks.add(
|
||||
'afterSaveMessage',
|
||||
(message, { room }) => this.onMessageReceived('local', 'onSaveMessage', message, room),
|
||||
callbacks.priority.LOW,
|
||||
'irc-on-save-message',
|
||||
);
|
||||
// Leaving
|
||||
afterLogoutCleanUpCallback.add(this.onMessageReceived.bind(this, 'local', 'onLogout'), callbacks.priority.LOW, 'irc-on-logout');
|
||||
}
|
||||
|
||||
removeLocalHandlers() {
|
||||
callbacks.remove('afterValidateLogin', 'irc-on-login');
|
||||
callbacks.remove('afterCreateUser', 'irc-on-create-user');
|
||||
callbacks.remove('afterCreateChannel', 'irc-on-create-channel');
|
||||
callbacks.remove('afterCreateRoom', 'irc-on-create-room');
|
||||
callbacks.remove('afterJoinRoom', 'irc-on-join-room');
|
||||
afterLeaveRoomCallback.remove('irc-on-leave-room');
|
||||
callbacks.remove('afterSaveMessage', 'irc-on-save-message');
|
||||
afterLogoutCleanUpCallback.remove('irc-on-logout');
|
||||
}
|
||||
|
||||
sendCommand(command, parameters) {
|
||||
this.server.emit('onReceiveFromLocal', command, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
export default Bridge;
|
||||
@@ -0,0 +1,9 @@
|
||||
import onCreateRoom from './onCreateRoom';
|
||||
import onCreateUser from './onCreateUser';
|
||||
import onJoinRoom from './onJoinRoom';
|
||||
import onLeaveRoom from './onLeaveRoom';
|
||||
import onLogin from './onLogin';
|
||||
import onLogout from './onLogout';
|
||||
import onSaveMessage from './onSaveMessage';
|
||||
|
||||
export { onCreateRoom, onJoinRoom, onLeaveRoom, onLogin, onLogout, onSaveMessage, onCreateUser };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
export default async function handleOnCreateRoom(user, room) {
|
||||
const users = await Users.findByRoomId(room._id);
|
||||
|
||||
users.forEach((user) => {
|
||||
if (user.profile?.irc?.fromIRC) {
|
||||
this.sendCommand('joinChannel', { room, user });
|
||||
} else {
|
||||
this.sendCommand('joinedChannel', { room, user });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
|
||||
export default async 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);
|
||||
|
||||
await Users.updateOne(
|
||||
{ _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 = await Users.findOne({
|
||||
_id: newUser._id,
|
||||
});
|
||||
|
||||
this.sendCommand('registerUser', user);
|
||||
|
||||
const rooms = await Rooms.findBySubscriptionUserId(user._id).toArray();
|
||||
|
||||
rooms.forEach((room) => this.sendCommand('joinedChannel', { room, user }));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default async function handleOnJoinRoom(user, room) {
|
||||
this.sendCommand('joinedChannel', { room, user });
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default async function handleOnLeaveRoom(user, room) {
|
||||
this.sendCommand('leftChannel', { room, user });
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
|
||||
export default async 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);
|
||||
|
||||
await Users.updateOne(
|
||||
{ _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 = await Users.findOne({
|
||||
_id: login.user._id,
|
||||
});
|
||||
|
||||
this.sendCommand('registerUser', user);
|
||||
const rooms = await Rooms.findBySubscriptionUserId(user._id).toArray();
|
||||
|
||||
rooms.forEach((room) => {
|
||||
if (room.t === 'd') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendCommand('joinedChannel', { room, user });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import _ from 'underscore';
|
||||
|
||||
export default async function handleOnLogout(user) {
|
||||
this.loggedInUsers = _.without(this.loggedInUsers, user._id);
|
||||
|
||||
this.sendCommand('disconnected', { user });
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Subscriptions, Users } from '@rocket.chat/models';
|
||||
|
||||
import { SystemLogger } from '../../../../lib/logger/system';
|
||||
|
||||
export default async function handleOnSaveMessage(message, to) {
|
||||
let toIdentification = '';
|
||||
// Direct message
|
||||
if (to.t === 'd') {
|
||||
const subscriptions = Subscriptions.findByRoomId(to._id).toArray();
|
||||
for await (const subscription of subscriptions) {
|
||||
if (subscription.u._id !== message.u._id) {
|
||||
const userData = await 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) {
|
||||
SystemLogger.error('[irc][server] Target user not found');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
toIdentification = `#${to.name}`;
|
||||
}
|
||||
|
||||
const user = await Users.findOne({ _id: message.u._id });
|
||||
|
||||
this.sendCommand('sentMessage', { to: toIdentification, user, message: message.msg });
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { notifyOnUserChange } from '../../../../lib/notifyListener';
|
||||
|
||||
export default async function handleQUIT(args) {
|
||||
const user = await Users.findOne({
|
||||
'profile.irc.nick': args.nick,
|
||||
});
|
||||
|
||||
await Users.updateOne(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
status: 'offline',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff: { status: 'offline' } });
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import disconnected from './disconnected';
|
||||
import joinedChannel from './joinedChannel';
|
||||
import leftChannel from './leftChannel';
|
||||
import nickChanged from './nickChanged';
|
||||
import sentMessage from './sentMessage';
|
||||
import userRegistered from './userRegistered';
|
||||
|
||||
export { disconnected, joinedChannel, leftChannel, nickChanged, sentMessage, userRegistered };
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Users, Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { addUserToRoom } from '../../../../lib/rooms/addUserToRoom';
|
||||
import { createRoom } from '../../../../lib/rooms/createRoom';
|
||||
|
||||
// TODO doesn't seem to be used anywhere, remove
|
||||
export default async function handleJoinedChannel(args) {
|
||||
const user = await Users.findOne({
|
||||
'profile.irc.nick': args.nick,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find a user with nick ${args.nick}`);
|
||||
}
|
||||
|
||||
let room = await Rooms.findOneByName(args.roomName);
|
||||
|
||||
if (!room) {
|
||||
const createdRoom = await createRoom('c', args.roomName, user, []);
|
||||
room = await Rooms.findOne({ _id: createdRoom.rid });
|
||||
|
||||
this.log(`${user.username} created room ${args.roomName}`);
|
||||
} else {
|
||||
await addUserToRoom(room._id, user);
|
||||
|
||||
this.log(`${user.username} joined room ${room.name}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Users, Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { removeUserFromRoom } from '../../../../lib/rooms/removeUserFromRoom';
|
||||
|
||||
export default async function handleLeftChannel(args) {
|
||||
const user = await Users.findOne({
|
||||
'profile.irc.nick': args.nick,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find a user with nick ${args.nick}`);
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneByName(args.roomName);
|
||||
|
||||
if (!room) {
|
||||
throw new Error(`Could not find a room with name ${args.roomName}`);
|
||||
}
|
||||
|
||||
this.log(`${user.username} left room ${room.name}`);
|
||||
await removeUserFromRoom(room._id, user);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { notifyOnUserChange } from '../../../../lib/notifyListener';
|
||||
|
||||
export default async function handleNickChanged(args) {
|
||||
const user = await Users.findOne({
|
||||
'profile.irc.nick': args.nick,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find an user with nick ${args.nick}`);
|
||||
}
|
||||
|
||||
this.log(`${user.username} changed nick: ${args.nick} -> ${args.newNick}`);
|
||||
|
||||
// Update on the database
|
||||
await Users.updateOne(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
'name': args.newNick,
|
||||
'profile.irc.nick': args.newNick,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { name: args.newNick } });
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Users, Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { sendMessage } from '../../../../lib/messages/sendMessage';
|
||||
import { createDirectRoom } from '../../../../lib/rooms/createDirectRoom';
|
||||
|
||||
/*
|
||||
*
|
||||
* Get direct chat room helper
|
||||
*
|
||||
*
|
||||
*/
|
||||
const getDirectRoom = async (source, target) => {
|
||||
const uids = [source._id, target._id];
|
||||
const { _id, ...extraData } = await createDirectRoom([source, target]);
|
||||
|
||||
const room = await Rooms.findOneDirectRoomContainingAllUserIDs(uids);
|
||||
if (room) {
|
||||
return {
|
||||
t: 'd',
|
||||
...room,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
_id,
|
||||
t: 'd',
|
||||
...extraData,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function handleSentMessage(args) {
|
||||
const user = await Users.findOne({
|
||||
'profile.irc.nick': args.nick,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find a user with nick ${args.nick}`);
|
||||
}
|
||||
|
||||
let room;
|
||||
|
||||
if (args.roomName) {
|
||||
room = await Rooms.findOneByName(args.roomName);
|
||||
} else {
|
||||
const recipientUser = await Users.findOne({
|
||||
'profile.irc.nick': args.recipientNick,
|
||||
});
|
||||
|
||||
room = await getDirectRoom(user, recipientUser);
|
||||
}
|
||||
|
||||
const message = {
|
||||
msg: args.message,
|
||||
ts: new Date(),
|
||||
};
|
||||
|
||||
await sendMessage(user, message, room);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { notifyOnUserChange } from '../../../../lib/notifyListener';
|
||||
|
||||
export default async function handleUserRegistered(args) {
|
||||
// Check if there is an user with the given username
|
||||
let user = await Users.findOne({
|
||||
'profile.irc.username': args.username,
|
||||
});
|
||||
|
||||
// If there is no user, create one...
|
||||
if (!user) {
|
||||
this.log(`Registering ${args.username} with nick: ${args.nick}`);
|
||||
|
||||
const userToInsert = {
|
||||
name: args.nick,
|
||||
username: `${args.username}-irc`,
|
||||
status: 'online',
|
||||
utcOffset: 0,
|
||||
active: true,
|
||||
type: 'user',
|
||||
profile: {
|
||||
irc: {
|
||||
fromIRC: true,
|
||||
nick: args.nick,
|
||||
username: args.username,
|
||||
hostname: args.hostname,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
user = await Users.create(userToInsert);
|
||||
|
||||
void notifyOnUserChange({ id: user._id, clientAction: 'inserted', data: user });
|
||||
} else {
|
||||
// ...otherwise, log the user in and update the information
|
||||
this.log(`Logging in ${args.username} with nick: ${args.nick}`);
|
||||
|
||||
await Users.updateOne(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
'status': 'online',
|
||||
'profile.irc.nick': args.nick,
|
||||
'profile.irc.username': args.username,
|
||||
'profile.irc.hostname': args.hostname,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff: { status: 'online' } });
|
||||
}
|
||||
}
|
||||
91
apps/meteor/server/bridges/irc/methods/resetIrcConnection.ts
Normal file
91
apps/meteor/server/bridges/irc/methods/resetIrcConnection.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { notifyOnSettingChangedById } from '../../../lib/notifyListener';
|
||||
import { settings } from '../../../settings';
|
||||
import { updateAuditedByUser } from '../../../settings/lib/auditedSettingUpdates';
|
||||
import Bridge from '../irc-bridge';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
resetIrcConnection(): { message: string; params: unknown[] };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async resetIrcConnection() {
|
||||
const ircEnabled = Boolean(settings.get('IRC_Enabled'));
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'resetIrcConnection' });
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'edit-privileged-setting'))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'resetIrcConnection' });
|
||||
}
|
||||
|
||||
const auditSettingOperation = updateAuditedByUser({
|
||||
_id: uid,
|
||||
username: (await Meteor.userAsync())!.username!,
|
||||
ip: this.connection?.clientAddress || '',
|
||||
useragent: this.connection?.httpHeaders['user-agent'] || '',
|
||||
});
|
||||
|
||||
const updatedLastPingValue = await auditSettingOperation(Settings.updateValueById, 'IRC_Bridge_Last_Ping', new Date(0), {
|
||||
upsert: true,
|
||||
});
|
||||
if (updatedLastPingValue.modifiedCount || updatedLastPingValue.upsertedCount) {
|
||||
void notifyOnSettingChangedById('IRC_Bridge_Last_Ping');
|
||||
}
|
||||
|
||||
const updatedResetTimeValue = await auditSettingOperation(Settings.updateValueById, 'IRC_Bridge_Reset_Time', new Date(), {
|
||||
upsert: true,
|
||||
});
|
||||
if (updatedResetTimeValue.modifiedCount || updatedResetTimeValue.upsertedCount) {
|
||||
void notifyOnSettingChangedById('IRC_Bridge_Last_Ping');
|
||||
}
|
||||
|
||||
if (!ircEnabled) {
|
||||
return {
|
||||
message: 'Connection_Closed',
|
||||
params: [],
|
||||
};
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
// Normalize the config values
|
||||
const config = {
|
||||
server: {
|
||||
protocol: settings.get('IRC_Protocol'),
|
||||
host: settings.get('IRC_Host'),
|
||||
port: settings.get('IRC_Port'),
|
||||
name: settings.get('IRC_Name'),
|
||||
description: settings.get('IRC_Description'),
|
||||
},
|
||||
passwords: {
|
||||
local: settings.get('IRC_Local_Password'),
|
||||
peer: settings.get('IRC_Peer_Password'),
|
||||
},
|
||||
};
|
||||
// TODO: is this the best way to do this? is this really necessary?
|
||||
Meteor.ircBridge = new Bridge(config);
|
||||
await Meteor.ircBridge.init();
|
||||
}, 300);
|
||||
|
||||
return {
|
||||
message: 'Connection_Reset',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
declare module 'meteor/meteor' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Meteor {
|
||||
let ircBridge: Bridge;
|
||||
}
|
||||
}
|
||||
519
apps/meteor/server/bridges/irc/servers/RFC2813/codes.js
Normal file
519
apps/meteor/server/bridges/irc/servers/RFC2813/codes.js
Normal file
@@ -0,0 +1,519 @@
|
||||
/**
|
||||
* This file is part of https://github.com/martynsmith/node-irc
|
||||
* by https://github.com/martynsmith
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'001': {
|
||||
name: 'rpl_welcome',
|
||||
type: 'reply',
|
||||
},
|
||||
'002': {
|
||||
name: 'rpl_yourhost',
|
||||
type: 'reply',
|
||||
},
|
||||
'003': {
|
||||
name: 'rpl_created',
|
||||
type: 'reply',
|
||||
},
|
||||
'004': {
|
||||
name: 'rpl_myinfo',
|
||||
type: 'reply',
|
||||
},
|
||||
'005': {
|
||||
name: 'rpl_isupport',
|
||||
type: 'reply',
|
||||
},
|
||||
'200': {
|
||||
name: 'rpl_tracelink',
|
||||
type: 'reply',
|
||||
},
|
||||
'201': {
|
||||
name: 'rpl_traceconnecting',
|
||||
type: 'reply',
|
||||
},
|
||||
'202': {
|
||||
name: 'rpl_tracehandshake',
|
||||
type: 'reply',
|
||||
},
|
||||
'203': {
|
||||
name: 'rpl_traceunknown',
|
||||
type: 'reply',
|
||||
},
|
||||
'204': {
|
||||
name: 'rpl_traceoperator',
|
||||
type: 'reply',
|
||||
},
|
||||
'205': {
|
||||
name: 'rpl_traceuser',
|
||||
type: 'reply',
|
||||
},
|
||||
'206': {
|
||||
name: 'rpl_traceserver',
|
||||
type: 'reply',
|
||||
},
|
||||
'208': {
|
||||
name: 'rpl_tracenewtype',
|
||||
type: 'reply',
|
||||
},
|
||||
'211': {
|
||||
name: 'rpl_statslinkinfo',
|
||||
type: 'reply',
|
||||
},
|
||||
'212': {
|
||||
name: 'rpl_statscommands',
|
||||
type: 'reply',
|
||||
},
|
||||
'213': {
|
||||
name: 'rpl_statscline',
|
||||
type: 'reply',
|
||||
},
|
||||
'214': {
|
||||
name: 'rpl_statsnline',
|
||||
type: 'reply',
|
||||
},
|
||||
'215': {
|
||||
name: 'rpl_statsiline',
|
||||
type: 'reply',
|
||||
},
|
||||
'216': {
|
||||
name: 'rpl_statskline',
|
||||
type: 'reply',
|
||||
},
|
||||
'218': {
|
||||
name: 'rpl_statsyline',
|
||||
type: 'reply',
|
||||
},
|
||||
'219': {
|
||||
name: 'rpl_endofstats',
|
||||
type: 'reply',
|
||||
},
|
||||
'221': {
|
||||
name: 'rpl_umodeis',
|
||||
type: 'reply',
|
||||
},
|
||||
'241': {
|
||||
name: 'rpl_statslline',
|
||||
type: 'reply',
|
||||
},
|
||||
'242': {
|
||||
name: 'rpl_statsuptime',
|
||||
type: 'reply',
|
||||
},
|
||||
'243': {
|
||||
name: 'rpl_statsoline',
|
||||
type: 'reply',
|
||||
},
|
||||
'244': {
|
||||
name: 'rpl_statshline',
|
||||
type: 'reply',
|
||||
},
|
||||
'250': {
|
||||
name: 'rpl_statsconn',
|
||||
type: 'reply',
|
||||
},
|
||||
'251': {
|
||||
name: 'rpl_luserclient',
|
||||
type: 'reply',
|
||||
},
|
||||
'252': {
|
||||
name: 'rpl_luserop',
|
||||
type: 'reply',
|
||||
},
|
||||
'253': {
|
||||
name: 'rpl_luserunknown',
|
||||
type: 'reply',
|
||||
},
|
||||
'254': {
|
||||
name: 'rpl_luserchannels',
|
||||
type: 'reply',
|
||||
},
|
||||
'255': {
|
||||
name: 'rpl_luserme',
|
||||
type: 'reply',
|
||||
},
|
||||
'256': {
|
||||
name: 'rpl_adminme',
|
||||
type: 'reply',
|
||||
},
|
||||
'257': {
|
||||
name: 'rpl_adminloc1',
|
||||
type: 'reply',
|
||||
},
|
||||
'258': {
|
||||
name: 'rpl_adminloc2',
|
||||
type: 'reply',
|
||||
},
|
||||
'259': {
|
||||
name: 'rpl_adminemail',
|
||||
type: 'reply',
|
||||
},
|
||||
'261': {
|
||||
name: 'rpl_tracelog',
|
||||
type: 'reply',
|
||||
},
|
||||
'265': {
|
||||
name: 'rpl_localusers',
|
||||
type: 'reply',
|
||||
},
|
||||
'266': {
|
||||
name: 'rpl_globalusers',
|
||||
type: 'reply',
|
||||
},
|
||||
'300': {
|
||||
name: 'rpl_none',
|
||||
type: 'reply',
|
||||
},
|
||||
'301': {
|
||||
name: 'rpl_away',
|
||||
type: 'reply',
|
||||
},
|
||||
'302': {
|
||||
name: 'rpl_userhost',
|
||||
type: 'reply',
|
||||
},
|
||||
'303': {
|
||||
name: 'rpl_ison',
|
||||
type: 'reply',
|
||||
},
|
||||
'305': {
|
||||
name: 'rpl_unaway',
|
||||
type: 'reply',
|
||||
},
|
||||
'306': {
|
||||
name: 'rpl_nowaway',
|
||||
type: 'reply',
|
||||
},
|
||||
'311': {
|
||||
name: 'rpl_whoisuser',
|
||||
type: 'reply',
|
||||
},
|
||||
'312': {
|
||||
name: 'rpl_whoisserver',
|
||||
type: 'reply',
|
||||
},
|
||||
'313': {
|
||||
name: 'rpl_whoisoperator',
|
||||
type: 'reply',
|
||||
},
|
||||
'314': {
|
||||
name: 'rpl_whowasuser',
|
||||
type: 'reply',
|
||||
},
|
||||
'315': {
|
||||
name: 'rpl_endofwho',
|
||||
type: 'reply',
|
||||
},
|
||||
'317': {
|
||||
name: 'rpl_whoisidle',
|
||||
type: 'reply',
|
||||
},
|
||||
'318': {
|
||||
name: 'rpl_endofwhois',
|
||||
type: 'reply',
|
||||
},
|
||||
'319': {
|
||||
name: 'rpl_whoischannels',
|
||||
type: 'reply',
|
||||
},
|
||||
'321': {
|
||||
name: 'rpl_liststart',
|
||||
type: 'reply',
|
||||
},
|
||||
'322': {
|
||||
name: 'rpl_list',
|
||||
type: 'reply',
|
||||
},
|
||||
'323': {
|
||||
name: 'rpl_listend',
|
||||
type: 'reply',
|
||||
},
|
||||
'324': {
|
||||
name: 'rpl_channelmodeis',
|
||||
type: 'reply',
|
||||
},
|
||||
'329': {
|
||||
name: 'rpl_creationtime',
|
||||
type: 'reply',
|
||||
},
|
||||
'331': {
|
||||
name: 'rpl_notopic',
|
||||
type: 'reply',
|
||||
},
|
||||
'332': {
|
||||
name: 'rpl_topic',
|
||||
type: 'reply',
|
||||
},
|
||||
'333': {
|
||||
name: 'rpl_topicwhotime',
|
||||
type: 'reply',
|
||||
},
|
||||
'341': {
|
||||
name: 'rpl_inviting',
|
||||
type: 'reply',
|
||||
},
|
||||
'342': {
|
||||
name: 'rpl_summoning',
|
||||
type: 'reply',
|
||||
},
|
||||
'351': {
|
||||
name: 'rpl_version',
|
||||
type: 'reply',
|
||||
},
|
||||
'352': {
|
||||
name: 'rpl_whoreply',
|
||||
type: 'reply',
|
||||
},
|
||||
'353': {
|
||||
name: 'rpl_namreply',
|
||||
type: 'reply',
|
||||
},
|
||||
'364': {
|
||||
name: 'rpl_links',
|
||||
type: 'reply',
|
||||
},
|
||||
'365': {
|
||||
name: 'rpl_endoflinks',
|
||||
type: 'reply',
|
||||
},
|
||||
'366': {
|
||||
name: 'rpl_endofnames',
|
||||
type: 'reply',
|
||||
},
|
||||
'367': {
|
||||
name: 'rpl_banlist',
|
||||
type: 'reply',
|
||||
},
|
||||
'368': {
|
||||
name: 'rpl_endofbanlist',
|
||||
type: 'reply',
|
||||
},
|
||||
'369': {
|
||||
name: 'rpl_endofwhowas',
|
||||
type: 'reply',
|
||||
},
|
||||
'371': {
|
||||
name: 'rpl_info',
|
||||
type: 'reply',
|
||||
},
|
||||
'372': {
|
||||
name: 'rpl_motd',
|
||||
type: 'reply',
|
||||
},
|
||||
'374': {
|
||||
name: 'rpl_endofinfo',
|
||||
type: 'reply',
|
||||
},
|
||||
'375': {
|
||||
name: 'rpl_motdstart',
|
||||
type: 'reply',
|
||||
},
|
||||
'376': {
|
||||
name: 'rpl_endofmotd',
|
||||
type: 'reply',
|
||||
},
|
||||
'381': {
|
||||
name: 'rpl_youreoper',
|
||||
type: 'reply',
|
||||
},
|
||||
'382': {
|
||||
name: 'rpl_rehashing',
|
||||
type: 'reply',
|
||||
},
|
||||
'391': {
|
||||
name: 'rpl_time',
|
||||
type: 'reply',
|
||||
},
|
||||
'392': {
|
||||
name: 'rpl_usersstart',
|
||||
type: 'reply',
|
||||
},
|
||||
'393': {
|
||||
name: 'rpl_users',
|
||||
type: 'reply',
|
||||
},
|
||||
'394': {
|
||||
name: 'rpl_endofusers',
|
||||
type: 'reply',
|
||||
},
|
||||
'395': {
|
||||
name: 'rpl_nousers',
|
||||
type: 'reply',
|
||||
},
|
||||
'401': {
|
||||
name: 'err_nosuchnick',
|
||||
type: 'error',
|
||||
},
|
||||
'402': {
|
||||
name: 'err_nosuchserver',
|
||||
type: 'error',
|
||||
},
|
||||
'403': {
|
||||
name: 'err_nosuchchannel',
|
||||
type: 'error',
|
||||
},
|
||||
'404': {
|
||||
name: 'err_cannotsendtochan',
|
||||
type: 'error',
|
||||
},
|
||||
'405': {
|
||||
name: 'err_toomanychannels',
|
||||
type: 'error',
|
||||
},
|
||||
'406': {
|
||||
name: 'err_wasnosuchnick',
|
||||
type: 'error',
|
||||
},
|
||||
'407': {
|
||||
name: 'err_toomanytargets',
|
||||
type: 'error',
|
||||
},
|
||||
'409': {
|
||||
name: 'err_noorigin',
|
||||
type: 'error',
|
||||
},
|
||||
'411': {
|
||||
name: 'err_norecipient',
|
||||
type: 'error',
|
||||
},
|
||||
'412': {
|
||||
name: 'err_notexttosend',
|
||||
type: 'error',
|
||||
},
|
||||
'413': {
|
||||
name: 'err_notoplevel',
|
||||
type: 'error',
|
||||
},
|
||||
'414': {
|
||||
name: 'err_wildtoplevel',
|
||||
type: 'error',
|
||||
},
|
||||
'421': {
|
||||
name: 'err_unknowncommand',
|
||||
type: 'error',
|
||||
},
|
||||
'422': {
|
||||
name: 'err_nomotd',
|
||||
type: 'error',
|
||||
},
|
||||
'423': {
|
||||
name: 'err_noadmininfo',
|
||||
type: 'error',
|
||||
},
|
||||
'424': {
|
||||
name: 'err_fileerror',
|
||||
type: 'error',
|
||||
},
|
||||
'431': {
|
||||
name: 'err_nonicknamegiven',
|
||||
type: 'error',
|
||||
},
|
||||
'432': {
|
||||
name: 'err_erroneusnickname',
|
||||
type: 'error',
|
||||
},
|
||||
'433': {
|
||||
name: 'err_nicknameinuse',
|
||||
type: 'error',
|
||||
},
|
||||
'436': {
|
||||
name: 'err_nickcollision',
|
||||
type: 'error',
|
||||
},
|
||||
'441': {
|
||||
name: 'err_usernotinchannel',
|
||||
type: 'error',
|
||||
},
|
||||
'442': {
|
||||
name: 'err_notonchannel',
|
||||
type: 'error',
|
||||
},
|
||||
'443': {
|
||||
name: 'err_useronchannel',
|
||||
type: 'error',
|
||||
},
|
||||
'444': {
|
||||
name: 'err_nologin',
|
||||
type: 'error',
|
||||
},
|
||||
'445': {
|
||||
name: 'err_summondisabled',
|
||||
type: 'error',
|
||||
},
|
||||
'446': {
|
||||
name: 'err_usersdisabled',
|
||||
type: 'error',
|
||||
},
|
||||
'451': {
|
||||
name: 'err_notregistered',
|
||||
type: 'error',
|
||||
},
|
||||
'461': {
|
||||
name: 'err_needmoreparams',
|
||||
type: 'error',
|
||||
},
|
||||
'462': {
|
||||
name: 'err_alreadyregistred',
|
||||
type: 'error',
|
||||
},
|
||||
'463': {
|
||||
name: 'err_nopermforhost',
|
||||
type: 'error',
|
||||
},
|
||||
'464': {
|
||||
name: 'err_passwdmismatch',
|
||||
type: 'error',
|
||||
},
|
||||
'465': {
|
||||
name: 'err_yourebannedcreep',
|
||||
type: 'error',
|
||||
},
|
||||
'467': {
|
||||
name: 'err_keyset',
|
||||
type: 'error',
|
||||
},
|
||||
'471': {
|
||||
name: 'err_channelisfull',
|
||||
type: 'error',
|
||||
},
|
||||
'472': {
|
||||
name: 'err_unknownmode',
|
||||
type: 'error',
|
||||
},
|
||||
'473': {
|
||||
name: 'err_inviteonlychan',
|
||||
type: 'error',
|
||||
},
|
||||
'474': {
|
||||
name: 'err_bannedfromchan',
|
||||
type: 'error',
|
||||
},
|
||||
'475': {
|
||||
name: 'err_badchannelkey',
|
||||
type: 'error',
|
||||
},
|
||||
'481': {
|
||||
name: 'err_noprivileges',
|
||||
type: 'error',
|
||||
},
|
||||
'482': {
|
||||
name: 'err_chanoprivsneeded',
|
||||
type: 'error',
|
||||
},
|
||||
'483': {
|
||||
name: 'err_cantkillserver',
|
||||
type: 'error',
|
||||
},
|
||||
'491': {
|
||||
name: 'err_nooperhost',
|
||||
type: 'error',
|
||||
},
|
||||
'501': {
|
||||
name: 'err_umodeunknownflag',
|
||||
type: 'error',
|
||||
},
|
||||
'502': {
|
||||
name: 'err_usersdontmatch',
|
||||
type: 'error',
|
||||
},
|
||||
};
|
||||
195
apps/meteor/server/bridges/irc/servers/RFC2813/index.js
Normal file
195
apps/meteor/server/bridges/irc/servers/RFC2813/index.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import net from 'node:net';
|
||||
import util from 'node:util';
|
||||
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
|
||||
import localCommandHandlers from './localCommandHandlers';
|
||||
import parseMessage from './parseMessage';
|
||||
import peerCommandHandlers from './peerCommandHandlers';
|
||||
|
||||
const logger = new Logger('IRC Server');
|
||||
|
||||
class RFC2813 {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
|
||||
// Hold registered state
|
||||
this.registerSteps = [];
|
||||
this.isRegistered = false;
|
||||
|
||||
// Hold peer server information
|
||||
this.serverPrefix = null;
|
||||
|
||||
// Hold the buffer while receiving
|
||||
this.receiveBuffer = Buffer.from('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup socket
|
||||
*/
|
||||
setupSocket() {
|
||||
// Setup socket
|
||||
this.socket = new net.Socket();
|
||||
this.socket.setNoDelay();
|
||||
this.socket.setEncoding('utf-8');
|
||||
this.socket.setKeepAlive(true);
|
||||
this.socket.setTimeout(90000);
|
||||
|
||||
this.socket.on('data', this.onReceiveFromPeer.bind(this));
|
||||
|
||||
this.socket.on('connect', this.onConnect.bind(this));
|
||||
this.socket.on('error', (err) => logger.error({ msg: 'Socket error', err }));
|
||||
this.socket.on('timeout', () => this.log({ msg: 'Timeout' }));
|
||||
this.socket.on('close', () => this.log({ msg: 'Connection Closed' }));
|
||||
// Setup local
|
||||
this.on('onReceiveFromLocal', this.onReceiveFromLocal.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log helper
|
||||
*/
|
||||
log(message) {
|
||||
// TODO logger: debug?
|
||||
if (typeof message === 'string') {
|
||||
logger.info({ msg: message });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect
|
||||
*/
|
||||
register() {
|
||||
this.log({
|
||||
msg: 'Connecting to IRC server',
|
||||
host: this.config.server.host,
|
||||
port: this.config.server.port,
|
||||
});
|
||||
|
||||
if (!this.socket) {
|
||||
this.setupSocket();
|
||||
}
|
||||
|
||||
this.socket.connect(this.config.server.port, this.config.server.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect
|
||||
*/
|
||||
disconnect() {
|
||||
this.log('Disconnecting from server.');
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
this.socket = undefined;
|
||||
}
|
||||
this.isRegistered = false;
|
||||
this.registerSteps = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the server connection
|
||||
*/
|
||||
onConnect() {
|
||||
this.log('Connected! Registering as server...');
|
||||
|
||||
this.write({
|
||||
command: 'PASS',
|
||||
parameters: [this.config.passwords.local, '0210', 'ngircd'],
|
||||
});
|
||||
|
||||
this.write({
|
||||
command: 'SERVER',
|
||||
parameters: [this.config.server.name],
|
||||
trailer: this.config.server.description,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command message through the socket
|
||||
*/
|
||||
write(command) {
|
||||
let buffer = command.prefix ? `:${command.prefix} ` : '';
|
||||
buffer += command.command;
|
||||
|
||||
if (command.parameters && command.parameters.length > 0) {
|
||||
buffer += ` ${command.parameters.join(' ')}`;
|
||||
}
|
||||
|
||||
if (command.trailer) {
|
||||
buffer += ` :${command.trailer}`;
|
||||
}
|
||||
|
||||
this.log({ msg: 'Sending Command', buffer });
|
||||
|
||||
return this.socket.write(`${buffer}\r\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Peer message handling
|
||||
*
|
||||
*
|
||||
*/
|
||||
onReceiveFromPeer(chunk) {
|
||||
if (typeof chunk === 'string') {
|
||||
this.receiveBuffer += chunk;
|
||||
} else {
|
||||
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
||||
}
|
||||
|
||||
const lines = this.receiveBuffer.toString().split(/\r\n|\r|\n|\u0007/); // eslint-disable-line no-control-regex
|
||||
|
||||
// If the buffer does not end with \r\n, more chunks are coming
|
||||
if (lines.pop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the buffer
|
||||
this.receiveBuffer = Buffer.from('');
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.length && !line.startsWith('a')) {
|
||||
const parsedMessage = parseMessage(line);
|
||||
|
||||
if (peerCommandHandlers[parsedMessage.command]) {
|
||||
this.log({ msg: 'Handling peer message', line });
|
||||
|
||||
const command = peerCommandHandlers[parsedMessage.command].call(this, parsedMessage);
|
||||
|
||||
if (command) {
|
||||
this.log({ msg: 'Emitting peer command to local', command });
|
||||
this.emit('peerCommand', command);
|
||||
}
|
||||
} else {
|
||||
this.log({ msg: 'Unhandled peer message', parsedMessage });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Local message handling
|
||||
*
|
||||
*
|
||||
*/
|
||||
onReceiveFromLocal(command, parameters) {
|
||||
if (localCommandHandlers[command]) {
|
||||
this.log({ msg: 'Handling local command', command });
|
||||
|
||||
localCommandHandlers[command].call(this, parameters, this);
|
||||
} else {
|
||||
this.log({ msg: 'Unhandled local command', command });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util.inherits(RFC2813, EventEmitter);
|
||||
|
||||
export default RFC2813;
|
||||
@@ -0,0 +1,111 @@
|
||||
function registerUser(parameters) {
|
||||
const {
|
||||
name,
|
||||
profile: {
|
||||
irc: { nick, username },
|
||||
},
|
||||
} = parameters;
|
||||
|
||||
this.write({
|
||||
prefix: this.config.server.name,
|
||||
command: 'NICK',
|
||||
parameters: [nick, 1, username, 'irc.rocket.chat', 1, '+i'],
|
||||
trailer: name,
|
||||
});
|
||||
}
|
||||
|
||||
function joinChannel(parameters) {
|
||||
const {
|
||||
room: { name: roomName },
|
||||
user: {
|
||||
profile: {
|
||||
irc: { nick },
|
||||
},
|
||||
},
|
||||
} = parameters;
|
||||
|
||||
this.write({
|
||||
prefix: this.config.server.name,
|
||||
command: 'NJOIN',
|
||||
parameters: [`#${roomName}`],
|
||||
trailer: nick,
|
||||
});
|
||||
}
|
||||
|
||||
function joinedChannel(parameters, handler) {
|
||||
const roomName = parameters.room?.name;
|
||||
const nick = parameters.user?.profile?.irc?.nick;
|
||||
|
||||
if (!roomName) {
|
||||
handler.log('Skipping room with no name.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nick) {
|
||||
handler.log('Skipping user with no irc nick.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.write({
|
||||
prefix: nick,
|
||||
command: 'JOIN',
|
||||
parameters: [`#${roomName}`],
|
||||
});
|
||||
}
|
||||
|
||||
function leftChannel(parameters) {
|
||||
const {
|
||||
room: { name: roomName },
|
||||
user: {
|
||||
profile: {
|
||||
irc: { nick },
|
||||
},
|
||||
},
|
||||
} = parameters;
|
||||
|
||||
this.write({
|
||||
prefix: nick,
|
||||
command: 'PART',
|
||||
parameters: [`#${roomName}`],
|
||||
});
|
||||
}
|
||||
|
||||
function sentMessage(parameters) {
|
||||
const {
|
||||
user: {
|
||||
profile: {
|
||||
irc: { nick },
|
||||
},
|
||||
},
|
||||
to,
|
||||
message,
|
||||
} = parameters;
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const lines = message.toString().split(/\r\n|\r|\n|\u0007/);
|
||||
for (const line of lines) {
|
||||
this.write({
|
||||
prefix: nick,
|
||||
command: 'PRIVMSG',
|
||||
parameters: [to],
|
||||
trailer: line,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function disconnected(parameters) {
|
||||
const {
|
||||
user: {
|
||||
profile: {
|
||||
irc: { nick },
|
||||
},
|
||||
},
|
||||
} = parameters;
|
||||
|
||||
this.write({
|
||||
prefix: nick,
|
||||
command: 'QUIT',
|
||||
});
|
||||
}
|
||||
|
||||
export default { registerUser, joinChannel, joinedChannel, leftChannel, sentMessage, disconnected };
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* This file is part of https://github.com/martynsmith/node-irc
|
||||
* by https://github.com/martynsmith
|
||||
*/
|
||||
|
||||
const replyFor = require('./codes');
|
||||
|
||||
/**
|
||||
* parseMessage(line, stripColors)
|
||||
*
|
||||
* takes a raw "line" from the IRC server and turns it into an object with
|
||||
* useful keys
|
||||
* @param {String} line Raw message from IRC server.
|
||||
* @return {Object} A parsed message object.
|
||||
*/
|
||||
module.exports = function parseMessage(line) {
|
||||
const message = {};
|
||||
let match;
|
||||
|
||||
// Parse prefix
|
||||
match = line.match(/^:([^ ]+) +/);
|
||||
if (match) {
|
||||
message.prefix = match[1];
|
||||
line = line.replace(/^:[^ ]+ +/, '');
|
||||
match = message.prefix.match(/^([_a-zA-Z0-9\~\[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/);
|
||||
if (match) {
|
||||
message.nick = match[1];
|
||||
message.user = match[3];
|
||||
message.host = match[4];
|
||||
} else {
|
||||
message.server = message.prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command
|
||||
match = line.match(/^([^ ]+) */);
|
||||
message.command = match[1];
|
||||
message.rawCommand = match[1];
|
||||
message.commandType = 'normal';
|
||||
line = line.replace(/^[^ ]+ +/, '');
|
||||
|
||||
if (replyFor[message.rawCommand]) {
|
||||
message.command = replyFor[message.rawCommand].name;
|
||||
message.commandType = replyFor[message.rawCommand].type;
|
||||
}
|
||||
|
||||
message.args = [];
|
||||
let middle;
|
||||
let trailing;
|
||||
|
||||
// Parse parameters
|
||||
if (line.search(/^:|\s+:/) !== -1) {
|
||||
match = line.match(/(.*?)(?:^:|\s+:)(.*)/);
|
||||
middle = match[1].trimRight();
|
||||
trailing = match[2];
|
||||
} else {
|
||||
middle = line;
|
||||
}
|
||||
|
||||
if (middle.length) {
|
||||
message.args = middle.split(/ +/);
|
||||
}
|
||||
|
||||
if (typeof trailing !== 'undefined' && trailing.length) {
|
||||
message.args.push(trailing);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
function PASS() {
|
||||
this.log('Received PASS command, continue registering...');
|
||||
|
||||
this.registerSteps.push('PASS');
|
||||
}
|
||||
|
||||
function SERVER(parsedMessage) {
|
||||
this.log('Received SERVER command, waiting for first PING...');
|
||||
|
||||
this.serverPrefix = parsedMessage.prefix;
|
||||
|
||||
this.registerSteps.push('SERVER');
|
||||
}
|
||||
|
||||
function PING() {
|
||||
if (!this.isRegistered && this.registerSteps.length === 2) {
|
||||
this.log('Received first PING command, server is registered!');
|
||||
|
||||
this.isRegistered = true;
|
||||
|
||||
this.emit('registered');
|
||||
}
|
||||
|
||||
this.write({
|
||||
prefix: this.config.server.name,
|
||||
command: 'PONG',
|
||||
parameters: [this.config.server.name],
|
||||
});
|
||||
}
|
||||
|
||||
function NICK(parsedMessage) {
|
||||
let command;
|
||||
|
||||
// Check if the message comes from the server,
|
||||
// which means it is a new user
|
||||
if (parsedMessage.prefix === this.serverPrefix) {
|
||||
command = {
|
||||
identifier: 'userRegistered',
|
||||
args: {
|
||||
nick: parsedMessage.args[0],
|
||||
username: parsedMessage.args[2],
|
||||
host: parsedMessage.args[3],
|
||||
name: parsedMessage.args[6],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Otherwise, it is a nick change
|
||||
command = {
|
||||
identifier: 'nickChanged',
|
||||
args: {
|
||||
nick: parsedMessage.nick,
|
||||
newNick: parsedMessage.args[0],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
function JOIN(parsedMessage) {
|
||||
const command = {
|
||||
identifier: 'joinedChannel',
|
||||
args: {
|
||||
roomName: parsedMessage.args[0].substring(1),
|
||||
nick: parsedMessage.prefix,
|
||||
},
|
||||
};
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
function PART(parsedMessage) {
|
||||
const command = {
|
||||
identifier: 'leftChannel',
|
||||
args: {
|
||||
roomName: parsedMessage.args[0].substring(1),
|
||||
nick: parsedMessage.prefix,
|
||||
},
|
||||
};
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
function PRIVMSG(parsedMessage) {
|
||||
const command = {
|
||||
identifier: 'sentMessage',
|
||||
args: {
|
||||
nick: parsedMessage.prefix,
|
||||
message: parsedMessage.args[1],
|
||||
},
|
||||
};
|
||||
|
||||
if (parsedMessage.args[0][0] === '#') {
|
||||
command.args.roomName = parsedMessage.args[0].substring(1);
|
||||
} else {
|
||||
command.args.recipientNick = parsedMessage.args[0];
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
function QUIT(parsedMessage) {
|
||||
const command = {
|
||||
identifier: 'disconnected',
|
||||
args: {
|
||||
nick: parsedMessage.prefix,
|
||||
},
|
||||
};
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
export default { PASS, SERVER, PING, NICK, JOIN, PART, PRIVMSG, QUIT };
|
||||
3
apps/meteor/server/bridges/irc/servers/index.ts
Normal file
3
apps/meteor/server/bridges/irc/servers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import RFC2813 from './RFC2813';
|
||||
|
||||
export { RFC2813 };
|
||||
Reference in New Issue
Block a user