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