// Main interface with the servers import * as electronRemote from '@electron/remote'; const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); import * as socketio from 'socket.io-client'; import * as crypto from 'crypto'; import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types'; import { IAddGuildData } from './elements/overlay-add-guild'; import { EventEmitter } from 'tsee'; import CombinedGuild from './guild-combined'; import PersonalDB from './personal-db'; import MessageRAMCache from './message-ram-cache'; import ResourceRAMCache from './resource-ram-cache'; export default class GuildsManager extends EventEmitter<{ 'connect': (guild: CombinedGuild) => void; 'disconnect': (guild: CombinedGuild) => void; 'verified': (guild: CombinedGuild) => void; 'update-metadata': (guild: CombinedGuild, guildMeta: GuildMetadata) => void; 'new-channels': (guild: CombinedGuild, channels: Channel[]) => void; 'update-channels': (guild: CombinedGuild, updatedChannels: Channel[]) => void; 'remove-channels': (guild: CombinedGuild, removedChannels: Channel[]) => void; 'new-members': (guild: CombinedGuild, members: Member[]) => void; 'update-members': (guild: CombinedGuild, updatedMembers: Member[]) => void; 'remove-members': (guild: CombinedGuild, removedMembers: Member[]) => void; 'new-messages': (guild: CombinedGuild, messages: Message[]) => void; 'update-messages': (guild: CombinedGuild, updatedMessages: Message[]) => void; 'remove-messages': (guild: CombinedGuild, removedMessages: Message[]) => void; 'conflict-metadata': (guild: CombinedGuild, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void; 'conflict-channels': (guild: CombinedGuild, changes: Changes) => void; 'conflict-members': (guild: CombinedGuild, changes: Changes) => void; 'conflict-messages': (guild: CombinedGuild, changes: Changes) => void; 'conflict-tokens': (guild: CombinedGuild, changes: Changes) => void; 'conflict-resource': (guild: CombinedGuild, oldResource: Resource, newResource: Resource) => void; }> { public guilds: CombinedGuild[] = []; constructor( private messageRAMCache: MessageRAMCache, private resourceRAMCache: ResourceRAMCache, private personalDB: PersonalDB ) { super(); } async _connectFromConfig(guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig): Promise { LOG.debug(`connecting to g#${guildMetadata.id} at ${socketConfig.url}`); let guild = await CombinedGuild.create( guildMetadata, socketConfig, this.messageRAMCache, this.resourceRAMCache, this.personalDB ); await this.personalDB.clearAllMembersStatus(guild.id); this.guilds.push(guild); // Forward guild events through this event emitter for (let eventName of guild.eventNames()) { guild.on(eventName as any, (...args: any) => { this.emit(eventName as any, guild, ...args); }); } return guild; } async init(): Promise { this.guilds = []; // TODO: connect concurrently for (let guildMeta of await this.personalDB.fetchGuilds()) { for (let guildSocket of await this.personalDB.fetchGuildSockets(guildMeta.id)) { await this._connectFromConfig(guildMeta, guildSocket); } } if (this.guilds.length === 0) { LOG.warn('no guilds found in client-side db'); } } static _socketEmitTimeout(socket: socketio.Socket, ms: number, name: string, ...args: any[]) { // see also client-controller.js let socketArgs = args.slice(0, args.length - 1); let respond = args[args.length - 1]; let cutoff = false; let timeout = setTimeout(() => { cutoff = true; respond('emit timeout'); }, ms); socket.emit(name, ...socketArgs, (...respondArgs: any[]) => { if (cutoff) { return; } clearTimeout(timeout); respond(...respondArgs); }); } async addNewGuild(guildConfig: IAddGuildData, displayName: string, avatarBuff: Buffer): Promise { const { name, url, cert, token } = guildConfig; LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff }); let socket = await new Promise((resolve, reject) => { let socket = socketio.connect(url, { forceNew: true, ca: cert, // verifies the server's identity reconnection: false, // a bit stricter for registration }); socket.on('connect_error', (e) => { LOG.error('connect error', e); reject(new Error('unable to connect to server')); }); socket.on('connect', () => { resolve(socket); }); }); try { // Create a new Public/Private key pair to identify ourselves with this guild let { publicKey, privateKey } = await new Promise((resolve, reject) => { crypto.generateKeyPair('rsa', { modulusLength: 4096 }, (err, publicKey, privateKey) => { if (err) { reject(err); } else { resolve({ publicKey, privateKey }); } }); }); return await new Promise((resolve, reject) => { let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' }); GuildsManager._socketEmitTimeout(socket, 5000, 'register-with-token', token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr: string, dataMember: any, dataMetadata: any) => { if (errStr) { reject(new Error(errStr)); } else { try { const member = Member.fromDBData(dataMember); const meta = GuildMetadata.fromDBData(dataMetadata); let guildMeta: GuildMetadataWithIds | null = null; let socketConfig: SocketConfig | null = null; await this.personalDB.queueTransaction(async () => { let guildId = await this.personalDB.addGuild(meta.name, meta.iconResourceId, member.id); let guildSocketId = await this.personalDB.addGuildSocket(guildId, url, cert, publicKey, privateKey); guildMeta = await this.personalDB.fetchGuild(guildId); socketConfig = await this.personalDB.fetchGuildSocket(guildId, guildSocketId); }); if (!guildMeta || !socketConfig) { throw new Error('unable to properly add guild'); } let server = await this._connectFromConfig(guildMeta, socketConfig); resolve(server); } catch (e) { reject(e); } } } ); }); } finally { socket.disconnect(); } } async removeGuild(guild: CombinedGuild): Promise { await this.personalDB.queueTransaction(async () => { await this.personalDB.removeGuildSockets(guild.id); await this.personalDB.removeGuild(guild.id); }); this.guilds = this.guilds.filter(g => g.id != guild.id); } }