- {{#each federationPeers}}
+ {{#each federationPeerStatuses}}
-
{{/each}}
diff --git a/app/federation/client/admin/dashboard.js b/app/federation/client/admin/dashboard.js
index e53325264d23..1979e798b262 100644
--- a/app/federation/client/admin/dashboard.js
+++ b/app/federation/client/admin/dashboard.js
@@ -18,7 +18,10 @@ let templateInstance; // current template instance/context
const updateOverviewData = () => {
Meteor.call('federation:getOverviewData', (error, result) => {
if (error) {
+ console.log(error);
+
return;
+ // return handleError(error);
}
const { data } = result;
@@ -27,29 +30,32 @@ const updateOverviewData = () => {
});
};
-const updateServers = () => {
- Meteor.call('federation:getServers', (error, result) => {
+const updatePeerStatuses = () => {
+ Meteor.call('federation:getPeerStatuses', (error, result) => {
if (error) {
+ console.log(error);
+
return;
+ // return handleError(error);
}
const { data } = result;
- templateInstance.federationPeers.set(data);
+ templateInstance.federationPeerStatuses.set(data);
});
};
const updateData = () => {
updateOverviewData();
- updateServers();
+ updatePeerStatuses();
};
Template.dashboard.helpers({
federationOverviewData() {
return templateInstance.federationOverviewData.get();
},
- federationPeers() {
- return templateInstance.federationPeers.get();
+ federationPeerStatuses() {
+ return templateInstance.federationPeerStatuses.get();
},
});
@@ -58,7 +64,7 @@ Template.dashboard.onCreated(function() {
templateInstance = Template.instance();
this.federationOverviewData = new ReactiveVar();
- this.federationPeers = new ReactiveVar();
+ this.federationPeerStatuses = new ReactiveVar();
});
Template.dashboard.onRendered(() => {
diff --git a/app/federation/client/admin/visualizer.css b/app/federation/client/admin/visualizer.css
deleted file mode 100644
index 31b7ade58d7a..000000000000
--- a/app/federation/client/admin/visualizer.css
+++ /dev/null
@@ -1,141 +0,0 @@
-.status {
- flex: 0 0 auto;
-
- width: 6px;
- height: 6px;
- margin: 0 7px;
-
- border-radius: 50%;
-}
-
-.status.stable {
- background-color: #2de0a5;
-}
-
-.status.unstable {
- background-color: #ffd21f;
-}
-
-.status.failing {
- background-color: #f5455c;
-}
-
-.frame {
- display: flex;
- flex-direction: row;
-}
-
-.group {
- display: flex;
- flex-direction: row;
- flex: 100%;
-
- max-width: 100%;
- margin: 10px;
-
- border-width: 1px;
- align-items: center;
- justify-content: center;
-}
-
-.group.left {
- justify-content: flex-start;
-}
-
-.group.wrap {
- flex-wrap: wrap;
-}
-
-.overview-column {
- flex: 100%;
-
- min-height: 20px;
- margin: 15px 0;
-}
-
-.overview-column.small {
- max-width: 20%;
-}
-
-.group .overview-column:not(:last-child) {
- border-right: 1px solid #e9e9e9;
-}
-
-.group .overview-column:nth-child(5n) {
- border-right: 0;
-}
-
-.overview-pill {
- display: flex;
-
- width: 100%;
- padding: 0 10px;
-
- user-select: text;
- text-align: center;
- align-items: center;
-}
-
-.overview-item {
- width: 100%;
-
- user-select: text;
- text-align: center;
-}
-
-.overview-item > .title {
- display: inline-block;
-
- margin-top: 8px;
-
- text-transform: uppercase;
-
- color: #9ea2a8;
-
- font-size: 0.875rem;
- font-weight: 300;
-}
-
-.overview-item > .value {
- display: inline-block;
-
- width: 100%;
-
- text-transform: capitalize;
-
- color: #383838;
-
- font-size: 1.75rem;
- font-weight: 400;
- line-height: 1;
-}
-
-@media screen and (max-width: 925px) {
- .overview-item > .title {
- font-size: 0.5rem;
- }
-
- .overview-item > .value {
- font-size: 1rem;
- }
-}
-
-@media screen and (max-width: 800px) {
- .overview-item > .title {
- font-size: 0.875rem;
- }
-
- .overview-item > .value {
- font-size: 1.75rem;
- }
-}
-
-@media screen and (max-width: 600px) {
- .overview-item > .title {
- font-size: 0.5rem;
- }
-
- .overview-item > .value {
- font-size: 1rem;
- }
-}
diff --git a/app/federation/client/admin/visualizer.html b/app/federation/client/admin/visualizer.html
deleted file mode 100644
index cd00f240895a..000000000000
--- a/app/federation/client/admin/visualizer.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- {{> header sectionName="Federation_Dashboard"}}
-
-
-
-
diff --git a/app/federation/client/admin/visualizer.js b/app/federation/client/admin/visualizer.js
deleted file mode 100644
index 68623f8f5546..000000000000
--- a/app/federation/client/admin/visualizer.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { Tracker } from 'meteor/tracker';
-import { Template } from 'meteor/templating';
-import { ReactiveVar } from 'meteor/reactive-var';
-import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import vis from 'vis';
-import moment from 'moment';
-
-import { AdminBox } from '../../../ui-utils';
-import { hasRole } from '../../../authorization';
-
-import './visualizer.html';
-import './visualizer.css';
-
-// Template controller
-let templateInstance; // current template instance/context
-
-// Vis datasets
-const visDataSets = {
- nodes: new vis.DataSet(),
- edges: new vis.DataSet(),
-};
-
-
-let latestEventTimestamp = moment().startOf('week');
-
-// Methods
-const loadContextEvents = () => {
- Meteor.call('federation:loadContextEvents', latestEventTimestamp.toISOString(), (error, result) => {
- if (error) {
- return;
- }
-
- for (const event of result) {
- let label = '';
-
- switch (event.type) {
- case 'genesis':
- label = `[${ event.origin }] Genesis`;
- break;
- case 'room_add_user':
- label = `[${ event.origin }] Added user => ${ event.data.user.username }`;
- break;
- case 'room_message':
- label = `[${ event.origin }] New message => ${ event.data.message.msg.substring(0, 10) }`;
- break;
- }
-
- visDataSets.nodes.add({
- id: event._id,
- label,
- });
-
- for (const previous_id of event.parentIds) {
- visDataSets.edges.add({
- id: `${ event._id }${ previous_id }`,
- from: previous_id,
- to: event._id,
- });
- }
-
- if (latestEventTimestamp === null || event.timestamp > latestEventTimestamp) {
- latestEventTimestamp = event.timestamp;
- }
- }
- });
-};
-
-const updateData = () => {
- // updateOverviewData();
- // updatePeerStatuses();
-
- loadContextEvents();
-};
-
-Template.dashboard.helpers({
- federationOverviewData() {
- return templateInstance.federationOverviewData.get();
- },
- federationPeerStatuses() {
- return templateInstance.federationPeerStatuses.get();
- },
-});
-
-// Events
-Template.dashboard.onCreated(function() {
- templateInstance = Template.instance();
-
- this.federationOverviewData = new ReactiveVar();
- this.federationPeerStatuses = new ReactiveVar();
-});
-
-Template.dashboard.onRendered(() => {
- Tracker.autorun(updateData);
-
- // Setup vis.js
- new vis.Network(templateInstance.find('#network'), visDataSets, {
- layout: {
- hierarchical: {
- direction: 'UD',
- sortMethod: 'directed',
- },
- },
- interaction: { dragNodes: false },
- physics: {
- enabled: false,
- },
- configure: {
- filter(option, path) {
- if (path.indexOf('hierarchical') !== -1) {
- return true;
- }
- return false;
- },
- showButton: false,
- },
- });
-
- setInterval(updateData, 5000);
-});
-
-// Route setup
-
-FlowRouter.route('/admin/federation-dashboard', {
- name: 'federation-dashboard',
- action() {
- BlazeLayout.render('main', { center: 'dashboard', old: true });
- },
-});
-
-AdminBox.addOption({
- icon: 'discover',
- href: 'admin/federation-dashboard',
- i18nLabel: 'Federation Dashboard',
- permissionGranted() {
- return hasRole(Meteor.userId(), 'admin');
- },
-});
diff --git a/app/federation/client/index.js b/app/federation/client/index.js
index 53425c4788eb..14655cf4b6a7 100644
--- a/app/federation/client/index.js
+++ b/app/federation/client/index.js
@@ -1,6 +1,2 @@
+import './messageTypes';
import './admin/dashboard';
-
-import './tabBar';
-
-import './views/federationFlexTab.html';
-import './views/federationFlexTab.js';
diff --git a/app/federation/client/messageTypes.js b/app/federation/client/messageTypes.js
new file mode 100644
index 000000000000..573845cba35a
--- /dev/null
+++ b/app/federation/client/messageTypes.js
@@ -0,0 +1,23 @@
+import { MessageTypes } from '../../ui-utils/client';
+
+// Register message types
+MessageTypes.registerType({
+ id: 'rejected-message-by-peer',
+ system: true,
+ message: 'This_message_was_rejected_by__peer__peer',
+ data(message) {
+ return {
+ peer: message.peer,
+ };
+ },
+});
+MessageTypes.registerType({
+ id: 'peer-does-not-exist',
+ system: true,
+ message: 'The_peer__peer__does_not_exist',
+ data(message) {
+ return {
+ peer: message.peer,
+ };
+ },
+});
diff --git a/app/federation/client/tabBar.js b/app/federation/client/tabBar.js
deleted file mode 100644
index 0b8e3d9de759..000000000000
--- a/app/federation/client/tabBar.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { Session } from 'meteor/session';
-import { Tracker } from 'meteor/tracker';
-
-import { ChatRoom } from '../../models';
-import { TabBar } from '../../ui-utils';
-import { settings } from '../../settings';
-
-Meteor.startup(() => {
- Tracker.autorun(function() {
- if (settings.get('FEDERATION_Enabled')) {
- const room = ChatRoom.findOne(Session.get('openedRoom'));
-
- // Only add if the room is federated
- if (!room || !room.federation) { return; }
-
- return TabBar.addButton({
- groups: ['channel', 'group', 'direct'],
- id: 'federation',
- i18nTitle: 'FEDERATION_Room_Status',
- icon: 'discover',
- template: 'federationFlexTab',
- order: 0,
- });
- }
-
- TabBar.removeButton('federation');
- });
-});
diff --git a/app/federation/client/views/federationFlexTab.html b/app/federation/client/views/federationFlexTab.html
deleted file mode 100644
index e492c322f492..000000000000
--- a/app/federation/client/views/federationFlexTab.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
- {{#each federationPeerStatuses}}
-
- {{/each}}
-
-
-
-
-
-
-
-
diff --git a/app/federation/client/views/federationFlexTab.js b/app/federation/client/views/federationFlexTab.js
deleted file mode 100644
index 404c8b7912a6..000000000000
--- a/app/federation/client/views/federationFlexTab.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Template } from 'meteor/templating';
-import { Session } from 'meteor/session';
-
-import { ChatRoom } from '../../../models';
-
-Template.federationFlexTab.helpers({
- federationPeerStatuses() {
- const room = ChatRoom.findOne(Session.get('openedRoom'));
-
- // Only add if the room is federated
- if (!room || !room.federation) { return []; }
-
- return [];
- },
-});
diff --git a/app/federation/server/PeerClient.js b/app/federation/server/PeerClient.js
new file mode 100644
index 000000000000..38ceabf807a8
--- /dev/null
+++ b/app/federation/server/PeerClient.js
@@ -0,0 +1,638 @@
+import qs from 'querystring';
+
+import { Meteor } from 'meteor/meteor';
+
+import { updateStatus } from './settingsUpdater';
+import { logger } from './logger';
+import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources';
+import { callbacks } from '../../callbacks/server';
+import { settings } from '../../settings/server';
+import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models/server';
+
+import { Federation } from '.';
+
+export class PeerClient {
+ constructor() {
+ this.config = {};
+
+ this.enabled = false;
+
+ // Keep resources we should skip callbacks
+ this.callbacksToSkip = {};
+ }
+
+ setConfig(config) {
+ // General
+ this.config = config;
+
+ // Setup HubPeer
+ const { hub: { url } } = this.config;
+
+ // Remove trailing slash
+ this.HubPeer = { url };
+
+ // Set the local peer
+ this.peer = {
+ domain: this.config.peer.domain,
+ url: this.config.peer.url,
+ public_key: this.config.peer.public_key,
+ cloud_token: this.config.cloud.token,
+ };
+ }
+
+ log(message) {
+ logger.peerClient.info(message);
+ }
+
+ disable() {
+ this.log('Disabling...');
+
+ this.enabled = false;
+ }
+
+ enable() {
+ this.log('Enabling...');
+
+ this.enabled = true;
+ }
+
+ start() {
+ this.setupCallbacks();
+ }
+
+ // ###########
+ //
+ // Registering
+ //
+ // ###########
+ register() {
+ if (this.config.hub.active) {
+ updateStatus('Registering with Hub...');
+
+ return Federation.peerDNS.register(this.peer);
+ }
+
+ return true;
+ }
+
+ // ###################
+ //
+ // Callback management
+ //
+ // ###################
+ addCallbackToSkip(callback, resourceId) {
+ this.callbacksToSkip[`${ callback }_${ resourceId }`] = true;
+ }
+
+ skipCallbackIfNeeded(callback, resource) {
+ const { federation } = resource;
+
+ if (!federation) { return false; }
+
+ const { _id } = federation;
+
+ const callbackName = `${ callback }_${ _id }`;
+
+ const skipCallback = this.callbacksToSkip[callbackName];
+
+ delete this.callbacksToSkip[callbackName];
+
+ this.log(`${ callbackName } callback ${ skipCallback ? '' : 'not ' }skipped`);
+
+ return skipCallback;
+ }
+
+ wrapEnabled(callbackHandler) {
+ return function(...parameters) {
+ if (!this.enabled) { return; }
+
+ callbackHandler.apply(this, parameters);
+ }.bind(this);
+ }
+
+ setupCallbacks() {
+ // Accounts.onLogin(onLoginCallbackHandler.bind(this));
+ // Accounts.onLogout(onLogoutCallbackHandler.bind(this));
+
+ FederationEvents.on('createEvent', this.wrapEnabled(this.onCreateEvent.bind(this)));
+
+ callbacks.add('afterCreateDirectRoom', this.wrapEnabled(this.afterCreateDirectRoom.bind(this)), callbacks.priority.LOW, 'federation-create-direct-room');
+ callbacks.add('afterCreateRoom', this.wrapEnabled(this.afterCreateRoom.bind(this)), callbacks.priority.LOW, 'federation-create-room');
+ callbacks.add('afterSaveRoomSettings', this.wrapEnabled(this.afterSaveRoomSettings.bind(this)), callbacks.priority.LOW, 'federation-after-save-room-settings');
+ callbacks.add('afterAddedToRoom', this.wrapEnabled(this.afterAddedToRoom.bind(this)), callbacks.priority.LOW, 'federation-added-to-room');
+ callbacks.add('beforeLeaveRoom', this.wrapEnabled(this.beforeLeaveRoom.bind(this)), callbacks.priority.LOW, 'federation-leave-room');
+ callbacks.add('beforeRemoveFromRoom', this.wrapEnabled(this.beforeRemoveFromRoom.bind(this)), callbacks.priority.LOW, 'federation-remove-from-room');
+ callbacks.add('afterSaveMessage', this.wrapEnabled(this.afterSaveMessage.bind(this)), callbacks.priority.LOW, 'federation-save-message');
+ callbacks.add('afterDeleteMessage', this.wrapEnabled(this.afterDeleteMessage.bind(this)), callbacks.priority.LOW, 'federation-delete-message');
+ callbacks.add('afterReadMessages', this.wrapEnabled(this.afterReadMessages.bind(this)), callbacks.priority.LOW, 'federation-read-messages');
+ callbacks.add('afterSetReaction', this.wrapEnabled(this.afterSetReaction.bind(this)), callbacks.priority.LOW, 'federation-after-set-reaction');
+ callbacks.add('afterUnsetReaction', this.wrapEnabled(this.afterUnsetReaction.bind(this)), callbacks.priority.LOW, 'federation-after-unset-reaction');
+ callbacks.add('afterMuteUser', this.wrapEnabled(this.afterMuteUser.bind(this)), callbacks.priority.LOW, 'federation-mute-user');
+ callbacks.add('afterUnmuteUser', this.wrapEnabled(this.afterUnmuteUser.bind(this)), callbacks.priority.LOW, 'federation-unmute-user');
+
+ this.log('Callbacks set');
+ }
+
+ // ################
+ //
+ // Event management
+ //
+ // ################
+ propagateEvent(e) {
+ this.log(`propagateEvent: ${ e.t }`);
+
+ const { peer: domain, options: eventOptions } = e;
+
+ const peer = Federation.peerDNS.searchPeer(domain);
+
+ if (!peer || !peer.public_key) {
+ this.log(`Could not find valid peer:${ domain }`);
+
+ FederationEvents.setEventAsErrored(e, 'Could not find valid peer');
+ } else {
+ try {
+ const stringPayload = JSON.stringify({ event: e });
+
+ // Encrypt with the peer's public key
+ let payload = FederationKeys.loadKey(peer.public_key, 'public').encrypt(stringPayload);
+
+ // Encrypt with the local private key
+ payload = Federation.privateKey.encryptPrivate(payload);
+
+ Federation.peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, eventOptions.retry || { total: 5, stepSize: 500, stepMultiplier: 10 });
+
+ FederationEvents.setEventAsFullfilled(e);
+ } catch (err) {
+ this.log(`[${ e.t }] Event could not be sent to peer:${ domain }`);
+
+ if (err.response) {
+ const { response: { data: error } } = err;
+
+ if (error.errorType === 'error-app-prevented-sending') {
+ const { payload: {
+ message: {
+ rid: roomId,
+ u: {
+ username,
+ federation: { _id: userId },
+ },
+ },
+ } } = e;
+
+ const localUsername = username.split('@')[0];
+
+ // Create system message
+ Messages.createRejectedMessageByPeer(roomId, localUsername, {
+ u: {
+ _id: userId,
+ username: localUsername,
+ },
+ peer: domain,
+ });
+
+ return FederationEvents.setEventAsErrored(e, err.error, true);
+ }
+ }
+
+ if (err.error === 'federation-peer-does-not-exist') {
+ const { payload: {
+ message: {
+ rid: roomId,
+ u: {
+ username,
+ federation: { _id: userId },
+ },
+ },
+ } } = e;
+
+ const localUsername = username.split('@')[0];
+
+ // Create system message
+ Messages.createPeerDoesNotExist(roomId, localUsername, {
+ u: {
+ _id: userId,
+ username: localUsername,
+ },
+ peer: domain,
+ });
+
+ return FederationEvents.setEventAsErrored(e, err.error, true);
+ }
+
+ return FederationEvents.setEventAsErrored(e, `Could not send request to ${ domain }`);
+ }
+ }
+ }
+
+ onCreateEvent(e) {
+ this.propagateEvent(e);
+ }
+
+ resendUnfulfilledEvents() {
+ // Should we use queues in here?
+ const events = FederationEvents.getUnfulfilled();
+
+ events.forEach((e) => this.propagateEvent(e));
+ }
+
+ // #####
+ //
+ // Users
+ //
+ // #####
+ findUsers(identifier, options = {}) {
+ const [username, domain] = identifier.split('@');
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ let peer = null;
+
+ try {
+ peer = Federation.peerDNS.searchPeer(options.domainOverride || domain);
+ } catch (err) {
+ this.log(`Could not find peer using domain:${ domain }`);
+ throw new Meteor.Error('federation-peer-does-not-exist', `Could not find peer using domain:${ domain }`);
+ }
+
+ try {
+ const { data: { federatedUsers: remoteFederatedUsers } } = Federation.peerHTTP.request(peer, 'GET', `/api/v1/federation.users?${ qs.stringify({ username, domain, usernameOnly: options.usernameOnly }) }`);
+
+ const federatedUsers = [];
+
+ for (const federatedUser of remoteFederatedUsers) {
+ federatedUsers.push(new FederatedUser(localPeerDomain, federatedUser.user));
+ }
+
+ return federatedUsers;
+ } catch (err) {
+ this.log(`Could not find user:${ username } at ${ peer.domain }`);
+ throw new Meteor.Error('federation-user-does-not-exist', `Could not find user:${ identifier } at ${ peer.domain }`);
+ }
+ }
+
+ // #######
+ //
+ // Uploads
+ //
+ // #######
+ getUpload(options) {
+ const { identifier: domain, localMessage: { file: { _id: fileId } } } = options;
+
+ let peer = null;
+
+ try {
+ peer = Federation.peerDNS.searchPeer(domain);
+ } catch (err) {
+ this.log(`Could not find peer using domain:${ domain }`);
+ throw new Meteor.Error('federation-peer-does-not-exist', `Could not find peer using domain:${ domain }`);
+ }
+
+ const { data: { upload, buffer } } = Federation.peerHTTP.request(peer, 'GET', `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`);
+
+ return { upload, buffer: Buffer.from(buffer) };
+ }
+
+ // #################
+ //
+ // Callback handlers
+ //
+ // #################
+ afterCreateDirectRoom(room, { from: owner }) {
+ this.log('afterCreateDirectRoom');
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return room; }
+
+ const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner });
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('afterCreateDirectRoom', federatedRoom.getLocalRoom())) { return room; }
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ FederationEvents.directRoomCreated(federatedRoom, { skipPeers: [localPeerDomain] });
+
+ return room;
+ }
+
+ afterCreateRoom(roomOwner, room) {
+ this.log('afterCreateRoom');
+
+ const { _id: ownerId } = roomOwner;
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return roomOwner; }
+
+ const owner = Users.findOneById(ownerId);
+
+ const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner });
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('afterCreateRoom', federatedRoom.getLocalRoom())) { return roomOwner; }
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ FederationEvents.roomCreated(federatedRoom, { skipPeers: [localPeerDomain] });
+
+ return roomOwner;
+ }
+
+ afterSaveRoomSettings(/* room */) {
+ this.log('afterSaveRoomSettings - NOT IMPLEMENTED');
+ }
+
+ afterAddedToRoom(users, room) {
+ this.log('afterAddedToRoom');
+
+ const { user: userWhoJoined, inviter: userWhoInvited } = users;
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('afterAddedToRoom', userWhoJoined)) { return users; }
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room or user who joined are federated
+ if ((!userWhoJoined.federation || userWhoJoined.federation.peer === localPeerDomain)
+ && !FederatedRoom.isFederated(localPeerDomain, room)) {
+ return users;
+ }
+
+ const extras = {};
+
+ // If the room is not federated and has an owner
+ if (!room.federation) {
+ let ownerId;
+
+ // If the room does not have an owner, get the first user subscribed to that room
+ if (!room.u) {
+ const userSubscription = Subscriptions.findOne({ rid: room._id }, {
+ sort: {
+ ts: 1,
+ },
+ });
+
+ ownerId = userSubscription.u._id;
+ } else {
+ ownerId = room.u._id;
+ }
+
+ extras.owner = Users.findOneById(ownerId);
+ }
+
+ const federatedRoom = new FederatedRoom(localPeerDomain, room, extras);
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ // If the user who joined is from a different peer...
+ if (userWhoJoined.federation && userWhoJoined.federation.peer !== localPeerDomain) {
+ // ...create a "create room" event for that peer
+ FederationEvents.roomCreated(federatedRoom, { peers: [userWhoJoined.federation.peer] });
+ }
+
+ // Then, create a "user join/added" event to the other peers
+ const federatedUserWhoJoined = FederatedUser.loadOrCreate(localPeerDomain, userWhoJoined);
+
+ if (userWhoInvited) {
+ const federatedInviter = FederatedUser.loadOrCreate(localPeerDomain, userWhoInvited);
+
+ FederationEvents.userAdded(federatedRoom, federatedUserWhoJoined, federatedInviter, { skipPeers: [localPeerDomain] });
+ } else {
+ FederationEvents.userJoined(federatedRoom, federatedUserWhoJoined, { skipPeers: [localPeerDomain] });
+ }
+
+ return users;
+ }
+
+ beforeLeaveRoom(userWhoLeft, room) {
+ this.log('beforeLeaveRoom');
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('beforeLeaveRoom', userWhoLeft)) { return userWhoLeft; }
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return userWhoLeft; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ const federatedUserWhoLeft = FederatedUser.loadByFederationId(localPeerDomain, userWhoLeft.federation._id);
+
+ // Then, create a "user left" event to the other peers
+ FederationEvents.userLeft(federatedRoom, federatedUserWhoLeft, { skipPeers: [localPeerDomain] });
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ return userWhoLeft;
+ }
+
+ beforeRemoveFromRoom(users, room) {
+ this.log('beforeRemoveFromRoom');
+
+ const { removedUser, userWhoRemoved } = users;
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('beforeRemoveFromRoom', removedUser)) { return users; }
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ const federatedRemovedUser = FederatedUser.loadByFederationId(localPeerDomain, removedUser.federation._id);
+
+ const federatedUserWhoRemoved = FederatedUser.loadByFederationId(localPeerDomain, userWhoRemoved.federation._id);
+
+ FederationEvents.userRemoved(federatedRoom, federatedRemovedUser, federatedUserWhoRemoved, { skipPeers: [localPeerDomain] });
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ return users;
+ }
+
+ afterSaveMessage(message, room) {
+ this.log('afterSaveMessage');
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('afterSaveMessage', message)) { return message; }
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ const federatedMessage = FederatedMessage.loadOrCreate(localPeerDomain, message);
+
+ // If editedAt exists, it means it is an update
+ if (message.editedAt) {
+ const user = Users.findOneById(message.editedBy._id);
+
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id);
+
+ FederationEvents.messageUpdated(federatedRoom, federatedMessage, federatedUser, { skipPeers: [localPeerDomain] });
+ } else {
+ FederationEvents.messageCreated(federatedRoom, federatedMessage, { skipPeers: [localPeerDomain] });
+ }
+
+ return message;
+ }
+
+ afterDeleteMessage(message) {
+ this.log('afterDeleteMessage');
+
+ // Check if this should be skipped
+ if (this.skipCallbackIfNeeded('afterDeleteMessage', message)) { return message; }
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ const room = Rooms.findOneById(message.rid);
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ const federatedMessage = new FederatedMessage(localPeerDomain, message);
+
+ FederationEvents.messageDeleted(federatedRoom, federatedMessage, { skipPeers: [localPeerDomain] });
+
+ return message;
+ }
+
+ afterReadMessages(roomId, { userId }) {
+ this.log('afterReadMessages');
+
+ if (!settings.get('Message_Read_Receipt_Enabled')) { this.log('Skipping: read receipts are not enabled'); return roomId; }
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ const room = Rooms.findOneById(roomId);
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return roomId; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ if (this.skipCallbackIfNeeded('afterReadMessages', federatedRoom.getLocalRoom())) { return roomId; }
+
+ const user = Users.findOneById(userId);
+
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id);
+
+ FederationEvents.messagesRead(federatedRoom, federatedUser, { skipPeers: [localPeerDomain] });
+
+ return roomId;
+ }
+
+ afterSetReaction(message, { user, reaction, shouldReact }) {
+ this.log('afterSetReaction');
+
+ const room = Rooms.findOneById(message.rid);
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; }
+
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id);
+
+ const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, message.federation._id);
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ FederationEvents.messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, { skipPeers: [localPeerDomain] });
+
+ return message;
+ }
+
+ afterUnsetReaction(message, { user, reaction, shouldReact }) {
+ this.log('afterUnsetReaction');
+
+ const room = Rooms.findOneById(message.rid);
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; }
+
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id);
+
+ const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, message.federation._id);
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ FederationEvents.messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, { skipPeers: [localPeerDomain] });
+
+ return message;
+ }
+
+ afterMuteUser(users, room) {
+ this.log('afterMuteUser');
+
+ const { mutedUser, fromUser } = users;
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ const federatedMutedUser = FederatedUser.loadByFederationId(localPeerDomain, mutedUser.federation._id);
+
+ const federatedUserWhoMuted = FederatedUser.loadByFederationId(localPeerDomain, fromUser.federation._id);
+
+ FederationEvents.userMuted(federatedRoom, federatedMutedUser, federatedUserWhoMuted, { skipPeers: [localPeerDomain] });
+
+ return users;
+ }
+
+ afterUnmuteUser(users, room) {
+ this.log('afterUnmuteUser');
+
+ const { unmutedUser, fromUser } = users;
+
+ const { peer: { domain: localPeerDomain } } = this;
+
+ // Check if room is federated
+ if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; }
+
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id);
+
+ const federatedUnmutedUser = FederatedUser.loadByFederationId(localPeerDomain, unmutedUser.federation._id);
+
+ const federatedUserWhoUnmuted = FederatedUser.loadByFederationId(localPeerDomain, fromUser.federation._id);
+
+ FederationEvents.userUnmuted(federatedRoom, federatedUnmutedUser, federatedUserWhoUnmuted, { skipPeers: [localPeerDomain] });
+
+ return users;
+ }
+}
diff --git a/app/federation/server/PeerDNS.js b/app/federation/server/PeerDNS.js
new file mode 100644
index 000000000000..ace2721769ca
--- /dev/null
+++ b/app/federation/server/PeerDNS.js
@@ -0,0 +1,185 @@
+import dns from 'dns';
+
+import { Meteor } from 'meteor/meteor';
+
+
+import { logger } from './logger';
+import { updateStatus } from './settingsUpdater';
+import { FederationDNSCache } from '../../models';
+
+import { Federation } from '.';
+
+const dnsResolveSRV = Meteor.wrapAsync(dns.resolveSrv);
+const dnsResolveTXT = Meteor.wrapAsync(dns.resolveTxt);
+
+export class PeerDNS {
+ constructor() {
+ this.config = {};
+ }
+
+ setConfig(config) {
+ // General
+ this.config = config;
+
+ // Setup HubPeer
+ const { hub: { url } } = config;
+ this.HubPeer = { url };
+ }
+
+ log(message) {
+ logger.dns.info(message);
+ }
+
+ // ########
+ //
+ // Register
+ //
+ // ########
+ register(peerConfig) {
+ const { uniqueId, domain, url, public_key, cloud_token } = peerConfig;
+
+ this.log(`Registering peer with domain ${ domain }...`);
+
+ let headers;
+ if (cloud_token && cloud_token !== '') {
+ headers = { Authorization: `Bearer ${ cloud_token }` };
+ }
+
+ // Attempt to register peer
+ try {
+ Federation.peerHTTP.request(this.HubPeer, 'POST', '/api/v1/peers', { uniqueId, domain, url, public_key }, { total: 5, stepSize: 1000, tryToUpdateDNS: false }, headers);
+
+ this.log('Peer registered!');
+
+ updateStatus('Running, registered to Hub');
+
+ return true;
+ } catch (err) {
+ this.log(err);
+
+ this.log('Could not register peer');
+
+ return false;
+ }
+ }
+
+ // #############
+ //
+ // Peer Handling
+ //
+ // #############
+ searchPeer(domain) {
+ this.log(`searchPeer: ${ domain }`);
+
+ let peer = FederationDNSCache.findOneByDomain(domain);
+
+ // Try to lookup at the DNS Cache
+ if (!peer) {
+ try {
+ this.updatePeerDNS(domain);
+
+ peer = FederationDNSCache.findOneByDomain(domain);
+ } catch (err) {
+ this.log(`Could not find peer for domain ${ domain }`);
+ }
+ }
+
+ return peer;
+ }
+
+ getPeerUsingDNS(domain) {
+ this.log(`getPeerUsingDNS: ${ domain }`);
+
+ // Try searching by DNS first
+ const srvEntries = dnsResolveSRV(`_rocketchat._tcp.${ domain }`);
+
+ const [srvEntry] = srvEntries;
+
+ // Get the protocol from the TXT record, if exists
+ let protocol = 'https';
+
+ try {
+ const protocolTxtRecords = dnsResolveTXT(`rocketchat-protocol.${ domain }`);
+
+ protocol = protocolTxtRecords[0][0].toLowerCase() === 'http' ? 'http' : 'https';
+ } catch (err) {
+ // Ignore the error if the rocketchat-protocol TXT entry does not exist
+ }
+
+
+ // Get the public key from the TXT record
+ const publicKeyTxtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`);
+
+ // Get the first TXT record, this subdomain should have only a single record
+ const publicKey = publicKeyTxtRecords[0].join('');
+
+ return {
+ domain,
+ url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`,
+ public_key: publicKey,
+ };
+ }
+
+ getPeerUsingHub(domain) {
+ this.log(`getPeerUsingHub: ${ domain }`);
+
+ // If there is no DNS entry for that, get from the Hub
+ const { data: { peer } } = Federation.peerHTTP.simpleRequest(this.HubPeer, 'GET', `/api/v1/peers?search=${ domain }`);
+
+ return peer;
+ }
+
+ // ##############
+ //
+ // DNS Management
+ //
+ // ##############
+ updatePeerDNS(domain) {
+ this.log(`updatePeerDNS: ${ domain }`);
+
+ let peer = null;
+
+ try {
+ peer = this.getPeerUsingDNS(domain);
+ } catch (err) {
+ if (['ENODATA', 'ENOTFOUND'].indexOf(err.code) === -1) {
+ this.log(err);
+
+ throw new Error(`Error trying to fetch SRV DNS entries for ${ domain }`);
+ }
+
+ try {
+ peer = this.getPeerUsingHub(domain);
+ } catch (err) {
+ throw new Error(`Could not find a peer with domain ${ domain } using the hub`);
+ }
+ }
+
+ this.updateDNSCache.call(this, peer);
+
+ return peer;
+ }
+
+ updateDNSEntry(peer) {
+ this.log('updateDNSEntry');
+
+ const { domain } = peer;
+
+ delete peer._id;
+
+ // Make sure public_key has no line breaks
+ peer.public_key = peer.public_key.replace(/\n|\r/g, '');
+
+ return FederationDNSCache.upsert({ domain }, peer);
+ }
+
+ updateDNSCache(peers) {
+ this.log('updateDNSCache');
+
+ peers = Array.isArray(peers) ? peers : [peers];
+
+ for (const peer of peers) {
+ this.updateDNSEntry.call(this, peer);
+ }
+ }
+}
diff --git a/app/federation/server/PeerHTTP/PeerHTTP.js b/app/federation/server/PeerHTTP/PeerHTTP.js
new file mode 100644
index 000000000000..fbcb1e72962d
--- /dev/null
+++ b/app/federation/server/PeerHTTP/PeerHTTP.js
@@ -0,0 +1,100 @@
+import { Meteor } from 'meteor/meteor';
+import { HTTP } from 'meteor/http';
+
+import { skipRetryOnSpecificError, delay } from './utils';
+import { logger } from '../logger';
+
+import { Federation } from '..';
+
+
+export class PeerHTTP {
+ constructor() {
+ this.config = {};
+ }
+
+ setConfig(config) {
+ // General
+ this.config = config;
+ }
+
+ log(message) {
+ logger.http.info(message);
+ }
+
+ //
+ // Direct request
+ simpleRequest(peer, method, uri, body, headers) {
+ const { url: serverBaseURL } = peer;
+
+ const url = `${ serverBaseURL }${ uri }`;
+
+ let data = null;
+
+ if (method === 'POST' || method === 'PUT') {
+ data = body;
+ }
+
+ this.log(`Sending request: ${ method } - ${ url }`);
+
+ return HTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': this.config.peer.domain } });
+ }
+
+ //
+ // Request trying to find DNS entries
+ request(peer, method, uri, body, retryInfo = {}, headers = {}) {
+ // Normalize retry info
+ retryInfo = {
+ total: retryInfo.total || 1,
+ stepSize: retryInfo.stepSize || 100,
+ stepMultiplier: retryInfo.stepMultiplier || 1,
+ tryToUpdateDNS: retryInfo.tryToUpdateDNS === undefined ? true : retryInfo.tryToUpdateDNS,
+ DNSUpdated: false,
+ };
+
+ for (let i = 0; i <= retryInfo.total; i++) {
+ try {
+ return this.simpleRequest(peer, method, uri, body, headers);
+ } catch (err) {
+ try {
+ if (retryInfo.tryToUpdateDNS && !retryInfo.DNSUpdated) {
+ i--;
+
+ retryInfo.DNSUpdated = true;
+
+ this.log(`Trying to update local DNS cache for peer:${ peer.domain }`);
+
+ peer = Federation.peerDNS.updatePeerDNS(peer.domain);
+
+ continue;
+ }
+ } catch (err) {
+ if (err.response && err.response.statusCode === 404) {
+ throw new Meteor.Error('federation-peer-does-not-exist', 'Peer does not exist');
+ }
+ }
+
+ // Check if we need to skip due to specific error
+ const { skip: skipOnSpecificError, error: specificError } = skipRetryOnSpecificError(err);
+ if (skipOnSpecificError) {
+ this.log(`Retry: skipping due to specific error: ${ specificError }`);
+
+ throw err;
+ }
+
+ if (i === retryInfo.total - 1) {
+ // Throw the error, as we could not fulfill the request
+ this.log('Retry: could not fulfill the request');
+
+ throw err;
+ }
+
+ const timeToRetry = retryInfo.stepSize * (i + 1) * retryInfo.stepMultiplier;
+
+ this.log(`Trying again in ${ timeToRetry / 1000 }s: ${ method } - ${ uri }`);
+
+ // Otherwise, wait and try again
+ delay(timeToRetry);
+ }
+ }
+ }
+}
diff --git a/app/federation/server/PeerHTTP/index.js b/app/federation/server/PeerHTTP/index.js
new file mode 100644
index 000000000000..3c9e957f1cc9
--- /dev/null
+++ b/app/federation/server/PeerHTTP/index.js
@@ -0,0 +1 @@
+export { PeerHTTP } from './PeerHTTP';
diff --git a/app/federation/server/PeerHTTP/utils.js b/app/federation/server/PeerHTTP/utils.js
new file mode 100644
index 000000000000..7cc6fc177096
--- /dev/null
+++ b/app/federation/server/PeerHTTP/utils.js
@@ -0,0 +1,16 @@
+import { Meteor } from 'meteor/meteor';
+
+// Should skip the retry if the error is one of the below?
+const errorsToSkipRetrying = ['error-app-prevented-sending', 'error-decrypt'];
+
+export function skipRetryOnSpecificError(err) {
+ err = err && err.response && err.response.data && err.response.data.errorType;
+ return { skip: errorsToSkipRetrying.includes(err), error: err };
+}
+
+// Delay method to wait a little bit before retrying
+export const delay = Meteor.wrapAsync(function(ms, callback) {
+ Meteor.setTimeout(function() {
+ callback(null);
+ }, ms);
+});
diff --git a/app/federation/server/PeerPinger.js b/app/federation/server/PeerPinger.js
new file mode 100644
index 000000000000..aa3bcbfa8a4c
--- /dev/null
+++ b/app/federation/server/PeerPinger.js
@@ -0,0 +1,37 @@
+import { Meteor } from 'meteor/meteor';
+import moment from 'moment';
+
+import { logger } from './logger';
+import { ping } from './methods/ping';
+import { FederationPeers } from '../../models';
+
+
+export class PeerPinger {
+ constructor() {
+ this.config = {
+ pingInterval: 5000,
+ };
+
+ this.peers = [];
+ }
+
+ log(message) {
+ logger.pinger.info(message);
+ }
+
+ start() {
+ this.pingAllPeers();
+ }
+
+ pingAllPeers() {
+ const lastSeenAt = moment().subtract(10, 'm').toDate();
+
+ const peers = FederationPeers.find({ $or: [{ last_seen_at: null }, { last_seen_at: { $lte: lastSeenAt } }] }).fetch();
+
+ const pingResults = ping(peers.map((p) => p.peer));
+
+ FederationPeers.updateStatuses(pingResults);
+
+ Meteor.setTimeout(this.pingAllPeers.bind(this), this.config.pingInterval);
+ }
+}
diff --git a/app/federation/server/PeerServer/PeerServer.js b/app/federation/server/PeerServer/PeerServer.js
new file mode 100644
index 000000000000..f6b2ecd5b794
--- /dev/null
+++ b/app/federation/server/PeerServer/PeerServer.js
@@ -0,0 +1,404 @@
+import { callbacks } from '../../../callbacks';
+import { setReaction } from '../../../reactions/server';
+import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib';
+import { Rooms, Subscriptions, FederationPeers } from '../../../models';
+import { FederatedMessage, FederatedRoom, FederatedUser } from '../federatedResources';
+import { logger } from '../logger.js';
+
+import { Federation } from '..';
+
+export class PeerServer {
+ constructor() {
+ this.config = {};
+ this.enabled = false;
+ }
+
+ setConfig(config) {
+ // General
+ this.config = config;
+ }
+
+ log(message) {
+ logger.peerServer.info(message);
+ }
+
+ disable() {
+ this.log('Disabling...');
+
+ this.enabled = false;
+ }
+
+ enable() {
+ this.log('Enabling...');
+
+ this.enabled = true;
+ }
+
+ start() {
+ this.log('Routes are set');
+ }
+
+ handleDirectRoomCreatedEvent(e) {
+ this.log('handleDirectRoomCreatedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { room, owner, users } } = e;
+
+ // Load the federated room
+ const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner });
+
+ // Set users
+ federatedRoom.setUsers(users);
+
+ // Create, if needed, all room's users
+ federatedRoom.createUsers();
+
+ // Then, create the room, if needed
+ federatedRoom.create();
+
+ // Refresh federation peers
+ FederationPeers.refreshPeers(localPeerDomain);
+ }
+
+ handleRoomCreatedEvent(e) {
+ this.log('handleRoomCreatedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { room, owner, users } } = e;
+
+ // Load the federated room
+ const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner });
+
+ // Set users
+ federatedRoom.setUsers(users);
+
+ // Create, if needed, all room's users
+ federatedRoom.createUsers();
+
+ // Then, create the room, if needed
+ federatedRoom.create(true);
+
+ // Refresh federation peers
+ FederationPeers.refreshPeers(localPeerDomain);
+ }
+
+ handleUserJoinedEvent(e) {
+ this.log('handleUserJoinedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, user } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ // Create the user, if needed
+ const federatedUser = FederatedUser.loadOrCreate(localPeerDomain, user);
+ const localUser = federatedUser.create();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId());
+
+ // Add the user to the room
+ addUserToRoom(federatedRoom.room._id, localUser, null, false);
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ // Refresh federation peers
+ FederationPeers.refreshPeers(localPeerDomain);
+ }
+
+ handleUserAddedEvent(e) {
+ this.log('handleUserAddedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_inviter_id, user } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ // Load the inviter
+ const federatedInviter = FederatedUser.loadByFederationId(localPeerDomain, federated_inviter_id);
+
+ if (!federatedInviter) {
+ throw new Error('Inviting user does not exist');
+ }
+
+ const localInviter = federatedInviter.getLocalUser();
+
+ // Create the user, if needed
+ const federatedUser = FederatedUser.loadOrCreate(localPeerDomain, user);
+ const localUser = federatedUser.create();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId());
+
+ // Add the user to the room
+ addUserToRoom(federatedRoom.room._id, localUser, localInviter, false);
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ // Refresh federation peers
+ FederationPeers.refreshPeers(localPeerDomain);
+ }
+
+ handleUserLeftEvent(e) {
+ this.log('handleUserLeftEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_user_id } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ // Load the user who left
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('beforeLeaveRoom', federatedUser.getFederationId());
+
+ // Remove the user from the room
+ removeUserFromRoom(federatedRoom.room._id, localUser);
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ // Refresh federation peers
+ FederationPeers.refreshPeers(localPeerDomain);
+ }
+
+ handleUserRemovedEvent(e) {
+ this.log('handleUserRemovedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_user_id, federated_removed_by_user_id } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ // Load the user who left
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // Load the user who removed
+ const federatedUserWhoRemoved = FederatedUser.loadByFederationId(localPeerDomain, federated_removed_by_user_id);
+ const localUserWhoRemoved = federatedUserWhoRemoved.getLocalUser();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('beforeRemoveFromRoom', federatedUser.getFederationId());
+
+ // Remove the user from the room
+ removeUserFromRoom(federatedRoom.room._id, localUser, { byUser: localUserWhoRemoved });
+
+ // Load federated users
+ federatedRoom.loadUsers();
+
+ // Refresh room's federation
+ federatedRoom.refreshFederation();
+
+ // Refresh federation peers
+ FederationPeers.refreshPeers(localPeerDomain);
+ }
+
+ handleUserMutedEvent(e) {
+ this.log('handleUserMutedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_user_id } } = e;
+ // const { payload: { federated_room_id, federated_user_id, federated_muted_by_user_id } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ // Load the user who left
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // // Load the user who muted
+ // const federatedUserWhoMuted = FederatedUser.loadByFederationId(localPeerDomain, federated_muted_by_user_id);
+ // const localUserWhoMuted = federatedUserWhoMuted.getLocalUser();
+
+ // Mute user
+ Rooms.muteUsernameByRoomId(federatedRoom.room._id, localUser.username);
+
+ // TODO: should we create a message?
+ }
+
+ handleUserUnmutedEvent(e) {
+ this.log('handleUserUnmutedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_user_id } } = e;
+ // const { payload: { federated_room_id, federated_user_id, federated_unmuted_by_user_id } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ // Load the user who left
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // // Load the user who muted
+ // const federatedUserWhoUnmuted = FederatedUser.loadByFederationId(localPeerDomain, federated_unmuted_by_user_id);
+ // const localUserWhoUnmuted = federatedUserWhoUnmuted.getLocalUser();
+
+ // Unmute user
+ Rooms.unmuteUsernameByRoomId(federatedRoom.room._id, localUser.username);
+
+ // TODO: should we create a message?
+ }
+
+ handleMessageCreatedEvent(e) {
+ this.log('handleMessageCreatedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { message } } = e;
+
+ // Load the federated message
+ const federatedMessage = new FederatedMessage(localPeerDomain, message);
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId());
+
+ // Create the federated message
+ federatedMessage.create();
+ }
+
+ handleMessageUpdatedEvent(e) {
+ this.log('handleMessageUpdatedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { message, federated_user_id } } = e;
+
+ // Load the federated message
+ const federatedMessage = new FederatedMessage(localPeerDomain, message);
+
+ // Load the federated user
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId());
+
+ // Update the federated message
+ federatedMessage.update(federatedUser);
+ }
+
+ handleMessageDeletedEvent(e) {
+ this.log('handleMessageDeletedEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_message_id } } = e;
+
+ const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id);
+
+ // Load the federated message
+ const localMessage = federatedMessage.getLocalMessage();
+
+ // Load the author
+ const localAuthor = federatedMessage.federatedAuthor.getLocalUser();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterDeleteMessage', federatedMessage.getFederationId());
+
+ // Create the federated message
+ deleteMessage(localMessage, localAuthor);
+ }
+
+ handleMessagesReadEvent(e) {
+ this.log('handleMessagesReadEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_user_id } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+
+ Federation.peerClient.addCallbackToSkip('afterReadMessages', federatedRoom.getFederationId());
+
+ // Load the user who left
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // Mark the messages as read
+ // TODO: move below calls to an exported function
+ const userSubscription = Subscriptions.findOneByRoomIdAndUserId(federatedRoom.room._id, localUser._id, { fields: { ls: 1 } });
+ Subscriptions.setAsReadByRoomIdAndUserId(federatedRoom.room._id, localUser._id);
+
+ callbacks.run('afterReadMessages', federatedRoom.room._id, { userId: localUser._id, lastSeen: userSubscription.ls });
+ }
+
+ handleMessagesSetReactionEvent(e) {
+ this.log('handleMessagesSetReactionEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_message_id, federated_user_id, reaction, shouldReact } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+ const localRoom = federatedRoom.getLocalRoom();
+
+ // Load the user who reacted
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // Load the message
+ const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id);
+ const localMessage = federatedMessage.getLocalMessage();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterSetReaction', federatedMessage.getFederationId());
+
+ // Set message reaction
+ setReaction(localRoom, localUser, localMessage, reaction, shouldReact);
+ }
+
+ handleMessagesUnsetReactionEvent(e) {
+ this.log('handleMessagesUnsetReactionEvent');
+
+ const { peer: { domain: localPeerDomain } } = this.config;
+
+ const { payload: { federated_room_id, federated_message_id, federated_user_id, reaction, shouldReact } } = e;
+
+ // Load the federated room
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id);
+ const localRoom = federatedRoom.getLocalRoom();
+
+ // Load the user who reacted
+ const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id);
+ const localUser = federatedUser.getLocalUser();
+
+ // Load the message
+ const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id);
+ const localMessage = federatedMessage.getLocalMessage();
+
+ // Callback management
+ Federation.peerClient.addCallbackToSkip('afterUnsetReaction', federatedMessage.getFederationId());
+
+ // Unset message reaction
+ setReaction(localRoom, localUser, localMessage, reaction, shouldReact);
+ }
+}
diff --git a/app/federation/server/PeerServer/index.js b/app/federation/server/PeerServer/index.js
new file mode 100644
index 000000000000..e1da97c3327a
--- /dev/null
+++ b/app/federation/server/PeerServer/index.js
@@ -0,0 +1,6 @@
+// Setup routes
+import './routes/events';
+import './routes/uploads';
+import './routes/users';
+
+export { PeerServer } from './PeerServer';
diff --git a/app/federation/server/PeerServer/routes/events.js b/app/federation/server/PeerServer/routes/events.js
new file mode 100644
index 000000000000..45153c80bd73
--- /dev/null
+++ b/app/federation/server/PeerServer/routes/events.js
@@ -0,0 +1,115 @@
+import { Meteor } from 'meteor/meteor';
+
+import { API } from '../../../../api';
+import { FederationKeys } from '../../../../models';
+import { Federation } from '../..';
+
+API.v1.addRoute('federation.events', { authRequired: false }, {
+ post() {
+ if (!Federation.peerServer.enabled) {
+ return API.v1.failure('Not found');
+ }
+
+ if (!this.bodyParams.payload) {
+ return API.v1.failure('Payload was not sent');
+ }
+
+ if (!this.request.headers['x-federation-domain']) {
+ return API.v1.failure('Cannot handle that request');
+ }
+
+ const remotePeerDomain = this.request.headers['x-federation-domain'];
+
+ const peer = Federation.peerDNS.searchPeer(remotePeerDomain);
+
+ if (!peer) {
+ return API.v1.failure('Could not find valid peer');
+ }
+
+ const payloadBuffer = Buffer.from(this.bodyParams.payload.data);
+
+ let payload;
+
+ // Decrypt with the peer's public key
+ try {
+ payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer);
+
+ // Decrypt with the local private key
+ payload = Federation.privateKey.decrypt(payload);
+ } catch (err) {
+ throw new Meteor.Error('error-decrypt', 'Could not decrypt');
+ }
+
+ // Get the event
+ const { event: e } = JSON.parse(payload.toString());
+
+ if (!e) {
+ return API.v1.failure('Event was not sent');
+ }
+
+ Federation.peerServer.log(`Received event:${ e.t }`);
+
+ try {
+ switch (e.t) {
+ case 'png':
+ // This is a ping so we should do nothing, just respond with success
+ break;
+ case 'drc':
+ Federation.peerServer.handleDirectRoomCreatedEvent(e);
+ break;
+ case 'roc':
+ Federation.peerServer.handleRoomCreatedEvent(e);
+ break;
+ case 'usj':
+ Federation.peerServer.handleUserJoinedEvent(e);
+ break;
+ case 'usa':
+ Federation.peerServer.handleUserAddedEvent(e);
+ break;
+ case 'usl':
+ Federation.peerServer.handleUserLeftEvent(e);
+ break;
+ case 'usr':
+ Federation.peerServer.handleUserRemovedEvent(e);
+ break;
+ case 'usm':
+ Federation.peerServer.handleUserMutedEvent(e);
+ break;
+ case 'usu':
+ Federation.peerServer.handleUserUnmutedEvent(e);
+ break;
+ case 'msc':
+ Federation.peerServer.handleMessageCreatedEvent(e);
+ break;
+ case 'msu':
+ Federation.peerServer.handleMessageUpdatedEvent(e);
+ break;
+ case 'msd':
+ Federation.peerServer.handleMessageDeletedEvent(e);
+ break;
+ case 'msr':
+ Federation.peerServer.handleMessagesReadEvent(e);
+ break;
+ case 'mrs':
+ Federation.peerServer.handleMessagesSetReactionEvent(e);
+ break;
+ case 'mru':
+ Federation.peerServer.handleMessagesUnsetReactionEvent(e);
+ break;
+ default:
+ throw new Error(`Invalid event:${ e.t }`);
+ }
+
+ Federation.peerServer.log('Success, responding...');
+
+ // Respond
+ return API.v1.success();
+ } catch (err) {
+ console.log(err);
+
+ Federation.peerServer.error(`Error handling event:${ e.t } - ${ err.toString() }`);
+
+ return API.v1.failure(`Error handling event:${ e.t } - ${ err.toString() }`, err.error || 'unknown-error');
+ }
+ },
+});
diff --git a/app/federation/server/_server/endpoints/uploads.js b/app/federation/server/PeerServer/routes/uploads.js
similarity index 94%
rename from app/federation/server/_server/endpoints/uploads.js
rename to app/federation/server/PeerServer/routes/uploads.js
index 04f171716584..ec24f75ca2ba 100644
--- a/app/federation/server/_server/endpoints/uploads.js
+++ b/app/federation/server/PeerServer/routes/uploads.js
@@ -7,7 +7,7 @@ import { Federation } from '../..';
API.v1.addRoute('federation.uploads', { authRequired: false }, {
get() {
- if (!Federation.enabled) {
+ if (!Federation.peerServer.enabled) {
return API.v1.failure('Not found');
}
diff --git a/app/federation/server/PeerServer/routes/users.js b/app/federation/server/PeerServer/routes/users.js
new file mode 100644
index 000000000000..89006ad583a9
--- /dev/null
+++ b/app/federation/server/PeerServer/routes/users.js
@@ -0,0 +1,48 @@
+import { API } from '../../../../api';
+import { Users } from '../../../../models';
+import { FederatedUser } from '../../federatedResources';
+import { Federation } from '../..';
+
+API.v1.addRoute('federation.users', { authRequired: false }, {
+ get() {
+ if (!Federation.peerServer.enabled) {
+ return API.v1.failure('Not found');
+ }
+
+ const { peer: { domain: localPeerDomain } } = Federation.peerServer.config;
+
+ const { username, domain, usernameOnly } = this.requestParams();
+
+ const email = `${ username }@${ domain }`;
+
+ Federation.peerServer.log(`[users] Trying to find user by username:${ username } and email:${ email }`);
+
+ const query = {
+ type: 'user',
+ };
+
+ if (usernameOnly === 'true') {
+ query.username = username;
+ } else {
+ query.$or = [
+ { name: username },
+ { username },
+ { 'emails.address': email },
+ ];
+ }
+
+ const users = Users.find(query, { fields: { services: 0, roles: 0 } }).fetch();
+
+ if (!users.length) {
+ return API.v1.failure('There is no such user in this server');
+ }
+
+ const federatedUsers = [];
+
+ for (const user of users) {
+ federatedUsers.push(new FederatedUser(localPeerDomain, user));
+ }
+
+ return API.v1.success({ federatedUsers });
+ },
+});
diff --git a/app/federation/server/_client/callbacks/afterAddedToRoom.js b/app/federation/server/_client/callbacks/afterAddedToRoom.js
deleted file mode 100644
index 605bd0a74cc5..000000000000
--- a/app/federation/server/_client/callbacks/afterAddedToRoom.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { logger } from '../../logger';
-import { isFederated, getFederatedRoomData } from './helpers/federatedResources';
-import { FederationRoomEvents, Subscriptions } from '../../../../models/server';
-import { Federation } from '../../federation';
-import { normalizers } from '../../normalizers';
-import { doAfterCreateRoom } from './afterCreateRoom';
-
-async function afterAddedToRoom(involvedUsers, room) {
- const { user: addedUser } = involvedUsers;
-
- if (!isFederated(room) && !isFederated(addedUser)) { return; }
-
- logger.client.debug(() => `afterAddedToRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- // If there are not federated users on this room, ignore it
- const { users, subscriptions } = getFederatedRoomData(room);
-
- // Load the subscription
- const subscription = Promise.await(Subscriptions.findOneByRoomIdAndUserId(room._id, addedUser._id));
-
- try {
- //
- // Check if the room is already federated, if it is not, create the genesis event
- //
- if (!room.federation) {
- //
- // Create the room with everything
- //
-
- await doAfterCreateRoom(room, users, subscriptions);
- } else {
- //
- // Normalize the room's federation status
- //
-
- // Get the users domains
- const domainsAfterAdd = users.map((u) => u.federation.origin);
-
- //
- // Create the user add event
- //
-
- const normalizedSourceUser = normalizers.normalizeUser(addedUser);
- const normalizedSourceSubscription = normalizers.normalizeSubscription(subscription);
-
- const addUserEvent = await FederationRoomEvents.createAddUserEvent(Federation.domain, room._id, normalizedSourceUser, normalizedSourceSubscription, domainsAfterAdd);
-
- // Dispatch the events
- Federation.client.dispatchEvent(domainsAfterAdd, addUserEvent);
- }
- } catch (err) {
- // Remove the user subscription from the room
- Promise.await(Subscriptions.remove({ _id: subscription._id }));
-
- logger.client.error(() => `afterAddedToRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ err }`);
- }
-
- return involvedUsers;
-}
-
-export const definition = {
- hook: 'afterAddedToRoom',
- callback: (roomOwner, room) => Promise.await(afterAddedToRoom(roomOwner, room)),
- id: 'federation-after-added-to-room',
-};
diff --git a/app/federation/server/_client/callbacks/afterCreateDirectRoom.js b/app/federation/server/_client/callbacks/afterCreateDirectRoom.js
deleted file mode 100644
index ddfb2890259c..000000000000
--- a/app/federation/server/_client/callbacks/afterCreateDirectRoom.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import { logger } from '../../logger';
-import { FederationRoomEvents, Subscriptions } from '../../../../models/server';
-import { Federation } from '../../federation';
-import { normalizers } from '../../normalizers';
-import { deleteRoom } from '../../../../lib/server/functions';
-
-async function afterCreateDirectRoom(room, extras) {
- logger.client.debug(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } extras=${ JSON.stringify(extras, null, 2) }`);
-
- // If the room is federated, ignore
- if (room.federation) { return; }
-
- // Check if there is a federated user on this direct room
- const hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1);
-
- // If there are not federated users on this room, ignore it
- if (!hasFederatedUser) { return; }
-
- try {
- //
- // Genesis
- //
-
- // Normalize room
- const normalizedRoom = normalizers.normalizeRoom(room);
-
- // Ensure a genesis event for this room
- const genesisEvent = await FederationRoomEvents.createGenesisEvent(Federation.domain, normalizedRoom);
-
- //
- // Source User
- //
-
- // Add the source user to the room
- const sourceUser = extras.from;
- const normalizedSourceUser = normalizers.normalizeUser(sourceUser);
-
- const sourceSubscription = Subscriptions.findOne({ rid: normalizedRoom._id, 'u._id': normalizedSourceUser._id });
- const normalizedSourceSubscription = normalizers.normalizeSubscription(sourceSubscription);
-
- // Build the source user event
- const sourceUserEvent = await FederationRoomEvents.createAddUserEvent(Federation.domain, normalizedRoom._id, normalizedSourceUser, normalizedSourceSubscription);
-
- //
- // Target User
- //
-
- // Add the target user to the room
- const targetUser = extras.to;
- const normalizedTargetUser = normalizers.normalizeUser(targetUser);
-
- const targetSubscription = Subscriptions.findOne({ rid: normalizedRoom._id, 'u._id': normalizedTargetUser._id });
- const normalizedTargetSubscription = normalizers.normalizeSubscription(targetSubscription);
-
- // Dispatch the target user event
- const targetUserEvent = await FederationRoomEvents.createAddUserEvent(Federation.domain, normalizedRoom._id, normalizedTargetUser, normalizedTargetSubscription);
-
- // Dispatch the events
- Federation.client.dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, sourceUserEvent, targetUserEvent]);
- } catch (err) {
- Promise.await(deleteRoom(room._id));
-
- logger.client.error(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } => Could not create federated room: ${ err }`);
- }
-
- return room;
-}
-
-export const definition = {
- hook: 'afterCreateDirectRoom',
- callback: (room, extras) => Promise.await(afterCreateDirectRoom(room, extras)),
- id: 'federation-after-create-direct-room',
-};
diff --git a/app/federation/server/_client/callbacks/afterCreateRoom.js b/app/federation/server/_client/callbacks/afterCreateRoom.js
deleted file mode 100644
index d48fb59df8d3..000000000000
--- a/app/federation/server/_client/callbacks/afterCreateRoom.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { logger } from '../../logger';
-import { FederationRoomEvents, Subscriptions, Users } from '../../../../models/server';
-import { Federation } from '../../federation';
-import { normalizers } from '../../normalizers';
-import { deleteRoom } from '../../../../lib/server/functions';
-
-export async function doAfterCreateRoom(room, users, subscriptions) {
- //
- // Genesis
- //
-
- // Normalize room
- const normalizedRoom = normalizers.normalizeRoom(room, users);
-
- // Ensure a genesis event for this room
- const genesisEvent = await FederationRoomEvents.createGenesisEvent(Federation.domain, normalizedRoom);
-
- //
- // Add user events
- //
- const addUserEvents = [];
-
- for (const user of users) {
- /* eslint-disable no-await-in-loop */
-
- const subscription = subscriptions[user._id];
-
- const normalizedSourceUser = normalizers.normalizeUser(user);
- const normalizedSourceSubscription = normalizers.normalizeSubscription(subscription);
-
- const addUserEvent = await FederationRoomEvents.createAddUserEvent(Federation.domain, normalizedRoom._id, normalizedSourceUser, normalizedSourceSubscription);
-
- addUserEvents.push(addUserEvent);
-
- /* eslint-enable no-await-in-loop */
- }
-
- // Dispatch the events
- Federation.client.dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]);
-}
-
-async function afterCreateRoom(roomOwner, room) {
- // If the room is federated, ignore
- if (room.federation) { return; }
-
- // Find all subscriptions of this room
- let subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch();
- subscriptions = subscriptions.reduce((acc, s) => {
- acc[s.u._id] = s;
-
- return acc;
- }, {});
-
- // Get all user ids
- const userIds = Object.keys(subscriptions);
-
- // Load all the users
- const users = Users.findUsersWithUsernameByIds(userIds).fetch();
-
- // Check if there is a federated user on this room
- const hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1);
-
- // If there are not federated users on this room, ignore it
- if (!hasFederatedUser) { return; }
-
- logger.client.debug(() => `afterCreateRoom => roomOwner=${ JSON.stringify(roomOwner, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- try {
- await doAfterCreateRoom(room, users, subscriptions);
- } catch (err) {
- deleteRoom(room._id);
-
- logger.client.error(() => `afterCreateRoom => room=${ JSON.stringify(room, null, 2) } => Could not create federated room: ${ err }`);
- }
-
- return room;
-}
-
-export const definition = {
- hook: 'afterCreateRoom',
- callback: (roomOwner, room) => Promise.await(afterCreateRoom(roomOwner, room)),
- id: 'federation-after-create-room',
-};
diff --git a/app/federation/server/_client/callbacks/afterDeleteMessage.js b/app/federation/server/_client/callbacks/afterDeleteMessage.js
deleted file mode 100644
index 8777ca11f748..000000000000
--- a/app/federation/server/_client/callbacks/afterDeleteMessage.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { FederationRoomEvents, Rooms } from '../../../../models/server';
-import { logger } from '../../logger';
-import { Federation } from '../../federation';
-import { isFederated } from './helpers/federatedResources';
-
-async function afterDeleteMessage(message) {
- const room = Rooms.findOneById(message.rid);
-
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `afterDeleteMessage => message=${ JSON.stringify(message, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- // Create the delete message event
- const event = await FederationRoomEvents.createDeleteMessageEvent(Federation.domain, room._id, message._id);
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
-
- return message;
-}
-
-export const definition = {
- hook: 'afterDeleteMessage',
- callback: (message) => Promise.await(afterDeleteMessage(message)),
- id: 'federation-after-delete-message',
-};
diff --git a/app/federation/server/_client/callbacks/afterMuteUser.js b/app/federation/server/_client/callbacks/afterMuteUser.js
deleted file mode 100644
index 7141dc8b21d5..000000000000
--- a/app/federation/server/_client/callbacks/afterMuteUser.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { FederationRoomEvents } from '../../../../models/server';
-import { logger } from '../../logger';
-import { Federation } from '../../federation';
-import { normalizers } from '../../normalizers';
-import { isFederated } from './helpers/federatedResources';
-
-async function afterMuteUser(involvedUsers, room) {
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `afterMuteUser => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- const { mutedUser } = involvedUsers;
-
- // Create the mute user event
- const event = await FederationRoomEvents.createMuteUserEvent(Federation.domain, room._id, normalizers.normalizeUser(mutedUser));
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
-
- return involvedUsers;
-}
-
-export const definition = {
- hook: 'afterMuteUser',
- callback: (involvedUsers, room) => Promise.await(afterMuteUser(involvedUsers, room)),
- id: 'federation-after-mute-user',
-};
diff --git a/app/federation/server/_client/callbacks/afterRemoveFromRoom.js b/app/federation/server/_client/callbacks/afterRemoveFromRoom.js
deleted file mode 100644
index 02997d378fff..000000000000
--- a/app/federation/server/_client/callbacks/afterRemoveFromRoom.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { FederationRoomEvents } from '../../../../models/server';
-import { isFederated, getFederatedRoomData } from './helpers/federatedResources';
-import { logger } from '../../logger';
-import { normalizers } from '../../normalizers';
-import { Federation } from '../../federation';
-
-async function afterRemoveFromRoom(involvedUsers, room) {
- const { removedUser } = involvedUsers;
-
- // If there are not federated users on this room, ignore it
- if (!isFederated(room) && !isFederated(removedUser)) { return; }
-
- logger.client.debug(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- const { users } = getFederatedRoomData(room);
-
- try {
- // Get the domains after removal
- const domainsAfterRemoval = users.map((u) => u.federation.origin);
-
- //
- // Normalize the room's federation status
- //
- const usersBeforeRemoval = users;
- usersBeforeRemoval.push(removedUser);
-
- // Get the users domains
- const domainsBeforeRemoval = usersBeforeRemoval.map((u) => u.federation.origin);
-
- //
- // Create the user remove event
- //
- const normalizedSourceUser = normalizers.normalizeUser(removedUser);
-
- const removeUserEvent = await FederationRoomEvents.createRemoveUserEvent(Federation.domain, room._id, normalizedSourceUser, domainsAfterRemoval);
-
- // Dispatch the events
- Federation.client.dispatchEvent(domainsBeforeRemoval, removeUserEvent);
- } catch (err) {
- logger.client.error(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ err }`);
-
- throw err;
- }
-
- return involvedUsers;
-}
-
-export const definition = {
- hook: 'afterRemoveFromRoom',
- callback: (roomOwner, room) => Promise.await(afterRemoveFromRoom(roomOwner, room)),
- id: 'federation-after-remove-from-room',
-};
diff --git a/app/federation/server/_client/callbacks/afterSaveMessage.js b/app/federation/server/_client/callbacks/afterSaveMessage.js
deleted file mode 100644
index 738a3fad25a2..000000000000
--- a/app/federation/server/_client/callbacks/afterSaveMessage.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { logger } from '../../logger';
-import { FederationRoomEvents } from '../../../../models/server';
-import { Federation } from '../../federation';
-import { normalizers } from '../../normalizers';
-import { isFederated } from './helpers/federatedResources';
-
-async function afterSaveMessage(message, room) {
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `afterSaveMessage => message=${ JSON.stringify(message, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- let event;
-
- // If editedAt exists, it means it is an update
- if (message.editedAt) {
- // Create the edit message event
- event = await FederationRoomEvents.createEditMessageEvent(Federation.domain, room._id, normalizers.normalizeMessage(message));
- } else {
- // Create the message event
- event = await FederationRoomEvents.createMessageEvent(Federation.domain, room._id, normalizers.normalizeMessage(message));
- }
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
-
- return message;
-}
-
-export const definition = {
- hook: 'afterSaveMessage',
- callback: (message, room) => Promise.await(afterSaveMessage(message, room)),
- id: 'federation-after-save-message',
-};
diff --git a/app/federation/server/_client/callbacks/afterSetReaction.js b/app/federation/server/_client/callbacks/afterSetReaction.js
deleted file mode 100644
index 8dfb4dee4ec3..000000000000
--- a/app/federation/server/_client/callbacks/afterSetReaction.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import _ from 'underscore';
-
-import { FederationRoomEvents, Rooms } from '../../../../models/server';
-import { logger } from '../../logger';
-import { Federation } from '../../federation';
-import { isFederated } from './helpers/federatedResources';
-
-async function afterSetReaction(message, { user, reaction }) {
- const room = Rooms.findOneById(message.rid, { fields: { _id: 1, federation: 1 } });
-
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `afterSetReaction => message=${ JSON.stringify(_.pick(message, '_id', 'msg'), null, 2) } room=${ JSON.stringify(_.pick(room, '_id'), null, 2) } user=${ JSON.stringify(_.pick(user, 'username'), null, 2) } reaction=${ reaction }`);
-
- // Create the event
- const event = await FederationRoomEvents.createSetMessageReactionEvent(Federation.domain, room._id, message._id, user.username, reaction);
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
-
- return message;
-}
-
-export const definition = {
- hook: 'afterSetReaction',
- callback: (message, extras) => Promise.await(afterSetReaction(message, extras)),
- id: 'federation-after-set-reaction',
-};
diff --git a/app/federation/server/_client/callbacks/afterUnmuteUser.js b/app/federation/server/_client/callbacks/afterUnmuteUser.js
deleted file mode 100644
index d577e7d22d7a..000000000000
--- a/app/federation/server/_client/callbacks/afterUnmuteUser.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { FederationRoomEvents } from '../../../../models/server';
-import { logger } from '../../logger';
-import { Federation } from '../../federation';
-import { normalizers } from '../../normalizers';
-import { isFederated } from './helpers/federatedResources';
-
-async function afterUnmuteUser(involvedUsers, room) {
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `afterUnmuteUser => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
-
- const { unmutedUser } = involvedUsers;
-
- // Create the mute user event
- const event = await FederationRoomEvents.createUnmuteUserEvent(Federation.domain, room._id, normalizers.normalizeUser(unmutedUser));
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
-
- return involvedUsers;
-}
-
-export const definition = {
- hook: 'afterUnmuteUser',
- callback: (involvedUsers, room) => Promise.await(afterUnmuteUser(involvedUsers, room)),
- id: 'federation-after-unmute-user',
-};
diff --git a/app/federation/server/_client/callbacks/afterUnsetReaction.js b/app/federation/server/_client/callbacks/afterUnsetReaction.js
deleted file mode 100644
index 3acbb0213752..000000000000
--- a/app/federation/server/_client/callbacks/afterUnsetReaction.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import _ from 'underscore';
-
-import { FederationRoomEvents, Rooms } from '../../../../models/server';
-import { logger } from '../../logger';
-import { Federation } from '../../federation';
-import { isFederated } from './helpers/federatedResources';
-
-async function afterUnsetReaction(message, { user, reaction }) {
- const room = Rooms.findOneById(message.rid, { fields: { _id: 1, federation: 1 } });
-
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `afterUnsetReaction => message=${ JSON.stringify(_.pick(message, '_id', 'msg'), null, 2) } room=${ JSON.stringify(_.pick(room, '_id'), null, 2) } user=${ JSON.stringify(_.pick(user, 'username'), null, 2) } reaction=${ reaction }`);
-
- // Create the event
- const event = await FederationRoomEvents.createUnsetMessageReactionEvent(Federation.domain, room._id, message._id, user.username, reaction);
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
-
- return message;
-}
-
-export const definition = {
- hook: 'afterUnsetReaction',
- callback: (message, extras) => Promise.await(afterUnsetReaction(message, extras)),
- id: 'federation-after-unset-reaction',
-};
diff --git a/app/federation/server/_client/callbacks/beforeDeleteRoom.js b/app/federation/server/_client/callbacks/beforeDeleteRoom.js
deleted file mode 100644
index a0bf9668753d..000000000000
--- a/app/federation/server/_client/callbacks/beforeDeleteRoom.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { logger } from '../../logger';
-import { FederationRoomEvents, Rooms } from '../../../../models/server';
-import { Federation } from '../../federation';
-import { isFederated } from './helpers/federatedResources';
-
-async function beforeDeleteRoom(roomId) {
- const room = Rooms.findOneById(roomId, { fields: { _id: 1, federation: 1 } });
-
- // If room does not exist, skip
- if (!room) { return; }
-
- // If there are not federated users on this room, ignore it
- if (!isFederated(room)) { return; }
-
- logger.client.debug(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) }`);
-
- try {
- // Create the message event
- const event = await FederationRoomEvents.createDeleteRoomEvent(Federation.domain, room._id);
-
- // Dispatch event (async)
- Federation.client.dispatchEvent(room.federation.domains, event);
- } catch (err) {
- logger.client.error(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) } => Could not remove room: ${ err }`);
-
- throw err;
- }
-
- return roomId;
-}
-
-export const definition = {
- hook: 'beforeDeleteRoom',
- callback: (roomId) => Promise.await(beforeDeleteRoom(roomId)),
- id: 'federation-before-delete-room',
-};
diff --git a/app/federation/server/_client/callbacks/helpers/federatedResources.js b/app/federation/server/_client/callbacks/helpers/federatedResources.js
deleted file mode 100644
index fec8f1e05ab1..000000000000
--- a/app/federation/server/_client/callbacks/helpers/federatedResources.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Subscriptions, Users } from '../../../../../models/server';
-
-export const isFederated = (resource) => !!resource.federation;
-
-export const getFederatedRoomData = (room) => {
- let hasFederatedUser = false;
-
- let users = null;
- let subscriptions = null;
-
- if (room.t === 'd') {
- // Check if there is a federated user on this room
- hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1);
- } else {
- // Find all subscriptions of this room
- subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch();
- subscriptions = subscriptions.reduce((acc, s) => {
- acc[s.u._id] = s;
-
- return acc;
- }, {});
-
- // Get all user ids
- const userIds = Object.keys(subscriptions);
-
- // Load all the users
- users = Users.findUsersWithUsernameByIds(userIds).fetch();
-
- // Check if there is a federated user on this room
- hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1);
- }
-
- return {
- hasFederatedUser,
- users,
- subscriptions,
- };
-};
diff --git a/app/federation/server/_client/callbacks/helpers/getFederatedRoomData.js b/app/federation/server/_client/callbacks/helpers/getFederatedRoomData.js
deleted file mode 100644
index 11bb6baa6069..000000000000
--- a/app/federation/server/_client/callbacks/helpers/getFederatedRoomData.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Subscriptions, Users } from '../../../../../models/server';
-
-module.exports = (room) => {
- let hasFederatedUser = false;
-
- let users = null;
- let subscriptions = null;
-
- if (room.t === 'd') {
- // Check if there is a federated user on this room
- hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1);
- } else {
- // Find all subscriptions of this room
- subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch();
- subscriptions = subscriptions.reduce((acc, s) => {
- acc[s.u._id] = s;
-
- return acc;
- }, {});
-
- // Get all user ids
- const userIds = Object.keys(subscriptions);
-
- // Load all the users
- users = Users.findUsersWithUsernameByIds(userIds).fetch();
-
- // Check if there is a federated user on this room
- hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1);
- }
-
- return {
- hasFederatedUser,
- users,
- subscriptions,
- };
-};
diff --git a/app/federation/server/_client/callbacks/helpers/getFederatedUserData.js b/app/federation/server/_client/callbacks/helpers/getFederatedUserData.js
deleted file mode 100644
index b3950b10b6dc..000000000000
--- a/app/federation/server/_client/callbacks/helpers/getFederatedUserData.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = (user) => ({
- isFederated: user.username.indexOf('@') !== -1,
-});
diff --git a/app/federation/server/_client/client.js b/app/federation/server/_client/client.js
deleted file mode 100644
index cb105e2fde12..000000000000
--- a/app/federation/server/_client/client.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import qs from 'querystring';
-
-import { logger } from '../logger';
-import { Federation } from '../federation';
-// Callbacks
-import { definition as afterAddedToRoomDef } from './callbacks/afterAddedToRoom';
-import { definition as afterCreateDirectRoomDef } from './callbacks/afterCreateDirectRoom';
-import { definition as afterCreateRoomDef } from './callbacks/afterCreateRoom';
-import { definition as afterDeleteMessageDef } from './callbacks/afterDeleteMessage';
-import { definition as afterMuteUserDef } from './callbacks/afterMuteUser';
-import { definition as afterRemoveFromRoomDef } from './callbacks/afterRemoveFromRoom';
-import { definition as afterSaveMessageDef } from './callbacks/afterSaveMessage';
-import { definition as afterSetReactionDef } from './callbacks/afterSetReaction';
-import { definition as afterUnmuteUserDef } from './callbacks/afterUnmuteUser';
-import { definition as afterUnsetReactionDef } from './callbacks/afterUnsetReaction';
-import { definition as beforeDeleteRoomDef } from './callbacks/beforeDeleteRoom';
-import { callbacks } from '../../../callbacks';
-
-class Client {
- callbackDefinitions = [];
-
- register(callbackDefition) {
- this.callbackDefinitions.push(callbackDefition);
- }
-
- enableCallbacks() {
- for (const definition of this.callbackDefinitions) {
- callbacks.add(definition.hook, definition.callback, callbacks.priority.LOW, definition.id);
- }
- }
-
- disableCallbacks() {
- for (const definition of this.callbackDefinitions) {
- callbacks.remove(definition.hook, definition.id);
- }
- }
-
- searchUsers(query) {
- if (!Federation.enabled) {
- throw Federation.errors.disabled('client.searchUsers');
- }
-
- logger.client.debug(() => `searchUsers => query=${ query }`);
-
- const [username, peerDomain] = query.split('@');
-
- const uri = `/api/v1/federation.users.search?${ qs.stringify({ username, domain: peerDomain }) }`;
-
- const { data: { users } } = Federation.http.requestToPeer('GET', peerDomain, uri);
-
- return users;
- }
-
- getUserByUsername(query) {
- if (!Federation.enabled) {
- throw Federation.errors.disabled('client.searchUsers');
- }
-
- logger.client.debug(() => `getUserByUsername => query=${ query }`);
-
- const [username, peerDomain] = query.split('@');
-
- const uri = `/api/v1/federation.users.getByUsername?${ qs.stringify({ username }) }`;
-
- const { data: { user } } = Federation.http.requestToPeer('GET', peerDomain, uri);
-
- return user;
- }
-
- dispatchEvent(domains, event) {
- if (!Federation.enabled) {
- throw Federation.errors.disabled('client.dispatchEvent');
- }
-
- this.dispatchEvents(domains, [event]);
- }
-
- dispatchEvents(domains, events) {
- if (!Federation.enabled) {
- throw Federation.errors.disabled('client.dispatchEvents');
- }
-
- logger.client.debug(() => `dispatchEvents => domains=${ domains.join(', ') } events=${ events.map((e) => JSON.stringify(e, null, 2)) }`);
-
- const uri = '/api/v1/federation.events.dispatch';
-
- for (const domain of domains) {
- Federation.http.requestToPeer('POST', domain, uri, { events }, { ignoreErrors: true });
- }
- }
-
- requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) {
- if (!Federation.enabled) {
- throw Federation.errors.disabled('client.requestEventsFromLatest');
- }
-
- logger.client.debug(() => `requestEventsFromLatest => domain=${ domain } contextType=${ contextType } contextQuery=${ JSON.stringify(contextQuery, null, 2) } latestEventIds=${ latestEventIds.join(', ') }`);
-
- const uri = '/api/v1/federation.events.requestFromLatest';
-
- Federation.http.requestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds });
- }
-
- getUpload(domain, fileId) {
- const { data: { upload, buffer } } = Federation.http.requestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`);
-
- return { upload, buffer: Buffer.from(buffer) };
- }
-}
-
-export const client = new Client();
-
-client.register(afterAddedToRoomDef);
-client.register(afterCreateDirectRoomDef);
-client.register(afterCreateRoomDef);
-client.register(afterDeleteMessageDef);
-client.register(afterMuteUserDef);
-client.register(beforeDeleteRoomDef);
-client.register(afterSaveMessageDef);
-client.register(afterSetReactionDef);
-client.register(afterUnmuteUserDef);
-client.register(afterUnsetReactionDef);
-client.register(afterRemoveFromRoomDef);
diff --git a/app/federation/server/_client/index.js b/app/federation/server/_client/index.js
deleted file mode 100644
index 8bc664d04988..000000000000
--- a/app/federation/server/_client/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { client } from './client';
diff --git a/app/federation/server/_server/endpoints/events/dispatch.js b/app/federation/server/_server/endpoints/events/dispatch.js
deleted file mode 100644
index 76edaf8c205d..000000000000
--- a/app/federation/server/_server/endpoints/events/dispatch.js
+++ /dev/null
@@ -1,410 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { EJSON } from 'meteor/ejson';
-
-import { API } from '../../../../../api/server';
-import { Federation } from '../../../federation';
-import { logger } from '../../../logger';
-import { contextDefinitions, eventTypes } from '../../../../../models/server/models/FederationEvents';
-import {
- FederationRoomEvents, FederationServers,
- Messages,
- Rooms,
- Subscriptions,
- Users,
-} from '../../../../../models/server';
-import { normalizers } from '../../../normalizers';
-import { deleteRoom } from '../../../../../lib/server/functions';
-import { Notifications } from '../../../../../notifications/server';
-import { FileUpload } from '../../../../../file-upload';
-
-API.v1.addRoute('federation.events.dispatch', { authRequired: false }, {
- async post() {
- if (!Federation.enabled) {
- return API.v1.failure('Not found');
- }
-
- //
- // Decrypt the payload if needed
- const payload = Federation.crypt.decryptIfNeeded(this.request, this.bodyParams);
-
- //
- // Convert from EJSON
- const { events } = EJSON.fromJSONValue(payload);
-
- logger.server.debug(`federation.events.dispatch => events=${ events.map((e) => JSON.stringify(e, null, 2)) }`);
-
- // Loop over received events
- for (const event of events) {
- /* eslint-disable no-await-in-loop */
-
- let eventResult;
-
- switch (event.type) {
- //
- // PING
- //
- case eventTypes.PING:
- eventResult = {
- success: true,
- };
- break;
-
- //
- // GENESIS
- //
- case eventTypes.GENESIS:
- switch (event.data.contextType) {
- case contextDefinitions.ROOM.type:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { room } } = event;
-
- // Check if room exists
- const persistedRoom = Rooms.findOne({ _id: room._id });
-
- if (persistedRoom) {
- // Update the federation
- Rooms.update({ _id: persistedRoom._id }, { $set: { federation: room.federation } });
- } else {
- // Denormalize room
- const denormalizedRoom = normalizers.denormalizeRoom(room);
-
- // Create the room
- Rooms.insert(denormalizedRoom);
- }
- }
- break;
- }
- break;
-
- //
- // ROOM_DELETE
- //
- case eventTypes.ROOM_DELETE:
- const { data: { roomId } } = event;
-
- // Check if room exists
- const persistedRoom = Rooms.findOne({ _id: roomId });
-
- if (persistedRoom) {
- // Delete the room
- deleteRoom(roomId);
- }
-
- // Remove all room events
- await FederationRoomEvents.removeRoomEvents(roomId);
-
- eventResult = {
- success: true,
- };
-
- break;
-
- //
- // ROOM_ADD_USER
- //
- case eventTypes.ROOM_ADD_USER:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { roomId, user, subscription, domainsAfterAdd } } = event;
-
- // Check if user exists
- const persistedUser = Users.findOne({ _id: user._id });
-
- if (persistedUser) {
- // Update the federation
- Users.update({ _id: persistedUser._id }, { $set: { federation: user.federation } });
- } else {
- // Denormalize user
- const denormalizedUser = normalizers.denormalizeUser(user);
-
- // Create the user
- Users.insert(denormalizedUser);
- }
-
- // Check if subscription exists
- const persistedSubscription = Subscriptions.findOne({ _id: subscription._id });
-
- if (persistedSubscription) {
- // Update the federation
- Subscriptions.update({ _id: persistedSubscription._id }, { $set: { federation: subscription.federation } });
- } else {
- // Denormalize subscription
- const denormalizedSubscription = normalizers.denormalizeSubscription(subscription);
-
- // Create the subscription
- Subscriptions.insert(denormalizedSubscription);
- }
-
- // Refresh the servers list
- FederationServers.refreshServers();
-
- // Update the room's federation property
- Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } });
- }
- break;
-
- //
- // ROOM_REMOVE_USER
- //
- case eventTypes.ROOM_REMOVE_USER:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { roomId, user, domainsAfterRemoval } } = event;
-
- // Remove the user's subscription
- Subscriptions.removeByRoomIdAndUserId(roomId, user._id);
-
- // Refresh the servers list
- FederationServers.refreshServers();
-
- // Update the room's federation property
- Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } });
- }
- break;
-
- //
- // ROOM_MESSAGE
- //
- case eventTypes.ROOM_MESSAGE:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { message } } = event;
-
- // Check if message exists
- const persistedMessage = Messages.findOne({ _id: message._id });
-
- if (persistedMessage) {
- // Update the federation
- Messages.update({ _id: persistedMessage._id }, { $set: { federation: message.federation } });
- } else {
- // Update the subscription open status
- Subscriptions.update({ rid: message.rid, name: message.u.username }, { $set: { open: true, alert: true } });
-
- // Denormalize user
- const denormalizedMessage = normalizers.denormalizeMessage(message);
-
- // Is there a file?
- if (denormalizedMessage.file) {
- const fileStore = FileUpload.getStore('Uploads');
-
- const { federation: { origin } } = denormalizedMessage;
-
- const { upload, buffer } = Federation.client.getUpload(origin, denormalizedMessage.file._id);
-
- const oldUploadId = upload._id;
-
- // Normalize upload
- delete upload._id;
- upload.rid = denormalizedMessage.rid;
- upload.userId = denormalizedMessage.u._id;
- upload.federation = {
- _id: denormalizedMessage.file._id,
- origin,
- };
-
- Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer));
-
- // Update the message's file
- denormalizedMessage.file._id = upload._id;
-
- // Update the message's attachments
- for (const attachment of denormalizedMessage.attachments) {
- attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id);
- attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id);
- }
- }
-
- // Create the message
- Messages.insert(denormalizedMessage);
- }
- }
- break;
-
- //
- // ROOM_EDIT_MESSAGE
- //
- case eventTypes.ROOM_EDIT_MESSAGE:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { message } } = event;
-
- // Check if message exists
- const persistedMessage = Messages.findOne({ _id: message._id });
-
- if (!persistedMessage) {
- eventResult.success = false;
- eventResult.reason = 'missingMessageToEdit';
- } else {
- // Update the message
- Messages.update({ _id: persistedMessage._id }, { $set: { msg: message.msg, federation: message.federation } });
- }
- }
- break;
-
- //
- // ROOM_DELETE_MESSAGE
- //
- case eventTypes.ROOM_DELETE_MESSAGE:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { roomId, messageId } } = event;
-
- // Remove the message
- Messages.removeById(messageId);
-
- // Notify the room
- Notifications.notifyRoom(roomId, 'deleteMessage', { _id: messageId });
- }
- break;
-
- //
- // ROOM_SET_MESSAGE_REACTION
- //
- case eventTypes.ROOM_SET_MESSAGE_REACTION:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { messageId, username, reaction } } = event;
-
- // Get persisted message
- const persistedMessage = Messages.findOne({ _id: messageId });
-
- // Make sure reactions exist
- persistedMessage.reactions = persistedMessage.reactions || {};
-
- let reactionObj = persistedMessage.reactions[reaction];
-
- // If there are no reactions of that type, add it
- if (!reactionObj) {
- reactionObj = {
- usernames: [username],
- };
- } else {
- // Otherwise, add the username
- reactionObj.usernames.push(username);
- reactionObj.usernames = [...new Set(reactionObj.usernames)];
- }
-
- // Update the property
- Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } });
- }
- break;
-
- //
- // ROOM_UNSET_MESSAGE_REACTION
- //
- case eventTypes.ROOM_UNSET_MESSAGE_REACTION:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { messageId, username, reaction } } = event;
-
- // Get persisted message
- const persistedMessage = Messages.findOne({ _id: messageId });
-
- // Make sure reactions exist
- persistedMessage.reactions = persistedMessage.reactions || {};
-
- // If there are no reactions of that type, ignore
- if (!persistedMessage.reactions[reaction]) {
- continue;
- }
-
- const reactionObj = persistedMessage.reactions[reaction];
-
- // Get the username index on the list
- const usernameIdx = reactionObj.usernames.indexOf(username);
-
- // If the index is not found, ignore
- if (usernameIdx === -1) {
- continue;
- }
-
- // Remove the username from the given reaction
- reactionObj.usernames.splice(usernameIdx, 1);
-
- // If there are no more users for that reaction, remove the property
- if (reactionObj.usernames.length === 0) {
- Messages.update({ _id: messageId }, { $unset: { [`reactions.${ reaction }`]: 1 } });
- } else {
- // Otherwise, update the property
- Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } });
- }
- }
- break;
-
- //
- // ROOM_MUTE_USER
- //
- case eventTypes.ROOM_MUTE_USER:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { roomId, user } } = event;
-
- // Denormalize user
- const denormalizedUser = normalizers.denormalizeUser(user);
-
- // Mute user
- Rooms.muteUsernameByRoomId(roomId, denormalizedUser.username);
- }
- break;
-
- //
- // ROOM_UNMUTE_USER
- //
- case eventTypes.ROOM_UNMUTE_USER:
- eventResult = await FederationRoomEvents.addEvent(event.context, event);
-
- // If the event was successfully added, handle the event locally
- if (eventResult.success) {
- const { data: { roomId, user } } = event;
-
- // Denormalize user
- const denormalizedUser = normalizers.denormalizeUser(user);
-
- // Mute user
- Rooms.unmuteUsernameByRoomId(roomId, denormalizedUser.username);
- }
- break;
-
- //
- // Could not find event
- //
- default:
- continue;
- }
-
- // If there was an error handling the event, take action
- if (!eventResult.success) {
- logger.server.debug(`federation.events.dispatch => Event has missing parents -> event=${ JSON.stringify(event, null, 2) }`);
-
- Federation.client.requestEventsFromLatest(event.origin, Federation.domain, contextDefinitions.defineType(event), event.context, eventResult.latestEventIds);
-
- // And stop handling the events
- break;
- }
-
- /* eslint-enable no-await-in-loop */
- }
-
- // Respond
- return API.v1.success();
- },
-});
diff --git a/app/federation/server/_server/endpoints/events/index.js b/app/federation/server/_server/endpoints/events/index.js
deleted file mode 100644
index cd68ca667454..000000000000
--- a/app/federation/server/_server/endpoints/events/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './dispatch';
-import './requestFromLatest';
diff --git a/app/federation/server/_server/endpoints/events/requestFromLatest.js b/app/federation/server/_server/endpoints/events/requestFromLatest.js
deleted file mode 100644
index b61ec4d285d2..000000000000
--- a/app/federation/server/_server/endpoints/events/requestFromLatest.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { EJSON } from 'meteor/ejson';
-
-import { API } from '../../../../../api/server';
-import { Federation } from '../../../federation';
-import { logger } from '../../../logger';
-import { FederationRoomEvents } from '../../../../../models/server';
-
-API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, {
- async post() {
- if (!Federation.enabled) {
- return API.v1.failure('Not found');
- }
-
- //
- // Decrypt the payload if needed
- const payload = Federation.crypt.decryptIfNeeded(this.request, this.bodyParams);
-
- const { fromDomain, contextType, contextQuery, latestEventIds } = EJSON.fromJSONValue(payload);
-
- logger.server.debug(`federation.events.requestFromLatest => contextType=${ contextType } contextQuery=${ JSON.stringify(contextQuery, null, 2) } latestEventIds=${ latestEventIds.join(', ') }`);
-
- let EventsModel;
-
- // Define the model for the context
- switch (contextType) {
- case 'room':
- EventsModel = FederationRoomEvents;
- break;
- }
-
- let missingEvents = [];
-
- if (latestEventIds.length) {
- // Get the oldest event from the latestEventIds
- const oldestEvent = EventsModel.findOne({ _id: { $in: latestEventIds } }, { $sort: { timestamp: 1 } });
-
- if (!oldestEvent) {
- return;
- }
-
- // Get all the missing events on this context, after the oldest one
- missingEvents = EventsModel.find({ _id: { $nin: latestEventIds }, context: contextQuery, timestamp: { $gte: oldestEvent.timestamp } }, { sort: { timestamp: 1 } }).fetch();
- } else {
- // If there are no latest events, send all of them
- missingEvents = EventsModel.find({ context: contextQuery }, { sort: { timestamp: 1 } }).fetch();
- }
-
- // Dispatch all the events, on the same request
- Federation.client.dispatchEvents([fromDomain], missingEvents);
- },
-});
diff --git a/app/federation/server/_server/endpoints/users.js b/app/federation/server/_server/endpoints/users.js
deleted file mode 100644
index 78db94023d24..000000000000
--- a/app/federation/server/_server/endpoints/users.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { API } from '../../../../api';
-import { Users } from '../../../../models';
-import { Federation } from '../..';
-import { normalizers } from '../../normalizers';
-import { logger } from '../../logger';
-
-const userFields = { _id: 1, username: 1, type: 1, emails: 1, name: 1 };
-
-API.v1.addRoute('federation.users.search', { authRequired: false }, {
- get() {
- if (!Federation.enabled) {
- return API.v1.failure('Not found');
- }
-
- const { username, domain } = this.requestParams();
-
- logger.server.debug(`federation.users.search => username=${ username } domain=${ domain }`);
-
- const query = {
- type: 'user',
- $or: [
- { name: username },
- { username },
- { 'emails.address': `${ username }@${ domain }` },
- ],
- };
-
- let users = Users.find(query, { fields: userFields }).fetch();
-
- users = normalizers.normalizeAllUsers(users);
-
- return API.v1.success({ users });
- },
-});
-
-API.v1.addRoute('federation.users.getByUsername', { authRequired: false }, {
- get() {
- if (!Federation.enabled) {
- return API.v1.failure('Not found');
- }
-
- const { username } = this.requestParams();
-
- logger.server.debug(`federation.users.getByUsername => username=${ username }`);
-
- const query = {
- type: 'user',
- username,
- };
-
- let user = Users.findOne(query, { fields: userFields });
-
- user = normalizers.normalizeUser(user);
-
- return API.v1.success({ user });
- },
-});
diff --git a/app/federation/server/_server/index.js b/app/federation/server/_server/index.js
deleted file mode 100644
index 1199af15d79f..000000000000
--- a/app/federation/server/_server/index.js
+++ /dev/null
@@ -1 +0,0 @@
-import './server';
diff --git a/app/federation/server/_server/server.js b/app/federation/server/_server/server.js
deleted file mode 100644
index 739145a446bd..000000000000
--- a/app/federation/server/_server/server.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import './endpoints/events';
-import './endpoints/uploads';
-import './endpoints/users';
diff --git a/app/federation/server/config.js b/app/federation/server/config.js
new file mode 100644
index 000000000000..b8f1f2b12287
--- /dev/null
+++ b/app/federation/server/config.js
@@ -0,0 +1,74 @@
+import mem from 'mem';
+
+import { getWorkspaceAccessToken } from '../../cloud/server';
+import { FederationKeys } from '../../models/server';
+import { settings } from '../../settings/server';
+import * as SettingsUpdater from './settingsUpdater';
+import { logger } from './logger';
+
+const defaultConfig = {
+ hub: {
+ active: null,
+ url: null,
+ },
+ peer: {
+ uniqueId: null,
+ domain: null,
+ url: null,
+ public_key: null,
+ },
+ cloud: {
+ token: null,
+ },
+};
+
+const getConfigLocal = () => {
+ const _enabled = settings.get('FEDERATION_Enabled');
+
+ if (!_enabled) { return defaultConfig; }
+
+ // If it is enabled, check if the settings are there
+ const _uniqueId = settings.get('FEDERATION_Unique_Id');
+ const _domain = settings.get('FEDERATION_Domain');
+ const _discoveryMethod = settings.get('FEDERATION_Discovery_Method');
+ const _hubUrl = settings.get('FEDERATION_Hub_URL');
+ const _peerUrl = settings.get('Site_Url');
+
+ if (!_domain || !_discoveryMethod || !_hubUrl || !_peerUrl) {
+ SettingsUpdater.updateStatus('Could not enable, settings are not fully set');
+
+ logger.setup.error('Could not enable Federation, settings are not fully set');
+
+ return defaultConfig;
+ }
+
+ logger.setup.info('Updating settings...');
+
+ // Normalize the config values
+ return {
+ hub: {
+ active: _discoveryMethod === 'hub',
+ url: _hubUrl.replace(/\/+$/, ''),
+ },
+ peer: {
+ uniqueId: _uniqueId,
+ domain: _domain.replace('@', '').trim(),
+ url: _peerUrl.replace(/\/+$/, ''),
+ public_key: FederationKeys.getPublicKeyString(),
+ },
+ cloud: {
+ token: getWorkspaceAccessToken(),
+ },
+ };
+};
+
+export const getConfig = mem(getConfigLocal);
+
+const updateValue = () => mem.clear(getConfig);
+
+settings.get('FEDERATION_Enabled', updateValue);
+settings.get('FEDERATION_Unique_Id', updateValue);
+settings.get('FEDERATION_Domain', updateValue);
+settings.get('FEDERATION_Status', updateValue);
+settings.get('FEDERATION_Discovery_Method', updateValue);
+settings.get('FEDERATION_Hub_URL', updateValue);
diff --git a/app/federation/server/crypt.js b/app/federation/server/crypt.js
deleted file mode 100644
index 9ef942a753c7..000000000000
--- a/app/federation/server/crypt.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { FederationKeys } from '../../models/server';
-import { API } from '../../api/server';
-
-import { Federation } from '.';
-
-class Crypt {
- decryptIfNeeded(request, bodyParams) {
- //
- // Look for the domain that sent this event
- const remotePeerDomain = request.headers['x-federation-domain'];
-
- if (!remotePeerDomain) {
- return API.v1.failure('Domain is unknown, ignoring event');
- }
-
- let payload;
-
- //
- // Decrypt payload if needed
- if (remotePeerDomain !== Federation.domain) {
- //
- // Find the peer's public key
- const { publicKey: peerKey } = Federation.dns.search(remotePeerDomain);
-
- if (!peerKey) {
- return API.v1.failure("Could not find the peer's public key to decrypt");
- }
-
- payload = Federation.crypt.decrypt(bodyParams, peerKey);
- } else {
- payload = bodyParams;
- }
-
- return payload;
- }
-
- encrypt(data, peerKey) {
- if (!data) {
- return data;
- }
-
- // Encrypt with the peer's public key
- data = FederationKeys.loadKey(peerKey, 'public').encrypt(data);
-
- // Encrypt with the local private key
- return Federation.privateKey.encryptPrivate(data);
- }
-
- decrypt(data, peerKey) {
- //
- // Decrypt the payload
- const payloadBuffer = Buffer.from(data);
-
- // Decrypt with the peer's public key
- try {
- data = FederationKeys.loadKey(peerKey, 'public').decryptPublic(payloadBuffer);
-
- // Decrypt with the local private key
- data = Federation.privateKey.decrypt(data);
- } catch (err) {
- throw new Error('Could not decrypt');
- }
-
- return JSON.parse(data.toString());
- }
-}
-
-export const crypt = new Crypt();
diff --git a/app/federation/server/dns.js b/app/federation/server/dns.js
deleted file mode 100644
index 88715c45ceba..000000000000
--- a/app/federation/server/dns.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import dnsResolver from 'dns';
-
-import { Meteor } from 'meteor/meteor';
-
-import { logger } from './logger';
-
-import { Federation } from '.';
-
-const dnsResolveSRV = Meteor.wrapAsync(dnsResolver.resolveSrv);
-const dnsResolveTXT = Meteor.wrapAsync(dnsResolver.resolveTxt);
-
-class DNS {
- constructor() {
- this.hubUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat';
- }
-
- registerWithHub(peerDomain, url, publicKey) {
- const body = { domain: peerDomain, url, public_key: publicKey };
-
- try {
- // If there is no DNS entry for that, get from the Hub
- Federation.http.request('POST', `${ this.hubUrl }/api/v1/peers`, body);
-
- return true;
- } catch (err) {
- logger.dns.error(err);
-
- throw Federation.errors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub');
- }
- }
-
- searchHub(peerDomain) {
- try {
- // If there is no DNS entry for that, get from the Hub
- const { data: { peer } } = Federation.http.request('GET', `${ this.hubUrl }/api/v1/peers?search=${ peerDomain }`);
-
- if (!peer) {
- throw Federation.errors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub');
- }
-
- const { url, public_key: publicKey } = peer;
-
- return {
- url,
- peerDomain,
- publicKey,
- };
- } catch (err) {
- logger.dns.error(err);
-
- throw Federation.errors.peerNotFoundUsingDNS('dns.searchHub');
- }
- }
-
- search(peerDomain) {
- if (!Federation.enabled) {
- throw Federation.errors.disabled('dns.search');
- }
-
- logger.dns.debug(`search: ${ peerDomain }`);
-
- let srvEntries = [];
- let protocol = '';
-
- // Search by HTTPS first
- try {
- srvEntries = dnsResolveSRV(`_rocketchat._https.${ peerDomain }`);
- protocol = 'https';
- } catch (err) {
- // Ignore errors when looking for DNS entries
- }
-
- // If there is not entry, try with http
- if (!srvEntries.length) {
- try {
- srvEntries = dnsResolveSRV(`_rocketchat._http.${ peerDomain }`);
- protocol = 'http';
- } catch (err) {
- // Ignore errors when looking for DNS entries
- }
- }
-
- const [srvEntry] = srvEntries;
-
- // If there is no entry, throw error
- if (!srvEntry) {
- return this.searchHub(peerDomain);
- }
-
- // Get the public key from the TXT record
- const publicKeyTxtRecords = dnsResolveTXT(`rocketchat-public-key.${ peerDomain }`);
-
- // Join the TXT record, that might be split
- const publicKey = publicKeyTxtRecords[0].join('');
-
- // If there is no entry, throw error
- if (!publicKey) {
- return this.searchHub(peerDomain);
- }
-
- return {
- url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`,
- peerDomain,
- publicKey,
- };
- }
-}
-
-export const dns = new DNS();
diff --git a/app/federation/server/errors.js b/app/federation/server/errors.js
deleted file mode 100644
index 0b70b1f531a5..000000000000
--- a/app/federation/server/errors.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-export const disabled = (method) => new Meteor.Error('federation-error-disabled', 'Federation disabled', { method });
-export const userNotFound = (query) => new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ query }"`);
-export const peerNotFoundUsingDNS = (method) => new Meteor.Error('federation-error-peer-no-found-using-dns', 'Could not find the peer using DNS or Hub', { method });
-export const peerCouldNotBeRegisteredWithHub = (method) => new Meteor.Error('federation-error-peer-could-not-register-with-hub', 'Could not register the peer using the Hub', { method });
diff --git a/app/federation/server/eventCrypto.js b/app/federation/server/eventCrypto.js
deleted file mode 100644
index e01e8207d5dd..000000000000
--- a/app/federation/server/eventCrypto.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { FederationKeys } from '../../models/server';
-import { Federation } from './federation';
-
-export class EventCrypto {}
-
-EventCrypto.encrypt = (payload, remotePublicKey) => {
- // Encrypt with the remote public key
- payload = FederationKeys.loadKey(remotePublicKey, 'public').encrypt(payload);
-
- // Encrypt with the local private key
- return Federation.privateKey.encryptPrivate(payload);
-};
-
-EventCrypto.decrypt = (payload, remotePublicKey) => {
- const payloadBuffer = Buffer.from(payload);
-
- // Decrypt with the remote public key
- payload = FederationKeys.loadKey(remotePublicKey, 'public').decryptPublic(payloadBuffer);
-
- // Decrypt with the local private key
- return Federation.privateKey.decrypt(payload).toString();
-};
diff --git a/app/federation/server/federatedResources/FederatedMessage.js b/app/federation/server/federatedResources/FederatedMessage.js
new file mode 100644
index 000000000000..92d98a0ada21
--- /dev/null
+++ b/app/federation/server/federatedResources/FederatedMessage.js
@@ -0,0 +1,263 @@
+import { Meteor } from 'meteor/meteor';
+
+import { sendMessage, updateMessage } from '../../../lib';
+import { Messages, Rooms, Users } from '../../../models';
+import { FileUpload } from '../../../file-upload';
+import { FederatedResource } from './FederatedResource';
+import { FederatedRoom } from './FederatedRoom';
+import { FederatedUser } from './FederatedUser';
+
+import { Federation } from '..';
+
+export class FederatedMessage extends FederatedResource {
+ constructor(localPeerIdentifier, message) {
+ super('message');
+
+ if (!message) {
+ throw new Error('message param cannot be empty');
+ }
+
+ // Set local peer identifier to local object
+ this.localPeerIdentifier = localPeerIdentifier;
+
+ // Make sure room dates are correct
+ message.ts = new Date(message.ts);
+ message._updatedAt = new Date(message._updatedAt);
+
+ // Set the message author
+ if (message.u.federation) {
+ this.federatedAuthor = FederatedUser.loadByFederationId(localPeerIdentifier, message.u.federation._id);
+ } else {
+ const author = Users.findOneById(message.u._id);
+ this.federatedAuthor = new FederatedUser(localPeerIdentifier, author);
+ }
+
+ message.u = {
+ username: this.federatedAuthor.user.username,
+ federation: {
+ _id: this.federatedAuthor.user.federation._id,
+ },
+ };
+
+ // Set the room
+ const room = Rooms.findOneById(message.rid);
+
+ // Prepare the federation property
+ if (!message.federation) {
+ const federation = {
+ _id: message._id,
+ peer: localPeerIdentifier,
+ roomId: room.federation._id,
+ };
+
+ // Prepare the user
+ message.federation = federation;
+
+ // Update the user
+ Messages.update(message._id, { $set: { federation } });
+
+ // Prepare mentions
+ for (const mention of message.mentions) {
+ mention.federation = mention.federation || {};
+
+ if (mention.username.indexOf('@') === -1) {
+ mention.federation.peer = localPeerIdentifier;
+ } else {
+ const [username, peer] = mention.username.split('@');
+
+ mention.username = username;
+ mention.federation.peer = peer;
+ }
+ }
+
+ // Prepare channels
+ for (const channel of message.channels) {
+ channel.federation = channel.federation || {};
+
+ if (channel.name.indexOf('@') === -1) {
+ channel.federation.peer = localPeerIdentifier;
+ } else {
+ channel.name = channel.name.split('@')[0];
+ channel.federation.peer = channel.name.split('@')[1];
+ }
+ }
+ }
+
+ // Set message property
+ this.message = message;
+ }
+
+ getFederationId() {
+ return this.message.federation._id;
+ }
+
+ getMessage() {
+ return this.message;
+ }
+
+ getLocalMessage() {
+ this.log('getLocalMessage');
+
+ const { localPeerIdentifier, message } = this;
+
+ const localMessage = Object.assign({}, message);
+
+ // Make sure `u` is correct
+ if (!this.federatedAuthor) {
+ throw new Error('Author does not exist');
+ }
+
+ const localAuthor = this.federatedAuthor.getLocalUser();
+
+ localMessage.u = {
+ _id: localAuthor._id,
+ username: localAuthor.username,
+ };
+
+ // Make sure `rid` is correct
+ const federatedRoom = FederatedRoom.loadByFederationId(localPeerIdentifier, message.federation.roomId);
+
+ if (!federatedRoom) {
+ throw new Error('Room does not exist');
+ }
+
+ const localRoom = federatedRoom.getLocalRoom();
+
+ localMessage.rid = localRoom._id;
+
+ return localMessage;
+ }
+
+ create() {
+ this.log('create');
+
+ // Get the local message object
+ const localMessageObject = this.getLocalMessage();
+
+ // Grab the federation id
+ const { federation: { _id: federationId } } = localMessageObject;
+
+ // Check if the message exists
+ let localMessage = Messages.findOne({ 'federation._id': federationId });
+
+ // Create if needed
+ if (!localMessage) {
+ delete localMessageObject._id;
+
+ localMessage = localMessageObject;
+
+ const localRoom = Rooms.findOneById(localMessage.rid);
+
+ // Normalize mentions
+ for (const mention of localMessage.mentions) {
+ // Ignore if we are dealing with all, here or rocket.cat
+ if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { continue; }
+
+ let usernameToReplace = '';
+
+ if (mention.federation.peer !== this.localPeerIdentifier) {
+ usernameToReplace = mention.username;
+
+ mention.username = `${ mention.username }@${ mention.federation.peer }`;
+ } else {
+ usernameToReplace = `${ mention.username }@${ mention.federation.peer }`;
+ }
+
+ localMessage.msg = localMessage.msg.split(usernameToReplace).join(mention.username);
+ }
+
+ // Normalize channels
+ for (const channel of localMessage.channels) {
+ if (channel.federation.peer !== this.localPeerIdentifier) {
+ channel.name = `${ channel.name }@${ channel.federation.peer }`;
+ }
+ }
+
+ // Is there a file?
+ if (localMessage.file) {
+ const fileStore = FileUpload.getStore('Uploads');
+
+ const { federation: { peer: identifier } } = localMessage;
+
+ const { upload, buffer } = Federation.peerClient.getUpload({ identifier, localMessage });
+
+ const oldUploadId = upload._id;
+
+ // Normalize upload
+ delete upload._id;
+ upload.rid = localMessage.rid;
+ upload.userId = localMessage.u._id;
+ upload.federation = {
+ _id: localMessage.file._id,
+ peer: identifier,
+ };
+
+ Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer));
+
+ // Update the message's file
+ localMessage.file._id = upload._id;
+
+ // Update the message's attachments
+ for (const attachment of localMessage.attachments) {
+ attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id);
+ attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id);
+ }
+ }
+
+ // Create the message
+ const { _id } = sendMessage(localMessage.u, localMessage, localRoom, false);
+
+ localMessage._id = _id;
+ }
+
+ return localMessage;
+ }
+
+ update(updatedByFederatedUser) {
+ this.log('update');
+
+ // Get the original message
+ const originalMessage = Messages.findOne({ 'federation._id': this.getFederationId() });
+
+ // Error if message does not exist
+ if (!originalMessage) {
+ throw new Error('Message does not exist');
+ }
+
+ // Get the local message object
+ const localMessage = this.getLocalMessage();
+
+ // Make sure the message has the correct _id
+ localMessage._id = originalMessage._id;
+
+ // Get the user who updated
+ const user = updatedByFederatedUser.getLocalUser();
+
+ // Update the message
+ updateMessage(localMessage, user, originalMessage);
+
+ return localMessage;
+ }
+}
+
+FederatedMessage.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) {
+ const localMessage = Messages.findOne({ 'federation._id': federationId });
+
+ if (!localMessage) { return; }
+
+ return new FederatedMessage(localPeerIdentifier, localMessage);
+};
+
+FederatedMessage.loadOrCreate = function loadOrCreate(localPeerIdentifier, message) {
+ const { federation } = message;
+
+ if (federation) {
+ const federatedMessage = FederatedMessage.loadByFederationId(localPeerIdentifier, federation._id);
+
+ if (federatedMessage) {
+ return federatedMessage;
+ }
+ }
+
+ return new FederatedMessage(localPeerIdentifier, message);
+};
diff --git a/app/federation/server/federatedResources/FederatedResource.js b/app/federation/server/federatedResources/FederatedResource.js
new file mode 100644
index 000000000000..7ecdb9ec1cd3
--- /dev/null
+++ b/app/federation/server/federatedResources/FederatedResource.js
@@ -0,0 +1,17 @@
+import { logger } from '../logger';
+
+export class FederatedResource {
+ constructor(name) {
+ this.resourceName = `federated-${ name }`;
+
+ this.log('Creating federated resource');
+ }
+
+ log(message) {
+ FederatedResource.log(this.resourceName, message);
+ }
+}
+
+FederatedResource.log = function log(name, message) {
+ logger.resource.info(`[${ name }] ${ message }`);
+};
diff --git a/app/federation/server/federatedResources/FederatedRoom.js b/app/federation/server/federatedResources/FederatedRoom.js
new file mode 100644
index 000000000000..494edb1f96c5
--- /dev/null
+++ b/app/federation/server/federatedResources/FederatedRoom.js
@@ -0,0 +1,275 @@
+import { FederatedResource } from './FederatedResource';
+import { FederatedUser } from './FederatedUser';
+import { createRoom } from '../../../lib';
+import { Rooms, Subscriptions, Users } from '../../../models';
+
+
+export class FederatedRoom extends FederatedResource {
+ constructor(localPeerIdentifier, room, extras = {}) {
+ super('room');
+
+ if (!room) {
+ throw new Error('room param cannot be empty');
+ }
+
+ this.localPeerIdentifier = localPeerIdentifier;
+
+ // Make sure room dates are correct
+ room.ts = new Date(room.ts);
+ room._updatedAt = new Date(room._updatedAt);
+
+ // Set the name
+ if (room.t !== 'd' && room.name.indexOf('@') === -1) {
+ room.name = `${ room.name }@${ localPeerIdentifier }`;
+ }
+
+ // Set the federated owner, if there is one
+ const { owner } = extras;
+
+ if (owner) {
+ this.federatedOwner = FederatedUser.loadOrCreate(localPeerIdentifier, owner);
+ } else if (!owner && room.federation && room.federation.ownerId) {
+ this.federatedOwner = FederatedUser.loadByFederationId(localPeerIdentifier, room.federation.ownerId);
+ }
+
+ // Set base federation
+ room.federation = room.federation || {
+ _id: room._id,
+ peer: localPeerIdentifier,
+ ownerId: this.federatedOwner ? this.federatedOwner.getFederationId() : null,
+ };
+
+ // Keep room's owner id
+ this.federationOwnerId = room.federation && room.federation.ownerId;
+
+ // Set room property
+ this.room = room;
+ }
+
+ getFederationId() {
+ return this.room.federation._id;
+ }
+
+ getPeers() {
+ return this.room.federation.peers;
+ }
+
+ getRoom() {
+ return this.room;
+ }
+
+ getOwner() {
+ return this.federatedOwner ? this.federatedOwner.getUser() : null;
+ }
+
+ getUsers() {
+ return this.federatedUsers.map((u) => u.getUser());
+ }
+
+ loadUsers() {
+ const { room } = this;
+
+ // Get all room users
+ const users = FederatedRoom.loadRoomUsers(room);
+
+ this.setUsers(users);
+ }
+
+ setUsers(users) {
+ const { localPeerIdentifier } = this;
+
+ // Initialize federatedUsers
+ this.federatedUsers = [];
+
+ for (const user of users) {
+ const federatedUser = FederatedUser.loadOrCreate(localPeerIdentifier, user);
+
+ // Set owner if it does not exist
+ if (!this.federatedOwner && user._id === this.federationOwnerId) {
+ this.federatedOwner = federatedUser;
+ this.room.federation.ownerId = this.federatedOwner.getFederationId();
+ }
+
+ // Keep the federated user
+ this.federatedUsers.push(federatedUser);
+ }
+ }
+
+ refreshFederation() {
+ const { room } = this;
+
+ // Prepare the federated users
+ let federation = {
+ peers: [],
+ users: [],
+ };
+
+ // Check all the peers
+ for (const federatedUser of this.federatedUsers) {
+ // Add federation data to the room
+ const { user: { federation: { _id, peer } } } = federatedUser;
+
+ federation.peers.push(peer);
+ federation.users.push({ _id, peer });
+ }
+
+ federation.peers = [...new Set(federation.peers)];
+
+ federation = Object.assign(room.federation || {}, federation);
+
+ // Prepare the room
+ room.federation = federation;
+
+ // Update the room
+ Rooms.update(room._id, { $set: { federation } });
+ }
+
+ getLocalRoom() {
+ this.log('getLocalRoom');
+
+ const { localPeerIdentifier, room, room: { federation } } = this;
+
+ const localRoom = Object.assign({}, room);
+
+ if (federation.peer === localPeerIdentifier) {
+ if (localRoom.t !== 'd') {
+ localRoom.name = room.name.split('@')[0];
+ }
+ }
+
+ return localRoom;
+ }
+
+ createUsers() {
+ this.log('createUsers');
+
+ const { federatedUsers } = this;
+
+ // Create, if needed, all room's users
+ for (const federatedUser of federatedUsers) {
+ federatedUser.create();
+ }
+ }
+
+ create(alertAndOpen = false) {
+ this.log('create');
+
+ // Get the local room object (with or without suffixes)
+ const localRoomObject = this.getLocalRoom();
+
+ // Grab the federation id
+ const { federation: { _id: federationId } } = localRoomObject;
+
+ // Check if the user exists
+ let localRoom = FederatedRoom.loadByFederationId(this.localPeerIdentifier, federationId);
+
+ // Create if needed
+ if (!localRoom) {
+ delete localRoomObject._id;
+
+ localRoom = localRoomObject;
+
+ const { t: type, name, broadcast, customFields, federation, sysMes } = localRoom;
+ const { federatedOwner, federatedUsers } = this;
+
+ // Get usernames for the owner and members
+ const ownerUsername = federatedOwner.user.username;
+ const members = [];
+
+ if (type !== 'd') {
+ for (const federatedUser of federatedUsers) {
+ const localUser = federatedUser.getLocalUser();
+ members.push(localUser.username);
+ }
+ } else {
+ for (const federatedUser of federatedUsers) {
+ const localUser = federatedUser.getLocalUser();
+ members.push(localUser);
+ }
+ }
+
+ // Is this a broadcast channel? Then mute everyone but the owner
+ let muted = [];
+
+ if (broadcast) {
+ muted = members.filter((u) => u !== ownerUsername);
+ }
+
+ // Set the extra data and create room options
+ let extraData = {
+ federation,
+ };
+
+ let createRoomOptions = {
+ subscriptionExtra: {
+ alert: alertAndOpen,
+ open: alertAndOpen,
+ },
+ };
+
+ if (type !== 'd') {
+ extraData = Object.assign(extraData, {
+ broadcast,
+ customFields,
+ encrypted: false, // Always false for now
+ muted,
+ sysMes,
+ });
+
+ createRoomOptions = Object.assign(extraData, {
+ nameValidationRegex: '^[0-9a-zA-Z-_.@]+$',
+ subscriptionExtra: {
+ alert: true,
+ },
+ });
+ }
+
+ // Create the room
+ // !!!! Forcing direct or private only, no public rooms for now
+ const { rid } = createRoom(type === 'd' ? type : 'p', name, ownerUsername, members, false, extraData, createRoomOptions);
+
+ localRoom._id = rid;
+ }
+
+ return localRoom;
+ }
+}
+
+FederatedRoom.loadByFederationId = function _loadByFederationId(localPeerIdentifier, federationId) {
+ const localRoom = Rooms.findOne({ 'federation._id': federationId });
+
+ if (!localRoom) { return; }
+
+ return new FederatedRoom(localPeerIdentifier, localRoom);
+};
+
+FederatedRoom.loadRoomUsers = function _loadRoomUsers(room) {
+ const subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id, { fields: { 'u._id': 1 } }).fetch();
+ const userIds = subscriptions.map((s) => s.u._id);
+ return Users.findUsersWithUsernameByIds(userIds).fetch();
+};
+
+FederatedRoom.isFederated = function _isFederated(localPeerIdentifier, room, options = {}) {
+ this.log('federated-room', `${ room._id } - isFederated?`);
+
+ let isFederated = false;
+
+ if (options.checkUsingUsers) {
+ // Get all room users
+ const users = FederatedRoom.loadRoomUsers(room);
+
+ // Check all the users
+ for (const user of users) {
+ if (user.federation && user.federation.peer !== localPeerIdentifier) {
+ isFederated = true;
+ break;
+ }
+ }
+ } else {
+ isFederated = room.federation && room.federation.peers.length > 1;
+ }
+
+ this.log('federated-room', `${ room._id } - isFederated? ${ isFederated ? 'yes' : 'no' }`);
+
+ return isFederated;
+};
diff --git a/app/federation/server/federatedResources/FederatedUser.js b/app/federation/server/federatedResources/FederatedUser.js
new file mode 100644
index 000000000000..5b21c5fd07f3
--- /dev/null
+++ b/app/federation/server/federatedResources/FederatedUser.js
@@ -0,0 +1,125 @@
+import { FederatedResource } from './FederatedResource';
+import { Users } from '../../../models';
+
+
+export class FederatedUser extends FederatedResource {
+ constructor(localPeerIdentifier, user) {
+ super('user');
+
+ if (!user) {
+ throw new Error('user param cannot be empty');
+ }
+
+ this.localPeerIdentifier = localPeerIdentifier;
+
+ // Make sure all properties are normalized
+ // Prepare the federation property
+ if (!user.federation) {
+ const federation = {
+ _id: user._id,
+ peer: localPeerIdentifier,
+ };
+
+ // Prepare the user
+ user.federation = federation;
+
+ // Update the user
+ Users.update(user._id, { $set: { federation } });
+ }
+
+ // Make sure user dates are correct
+ user.createdAt = new Date(user.createdAt);
+ user.lastLogin = new Date(user.lastLogin);
+ user._updatedAt = new Date(user._updatedAt);
+
+ // Delete sensitive data as well
+ delete user.roles;
+ delete user.services;
+
+ // Make sure some other properties are ready
+ user.name = user.name;
+ user.username = user.username.indexOf('@') === -1 ? `${ user.username }@${ user.federation.peer }` : user.username;
+ user.roles = ['user'];
+ user.status = 'online';
+ user.statusConnection = 'online';
+ user.type = 'user';
+
+ // Set user property
+ this.user = user;
+ }
+
+ getFederationId() {
+ return this.user.federation._id;
+ }
+
+ getUser() {
+ return this.user;
+ }
+
+ getLocalUser() {
+ this.log('getLocalUser');
+
+ const { localPeerIdentifier, user, user: { federation } } = this;
+
+ const localUser = Object.assign({}, user);
+
+ if (federation.peer === localPeerIdentifier || user.username === 'rocket.cat') {
+ localUser.username = user.username.split('@')[0];
+ localUser.name = user.name.split('@')[0];
+ }
+ if (federation.peer !== localPeerIdentifier) {
+ localUser.isRemote = true;
+ }
+
+ return localUser;
+ }
+
+ create() {
+ this.log('create');
+
+ // Get the local user object (with or without suffixes)
+ const localUserObject = this.getLocalUser();
+
+ // Grab the federation id
+ const { federation: { _id: federationId } } = localUserObject;
+
+ // Check if the user exists
+ let localUser = Users.findOne({ 'federation._id': federationId });
+
+ // Create if needed
+ if (!localUser) {
+ delete localUserObject._id;
+
+ localUser = localUserObject;
+
+ localUser._id = Users.create(localUserObject);
+ }
+
+ // Update the id
+ this.user._id = localUser._id;
+
+ return localUser;
+ }
+}
+
+FederatedUser.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) {
+ const localUser = Users.findOne({ 'federation._id': federationId });
+
+ if (!localUser) { return; }
+
+ return new FederatedUser(localPeerIdentifier, localUser);
+};
+
+FederatedUser.loadOrCreate = function loadOrCreate(localPeerIdentifier, user) {
+ const { federation } = user;
+
+ if (federation) {
+ const federatedUser = FederatedUser.loadByFederationId(localPeerIdentifier, federation._id);
+
+ if (federatedUser) {
+ return federatedUser;
+ }
+ }
+
+ return new FederatedUser(localPeerIdentifier, user);
+};
diff --git a/app/federation/server/federatedResources/index.js b/app/federation/server/federatedResources/index.js
new file mode 100644
index 000000000000..90d98b351cab
--- /dev/null
+++ b/app/federation/server/federatedResources/index.js
@@ -0,0 +1,4 @@
+export { FederatedMessage } from './FederatedMessage';
+export { FederatedResource } from './FederatedResource';
+export { FederatedRoom } from './FederatedRoom';
+export { FederatedUser } from './FederatedUser';
diff --git a/app/federation/server/federationSettings.js b/app/federation/server/federation-settings.js
similarity index 67%
rename from app/federation/server/federationSettings.js
rename to app/federation/server/federation-settings.js
index 4ff82fbfd5ce..385d077af5f0 100644
--- a/app/federation/server/federationSettings.js
+++ b/app/federation/server/federation-settings.js
@@ -1,11 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings';
-import { FederationKeys } from '../../models/server';
+import { FederationKeys } from '../../models';
Meteor.startup(function() {
+ // const federationUniqueId = FederationKeys.getUniqueId();
const federationPublicKey = FederationKeys.getPublicKeyString();
+ const defaultHubURL = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat';
+
settings.addGroup('Federation', function() {
this.add('FEDERATION_Enabled', false, {
type: 'boolean',
@@ -15,12 +18,19 @@ Meteor.startup(function() {
public: true,
});
- this.add('FEDERATION_Status', 'Disabled', {
+ this.add('FEDERATION_Status', '-', {
readonly: true,
type: 'string',
i18nLabel: 'FEDERATION_Status',
});
+ // this.add('FEDERATION_Unique_Id', federationUniqueId, {
+ // readonly: true,
+ // type: 'string',
+ // i18nLabel: 'FEDERATION_Unique_Id',
+ // i18nDescription: 'FEDERATION_Unique_Id_Description',
+ // });
+
this.add('FEDERATION_Domain', '', {
type: 'string',
i18nLabel: 'FEDERATION_Domain',
@@ -37,6 +47,13 @@ Meteor.startup(function() {
i18nDescription: 'FEDERATION_Public_Key_Description',
});
+ this.add('FEDERATION_Hub_URL', defaultHubURL, {
+ group: 'Federation Hub',
+ type: 'string',
+ i18nLabel: 'FEDERATION_Hub_URL',
+ i18nDescription: 'FEDERATION_Hub_URL_Description',
+ });
+
this.add('FEDERATION_Discovery_Method', 'dns', {
type: 'select',
values: [{
diff --git a/app/federation/server/federation.js b/app/federation/server/federation.js
deleted file mode 100644
index 7d4a135ac037..000000000000
--- a/app/federation/server/federation.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import './federationSettings';
-import { Meteor } from 'meteor/meteor';
-import _ from 'underscore';
-
-import { settings } from '../../settings';
-import { logger } from './logger';
-import * as errors from './errors';
-import { addUser } from './methods/addUser';
-import { searchUsers } from './methods/searchUsers';
-import { dns } from './dns';
-import { http } from './http';
-import { client } from './_client';
-import { crypt } from './crypt';
-import { FederationKeys } from '../../models/server';
-import { updateStatus, updateEnabled } from './settingsUpdater';
-import './_server';
-
-import './methods/testSetup';
-
-// Export Federation object
-export const Federation = {
- enabled: false,
- domain: '',
-
- errors,
-
- client,
- dns,
- http,
- crypt,
-};
-
-// Add Federation methods
-Federation.methods = {
- addUser,
- searchUsers,
-};
-
-// Create key pair if needed
-if (!FederationKeys.getPublicKey()) {
- FederationKeys.generateKeys();
-}
-
-const updateSettings = _.debounce(Meteor.bindEnvironment(function() {
- Federation.domain = settings.get('FEDERATION_Domain').replace('@', '');
- Federation.discoveryMethod = settings.get('FEDERATION_Discovery_Method');
-
- // Get the key pair
- Federation.privateKey = FederationKeys.getPrivateKey();
- Federation.publicKey = FederationKeys.getPublicKey();
-
- if (Federation.discoveryMethod === 'hub') {
- // Register with hub
- try {
- Federation.dns.registerWithHub(Federation.domain, settings.get('Site_Url'), FederationKeys.getPublicKeyString());
- } catch (err) {
- // Disable federation
- updateEnabled(false);
-
- updateStatus('Could not register with Hub');
- }
- } else {
- updateStatus('Enabled');
- }
-}), 150);
-
-function enableOrDisable() {
- Federation.enabled = settings.get('FEDERATION_Enabled');
-
- logger.setup.info(`Federation is ${ Federation.enabled ? 'enabled' : 'disabled' }`);
-
- if (Federation.enabled) {
- updateSettings();
-
- Federation.client.enableCallbacks();
- } else {
- updateStatus('Disabled');
-
- Federation.client.disableCallbacks();
- }
-
- Federation.enabled && updateSettings();
-}
-
-// Add settings listeners
-settings.get('FEDERATION_Enabled', enableOrDisable);
-settings.get('FEDERATION_Domain', updateSettings);
-settings.get('FEDERATION_Discovery_Method', updateSettings);
diff --git a/app/federation/server/http.js b/app/federation/server/http.js
deleted file mode 100644
index d73b7bb5bebf..000000000000
--- a/app/federation/server/http.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { HTTP as MeteorHTTP } from 'meteor/http';
-import { EJSON } from 'meteor/ejson';
-
-import { logger } from './logger';
-
-import { Federation } from '.';
-
-class HTTP {
- requestToPeer(method, peerDomain, uri, body, options = {}) {
- const ignoreErrors = peerDomain === Federation.domain ? false : options.ignoreErrors;
-
- const { url: baseUrl, publicKey } = Federation.dns.search(peerDomain);
-
- let peerKey = null;
-
- // Only encrypt if it is not local
- if (peerDomain !== Federation.domain) {
- peerKey = publicKey;
- }
-
- let result;
-
- try {
- result = this.request(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey);
- } catch (err) {
- logger.http.error(`${ ignoreErrors ? '[IGNORED] ' : '' }Error ${ err }`);
-
- if (!ignoreErrors) {
- throw err;
- } else {
- return { success: false };
- }
- }
-
- return { success: true, data: result.data };
- }
-
- request(method, url, body, headers, peerKey = null) {
- let data = null;
-
- if ((method === 'POST' || method === 'PUT') && body) {
- data = EJSON.toJSONValue(body);
-
- if (peerKey) {
- data = Federation.crypt.encrypt(data, peerKey);
- }
- }
-
- logger.http.debug(`[${ method }] ${ url }`);
-
- return MeteorHTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': Federation.domain } });
- }
-}
-
-export const http = new HTTP();
diff --git a/app/federation/server/index.js b/app/federation/server/index.js
index 3be3dfaf85af..f0c6844e86a1 100644
--- a/app/federation/server/index.js
+++ b/app/federation/server/index.js
@@ -1 +1,149 @@
-export { Federation } from './federation';
+import { Meteor } from 'meteor/meteor';
+import { _ } from 'meteor/underscore';
+
+import './federation-settings';
+import { logger } from './logger';
+import { PeerClient } from './PeerClient';
+import { PeerDNS } from './PeerDNS';
+import { PeerHTTP } from './PeerHTTP';
+import { PeerPinger } from './PeerPinger';
+import { PeerServer } from './PeerServer';
+import * as SettingsUpdater from './settingsUpdater';
+import './methods/dashboard';
+import { addUser } from './methods/addUser';
+import { searchUsers } from './methods/searchUsers';
+import { ping } from './methods/ping';
+import { FederationKeys } from '../../models';
+import { settings } from '../../settings';
+import { getConfig } from './config';
+
+const peerClient = new PeerClient();
+const peerDNS = new PeerDNS();
+const peerHTTP = new PeerHTTP();
+const peerPinger = new PeerPinger();
+const peerServer = new PeerServer();
+
+export const Federation = {
+ enabled: false,
+ privateKey: null,
+ publicKey: null,
+ usingHub: null,
+ uniqueId: null,
+ localIdentifier: null,
+
+ peerClient,
+ peerDNS,
+ peerHTTP,
+ peerPinger,
+ peerServer,
+};
+
+// Add Federation methods
+Federation.methods = {
+ addUser,
+ searchUsers,
+ ping,
+};
+
+// Generate keys
+
+// Create unique id if needed
+if (!FederationKeys.getUniqueId()) {
+ FederationKeys.generateUniqueId();
+}
+
+// Create key pair if needed
+if (!FederationKeys.getPublicKey()) {
+ FederationKeys.generateKeys();
+}
+
+// Initializations
+
+// Start the client, setting up all the callbacks
+peerClient.start();
+
+// Start the server, setting up all the endpoints
+peerServer.start();
+
+// Start the pinger, to check the status of all peers
+peerPinger.start();
+
+const updateSettings = _.debounce(Meteor.bindEnvironment(function() {
+ const _enabled = settings.get('FEDERATION_Enabled');
+
+ if (!_enabled) { return; }
+
+ const config = getConfig();
+
+ // If the settings are correctly set, let's update the configuration
+
+ // Get the key pair
+ Federation.privateKey = FederationKeys.getPrivateKey();
+ Federation.publicKey = FederationKeys.getPublicKey();
+
+ // Set important information
+ Federation.enabled = true;
+ Federation.usingHub = config.hub.active;
+ Federation.uniqueId = config.peer.uniqueId;
+ Federation.localIdentifier = config.peer.domain;
+
+ // Set DNS
+ peerDNS.setConfig(config);
+
+ // Set HTTP
+ peerHTTP.setConfig(config);
+
+ // Set Client
+ peerClient.setConfig(config);
+ peerClient.enable();
+
+ // Set server
+ peerServer.setConfig(config);
+ peerServer.enable();
+
+ // Register the client
+ if (peerClient.register()) {
+ SettingsUpdater.updateStatus('Running');
+ } else {
+ SettingsUpdater.updateNextStatusTo('Disabled, could not register with Hub');
+ SettingsUpdater.updateEnabled(false);
+ }
+}), 150);
+
+function enableOrDisable() {
+ const _enabled = settings.get('FEDERATION_Enabled');
+
+ // If it was enabled, and was disabled now,
+ // make sure we disable everything: callbacks and endpoints
+ if (Federation.enabled && !_enabled) {
+ peerClient.disable();
+ peerServer.disable();
+
+ // Disable federation
+ Federation.enabled = false;
+
+ SettingsUpdater.updateStatus('Disabled');
+
+ logger.setup.info('Shutting down...');
+
+ return;
+ }
+
+ // If not enabled, skip
+ if (!_enabled) {
+ SettingsUpdater.updateStatus('Disabled');
+ return;
+ }
+
+ logger.setup.info('Booting...');
+
+ SettingsUpdater.updateStatus('Booting...');
+
+ updateSettings();
+}
+
+// Add settings listeners
+settings.get('FEDERATION_Enabled', enableOrDisable);
+settings.get('FEDERATION_Domain', updateSettings);
+settings.get('FEDERATION_Discovery_Method', updateSettings);
+settings.get('FEDERATION_Hub_URL', updateSettings);
diff --git a/app/federation/server/logger.js b/app/federation/server/logger.js
index 6530fdd2e3c2..77ffd9c4ab81 100644
--- a/app/federation/server/logger.js
+++ b/app/federation/server/logger.js
@@ -2,10 +2,12 @@ import { Logger } from '../../logger';
export const logger = new Logger('Federation', {
sections: {
- client: 'client',
- dns: 'dns',
- http: 'http',
- server: 'server',
+ resource: 'Resource',
setup: 'Setup',
+ peerClient: 'Peer Client',
+ peerServer: 'Peer Server',
+ dns: 'DNS',
+ http: 'HTTP',
+ pinger: 'Pinger',
},
});
diff --git a/app/federation/server/methods/addUser.js b/app/federation/server/methods/addUser.js
index 715e13833b4b..9a0d7a3ceaa4 100644
--- a/app/federation/server/methods/addUser.js
+++ b/app/federation/server/methods/addUser.js
@@ -1,35 +1,50 @@
import { Meteor } from 'meteor/meteor';
-import { FederationServers, Users } from '../../../models';
+import { Users, FederationPeers } from '../../../models';
import { Federation } from '..';
-export function addUser(query) {
+import { logger } from '../logger';
+
+export function addUser(identifier) {
if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUser' });
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' });
+ }
+
+ if (!Federation.peerServer.enabled) {
+ throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'Federation.addUser' });
}
- const user = Federation.client.getUserByUsername(query);
+ // Make sure the federated user still exists, and get the unique one, by email address
+ const [federatedUser] = Federation.peerClient.findUsers(identifier, { usernameOnly: true });
- if (!user) {
- throw Federation.errors.userNotFound(query);
+ if (!federatedUser) {
+ throw new Meteor.Error('federation-invalid-user', 'There is no user to add.');
}
- let userId = user._id;
+ let user = null;
+
+ const localUser = federatedUser.getLocalUser();
+
+ localUser.name += `@${ federatedUser.user.federation.peer }`;
+
+ // Delete the _id
+ delete localUser._id;
try {
// Create the local user
- userId = Users.create(user);
+ user = Users.create(localUser);
- // Refresh the servers list
- FederationServers.refreshServers();
+ // Refresh the peers list
+ FederationPeers.refreshPeers();
} catch (err) {
- // This might get called twice by the createDirectMessage method
- // so we need to handle the situation accordingly
- if (err.code !== 11000) {
- throw err;
+ // If the user already exists, return the existing user
+ if (err.code === 11000) {
+ user = Users.findOne({ 'federation._id': localUser.federation._id });
}
+
+ logger.error(err);
}
- return Users.findOne({ _id: userId });
+ return user;
}
diff --git a/app/federation/server/methods/dashboard.js b/app/federation/server/methods/dashboard.js
index 1ffb2367a65d..d272991cd641 100644
--- a/app/federation/server/methods/dashboard.js
+++ b/app/federation/server/methods/dashboard.js
@@ -1,13 +1,17 @@
import { Meteor } from 'meteor/meteor';
+import moment from 'moment';
-import { FederationServers, FederationRoomEvents, Users } from '../../../models/server';
+// We do not import the whole Federation object here because statistics cron
+// job use this file, and some of the features are not available on the cron
+import { FederationEvents, FederationPeers, Users } from '../../../models/server';
export function getStatistics() {
- const numberOfEvents = FederationRoomEvents.find().count();
+ const numberOfEvents = FederationEvents.findByType('png').count();
const numberOfFederatedUsers = Users.findRemote().count();
- const numberOfServers = FederationServers.find().count();
+ const numberOfActivePeers = FederationPeers.findActiveRemote().count();
+ const numberOfInactivePeers = FederationPeers.findNotActiveRemote().count();
- return { numberOfEvents, numberOfFederatedUsers, numberOfServers };
+ return { numberOfEvents, numberOfFederatedUsers, numberOfActivePeers, numberOfInactivePeers };
}
export function federationGetOverviewData() {
@@ -15,7 +19,7 @@ export function federationGetOverviewData() {
throw new Meteor.Error('not-authorized');
}
- const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = getStatistics();
+ const { numberOfEvents, numberOfFederatedUsers, numberOfActivePeers, numberOfInactivePeers } = getStatistics();
return {
data: [{
@@ -25,25 +29,48 @@ export function federationGetOverviewData() {
title: 'Number_of_federated_users',
value: numberOfFederatedUsers,
}, {
- title: 'Number_of_federated_servers',
- value: numberOfServers,
+ title: 'Number_of_active_peers',
+ value: numberOfActivePeers,
+ }, {
+ title: 'Number_of_inactive_peers',
+ value: numberOfInactivePeers,
}],
};
}
-export function federationGetServers() {
+export function federationGetPeerStatuses() {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized');
}
- const servers = FederationServers.find().fetch();
+ const peers = FederationPeers.findRemote().fetch();
+
+ const peerStatuses = [];
+
+ const stabilityLimit = moment().subtract(5, 'days');
+
+ for (const { peer, active, last_seen_at: lastSeenAt, last_failure_at: lastFailureAt } of peers) {
+ let status = 'failing';
+
+ if (active && lastFailureAt && moment(lastFailureAt).isAfter(stabilityLimit)) {
+ status = 'unstable';
+ } else if (active) {
+ status = 'stable';
+ }
+
+ peerStatuses.push({
+ peer,
+ status,
+ statusAt: active ? lastSeenAt : lastFailureAt,
+ });
+ }
return {
- data: servers,
+ data: peerStatuses,
};
}
Meteor.methods({
'federation:getOverviewData': federationGetOverviewData,
- 'federation:getServers': federationGetServers,
+ 'federation:getPeerStatuses': federationGetPeerStatuses,
});
diff --git a/app/federation/server/methods/loadContextEvents.js b/app/federation/server/methods/loadContextEvents.js
deleted file mode 100644
index c68700d3ab43..000000000000
--- a/app/federation/server/methods/loadContextEvents.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasRole } from '../../../authorization/server';
-import { FederationRoomEvents } from '../../../models/server';
-
-Meteor.methods({
- 'federation:loadContextEvents': (latestEventTimestamp) => {
- if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'loadContextEvents' });
- }
-
- if (!hasRole(Meteor.userId(), 'admin')) {
- throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'loadContextEvents' });
- }
-
- return FederationRoomEvents.find({ timestamp: { $gt: new Date(latestEventTimestamp) } }, { sort: { timestamp: 1 } }).fetch();
- },
-});
diff --git a/app/federation/server/methods/ping.js b/app/federation/server/methods/ping.js
new file mode 100644
index 000000000000..30cace3ad961
--- /dev/null
+++ b/app/federation/server/methods/ping.js
@@ -0,0 +1,54 @@
+import { Meteor } from 'meteor/meteor';
+
+import { FederationEvents } from '../../../models';
+import { settings } from '../../../settings';
+import { delay } from '../PeerHTTP/utils';
+
+export function ping(peers, timeToWait = 5000) {
+ // Create the ping events
+ const pingEvents = FederationEvents.ping(peers);
+
+ // Make sure timeToWait is at least one second
+ timeToWait = timeToWait < 1000 ? 1000 : timeToWait;
+
+ const results = {};
+
+ while (timeToWait > 0) {
+ timeToWait -= 500;
+ delay(500);
+
+ for (const { _id: pingEventId } of pingEvents) {
+ // Get the ping event
+ const pingEvent = FederationEvents.findOne({ _id: pingEventId });
+
+ if (!pingEvent.fulfilled && !pingEvent.error) { continue; }
+
+ // If there is an error or the event is fulfilled, it means it is already handled.
+ // Given that, fulfilled will be true if everything went well, or false if there was an error;
+ results[pingEvent.peer] = pingEvent.fulfilled;
+ }
+
+ // If we already have all the results, break
+ if (Object.keys(results).length === peers.length) {
+ break;
+ }
+ }
+
+ return results;
+}
+
+Meteor.methods({
+ FEDERATION_Test_Setup() {
+ const localPeer = settings.get('FEDERATION_Domain');
+
+ const results = ping([localPeer]);
+
+ if (!results[localPeer]) {
+ throw new Meteor.Error('FEDERATION_Test_Setup_Error');
+ }
+
+ return {
+ message: 'FEDERATION_Test_Setup_Success',
+ };
+ },
+});
diff --git a/app/federation/server/methods/searchUsers.js b/app/federation/server/methods/searchUsers.js
index c94eac019615..b27d411213b0 100644
--- a/app/federation/server/methods/searchUsers.js
+++ b/app/federation/server/methods/searchUsers.js
@@ -2,18 +2,20 @@ import { Meteor } from 'meteor/meteor';
import { Federation } from '..';
-import { normalizers } from '../normalizers';
-
-export function searchUsers(query) {
+export function searchUsers(identifier) {
if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'searchUsers' });
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' });
+ }
+
+ if (!Federation.peerClient.enabled) {
+ throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' });
}
- const users = Federation.client.searchUsers(query);
+ const federatedUsers = Federation.peerClient.findUsers(identifier);
- if (!users.length) {
- throw Federation.errors.userNotFound(query);
+ if (!federatedUsers.length) {
+ throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ identifier }"`);
}
- return normalizers.denormalizeAllUsers(users);
+ return federatedUsers;
}
diff --git a/app/federation/server/methods/testSetup.js b/app/federation/server/methods/testSetup.js
deleted file mode 100644
index d97ac9a006e0..000000000000
--- a/app/federation/server/methods/testSetup.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { eventTypes } from '../../../models/server/models/FederationEvents';
-import { Federation } from '../federation';
-
-Meteor.methods({
- FEDERATION_Test_Setup() {
- try {
- Federation.client.dispatchEvent([Federation.domain], {
- type: eventTypes.PING,
- });
-
- return {
- message: 'FEDERATION_Test_Setup_Success',
- };
- } catch (err) {
- throw new Meteor.Error('FEDERATION_Test_Setup_Error');
- }
- },
-});
diff --git a/app/federation/server/normalizers/helpers/federatedResources.js b/app/federation/server/normalizers/helpers/federatedResources.js
deleted file mode 100644
index a364444c2599..000000000000
--- a/app/federation/server/normalizers/helpers/federatedResources.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const getNameAndDomain = (fullyQualifiedName) => fullyQualifiedName.split('@');
-export const isFullyQualified = (name) => name.indexOf('@') !== -1;
diff --git a/app/federation/server/normalizers/index.js b/app/federation/server/normalizers/index.js
deleted file mode 100644
index ae1e2183626b..000000000000
--- a/app/federation/server/normalizers/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import message from './message';
-import room from './room';
-import subscription from './subscription';
-import user from './user';
-
-export const normalizers = {
- ...message,
- ...room,
- ...subscription,
- ...user,
-};
diff --git a/app/federation/server/normalizers/message.js b/app/federation/server/normalizers/message.js
deleted file mode 100644
index 83bb41c27795..000000000000
--- a/app/federation/server/normalizers/message.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Federation } from '../index';
-import { getNameAndDomain, isFullyQualified } from './helpers/federatedResources';
-
-const denormalizeMessage = (originalResource) => {
- const resource = { ...originalResource };
-
- const [username, domain] = getNameAndDomain(resource.u.username);
-
- // Denormalize username
- resource.u.username = domain === Federation.domain ? username : resource.u.username;
-
- // Denormalize mentions
- for (const mention of resource.mentions) {
- // Ignore if we are dealing with all, here or rocket.cat
- if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { continue; }
-
- const [username, domain] = getNameAndDomain(mention.username);
-
- if (domain === Federation.domain) {
- const originalUsername = mention.username;
-
- mention.username = username;
-
- resource.msg = resource.msg.split(originalUsername).join(username);
- }
- }
-
- // Denormalize channels
- for (const channel of resource.channels) {
- // Ignore if we are dealing with all, here or rocket.cat
- if (['all', 'here', 'rocket.cat'].indexOf(channel.name) !== -1) { continue; }
-
- const [username, domain] = getNameAndDomain(channel.name);
-
- if (domain === Federation.domain) {
- const originalUsername = channel.name;
-
- channel.name = username;
-
- resource.msg = resource.msg.split(originalUsername).join(username);
- }
- }
-
- return resource;
-};
-
-const denormalizeAllMessages = (resources) => resources.map(denormalizeMessage);
-
-const normalizeMessage = (originalResource) => {
- const resource = { ...originalResource };
-
- resource.u.username = !isFullyQualified(resource.u.username) ? `${ resource.u.username }@${ Federation.domain }` : resource.u.username;
-
- // Federation
- resource.federation = resource.federation || {
- origin: Federation.domain, // The origin of this resource, where it was created
- };
-
- // Normalize mentions
- for (const mention of resource.mentions) {
- // Ignore if we are dealing with all, here or rocket.cat
- if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { continue; }
-
- if (!isFullyQualified(mention.username)) {
- const originalUsername = mention.username;
-
- mention.username = `${ mention.username }@${ Federation.domain }`;
-
- resource.msg = resource.msg.split(originalUsername).join(mention.username);
- }
- }
-
- // Normalize channels
- for (const channel of resource.channels) {
- if (!isFullyQualified(channel.name)) {
- const originalUsername = channel.name;
-
- channel.name = `${ channel.name }@${ Federation.domain }`;
-
- resource.msg = resource.msg.split(originalUsername).join(channel.name);
- }
- }
-
- return resource;
-};
-
-const normalizeAllMessages = (resources) => resources.map(normalizeMessage);
-
-export default {
- denormalizeMessage,
- denormalizeAllMessages,
- normalizeMessage,
- normalizeAllMessages,
-};
diff --git a/app/federation/server/normalizers/room.js b/app/federation/server/normalizers/room.js
deleted file mode 100644
index 6930f4f7e34a..000000000000
--- a/app/federation/server/normalizers/room.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { Federation } from '../index';
-import { getNameAndDomain, isFullyQualified } from './helpers/federatedResources';
-
-const denormalizeRoom = (originalResource) => {
- const resource = { ...originalResource };
-
- if (resource.t === 'd') {
- resource.usernames = resource.usernames.map((u) => {
- const [username, domain] = getNameAndDomain(u);
-
- return domain === Federation.domain ? username : u;
- });
- } else {
- // Denormalize room name
- const [roomName, roomDomain] = getNameAndDomain(resource.name);
-
- resource.name = roomDomain === Federation.domain ? roomName : resource.name;
-
- // Denormalize room owner name
- const [username, userDomain] = getNameAndDomain(resource.u.username);
-
- resource.u.username = userDomain === Federation.domain ? username : resource.u.username;
-
- // Denormalize muted users
- if (resource.muted) {
- resource.muted = resource.muted.map((u) => {
- const [username, domain] = getNameAndDomain(u);
-
- return domain === Federation.domain ? username : u;
- });
- }
-
- // Denormalize unmuted users
- if (resource.unmuted) {
- resource.unmuted = resource.unmuted.map((u) => {
- const [username, domain] = getNameAndDomain(u);
-
- return domain === Federation.unmuted ? username : u;
- });
- }
- }
-
- return resource;
-};
-
-const normalizeRoom = (originalResource, users) => {
- const resource = { ...originalResource };
-
- let domains = '';
-
- if (resource.t === 'd') {
- // Handle user names, adding the Federation domain to local users
- resource.usernames = resource.usernames.map((u) => (!isFullyQualified(u) ? `${ u }@${ Federation.domain }` : u));
-
- // Get the domains of the usernames
- domains = resource.usernames.map((u) => getNameAndDomain(u)[1]);
- } else {
- // Ensure private
- resource.t = 'p';
-
- // Normalize room name
- resource.name = !isFullyQualified(resource.name) ? `${ resource.name }@${ Federation.domain }` : resource.name;
-
- // Get the users domains
- domains = users.map((u) => u.federation.origin);
-
- // Normalize the username
- resource.u.username = !isFullyQualified(resource.u.username) ? `${ resource.u.username }@${ Federation.domain }` : resource.u.username;
-
- // Normalize the muted users
- if (resource.muted) {
- resource.muted = resource.muted.map((u) => (!isFullyQualified(u) ? `${ u }@${ Federation.domain }` : u));
- }
-
- // Normalize the unmuted users
- if (resource.unmuted) {
- resource.unmuted = resource.unmuted.map((u) => (!isFullyQualified(u) ? `${ u }@${ Federation.domain }` : u));
- }
- }
-
- // Federation
- resource.federation = resource.federation || {
- origin: Federation.domain, // The origin of this resource, where it was created
- domains, // The domains where this room exist (or will exist)
- };
-
- return resource;
-};
-
-export default {
- denormalizeRoom,
- normalizeRoom,
-};
diff --git a/app/federation/server/normalizers/subscription.js b/app/federation/server/normalizers/subscription.js
deleted file mode 100644
index 130c29be1b37..000000000000
--- a/app/federation/server/normalizers/subscription.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Federation } from '../index';
-import { getNameAndDomain, isFullyQualified } from './helpers/federatedResources';
-
-const denormalizeSubscription = (originalResource) => {
- const resource = { ...originalResource };
-
- const [username, domain] = getNameAndDomain(resource.u.username);
-
- resource.u.username = domain === Federation.domain ? username : resource.u.username;
-
- const [nameUsername, nameDomain] = getNameAndDomain(resource.name);
-
- resource.name = nameDomain === Federation.domain ? nameUsername : resource.name;
-
- return resource;
-};
-
-const denormalizeAllSubscriptions = (resources) => resources.map(denormalizeSubscription);
-
-const normalizeSubscription = (originalResource) => {
- const resource = { ...originalResource };
-
- resource.u.username = !isFullyQualified(resource.u.username) ? `${ resource.u.username }@${ Federation.domain }` : resource.u.username;
-
- resource.name = !isFullyQualified(resource.name) ? `${ resource.name }@${ Federation.domain }` : resource.name;
-
- // Federation
- resource.federation = resource.federation || {
- origin: Federation.domain, // The origin of this resource, where it was created
- };
-
- return resource;
-};
-
-const normalizeAllSubscriptions = (resources) => resources.map(normalizeSubscription);
-
-export default {
- denormalizeSubscription,
- denormalizeAllSubscriptions,
- normalizeSubscription,
- normalizeAllSubscriptions,
-};
diff --git a/app/federation/server/normalizers/user.js b/app/federation/server/normalizers/user.js
deleted file mode 100644
index 5c98feed32f4..000000000000
--- a/app/federation/server/normalizers/user.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import _ from 'underscore';
-
-import { Federation } from '../index';
-import { Users } from '../../../models/server';
-import { getNameAndDomain, isFullyQualified } from './helpers/federatedResources';
-
-const denormalizeUser = (originalResource) => {
- const resource = { ...originalResource };
-
- resource.emails = [{
- address: resource.federation.originalInfo.email,
- }];
-
- const [username, domain] = getNameAndDomain(resource.username);
-
- resource.username = domain === Federation.domain ? username : resource.username;
-
- return resource;
-};
-
-const denormalizeAllUsers = (resources) => resources.map(denormalizeUser);
-
-const normalizeUser = (originalResource) => {
- // Get only what we need, non-sensitive data
- const resource = _.pick(originalResource, '_id', 'username', 'type', 'emails', 'name', 'federation', 'isRemote', 'createdAt', '_updatedAt');
-
- const email = resource.emails[0].address;
-
- resource.emails = [{
- address: `${ resource._id }@${ Federation.domain }`,
- }];
-
- resource.active = true;
- resource.roles = ['user'];
- resource.status = 'online';
- resource.username = !isFullyQualified(resource.username) ? `${ resource.username }@${ Federation.domain }` : resource.username;
-
- // Federation
- resource.federation = resource.federation || {
- origin: Federation.domain,
- originalInfo: {
- email,
- },
- };
-
- resource.isRemote = resource.federation.origin !== Federation.domain;
-
- // Persist the normalization
- Users.update({ _id: resource._id }, { $set: { isRemote: resource.isRemote, federation: resource.federation } });
-
- return resource;
-};
-
-const normalizeAllUsers = (resources) => resources.map(normalizeUser);
-
-export default {
- denormalizeUser,
- denormalizeAllUsers,
- normalizeUser,
- normalizeAllUsers,
-};
diff --git a/app/federation/server/settingsUpdater.js b/app/federation/server/settingsUpdater.js
index 5e485d1b4b42..344d6545e954 100644
--- a/app/federation/server/settingsUpdater.js
+++ b/app/federation/server/settingsUpdater.js
@@ -1,7 +1,15 @@
import { Settings } from '../../models';
+let nextStatus;
+
export function updateStatus(status) {
- Settings.updateValueById('FEDERATION_Status', status);
+ Settings.updateValueById('FEDERATION_Status', nextStatus || status);
+
+ nextStatus = null;
+}
+
+export function updateNextStatusTo(status) {
+ nextStatus = status;
}
export function updateEnabled(enabled) {
diff --git a/app/lib/server/functions/createDirectRoom.js b/app/lib/server/functions/createDirectRoom.js
deleted file mode 100644
index 67d516adff7d..000000000000
--- a/app/lib/server/functions/createDirectRoom.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Rooms, Subscriptions } from '../../../models/server';
-
-export const createDirectRoom = function(source, target, extraData, options) {
- const rid = [source._id, target._id].sort().join('');
-
- Rooms.upsert({ _id: rid }, {
- $setOnInsert: Object.assign({
- t: 'd',
- usernames: [source.username, target.username],
- msgs: 0,
- ts: new Date(),
- }, extraData),
- });
-
- Subscriptions.upsert({ rid, 'u._id': target._id }, {
- $setOnInsert: Object.assign({
- name: source.username,
- t: 'd',
- open: true,
- alert: true,
- unread: 0,
- u: {
- _id: target._id,
- username: target.username,
- },
- }, options.subscriptionExtra),
- });
-
- Subscriptions.upsert({ rid, 'u._id': source._id }, {
- $setOnInsert: Object.assign({
- name: target.username,
- t: 'd',
- open: true,
- alert: true,
- unread: 0,
- u: {
- _id: source._id,
- username: source.username,
- },
- }, options.subscriptionExtra),
- });
-
- return {
- _id: rid,
- t: 'd',
- };
-};
diff --git a/app/lib/server/functions/createRoom.js b/app/lib/server/functions/createRoom.js
index 116c6f5e92dc..31eb76285e17 100644
--- a/app/lib/server/functions/createRoom.js
+++ b/app/lib/server/functions/createRoom.js
@@ -7,7 +7,52 @@ import { callbacks } from '../../../callbacks';
import { addUserRoles } from '../../../authorization';
import { getValidRoomName } from '../../../utils';
import { Apps } from '../../../apps/server';
-import { createDirectRoom } from './createDirectRoom';
+
+function createDirectRoom(source, target, extraData, options) {
+ const rid = [source._id, target._id].sort().join('');
+
+ Rooms.upsert({ _id: rid }, {
+ $setOnInsert: Object.assign({
+ t: 'd',
+ usernames: [source.username, target.username],
+ msgs: 0,
+ ts: new Date(),
+ }, extraData),
+ });
+
+ Subscriptions.upsert({ rid, 'u._id': target._id }, {
+ $setOnInsert: Object.assign({
+ name: source.username,
+ t: 'd',
+ open: true,
+ alert: true,
+ unread: 0,
+ u: {
+ _id: target._id,
+ username: target.username,
+ },
+ }, options.subscriptionExtra),
+ });
+
+ Subscriptions.upsert({ rid, 'u._id': source._id }, {
+ $setOnInsert: Object.assign({
+ name: target.username,
+ t: 'd',
+ open: true,
+ alert: true,
+ unread: 0,
+ u: {
+ _id: source._id,
+ username: source.username,
+ },
+ }, options.subscriptionExtra),
+ });
+
+ return {
+ _id: rid,
+ t: 'd',
+ };
+}
export const createRoom = function(type, name, owner, members, readOnly, extraData = {}, options = {}) {
if (type === 'd') {
diff --git a/app/lib/server/functions/deleteRoom.js b/app/lib/server/functions/deleteRoom.js
index a00d11d0acfa..8d113906d8b8 100644
--- a/app/lib/server/functions/deleteRoom.js
+++ b/app/lib/server/functions/deleteRoom.js
@@ -4,7 +4,6 @@ import { callbacks } from '../../../callbacks';
export const deleteRoom = function(rid) {
Messages.removeFilesByRoomId(rid);
Messages.removeByRoomId(rid);
- callbacks.run('beforeDeleteRoom', rid);
Subscriptions.removeByRoomId(rid);
callbacks.run('afterDeleteRoom', rid);
return Rooms.removeById(rid);
diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js
index 5f127edb1bf5..e40b6c88f491 100644
--- a/app/lib/server/functions/deleteUser.js
+++ b/app/lib/server/functions/deleteUser.js
@@ -2,24 +2,17 @@ import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { FileUpload } from '../../../file-upload';
-import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } from '../../../models';
+import { Users, Subscriptions, Messages, Rooms, Integrations, FederationPeers } from '../../../models';
import { hasRole, getUsersInRole } from '../../../authorization';
import { settings } from '../../../settings';
import { Notifications } from '../../../notifications';
+import { getConfig } from '../../../federation/server/config';
export const deleteUser = function(userId) {
const user = Users.findOneById(userId, {
- fields: { username: 1, avatarOrigin: 1, federation: 1 },
+ fields: { username: 1, avatarOrigin: 1 },
});
- if (user.federation) {
- const existingSubscriptions = Subscriptions.find({ 'u._id': user._id }).count();
-
- if (existingSubscriptions > 0) {
- throw new Meteor.Error('FEDERATION_Error_user_is_federated_on_rooms');
- }
- }
-
// Users without username can't do anything, so there is nothing to remove
if (user.username != null) {
const roomCache = [];
@@ -106,6 +99,7 @@ export const deleteUser = function(userId) {
Users.removeById(userId); // Remove user from users database
- // Refresh the servers list
- FederationServers.refreshServers();
+ // Refresh the peers list
+ const { peer: { domain: localPeerDomain } } = getConfig();
+ FederationPeers.refreshPeers(localPeerDomain);
};
diff --git a/app/lib/server/functions/index.js b/app/lib/server/functions/index.js
index f318ad2ccbb1..7a94e02f36fc 100644
--- a/app/lib/server/functions/index.js
+++ b/app/lib/server/functions/index.js
@@ -6,7 +6,6 @@ export { checkEmailAvailability } from './checkEmailAvailability';
export { checkUsernameAvailability } from './checkUsernameAvailability';
export { cleanRoomHistory } from './cleanRoomHistory';
export { createRoom } from './createRoom';
-export { createDirectRoom } from './createDirectRoom';
export { deleteMessage } from './deleteMessage';
export { deleteRoom } from './deleteRoom';
export { deleteUser } from './deleteUser';
diff --git a/app/logger/server/server.js b/app/logger/server/server.js
index 40c3c7dc8237..6d15ed22883a 100644
--- a/app/logger/server/server.js
+++ b/app/logger/server/server.js
@@ -289,11 +289,6 @@ class _Logger {
return;
}
- // Deferred logging
- if (typeof options.arguments[0] === 'function') {
- options.arguments[0] = options.arguments[0]();
- }
-
const prefix = this.getPrefix(options);
if (options.box === true && _.isString(options.arguments[0])) {
diff --git a/app/models/client/models/FederationPeers.js b/app/models/client/models/FederationPeers.js
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/app/models/server/index.js b/app/models/server/index.js
index 81b288f34d53..f788aed8eaa6 100644
--- a/app/models/server/index.js
+++ b/app/models/server/index.js
@@ -38,9 +38,9 @@ export { AppsLogsModel } from './models/apps-logs-model';
export { AppsPersistenceModel } from './models/apps-persistence-model';
export { AppsModel } from './models/apps-model';
export { FederationDNSCache } from './models/FederationDNSCache';
-export { FederationRoomEvents } from './models/FederationRoomEvents';
+export { FederationEvents } from './models/FederationEvents';
export { FederationKeys } from './models/FederationKeys';
-export { FederationServers } from './models/FederationServers';
+export { FederationPeers } from './models/FederationPeers';
export {
Base,
diff --git a/app/models/server/models/FederationEvents.js b/app/models/server/models/FederationEvents.js
index bde42d59eb11..55dbcb366acd 100644
--- a/app/models/server/models/FederationEvents.js
+++ b/app/models/server/models/FederationEvents.js
@@ -1,160 +1,271 @@
-import { SHA256 } from 'meteor/sha';
+import { Meteor } from 'meteor/meteor';
import { Base } from './_Base';
-export const eventTypes = {
- // Global
- GENESIS: 'genesis',
- PING: 'ping',
-
- // Room
- ROOM_DELETE: 'room_delete',
- ROOM_ADD_USER: 'room_add_user',
- ROOM_REMOVE_USER: 'room_remove_user',
- ROOM_MESSAGE: 'room_message',
- ROOM_EDIT_MESSAGE: 'room_edit_message',
- ROOM_DELETE_MESSAGE: 'room_delete_message',
- ROOM_SET_MESSAGE_REACTION: 'room_set_message_reaction',
- ROOM_UNSET_MESSAGE_REACTION: 'room_unset_message_reaction',
- ROOM_MUTE_USER: 'room_mute_user',
- ROOM_UNMUTE_USER: 'room_unmute_user',
-};
+const normalizePeers = (basePeers, options) => {
+ const { peers: sentPeers, skipPeers } = options;
-export const contextDefinitions = {
- ROOM: {
- type: 'room',
- isRoom(event) {
- return !!event.context.roomId;
- },
- contextQuery(roomId) {
- return { roomId };
- },
- },
-
- defineType(event) {
- if (this.ROOM.isRoom(event)) {
- return this.ROOM.type;
- }
+ let peers = sentPeers || basePeers || [];
+
+ if (skipPeers) {
+ peers = peers.filter((p) => skipPeers.indexOf(p) === -1);
+ }
- return 'undefined';
- },
+ return peers;
};
-export class FederationEventsModel extends Base {
- constructor(nameOrModel) {
- super(nameOrModel);
+//
+// We should create a time to live index in this table to remove fulfilled events
+//
+class FederationEventsModel extends Base {
+ constructor() {
+ super('federation_events');
- this.tryEnsureIndex({ hasChildren: 1 }, { sparse: true });
- this.tryEnsureIndex({ timestamp: 1 });
+ this.tryEnsureIndex({ t: 1 });
+ this.tryEnsureIndex({ fulfilled: 1 });
+ this.tryEnsureIndex({ ts: 1 });
}
- getEventHash(contextQuery, event) {
- return SHA256(`${ event.origin }${ JSON.stringify(contextQuery) }${ event.parentIds.join(',') }${ event.type }${ event.timestamp }${ JSON.stringify(event.data) }`);
+ // Sometimes events errored but the error is final
+ setEventAsErrored(e, error, fulfilled = false) {
+ this.update({ _id: e._id }, {
+ $set: {
+ fulfilled,
+ lastAttemptAt: new Date(),
+ error,
+ },
+ });
}
- async createEvent(origin, contextQuery, type, data) {
- let previousEventsIds = [];
+ setEventAsFullfilled(e) {
+ this.update({ _id: e._id }, {
+ $set: { fulfilled: true },
+ $unset: { error: 1 },
+ });
+ }
+
+ createEvent(type, payload, peer, options) {
+ const record = {
+ t: type,
+ ts: new Date(),
+ fulfilled: false,
+ payload,
+ peer,
+ options,
+ };
+
+ record._id = this.insert(record);
- // If it is not a GENESIS event, we need to get the previous events
- if (type !== eventTypes.GENESIS) {
- const previousEvents = await this.model
- .rawCollection()
- .find({ context: contextQuery, hasChildren: false })
- .toArray();
+ Meteor.defer(() => {
+ this.emit('createEvent', record);
+ });
- // if (!previousEvents.length) {
- // throw new Error('Could not create event, the context does not exist');
- // }
+ return record;
+ }
+
+ createEventForPeers(type, payload, peers, options = {}) {
+ const records = [];
- previousEventsIds = previousEvents.map((e) => e._id);
+ for (const peer of peers) {
+ const record = this.createEvent(type, payload, peer, options);
+
+ records.push(record);
}
- const event = {
- origin,
- context: contextQuery,
- parentIds: previousEventsIds || [],
- type,
- timestamp: new Date(),
- data,
- hasChildren: false,
+ return records;
+ }
+
+ // Create a `ping(png)` event
+ ping(peers) {
+ return this.createEventForPeers('png', {}, peers, { retry: { total: 1 } });
+ }
+
+ // Create a `directRoomCreated(drc)` event
+ directRoomCreated(federatedRoom, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ room: federatedRoom.getRoom(),
+ owner: federatedRoom.getOwner(),
+ users: federatedRoom.getUsers(),
};
- event._id = this.getEventHash(contextQuery, event);
+ return this.createEventForPeers('drc', payload, peers);
+ }
- // this.insert(event);
+ // Create a `roomCreated(roc)` event
+ roomCreated(federatedRoom, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
- // Clear the "hasChildren" of those events
- await this.update({ _id: { $in: previousEventsIds } }, { $unset: { hasChildren: '' } }, { multi: 1 });
+ const payload = {
+ room: federatedRoom.getRoom(),
+ owner: federatedRoom.getOwner(),
+ users: federatedRoom.getUsers(),
+ };
- return event;
+ return this.createEventForPeers('roc', payload, peers);
}
- async createGenesisEvent(origin, contextQuery, data) {
- // Check if genesis event already exists, if so, do not create
- const genesisEvent = await this.model
- .rawCollection()
- .findOne({ context: contextQuery, type: eventTypes.GENESIS });
+ // Create a `userJoined(usj)` event
+ userJoined(federatedRoom, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
- if (genesisEvent) {
- throw new Error(`A GENESIS event for this context query already exists: ${ JSON.stringify(contextQuery, null, 2) }`);
- }
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ user: federatedUser.getUser(),
+ };
- return this.createEvent(origin, contextQuery, eventTypes.GENESIS, data);
+ return this.createEventForPeers('usj', payload, peers);
}
- async addEvent(contextQuery, event) {
- // Check if the event does not exit
- const existingEvent = this.findOne({ _id: event._id });
+ // Create a `userAdded(usa)` event
+ userAdded(federatedRoom, federatedUser, federatedInviter, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
- // If it does not, we insert it, checking for the parents
- if (!existingEvent) {
- // Check if we have the parents
- const parents = await this.model.rawCollection().find({ context: contextQuery, _id: { $in: event.parentIds } }, { _id: 1 }).toArray();
- const parentIds = parents.map(({ _id }) => _id);
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_inviter_id: federatedInviter.getFederationId(),
+ user: federatedUser.getUser(),
+ };
- // This means that we do not have the parents of the event we are adding
- if (parentIds.length !== event.parentIds.length) {
- const { origin } = event;
+ return this.createEventForPeers('usa', payload, peers);
+ }
- // Get the latest events for that context and origin
- const latestEvents = await this.model.rawCollection().find({ context: contextQuery, origin }, { _id: 1 }).toArray();
- const latestEventIds = latestEvents.map(({ _id }) => _id);
+ // Create a `userLeft(usl)` event
+ userLeft(federatedRoom, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
- return {
- success: false,
- reason: 'missingParents',
- missingParentIds: event.parentIds.filter(({ _id }) => parentIds.indexOf(_id) === -1),
- latestEventIds,
- };
- }
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ };
- // Clear the "hasChildren" of the parent events
- await this.update({ _id: { $in: parentIds } }, { $unset: { hasChildren: '' } }, { multi: 1 });
+ return this.createEventForPeers('usl', payload, peers);
+ }
- this.insert(event);
- }
+ // Create a `userRemoved(usr)` event
+ userRemoved(federatedRoom, federatedUser, federatedRemovedByUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
- return {
- success: true,
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ federated_removed_by_user_id: federatedRemovedByUser.getFederationId(),
};
+
+ return this.createEventForPeers('usr', payload, peers);
}
- async getEventById(contextQuery, eventId) {
- const event = await this.model
- .rawCollection()
- .findOne({ context: contextQuery, _id: eventId });
+ // Create a `userMuted(usm)` event
+ userMuted(federatedRoom, federatedUser, federatedMutedByUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
- return {
- success: !!event,
- event,
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ federated_muted_by_user_id: federatedMutedByUser.getFederationId(),
};
+
+ return this.createEventForPeers('usm', payload, peers);
}
- async getLatestEvents(contextQuery, fromTimestamp) {
- return this.model.rawCollection().find({ context: contextQuery, timestamp: { $gt: new Date(fromTimestamp) } }).toArray();
+ // Create a `userUnmuted(usu)` event
+ userUnmuted(federatedRoom, federatedUser, federatedUnmutedByUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ federated_unmuted_by_user_id: federatedUnmutedByUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('usu', payload, peers);
}
- async removeContextEvents(contextQuery) {
- return this.model.rawCollection().remove({ context: contextQuery });
+ // Create a `messageCreated(msc)` event
+ messageCreated(federatedRoom, federatedMessage, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ message: federatedMessage.getMessage(),
+ };
+
+ return this.createEventForPeers('msc', payload, peers);
+ }
+
+ // Create a `messageUpdated(msu)` event
+ messageUpdated(federatedRoom, federatedMessage, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ message: federatedMessage.getMessage(),
+ federated_user_id: federatedUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('msu', payload, peers);
+ }
+
+ // Create a `deleteMessage(msd)` event
+ messageDeleted(federatedRoom, federatedMessage, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_message_id: federatedMessage.getFederationId(),
+ };
+
+ return this.createEventForPeers('msd', payload, peers);
+ }
+
+ // Create a `messagesRead(msr)` event
+ messagesRead(federatedRoom, federatedUser, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ };
+
+ return this.createEventForPeers('msr', payload, peers);
+ }
+
+ // Create a `messagesSetReaction(mrs)` event
+ messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_message_id: federatedMessage.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ reaction,
+ shouldReact,
+ };
+
+ return this.createEventForPeers('mrs', payload, peers);
+ }
+
+ // Create a `messagesUnsetReaction(mru)` event
+ messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) {
+ const peers = normalizePeers(federatedRoom.getPeers(), options);
+
+ const payload = {
+ federated_room_id: federatedRoom.getFederationId(),
+ federated_message_id: federatedMessage.getFederationId(),
+ federated_user_id: federatedUser.getFederationId(),
+ reaction,
+ shouldReact,
+ };
+
+ return this.createEventForPeers('mru', payload, peers);
+ }
+
+ // Get all unfulfilled events
+ getUnfulfilled() {
+ return this.find({ fulfilled: false }, { sort: { ts: 1 } });
+ }
+
+ findByType(t) {
+ return this.find({ t });
}
}
+
+export const FederationEvents = new FederationEventsModel();
diff --git a/app/models/server/models/FederationKeys.js b/app/models/server/models/FederationKeys.js
index 188f7cdc434e..8e2e9c26756d 100644
--- a/app/models/server/models/FederationKeys.js
+++ b/app/models/server/models/FederationKeys.js
@@ -1,4 +1,5 @@
import NodeRSA from 'node-rsa';
+import uuid from 'uuid/v4';
import { Base } from './_Base';
@@ -34,6 +35,16 @@ class FederationKeysModel extends Base {
};
}
+ generateUniqueId() {
+ const uniqueId = uuid();
+
+ this.update({ type: 'unique' }, { type: 'unique', key: uniqueId }, { upsert: true });
+ }
+
+ getUniqueId() {
+ return (this.findOne({ type: 'unique' }) || {}).key;
+ }
+
getPrivateKey() {
const keyData = this.getKey('private');
diff --git a/app/models/server/models/FederationPeers.js b/app/models/server/models/FederationPeers.js
new file mode 100644
index 000000000000..283d2f31e761
--- /dev/null
+++ b/app/models/server/models/FederationPeers.js
@@ -0,0 +1,60 @@
+import { Base } from './_Base';
+import { Users } from '../raw';
+
+class FederationPeersModel extends Base {
+ constructor() {
+ super('federation_peers');
+
+ this.tryEnsureIndex({ active: 1, isRemote: 1 });
+ }
+
+ async refreshPeers(localIdentifier) {
+ const peers = await Users.getDistinctFederationPeers();
+
+ peers.forEach((peer) =>
+ this.update({ peer }, {
+ $setOnInsert: {
+ isRemote: localIdentifier !== peer,
+ active: false,
+ peer,
+ last_seen_at: null,
+ last_failure_at: null,
+ },
+ }, { upsert: true })
+ );
+
+ this.remove({ peer: { $nin: peers } });
+ }
+
+ updateStatuses(seenPeers) {
+ for (const peer of Object.keys(seenPeers)) {
+ const seen = seenPeers[peer];
+
+ const updateQuery = {};
+
+ if (seen) {
+ updateQuery.active = true;
+ updateQuery.last_seen_at = new Date();
+ } else {
+ updateQuery.active = false;
+ updateQuery.last_failure_at = new Date();
+ }
+
+ this.update({ peer }, { $set: updateQuery });
+ }
+ }
+
+ findActiveRemote() {
+ return this.find({ active: true, isRemote: true });
+ }
+
+ findNotActiveRemote() {
+ return this.find({ active: false, isRemote: true });
+ }
+
+ findRemote() {
+ return this.find({ isRemote: true });
+ }
+}
+
+export const FederationPeers = new FederationPeersModel();
diff --git a/app/models/server/models/FederationRoomEvents.js b/app/models/server/models/FederationRoomEvents.js
deleted file mode 100644
index a7b375fdd454..000000000000
--- a/app/models/server/models/FederationRoomEvents.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { FederationEventsModel, contextDefinitions, eventTypes } from './FederationEvents';
-
-const { type, contextQuery } = contextDefinitions.ROOM;
-
-class FederationRoomEventsModel extends FederationEventsModel {
- constructor() {
- super('federation_room_events');
-
- this.tryEnsureIndex({ 'context.roomId': 1 });
- }
-
- async createGenesisEvent(origin, room) {
- return super.createGenesisEvent(origin, contextQuery(room._id), { contextType: type, room });
- }
-
- async createDeleteRoomEvent(origin, roomId) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_DELETE, { roomId });
- }
-
- async createAddUserEvent(origin, roomId, user, subscription, domainsAfterAdd) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_ADD_USER, { roomId, user, subscription, domainsAfterAdd });
- }
-
- async createRemoveUserEvent(origin, roomId, user, domainsAfterRemoval) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_REMOVE_USER, { roomId, user, domainsAfterRemoval });
- }
-
- async createMessageEvent(origin, roomId, message) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_MESSAGE, { message });
- }
-
- async createEditMessageEvent(origin, roomId, originalMessage) {
- const message = {
- _id: originalMessage._id,
- msg: originalMessage.msg,
- federation: originalMessage.federation,
- };
-
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_EDIT_MESSAGE, { message });
- }
-
- async createDeleteMessageEvent(origin, roomId, messageId) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_DELETE_MESSAGE, { roomId, messageId });
- }
-
- async createSetMessageReactionEvent(origin, roomId, messageId, username, reaction) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_SET_MESSAGE_REACTION, { roomId, messageId, username, reaction });
- }
-
- async createUnsetMessageReactionEvent(origin, roomId, messageId, username, reaction) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_UNSET_MESSAGE_REACTION, { roomId, messageId, username, reaction });
- }
-
- async createMuteUserEvent(origin, roomId, user) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_MUTE_USER, { roomId, user });
- }
-
- async createUnmuteUserEvent(origin, roomId, user) {
- return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_UNMUTE_USER, { roomId, user });
- }
-
- async removeRoomEvents(roomId) {
- return super.removeContextEvents(contextQuery(roomId));
- }
-}
-
-export const FederationRoomEvents = new FederationRoomEventsModel();
diff --git a/app/models/server/models/FederationServers.js b/app/models/server/models/FederationServers.js
deleted file mode 100644
index 9daf20d5a128..000000000000
--- a/app/models/server/models/FederationServers.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Base } from './_Base';
-import { Users } from '../raw';
-
-class FederationServersModel extends Base {
- constructor() {
- super('federation_servers');
-
- this.tryEnsureIndex({ domain: 1 });
- }
-
- async refreshServers() {
- const domains = await Users.getDistinctFederationDomains();
-
- domains.forEach((domain) => {
- this.update({ domain }, {
- $setOnInsert: {
- domain,
- },
- }, { upsert: true });
- });
-
- this.remove({ domain: { $nin: domains } });
- }
-}
-
-export const FederationServers = new FederationServersModel();
diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js
index 0489bc85226c..64e40d157290 100644
--- a/app/models/server/models/Messages.js
+++ b/app/models/server/models/Messages.js
@@ -850,6 +850,16 @@ export class Messages extends Base {
return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData);
}
+ createRejectedMessageByPeer(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('rejected-message-by-peer', roomId, message, user, extraData);
+ }
+
+ createPeerDoesNotExist(roomId, user, extraData) {
+ const message = user.username;
+ return this.createWithTypeRoomIdMessageAndUser('peer-does-not-exist', roomId, message, user, extraData);
+ }
+
// REMOVE
removeById(_id) {
const query = { _id };
diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js
index 6ed4ff8b85b5..cfcda4d4f0b9 100644
--- a/app/models/server/models/Users.js
+++ b/app/models/server/models/Users.js
@@ -555,22 +555,22 @@ export class Users extends Base {
return this._db.find(query, options);
}
- findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) {
+ findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) {
const extraQuery = [
{
$or: [
{ federation: { $exists: false } },
- { 'federation.origin': localDomain },
+ { 'federation.peer': localPeer },
],
},
];
return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery);
}
- findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) {
+ findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) {
const extraQuery = [
{ federation: { $exists: true } },
- { 'federation.origin': { $ne: localDomain } },
+ { 'federation.peer': { $ne: localPeer } },
];
return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery);
}
diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js
index a9bffcdaff83..ff9077f9e14a 100644
--- a/app/models/server/raw/Users.js
+++ b/app/models/server/raw/Users.js
@@ -22,8 +22,8 @@ export class UsersRaw extends BaseRaw {
return this.findOne(query, { fields: { roles: 1 } });
}
- getDistinctFederationDomains() {
- return this.col.distinct('federation.origin', { federation: { $exists: true } });
+ getDistinctFederationPeers() {
+ return this.col.distinct('federation.peer', { federation: { $exists: true } });
}
async getNextLeastBusyAgent(department) {
diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js
index a2613f70e9b0..32c8801827b3 100644
--- a/app/statistics/server/functions/get.js
+++ b/app/statistics/server/functions/get.js
@@ -94,7 +94,8 @@ statistics.get = function _getStatistics() {
// Federation statistics
const federationOverviewData = federationGetStatistics();
- statistics.federatedServers = federationOverviewData.numberOfServers;
+ statistics.federatedServers = federationOverviewData.numberOfActivePeers + federationOverviewData.numberOfInactivePeers;
+ statistics.federatedServersActive = federationOverviewData.numberOfActivePeers;
statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers;
statistics.lastLogin = Users.getLastLogin();
diff --git a/app/ui/client/views/app/directory.html b/app/ui/client/views/app/directory.html
index 574d5b4a9ca5..a209293b1fc4 100644
--- a/app/ui/client/views/app/directory.html
+++ b/app/ui/client/views/app/directory.html
@@ -117,11 +117,9 @@
{{_ "Domain"}} {{> icon icon=(sortIcon 'domain') }}
{{/if}}
- {{#if $eq searchWorkspace 'internal'}}
{{_ "Created_at"}} {{> icon icon=(sortIcon 'createdAt') }}
|
- {{/if}}
diff --git a/app/ui/client/views/app/directory.js b/app/ui/client/views/app/directory.js
index 73299732aa5c..ba4af117e7f0 100644
--- a/app/ui/client/views/app/directory.js
+++ b/app/ui/client/views/app/directory.js
@@ -33,8 +33,7 @@ function directorySearch(config, cb) {
// If there is no email address (probably only rocket.cat) show the username)
email: (result.emails && result.emails[0] && result.emails[0].address) || result.username,
createdAt: timeAgo(result.createdAt, t),
- origin: result.federation && result.federation.origin,
- isRemote: result.isRemote,
+ domain: result.federation && result.federation.peer,
};
}
return null;
diff --git a/package.json b/package.json
index 3077a4ce8986..14e79a8b8780 100644
--- a/package.json
+++ b/package.json
@@ -235,7 +235,6 @@
"underscore.string": "^3.3.5",
"url-polyfill": "^1.1.5",
"uuid": "^3.3.2",
- "vis": "^4.21.0",
"webdav": "^2.0.0",
"wolfy87-eventemitter": "^5.2.5",
"xml-crypto": "^1.0.2",
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index 8a7634c7f9ac..3bc8e6270a08 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -1367,14 +1367,12 @@
"FEDERATION_Hub_URL_Description": "Set the hub URL, for example: https://hub.rocket.chat. Ports are accepted as well.",
"FEDERATION_Public_Key": "Public Key",
"FEDERATION_Public_Key_Description": "This is the key you need to share with your peers.",
- "FEDERATION_Room_Status": "Federation Status",
"FEDERATION_Status": "Status",
"FEDERATION_Test_Setup": "Test setup",
"FEDERATION_Test_Setup_Error": "Could not find your server using your setup, please review your settings.",
"FEDERATION_Test_Setup_Success": "Your federation setup is working and other servers can find you!",
"FEDERATION_Unique_Id": "Unique ID",
"FEDERATION_Unique_Id_Description": "This is your federation unique ID, used to identify your peer on the mesh.",
- "FEDERATION_Error_user_is_federated_on_rooms": "You can't remove federated users who belongs to rooms",
"Field": "Field",
"Field_removed": "Field removed",
"Field_required": "Field required",
@@ -2279,9 +2277,10 @@
"Notify_active_in_this_room": "Notify active users in this room",
"Notify_all_in_this_room": "Notify all in this room",
"Num_Agents": "# Agents",
+ "Number_of_active_peers": "Number of active peers",
"Number_of_events": "Number of events",
"Number_of_federated_users": "Number of federated users",
- "Number_of_federated_servers": "Number of federated servers",
+ "Number_of_inactive_peers": "Number of inactive peers",
"Number_of_messages": "Number of messages",
"OAuth Apps": "OAuth Apps",
"OAuth_Application": "OAuth Application",
diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js
index 3aa357398477..64ab04e377d0 100644
--- a/server/methods/browseChannels.js
+++ b/server/methods/browseChannels.js
@@ -118,9 +118,9 @@ Meteor.methods({
if (workspace === 'all') {
result = Users.findByActiveUsersExcept(text, exceptions, options, forcedSearchFields);
} else if (workspace === 'external') {
- result = Users.findByActiveExternalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.domain);
+ result = Users.findByActiveExternalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier);
} else {
- result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.domain);
+ result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier);
}
const total = result.count(); // count ignores the `skip` and `limit` options
@@ -128,18 +128,22 @@ Meteor.methods({
// Try to find federated users, when appliable
if (Federation.enabled && type === 'users' && workspace === 'external' && text.indexOf('@') !== -1) {
- const users = Federation.methods.searchUsers(text);
+ const federatedUsers = Federation.methods.searchUsers(text);
- for (const user of users) {
- if (results.find((e) => e._id === user._id)) { continue; }
+ for (const federatedUser of federatedUsers) {
+ const { user } = federatedUser;
+
+ const exists = results.findIndex((e) => e.domain === user.federation.peer && e.username === user.username) !== -1;
+
+ if (exists) { continue; }
// Add the federated user to the results
results.unshift({
username: user.username,
name: user.name,
+ createdAt: user.createdAt,
emails: user.emails,
federation: user.federation,
- isRemote: true,
});
}
}
diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js
index 59fbfeb1cbb9..2fb895435984 100644
--- a/server/methods/createDirectMessage.js
+++ b/server/methods/createDirectMessage.js
@@ -41,9 +41,11 @@ Meteor.methods({
let to = Users.findOneByUsernameIgnoringCase(username);
- // If the username does have an `@`, but does not exist locally, we create it first
if (!to && username.indexOf('@') !== -1) {
- to = Federation.methods.addUser(username);
+ // If the username does have an `@`, but does not exist locally, we create it first
+ const toId = Federation.methods.addUser(username);
+
+ to = Users.findOneById(toId);
}
if (!to) {
diff --git a/server/publications/room/index.js b/server/publications/room/index.js
index 97489cccfdf9..2643c68982c5 100644
--- a/server/publications/room/index.js
+++ b/server/publications/room/index.js
@@ -51,7 +51,6 @@ export const fields = {
e2eKeyId: 1,
departmentId: 1,
servedBy: 1,
- federation: 1,
};
const roomMap = (record) => {
diff --git a/server/startup/migrations/v143.js b/server/startup/migrations/v143.js
index 1251c7a6002b..15f93b2b0bfb 100644
--- a/server/startup/migrations/v143.js
+++ b/server/startup/migrations/v143.js
@@ -1,5 +1,5 @@
import { Migrations } from '../../../app/migrations/server';
-import { Users, FederationServers } from '../../../app/models/server';
+import { Users, FederationPeers } from '../../../app/models/server';
Migrations.add({
version: 143,
@@ -15,7 +15,7 @@ Migrations.add({
}));
if (peers.length) {
- FederationServers.model.rawCollection().insertMany(peers);
+ FederationPeers.model.rawCollection().insertMany(peers);
}
},
});
diff --git a/server/startup/migrations/v148.js b/server/startup/migrations/v148.js
index 1470efe6109c..8333bc51659d 100644
--- a/server/startup/migrations/v148.js
+++ b/server/startup/migrations/v148.js
@@ -1,12 +1,10 @@
import { Migrations } from '../../../app/migrations/server';
-import { Users, Settings, FederationServers } from '../../../app/models/server';
+import { Users, Settings, FederationPeers } from '../../../app/models/server';
Migrations.add({
version: 148,
up() {
- let { value: localDomain } = Settings.findOne({ _id: 'FEDERATION_Domain' });
-
- localDomain = localDomain.replace('@', '');
+ const { value: localDomain } = Settings.findOne({ _id: 'FEDERATION_Domain' });
Users.update({
federation: { $exists: true }, 'federation.peer': { $ne: localDomain },
@@ -14,13 +12,13 @@ Migrations.add({
$set: { isRemote: true },
}, { multi: true });
- FederationServers.update({
+ FederationPeers.update({
peer: { $ne: localDomain },
}, {
$set: { isRemote: true },
}, { multi: true });
- FederationServers.update({
+ FederationPeers.update({
peer: localDomain,
}, {
$set: { isRemote: false },