Added federation ping, loopback and dashboard (#14007)

* Added federation ping event to check if setup works

* Update en.i18n.json

* Added ping process and status tracker

* Added new ping system, also a federation dashboard

* Line wrap fixes

* Fix migration
pull/14265/head^2
Alan Sikora 6 years ago committed by Rodrigo Nascimento
parent 85530cfa28
commit e02187278b
  1. 141
      app/federation/client/admin/dashboard.css
  2. 33
      app/federation/client/admin/dashboard.html
  3. 92
      app/federation/client/admin/dashboard.js
  4. 25
      app/federation/client/index.js
  5. 23
      app/federation/client/messageTypes.js
  6. 4
      app/federation/server/PeerClient.js
  7. 16
      app/federation/server/PeerDNS.js
  8. 11
      app/federation/server/PeerHTTP/PeerHTTP.js
  9. 9
      app/federation/server/PeerHTTP/utils.js
  10. 38
      app/federation/server/PeerPinger.js
  11. 20
      app/federation/server/PeerServer/PeerServer.js
  12. 20
      app/federation/server/PeerServer/routes/events.js
  13. 17
      app/federation/server/federatedResources/FederatedRoom.js
  14. 4
      app/federation/server/federation-settings.js
  15. 11
      app/federation/server/index.js
  16. 1
      app/federation/server/logger.js
  17. 5
      app/federation/server/methods/addUser.js
  18. 69
      app/federation/server/methods/dashboard.js
  19. 53
      app/federation/server/methods/ping.js
  20. 5
      app/lib/server/functions/deleteUser.js
  21. 1
      app/models/server/index.js
  22. 19
      app/models/server/models/FederationEvents.js
  23. 52
      app/models/server/models/FederationPeers.js
  24. 19
      packages/rocketchat-i18n/i18n/en.i18n.json
  25. 1
      server/startup/migrations/index.js
  26. 21
      server/startup/migrations/v143.js

@ -0,0 +1,141 @@
.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;
}
}

@ -0,0 +1,33 @@
<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 class="section">
<div class="section-content">
<div class="group border-component-color">
{{#each federationOverviewData}}
<div class="overview-column">
<div class="overview-item">
<span class="value">{{value}}</span>
<span class="title">{{_ title}}</span>
</div>
</div>
{{/each}}
</div>
<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>
</template>

@ -0,0 +1,92 @@
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 { AdminBox } from '../../../ui-utils';
import { hasRole } from '../../../authorization';
import './dashboard.html';
import './dashboard.css';
// Template controller
let templateInstance; // current template instance/context
// Methods
const updateOverviewData = () => {
Meteor.call('federation:getOverviewData', (error, result) => {
if (error) {
console.log(error);
return;
// return handleError(error);
}
const { data } = result;
templateInstance.federationOverviewData.set(data);
});
};
const updatePeerStatuses = () => {
Meteor.call('federation:getPeerStatuses', (error, result) => {
if (error) {
console.log(error);
return;
// return handleError(error);
}
const { data } = result;
templateInstance.federationPeerStatuses.set(data);
});
};
const updateData = () => {
updateOverviewData();
updatePeerStatuses();
};
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);
setInterval(updateData, 10000);
});
// 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,23 +1,2 @@
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,
};
},
});
import './messageTypes';
import './admin/dashboard';

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

