358 lines
14 KiB
TypeScript
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);
|
|
}
|
|
}
|