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, GuildMetadata, Member, Message, Resource, ServerMetaData, 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( private readonly id: number, private 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); // TODO: Only unverify the personaldb->socket connection on d/c? // Connect/Disconnect this.socketGuild.on('connect', () => { LOG.info(`g#${this.id} connected`); this.emit('connect'); }); this.socketGuild.on('disconnect', () => { LOG.info(`g#${this.id} disconnected`); this.unverify(); 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( personalGuildId: number, socketConfig: SocketConfig, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB ) { let socket = socketio.connect(socketConfig.url, { forceNew: true, ca: socketConfig.cert }); let socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey); let memberId = await socketVerifier.verify(); return new CombinedGuild( personalGuildId, memberId, socket, socketVerifier, messageRAMCache, resourceRAMCache, personalDB ); } private unverify() { for (let pairVerifier of this.pairVerifiers) { pairVerifier.unverify(); } } async ensureRAMMembers() { 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'); } async ensureRAMChannels() { 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'); } // 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); } async requestSetServerName(serverName: string): Promise { await this.socketGuild.requestSetServerName(serverName); } async requestSetServerIcon(serverIcon: Buffer): Promise { await this.socketGuild.requestSetServerIcon(serverIcon); } 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); } }