Revert federation (#15278)
* Revert "Federation improvements (#15234)" This reverts commitpull/15258/head^2f5b0a610bf. * Revert "[BREAK] Federation refactor with addition of chained events (#15206)" This reverts commit4166e0b459.
parent
d8d3e8a9e6
commit
81b9fbb944
@ -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'); |
||||
}, |
||||
}); |
||||
@ -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'); |
||||
} |
||||
}, |
||||
}); |
||||
@ -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 +0,0 @@ |
||||
import './server'; |
||||
@ -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,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); |
||||
|
||||
@ -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,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', |
||||
}; |
||||
}, |
||||
}); |
||||
@ -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,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', |
||||
}; |
||||
}; |
||||
@ -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(); |
||||
|
||||
@ -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(); |
||||
Loading…
Reference in new issue