open-collaboration-server
Version:
Open Collaboration Server implementation, part of the Open Collaboration Tools project
226 lines • 9.5 kB
JavaScript
// ******************************************************************************
// Copyright 2024 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.
// ******************************************************************************
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { inject, injectable } from 'inversify';
import { CredentialsManager } from './credentials-manager.js';
import { MessageRelay } from './message-relay.js';
import { Room, isUser } from './types.js';
import { Messages, BroadcastMessage, NotificationMessage, RequestMessage, ResponseMessage, isObject, Emitter } from 'open-collaboration-protocol';
import { Logger } from './utils/logging.js';
export function isRoomClaim(obj) {
return isObject(obj) && typeof obj.room === 'string' && isUser(obj.user);
}
let RoomManager = class RoomManager {
rooms = new Map();
peers = new Map();
pollResults = new Map();
messageRelay;
credentials;
logger;
async closeRoom(id) {
const room = this.getRoomById(id);
if (room) {
const broadcastMessage = BroadcastMessage.create(Messages.Room.Closed, '');
this.messageRelay.sendBroadcast(room.host, broadcastMessage);
for (const peer of room.peers) {
this.peers.delete(peer.id);
peer.dispose();
}
this.rooms.delete(id);
this.logger.info(`Deleted room '${room.id}'`);
}
}
async prepareRoom(user) {
const id = this.credentials.secureId();
const claim = {
room: id,
user: {
id: user.id,
name: user.name,
email: user.email
},
host: true,
roomClock: 0
};
this.logger.info(`Prepared room [id: '${claim.room}'] for user [provider: '${user.authProvider || '<none>'}' | id: '${user.id}' | name: '${user.name}' | email: '${user.email || '<none>'}']`);
const jwt = await this.credentials.generateJwt(claim);
return {
id,
jwt
};
}
async join(peer, roomId) {
let room;
if (peer.host) {
room = new Room(roomId, peer, []);
this.rooms.set(room.id, room);
this.peers.set(peer.id, room);
peer.onDispose(() => {
this.closeRoom(room.id);
});
this.logger.info(`Host [id: '${peer.id}' | client: '${peer.client}' | userId: '${peer.user.id}' | name: '${peer.user.name}' | email: '${peer.user.email || '<none>'}'] created room [id: '${room.id}']`);
}
else {
room = this.rooms.get(roomId);
if (!room) {
throw this.logger.createErrorAndLog(`Could not find room to join from id: ${roomId}`);
}
const broadcastMessage = BroadcastMessage.create(Messages.Room.Joined, '', [peer.toProtocol()]);
this.peers.set(peer.id, room);
room.guests.push(peer);
this.logger.info(`Peer [id: '${peer.id}' | client: '${peer.client}' | userId: '${peer.user.id}' | name: '${peer.user.name}' | email: '${peer.user.email || '<none>'}'] joined room [id: '${room.id}']`);
try {
this.messageRelay.sendBroadcast(peer, broadcastMessage);
}
catch (err) {
this.logger.error('Failed to send join broadcast', err);
}
peer.onDispose(() => {
this.leaveRoom(peer);
});
}
// Send the identity info to the user (i.e. what the user needs to know about itself)
const infoNotification = NotificationMessage.create(Messages.Peer.Info, '', peer.id, [peer.toProtocol()]);
this.messageRelay.sendNotification(peer, infoNotification);
return room;
}
async leaveRoom(peer) {
const room = this.getRoomByPeerId(peer.id);
if (!room) {
return;
}
if (peer.host) {
this.closeRoom(room.id);
}
else {
try {
const broadcastMessage = BroadcastMessage.create(Messages.Room.Left, '', [peer.toProtocol()]);
this.messageRelay.sendBroadcast(peer, broadcastMessage);
}
catch (err) {
this.logger.error('Failed to send leave broadcast', err);
}
// Remove the peer from the room as the last step
this.peers.delete(peer.id);
room.removeGuest(peer.id);
}
}
getRoomById(id) {
return this.rooms.get(id);
}
getRoomByPeerId(id) {
return this.peers.get(id);
}
async requestJoin(room, user) {
this.logger.info(`Request to join room [id: '${room.id}'] by user [id: '${user.id}' | name: '${user.name}' | email: '${user.email ?? '<none>'}']`);
const responseId = this.credentials.secureId();
const timeout = setTimeout(() => {
pollResult.update({
code: 'JoinTimeout',
message: 'Join request has timed out',
params: [],
failure: true
});
pollResult.dispose();
}, 300_000); // 5 minutes of timeout
const updateEmitter = new Emitter();
const pollResult = {
update: result => {
pollResult.result = result;
updateEmitter.fire(result);
},
onUpdate: updateEmitter.event,
dispose: () => {
updateEmitter.dispose();
this.pollResults.delete(responseId);
clearTimeout(timeout);
}
};
this.pollResults.set(responseId, pollResult);
try {
const requestMessage = RequestMessage.create(Messages.Peer.Join, this.credentials.secureId(), '', room.host.id, [user]);
const responsePromise = this.messageRelay.sendRequest(room.host, requestMessage, 300_000);
responsePromise.then(async (response) => {
if (ResponseMessage.is(response)) {
const joinResponse = response.content.response;
if (!joinResponse) {
pollResult.update({
failure: true,
code: 'JoinRejected',
params: [],
message: 'Join request has been rejected'
});
return;
}
const claim = {
room: room.id,
user: { ...user },
// Increment the clock to identify unique sessions
// If a user joins a room multiple times, the clock will be incremented
// This way, they get a new JWT for each session
// If a user reconnects using an old JWT, we can identify a reconnect attempt
roomClock: ++room.clock
};
const jwt = await this.credentials.generateJwt(claim);
const joinRoomResponse = {
roomId: room.id,
roomToken: jwt,
workspace: joinResponse.workspace,
host: room.host.toProtocol()
};
pollResult.update(joinRoomResponse);
}
else {
pollResult.update({
failure: true,
code: 'JoinRejected',
params: [],
message: 'Join request has been rejected'
});
}
}).catch(() => {
pollResult.update({
code: 'JoinTimeout',
message: 'Join request has timed out',
params: [],
failure: true
});
});
pollResult.update({
failure: false,
code: 'WaitingForHost',
params: [],
message: 'Waiting for host to accept join request',
});
return responseId;
}
catch {
throw this.logger.createErrorAndLog('Failed to request join');
}
}
pollJoin(responseId) {
return this.pollResults.get(responseId);
}
};
__decorate([
inject(MessageRelay)
], RoomManager.prototype, "messageRelay", void 0);
__decorate([
inject(CredentialsManager)
], RoomManager.prototype, "credentials", void 0);
__decorate([
inject(Logger)
], RoomManager.prototype, "logger", void 0);
RoomManager = __decorate([
injectable()
], RoomManager);
export { RoomManager };
//# sourceMappingURL=room-manager.js.map