@ -139,7 +139,7 @@ export class PeerClient {
propagateEvent(e) {
this.log(`propagateEvent: ${ e.t }`);
const { peer: domain } = e;
const { peer: domain, options: eventOptions } = e;
const peer = Federation.peerDNS.searchPeer(domain);
@ -157,7 +157,7 @@ export class PeerClient {
// Encrypt with the local private key
payload = Federation.privateKey.encryptPrivate(payload);
Federation.peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, { total: 5, stepSize: 500, stepMultiplier: 10 });
Federation.peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, eventOptions.retry || { total: 5, stepSize: 500, stepMultiplier: 10 });
FederationEvents.setEventAsFullfilled(e);
} catch (err) {

@ -72,9 +72,13 @@ export class PeerDNS {
// Try to lookup at the DNS Cache
if (!peer) {
this.updatePeerDNS(domain);
try {
this.updatePeerDNS(domain);
peer = FederationDNSCache.findOneByDomain(domain);
peer = FederationDNSCache.findOneByDomain(domain);
} catch (err) {
this.log(`Could not find peer for domain ${ domain }`);
}
}
return peer;
@ -127,7 +131,7 @@ export class PeerDNS {
updatePeerDNS(domain) {
this.log(`updatePeerDNS: ${ domain }`);
let peer;
let peer = null;
try {
peer = this.getPeerUsingDNS(domain);
@ -138,7 +142,11 @@ export class PeerDNS {
throw new Error(`Error trying to fetch SRV DNS entries for ${ domain }`);
}
peer = this.getPeerUsingHub(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);

@ -23,8 +23,6 @@ export class PeerHTTP {
//
// Direct request
simpleRequest(peer, method, uri, body, headers) {
this.log(`Request: ${ method } ${ uri }`);
const { url: serverBaseURL } = peer;
const url = `${ serverBaseURL }${ uri }`;
@ -35,7 +33,7 @@ export class PeerHTTP {
data = body;
}
this.log(`Sending request: ${ method } - ${ uri }`);
this.log(`Sending request: ${ method } - ${ url }`);
return HTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': this.config.peer.domain } });
}
@ -75,14 +73,15 @@ export class PeerHTTP {
}
// Check if we need to skip due to specific error
if (skipRetryOnSpecificError(err)) {
this.log('Retry: skipping 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
// Throw the error, as we could not fulfill the request
this.log('Retry: could not fulfill the request');
throw err;

@ -1,13 +1,11 @@
import { Meteor } from 'meteor/meteor';
// Should skip the retry if the error is one of the below?
const errorsToSkipRetrying = [
'error-app-prevented-sending',
];
const errorsToSkipRetrying = ['error-app-prevented-sending', 'error-decrypt'];
export function skipRetryOnSpecificError(err) {
err = err && err.response && err.response.data;
return errorsToSkipRetrying.includes(err && err.errorType);
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
@ -16,4 +14,3 @@ export const delay = Meteor.wrapAsync(function(ms, callback) {
callback(null);
}, ms);
});

@ -0,0 +1,38 @@
import { Meteor } from 'meteor/meteor';
import { logger } from './logger';
import { FederationPeers } from '../../models';
import { ping } from './methods/ping';
import moment from 'moment';
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);
}
}

@ -1,7 +1,7 @@
import { callbacks } from '../../../callbacks';
import { setReaction } from '../../../reactions/server';
import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib';
import { Rooms, Subscriptions } from '../../../models';
import { Rooms, Subscriptions, FederationPeers } from '../../../models';
import { FederatedMessage, FederatedRoom, FederatedUser } from '../federatedResources';
import { logger } from '../logger.js';
@ -56,6 +56,9 @@ export class PeerServer {
// Then, create the room, if needed
federatedRoom.create();
// Refresh federation peers
FederationPeers.refreshPeers();
}
handleRoomCreatedEvent(e) {
@ -76,6 +79,9 @@ export class PeerServer {
// Then, create the room, if needed
federatedRoom.create(true);
// Refresh federation peers
FederationPeers.refreshPeers();
}
handleUserJoinedEvent(e) {
@ -103,6 +109,9 @@ export class PeerServer {
// Refresh room's federation
federatedRoom.refreshFederation();
// Refresh federation peers
FederationPeers.refreshPeers();
}
handleUserAddedEvent(e) {
@ -139,6 +148,9 @@ export class PeerServer {
// Refresh room's federation
federatedRoom.refreshFederation();
// Refresh federation peers
FederationPeers.refreshPeers();
}
handleUserLeftEvent(e) {
@ -166,6 +178,9 @@ export class PeerServer {
// Refresh room's federation
federatedRoom.refreshFederation();
// Refresh federation peers
FederationPeers.refreshPeers();
}
handleUserRemovedEvent(e) {
@ -197,6 +212,9 @@ export class PeerServer {
// Refresh room's federation
federatedRoom.refreshFederation();
// Refresh federation peers
FederationPeers.refreshPeers();
}
handleUserMutedEvent(e) {

@ -1,3 +1,4 @@
import { Meteor } from 'meteor/meteor';
import { API } from '../../../../api';
import { FederationKeys } from '../../../../models';
@ -27,11 +28,17 @@ API.v1.addRoute('federation.events', { authRequired: false }, {
const payloadBuffer = Buffer.from(this.bodyParams.payload.data);
let payload;
// Decrypt with the peer's public key
let payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer);
try {
payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer);
// Decrypt with the local private key
payload = Federation.privateKey.decrypt(payload);
// 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());
@ -44,6 +51,9 @@ API.v1.addRoute('federation.events', { authRequired: false }, {
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;
@ -95,7 +105,9 @@ API.v1.addRoute('federation.events', { authRequired: false }, {
// Respond
return API.v1.success();
} catch (err) {
Federation.peerServer.log(`Error handling event:${ e.t } - ${ err.toString() }`);
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');
}

@ -27,11 +27,9 @@ export class FederatedRoom extends FederatedResource {
const { owner } = extras;
if (owner) {
if (!owner && room.federation) {
this.federatedOwner = FederatedUser.loadByFederationId(localPeerIdentifier, room.federation.ownerId);
} else {
this.federatedOwner = FederatedUser.loadOrCreate(localPeerIdentifier, 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
@ -41,6 +39,9 @@ export class FederatedRoom extends FederatedResource {
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;
}
@ -83,6 +84,12 @@ export class FederatedRoom extends FederatedResource {
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);
}

@ -65,5 +65,9 @@ Meteor.startup(function() {
public: true,
});
this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', {
type: 'action',
actionText: 'Test_Setup',
});
});
});

@ -10,15 +10,19 @@ 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';
const peerClient = new PeerClient();
const peerDNS = new PeerDNS();
const peerHTTP = new PeerHTTP();
const peerPinger = new PeerPinger();
const peerServer = new PeerServer();
export const Federation = {
@ -32,13 +36,15 @@ export const Federation = {
peerClient,
peerDNS,
peerHTTP,
peerPinger,
peerServer,
};
// Add Federation methods with bound context
// Add Federation methods
Federation.methods = {
addUser,
searchUsers,
ping,
};
// Generate keys
@ -61,6 +67,9 @@ 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');

@ -8,5 +8,6 @@ export const logger = new Logger('Federation', {
peerServer: 'Peer Server',
dns: 'DNS',
http: 'HTTP',
pinger: 'Pinger',
},
});

@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
import { Users, FederationPeers } from '../../../models';
import { Federation } from '..';
import { logger } from '../logger';
@ -32,6 +32,9 @@ export function addUser(identifier) {
try {
// Create the local user
user = Users.create(localUser);
// Refresh the peers list
FederationPeers.refreshPeers();
} catch (err) {
// If the user already exists, return the existing user
if (err.code === 11000) {

@ -0,0 +1,69 @@
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { FederationEvents, FederationPeers, Users } from '../../../models';
import { Federation } from '..';
export function federationGetOverviewData() {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized');
}
const numberOfEvents = FederationEvents.find({ t: { $ne: 'png' } }).count();
const numberOfFederatedUsers = Users.find({ federation: { $exists: true }, 'federation.peer': { $ne: Federation.localIdentifier } }).count();
const numberOfActivePeers = FederationPeers.find({ active: true, peer: { $ne: Federation.localIdentifier } }).count();
const numberOfInactivePeers = FederationPeers.find({ active: false, peer: { $ne: Federation.localIdentifier } }).count();
return {
data: [{
title: 'Number_of_events',
value: numberOfEvents,
}, {
title: 'Number_of_federated_users',
value: numberOfFederatedUsers,
}, {
title: 'Number_of_active_peers',
value: numberOfActivePeers,
}, {
title: 'Number_of_inactive_peers',
value: numberOfInactivePeers,
}],
};
}
export function federationGetPeerStatuses() {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized');
}
const peers = FederationPeers.find({ peer: { $ne: Federation.localIdentifier } }).fetch();
const peerStatuses = [];
const stabilityLimit = moment().subtract(5, 'days');
for (const { peer, active, last_seen_at: lastSeenAt, last_failure_at: lastFailureAt } of peers) {
let status = 'failing';
if (active && lastFailureAt && moment(lastFailureAt).isAfter(stabilityLimit)) {
status = 'unstable';
} else if (active) {
status = 'stable';
}
peerStatuses.push({
peer,
status,
statusAt: active ? lastSeenAt : lastFailureAt,
});
}
return {
data: peerStatuses,
};
}
Meteor.methods({
'federation:getOverviewData': federationGetOverviewData,
'federation:getPeerStatuses': federationGetPeerStatuses,
});

@ -0,0 +1,53 @@
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',
};
},
});

@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/tap:i18n';
import { FileUpload } from '../../../file-upload';
import { Users, Subscriptions, Messages, Rooms, Integrations } from '../../../models';
import { Users, Subscriptions, Messages, Rooms, Integrations, FederationPeers } from '../../../models';
import { hasRole, getUsersInRole } from '../../../authorization';
import { settings } from '../../../settings';
import { Notifications } from '../../../notifications';
@ -96,4 +96,7 @@ export const deleteUser = function(userId) {
}
Users.removeById(userId); // Remove user from users database
// Refresh the peers list
FederationPeers.refreshPeers();
};

@ -38,6 +38,7 @@ export { AppsModel } from './models/apps-model';
export { FederationDNSCache } from './models/FederationDNSCache';
export { FederationEvents } from './models/FederationEvents';
export { FederationKeys } from './models/FederationKeys';
export { FederationPeers } from './models/FederationPeers';
export {
Base,

@ -1,3 +1,4 @@
import { Meteor } from 'meteor/meteor';
import { Base } from './_Base';
const normalizePeers = (basePeers, options) => {
@ -38,27 +39,30 @@ class FederationEventsModel extends Base {
});
}
createEvent(type, payload, peer) {
createEvent(type, payload, peer, options) {
const record = {
t: type,
ts: new Date(),
fulfilled: false,
payload,
peer,
options,
};
record._id = this.insert(record);
this.emit('createEvent', record);
Meteor.defer(() => {
this.emit('createEvent', record);
});
return record;
}
createEventForPeers(type, payload, peers) {
createEventForPeers(type, payload, peers, options = {}) {
const records = [];
for (const peer of peers) {
const record = this.createEvent(type, payload, peer);
const record = this.createEvent(type, payload, peer, options);
records.push(record);
}
@ -66,6 +70,11 @@ class FederationEventsModel extends Base {
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);
@ -248,8 +257,6 @@ class FederationEventsModel extends Base {
getUnfulfilled() {
return this.find({ fulfilled: false }, { sort: { ts: 1 } }).fetch();
}
}
export const FederationEvents = new FederationEventsModel();

@ -0,0 +1,52 @@
import { Meteor } from 'meteor/meteor';
import { Base } from './_Base';
import { Users } from '..';
class FederationPeersModel extends Base {
constructor() {
super('federation_peers');
}
refreshPeers() {
const collectionObj = this.model.rawCollection();
const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);
const users = Users.find({ federation: { $exists: true } }, { fields: { federation: 1 } }).fetch();
const peers = [...new Set(users.map((u) => u.federation.peer))];
for (const peer of peers) {
findAndModify({ peer }, [], {
$setOnInsert: {
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 });
}
}
}
export const FederationPeers = new FederationPeersModel();

@ -1298,20 +1298,23 @@
"Favorites": "Favorites",
"Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.",
"Features_Enabled": "Features Enabled",
"FEDERATION_Enabled": "Attempt to integrate federation support. Changing this value requires restarting Rocket.Chat.",
"FEDERATION_Enabled_Alert": "Federation Support is a work in progress. Use on a production system is not recommended at this time.",
"Federation_Dashboard": "Federation Dashboard",
"FEDERATION_Discovery_Method": "Discovery Method",
"FEDERATION_Discovery_Method_Description": "You can use the hub or a SRV and a TXT entry on your DNS records.",
"FEDERATION_Domain": "Domain",
"FEDERATION_Domain_Description": "Add the domain that this server should be linked to - for example: @rocket.chat.",
"FEDERATION_Domain_Alert": "Do not change this after enabling the feature, we can't handle domain changes yet.",
"FEDERATION_Public_Key": "Public Key",
"FEDERATION_Public_Key_Description": "This is the key you need to share with your peers.",
"FEDERATION_Enabled": "Attempt to integrate federation support. Changing this value requires restarting Rocket.Chat.",
"FEDERATION_Enabled_Alert": "Federation Support is a work in progress. Use on a production system is not recommended at this time.",
"FEDERATION_Hub_URL": "Hub URL",
"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_Status": "Status",
"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_Status": "Status",
"Field": "Field",
"Field_removed": "Field removed",
"Field_required": "Field required",
@ -2139,6 +2142,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_inactive_peers": "Number of inactive peers",
"Number_of_messages": "Number of messages",
"OAuth Apps": "OAuth Apps",
"OAuth_Application": "OAuth Application",
@ -3202,4 +3209,4 @@
"Your_question": "Your question",
"Your_server_link": "Your server link",
"Your_workspace_is_ready": "Your workspace is ready to use 🎉"
}
}

@ -140,4 +140,5 @@ import './v139';
import './v140';
import './v141';
import './v142';
import './v143';
import './xrun';

@ -0,0 +1,21 @@
import { Migrations } from '../../../app/migrations/server';
import { Users, FederationPeers } from '../../../app/models/server';
Migrations.add({
version: 143,
up() {
const users = Users.find({ federation: { $exists: true } }, { fields: { federation: 1 } }).fetch();
let peers = [...new Set(users.map((u) => u.federation.peer))];
peers = peers.map((peer) => ({
active: false,
peer,
last_seen_at: null,
}));
if (peers.length) {
FederationPeers.model.rawCollection().insertMany(peers);
}
},
});
Loading…
Cancel
Save