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 };
|
||||
41
apps/meteor/server/bridges/nextcloud/addWebdavServer.ts
Normal file
41
apps/meteor/server/bridges/nextcloud/addWebdavServer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { settings } from '../../settings';
|
||||
import { addWebdavAccountByToken } from '../webdav/methods/addWebdavAccount';
|
||||
|
||||
Meteor.startup(() => {
|
||||
settings.watch('Webdav_Integration_Enabled', (value) => {
|
||||
if (value) {
|
||||
return callbacks.add(
|
||||
'afterValidateLogin',
|
||||
async (login) => {
|
||||
const { user } = login;
|
||||
const { services } = user;
|
||||
if (!services?.nextcloud) {
|
||||
return;
|
||||
}
|
||||
const token = {
|
||||
token_type: 'Bearer',
|
||||
access_token: services.nextcloud.accessToken,
|
||||
refresh_token: services.nextcloud.refreshToken,
|
||||
};
|
||||
const data = {
|
||||
name: 'Nextcloud',
|
||||
serverURL: `${services.nextcloud.serverURL}/remote.php/webdav/`,
|
||||
token,
|
||||
};
|
||||
try {
|
||||
await addWebdavAccountByToken(user._id, data);
|
||||
} catch (err) {
|
||||
SystemLogger.error({ err });
|
||||
}
|
||||
},
|
||||
callbacks.priority.MEDIUM,
|
||||
'add-webdav-server',
|
||||
);
|
||||
}
|
||||
callbacks.remove('afterValidateLogin', 'add-webdav-server');
|
||||
});
|
||||
});
|
||||
2
apps/meteor/server/bridges/nextcloud/index.ts
Normal file
2
apps/meteor/server/bridges/nextcloud/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import './lib';
|
||||
import './addWebdavServer';
|
||||
60
apps/meteor/server/bridges/nextcloud/lib.ts
Normal file
60
apps/meteor/server/bridges/nextcloud/lib.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { OAuthConfiguration } from '@rocket.chat/core-typings';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import passport from 'passport';
|
||||
|
||||
import { CustomOAuth } from '../../lib/auth-providers/custom-oauth/custom_oauth_server';
|
||||
import { addPassportCustomOAuth } from '../../lib/oauth/addPassportCustomOAuth';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const NEXTCLOUD_PATHS = {
|
||||
serverURL: '',
|
||||
tokenPath: '/index.php/apps/oauth2/api/v1/token',
|
||||
tokenSentVia: 'header' as OAuthConfiguration['tokenSentVia'],
|
||||
authorizePath: '/index.php/apps/oauth2/authorize',
|
||||
identityPath: '/ocs/v2.php/cloud/user?format=json',
|
||||
scope: 'openid',
|
||||
addAutopublishFields: {
|
||||
forLoggedInUser: ['services.nextcloud'],
|
||||
forOtherUsers: ['services.nextcloud.name'],
|
||||
},
|
||||
};
|
||||
|
||||
const Nextcloud = new CustomOAuth('nextcloud', NEXTCLOUD_PATHS);
|
||||
|
||||
function configureNextcloudOAuth(): void {
|
||||
passport.unuse('nextcloud');
|
||||
const enabled = settings.get<boolean>('Accounts_OAuth_Nextcloud');
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverURL = settings.get<string>('Accounts_OAuth_Nextcloud_URL')?.trim().replace(/\/*$/, '');
|
||||
const clientId = settings.get<string>('Accounts_OAuth_Nextcloud_id');
|
||||
const clientSecret = settings.get<string>('Accounts_OAuth_Nextcloud_secret');
|
||||
|
||||
if (!serverURL || !clientId || !clientSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { ...NEXTCLOUD_PATHS, serverURL, clientId, clientSecret };
|
||||
|
||||
if (settings.get<boolean>('Accounts_OAuth_Use_Modern_Flow')) {
|
||||
addPassportCustomOAuth('nextcloud', config);
|
||||
return;
|
||||
}
|
||||
|
||||
Nextcloud.configure(config);
|
||||
}
|
||||
|
||||
Meteor.startup(() => {
|
||||
settings.watchMultiple(
|
||||
[
|
||||
'Accounts_OAuth_Nextcloud',
|
||||
'Accounts_OAuth_Nextcloud_URL',
|
||||
'Accounts_OAuth_Nextcloud_id',
|
||||
'Accounts_OAuth_Nextcloud_secret',
|
||||
'Accounts_OAuth_Use_Modern_Flow',
|
||||
],
|
||||
configureNextcloudOAuth,
|
||||
);
|
||||
});
|
||||
40
apps/meteor/server/bridges/slack/README.md
Normal file
40
apps/meteor/server/bridges/slack/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Rocket.Chat Slack Bridge
|
||||
|
||||
This package creates a bi-directional bridge between a single Slack installation and your Rocket.Chat installation.
|
||||
|
||||
* Configure Rocket.Chat with your Slack API token
|
||||
* Invite 'rocketbot' to your Slack channel
|
||||
|
||||
### Settings
|
||||
|
||||
The following can be configured in your Rocket.Chat Administration SlackBridge panel.
|
||||
|
||||
#### Enabled
|
||||
|
||||
#### API Token
|
||||
|
||||
#### Alias format
|
||||
|
||||
#### Exclude bots
|
||||
|
||||
#### SlackBridge Out Enabled
|
||||
|
||||
#### SlackBridge Out All
|
||||
|
||||
#### SlackBridge Out Channels
|
||||
|
||||
### Features
|
||||
|
||||
#### Group Chat Messages
|
||||
* Send and receive basic messages
|
||||
* Delete messages (Can't delete slack message from rocket)
|
||||
* Edit messages (Slack doesn't allow editing of BOT messages, so can't edit a Rocket msg in Slack)
|
||||
* React to messages (as BOT in Slack)
|
||||
|
||||
#### Files
|
||||
|
||||
#### Imports
|
||||
|
||||
### TODO
|
||||
* Support multiple slack installations
|
||||
* Private IM's
|
||||
551
apps/meteor/server/bridges/slack/RocketAdapter.ts
Normal file
551
apps/meteor/server/bridges/slack/RocketAdapter.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import util from 'node:util';
|
||||
|
||||
import { Messages, Rooms, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { rocketLogger } from './logger';
|
||||
import { sleep } from '../../../lib/utils/sleep';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { sendMessage } from '../../lib/messages/sendMessage';
|
||||
import { createRoom } from '../../lib/rooms/createRoom';
|
||||
import { setUserAvatar } from '../../lib/users/setUserAvatar';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export default class RocketAdapter {
|
||||
constructor(slackBridge) {
|
||||
rocketLogger.debug('constructor');
|
||||
this.slackBridge = slackBridge;
|
||||
this.util = util;
|
||||
this.userTags = {};
|
||||
this.slackAdapters = [];
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.registerForEvents();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.unregisterForEvents();
|
||||
}
|
||||
|
||||
addSlack(slack) {
|
||||
if (this.slackAdapters.indexOf(slack) < 0) {
|
||||
this.slackAdapters.push(slack);
|
||||
}
|
||||
}
|
||||
|
||||
clearSlackAdapters() {
|
||||
this.slackAdapters = [];
|
||||
}
|
||||
|
||||
registerForEvents() {
|
||||
rocketLogger.debug('Register for events');
|
||||
callbacks.add('afterSaveMessage', this.onMessage.bind(this), callbacks.priority.LOW, 'SlackBridge_Out');
|
||||
callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), callbacks.priority.LOW, 'SlackBridge_Delete');
|
||||
callbacks.add('afterSetReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction');
|
||||
callbacks.add('afterUnsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction');
|
||||
}
|
||||
|
||||
unregisterForEvents() {
|
||||
rocketLogger.debug('Unregister for events');
|
||||
callbacks.remove('afterSaveMessage', 'SlackBridge_Out');
|
||||
callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete');
|
||||
callbacks.remove('afterSetReaction', 'SlackBridge_SetReaction');
|
||||
callbacks.remove('afterUnsetReaction', 'SlackBridge_UnSetReaction');
|
||||
}
|
||||
|
||||
async onMessageDelete(rocketMessageDeleted) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
try {
|
||||
if (!slack.getSlackChannel(rocketMessageDeleted.rid)) {
|
||||
// This is on a channel that the rocket bot is not subscribed on this slack server
|
||||
continue;
|
||||
}
|
||||
|
||||
rocketLogger.debug({ msg: 'onRocketMessageDelete', rocketMessageDeleted });
|
||||
await slack.postDeleteMessage(rocketMessageDeleted);
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onMessageDelete', err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onSetReaction(rocketMsg, { reaction }) {
|
||||
try {
|
||||
if (!this.slackBridge.isReactionsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
rocketLogger.debug('onRocketSetReaction');
|
||||
|
||||
if (rocketMsg._id && reaction) {
|
||||
if (this.slackBridge.reactionsMap.delete(`set${rocketMsg._id}${reaction}`)) {
|
||||
// This was a Slack reaction, we don't need to tell Slack about it
|
||||
return;
|
||||
}
|
||||
if (rocketMsg) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
const slackChannel = slack.getSlackChannel(rocketMsg.rid);
|
||||
if (slackChannel != null) {
|
||||
const slackTS = slack.getTimeStamp(rocketMsg);
|
||||
await slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onSetReaction', err });
|
||||
}
|
||||
}
|
||||
|
||||
async onUnSetReaction(rocketMsg, { reaction }) {
|
||||
try {
|
||||
if (!this.slackBridge.isReactionsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
rocketLogger.debug('onRocketUnSetReaction');
|
||||
|
||||
if (rocketMsg._id && reaction) {
|
||||
if (this.slackBridge.reactionsMap.delete(`unset${rocketMsg._id}${reaction}`)) {
|
||||
// This was a Slack unset reaction, we don't need to tell Slack about it
|
||||
return;
|
||||
}
|
||||
|
||||
if (rocketMsg) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
const slackChannel = slack.getSlackChannel(rocketMsg.rid);
|
||||
if (slackChannel != null) {
|
||||
const slackTS = slack.getTimeStamp(rocketMsg);
|
||||
await slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onUnSetReaction', err });
|
||||
}
|
||||
}
|
||||
|
||||
async onMessage(rocketMessage) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
try {
|
||||
if (!slack.getSlackChannel(rocketMessage.rid)) {
|
||||
// This is on a channel that the rocket bot is not subscribed
|
||||
continue;
|
||||
}
|
||||
rocketLogger.debug({ msg: 'onRocketMessage', rocketMessage });
|
||||
|
||||
if (rocketMessage.editedAt) {
|
||||
// This is an Edit Event
|
||||
await this.processMessageChanged(rocketMessage, slack);
|
||||
continue;
|
||||
}
|
||||
// Ignore messages originating from Slack
|
||||
if (rocketMessage._id.indexOf('slack-') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rocketMessage.file) {
|
||||
await this.processFileShare(rocketMessage, slack);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A new message from Rocket.Chat
|
||||
await this.processSendMessage(rocketMessage, slack);
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onMessage', err });
|
||||
}
|
||||
}
|
||||
|
||||
return rocketMessage;
|
||||
}
|
||||
|
||||
async processSendMessage(rocketMessage, slack) {
|
||||
// Since we got this message, SlackBridge_Out_Enabled is true
|
||||
if (settings.get('SlackBridge_Out_All') === true) {
|
||||
await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
|
||||
} else {
|
||||
// They want to limit to certain groups
|
||||
const outSlackChannels = _.pluck(settings.get('SlackBridge_Out_Channels'), '_id') || [];
|
||||
// rocketLogger.debug('Out SlackChannels: ', outSlackChannels);
|
||||
if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) {
|
||||
await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMessageAttachment(rocketMessage) {
|
||||
if (!rocketMessage.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rocketMessage.attachments || !rocketMessage.attachments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = rocketMessage.file._id;
|
||||
return rocketMessage.attachments.find((attachment) => attachment.title_link && attachment.title_link.indexOf(`/${fileId}/`) >= 0);
|
||||
}
|
||||
|
||||
async processFileShare(rocketMessage, slack) {
|
||||
if (!settings.get('SlackBridge_FileUpload_Enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rocketMessage.file.name) {
|
||||
let fileName = rocketMessage.file.name;
|
||||
let text = rocketMessage.msg;
|
||||
|
||||
const attachment = this.getMessageAttachment(rocketMessage);
|
||||
if (attachment) {
|
||||
fileName = Meteor.absoluteUrl(attachment.title_link);
|
||||
if (!text) {
|
||||
text = attachment.description;
|
||||
}
|
||||
}
|
||||
|
||||
await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg: `${text} ${fileName}` });
|
||||
}
|
||||
}
|
||||
|
||||
async processMessageChanged(rocketMessage, slack) {
|
||||
if (rocketMessage) {
|
||||
if (rocketMessage.updatedBySlack) {
|
||||
// We have already processed this
|
||||
delete rocketMessage.updatedBySlack;
|
||||
return;
|
||||
}
|
||||
|
||||
// This was a change from Rocket.Chat
|
||||
const slackChannel = slack.getSlackChannel(rocketMessage.rid);
|
||||
await slack.postMessageUpdate(slackChannel, rocketMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async getChannel(slackMessage) {
|
||||
return slackMessage.channel ? (await this.findChannel(slackMessage.channel)) || this.addChannel(slackMessage.channel) : null;
|
||||
}
|
||||
|
||||
async getUser(slackUser) {
|
||||
return slackUser ? (await this.findUser(slackUser)) || this.addUser(slackUser) : null;
|
||||
}
|
||||
|
||||
createRocketID(slackChannel, ts) {
|
||||
return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`;
|
||||
}
|
||||
|
||||
async findChannel(slackChannelId) {
|
||||
return Rooms.findOneByImportId(slackChannelId);
|
||||
}
|
||||
|
||||
async getRocketUsers(members, slackChannel) {
|
||||
const rocketUsers = [];
|
||||
for await (const member of members) {
|
||||
if (member !== slackChannel.creator) {
|
||||
const rocketUser = (await this.findUser(member)) || (await this.addUser(member));
|
||||
if (rocketUser && rocketUser.username) {
|
||||
rocketUsers.push(rocketUser.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rocketUsers;
|
||||
}
|
||||
|
||||
async getRocketUserCreator(slackChannel) {
|
||||
return slackChannel.creator ? (await this.findUser(slackChannel.creator)) || this.addUser(slackChannel.creator) : null;
|
||||
}
|
||||
|
||||
async addChannel(slackChannelID, hasRetried = false) {
|
||||
rocketLogger.debug({ msg: 'Adding Rocket.Chat channel from Slack', slackChannelID });
|
||||
let addedRoom;
|
||||
|
||||
for await (const slack of this.slackAdapters) {
|
||||
if (addedRoom) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const slackChannel = await slack.slackAPI.getRoomInfo(slackChannelID);
|
||||
if (slackChannel) {
|
||||
const members = await slack.slackAPI.getMembers(slackChannelID);
|
||||
if (!members) {
|
||||
rocketLogger.error('Could not fetch room members');
|
||||
continue;
|
||||
}
|
||||
|
||||
const rocketRoom = await Rooms.findOneByName(slackChannel.name);
|
||||
|
||||
if (rocketRoom || slackChannel.is_general) {
|
||||
slackChannel.rocketId = slackChannel.is_general ? 'GENERAL' : rocketRoom._id;
|
||||
await Rooms.addImportIds(slackChannel.rocketId, slackChannel.id);
|
||||
} else {
|
||||
const rocketUsers = await this.getRocketUsers(members, slackChannel);
|
||||
const rocketUserCreator = await this.getRocketUserCreator(slackChannel);
|
||||
|
||||
if (!rocketUserCreator) {
|
||||
rocketLogger.error({ msg: 'Could not fetch room creator information', creator: slackChannel.creator });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const isPrivate = slackChannel.is_private;
|
||||
const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator, rocketUsers);
|
||||
slackChannel.rocketId = rocketChannel.rid;
|
||||
} catch (e) {
|
||||
if (!hasRetried) {
|
||||
rocketLogger.debug({ msg: 'Error adding channel from Slack. Will retry in 1s.', err: e });
|
||||
// If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s.
|
||||
await sleep(1000);
|
||||
return (await this.findChannel(slackChannelID)) || this.addChannel(slackChannelID, true);
|
||||
}
|
||||
rocketLogger.error({ msg: 'Error adding channel from Slack', err: e });
|
||||
}
|
||||
|
||||
const roomUpdate = {
|
||||
ts: new Date(slackChannel.created * 1000),
|
||||
};
|
||||
|
||||
let lastSetTopic = 0;
|
||||
if (slackChannel.topic && slackChannel.topic.value) {
|
||||
roomUpdate.topic = slackChannel.topic.value;
|
||||
lastSetTopic = slackChannel.topic.last_set;
|
||||
}
|
||||
|
||||
if (slackChannel.purpose && slackChannel.purpose.value && slackChannel.purpose.last_set > lastSetTopic) {
|
||||
roomUpdate.topic = slackChannel.purpose.value;
|
||||
}
|
||||
await Rooms.addImportIds(slackChannel.rocketId, slackChannel.id);
|
||||
slack.addSlackChannel(slackChannel.rocketId, slackChannelID);
|
||||
}
|
||||
|
||||
addedRoom = await Rooms.findOneById(slackChannel.rocketId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedRoom) {
|
||||
rocketLogger.debug('Channel not added');
|
||||
}
|
||||
return addedRoom;
|
||||
}
|
||||
|
||||
async findUser(slackUserID) {
|
||||
const rocketUser = await Users.findOneByImportId(slackUserID);
|
||||
if (rocketUser && !this.userTags[slackUserID]) {
|
||||
this.userTags[slackUserID] = {
|
||||
slack: `<@${slackUserID}>`,
|
||||
rocket: `@${rocketUser.username}`,
|
||||
};
|
||||
}
|
||||
return rocketUser;
|
||||
}
|
||||
|
||||
async addUser(slackUserID) {
|
||||
rocketLogger.debug({ msg: 'Adding Rocket.Chat user from Slack', slackUserID });
|
||||
let addedUser;
|
||||
for await (const slack of this.slackAdapters) {
|
||||
if (addedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await slack.slackAPI.getUser(slackUserID);
|
||||
if (user) {
|
||||
const rocketUserData = user;
|
||||
const isBot = rocketUserData.is_bot === true;
|
||||
const email = (rocketUserData.profile && rocketUserData.profile.email) || '';
|
||||
let existingRocketUser;
|
||||
if (!isBot) {
|
||||
existingRocketUser =
|
||||
(await Users.findOneByEmailAddress(email)) || (await Users.findOneByUsernameIgnoringCase(rocketUserData.name));
|
||||
} else {
|
||||
existingRocketUser = await Users.findOneByUsernameIgnoringCase(rocketUserData.name);
|
||||
}
|
||||
|
||||
if (existingRocketUser) {
|
||||
rocketUserData.rocketId = existingRocketUser._id;
|
||||
rocketUserData.name = existingRocketUser.username;
|
||||
} else {
|
||||
const newUser = {
|
||||
password: Random.id(),
|
||||
username: rocketUserData.name,
|
||||
};
|
||||
|
||||
if (!isBot && email) {
|
||||
newUser.email = email;
|
||||
}
|
||||
|
||||
if (isBot) {
|
||||
newUser.joinDefaultChannels = false;
|
||||
}
|
||||
|
||||
rocketUserData.rocketId = await Accounts.createUserAsync(newUser);
|
||||
const userUpdate = {
|
||||
utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600,
|
||||
roles: isBot ? ['bot'] : ['user'],
|
||||
};
|
||||
|
||||
if (rocketUserData.profile && rocketUserData.profile.real_name) {
|
||||
userUpdate.name = rocketUserData.profile.real_name;
|
||||
}
|
||||
|
||||
if (rocketUserData.deleted) {
|
||||
userUpdate.active = false;
|
||||
userUpdate['services.resume.loginTokens'] = [];
|
||||
}
|
||||
|
||||
await Users.updateOne({ _id: rocketUserData.rocketId }, { $set: userUpdate });
|
||||
|
||||
const user = await Users.findOneById(rocketUserData.rocketId);
|
||||
|
||||
let url = null;
|
||||
if (rocketUserData.profile) {
|
||||
if (rocketUserData.profile.image_original) {
|
||||
url = rocketUserData.profile.image_original;
|
||||
} else if (rocketUserData.profile.image_512) {
|
||||
url = rocketUserData.profile.image_512;
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
try {
|
||||
await setUserAvatar(user, url, null, 'url');
|
||||
} catch (err) {
|
||||
rocketLogger.debug({ msg: 'Error setting user avatar from Slack', err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importIds = [rocketUserData.id];
|
||||
if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) {
|
||||
importIds.push(rocketUserData.profile.bot_id);
|
||||
}
|
||||
await Users.addImportIds(rocketUserData.rocketId, importIds);
|
||||
if (!this.userTags[slackUserID]) {
|
||||
this.userTags[slackUserID] = {
|
||||
slack: `<@${slackUserID}>`,
|
||||
rocket: `@${rocketUserData.name}`,
|
||||
};
|
||||
}
|
||||
addedUser = await Users.findOneById(rocketUserData.rocketId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedUser) {
|
||||
rocketLogger.debug('User not added');
|
||||
}
|
||||
|
||||
return addedUser;
|
||||
}
|
||||
|
||||
addAliasToMsg(rocketUserName, rocketMsgObj) {
|
||||
const aliasFormat = settings.get('SlackBridge_AliasFormat');
|
||||
if (aliasFormat) {
|
||||
const alias = this.util.format(aliasFormat, rocketUserName);
|
||||
|
||||
if (alias !== rocketUserName) {
|
||||
rocketMsgObj.alias = alias;
|
||||
}
|
||||
}
|
||||
|
||||
return rocketMsgObj;
|
||||
}
|
||||
|
||||
async createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting, slack) {
|
||||
if (slackMessage.type === 'message') {
|
||||
let rocketMsgObj = {};
|
||||
if (!_.isEmpty(slackMessage.subtype)) {
|
||||
rocketMsgObj = await slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting);
|
||||
if (!rocketMsgObj) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
rocketMsgObj = {
|
||||
msg: await this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text),
|
||||
rid: rocketChannel._id,
|
||||
u: {
|
||||
_id: rocketUser._id,
|
||||
username: rocketUser.username,
|
||||
},
|
||||
};
|
||||
|
||||
this.addAliasToMsg(rocketUser.username, rocketMsgObj);
|
||||
}
|
||||
_.extend(rocketMsgObj, rocketMsgDataDefaults);
|
||||
if (slackMessage.edited) {
|
||||
rocketMsgObj.editedAt = new Date(parseInt(slackMessage.edited.ts.split('.')[0]) * 1000);
|
||||
}
|
||||
rocketMsgObj.slackTs = slackMessage.ts;
|
||||
if (slackMessage.thread_ts) {
|
||||
const tmessage = await Messages.findOneBySlackTs(slackMessage.thread_ts);
|
||||
if (tmessage) {
|
||||
rocketMsgObj.tmid = tmessage._id;
|
||||
}
|
||||
}
|
||||
|
||||
if (slackMessage.subtype === 'bot_message') {
|
||||
rocketUser = await Users.findOneById('rocket.cat', { projection: { username: 1 } });
|
||||
}
|
||||
|
||||
if (slackMessage.pinned_to && slackMessage.pinned_to.indexOf(slackMessage.channel) !== -1) {
|
||||
rocketMsgObj.pinned = true;
|
||||
rocketMsgObj.pinnedAt = Date.now;
|
||||
rocketMsgObj.pinnedBy = _.pick(rocketUser, '_id', 'username');
|
||||
}
|
||||
if (slackMessage.subtype === 'bot_message') {
|
||||
setTimeout(async () => {
|
||||
if (slackMessage.bot_id && slackMessage.ts) {
|
||||
// Make sure that a message with the same bot_id and timestamp doesn't already exists
|
||||
const msg = await Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts);
|
||||
if (!msg) {
|
||||
void sendMessage(rocketUser, rocketMsgObj, rocketChannel, { upsert: true });
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
rocketLogger.debug('Send message to Rocket.Chat');
|
||||
await sendMessage(rocketUser, rocketMsgObj, rocketChannel, { upsert: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) {
|
||||
const regex = /(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g;
|
||||
if (!_.isEmpty(slackMsgTxt)) {
|
||||
slackMsgTxt = slackMsgTxt.replace(/<!everyone>/g, '@all');
|
||||
slackMsgTxt = slackMsgTxt.replace(/<!channel>/g, '@all');
|
||||
slackMsgTxt = slackMsgTxt.replace(/<!here>/g, '@here');
|
||||
slackMsgTxt = slackMsgTxt.replace(/>/g, '>');
|
||||
slackMsgTxt = slackMsgTxt.replace(/</g, '<');
|
||||
slackMsgTxt = slackMsgTxt.replace(/&/g, '&');
|
||||
slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:');
|
||||
slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:');
|
||||
slackMsgTxt = slackMsgTxt.replace(/:piggy:/g, ':pig:');
|
||||
slackMsgTxt = slackMsgTxt.replace(/:uk:/g, ':gb:');
|
||||
slackMsgTxt = slackMsgTxt.replace(/<(http[s]?:[^>]*)>/g, '$1');
|
||||
|
||||
const promises = [];
|
||||
|
||||
slackMsgTxt.replace(regex, async (match, userId) => {
|
||||
if (!this.userTags[userId]) {
|
||||
(await this.findUser(userId)) || (await this.addUser(userId)); // This adds userTags for the userId
|
||||
}
|
||||
const userTags = this.userTags[userId];
|
||||
if (userTags) {
|
||||
promises.push(slackMsgTxt.replace(userTags.slack, userTags.rocket));
|
||||
}
|
||||
});
|
||||
|
||||
const result = await Promise.all(promises);
|
||||
slackMsgTxt = slackMsgTxt.replace(regex, () => result.shift());
|
||||
} else {
|
||||
slackMsgTxt = '';
|
||||
}
|
||||
return slackMsgTxt;
|
||||
}
|
||||
}
|
||||
253
apps/meteor/server/bridges/slack/SlackAPI.ts
Normal file
253
apps/meteor/server/bridges/slack/SlackAPI.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
|
||||
|
||||
export class SlackAPI {
|
||||
constructor(apiOrBotToken) {
|
||||
this.token = apiOrBotToken;
|
||||
}
|
||||
|
||||
async getChannels(cursor = null) {
|
||||
let channels = [];
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/conversations.list', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
types: 'public_channel',
|
||||
exclude_archived: true,
|
||||
limit: 1000,
|
||||
...(cursor && { cursor }),
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
|
||||
if (response && response && Array.isArray(response.channels) && response.channels.length > 0) {
|
||||
channels = channels.concat(response.channels);
|
||||
if (response.response_metadata?.next_cursor) {
|
||||
const nextChannels = await this.getChannels(response.response_metadata.next_cursor);
|
||||
channels = channels.concat(nextChannels);
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
async getGroups(cursor = null) {
|
||||
let groups = [];
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/conversations.list', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
types: 'private_channel',
|
||||
exclude_archived: true,
|
||||
limit: 1000,
|
||||
...(cursor && { cursor }),
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
|
||||
if (response && response && Array.isArray(response.channels) && response.channels.length > 0) {
|
||||
groups = groups.concat(response.channels);
|
||||
if (response.response_metadata?.next_cursor) {
|
||||
const nextGroups = await this.getGroups(response.response_metadata.next_cursor);
|
||||
groups = groups.concat(nextGroups);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
async getRoomInfo(roomId) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch(`https://slack.com/api/conversations.info`, {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
channel: roomId,
|
||||
include_num_members: true,
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.channel;
|
||||
}
|
||||
|
||||
async getMembers(channelId) {
|
||||
const { num_members } = this.getRoomInfo(channelId);
|
||||
const MAX_MEMBERS_PER_CALL = 100;
|
||||
let members = [];
|
||||
let currentCursor = '';
|
||||
for (let index = 0; index < num_members; index += MAX_MEMBERS_PER_CALL) {
|
||||
const request = await fetch('https://slack.com/api/conversations.members', {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
channel: channelId,
|
||||
limit: MAX_MEMBERS_PER_CALL,
|
||||
...(currentCursor && { cursor: currentCursor }),
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
if (response && response && request.status === 200 && request.ok && Array.isArray(response.members)) {
|
||||
members = members.concat(response.members);
|
||||
const hasMoreItems = response.response_metadata?.next_cursor;
|
||||
if (hasMoreItems) {
|
||||
currentCursor = response.response_metadata.next_cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
async react(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/reactions.add', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async removeReaction(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/reactions.remove', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async removeMessage(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/chat.delete', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async sendMessage(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
return request.json();
|
||||
}
|
||||
|
||||
async updateMessage(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/chat.update', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async getHistory(options) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch(`https://slack.com/api/conversations.history`, {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: options,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response;
|
||||
}
|
||||
|
||||
async getPins(channelId) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/pins.list', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
channel: channelId,
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.items;
|
||||
}
|
||||
|
||||
async getUser(userId) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/users.info', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
user: userId,
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.user;
|
||||
}
|
||||
|
||||
static async verifyToken(token) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/auth.test', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.ok;
|
||||
}
|
||||
|
||||
static async verifyAppCredentials({ botToken, appToken }) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/apps.connections.open', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const response = await request.json();
|
||||
const isAppTokenOk = response && response && request.status === 200 && request.ok && response.ok;
|
||||
const isBotTokenOk = await this.verifyToken(botToken);
|
||||
return isAppTokenOk && isBotTokenOk;
|
||||
}
|
||||
}
|
||||
1453
apps/meteor/server/bridges/slack/SlackAdapter.ts
Normal file
1453
apps/meteor/server/bridges/slack/SlackAdapter.ts
Normal file
File diff suppressed because it is too large
Load Diff
3
apps/meteor/server/bridges/slack/index.ts
Normal file
3
apps/meteor/server/bridges/slack/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import './slackbridge';
|
||||
import './slackbridge_import.server';
|
||||
import './removeChannelLinks';
|
||||
8
apps/meteor/server/bridges/slack/logger.ts
Normal file
8
apps/meteor/server/bridges/slack/logger.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
|
||||
const logger = new Logger('SlackBridge');
|
||||
|
||||
export const connLogger = logger.section('Connection');
|
||||
export const classLogger = logger.section('Class');
|
||||
export const slackLogger = logger.section('Slack');
|
||||
export const rocketLogger = logger.section('Rocket');
|
||||
41
apps/meteor/server/bridges/slack/removeChannelLinks.ts
Normal file
41
apps/meteor/server/bridges/slack/removeChannelLinks.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
removeSlackBridgeChannelLinks(): { message: string; params: unknown[] };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async removeSlackBridgeChannelLinks() {
|
||||
const user = await Meteor.userAsync();
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'removeSlackBridgeChannelLinks',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(user, 'remove-slackbridge-links'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'removeSlackBridgeChannelLinks',
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.get('SlackBridge_Enabled') !== true) {
|
||||
throw new Meteor.Error('SlackBridge_disabled');
|
||||
}
|
||||
|
||||
await Rooms.unsetAllImportIds();
|
||||
|
||||
return {
|
||||
message: 'Slackbridge_channel_links_removed_successfully',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
199
apps/meteor/server/bridges/slack/slackbridge.ts
Normal file
199
apps/meteor/server/bridges/slack/slackbridge.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import RocketAdapter from './RocketAdapter';
|
||||
import SlackAdapter from './SlackAdapter';
|
||||
import { classLogger, connLogger } from './logger';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
/**
|
||||
* SlackBridge interfaces between this Rocket installation and a remote Slack installation.
|
||||
*/
|
||||
class SlackBridgeClass {
|
||||
constructor() {
|
||||
this.isEnabled = false;
|
||||
this.isLegacyRTM = true;
|
||||
this.slackAdapters = [];
|
||||
this.rocket = new RocketAdapter(this);
|
||||
this.reactionsMap = new Map(); // Sync object between rocket and slack
|
||||
|
||||
this.connected = false;
|
||||
this.rocket.clearSlackAdapters();
|
||||
|
||||
// Settings that we cache versus looking up at runtime
|
||||
this.apiTokens = false;
|
||||
this.botTokens = false;
|
||||
this.appTokens = false;
|
||||
this.signingSecrets = false;
|
||||
this.aliasFormat = '';
|
||||
this.excludeBotnames = '';
|
||||
this.isReactionsEnabled = true;
|
||||
|
||||
this.processSettings();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.connected === false) {
|
||||
this.slackAdapters = [];
|
||||
this.rocket.clearSlackAdapters();
|
||||
|
||||
if (this.isLegacyRTM) {
|
||||
const tokenList = this.apiTokens.split('\n');
|
||||
|
||||
tokenList.forEach((apiToken) => {
|
||||
const slack = new SlackAdapter(this);
|
||||
slack.setRocket(this.rocket);
|
||||
this.rocket.addSlack(slack);
|
||||
this.slackAdapters.push(slack);
|
||||
|
||||
slack.connect({ apiToken }).catch((err) => connLogger.error({ msg: 'error connecting to slack', err }));
|
||||
});
|
||||
} else {
|
||||
const botTokenList = this.botTokens.split('\n'); // Bot token list
|
||||
const appTokenList = this.appTokens.split('\n'); // App token list
|
||||
const signingSecretList = this.signingSecrets.split('\n'); // Signing secret list
|
||||
|
||||
// Check if the number of tokens are the same
|
||||
if (botTokenList.length !== appTokenList.length || botTokenList.length !== signingSecretList.length) {
|
||||
connLogger.error('error connecting to slack: number of tokens are not the same');
|
||||
return;
|
||||
}
|
||||
|
||||
const appCredentials = botTokenList.map((botToken, i) => ({
|
||||
botToken,
|
||||
appToken: appTokenList[i],
|
||||
signingSecret: signingSecretList[i],
|
||||
}));
|
||||
|
||||
appCredentials.forEach((appCredential) => {
|
||||
const slack = new SlackAdapter(this);
|
||||
slack.setRocket(this.rocket);
|
||||
this.rocket.addSlack(slack);
|
||||
this.slackAdapters.push(slack);
|
||||
|
||||
slack.connect({ appCredential }).catch((err) => connLogger.error({ msg: 'error connecting to slack', err }));
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.get('SlackBridge_Out_Enabled')) {
|
||||
this.rocket.connect();
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
connLogger.info('Enabled');
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
await this.disconnect();
|
||||
// connect if either apiTokens or appCredentials are set
|
||||
if (this.isLegacyRTM && this.apiTokens) {
|
||||
this.connect();
|
||||
} else if (!this.isLegacyRTM && this.botTokens && this.appTokens && this.signingSecrets) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
debouncedReconnectIfEnabled = debounce(() => {
|
||||
if (this.isEnabled) {
|
||||
this.reconnect();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
async disconnect() {
|
||||
try {
|
||||
if (this.connected === true) {
|
||||
await this.rocket.disconnect();
|
||||
await Promise.all(this.slackAdapters.map((slack) => slack.disconnect()));
|
||||
this.slackAdapters = [];
|
||||
this.connected = false;
|
||||
connLogger.info('Slack Bridge Disconnected');
|
||||
}
|
||||
} catch (error) {
|
||||
connLogger.error({ msg: 'An error occurred during disconnection', err: error });
|
||||
}
|
||||
}
|
||||
|
||||
processSettings() {
|
||||
// Check if legacy realtime api is enabled
|
||||
settings.watch('SlackBridge_UseLegacy', (value) => {
|
||||
if (value !== this.isLegacyRTM) {
|
||||
this.isLegacyRTM = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_UseLegacy', value });
|
||||
});
|
||||
|
||||
// Slack installtion Bot token
|
||||
settings.watch('SlackBridge_BotToken', (value) => {
|
||||
if (value !== this.botTokens) {
|
||||
this.botTokens = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_BotToken', value });
|
||||
});
|
||||
// Slack installtion App token
|
||||
settings.watch('SlackBridge_AppToken', (value) => {
|
||||
if (value !== this.appTokens) {
|
||||
this.appTokens = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_AppToken', value });
|
||||
});
|
||||
// Slack installtion Signing token
|
||||
settings.watch('SlackBridge_SigningSecret', (value) => {
|
||||
if (value !== this.signingSecrets) {
|
||||
this.signingSecrets = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_SigningSecret', value });
|
||||
});
|
||||
|
||||
// Slack installation API token
|
||||
settings.watch('SlackBridge_APIToken', (value) => {
|
||||
if (value !== this.apiTokens) {
|
||||
this.apiTokens = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_APIToken', value });
|
||||
});
|
||||
|
||||
// Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used.
|
||||
settings.watch('SlackBridge_AliasFormat', (value) => {
|
||||
this.aliasFormat = value;
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_AliasFormat', value });
|
||||
});
|
||||
|
||||
// Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.
|
||||
settings.watch('SlackBridge_ExcludeBotnames', (value) => {
|
||||
this.excludeBotnames = value;
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_ExcludeBotnames', value });
|
||||
});
|
||||
|
||||
// Reactions
|
||||
settings.watch('SlackBridge_Reactions_Enabled', (value) => {
|
||||
this.isReactionsEnabled = value;
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_Reactions_Enabled', value });
|
||||
});
|
||||
|
||||
// Is this entire SlackBridge enabled
|
||||
settings.watch('SlackBridge_Enabled', (value) => {
|
||||
if (this.isEnabled !== value) {
|
||||
this.isEnabled = value;
|
||||
if (this.isEnabled) {
|
||||
this.debouncedReconnectIfEnabled();
|
||||
} else {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_Enabled', value });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const SlackBridge = new SlackBridgeClass();
|
||||
@@ -0,0 +1,84 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { Match } from 'meteor/check';
|
||||
|
||||
import { SlackBridge } from './slackbridge';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { msgStream } from '../../lib/messaging/msgStream';
|
||||
import { slashCommands } from '../../lib/utils/slashCommand';
|
||||
|
||||
async function SlackBridgeImport({ command, params, message, userId }) {
|
||||
if (command !== 'slackbridge-import' || !Match.test(params, String)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(message.rid);
|
||||
const channel = room.name;
|
||||
const user = await Users.findOneById(userId);
|
||||
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_start', {
|
||||
username: user.username,
|
||||
channelName: channel,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const slack of SlackBridge.slackAdapters) {
|
||||
await slack.importMessages(message.rid, (error) => {
|
||||
if (error) {
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_error', {
|
||||
channelName: channel,
|
||||
errorMessage: error.message,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_finish', {
|
||||
channelName: channel,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_error', {
|
||||
channelName: channel,
|
||||
errorMessage: error.message,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
slashCommands.add({ command: 'slackbridge-import', callback: SlackBridgeImport });
|
||||
141
apps/meteor/server/bridges/smarsh/functions/generateEml.ts
Normal file
141
apps/meteor/server/bridges/smarsh/functions/generateEml.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { MessageTypes } from '@rocket.chat/message-types';
|
||||
import { Messages, SmarshHistory, Users, Rooms } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { sendEmail } from './sendEmail';
|
||||
import { i18n } from '../../../lib/i18n';
|
||||
import { settings } from '../../../settings';
|
||||
|
||||
const start =
|
||||
'<table style="width: 100%; border: 1px solid; border-collapse: collapse; table-layout: fixed; margin-top: 10px; font-size: 12px; word-break: break-word;"><tbody>';
|
||||
const end = '</tbody></table>';
|
||||
const opentr = '<tr style="border: 1px solid;">';
|
||||
const closetr = '</tr>';
|
||||
const open20td = '<td style="border: 1px solid; text-align: center; width: 20%;">';
|
||||
const open60td = '<td style="border: 1px solid; text-align: left; width: 60%; padding: 0 5px;">';
|
||||
const closetd = '</td>';
|
||||
|
||||
function _getLink(attachment: { title_link: string }): string {
|
||||
const url = attachment.title_link.replace(/ /g, '%20');
|
||||
|
||||
if (url.match(/^(https?:)?\/\//i)) {
|
||||
return url;
|
||||
}
|
||||
return Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + url;
|
||||
}
|
||||
|
||||
export const generateEml = async (): Promise<void> => {
|
||||
setImmediate(async () => {
|
||||
const smarshMissingEmail = settings.get('Smarsh_MissingEmail_Email');
|
||||
const timeZone = settings.get<string>('Smarsh_Timezone');
|
||||
|
||||
// TODO: revisit with more time => This appears to be a super expensive operation, going through all rooms
|
||||
for await (const room of Rooms.find()) {
|
||||
const smarshHistory = await SmarshHistory.findOne({ _id: room._id });
|
||||
const query: Record<string, any> = { rid: room._id };
|
||||
|
||||
if (smarshHistory) {
|
||||
query.ts = { $gt: smarshHistory.lastRan };
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const rows: string[] = [];
|
||||
const data: {
|
||||
users: string[];
|
||||
msgs: number;
|
||||
files: string[];
|
||||
time: number;
|
||||
room: string;
|
||||
} = {
|
||||
users: [],
|
||||
msgs: 0,
|
||||
files: [],
|
||||
time: smarshHistory ? moment(date).diff(moment(smarshHistory.lastRan), 'minutes') : moment(date).diff(moment(room.ts), 'minutes'),
|
||||
room: room.name ? `#${room.name}` : `Direct Message Between: ${room?.usernames?.join(' & ')}`,
|
||||
};
|
||||
|
||||
const cursor = Messages.find(query);
|
||||
|
||||
for await (const message of cursor) {
|
||||
rows.push(opentr);
|
||||
|
||||
// The timestamp
|
||||
rows.push(open20td);
|
||||
rows.push(moment(message.ts).tz(timeZone).format('YYYY-MM-DD HH-mm-ss z'));
|
||||
rows.push(closetd);
|
||||
|
||||
// The sender
|
||||
rows.push(open20td);
|
||||
const sender = await Users.findOne({ _id: message.u._id });
|
||||
if (!sender) {
|
||||
return;
|
||||
}
|
||||
if (data.users.indexOf(sender?._id) === -1) {
|
||||
data.users.push(sender._id);
|
||||
}
|
||||
|
||||
// Get the user's email, can be nothing if it is an unconfigured bot account (like rocket.cat)
|
||||
if (sender.emails?.[0]?.address) {
|
||||
rows.push(`${sender.name} <${sender.emails[0].address}>`);
|
||||
} else {
|
||||
rows.push(`${sender.name} <${smarshMissingEmail}>`);
|
||||
}
|
||||
rows.push(closetd);
|
||||
|
||||
// The message
|
||||
rows.push(open60td);
|
||||
data.msgs++;
|
||||
if (message.t) {
|
||||
const messageType = MessageTypes.getType(message);
|
||||
if (messageType) {
|
||||
rows.push(messageType.text(i18n.getFixedT('en'), message));
|
||||
} else {
|
||||
rows.push(`${message.msg} (${message.t})`);
|
||||
}
|
||||
} else if (message.file) {
|
||||
data.files.push(message.file._id);
|
||||
rows.push(`${message?.attachments?.[0].title} (${_getLink({ title_link: message?.attachments?.[0].title_link || '' })})})`);
|
||||
} else if (message.attachments) {
|
||||
const attaches: string[] = [];
|
||||
message.attachments.forEach((a) => {
|
||||
if ('image_url' in a && a.image_url !== undefined) {
|
||||
attaches.push(a.image_url);
|
||||
}
|
||||
// TODO: Verify other type of attachments which need to be handled that aren't file uploads and image urls
|
||||
// } else {
|
||||
// console.log(a);
|
||||
// }
|
||||
});
|
||||
|
||||
rows.push(`${message.msg} (${attaches.join(', ')})`);
|
||||
} else {
|
||||
rows.push(message.msg);
|
||||
}
|
||||
rows.push(closetd);
|
||||
|
||||
rows.push(closetr);
|
||||
}
|
||||
|
||||
if (rows.length !== 0) {
|
||||
const result = start + rows.join('') + end;
|
||||
|
||||
await SmarshHistory.updateOne(
|
||||
{ _id: room._id },
|
||||
{
|
||||
_id: room._id,
|
||||
lastRan: date,
|
||||
lastResult: result,
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
|
||||
await sendEmail({
|
||||
body: result,
|
||||
subject: `Rocket.Chat, ${data.users.length} Users, ${data.msgs} Messages, ${data.files.length} Files, ${data.time} Minutes, in ${data.room}`,
|
||||
files: data.files,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
33
apps/meteor/server/bridges/smarsh/functions/sendEmail.ts
Normal file
33
apps/meteor/server/bridges/smarsh/functions/sendEmail.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Expects the following details:
|
||||
// {
|
||||
// body: '<table>',
|
||||
// subject: 'Rocket.Chat, 17 Users, 24 Messages, 1 File, 799504 Minutes, in #random',
|
||||
// files: ['i3nc9l3mn']
|
||||
// }
|
||||
import { Uploads } from '@rocket.chat/models';
|
||||
|
||||
import * as Mailer from '../../../lib/notifications/email/api';
|
||||
import { settings } from '../../../settings';
|
||||
import { UploadFS } from '../../../ufs';
|
||||
|
||||
export const sendEmail = async (data: { files: string[]; subject: string; body: string }) => {
|
||||
const attachments = [];
|
||||
|
||||
for await (const fileId of data.files) {
|
||||
const file = await Uploads.findOneById(fileId);
|
||||
if (file?.store === 'rocketchat_uploads' || file?.store === 'fileSystem') {
|
||||
const rs = await UploadFS.getStore(file.store).getReadStream(fileId, file);
|
||||
attachments.push({
|
||||
filename: file.name,
|
||||
streamSource: rs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await Mailer.sendNoWrap({
|
||||
to: settings.get('Smarsh_Email'),
|
||||
from: settings.get('From_Email'),
|
||||
subject: data.subject,
|
||||
html: data.body,
|
||||
});
|
||||
};
|
||||
3
apps/meteor/server/bridges/smarsh/index.ts
Normal file
3
apps/meteor/server/bridges/smarsh/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import './functions/sendEmail';
|
||||
import './functions/generateEml';
|
||||
import './startup';
|
||||
21
apps/meteor/server/bridges/smarsh/startup.ts
Normal file
21
apps/meteor/server/bridges/smarsh/startup.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cronJobs } from '@rocket.chat/cron';
|
||||
|
||||
import { generateEml } from './functions/generateEml';
|
||||
import { settings } from '../../settings';
|
||||
import { smarshIntervalValuesToCronMap } from '../../settings/smarsh';
|
||||
|
||||
const smarshJobName = 'Smarsh EML Connector';
|
||||
|
||||
settings.watchMultiple(
|
||||
['Smarsh_Enabled', 'Smarsh_Email', 'From_Email', 'Smarsh_Interval'],
|
||||
async function __addSmarshSyncedCronJobDebounced() {
|
||||
if (await cronJobs.has(smarshJobName)) {
|
||||
await cronJobs.remove(smarshJobName);
|
||||
}
|
||||
|
||||
if (settings.get('Smarsh_Enabled') && settings.get('Smarsh_Email') !== '' && settings.get('From_Email') !== '') {
|
||||
const cronInterval = smarshIntervalValuesToCronMap[settings.get<string>('Smarsh_Interval')];
|
||||
await cronJobs.add(smarshJobName, cronInterval, async () => generateEml());
|
||||
}
|
||||
},
|
||||
);
|
||||
3
apps/meteor/server/bridges/webdav/README.md
Normal file
3
apps/meteor/server/bridges/webdav/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# RocketChat WebDAV
|
||||
|
||||
Package for RocketChat users to interact with WebDAV servers (Tested with ownCloud and Nextcloud).
|
||||
5
apps/meteor/server/bridges/webdav/index.ts
Normal file
5
apps/meteor/server/bridges/webdav/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import './methods/addWebdavAccount';
|
||||
import './methods/getWebdavFileList';
|
||||
import './methods/getWebdavFilePreview';
|
||||
import './methods/getFileFromWebdav';
|
||||
import './methods/uploadFileToWebdav';
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { WebDAVClientOptions } from 'webdav';
|
||||
|
||||
export function getWebdavCredentials(account: WebDAVClientOptions): WebDAVClientOptions {
|
||||
const cred = account.token
|
||||
? { token: account.token }
|
||||
: {
|
||||
username: account.username,
|
||||
password: account.password,
|
||||
};
|
||||
return cred;
|
||||
}
|
||||
21
apps/meteor/server/bridges/webdav/lib/uploadFileToWebdav.ts
Normal file
21
apps/meteor/server/bridges/webdav/lib/uploadFileToWebdav.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { IWebdavAccount } from '@rocket.chat/core-typings';
|
||||
import { WebdavAccounts } from '@rocket.chat/models';
|
||||
|
||||
import { getWebdavCredentials } from './getWebdavCredentials';
|
||||
import { WebdavClientAdapter } from './webdavClientAdapter';
|
||||
|
||||
export const uploadFileToWebdav = async (accountId: IWebdavAccount['_id'], fileData: string | Buffer, name: string): Promise<void> => {
|
||||
const account = await WebdavAccounts.findOneById(accountId);
|
||||
if (!account) {
|
||||
throw new Error('error-invalid-account');
|
||||
}
|
||||
|
||||
const uploadFolder = 'Rocket.Chat Uploads/';
|
||||
const buffer = Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData);
|
||||
|
||||
const cred = getWebdavCredentials(account);
|
||||
const client = new WebdavClientAdapter(account.serverURL, cred);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
await client.createDirectory(uploadFolder).catch(() => {});
|
||||
await client.putFileContents(`${uploadFolder}/${name}`, buffer, { overwrite: false });
|
||||
};
|
||||
91
apps/meteor/server/bridges/webdav/lib/webdavClientAdapter.ts
Normal file
91
apps/meteor/server/bridges/webdav/lib/webdavClientAdapter.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import stream from 'node:stream';
|
||||
import type { Readable, Writable } from 'node:stream';
|
||||
|
||||
import type { WebDAVClient, FileStat, ResponseDataDetailed, WebDAVClientOptions } from 'webdav';
|
||||
import { createClient } from 'webdav';
|
||||
|
||||
export class WebdavClientAdapter {
|
||||
_client: WebDAVClient;
|
||||
|
||||
constructor(serverConfig: string, cred: WebDAVClientOptions) {
|
||||
if (cred.token) {
|
||||
this._client = createClient(serverConfig, { token: cred.token });
|
||||
} else {
|
||||
this._client = createClient(serverConfig, {
|
||||
username: cred.username,
|
||||
password: cred.password,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async stat(path: string): Promise<FileStat | ResponseDataDetailed<FileStat>> {
|
||||
try {
|
||||
return await this._client.stat(path);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.statusText ? error.response.statusText : 'Error checking if directory exists on webdav');
|
||||
}
|
||||
}
|
||||
|
||||
async createDirectory(path: string): Promise<void> {
|
||||
try {
|
||||
return await this._client.createDirectory(path);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.statusText ? error.response.statusText : 'Error creating directory on webdav');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(path: string): Promise<void> {
|
||||
try {
|
||||
return await this._client.deleteFile(path);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.statusText ? error.response.statusText : 'Error deleting file on webdav');
|
||||
}
|
||||
}
|
||||
|
||||
async getFileContents(filename: string): Promise<Buffer> {
|
||||
try {
|
||||
return (await this._client.getFileContents(filename)) as Buffer;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.statusText ? error.response.statusText : 'Error getting file contents webdav');
|
||||
}
|
||||
}
|
||||
|
||||
async getDirectoryContents(path: string): Promise<FileStat[] | ResponseDataDetailed<FileStat[]>> {
|
||||
try {
|
||||
return await this._client.getDirectoryContents(path);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.statusText ? error.response.statusText : 'Error getting directory contents webdav');
|
||||
}
|
||||
}
|
||||
|
||||
async putFileContents(path: string, data: Buffer, options: Record<string, any> = {}): Promise<any> {
|
||||
try {
|
||||
return await this._client.putFileContents(path, data, options);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.statusText ?? 'Error updating file contents.');
|
||||
}
|
||||
}
|
||||
|
||||
createReadStream(path: string, options?: Record<string, any>): Readable {
|
||||
return this._client.createReadStream(path, options);
|
||||
}
|
||||
|
||||
createWriteStream(path: string, fileSize: number): Writable {
|
||||
const ws = new stream.PassThrough();
|
||||
|
||||
this._client
|
||||
.customRequest(path, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
...(fileSize ? { 'Content-Length': String(fileSize) } : {}),
|
||||
},
|
||||
data: ws,
|
||||
maxRedirects: 0,
|
||||
})
|
||||
.catch((err) => {
|
||||
ws.emit('error', err);
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
}
|
||||
153
apps/meteor/server/bridges/webdav/methods/addWebdavAccount.ts
Normal file
153
apps/meteor/server/bridges/webdav/methods/addWebdavAccount.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { IWebdavAccountPayload } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { WebdavAccounts } from '@rocket.chat/models';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { WebdavClientAdapter } from '../lib/webdavClientAdapter';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addWebdavAccount(formData: IWebdavAccountPayload): boolean;
|
||||
addWebdavAccountByToken(data: IWebdavAccountPayload): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const addWebdavAccountByToken = async (userId: string, data: IWebdavAccountPayload): Promise<boolean> => {
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'addWebdavAccount' });
|
||||
}
|
||||
|
||||
if (!settings.get('Webdav_Integration_Enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', {
|
||||
method: 'addWebdavAccount',
|
||||
});
|
||||
}
|
||||
|
||||
check(
|
||||
data,
|
||||
Match.ObjectIncluding({
|
||||
serverURL: String,
|
||||
token: Match.ObjectIncluding({
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
refresh_token: Match.Optional(String),
|
||||
}),
|
||||
name: Match.Maybe(String),
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
const client = new WebdavClientAdapter(data.serverURL, { token: data.token });
|
||||
|
||||
const accountData = {
|
||||
userId,
|
||||
serverURL: data.serverURL,
|
||||
token: data.token,
|
||||
name: data.name ?? '',
|
||||
};
|
||||
|
||||
await client.stat('/');
|
||||
await WebdavAccounts.updateOne(
|
||||
{
|
||||
userId,
|
||||
serverURL: data.serverURL,
|
||||
name: data.name ?? '',
|
||||
},
|
||||
{
|
||||
$set: accountData,
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
void api.broadcast('notify.webdav', userId, {
|
||||
type: 'changed',
|
||||
account: accountData,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Meteor.Error('could-not-access-webdav', 'Could not access webdav', {
|
||||
method: 'addWebdavAccount',
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addWebdavAccount(formData) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'addWebdavAccount' });
|
||||
}
|
||||
|
||||
if (!settings.get('Webdav_Integration_Enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', {
|
||||
method: 'addWebdavAccount',
|
||||
});
|
||||
}
|
||||
|
||||
check(
|
||||
formData,
|
||||
Match.ObjectIncluding({
|
||||
serverURL: String,
|
||||
username: String,
|
||||
password: String,
|
||||
name: Match.Maybe(String),
|
||||
}),
|
||||
);
|
||||
|
||||
const duplicateAccount = await WebdavAccounts.findOneByUserIdServerUrlAndUsername(
|
||||
{ userId, serverURL: formData.serverURL, username: formData.username },
|
||||
{},
|
||||
);
|
||||
|
||||
if (duplicateAccount !== null) {
|
||||
throw new Meteor.Error('duplicated-account', 'Account not found', {
|
||||
method: 'addWebdavAccount',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new WebdavClientAdapter(formData.serverURL, {
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
});
|
||||
|
||||
const accountData = {
|
||||
userId,
|
||||
serverURL: formData.serverURL,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
name: formData.name ?? '',
|
||||
};
|
||||
|
||||
await client.stat('/');
|
||||
await WebdavAccounts.insertOne(accountData);
|
||||
|
||||
void api.broadcast('notify.webdav', userId, {
|
||||
type: 'changed',
|
||||
account: accountData,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Meteor.Error('could-not-access-webdav', 'Could not access webdav', {
|
||||
method: 'addWebdavAccount',
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async addWebdavAccountByToken(data) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'addWebdavAccount' });
|
||||
}
|
||||
|
||||
return addWebdavAccountByToken(userId, data);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { IWebdavAccount, IWebdavNode } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { WebdavAccounts } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { getWebdavCredentials } from '../lib/getWebdavCredentials';
|
||||
import { WebdavClientAdapter } from '../lib/webdavClientAdapter';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getFileFromWebdav(accountId: IWebdavAccount['_id'], file: IWebdavNode): Promise<{ success: boolean; data: Uint8Array<ArrayBuffer> }>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getFileFromWebdav(accountId, file) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'getFileFromWebdav' });
|
||||
}
|
||||
if (!settings.get('Webdav_Integration_Enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', {
|
||||
method: 'getFileFromWebdav',
|
||||
});
|
||||
}
|
||||
|
||||
const account = await WebdavAccounts.findOneByIdAndUserId(accountId, userId, {});
|
||||
if (!account) {
|
||||
throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', {
|
||||
method: 'getFileFromWebdav',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const cred = getWebdavCredentials(account);
|
||||
const client = new WebdavClientAdapter(account.serverURL, cred);
|
||||
const fileContent = await client.getFileContents(file.filename);
|
||||
const data = new Uint8Array(fileContent);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
throw new Meteor.Error('unable-to-get-file', 'Unable to get file', {
|
||||
method: 'getFileFromWebdav',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { IWebdavAccount, IWebdavNode } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { WebdavAccounts } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { getWebdavCredentials } from '../lib/getWebdavCredentials';
|
||||
import { WebdavClientAdapter } from '../lib/webdavClientAdapter';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getWebdavFileList(accountId: IWebdavAccount['_id'], path: string): { success: boolean; data: IWebdavNode[] };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getWebdavFileList(accountId, path) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'getWebdavFileList' });
|
||||
}
|
||||
|
||||
if (!settings.get('Webdav_Integration_Enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', {
|
||||
method: 'getWebdavFileList',
|
||||
});
|
||||
}
|
||||
|
||||
const account = await WebdavAccounts.findOneByIdAndUserId(accountId, userId, {});
|
||||
if (!account) {
|
||||
throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', {
|
||||
method: 'getWebdavFileList',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const cred = getWebdavCredentials(account);
|
||||
const client = new WebdavClientAdapter(account.serverURL, cred);
|
||||
const data = (await client.getDirectoryContents(path)) as IWebdavNode[];
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
throw new Meteor.Error('could-not-access-webdav', 'Could not access webdav', {
|
||||
method: 'getWebdavFileList',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { IWebdavAccount } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { WebdavAccounts } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createClient } from 'webdav';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { getWebdavCredentials } from '../lib/getWebdavCredentials';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getWebdavFilePreview(accountId: IWebdavAccount['_id'], path: string): { success: true; data: ArrayBuffer } | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getWebdavFilePreview(accountId, path) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid User', {
|
||||
method: 'getWebdavFilePreview',
|
||||
});
|
||||
}
|
||||
|
||||
if (!settings.get('Webdav_Integration_Enabled') || !settings.get('Accounts_OAuth_Nextcloud_URL')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', {
|
||||
method: 'getWebdavFilePreview',
|
||||
});
|
||||
}
|
||||
|
||||
const account = await WebdavAccounts.findOneByIdAndUserId(accountId, userId, {});
|
||||
if (!account) {
|
||||
throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', {
|
||||
method: 'getWebdavFilePreview',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const cred = getWebdavCredentials(account);
|
||||
const client = createClient(account.serverURL, cred);
|
||||
const serverURL = settings.get('Accounts_OAuth_Nextcloud_URL');
|
||||
const res = await client.customRequest(`${serverURL}/index.php/core/preview.png?file=${path}&x=64&y=64`, {
|
||||
method: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
return { success: true, data: res.data as ArrayBuffer };
|
||||
} catch (error) {
|
||||
// ignore error
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MeteorError } from '@rocket.chat/core-services';
|
||||
import type { IWebdavAccount } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { uploadFileToWebdav } from '../lib/uploadFileToWebdav';
|
||||
|
||||
const logger = new Logger('WebDAV_Upload');
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
uploadFileToWebdav(
|
||||
accountId: IWebdavAccount['_id'],
|
||||
fileData: string | Buffer | ArrayBuffer,
|
||||
name: string,
|
||||
): { success: boolean; message?: TranslationKey };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async uploadFileToWebdav(accountId, fileData, name) {
|
||||
if (!Meteor.userId()) {
|
||||
throw new MeteorError('error-invalid-user', 'Invalid User', {
|
||||
method: 'uploadFileToWebdav',
|
||||
});
|
||||
}
|
||||
|
||||
if (!settings.get('Webdav_Integration_Enabled')) {
|
||||
throw new MeteorError('error-not-allowed', 'WebDAV Integration Not Allowed', {
|
||||
method: 'uploadFileToWebdav',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await uploadFileToWebdav(accountId, fileData instanceof ArrayBuffer ? Buffer.from(fileData) : fileData, name);
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
if (typeof err === 'object' && err instanceof Error && err.name === 'error-invalid-account') {
|
||||
throw new MeteorError(err.name, 'Invalid WebDAV Account', {
|
||||
method: 'uploadFileToWebdav',
|
||||
});
|
||||
}
|
||||
|
||||
logger.error({ err });
|
||||
|
||||
if (err.response) {
|
||||
const { status } = err.response;
|
||||
if (status === 404) {
|
||||
return { success: false, message: 'webdav-server-not-found' };
|
||||
}
|
||||
if (status === 401) {
|
||||
return { success: false, message: 'error-invalid-account' };
|
||||
}
|
||||
if (status === 412) {
|
||||
return { success: false, message: 'Duplicate_file_name_found' };
|
||||
}
|
||||
}
|
||||
return { success: false, message: 'FileUpload_Error' };
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user