Revert federation (#15278)

* Revert "Federation improvements (#15234)"

This reverts commit f5b0a610bf.

* Revert "[BREAK] Federation refactor with addition of chained events (#15206)"

This reverts commit 4166e0b459.
pull/15258/head^2
Diego Sampaio 6 years ago committed by Rodrigo Nascimento
parent d8d3e8a9e6
commit 81b9fbb944
  1. 7
      app/federation/client/admin/dashboard.html
  2. 20
      app/federation/client/admin/dashboard.js
  3. 141
      app/federation/client/admin/visualizer.css
  4. 10
      app/federation/client/admin/visualizer.html
  5. 139
      app/federation/client/admin/visualizer.js
  6. 6
      app/federation/client/index.js
  7. 23
      app/federation/client/messageTypes.js
  8. 29
      app/federation/client/tabBar.js
  9. 24
      app/federation/client/views/federationFlexTab.html
  10. 15
      app/federation/client/views/federationFlexTab.js
  11. 638
      app/federation/server/PeerClient.js
  12. 185
      app/federation/server/PeerDNS.js
  13. 100
      app/federation/server/PeerHTTP/PeerHTTP.js
  14. 1
      app/federation/server/PeerHTTP/index.js
  15. 16
      app/federation/server/PeerHTTP/utils.js
  16. 37
      app/federation/server/PeerPinger.js
  17. 404
      app/federation/server/PeerServer/PeerServer.js
  18. 6
      app/federation/server/PeerServer/index.js
  19. 115
      app/federation/server/PeerServer/routes/events.js
  20. 2
      app/federation/server/PeerServer/routes/uploads.js
  21. 48
      app/federation/server/PeerServer/routes/users.js
  22. 65
      app/federation/server/_client/callbacks/afterAddedToRoom.js
  23. 73
      app/federation/server/_client/callbacks/afterCreateDirectRoom.js
  24. 83
      app/federation/server/_client/callbacks/afterCreateRoom.js
  25. 27
      app/federation/server/_client/callbacks/afterDeleteMessage.js
  26. 28
      app/federation/server/_client/callbacks/afterMuteUser.js
  27. 52
      app/federation/server/_client/callbacks/afterRemoveFromRoom.js
  28. 34
      app/federation/server/_client/callbacks/afterSaveMessage.js
  29. 29
      app/federation/server/_client/callbacks/afterSetReaction.js
  30. 28
      app/federation/server/_client/callbacks/afterUnmuteUser.js
  31. 29
      app/federation/server/_client/callbacks/afterUnsetReaction.js
  32. 36
      app/federation/server/_client/callbacks/beforeDeleteRoom.js
  33. 38
      app/federation/server/_client/callbacks/helpers/federatedResources.js
  34. 36
      app/federation/server/_client/callbacks/helpers/getFederatedRoomData.js
  35. 3
      app/federation/server/_client/callbacks/helpers/getFederatedUserData.js
  36. 123
      app/federation/server/_client/client.js
  37. 1
      app/federation/server/_client/index.js
  38. 410
      app/federation/server/_server/endpoints/events/dispatch.js
  39. 2
      app/federation/server/_server/endpoints/events/index.js
  40. 51
      app/federation/server/_server/endpoints/events/requestFromLatest.js
  41. 57
      app/federation/server/_server/endpoints/users.js
  42. 1
      app/federation/server/_server/index.js
  43. 3
      app/federation/server/_server/server.js
  44. 74
      app/federation/server/config.js
  45. 68
      app/federation/server/crypt.js
  46. 109
      app/federation/server/dns.js
  47. 6
      app/federation/server/errors.js
  48. 22
      app/federation/server/eventCrypto.js
  49. 263
      app/federation/server/federatedResources/FederatedMessage.js
  50. 17
      app/federation/server/federatedResources/FederatedResource.js
  51. 275
      app/federation/server/federatedResources/FederatedRoom.js
  52. 125
      app/federation/server/federatedResources/FederatedUser.js
  53. 4
      app/federation/server/federatedResources/index.js
  54. 21
      app/federation/server/federation-settings.js
  55. 88
      app/federation/server/federation.js
  56. 55
      app/federation/server/http.js
  57. 150
      app/federation/server/index.js
  58. 10
      app/federation/server/logger.js
  59. 45
      app/federation/server/methods/addUser.js
  60. 49
      app/federation/server/methods/dashboard.js
  61. 18
      app/federation/server/methods/loadContextEvents.js
  62. 54
      app/federation/server/methods/ping.js
  63. 18
      app/federation/server/methods/searchUsers.js
  64. 20
      app/federation/server/methods/testSetup.js
  65. 2
      app/federation/server/normalizers/helpers/federatedResources.js
  66. 11
      app/federation/server/normalizers/index.js
  67. 94
      app/federation/server/normalizers/message.js
  68. 93
      app/federation/server/normalizers/room.js
  69. 42
      app/federation/server/normalizers/subscription.js
  70. 61
      app/federation/server/normalizers/user.js
  71. 10
      app/federation/server/settingsUpdater.js
  72. 47
      app/lib/server/functions/createDirectRoom.js
  73. 47
      app/lib/server/functions/createRoom.js
  74. 1
      app/lib/server/functions/deleteRoom.js
  75. 18
      app/lib/server/functions/deleteUser.js
  76. 1
      app/lib/server/functions/index.js
  77. 5
      app/logger/server/server.js
  78. 0
      app/models/client/models/FederationPeers.js
  79. 4
      app/models/server/index.js
  80. 339
      app/models/server/models/FederationEvents.js
  81. 11
      app/models/server/models/FederationKeys.js
  82. 60
      app/models/server/models/FederationPeers.js
  83. 67
      app/models/server/models/FederationRoomEvents.js
  84. 26
      app/models/server/models/FederationServers.js
  85. 10
      app/models/server/models/Messages.js
  86. 8
      app/models/server/models/Users.js
  87. 4
      app/models/server/raw/Users.js
  88. 3
      app/statistics/server/functions/get.js
  89. 2
      app/ui/client/views/app/directory.html
  90. 3
      app/ui/client/views/app/directory.js
  91. 1
      package.json
  92. 5
      packages/rocketchat-i18n/i18n/en.i18n.json
  93. 16
      server/methods/browseChannels.js
  94. 6
      server/methods/createDirectMessage.js
  95. 1
      server/publications/room/index.js
  96. 4
      server/startup/migrations/v143.js
  97. 10
      server/startup/migrations/v148.js

@ -16,10 +16,11 @@
{{/each}}
</div>
<div class="group left wrap border-component-color">
{{#each federationPeers}}
{{#each federationPeerStatuses}}
<div class="overview-column small">
<div class="overview-pill">
<span class="title">{{domain}}</span>
<div class="overview-pill" title="{{status}} - {{statusAt}}">
<div class="status {{status}}"></div>
<span class="title">{{peer}}</span>
</div>
</div>
{{/each}}

@ -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(() => {

@ -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;
}
}

@ -1,10 +0,0 @@
<template name="dashboard">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
{{> header sectionName="Federation_Dashboard"}}
<div class="content">
<div id="network" style="height: 900px"></div>
</div>
</section>
</div>
</template>

@ -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');
},
});

@ -1,6 +1,2 @@
import './messageTypes';
import './admin/dashboard';
import './tabBar';
import './views/federationFlexTab.html';
import './views/federationFlexTab.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,
};
},
});

