cordis/client/webapp/guilds-manager.ts
Michael Peters 1592a38c7c added conflict forwarding
also fixed personal connection icon starting as unknown
2021-12-02 19:08:05 -06:00

191 lines
7.1 KiB
TypeScript

// 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';
import { GuildEventNames } from './guild-types';
import { AutoVerifierChangesType } from './auto-verifier';
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, changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void;
'conflict-channels': (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Channel>) => void;
'conflict-members': (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Member>) => void;
'conflict-messages': (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Message>) => void;
'conflict-tokens': (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Token>) => void;
'conflict-resource': (guild: CombinedGuild, changesType: AutoVerifierChangesType, 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<CombinedGuild> {
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 GuildEventNames) {
guild.on(eventName as any, (...args: any) => {
this.emit(eventName as any, guild, ...args);
});
}
return guild;
}
async init(): Promise<void> {
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<CombinedGuild> {
const { name, url, cert, token } = guildConfig;
LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff });
let socket = await new Promise<socketio.Socket>((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<void> {
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);
}
}