cordis/client/webapp/guild-combined.ts

358 lines
14 KiB
TypeScript

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';
import { AutoVerifierChangesType } from './auto-verifier';
import { IDQuery, PartialMessageListQuery } from './auto-verifier-with-args';
export default class CombinedGuild extends EventEmitter<Connectable & Conflictable> implements AsyncGuaranteedFetchable, AsyncRequestable {
private readonly ramGuild: RAMGuild;
private readonly personalDBGuild: PersonalDBGuild;
private readonly socketGuild: SocketGuild;
private readonly fetchable: AsyncGuaranteedFetchable;
private readonly mainPairVerifier: PairVerifierFetchable;
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', async (guildMeta: GuildMetadata) => {
LOG.info(`g#${this.id} updated metadata: ${guildMeta}`);
this.ramGuild.handleMetadataChanged(guildMeta);
await this.personalDBGuild.handleMetadataChanged(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);
let members = await this.grabRAMMembersMap();
let channels = await this.grabRAMChannelsMap();
for (let message of messages) message.fill(members, channels);
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);
let members = await this.grabRAMMembersMap();
let channels = await this.grabRAMChannelsMap();
for (let message of messages) message.fill(members, channels);
this.emit('update-messages', messages);
});
let diskSocket = new PairVerifierFetchable(this.personalDBGuild, this.socketGuild);
let ramDiskSocket = new PairVerifierFetchable(this.ramGuild, diskSocket);
// Forward the conflict events from the last verifier in the chain
ramDiskSocket.on('conflict-metadata', (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => {
LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta });
this.emit('conflict-metadata', changesType, oldGuildMeta, newGuildMeta);
});
ramDiskSocket.on('conflict-members', (changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
LOG.info(`g#${this.id} members conflict`);
this.emit('conflict-members', changesType, changes);
});
ramDiskSocket.on('conflict-channels', (changesType: AutoVerifierChangesType, changes: Changes<Channel>) => {
LOG.info(`g#${this.id} channels conflict`, { changes });
this.emit('conflict-channels', changesType, changes);
});
ramDiskSocket.on('conflict-messages', async (changesType: AutoVerifierChangesType, query: PartialMessageListQuery, changes: Changes<Message>) => {
let members = await this.grabRAMMembersMap();
let channels = await this.grabRAMChannelsMap();
for (let message of changes.added) message.fill(members, channels);
for (let dataPoint of changes.updated) dataPoint.newDataPoint.fill(members, channels);
for (let message of changes.deleted) message.fill(members, channels);
this.emit('conflict-messages', changesType, query, changes);
});
ramDiskSocket.on('conflict-tokens', (changesType: AutoVerifierChangesType, changes: Changes<Token>) => {
LOG.info(`g#${this.id} tokens conflict`, { changes });
this.emit('conflict-tokens', changesType, changes);
});
ramDiskSocket.on('conflict-resource', (changesType: AutoVerifierChangesType, query: IDQuery, oldResource: Resource, newResource: Resource) => {
LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource });
this.emit('conflict-resource', changesType, query, oldResource, newResource);
});
this.fetchable = new EnsuredFetchable(ramDiskSocket);
this.mainPairVerifier = ramDiskSocket;
}
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<CombinedGuild>(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 {
this.mainPairVerifier.unverify();
}
public disconnect(): void {
this.socketGuild.disconnect();
}
private async ensureRAMMembers(): Promise<void> {
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<void> {
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<Map<string, Member>> {
await this.ensureRAMMembers();
return this.ramGuild.getMembers();
}
async grabRAMChannelsMap(): Promise<Map<string, Channel>> {
await this.ensureRAMChannels();
return this.ramGuild.getChannels();
}
async fetchConnectionInfo(): Promise<ConnectionInfo> {
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(`g#${this.id} unable to find self in members (of ${members.size}, [${Array.from(members.keys()).join(',')}], ${member}, ${this.memberId})`);
}
} 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<GuildMetadata> {
return await this.fetchable.fetchMetadata();
}
async fetchMembers(): Promise<Member[]> {
return await this.fetchable.fetchMembers();
}
async fetchChannels(): Promise<Channel[]> {
return await this.fetchable.fetchChannels();
}
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[]> {
let members = await this.grabRAMMembersMap();
let channels = await this.grabRAMChannelsMap();
let messages = await this.fetchable.fetchMessagesRecent(channelId, number);
for (let message of messages) {
message.fill(members, channels);
}
return messages;
}
async fetchMessagesBefore(channelId: string, messageId: string, number: number): Promise<Message[]> {
let members = await this.grabRAMMembersMap();
let channels = await this.grabRAMChannelsMap();
let messages = await this.fetchable.fetchMessagesBefore(channelId, messageId, number);
for (let message of messages) {
message.fill(members, channels);
}
return messages;
}
async fetchMessagesAfter(channelId: string, messageId: string, number: number): Promise<Message[]> {
let members = await this.grabRAMMembersMap();
let channels = await this.grabRAMChannelsMap();
let messages = await this.fetchable.fetchMessagesAfter(channelId, messageId, number);
for (let message of messages) {
message.fill(members, channels);
}
return messages;
}
async fetchResource(resourceId: string): Promise<Resource> {
return await this.fetchable.fetchResource(resourceId);
}
async fetchTokens(): Promise<Token[]> {
return await this.fetchable.fetchTokens();
}
// Simply forwarded to the socket guild
async requestSendMessage(channelId: string, text: string): Promise<void> {
await this.socketGuild.requestSendMessage(channelId, text);
}
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName);
}
async requestSetStatus(status: string): Promise<void> {
await this.socketGuild.requestSetStatus(status);
}
async requestSetDisplayName(displayName: string): Promise<void> {
await this.socketGuild.requestSetDisplayName(displayName);
}
async requestSetAvatar(avatar: Buffer): Promise<void> {
await this.socketGuild.requestSetAvatar(avatar);
}
// TODO: Rename Server -> Guild
async requestSetGuildName(guildName: string): Promise<void> {
await this.socketGuild.requestSetGuildName(guildName);
}
async requestSetGuildIcon(guildIcon: Buffer): Promise<void> {
await this.socketGuild.requestSetGuildIcon(guildIcon);
}
async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void> {
await this.socketGuild.requestDoUpdateChannel(channelId, name, flavorText);
}
async requestDoCreateChannel(name: string, flavorText: string | null): Promise<void> {
await this.socketGuild.requestDoCreateChannel(name, flavorText);
}
async requestDoRevokeToken(token: string): Promise<void> {
await this.socketGuild.requestDoRevokeToken(token);
}
}