1592a38c7c
also fixed personal connection icon starting as unknown
191 lines
7.1 KiB
TypeScript
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);
|
|
}
|
|
}
|