- {{#each federationPeerStatuses}}
+ {{#each federationPeers}}
-
-
-
{{peer}}
+
+ {{domain}}
{{/each}}
diff --git a/app/federation/client/admin/dashboard.js b/app/federation/client/admin/dashboard.js
index 1979e798b26..e53325264d2 100644
--- a/app/federation/client/admin/dashboard.js
+++ b/app/federation/client/admin/dashboard.js
@@ -18,10 +18,7 @@ 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;
@@ -30,32 +27,29 @@ const updateOverviewData = () => {
});
};
-const updatePeerStatuses = () => {
- Meteor.call('federation:getPeerStatuses', (error, result) => {
+const updateServers = () => {
+ Meteor.call('federation:getServers', (error, result) => {
if (error) {
- console.log(error);
-
return;
- // return handleError(error);
}
const { data } = result;
- templateInstance.federationPeerStatuses.set(data);
+ templateInstance.federationPeers.set(data);
});
};
const updateData = () => {
updateOverviewData();
- updatePeerStatuses();
+ updateServers();
};
Template.dashboard.helpers({
federationOverviewData() {
return templateInstance.federationOverviewData.get();
},
- federationPeerStatuses() {
- return templateInstance.federationPeerStatuses.get();
+ federationPeers() {
+ return templateInstance.federationPeers.get();
},
});
@@ -64,7 +58,7 @@ Template.dashboard.onCreated(function() {
templateInstance = Template.instance();
this.federationOverviewData = new ReactiveVar();
- this.federationPeerStatuses = new ReactiveVar();
+ this.federationPeers = new ReactiveVar();
});
Template.dashboard.onRendered(() => {
diff --git a/app/federation/client/index.js b/app/federation/client/index.js
index 14655cf4b6a..1a16fba9d1f 100644
--- a/app/federation/client/index.js
+++ b/app/federation/client/index.js
@@ -1,2 +1 @@
-import './messageTypes';
import './admin/dashboard';
diff --git a/app/federation/client/messageTypes.js b/app/federation/client/messageTypes.js
deleted file mode 100644
index 573845cba35..00000000000
--- a/app/federation/client/messageTypes.js
+++ /dev/null
@@ -1,23 +0,0 @@
-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/server/PeerClient.js b/app/federation/server/PeerClient.js
deleted file mode 100644
index 38ceabf807a..00000000000
--- a/app/federation/server/PeerClient.js
+++ /dev/null
@@ -1,638 +0,0 @@
-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
deleted file mode 100644
index ace2721769c..00000000000
--- a/app/federation/server/PeerDNS.js
+++ /dev/null
@@ -1,185 +0,0 @@
-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
deleted file mode 100644
index fbcb1e72962..00000000000
--- a/app/federation/server/PeerHTTP/PeerHTTP.js
+++ /dev/null
@@ -1,100 +0,0 @@
-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
deleted file mode 100644
index 3c9e957f1cc..00000000000
--- a/app/federation/server/PeerHTTP/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { PeerHTTP } from './PeerHTTP';
diff --git a/app/federation/server/PeerHTTP/utils.js b/app/federation/server/PeerHTTP/utils.js
deleted file mode 100644
index 7cc6fc17709..00000000000
--- a/app/federation/server/PeerHTTP/utils.js
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index aa3bcbfa8a4..00000000000
--- a/app/federation/server/PeerPinger.js
+++ /dev/null
@@ -1,37 +0,0 @@
-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
deleted file mode 100644
index f6b2ecd5b79..00000000000
--- a/app/federation/server/PeerServer/PeerServer.js
+++ /dev/null
@@ -1,404 +0,0 @@
-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
deleted file mode 100644
index e1da97c3327..00000000000
--- a/app/federation/server/PeerServer/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// 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
deleted file mode 100644
index 45153c80bd7..00000000000
--- a/app/federation/server/PeerServer/routes/events.js
+++ /dev/null
@@ -1,115 +0,0 @@
-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/PeerServer/routes/users.js b/app/federation/server/PeerServer/routes/users.js
deleted file mode 100644
index 89006ad583a..00000000000
--- a/app/federation/server/PeerServer/routes/users.js
+++ /dev/null
@@ -1,48 +0,0 @@
-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/config.js b/app/federation/server/config.js
deleted file mode 100644
index b8f1f2b1228..00000000000
--- a/app/federation/server/config.js
+++ /dev/null
@@ -1,74 +0,0 @@
-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/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js
new file mode 100644
index 00000000000..b0693b2ab43
--- /dev/null
+++ b/app/federation/server/endpoints/dispatch.js
@@ -0,0 +1,419 @@
+import { Meteor } from 'meteor/meteor';
+import { EJSON } from 'meteor/ejson';
+
+import { API } from '../../../api/server';
+import { logger } from '../lib/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';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { decryptIfNeeded } from '../lib/crypt';
+import { isFederationEnabled } from '../lib/isFederationEnabled';
+import { getUpload, requestEventsFromLatest } from '../handler';
+
+API.v1.addRoute('federation.events.dispatch', { authRequired: false }, {
+ async post() {
+ if (!isFederationEnabled()) {
+ return API.v1.failure('Federation not enabled');
+ }
+
+ //
+ // Decrypt the payload if needed
+ let payload;
+
+ try {
+ payload = decryptIfNeeded(this.request, this.bodyParams);
+ } catch (err) {
+ return API.v1.failure('Could not decrypt payload');
+ }
+
+ //
+ // 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 } = 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) }`);
+
+ requestEventsFromLatest(event.origin, getFederationDomain(), 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/endpoints/index.js b/app/federation/server/endpoints/index.js
new file mode 100644
index 00000000000..a8a11611b82
--- /dev/null
+++ b/app/federation/server/endpoints/index.js
@@ -0,0 +1,4 @@
+import './dispatch';
+import './requestFromLatest';
+import './uploads';
+import './users';
diff --git a/app/federation/server/endpoints/requestFromLatest.js b/app/federation/server/endpoints/requestFromLatest.js
new file mode 100644
index 00000000000..87917880229
--- /dev/null
+++ b/app/federation/server/endpoints/requestFromLatest.js
@@ -0,0 +1,61 @@
+import { EJSON } from 'meteor/ejson';
+
+import { API } from '../../../api/server';
+import { logger } from '../lib/logger';
+import { FederationRoomEvents } from '../../../models/server';
+import { decryptIfNeeded } from '../lib/crypt';
+import { isFederationEnabled } from '../lib/isFederationEnabled';
+import { dispatchEvents } from '../handler';
+
+API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, {
+ async post() {
+ if (!isFederationEnabled()) {
+ return API.v1.failure('Federation not enabled');
+ }
+
+ //
+ // Decrypt the payload if needed
+ let payload;
+
+ try {
+ payload = decryptIfNeeded(this.request, this.bodyParams);
+ } catch (err) {
+ return API.v1.failure('Could not decrypt payload');
+ }
+
+ 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
+ dispatchEvents([fromDomain], missingEvents);
+
+ return API.v1.success();
+ },
+});
diff --git a/app/federation/server/PeerServer/routes/uploads.js b/app/federation/server/endpoints/uploads.js
similarity index 61%
rename from app/federation/server/PeerServer/routes/uploads.js
rename to app/federation/server/endpoints/uploads.js
index ec24f75ca2b..ca8b81a9208 100644
--- a/app/federation/server/PeerServer/routes/uploads.js
+++ b/app/federation/server/endpoints/uploads.js
@@ -1,14 +1,14 @@
import { Meteor } from 'meteor/meteor';
-import { API } from '../../../../api';
-import { Uploads } from '../../../../models';
-import { FileUpload } from '../../../../file-upload';
-import { Federation } from '../..';
+import { API } from '../../../api/server';
+import { Uploads } from '../../../models/server';
+import { FileUpload } from '../../../file-upload/server';
+import { isFederationEnabled } from '../lib/isFederationEnabled';
API.v1.addRoute('federation.uploads', { authRequired: false }, {
get() {
- if (!Federation.peerServer.enabled) {
- return API.v1.failure('Not found');
+ if (!isFederationEnabled()) {
+ return API.v1.failure('Federation not enabled');
}
const { upload_id } = this.requestParams();
diff --git a/app/federation/server/endpoints/users.js b/app/federation/server/endpoints/users.js
new file mode 100644
index 00000000000..d74cdae8324
--- /dev/null
+++ b/app/federation/server/endpoints/users.js
@@ -0,0 +1,57 @@
+import { API } from '../../../api/server';
+import { Users } from '../../../models/server';
+import { normalizers } from '../normalizers';
+import { logger } from '../lib/logger';
+import { isFederationEnabled } from '../lib/isFederationEnabled';
+
+const userFields = { _id: 1, username: 1, type: 1, emails: 1, name: 1 };
+
+API.v1.addRoute('federation.users.search', { authRequired: false }, {
+ get() {
+ if (!isFederationEnabled()) {
+ return API.v1.failure('Federation not enabled');
+ }
+
+ 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 (!isFederationEnabled()) {
+ return API.v1.failure('Federation not enabled');
+ }
+
+ 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/federatedResources/FederatedMessage.js b/app/federation/server/federatedResources/FederatedMessage.js
deleted file mode 100644
index 92d98a0ada2..00000000000
--- a/app/federation/server/federatedResources/FederatedMessage.js
+++ /dev/null
@@ -1,263 +0,0 @@
-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
deleted file mode 100644
index 7ecdb9ec1cd..00000000000
--- a/app/federation/server/federatedResources/FederatedResource.js
+++ /dev/null
@@ -1,17 +0,0 @@
-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
deleted file mode 100644
index 494edb1f96c..00000000000
--- a/app/federation/server/federatedResources/FederatedRoom.js
+++ /dev/null
@@ -1,275 +0,0 @@
-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
deleted file mode 100644
index 5b21c5fd07f..00000000000
--- a/app/federation/server/federatedResources/FederatedUser.js
+++ /dev/null
@@ -1,125 +0,0 @@
-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
deleted file mode 100644
index 90d98b351ca..00000000000
--- a/app/federation/server/federatedResources/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export { FederatedMessage } from './FederatedMessage';
-export { FederatedResource } from './FederatedResource';
-export { FederatedRoom } from './FederatedRoom';
-export { FederatedUser } from './FederatedUser';
diff --git a/app/federation/server/federation-settings.js b/app/federation/server/federation-settings.js
deleted file mode 100644
index 385d077af5f..00000000000
--- a/app/federation/server/federation-settings.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { settings } from '../../settings';
-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',
- i18nLabel: 'Enabled',
- i18nDescription: 'FEDERATION_Enabled',
- alert: 'FEDERATION_Enabled_Alert',
- public: true,
- });
-
- 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',
- i18nDescription: 'FEDERATION_Domain_Description',
- alert: 'FEDERATION_Domain_Alert',
- disableReset: true,
- });
-
- this.add('FEDERATION_Public_Key', federationPublicKey, {
- readonly: true,
- type: 'string',
- multiline: true,
- i18nLabel: 'FEDERATION_Public_Key',
- 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: [{
- key: 'dns',
- i18nLabel: 'DNS',
- }, {
- key: 'hub',
- i18nLabel: 'Hub',
- }],
- i18nLabel: 'FEDERATION_Discovery_Method',
- i18nDescription: 'FEDERATION_Discovery_Method_Description',
- public: true,
- });
-
- this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', {
- type: 'action',
- actionText: 'FEDERATION_Test_Setup',
- });
- });
-});
diff --git a/app/federation/server/functions/addUser.js b/app/federation/server/functions/addUser.js
new file mode 100644
index 00000000000..eebd1656260
--- /dev/null
+++ b/app/federation/server/functions/addUser.js
@@ -0,0 +1,35 @@
+import { Meteor } from 'meteor/meteor';
+
+import * as federationErrors from './errors';
+import { FederationServers, Users } from '../../../models/server';
+import { getUserByUsername } from '../handler';
+
+export function addUser(query) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUser' });
+ }
+
+ const user = getUserByUsername(query);
+
+ if (!user) {
+ throw federationErrors.userNotFound(query);
+ }
+
+ let userId = user._id;
+
+ try {
+ // Create the local user
+ userId = Users.create(user);
+
+ // Refresh the servers list
+ FederationServers.refreshServers();
+ } 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;
+ }
+ }
+
+ return Users.findOne({ _id: userId });
+}
diff --git a/app/federation/server/functions/dashboard.js b/app/federation/server/functions/dashboard.js
new file mode 100644
index 00000000000..137ef802c5d
--- /dev/null
+++ b/app/federation/server/functions/dashboard.js
@@ -0,0 +1,44 @@
+import { Meteor } from 'meteor/meteor';
+
+import { FederationServers, FederationRoomEvents, Users } from '../../../models/server';
+
+export function getStatistics() {
+ const numberOfEvents = FederationRoomEvents.find().count();
+ const numberOfFederatedUsers = Users.findRemote().count();
+ const numberOfServers = FederationServers.find().count();
+
+ return { numberOfEvents, numberOfFederatedUsers, numberOfServers };
+}
+
+export function federationGetOverviewData() {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('not-authorized');
+ }
+
+ const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = getStatistics();
+
+ return {
+ data: [{
+ title: 'Number_of_events',
+ value: numberOfEvents,
+ }, {
+ title: 'Number_of_federated_users',
+ value: numberOfFederatedUsers,
+ }, {
+ title: 'Number_of_federated_servers',
+ value: numberOfServers,
+ }],
+ };
+}
+
+export function federationGetServers() {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('not-authorized');
+ }
+
+ const servers = FederationServers.find().fetch();
+
+ return {
+ data: servers,
+ };
+}
diff --git a/app/federation/server/functions/errors.js b/app/federation/server/functions/errors.js
new file mode 100644
index 00000000000..f4eb74e2b44
--- /dev/null
+++ b/app/federation/server/functions/errors.js
@@ -0,0 +1,13 @@
+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/functions/helpers.js b/app/federation/server/functions/helpers.js
new file mode 100644
index 00000000000..adbe2ac2162
--- /dev/null
+++ b/app/federation/server/functions/helpers.js
@@ -0,0 +1,62 @@
+import { Settings, Subscriptions, Users } from '../../../models/server';
+
+export const getNameAndDomain = (fullyQualifiedName) => fullyQualifiedName.split('@');
+export const isFullyQualified = (name) => name.indexOf('@') !== -1;
+
+export function updateStatus(status) {
+ Settings.updateValueById('FEDERATION_Status', status);
+}
+
+export function updateEnabled(enabled) {
+ Settings.updateValueById('FEDERATION_Enabled', enabled);
+}
+
+export const isFederated = (resource) => !!resource.federation;
+
+export const hasExternalDomain = ({ federation }) => {
+ // same test as isFederated(room)
+ if (!federation) {
+ return false;
+ }
+
+ return federation.domains
+ .some((domain) => domain !== federation.origin);
+};
+
+export const isLocalUser = ({ federation }, localDomain) =>
+ !federation || federation.origin === localDomain;
+
+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/functions/searchUsers.js b/app/federation/server/functions/searchUsers.js
new file mode 100644
index 00000000000..7d9e349b9fe
--- /dev/null
+++ b/app/federation/server/functions/searchUsers.js
@@ -0,0 +1,19 @@
+import { Meteor } from 'meteor/meteor';
+
+import * as federationErrors from './errors';
+import { normalizers } from '../normalizers';
+import { federationSearchUsers } from '../handler';
+
+export function searchUsers(query) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'searchUsers' });
+ }
+
+ const users = federationSearchUsers(query);
+
+ if (!users.length) {
+ throw federationErrors.userNotFound(query);
+ }
+
+ return normalizers.denormalizeAllUsers(users);
+}
diff --git a/app/federation/server/handler/index.js b/app/federation/server/handler/index.js
new file mode 100644
index 00000000000..e6bf2be85cb
--- /dev/null
+++ b/app/federation/server/handler/index.js
@@ -0,0 +1,75 @@
+import qs from 'querystring';
+
+import { disabled } from '../functions/errors';
+import { logger } from '../lib/logger';
+import { isFederationEnabled } from '../lib/isFederationEnabled';
+import { federationRequestToPeer } from '../lib/http';
+
+export function federationSearchUsers(query) {
+ if (!isFederationEnabled()) {
+ throw 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 } } = federationRequestToPeer('GET', peerDomain, uri);
+
+ return users;
+}
+
+export function getUserByUsername(query) {
+ if (!isFederationEnabled()) {
+ throw 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 } } = federationRequestToPeer('GET', peerDomain, uri);
+
+ return user;
+}
+
+export function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) {
+ if (!isFederationEnabled()) {
+ throw 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';
+
+ federationRequestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds });
+}
+
+
+export function dispatchEvents(domains, events) {
+ if (!isFederationEnabled()) {
+ throw 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) {
+ federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true });
+ }
+}
+
+export function dispatchEvent(domains, event) {
+ dispatchEvents(domains, [event]);
+}
+
+export function getUpload(domain, fileId) {
+ const { data: { upload, buffer } } = federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`);
+
+ return { upload, buffer: Buffer.from(buffer) };
+}
diff --git a/app/federation/server/hooks/afterAddedToRoom.js b/app/federation/server/hooks/afterAddedToRoom.js
new file mode 100644
index 00000000000..14f50d2c083
--- /dev/null
+++ b/app/federation/server/hooks/afterAddedToRoom.js
@@ -0,0 +1,70 @@
+import { logger } from '../lib/logger';
+import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers';
+import { FederationRoomEvents, Subscriptions } from '../../../models/server';
+import { normalizers } from '../normalizers';
+import { doAfterCreateRoom } from './afterCreateRoom';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterAddedToRoom(involvedUsers, room) {
+ const { user: addedUser } = involvedUsers;
+
+ const localDomain = getFederationDomain();
+
+ if (!hasExternalDomain(room) && isLocalUser(addedUser, localDomain)) {
+ return involvedUsers;
+ }
+
+ 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 = 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(localDomain, room._id, normalizedSourceUser, normalizedSourceSubscription, domainsAfterAdd);
+
+ // Dispatch the events
+ dispatchEvent(domainsAfterAdd, addUserEvent);
+ }
+ } catch (err) {
+ // Remove the user subscription from the room
+ 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/hooks/afterCreateDirectRoom.js b/app/federation/server/hooks/afterCreateDirectRoom.js
new file mode 100644
index 00000000000..78f1e303627
--- /dev/null
+++ b/app/federation/server/hooks/afterCreateDirectRoom.js
@@ -0,0 +1,75 @@
+import { logger } from '../lib/logger';
+import { FederationRoomEvents, Subscriptions } from '../../../models/server';
+import { normalizers } from '../normalizers';
+import { deleteRoom } from '../../../lib/server/functions';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvents } from '../handler';
+import { hasExternalDomain } from '../functions/helpers';
+
+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 (!hasExternalDomain(room)) { return room; }
+
+ // 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 room; }
+
+ try {
+ //
+ // Genesis
+ //
+
+ // Normalize room
+ const normalizedRoom = normalizers.normalizeRoom(room);
+
+ // Ensure a genesis event for this room
+ const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), 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(getFederationDomain(), 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(getFederationDomain(), normalizedRoom._id, normalizedTargetUser, normalizedTargetSubscription);
+
+ // Dispatch the events
+ 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/hooks/afterCreateRoom.js b/app/federation/server/hooks/afterCreateRoom.js
new file mode 100644
index 00000000000..83043c0062e
--- /dev/null
+++ b/app/federation/server/hooks/afterCreateRoom.js
@@ -0,0 +1,88 @@
+import { logger } from '../lib/logger';
+import { FederationRoomEvents, Subscriptions, Users } from '../../../models/server';
+import { normalizers } from '../normalizers';
+import { deleteRoom } from '../../../lib/server/functions';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvents } from '../handler';
+
+export async function doAfterCreateRoom(room, users, subscriptions) {
+ const normalizedUsers = [];
+
+ //
+ // 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);
+
+ normalizedUsers.push(normalizedSourceUser);
+
+ const addUserEvent = await FederationRoomEvents.createAddUserEvent(getFederationDomain(), room._id, normalizedSourceUser, normalizedSourceSubscription);
+
+ addUserEvents.push(addUserEvent);
+
+ /* eslint-enable no-await-in-loop */
+ }
+
+ //
+ // Genesis
+ //
+
+ // Normalize room
+ const normalizedRoom = normalizers.normalizeRoom(room, normalizedUsers);
+
+ // Ensure a genesis event for this room
+ const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom);
+
+ // Dispatch the events
+ dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]);
+}
+
+async function afterCreateRoom(roomOwner, room) {
+ // If the room is federated, ignore
+ if (room.federation) { return roomOwner; }
+
+ // 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/hooks/afterDeleteMessage.js b/app/federation/server/hooks/afterDeleteMessage.js
new file mode 100644
index 00000000000..714de63a6e4
--- /dev/null
+++ b/app/federation/server/hooks/afterDeleteMessage.js
@@ -0,0 +1,28 @@
+import { FederationRoomEvents, Rooms } from '../../../models/server';
+import { logger } from '../lib/logger';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterDeleteMessage(message) {
+ const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } });
+
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return message; }
+
+ 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(getFederationDomain(), room._id, message._id);
+
+ // Dispatch event (async)
+ 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/hooks/afterMuteUser.js b/app/federation/server/hooks/afterMuteUser.js
new file mode 100644
index 00000000000..4dba95ee4df
--- /dev/null
+++ b/app/federation/server/hooks/afterMuteUser.js
@@ -0,0 +1,29 @@
+import { FederationRoomEvents } from '../../../models/server';
+import { logger } from '../lib/logger';
+import { normalizers } from '../normalizers';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterMuteUser(involvedUsers, room) {
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return involvedUsers; }
+
+ 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(getFederationDomain(), room._id, normalizers.normalizeUser(mutedUser));
+
+ // Dispatch event (async)
+ 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/hooks/afterRemoveFromRoom.js b/app/federation/server/hooks/afterRemoveFromRoom.js
new file mode 100644
index 00000000000..36ec2cebe17
--- /dev/null
+++ b/app/federation/server/hooks/afterRemoveFromRoom.js
@@ -0,0 +1,55 @@
+import { FederationRoomEvents } from '../../../models/server';
+import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers';
+import { logger } from '../lib/logger';
+import { normalizers } from '../normalizers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterRemoveFromRoom(involvedUsers, room) {
+ const { removedUser } = involvedUsers;
+
+ const localDomain = getFederationDomain();
+
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room) && isLocalUser(removedUser, localDomain)) {
+ return involvedUsers;
+ }
+
+ 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(localDomain, room._id, normalizedSourceUser, domainsAfterRemoval);
+
+ // Dispatch the events
+ dispatchEvent(domainsBeforeRemoval, removeUserEvent);
+ } catch (err) {
+ logger.client.error(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ 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/hooks/afterSaveMessage.js b/app/federation/server/hooks/afterSaveMessage.js
new file mode 100644
index 00000000000..5f72868a232
--- /dev/null
+++ b/app/federation/server/hooks/afterSaveMessage.js
@@ -0,0 +1,35 @@
+import { logger } from '../lib/logger';
+import { FederationRoomEvents } from '../../../models/server';
+import { normalizers } from '../normalizers';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterSaveMessage(message, room) {
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return message; }
+
+ 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(getFederationDomain(), room._id, normalizers.normalizeMessage(message));
+ } else {
+ // Create the message event
+ event = await FederationRoomEvents.createMessageEvent(getFederationDomain(), room._id, normalizers.normalizeMessage(message));
+ }
+
+ // Dispatch event (async)
+ 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/hooks/afterSetReaction.js b/app/federation/server/hooks/afterSetReaction.js
new file mode 100644
index 00000000000..fec108dd91d
--- /dev/null
+++ b/app/federation/server/hooks/afterSetReaction.js
@@ -0,0 +1,30 @@
+import _ from 'underscore';
+
+import { FederationRoomEvents, Rooms } from '../../../models/server';
+import { logger } from '../lib/logger';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterSetReaction(message, { user, reaction }) {
+ const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } });
+
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return message; }
+
+ 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(getFederationDomain(), room._id, message._id, user.username, reaction);
+
+ // Dispatch event (async)
+ 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/hooks/afterUnmuteUser.js b/app/federation/server/hooks/afterUnmuteUser.js
new file mode 100644
index 00000000000..3578e08c97d
--- /dev/null
+++ b/app/federation/server/hooks/afterUnmuteUser.js
@@ -0,0 +1,29 @@
+import { FederationRoomEvents } from '../../../models/server';
+import { logger } from '../lib/logger';
+import { normalizers } from '../normalizers';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterUnmuteUser(involvedUsers, room) {
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return involvedUsers; }
+
+ 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(getFederationDomain(), room._id, normalizers.normalizeUser(unmutedUser));
+
+ // Dispatch event (async)
+ 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/hooks/afterUnsetReaction.js b/app/federation/server/hooks/afterUnsetReaction.js
new file mode 100644
index 00000000000..1ee72ff1f35
--- /dev/null
+++ b/app/federation/server/hooks/afterUnsetReaction.js
@@ -0,0 +1,29 @@
+import _ from 'underscore';
+
+import { FederationRoomEvents, Rooms } from '../../../models/server';
+import { logger } from '../lib/logger';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+
+async function afterUnsetReaction(message, { user, reaction }) {
+ const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } });
+
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return message; }
+
+ 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(getFederationDomain(), room._id, message._id, user.username, reaction);
+
+ // Dispatch event (async)
+ 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/hooks/beforeDeleteRoom.js b/app/federation/server/hooks/beforeDeleteRoom.js
new file mode 100644
index 00000000000..44005d9059d
--- /dev/null
+++ b/app/federation/server/hooks/beforeDeleteRoom.js
@@ -0,0 +1,36 @@
+import { logger } from '../lib/logger';
+import { FederationRoomEvents, Rooms } from '../../../models/server';
+import { hasExternalDomain } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+
+async function beforeDeleteRoom(roomId) {
+ const room = Rooms.findOneById(roomId, { fields: { federation: 1 } });
+
+ // If room does not exist, skip
+ if (!room) { return roomId; }
+
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room)) { return roomId; }
+
+ logger.client.debug(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) }`);
+
+ try {
+ // Create the message event
+ const event = await FederationRoomEvents.createDeleteRoomEvent(getFederationDomain(), room._id);
+
+ // Dispatch event (async)
+ 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/index.js b/app/federation/server/index.js
index f0c6844e86a..0fddfc2fe2e 100644
--- a/app/federation/server/index.js
+++ b/app/federation/server/index.js
@@ -1,149 +1,3 @@
-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);
+import './methods';
+import './endpoints';
+import './startup';
diff --git a/app/federation/server/lib/callbacks.js b/app/federation/server/lib/callbacks.js
new file mode 100644
index 00000000000..c2712ad80d3
--- /dev/null
+++ b/app/federation/server/lib/callbacks.js
@@ -0,0 +1,19 @@
+import { callbacks } from '../../../callbacks/server';
+
+const callbackDefinitions = [];
+
+export function registerCallback(callbackDefition) {
+ callbackDefinitions.push(callbackDefition);
+}
+
+export function enableCallbacks() {
+ for (const definition of callbackDefinitions) {
+ callbacks.add(definition.hook, definition.callback, callbacks.priority.LOW, definition.id);
+ }
+}
+
+export function disableCallbacks() {
+ for (const definition of callbackDefinitions) {
+ callbacks.remove(definition.hook, definition.id);
+ }
+}
diff --git a/app/federation/server/lib/crypt.js b/app/federation/server/lib/crypt.js
new file mode 100644
index 00000000000..ce92cfd8405
--- /dev/null
+++ b/app/federation/server/lib/crypt.js
@@ -0,0 +1,67 @@
+import { FederationKeys } from '../../../models/server';
+import { getFederationDomain } from './getFederationDomain';
+import { search } from './dns';
+import { logger } from './logger';
+
+export function 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 = FederationKeys.getPrivateKey().decrypt(data);
+ } catch (err) {
+ logger.crypt.error(err);
+
+ throw new Error('Could not decrypt');
+ }
+
+ return JSON.parse(data.toString());
+}
+
+export function decryptIfNeeded(request, bodyParams) {
+ //
+ // Look for the domain that sent this event
+ const remotePeerDomain = request.headers['x-federation-domain'];
+
+ if (!remotePeerDomain) {
+ throw new Error('Domain is unknown, ignoring event');
+ }
+
+ //
+ // Decrypt payload if needed
+ if (remotePeerDomain === getFederationDomain()) {
+ return bodyParams;
+ }
+ //
+ // Find the peer's public key
+ const { publicKey: peerKey } = search(remotePeerDomain);
+
+ if (!peerKey) {
+ throw new Error("Could not find the peer's public key to decrypt");
+ }
+
+ return decrypt(bodyParams, peerKey);
+}
+
+export function encrypt(data, peerKey) {
+ if (!data) {
+ return data;
+ }
+
+ try {
+ // Encrypt with the peer's public key
+ data = FederationKeys.loadKey(peerKey, 'public').encrypt(data);
+
+ // Encrypt with the local private key
+ return FederationKeys.getPrivateKey().encryptPrivate(data);
+ } catch (err) {
+ logger.crypt.error(err);
+
+ throw new Error('Could not encrypt');
+ }
+}
diff --git a/app/federation/server/lib/dns.js b/app/federation/server/lib/dns.js
new file mode 100644
index 00000000000..b6a58ba21ff
--- /dev/null
+++ b/app/federation/server/lib/dns.js
@@ -0,0 +1,104 @@
+import dnsResolver from 'dns';
+
+import { Meteor } from 'meteor/meteor';
+
+import * as federationErrors from '../functions/errors';
+import { logger } from './logger';
+import { isFederationEnabled } from './isFederationEnabled';
+import { federationRequest } from './http';
+
+const dnsResolveSRV = Meteor.wrapAsync(dnsResolver.resolveSrv);
+const dnsResolveTXT = Meteor.wrapAsync(dnsResolver.resolveTxt);
+
+const hubUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat';
+
+export function 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
+ federationRequest('POST', `${ hubUrl }/api/v1/peers`, body);
+
+ return true;
+ } catch (err) {
+ logger.dns.error(err);
+
+ throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub');
+ }
+}
+
+export function searchHub(peerDomain) {
+ try {
+ // If there is no DNS entry for that, get from the Hub
+ const { data: { peer } } = federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`);
+
+ if (!peer) {
+ throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub');
+ }
+
+ const { url, public_key: publicKey } = peer;
+
+ return {
+ url,
+ peerDomain,
+ publicKey,
+ };
+ } catch (err) {
+ logger.dns.error(err);
+
+ throw federationErrors.peerNotFoundUsingDNS('dns.searchHub');
+ }
+}
+
+export function search(peerDomain) {
+ if (!isFederationEnabled()) {
+ throw federationErrors.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 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 searchHub(peerDomain);
+ }
+
+ return {
+ url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`,
+ peerDomain,
+ publicKey,
+ };
+}
diff --git a/app/federation/server/lib/getFederationDiscoveryMethod.js b/app/federation/server/lib/getFederationDiscoveryMethod.js
new file mode 100644
index 00000000000..2da490942fd
--- /dev/null
+++ b/app/federation/server/lib/getFederationDiscoveryMethod.js
@@ -0,0 +1,3 @@
+import { settings } from '../../../settings/server';
+
+export const getFederationDiscoveryMethod = () => settings.get('FEDERATION_Discovery_Method');
diff --git a/app/federation/server/lib/getFederationDomain.js b/app/federation/server/lib/getFederationDomain.js
new file mode 100644
index 00000000000..c5e67629db7
--- /dev/null
+++ b/app/federation/server/lib/getFederationDomain.js
@@ -0,0 +1,3 @@
+import { settings } from '../../../settings/server';
+
+export const getFederationDomain = () => settings.get('FEDERATION_Domain').replace('@', '');
diff --git a/app/federation/server/lib/http.js b/app/federation/server/lib/http.js
new file mode 100644
index 00000000000..191e7a473ea
--- /dev/null
+++ b/app/federation/server/lib/http.js
@@ -0,0 +1,52 @@
+import { HTTP as MeteorHTTP } from 'meteor/http';
+import { EJSON } from 'meteor/ejson';
+
+import { logger } from './logger';
+import { getFederationDomain } from './getFederationDomain';
+import { search } from './dns';
+import { encrypt } from './crypt';
+
+export function federationRequest(method, url, body, headers, peerKey = null) {
+ let data = null;
+
+ if ((method === 'POST' || method === 'PUT') && body) {
+ data = EJSON.toJSONValue(body);
+
+ if (peerKey) {
+ data = encrypt(data, peerKey);
+ }
+ }
+
+ logger.http.debug(`[${ method }] ${ url }`);
+
+ return MeteorHTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': getFederationDomain() } });
+}
+
+export function federationRequestToPeer(method, peerDomain, uri, body, options = {}) {
+ const ignoreErrors = peerDomain === getFederationDomain() ? false : options.ignoreErrors;
+
+ const { url: baseUrl, publicKey } = search(peerDomain);
+
+ let peerKey = null;
+
+ // Only encrypt if it is not local
+ if (peerDomain !== getFederationDomain()) {
+ peerKey = publicKey;
+ }
+
+ let result;
+
+ try {
+ result = federationRequest(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 };
+}
diff --git a/app/federation/server/lib/isFederationEnabled.js b/app/federation/server/lib/isFederationEnabled.js
new file mode 100644
index 00000000000..9e46d3004ac
--- /dev/null
+++ b/app/federation/server/lib/isFederationEnabled.js
@@ -0,0 +1,3 @@
+import { settings } from '../../../settings/server';
+
+export const isFederationEnabled = () => settings.get('FEDERATION_Enabled');
diff --git a/app/federation/server/lib/logger.js b/app/federation/server/lib/logger.js
new file mode 100644
index 00000000000..f3e791a14bf
--- /dev/null
+++ b/app/federation/server/lib/logger.js
@@ -0,0 +1,12 @@
+import { Logger } from '../../../logger/server';
+
+export const logger = new Logger('Federation', {
+ sections: {
+ client: 'client',
+ crypt: 'crypt',
+ dns: 'dns',
+ http: 'http',
+ server: 'server',
+ setup: 'Setup',
+ },
+});
diff --git a/app/federation/server/logger.js b/app/federation/server/logger.js
deleted file mode 100644
index 77ffd9c4ab8..00000000000
--- a/app/federation/server/logger.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Logger } from '../../logger';
-
-export const logger = new Logger('Federation', {
- sections: {
- 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
deleted file mode 100644
index 9a0d7a3ceaa..00000000000
--- a/app/federation/server/methods/addUser.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Users, FederationPeers } from '../../../models';
-
-import { Federation } from '..';
-
-import { logger } from '../logger';
-
-export function addUser(identifier) {
- if (!Meteor.userId()) {
- 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' });
- }
-
- // Make sure the federated user still exists, and get the unique one, by email address
- const [federatedUser] = Federation.peerClient.findUsers(identifier, { usernameOnly: true });
-
- if (!federatedUser) {
- throw new Meteor.Error('federation-invalid-user', 'There is no user to add.');
- }
-
- let user = null;
-
- const localUser = federatedUser.getLocalUser();
-
- localUser.name += `@${ federatedUser.user.federation.peer }`;
-
- // Delete the _id
- delete localUser._id;
-
- try {
- // Create the local user
- user = Users.create(localUser);
-
- // Refresh the peers list
- FederationPeers.refreshPeers();
- } catch (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 user;
-}
diff --git a/app/federation/server/methods/dashboard.js b/app/federation/server/methods/dashboard.js
index d272991cd64..49a374141b4 100644
--- a/app/federation/server/methods/dashboard.js
+++ b/app/federation/server/methods/dashboard.js
@@ -1,76 +1,8 @@
import { Meteor } from 'meteor/meteor';
-import moment from 'moment';
-// 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 = FederationEvents.findByType('png').count();
- const numberOfFederatedUsers = Users.findRemote().count();
- const numberOfActivePeers = FederationPeers.findActiveRemote().count();
- const numberOfInactivePeers = FederationPeers.findNotActiveRemote().count();
-
- return { numberOfEvents, numberOfFederatedUsers, numberOfActivePeers, numberOfInactivePeers };
-}
-
-export function federationGetOverviewData() {
- if (!Meteor.userId()) {
- throw new Meteor.Error('not-authorized');
- }
-
- const { numberOfEvents, numberOfFederatedUsers, numberOfActivePeers, numberOfInactivePeers } = getStatistics();
-
- return {
- data: [{
- title: 'Number_of_events',
- value: numberOfEvents,
- }, {
- title: 'Number_of_federated_users',
- value: numberOfFederatedUsers,
- }, {
- title: 'Number_of_active_peers',
- value: numberOfActivePeers,
- }, {
- title: 'Number_of_inactive_peers',
- value: numberOfInactivePeers,
- }],
- };
-}
-
-export function federationGetPeerStatuses() {
- if (!Meteor.userId()) {
- throw new Meteor.Error('not-authorized');
- }
-
- 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: peerStatuses,
- };
-}
+import { federationGetServers, federationGetOverviewData } from '../functions/dashboard';
Meteor.methods({
'federation:getOverviewData': federationGetOverviewData,
- 'federation:getPeerStatuses': federationGetPeerStatuses,
+ 'federation:getServers': federationGetServers,
});
diff --git a/app/federation/server/methods/index.js b/app/federation/server/methods/index.js
new file mode 100644
index 00000000000..26d3e1f619f
--- /dev/null
+++ b/app/federation/server/methods/index.js
@@ -0,0 +1,3 @@
+import './dashboard';
+import './loadContextEvents';
+import './testSetup';
diff --git a/app/federation/server/methods/loadContextEvents.js b/app/federation/server/methods/loadContextEvents.js
new file mode 100644
index 00000000000..c68700d3ab4
--- /dev/null
+++ b/app/federation/server/methods/loadContextEvents.js
@@ -0,0 +1,18 @@
+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
deleted file mode 100644
index 30cace3ad96..00000000000
--- a/app/federation/server/methods/ping.js
+++ /dev/null
@@ -1,54 +0,0 @@
-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
deleted file mode 100644
index b27d411213b..00000000000
--- a/app/federation/server/methods/searchUsers.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Federation } from '..';
-
-export function searchUsers(identifier) {
- if (!Meteor.userId()) {
- 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 federatedUsers = Federation.peerClient.findUsers(identifier);
-
- if (!federatedUsers.length) {
- throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ identifier }"`);
- }
-
- return federatedUsers;
-}
diff --git a/app/federation/server/methods/testSetup.js b/app/federation/server/methods/testSetup.js
new file mode 100644
index 00000000000..0e242fac376
--- /dev/null
+++ b/app/federation/server/methods/testSetup.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+
+import { eventTypes } from '../../../models/server/models/FederationEvents';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+Meteor.methods({
+ FEDERATION_Test_Setup() {
+ try {
+ dispatchEvent([getFederationDomain()], {
+ 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/index.js b/app/federation/server/normalizers/index.js
new file mode 100644
index 00000000000..ae1e2183626
--- /dev/null
+++ b/app/federation/server/normalizers/index.js
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 00000000000..004c7fe6d57
--- /dev/null
+++ b/app/federation/server/normalizers/message.js
@@ -0,0 +1,96 @@
+import { getNameAndDomain, isFullyQualified } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+
+const denormalizeMessage = (originalResource) => {
+ const resource = { ...originalResource };
+
+ const [username, domain] = getNameAndDomain(resource.u.username);
+
+ const localDomain = getFederationDomain();
+
+ // Denormalize username
+ resource.u.username = domain === localDomain ? 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 === localDomain) {
+ 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 === localDomain) {
+ 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 }@${ getFederationDomain() }` : resource.u.username;
+
+ // Federation
+ resource.federation = resource.federation || {
+ origin: getFederationDomain(), // 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 }@${ getFederationDomain() }`;
+
+ 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 }@${ getFederationDomain() }`;
+
+ 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
new file mode 100644
index 00000000000..4aceb9dbdb9
--- /dev/null
+++ b/app/federation/server/normalizers/room.js
@@ -0,0 +1,93 @@
+import { getNameAndDomain, isFullyQualified } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+
+const denormalizeRoom = (originalResource) => {
+ const resource = { ...originalResource };
+
+ if (resource.t === 'd') {
+ resource.usernames = resource.usernames.map((u) => {
+ const [username, domain] = getNameAndDomain(u);
+
+ return domain === getFederationDomain() ? username : u;
+ });
+ } else {
+ // Denormalize room name
+ const [roomName, roomDomain] = getNameAndDomain(resource.name);
+
+ resource.name = roomDomain === getFederationDomain() ? roomName : resource.name;
+
+ // Denormalize room owner name
+ const [username, userDomain] = getNameAndDomain(resource.u.username);
+
+ resource.u.username = userDomain === getFederationDomain() ? username : resource.u.username;
+
+ // Denormalize muted users
+ if (resource.muted) {
+ resource.muted = resource.muted.map((u) => {
+ const [username, domain] = getNameAndDomain(u);
+
+ return domain === getFederationDomain() ? username : u;
+ });
+ }
+
+ // Denormalize unmuted users
+ if (resource.unmuted) {
+ resource.unmuted = resource.unmuted.map((u) => {
+ const [username, domain] = getNameAndDomain(u);
+
+ return domain === getFederationDomain() ? 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 }@${ getFederationDomain() }` : 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 }@${ getFederationDomain() }` : 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 }@${ getFederationDomain() }` : resource.u.username;
+
+ // Normalize the muted users
+ if (resource.muted) {
+ resource.muted = resource.muted.map((u) => (!isFullyQualified(u) ? `${ u }@${ getFederationDomain() }` : u));
+ }
+
+ // Normalize the unmuted users
+ if (resource.unmuted) {
+ resource.unmuted = resource.unmuted.map((u) => (!isFullyQualified(u) ? `${ u }@${ getFederationDomain() }` : u));
+ }
+ }
+
+ // Federation
+ resource.federation = resource.federation || {
+ origin: getFederationDomain(), // 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
new file mode 100644
index 00000000000..814ac79696f
--- /dev/null
+++ b/app/federation/server/normalizers/subscription.js
@@ -0,0 +1,42 @@
+import { getNameAndDomain, isFullyQualified } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+
+const denormalizeSubscription = (originalResource) => {
+ const resource = { ...originalResource };
+
+ const [username, domain] = getNameAndDomain(resource.u.username);
+
+ resource.u.username = domain === getFederationDomain() ? username : resource.u.username;
+
+ const [nameUsername, nameDomain] = getNameAndDomain(resource.name);
+
+ resource.name = nameDomain === getFederationDomain() ? 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 }@${ getFederationDomain() }` : resource.u.username;
+
+ resource.name = !isFullyQualified(resource.name) ? `${ resource.name }@${ getFederationDomain() }` : resource.name;
+
+ // Federation
+ resource.federation = resource.federation || {
+ origin: getFederationDomain(), // 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
new file mode 100644
index 00000000000..8dbc50a1682
--- /dev/null
+++ b/app/federation/server/normalizers/user.js
@@ -0,0 +1,61 @@
+import _ from 'underscore';
+
+import { Users } from '../../../models/server';
+import { getNameAndDomain, isFullyQualified } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+
+const denormalizeUser = (originalResource) => {
+ const resource = { ...originalResource };
+
+ resource.emails = [{
+ address: resource.federation.originalInfo.email,
+ }];
+
+ const [username, domain] = getNameAndDomain(resource.username);
+
+ resource.username = domain === getFederationDomain() ? 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 }@${ getFederationDomain() }`,
+ }];
+
+ resource.active = true;
+ resource.roles = ['user'];
+ resource.status = 'online';
+ resource.username = !isFullyQualified(resource.username) ? `${ resource.username }@${ getFederationDomain() }` : resource.username;
+
+ // Federation
+ resource.federation = resource.federation || {
+ origin: getFederationDomain(),
+ originalInfo: {
+ email,
+ },
+ };
+
+ resource.isRemote = resource.federation.origin !== getFederationDomain();
+
+ // 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
deleted file mode 100644
index 344d6545e95..00000000000
--- a/app/federation/server/settingsUpdater.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Settings } from '../../models';
-
-let nextStatus;
-
-export function updateStatus(status) {
- Settings.updateValueById('FEDERATION_Status', nextStatus || status);
-
- nextStatus = null;
-}
-
-export function updateNextStatusTo(status) {
- nextStatus = status;
-}
-
-export function updateEnabled(enabled) {
- Settings.updateValueById('FEDERATION_Enabled', enabled);
-}
diff --git a/app/federation/server/startup/generateKeys.js b/app/federation/server/startup/generateKeys.js
new file mode 100644
index 00000000000..012cdd0b48f
--- /dev/null
+++ b/app/federation/server/startup/generateKeys.js
@@ -0,0 +1,6 @@
+import { FederationKeys } from '../../../models/server';
+
+// Create key pair if needed
+if (!FederationKeys.getPublicKey()) {
+ FederationKeys.generateKeys();
+}
diff --git a/app/federation/server/startup/index.js b/app/federation/server/startup/index.js
new file mode 100644
index 00000000000..17714a5c69c
--- /dev/null
+++ b/app/federation/server/startup/index.js
@@ -0,0 +1,3 @@
+import './generateKeys';
+import './settings';
+import './registerCallbacks';
diff --git a/app/federation/server/startup/registerCallbacks.js b/app/federation/server/startup/registerCallbacks.js
new file mode 100644
index 00000000000..677b4daddcc
--- /dev/null
+++ b/app/federation/server/startup/registerCallbacks.js
@@ -0,0 +1,24 @@
+import { registerCallback } from '../lib/callbacks';
+import { definition as afterAddedToRoomDef } from '../hooks/afterAddedToRoom';
+import { definition as afterCreateDirectRoomDef } from '../hooks/afterCreateDirectRoom';
+import { definition as afterCreateRoomDef } from '../hooks/afterCreateRoom';
+import { definition as afterDeleteMessageDef } from '../hooks/afterDeleteMessage';
+import { definition as afterMuteUserDef } from '../hooks/afterMuteUser';
+import { definition as afterRemoveFromRoomDef } from '../hooks/afterRemoveFromRoom';
+import { definition as afterSaveMessageDef } from '../hooks/afterSaveMessage';
+import { definition as afterSetReactionDef } from '../hooks/afterSetReaction';
+import { definition as afterUnmuteUserDef } from '../hooks/afterUnmuteUser';
+import { definition as afterUnsetReactionDef } from '../hooks/afterUnsetReaction';
+import { definition as beforeDeleteRoomDef } from '../hooks/beforeDeleteRoom';
+
+registerCallback(afterAddedToRoomDef);
+registerCallback(afterCreateDirectRoomDef);
+registerCallback(afterCreateRoomDef);
+registerCallback(afterDeleteMessageDef);
+registerCallback(afterMuteUserDef);
+registerCallback(beforeDeleteRoomDef);
+registerCallback(afterSaveMessageDef);
+registerCallback(afterSetReactionDef);
+registerCallback(afterUnmuteUserDef);
+registerCallback(afterUnsetReactionDef);
+registerCallback(afterRemoveFromRoomDef);
diff --git a/app/federation/server/startup/settings.js b/app/federation/server/startup/settings.js
new file mode 100644
index 00000000000..3bcb454523a
--- /dev/null
+++ b/app/federation/server/startup/settings.js
@@ -0,0 +1,105 @@
+import { debounce } from 'underscore';
+import { Meteor } from 'meteor/meteor';
+
+import { settings } from '../../../settings/server';
+import { updateStatus, updateEnabled } from '../functions/helpers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { getFederationDiscoveryMethod } from '../lib/getFederationDiscoveryMethod';
+import { registerWithHub } from '../lib/dns';
+import { enableCallbacks, disableCallbacks } from '../lib/callbacks';
+import { logger } from '../lib/logger';
+import { FederationKeys } from '../../../models/server';
+
+Meteor.startup(function() {
+ const federationPublicKey = FederationKeys.getPublicKeyString();
+
+ settings.addGroup('Federation', function() {
+ this.add('FEDERATION_Enabled', false, {
+ type: 'boolean',
+ i18nLabel: 'Enabled',
+ i18nDescription: 'FEDERATION_Enabled',
+ alert: 'FEDERATION_Enabled_Alert',
+ public: true,
+ });
+
+ this.add('FEDERATION_Status', 'Disabled', {
+ readonly: true,
+ type: 'string',
+ i18nLabel: 'FEDERATION_Status',
+ });
+
+ this.add('FEDERATION_Domain', '', {
+ type: 'string',
+ i18nLabel: 'FEDERATION_Domain',
+ i18nDescription: 'FEDERATION_Domain_Description',
+ alert: 'FEDERATION_Domain_Alert',
+ disableReset: true,
+ });
+
+ this.add('FEDERATION_Public_Key', federationPublicKey, {
+ readonly: true,
+ type: 'string',
+ multiline: true,
+ i18nLabel: 'FEDERATION_Public_Key',
+ i18nDescription: 'FEDERATION_Public_Key_Description',
+ });
+
+ this.add('FEDERATION_Discovery_Method', 'dns', {
+ type: 'select',
+ values: [{
+ key: 'dns',
+ i18nLabel: 'DNS',
+ }, {
+ key: 'hub',
+ i18nLabel: 'Hub',
+ }],
+ i18nLabel: 'FEDERATION_Discovery_Method',
+ i18nDescription: 'FEDERATION_Discovery_Method_Description',
+ public: true,
+ });
+
+ this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', {
+ type: 'action',
+ actionText: 'FEDERATION_Test_Setup',
+ });
+ });
+});
+
+const updateSettings = debounce(Meteor.bindEnvironment(function() {
+ // Get the key pair
+
+ if (getFederationDiscoveryMethod() === 'hub') {
+ // Register with hub
+ try {
+ registerWithHub(getFederationDomain(), settings.get('Site_Url'), FederationKeys.getPublicKeyString());
+ } catch (err) {
+ // Disable federation
+ updateEnabled(false);
+
+ updateStatus('Could not register with Hub');
+ }
+ } else {
+ updateStatus('Enabled');
+ }
+}), 150);
+
+function enableOrDisable(key, value) {
+ logger.setup.info(`Federation is ${ value ? 'enabled' : 'disabled' }`);
+
+ if (value) {
+ updateSettings();
+
+ enableCallbacks();
+ } else {
+ updateStatus('Disabled');
+
+ disableCallbacks();
+ }
+
+ value && updateSettings();
+}
+
+// Add settings listeners
+settings.get('FEDERATION_Enabled', enableOrDisable);
+settings.get('FEDERATION_Domain', updateSettings);
+settings.get('FEDERATION_Discovery_Method', updateSettings);
diff --git a/app/lib/server/functions/createDirectRoom.js b/app/lib/server/functions/createDirectRoom.js
new file mode 100644
index 00000000000..67d516adff7
--- /dev/null
+++ b/app/lib/server/functions/createDirectRoom.js
@@ -0,0 +1,47 @@
+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 31eb76285e1..116c6f5e92d 100644
--- a/app/lib/server/functions/createRoom.js
+++ b/app/lib/server/functions/createRoom.js
@@ -7,52 +7,7 @@ import { callbacks } from '../../../callbacks';
import { addUserRoles } from '../../../authorization';
import { getValidRoomName } from '../../../utils';
import { Apps } from '../../../apps/server';
-
-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',
- };
-}
+import { createDirectRoom } from './createDirectRoom';
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 8d113906d8b..a00d11d0acf 100644
--- a/app/lib/server/functions/deleteRoom.js
+++ b/app/lib/server/functions/deleteRoom.js
@@ -4,6 +4,7 @@ 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 e40b6c88f49..5f127edb1bf 100644
--- a/app/lib/server/functions/deleteUser.js
+++ b/app/lib/server/functions/deleteUser.js
@@ -2,17 +2,24 @@ import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { FileUpload } from '../../../file-upload';
-import { Users, Subscriptions, Messages, Rooms, Integrations, FederationPeers } from '../../../models';
+import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } 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 },
+ fields: { username: 1, avatarOrigin: 1, federation: 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 = [];
@@ -99,7 +106,6 @@ export const deleteUser = function(userId) {
Users.removeById(userId); // Remove user from users database
- // Refresh the peers list
- const { peer: { domain: localPeerDomain } } = getConfig();
- FederationPeers.refreshPeers(localPeerDomain);
+ // Refresh the servers list
+ FederationServers.refreshServers();
};
diff --git a/app/lib/server/functions/index.js b/app/lib/server/functions/index.js
index 7a94e02f36f..f318ad2ccbb 100644
--- a/app/lib/server/functions/index.js
+++ b/app/lib/server/functions/index.js
@@ -6,6 +6,7 @@ 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 6d15ed22883..40c3c7dc823 100644
--- a/app/logger/server/server.js
+++ b/app/logger/server/server.js
@@ -289,6 +289,11 @@ 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
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/app/models/server/index.js b/app/models/server/index.js
index f788aed8eaa..81b288f34d5 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 { FederationEvents } from './models/FederationEvents';
+export { FederationRoomEvents } from './models/FederationRoomEvents';
export { FederationKeys } from './models/FederationKeys';
-export { FederationPeers } from './models/FederationPeers';
+export { FederationServers } from './models/FederationServers';
export {
Base,
diff --git a/app/models/server/models/FederationEvents.js b/app/models/server/models/FederationEvents.js
index 55dbcb366ac..bde42d59eb1 100644
--- a/app/models/server/models/FederationEvents.js
+++ b/app/models/server/models/FederationEvents.js
@@ -1,271 +1,160 @@
-import { Meteor } from 'meteor/meteor';
+import { SHA256 } from 'meteor/sha';
import { Base } from './_Base';
-const normalizePeers = (basePeers, options) => {
- const { peers: sentPeers, skipPeers } = options;
-
- let peers = sentPeers || basePeers || [];
-
- if (skipPeers) {
- peers = peers.filter((p) => skipPeers.indexOf(p) === -1);
- }
-
- return peers;
+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',
};
-//
-// 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({ t: 1 });
- this.tryEnsureIndex({ fulfilled: 1 });
- this.tryEnsureIndex({ ts: 1 });
- }
-
- // Sometimes events errored but the error is final
- setEventAsErrored(e, error, fulfilled = false) {
- this.update({ _id: e._id }, {
- $set: {
- fulfilled,
- lastAttemptAt: new Date(),
- error,
- },
- });
- }
-
- 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);
-
- Meteor.defer(() => {
- this.emit('createEvent', record);
- });
-
- return record;
- }
-
- createEventForPeers(type, payload, peers, options = {}) {
- const records = [];
-
- for (const peer of peers) {
- const record = this.createEvent(type, payload, peer, options);
-
- records.push(record);
+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;
}
- 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(),
- };
-
- return this.createEventForPeers('drc', payload, peers);
- }
-
- // Create a `roomCreated(roc)` event
- roomCreated(federatedRoom, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
-
- const payload = {
- room: federatedRoom.getRoom(),
- owner: federatedRoom.getOwner(),
- users: federatedRoom.getUsers(),
- };
-
- return this.createEventForPeers('roc', payload, peers);
- }
-
- // Create a `userJoined(usj)` event
- userJoined(federatedRoom, federatedUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
-
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- user: federatedUser.getUser(),
- };
-
- return this.createEventForPeers('usj', payload, peers);
- }
-
- // Create a `userAdded(usa)` event
- userAdded(federatedRoom, federatedUser, federatedInviter, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ return 'undefined';
+ },
+};
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_inviter_id: federatedInviter.getFederationId(),
- user: federatedUser.getUser(),
- };
+export class FederationEventsModel extends Base {
+ constructor(nameOrModel) {
+ super(nameOrModel);
- return this.createEventForPeers('usa', payload, peers);
+ this.tryEnsureIndex({ hasChildren: 1 }, { sparse: true });
+ this.tryEnsureIndex({ timestamp: 1 });
}
- // Create a `userLeft(usl)` event
- userLeft(federatedRoom, federatedUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
-
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_user_id: federatedUser.getFederationId(),
- };
-
- return this.createEventForPeers('usl', payload, peers);
+ getEventHash(contextQuery, event) {
+ return SHA256(`${ event.origin }${ JSON.stringify(contextQuery) }${ event.parentIds.join(',') }${ event.type }${ event.timestamp }${ JSON.stringify(event.data) }`);
}
- // Create a `userRemoved(usr)` event
- userRemoved(federatedRoom, federatedUser, federatedRemovedByUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
-
- 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 createEvent(origin, contextQuery, type, data) {
+ let previousEventsIds = [];
- // Create a `userMuted(usm)` event
- userMuted(federatedRoom, federatedUser, federatedMutedByUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ // 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();
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_user_id: federatedUser.getFederationId(),
- federated_muted_by_user_id: federatedMutedByUser.getFederationId(),
- };
+ // if (!previousEvents.length) {
+ // throw new Error('Could not create event, the context does not exist');
+ // }
- return this.createEventForPeers('usm', payload, peers);
- }
-
- // Create a `userUnmuted(usu)` event
- userUnmuted(federatedRoom, federatedUser, federatedUnmutedByUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ previousEventsIds = previousEvents.map((e) => e._id);
+ }
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_user_id: federatedUser.getFederationId(),
- federated_unmuted_by_user_id: federatedUnmutedByUser.getFederationId(),
+ const event = {
+ origin,
+ context: contextQuery,
+ parentIds: previousEventsIds || [],
+ type,
+ timestamp: new Date(),
+ data,
+ hasChildren: false,
};
- return this.createEventForPeers('usu', payload, peers);
- }
+ event._id = this.getEventHash(contextQuery, event);
- // Create a `messageCreated(msc)` event
- messageCreated(federatedRoom, federatedMessage, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ // this.insert(event);
- const payload = {
- message: federatedMessage.getMessage(),
- };
+ // Clear the "hasChildren" of those events
+ await this.update({ _id: { $in: previousEventsIds } }, { $unset: { hasChildren: '' } }, { multi: 1 });
- return this.createEventForPeers('msc', payload, peers);
+ return event;
}
- // Create a `messageUpdated(msu)` event
- messageUpdated(federatedRoom, federatedMessage, federatedUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ 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 });
- const payload = {
- message: federatedMessage.getMessage(),
- federated_user_id: federatedUser.getFederationId(),
- };
+ if (genesisEvent) {
+ throw new Error(`A GENESIS event for this context query already exists: ${ JSON.stringify(contextQuery, null, 2) }`);
+ }
- return this.createEventForPeers('msu', payload, peers);
+ return this.createEvent(origin, contextQuery, eventTypes.GENESIS, data);
}
- // Create a `deleteMessage(msd)` event
- messageDeleted(federatedRoom, federatedMessage, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ async addEvent(contextQuery, event) {
+ // Check if the event does not exit
+ const existingEvent = this.findOne({ _id: event._id });
- const payload = {
- federated_message_id: federatedMessage.getFederationId(),
- };
+ // 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);
- return this.createEventForPeers('msd', payload, peers);
- }
+ // This means that we do not have the parents of the event we are adding
+ if (parentIds.length !== event.parentIds.length) {
+ const { origin } = event;
- // Create a `messagesRead(msr)` event
- messagesRead(federatedRoom, federatedUser, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ // 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);
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_user_id: federatedUser.getFederationId(),
- };
+ return {
+ success: false,
+ reason: 'missingParents',
+ missingParentIds: event.parentIds.filter(({ _id }) => parentIds.indexOf(_id) === -1),
+ latestEventIds,
+ };
+ }
- return this.createEventForPeers('msr', payload, peers);
- }
+ // Clear the "hasChildren" of the parent events
+ await this.update({ _id: { $in: parentIds } }, { $unset: { hasChildren: '' } }, { multi: 1 });
- // Create a `messagesSetReaction(mrs)` event
- messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ this.insert(event);
+ }
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_message_id: federatedMessage.getFederationId(),
- federated_user_id: federatedUser.getFederationId(),
- reaction,
- shouldReact,
+ return {
+ success: true,
};
-
- return this.createEventForPeers('mrs', payload, peers);
}
- // Create a `messagesUnsetReaction(mru)` event
- messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) {
- const peers = normalizePeers(federatedRoom.getPeers(), options);
+ async getEventById(contextQuery, eventId) {
+ const event = await this.model
+ .rawCollection()
+ .findOne({ context: contextQuery, _id: eventId });
- const payload = {
- federated_room_id: federatedRoom.getFederationId(),
- federated_message_id: federatedMessage.getFederationId(),
- federated_user_id: federatedUser.getFederationId(),
- reaction,
- shouldReact,
+ return {
+ success: !!event,
+ event,
};
-
- return this.createEventForPeers('mru', payload, peers);
}
- // Get all unfulfilled events
- getUnfulfilled() {
- return this.find({ fulfilled: false }, { sort: { ts: 1 } });
+ async getLatestEvents(contextQuery, fromTimestamp) {
+ return this.model.rawCollection().find({ context: contextQuery, timestamp: { $gt: new Date(fromTimestamp) } }).toArray();
}
- findByType(t) {
- return this.find({ t });
+ async removeContextEvents(contextQuery) {
+ return this.model.rawCollection().remove({ context: contextQuery });
}
}
-
-export const FederationEvents = new FederationEventsModel();
diff --git a/app/models/server/models/FederationKeys.js b/app/models/server/models/FederationKeys.js
index 8e2e9c26756..188f7cdc434 100644
--- a/app/models/server/models/FederationKeys.js
+++ b/app/models/server/models/FederationKeys.js
@@ -1,5 +1,4 @@
import NodeRSA from 'node-rsa';
-import uuid from 'uuid/v4';
import { Base } from './_Base';
@@ -35,16 +34,6 @@ 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
deleted file mode 100644
index 283d2f31e76..00000000000
--- a/app/models/server/models/FederationPeers.js
+++ /dev/null
@@ -1,60 +0,0 @@
-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
new file mode 100644
index 00000000000..a7b375fdd45
--- /dev/null
+++ b/app/models/server/models/FederationRoomEvents.js
@@ -0,0 +1,67 @@
+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
new file mode 100644
index 00000000000..9daf20d5a12
--- /dev/null
+++ b/app/models/server/models/FederationServers.js
@@ -0,0 +1,26 @@
+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 64e40d15729..0489bc85226 100644
--- a/app/models/server/models/Messages.js
+++ b/app/models/server/models/Messages.js
@@ -850,16 +850,6 @@ 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 cfcda4d4f0b..6ed4ff8b85b 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, localPeer) {
+ findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) {
const extraQuery = [
{
$or: [
{ federation: { $exists: false } },
- { 'federation.peer': localPeer },
+ { 'federation.origin': localDomain },
],
},
];
return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery);
}
- findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) {
+ findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) {
const extraQuery = [
{ federation: { $exists: true } },
- { 'federation.peer': { $ne: localPeer } },
+ { 'federation.origin': { $ne: localDomain } },
];
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 ff9077f9e14..a9bffcdaff8 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 } });
}
- getDistinctFederationPeers() {
- return this.col.distinct('federation.peer', { federation: { $exists: true } });
+ getDistinctFederationDomains() {
+ return this.col.distinct('federation.origin', { federation: { $exists: true } });
}
async getNextLeastBusyAgent(department) {
diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js
index 32c8801827b..2501754e680 100644
--- a/app/statistics/server/functions/get.js
+++ b/app/statistics/server/functions/get.js
@@ -20,7 +20,7 @@ import { Info, getMongoInfo } from '../../../utils/server';
import { Migrations } from '../../../migrations/server';
import { statistics } from '../statisticsNamespace';
import { Apps } from '../../../apps/server';
-import { getStatistics as federationGetStatistics } from '../../../federation/server/methods/dashboard';
+import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard';
const wizardFields = [
'Organization_Type',
@@ -94,8 +94,7 @@ statistics.get = function _getStatistics() {
// Federation statistics
const federationOverviewData = federationGetStatistics();
- statistics.federatedServers = federationOverviewData.numberOfActivePeers + federationOverviewData.numberOfInactivePeers;
- statistics.federatedServersActive = federationOverviewData.numberOfActivePeers;
+ statistics.federatedServers = federationOverviewData.numberOfServers;
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 a209293b1fc..fc22abf3aac 100644
--- a/app/ui/client/views/app/directory.html
+++ b/app/ui/client/views/app/directory.html
@@ -113,13 +113,15 @@
{{/if}}
{{#if $eq searchWorkspace 'external'}}
-
- {{_ "Domain"}} {{> icon icon=(sortIcon 'domain') }}
+ |
+ {{_ "Domain"}} {{> icon icon=(sortIcon 'origin') }}
|
{{/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 ba4af117e7f..73299732aa5 100644
--- a/app/ui/client/views/app/directory.js
+++ b/app/ui/client/views/app/directory.js
@@ -33,7 +33,8 @@ 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),
- domain: result.federation && result.federation.peer,
+ origin: result.federation && result.federation.origin,
+ isRemote: result.isRemote,
};
}
return null;
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index badfca41d0e..a85695a322e 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -1368,12 +1368,14 @@
"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",
@@ -2277,10 +2279,9 @@
"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_inactive_peers": "Number of inactive peers",
+ "Number_of_federated_servers": "Number of federated servers",
"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 64ab04e377d..25dd440b665 100644
--- a/server/methods/browseChannels.js
+++ b/server/methods/browseChannels.js
@@ -4,8 +4,10 @@ import s from 'underscore.string';
import { hasPermission } from '../../app/authorization';
import { Rooms, Users } from '../../app/models';
-import { Federation } from '../../app/federation/server';
import { settings } from '../../app/settings/server';
+import { getFederationDomain } from '../../app/federation/server/lib/getFederationDomain';
+import { isFederationEnabled } from '../../app/federation/server/lib/isFederationEnabled';
+import { federationSearchUsers } from '../../app/federation/server/handler';
const sortChannels = function(field, direction) {
switch (field) {
@@ -118,32 +120,28 @@ Meteor.methods({
if (workspace === 'all') {
result = Users.findByActiveUsersExcept(text, exceptions, options, forcedSearchFields);
} else if (workspace === 'external') {
- result = Users.findByActiveExternalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier);
+ result = Users.findByActiveExternalUsersExcept(text, exceptions, options, forcedSearchFields, getFederationDomain());
} else {
- result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier);
+ result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, getFederationDomain());
}
const total = result.count(); // count ignores the `skip` and `limit` options
const results = result.fetch();
// Try to find federated users, when appliable
- if (Federation.enabled && type === 'users' && workspace === 'external' && text.indexOf('@') !== -1) {
- const federatedUsers = Federation.methods.searchUsers(text);
+ if (isFederationEnabled() && type === 'users' && workspace === 'external' && text.indexOf('@') !== -1) {
+ const users = federationSearchUsers(text);
- 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; }
+ for (const user of users) {
+ if (results.find((e) => e._id === user._id)) { 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 2fb89543598..c2d2dcc9d17 100644
--- a/server/methods/createDirectMessage.js
+++ b/server/methods/createDirectMessage.js
@@ -7,7 +7,7 @@ import { Users, Rooms, Subscriptions } from '../../app/models';
import { getDefaultSubscriptionPref } from '../../app/utils';
import { RateLimiter } from '../../app/lib';
import { callbacks } from '../../app/callbacks';
-import { Federation } from '../../app/federation/server';
+import { addUser } from '../../app/federation/server/functions/addUser';
Meteor.methods({
createDirectMessage(username) {
@@ -41,11 +41,9 @@ 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) {
- // 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);
+ to = addUser(username);
}
if (!to) {
diff --git a/server/startup/migrations/v143.js b/server/startup/migrations/v143.js
index 15f93b2b0bf..1251c7a6002 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, FederationPeers } from '../../../app/models/server';
+import { Users, FederationServers } from '../../../app/models/server';
Migrations.add({
version: 143,
@@ -15,7 +15,7 @@ Migrations.add({
}));
if (peers.length) {
- FederationPeers.model.rawCollection().insertMany(peers);
+ FederationServers.model.rawCollection().insertMany(peers);
}
},
});
diff --git a/server/startup/migrations/v148.js b/server/startup/migrations/v148.js
index a72665351dd..62740cdd2d0 100644
--- a/server/startup/migrations/v148.js
+++ b/server/startup/migrations/v148.js
@@ -1,5 +1,5 @@
import { Migrations } from '../../../app/migrations/server';
-import { Users, Settings, FederationPeers } from '../../../app/models/server';
+import { Users, Settings, FederationServers } from '../../../app/models/server';
Migrations.add({
version: 148,
@@ -10,7 +10,9 @@ Migrations.add({
return;
}
- const { value: localDomain } = domainSetting;
+ const { value: domain } = domainSetting;
+
+ const localDomain = domain.replace('@', '');
Users.update({
federation: { $exists: true }, 'federation.peer': { $ne: localDomain },
@@ -18,13 +20,13 @@ Migrations.add({
$set: { isRemote: true },
}, { multi: true });
- FederationPeers.update({
+ FederationServers.update({
peer: { $ne: localDomain },
}, {
$set: { isRemote: true },
}, { multi: true });
- FederationPeers.update({
+ FederationServers.update({
peer: localDomain,
}, {
$set: { isRemote: false },