@ -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');
});
});

@ -1,24 +0,0 @@
<template name="federationFlexTab">
<div class="content">
<div class="main-content-flex">
<section class="page-container page-list flex-tab-main-content">
<div class="content">
<div class="section">
<div class="section-content">
<div class="group left wrap border-component-color">
{{#each federationPeerStatuses}}
<div class="overview-column small">
<div class="overview-pill" title="{{status}} - {{statusAt}}">
<div class="status {{status}}"></div>
<span class="title">{{peer}}</span>
</div>
</div>
{{/each}}
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</template>

@ -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 [];
},
});

@ -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;
}
}

@ -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);
}
}
}

@ -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);
}
}
}
}

@ -0,0 +1 @@
export { PeerHTTP } from './PeerHTTP';

@ -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);
});

@ -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);
}
}

@ -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);
}
}

@ -0,0 +1,6 @@
// Setup routes
import './routes/events';
import './routes/uploads';
import './routes/users';
export { PeerServer } from './PeerServer';

@ -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');
}
},
});

@ -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');
}

@ -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 });
},
});

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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',
};

@ -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,
};
};

@ -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,
};
};

@ -1,3 +0,0 @@
module.exports = (user) => ({
isFederated: user.username.indexOf('@') !== -1,
});

@ -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);

@ -1 +0,0 @@
export { client } from './client';

@ -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();
},
});

@ -1,2 +0,0 @@
import './dispatch';
import './requestFromLatest';

@ -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);
},
});

@ -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 });
},
});

@ -1,3 +0,0 @@
import './endpoints/events';
import './endpoints/uploads';
import './endpoints/users';

@ -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);

@ -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();

@ -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();

@ -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 });

@ -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();
};

@ -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);
};

@ -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 }`);
};

@ -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;
};

@ -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);
};

@ -0,0 +1,4 @@
export { FederatedMessage } from './FederatedMessage';
export { FederatedResource } from './FederatedResource';
export { FederatedRoom } from './FederatedRoom';
export { FederatedUser } from './FederatedUser';

@ -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: [{

@ -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);

@ -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();

@ -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);

@ -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',
},
});

@ -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;
}

@ -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,
});

@ -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();
},
});

@ -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',
};
},
});

@ -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;
}

@ -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');
}
},
});

@ -1,2 +0,0 @@
export const getNameAndDomain = (fullyQualifiedName) => fullyQualifiedName.split('@');
export const isFullyQualified = (name) => name.indexOf('@') !== -1;

@ -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,
};

@ -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,
};

@ -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,
};

@ -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,
};

@ -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,
};

@ -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) {

@ -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',
};
};

@ -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') {

@ -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);

@ -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);
};

@ -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';

@ -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])) {

@ -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,

@ -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();

@ -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');

@ -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();

@ -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();

@ -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();

@ -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 };

@ -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);
}

@ -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) {

@ -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();

@ -117,11 +117,9 @@
<div class="table-fake-th"><span>{{_ "Domain"}}</span> {{> icon icon=(sortIcon 'domain') }}</div>
</th>
{{/if}}
{{#if $eq searchWorkspace 'internal'}}
<th class="table-column-date js-sort {{#if searchSortBy 'createdAt'}}is-sorting{{/if}}" data-sort="createdAt">
<div class="table-fake-th"><span>{{_ "Created_at"}}</span> {{> icon icon=(sortIcon 'createdAt') }}</div>
</th>
{{/if}}
</tr>
</thead>
<tbody>

@ -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;

@ -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",

@ -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",

@ -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,
});
}
}

@ -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) {

@ -51,7 +51,6 @@ export const fields = {
e2eKeyId: 1,
departmentId: 1,
servedBy: 1,
federation: 1,
};
const roomMap = (record) => {

@ -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);
}
},
});

@ -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 },

Loading…
Cancel
Save