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 PersonalDBGuild from './guild-personal-db'; import RAMGuild from './guild-ram'; import SocketGuild from './guild-socket'; import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types'; import MessageRAMCache from "./message-ram-cache"; import PersonalDB from "./personal-db"; import ResourceRAMCache from "./resource-ram-cache"; import SocketVerifier from './socket-verifier'; import { Connectable, AsyncGuaranteedFetchable, Conflictable, AsyncRequestable } from './guild-types'; import PairVerifierFetchable from './fetchable-pair-verifier'; import EnsuredFetchable from './fetchable-ensured'; import { EventEmitter } from 'tsee'; export default class CombinedGuild extends EventEmitter implements AsyncGuaranteedFetchable, AsyncRequestable { private readonly ramGuild: RAMGuild; private readonly personalDBGuild: PersonalDBGuild; private readonly socketGuild: SocketGuild; private readonly pairVerifiers: PairVerifierFetchable[]; private readonly fetchable: AsyncGuaranteedFetchable; constructor( public readonly id: number, public readonly memberId: string, socket: socketio.Socket, socketVerifier: SocketVerifier, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB ) { super(); this.ramGuild = new RAMGuild(messageRAMCache, resourceRAMCache, this.id); this.personalDBGuild = new PersonalDBGuild(personalDB, this.id, this.memberId); this.socketGuild = new SocketGuild(socket, socketVerifier); // Connect/Disconnect this.socketGuild.on('connect', () => { LOG.info(`g#${this.id} connected`); this.emit('connect'); }); this.socketGuild.on('disconnect', async () => { LOG.info(`g#${this.id} disconnected`); this.unverify(); await personalDB.clearAllMembersStatus(this.id); this.emit('disconnect'); }); // Metadata this.socketGuild.on('update-metadata', (guildMeta: GuildMetadata) => { LOG.info(`g#${this.id} updated metadata: ${guildMeta}`); this.emit('update-metadata', guildMeta); }); // Members this.socketGuild.on('new-members', async (members: Member[]) => { for (let member of members) { LOG.info(`g#${this.id} ${member}`); } this.ramGuild.handleMembersAdded(members); await this.personalDBGuild.handleMembersAdded(members); this.emit('new-members', members); }); this.socketGuild.on('update-members', async (members: Member[]) => { for (let member of members) { LOG.info(`g#${this.id} updated ${member}`); } this.ramGuild.handleMembersChanged(members); await this.personalDBGuild.handleMembersChanged(members); this.emit('update-members', members); }); // Channels this.socketGuild.on('new-channels', async (channels: Channel[]) => { for (let channel of channels) { LOG.info(`g#${this.id} ${channel}`); } this.ramGuild.handleChannelsAdded(channels); await this.personalDBGuild.handleChannelsAdded(channels); this.emit('new-channels', channels); }); this.socketGuild.on('update-channels', async (channels: Channel[]) => { for (let channel of channels) { LOG.info(`g#${this.id} updated ${channel}`); } this.ramGuild.handleChannelsChanged(channels); await this.personalDBGuild.handleChannelsChanged(channels); this.emit('update-channels', channels); }); // Messages this.socketGuild.on('new-messages', async (messages: Message[]) => { for (let message of messages) { LOG.info(`g#${this.id} ${message}`); } this.ramGuild.handleMessagesAdded(messages); await this.personalDBGuild.handleMessagesAdded(messages); this.emit('new-messages', messages); }); this.socketGuild.on('update-messages', async (messages: Message[]) => { for (let message of messages) { LOG.info(`g#${this.id} updated ${message}`); } this.ramGuild.handleMessagesChanged(messages); await this.personalDBGuild.handleMessagesChanged(messages); this.emit('update-messages', messages); }); let personalDBSocketPairVerifier = new PairVerifierFetchable(this.personalDBGuild, this.socketGuild); let ramPersonalDBSocketPairVerifier = new PairVerifierFetchable(this.ramGuild, personalDBSocketPairVerifier); // Forward the conflict events from the last verifier in the chain ramPersonalDBSocketPairVerifier.on('conflict-metadata', (oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => { LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta }); this.emit('conflict-metadata', oldGuildMeta, newGuildMeta); }); ramPersonalDBSocketPairVerifier.on('conflict-members', (changes: Changes) => { LOG.info(`g#${this.id} members conflict`, { changes }); this.emit('conflict-members', changes); }); ramPersonalDBSocketPairVerifier.on('conflict-channels', (changes: Changes) => { LOG.info(`g#${this.id} channels conflict`, { changes }); this.emit('conflict-channels', changes); }); ramPersonalDBSocketPairVerifier.on('conflict-messages', (changes: Changes) => { LOG.info(`g#${this.id} messages conflict`, { changes }); this.emit('conflict-messages', changes); }); ramPersonalDBSocketPairVerifier.on('conflict-tokens', (changes: Changes) => { LOG.info(`g#${this.id} tokens conflict`, { changes }); this.emit('conflict-tokens', changes); }); ramPersonalDBSocketPairVerifier.on('conflict-resource', (oldResource: Resource, newResource: Resource) => { LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource }); this.emit('conflict-resource', oldResource, newResource); }); this.pairVerifiers = [ personalDBSocketPairVerifier, ramPersonalDBSocketPairVerifier ]; this.fetchable = new EnsuredFetchable(ramPersonalDBSocketPairVerifier); } static async create( guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB ) { let socket = socketio.connect(socketConfig.url, { forceNew: true, ca: socketConfig.cert }); return new Promise(async (resolve, reject) => { const connectFail = (err: any) => { socket.off('connect', connectSucceed); reject(new Error('unable to connect: ' + err.message)); } const connectSucceed = async () => { socket.off('connect_error', connectFail); let socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey); let memberId = await socketVerifier.verify(); if (guildMetadata.memberId && memberId !== guildMetadata.memberId) { reject(new Error('Verified member differs from original member')); } resolve(new CombinedGuild( guildMetadata.id, memberId, socket, socketVerifier, messageRAMCache, resourceRAMCache, personalDB )); } socket.once('connect', connectSucceed); socket.once('connect_error', connectFail); }); } public isSocketVerified(): boolean { return this.socketGuild.verifier.isVerified; } private unverify(): void { for (let pairVerifier of this.pairVerifiers) { pairVerifier.unverify(); } } public disconnect(): void { this.socketGuild.disconnect(); } private async ensureRAMMembers(): Promise { if (this.ramGuild.getMembers().size === 0) { await this.fetchMembers(); } if (this.ramGuild.getMembers().size === 0) throw new Error('RAM Members was not updated through fetchMembers'); } private async ensureRAMChannels(): Promise { if (this.ramGuild.getChannels().size === 0) { await this.fetchChannels(); } if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels'); } async grabRAMMembersMap(): Promise> { await this.ensureRAMMembers(); return this.ramGuild.getMembers(); } async grabRAMChannelsMap(): Promise> { await this.ensureRAMChannels(); return this.ramGuild.getChannels(); } async fetchConnectionInfo(): Promise { let connection: ConnectionInfo = { id: null, avatarResourceId: null, displayName: 'Connecting...', status: '', privileges: [], roleName: null, roleColor: null, rolePriority: null }; if (this.socketGuild.verifier.isVerified) { let members = await this.grabRAMMembersMap(); let member = members.get(this.memberId); if (member) { connection.id = member.id; connection.avatarResourceId = member.avatarResourceId; connection.displayName = member.displayName; connection.status = member.status; connection.roleName = member.roleName; connection.roleColor = member.roleColor; connection.rolePriority = member.rolePriority; connection.privileges = member.privileges; } else { LOG.warn('unable to find self in members'); } } else { let members = await this.personalDBGuild.fetchMembers(); if (members) { let member = members.find(m => m.id === this.memberId); if (member) { connection.id = member.id; connection.avatarResourceId = member.avatarResourceId; connection.displayName = member.displayName; connection.status = 'connecting'; connection.privileges = []; } else { LOG.warn('unable to find self in cached members'); } } } return connection; } // Fetched through the triple-cache system (RAM -> Disk -> Server) async fetchMetadata(): Promise { return await this.fetchable.fetchMetadata(); } async fetchMembers(): Promise { return await this.fetchable.fetchMembers(); } async fetchChannels(): Promise { return await this.fetchable.fetchChannels(); } async fetchMessagesRecent(channelId: string, number: number): Promise { return await this.fetchable.fetchMessagesRecent(channelId, number); } async fetchMessagesBefore(channelId: string, messageId: string, number: number): Promise { return await this.fetchable.fetchMessagesBefore(channelId, messageId, number); } async fetchMessagesAfter(channelId: string, messageId: string, number: number): Promise { return await this.fetchable.fetchMessagesAfter(channelId, messageId, number); } async fetchResource(resourceId: string): Promise { return await this.fetchable.fetchResource(resourceId); } async fetchTokens(): Promise { return await this.fetchable.fetchTokens(); } // Simply forwarded to the socket guild async requestSendMessage(channelId: string, text: string): Promise { await this.socketGuild.requestSendMessage(channelId, text); } async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise { await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName); } async requestSetStatus(status: string): Promise { await this.socketGuild.requestSetStatus(status); } async requestSetDisplayName(displayName: string): Promise { await this.socketGuild.requestSetDisplayName(displayName); } async requestSetAvatar(avatar: Buffer): Promise { await this.socketGuild.requestSetAvatar(avatar); } // TODO: Rename Server -> Guild async requestSetGuildName(guildName: string): Promise { await this.socketGuild.requestSetGuildName(guildName); } async requestSetGuildIcon(guildIcon: Buffer): Promise { await this.socketGuild.requestSetGuildIcon(guildIcon); } async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise { await this.socketGuild.requestDoUpdateChannel(channelId, name, flavorText); } async requestDoCreateChannel(name: string, flavorText: string | null): Promise { await this.socketGuild.requestDoCreateChannel(name, flavorText); } async requestDoRevokeToken(token: string): Promise { await this.socketGuild.requestDoRevokeToken(token); } }