cordis/server/server-controller.ts

854 lines
35 KiB
TypeScript
Raw Normal View History

2021-10-30 17:26:41 +00:00
import * as crypto from 'crypto';
import * as moment from 'moment';
import sizeOf from 'image-size';
import * as FileType from 'file-type';
import * as sharp from 'sharp';
import * as socketio from 'socket.io';
import Logger from '../logger/logger';
import DB from './db';
2021-11-02 04:29:24 +00:00
const LOG = Logger.create('db');
2021-10-30 17:26:41 +00:00
const MAX_TEXT_MESSAGE_LENGTH = 1024 * 2; // 2 KB character message max
const MAX_RESOURCE_SIZE = 1024 * 1024 * 50; // 50 MB max resource size
const MAX_AVATAR_SIZE = 1024 * 128; // 128 KB max avatar size
const MAX_DISPLAY_NAME_LENGTH = 32; // 32 char max display name length
const MAX_CHANNEL_NAME_LENGTH = 32; // 32 char max channel name length
const MAX_CHANNEL_FLAVOR_TEXT_LENGTH = 256; // 256 char max channel flavor text length
const MAX_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size
const MAX_GUILD_NAME_LENGTH = 64; // 64 char max guild name length
2021-10-30 17:26:41 +00:00
interface IIdentity {
guildId: string | null;
2021-10-30 17:26:41 +00:00
memberId: string | null;
verified: boolean;
publicKey: crypto.KeyObject | null;
challenge: Buffer | null
}
interface IChecks {
verified?: boolean;
privileges?: string[];
}
function formatDate(date: Date): string {
return moment(date).format('YYYY-MM-DD HH:mm:ss');
}
let connected: IIdentity[] = [];
class EventError extends Error {
public cause: Error | undefined;
public extended_message: string | undefined;
constructor(message: string, cause?: Error, extended_message?: string) {
super(message);
this.name = 'EventError';
this.cause = cause;
this.extended_message = extended_message;
}
}
class SignatureError extends Error {
constructor(message: string) {
super(message);
this.name = 'SignatureError';
}
}
function bindEvent(
client: socketio.Socket,
identity: IIdentity | null,
checks: IChecks | null,
name: string,
signature: string[],
handler: ((...args: any[]) => Promise<void>)
) {
client.on(name, async (...args) => {
let respond = (_result: string) => {};
try {
if (signature.length != args.length) {
throw new SignatureError('invalid length');
}
for (let i = 0; i < signature.length; ++i) {
if (signature[i] == 'buffer') {
if (!Buffer.isBuffer(args[i])) {
throw new SignatureError('invalid types (buffer expected)');
}
} else if (signature[i].endsWith('?')) {
if (args[i] !== null && typeof args[i] !== signature[i].slice(0, -1)) {
throw new SignatureError('invalid types (nullable)');
2021-10-30 17:26:41 +00:00
}
} else {
if (typeof args[i] !== signature[i]) {
throw new SignatureError('invalid types (not nullable)');
2021-10-30 17:26:41 +00:00
}
}
}
if (signature[signature.length - 1] === 'function') {
respond = args[args.length - 1];
}
if (checks && checks.verified && (!identity || !identity.verified)) {
throw new EventError('not verified');
}
if (checks && checks.privileges) {
for (let privilege of checks.privileges) {
if (!identity || !identity.guildId || !identity.memberId) {
2021-10-30 17:26:41 +00:00
throw new EventError('not authorized');
}
let hasPrivilege: boolean;
try {
hasPrivilege = await DB.hasPrivilege(identity.guildId, identity.memberId, privilege);
2021-10-30 17:26:41 +00:00
} catch (e) {
throw new EventError('not authorized', e, 'unable to check privilege');
}
if (!hasPrivilege) {
throw new EventError('not authorized');
}
}
}
//LOG.debug(`c#${client.id}: ${name}`);
try {
await handler(...args);
} catch (e) {
if (e instanceof EventError) {
throw e;
} else {
throw new EventError(`unable to handle ${name} request`, e);
}
}
} catch (e) {
if (e instanceof SignatureError) {
LOG.warn(`c#${client.id}: ${name} request does not match expected signature`, { signature, args, msg: e.message });
2021-10-30 17:26:41 +00:00
// do not respond to requests with invalid signatures
} else if (e instanceof EventError) {
LOG.warn(`c#${client.id}: ${e.message}${e.extended_message ? ' / ' + e.extended_message : ''}`, e.cause);
respond(e.message);
} else {
LOG.error('caught unhandled error', e);
}
}
})
}
function bindRegistrationEvents(io: socketio.Server, client: socketio.Socket): void { // non-identity events
/*
* Register for a guild with a token
2021-10-30 17:26:41 +00:00
* @param token The token sent in the .cordis file
* @param publicKeyBuff The client's public key
* @param displayName The client's display name
* @param avatarBuff The client's avatar image
* @param respond function(errStr, member) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.member The member created by the registration
*/
bindEvent(
client, null, null,
'register-with-token', [ 'string', 'buffer', 'string', 'buffer', 'function' ],
async (token, publicKeyBuff, displayName, avatarBuff, respond) => {
if (displayName.length > MAX_DISPLAY_NAME_LENGTH || displayName.length == 0) {
throw new EventError('invalid display name');
}
if (avatarBuff.length > MAX_AVATAR_SIZE) {
throw new EventError('invalid avatar');
}
let typeResult = await FileType.fromBuffer(avatarBuff);
if (!typeResult || ['image/png', 'image/jpeg', 'image/jpg'].indexOf(typeResult.mime) == -1) {
throw new EventError('invalid avatar mime type');
}
if (!(await DB.isTokenReal(token))) {
throw new EventError('not a real token');
}
if (await DB.isTokenTaken(token)) {
throw new EventError('token already used');
}
if (!(await DB.isTokenActive(token))) {
throw new EventError('token expired');
}
const { guildId, memberId } = await DB.registerWithToken(token, publicKeyBuff, displayName, avatarBuff);
2021-10-30 17:26:41 +00:00
const member = await DB.getMember(guildId, memberId);
2021-11-21 18:29:42 +00:00
const meta = await DB.getGuild(guildId);
2021-10-30 17:26:41 +00:00
LOG.info(`c#${client.id}: registered with t#${token} as u#${member.id} / ${member.display_name}`);
2021-11-21 18:29:42 +00:00
respond(null, member, meta);
2021-10-30 17:26:41 +00:00
io.to(guildId).emit('new-member', member);
2021-10-30 17:26:41 +00:00
}
);
}
function bindChallengeVerificationEvents(io: socketio.Server, client: socketio.Socket, identity: IIdentity): void {
identity.memberId = null;
identity.guildId = null;
2021-10-30 17:26:41 +00:00
identity.publicKey = null;
identity.challenge = null;
identity.verified = false;
/*
* Request a challenge for login
* @param publicKeyBuff The public key encoded with der/spki
* @param respond function(err, challenge) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.algo Signature algorithm to use
* @param respond.type Signature response type to use
* @param respond.challenge Challenge buffer to sign generated by crypto.randomBytes
*/
bindEvent(
client, null, null,
'challenge', [ 'buffer', 'function' ],
async (publicKeyBuff, respond) => {
identity.verified = false;
identity.memberId = null;
identity.guildId = null;
2021-10-30 17:26:41 +00:00
identity.publicKey = crypto.createPublicKey({ key: publicKeyBuff, format: 'der', type: 'spki' });
try {
let memberInfo = await DB.getMemberInfo(identity.publicKey);
identity.memberId = memberInfo.member_id;
identity.guildId = memberInfo.guild_id;
2021-10-30 17:26:41 +00:00
} catch (e) {
// unable to find a member with the specified public key
throw new EventError('unauthorized public key', e);
}
LOG.debug(`c#${client.id}: challenging for u#${identity.memberId}`);
if (connected.find(i => i.memberId == identity.memberId && i.verified)) {
throw new EventError('member already connected');
}
identity.challenge = crypto.randomBytes(64);
respond(null, 'sha512', 'hex', identity.challenge);
}
);
/*
* Verify a signature for login
* @param signature The signed challenge using the client's private key
* @param respond function(errStr, memberId) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.memberId The signed in member id
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, null, null,
'verify', [ 'string', 'function' ],
async (signature, respond) => {
if (connected.find(i => i.memberId == identity.memberId && i.verified)) {
throw new EventError('member already connected');
}
if (!identity.challenge) {
throw new EventError('challenge not requested');
}
if (!identity.publicKey) {
throw new EventError('publicKey not provided')
}
if (!identity.guildId) {
throw new EventError('guildId not targeted')
2021-10-30 17:26:41 +00:00
}
if (!identity.memberId) {
throw new EventError('memberId not targeted')
}
let verify = crypto.createVerify('sha512');
verify.write(identity.challenge);
verify.end();
let verified: boolean;
try {
verified = verify.verify(identity.publicKey, signature, 'hex');
} catch (e) {
throw new EventError('unable to verify signature', e);
}
if (!verified) {
throw new EventError('invalid signature');
}
try {
await DB.setMemberStatus(identity.guildId, identity.memberId, 'online');
let member = await DB.getMember(identity.guildId, identity.memberId);
io.to(identity.guildId).emit('update-member', member);
2021-10-30 17:26:41 +00:00
} catch (e) {
LOG.warn('unable to set status for m#' + identity.memberId, e);
// not killing here since this should not be a game ender. Most likely, the SQL server is bad though
}
identity.verified = true;
client.join(identity.guildId); // join the socket.io guild room
LOG.info(`c#${client.id}: verified as g#${identity.guildId} u#${identity.memberId}`);
2021-10-30 17:26:41 +00:00
respond(null, identity.memberId);
}
);
}
function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity: IIdentity): void {
/*
* Sets the name of the guild
* @param name The new name of the guild
* @param respond function(errStr, guildMeta) as a socket.io response function
2021-10-30 17:26:41 +00:00
* @param respond.errStr Error string if an error, else null
* @param respond.guildMeta The update guild metadata information
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, identity,
{ verified: true, privileges: [ 'modify_profile' ] },
'set-name', [ 'string', 'function' ],
async (name, respond) => {
if (name.length == 0 || name.length > MAX_GUILD_NAME_LENGTH) throw new EventError('invalid guild name');
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
let newMeta = await DB.setName(identity.guildId, name);
2021-10-30 17:26:41 +00:00
respond(null, newMeta);
io.emit('update-metadata', newMeta);
2021-10-30 17:26:41 +00:00
}
);
/*
* Sets the icon of the guild
* @param iconBuff The new icon buffer of the guild
* @param respond function(errStr, guildMeta) as a socket.io response function
2021-10-30 17:26:41 +00:00
* @param respond.errStr Error string if an error, else null
* @param respond.guildMeta The update guild metadata information
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, identity,
{ verified: true, privileges: [ 'modify_profile' ] },
'set-icon', [ 'buffer', 'function' ],
async (iconBuff, respond) => {
if (iconBuff.length == 0 || iconBuff.length > MAX_ICON_SIZE) throw new EventError('invalid guild icon');
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
let typeResult = await FileType.fromBuffer(iconBuff);
if (!typeResult || !['image/png', 'image/jpeg', 'image/jpg'].includes(typeResult.mime)) {
throw new EventError('detected invalid mime type');
}
let iconResourceId = await DB.insertResource(identity.guildId, iconBuff);
let newMeta = await DB.setIcon(identity.guildId, iconResourceId);
2021-10-30 17:26:41 +00:00
respond(null, newMeta);
io.emit('update-metadata', newMeta);
2021-10-30 17:26:41 +00:00
}
);
/*
* Creates a text channel
* @param name The name of the new channel
* @param flavorText The flavor text of the new channel (can be null)
* @para repsond function(errStr, channel) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.channel The created channel
*/
bindEvent(
client, identity,
{ verified: true, privileges: [ 'modify_channels' ] },
2021-12-04 00:47:33 +00:00
'create-text-channel', [ 'string', 'string?', 'function' ],
2021-10-30 17:26:41 +00:00
async (name, flavorText, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
flavorText = flavorText || null;
if (name.length == 0 || name.length > MAX_CHANNEL_NAME_LENGTH || !(/^[A-Za-z0-9-]+$/.exec(name))) {
throw new EventError('invalid channel name');
}
if (flavorText != null && (flavorText.length == 0 || flavorText.length > MAX_CHANNEL_FLAVOR_TEXT_LENGTH)) {
throw new EventError('invalid flavor text');
}
let newChannel = await DB.createChannel(identity.guildId, name, flavorText);
2021-10-30 17:26:41 +00:00
respond(null, newChannel);
io.to(identity.guildId).emit('new-channel', newChannel);
2021-10-30 17:26:41 +00:00
}
);
/*
* Updates a channel
* @param channelId The uuid of the channel to update
* @param name The new name of the channel
* @param flavorText The new flavor text of the channel
* @param respond function(errStr, message) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.channel Updated channel
*/
bindEvent(
client, identity,
{ verified: true, privileges: [ 'modify_channels' ] },
'update-channel', [ 'string', 'string', 'string', 'function' ],
async (channelId, name, flavorText, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
flavorText = flavorText || null;
if (name.length == 0 || name.length > MAX_CHANNEL_NAME_LENGTH || !(/^[A-Za-z0-9-]+$/.exec(name))) {
throw new EventError('invalid name');
}
if (flavorText != null && (flavorText.length == 0 || flavorText.length > MAX_CHANNEL_FLAVOR_TEXT_LENGTH)) {
throw new EventError('invalid flavor text');
}
let updatedChannel = await DB.updateChannel(identity.guildId, channelId, name, flavorText);
2021-10-30 17:26:41 +00:00
respond(null, updatedChannel);
io.to(identity.guildId).emit('update-channel', updatedChannel);
2021-10-30 17:26:41 +00:00
}
);
/*
* Gets the list of tokens that have been or are available to use
* @param respond function(errStr, message) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.tokens The list of outstanding tokens in form of [ { token, member_id, created, expires } ]
*/
bindEvent(
client, identity,
{ verified: true, privileges: [ 'modify_members' ] },
'fetch-tokens', [ 'function' ],
async (respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
LOG.debug(`u#${identity.memberId}: fetching tokens`);
let tokens = await DB.getTokens(identity.guildId);
2021-10-30 17:26:41 +00:00
respond(null, tokens);
}
);
/*
* Revokes a token so that it can no longer be used
* @param token The token to revoke
* @param respond function(errStr) as a socket.io response function
* @param respond.errStr Error string if an error, else null
*/
bindEvent(
client, identity,
{ verified: true, privileges: [ 'modify_members' ] },
'revoke-token', [ 'string', 'function' ],
async (token, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!(await DB.isTokenReal(token))) {
throw new EventError('fake token');
}
if (!(await DB.isTokenForGuild(token, identity.guildId))) {
throw new EventError('fake token', undefined, 'tried to revoke token for a different guild');
2021-10-30 17:26:41 +00:00
}
if (await DB.isTokenTaken(token)) {
throw new EventError('token already taken');
}
LOG.debug(`u#${identity.memberId}: revoking t#${token}`);
await DB.revokeToken(token);
respond(null);
}
);
}
function bindActionEvents(io: socketio.Server, client: socketio.Socket, identity: IIdentity) {
/*
* Send a message to a channel
* @param channelId The uuid of the channel to send a message to
* @param text The text of the message
* @param respond function(errStr, message) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.message Sent message data object parsed from PostgreSQL
*/
bindEvent(
client, identity,
{ verified: true },
'send-message', [ 'string', 'string', 'function' ],
async (channelId, text, respond) => {
if (text.length > MAX_TEXT_MESSAGE_LENGTH) throw new EventError('message is too long');
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
let message = await DB.insertMessage(identity.guildId, channelId, identity.memberId, text);
2021-10-30 17:26:41 +00:00
LOG.info(`m#${message.id} ch#${message.channel_id} s@${formatDate(message.sent_dtg)} u#${message.member_id}: ${message.text}`);
respond(null, message);
io.to(identity.guildId).emit('new-message', message);
2021-10-30 17:26:41 +00:00
}
);
/*
* Send a message with a resource to a channel
* @param channelId The uuid of the channel to send a message to
* @param text The text of the message
* @param resource The resource buffer of the message
* @param resourceName The name of the resource
* @param respond function(errStr, message) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.message Sent message data object parsed from PostgreSQL
*/
bindEvent(
client, identity,
{ verified: true },
'send-message-with-resource', [ 'string', 'string?', 'buffer', 'string', 'function' ],
2021-10-30 17:26:41 +00:00
async (channelId, text, resource, resourceName, respond) => {
if (text && text.length > MAX_TEXT_MESSAGE_LENGTH) throw new EventError('message is too long');
if (resource.length > MAX_RESOURCE_SIZE) throw new EventError('resource is too large');
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
resourceName = resourceName.trim();
resourceName = resourceName.replace(/[^A-Za-z0-9 \.]/g, '_'); // only alphanumerics for file names
LOG.info(`u#${identity.memberId}: resource message with resource of size: ${resource.length} bytes`);
// Try to get the dimensions of the resource if it is an image so that we can scale it down for the preview
let fileType = (await FileType.fromBuffer(resource)) ?? { mime: null, ext: null };
let dimensions: { width: number | null, height: number | null } = { width: null, height: null };
switch (fileType.mime) {
case 'image/png':
case 'image/jpeg':
case 'image/gif':
let size = sizeOf(resource);
dimensions.width = size.width ?? null;
dimensions.height = size.height ?? null;
break;
}
// Pre-scale the image (optimized for the electron discord client's max-dimensions of 400x300)
let resourcePreview: Buffer | null = null;
if (dimensions.width != null && dimensions.height != null) {
let previewWidth = dimensions.width;
let previewHeight = dimensions.height;
if (previewWidth > 400) {
let scale = 400 / previewWidth;
previewWidth *= scale;
previewHeight *= scale;
}
if (previewHeight > 300) {
let scale = 300 / previewHeight;
previewWidth *= scale;
previewHeight *= scale;
}
// jpeg for image compression and trash visuals B)
resourcePreview = await sharp(resource).resize(Math.floor(previewWidth), Math.floor(previewHeight)).jpeg().toBuffer();
}
let message: any | null = null;
try {
await DB.queueTransaction(async () => {
if (!identity.guildId) throw new EventError('identity no guildId');
if (!identity.memberId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
let resourcePreviewId: string | null = null;
if (resourcePreview) {
resourcePreviewId = await DB.insertResource(identity.guildId, resourcePreview);
2021-10-30 17:26:41 +00:00
}
let resourceId = await DB.insertResource(identity.guildId, resource);
2021-10-30 17:26:41 +00:00
message = await DB.insertMessageWithResource(identity.guildId, channelId, identity.memberId, text, resourceId, resourceName, dimensions.width, dimensions.height, resourcePreviewId);
2021-10-30 17:26:41 +00:00
});
} catch (e) {
throw new EventError('unable to insert message with resource');
}
LOG.info(`m#${message.id} ch#${message.channel_id} s@${formatDate(message.sent_dtg)} u#${message.member_id}: ${message.text ? message.text + ' / ' : ''}${message.resource_name}`);
respond(null, message);
io.to(identity.guildId).emit('new-message', message);
2021-10-30 17:26:41 +00:00
}
);
/*
* Set the status for a verified member
* @param status The status for the member to be set to
* @param respond function(errStr, updatedMember) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.updatedMember The updated member object (note: an update-member event will be emitted)
*/
bindEvent(
client, identity,
{ verified: true },
'set-status', [ 'string', 'function' ],
async (status, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: setting status to ${status}`);
await DB.setMemberStatus(identity.guildId, identity.memberId, status);
let updated = await DB.getMember(identity.guildId, identity.memberId);
2021-10-30 17:26:41 +00:00
respond(null, updated);
io.to(identity.guildId).emit('update-member', updated);
2021-10-30 17:26:41 +00:00
}
);
/*
* Set the status for a verified member
* @param displayName The new chosen display name
* @param respond function(errStr, displayName) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.updatedMember The updated member object (note: an update-member event will be emitted)
*/
bindEvent(
client, identity,
{ verified: true },
'set-display-name', [ 'string', 'function' ],
async (displayName, respond) => {
if (displayName.length > MAX_DISPLAY_NAME_LENGTH || displayName.length == 0) throw new EventError('invalid display name');
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: setting display name to ${displayName}`);
await DB.setMemberDisplayName(identity.guildId, identity.memberId, displayName);
let updated = await DB.getMember(identity.guildId, identity.memberId);
2021-10-30 17:26:41 +00:00
respond(null, updated);
io.to(identity.guildId).emit('update-member', updated);
2021-10-30 17:26:41 +00:00
}
);
/*
* Set the status for a verified member
* @param avatarBuff The new chosen avatar buffer
* @param respond function(errStr, displayName) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.updatedMember The updated member object (note: an update-member event will be emitted)
*/
bindEvent(
client, identity,
{ verified: true },
'set-avatar', [ 'buffer', 'function' ],
async (avatarBuff, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
if (avatarBuff.length > MAX_AVATAR_SIZE) {
LOG.warn(`c#${client.id}: avatar too large`);
respond('buffer too large');
return;
}
let typeResult = (await FileType.fromBuffer(avatarBuff)) ?? { mime: null, ext: null };
if ((['image/png', 'image/jpeg', 'image/jpg'] as (string | null)[]).indexOf(typeResult.mime) == -1) {
throw new EventError('invalid avatar buffer');
}
LOG.info(`u#${identity.memberId}: uploaded new avatar`);
let resourceId = await DB.insertResource(identity.guildId, avatarBuff);
await DB.setMemberAvatarResourceId(identity.guildId, identity.memberId, resourceId);
let updated = await DB.getMember(identity.guildId, identity.memberId);
2021-10-30 17:26:41 +00:00
respond(null, updated);
io.to(identity.guildId).emit('update-member', updated);
2021-10-30 17:26:41 +00:00
}
);
}
function bindFetchEvents(client: socketio.Socket, identity: IIdentity): void { // TODO: consider changing all or some of these to HTTP requests
/*
* Fetch the guild information (name, icon, icon_hash, etc.)
* @param respond function(errStr, guild) as a socket.io response function
2021-10-30 17:26:41 +00:00
* @param respond.errStr Error string if an error, else null
* @param respond.guild The guild information
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-guild', [ 'function' ],
2021-10-30 17:26:41 +00:00
async (respond) => {
// TODO: Make sure this corresponds with the client
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching guild`);
let guild = await DB.getGuild(identity.guildId);
respond(null, guild);
2021-10-30 17:26:41 +00:00
}
);
/*
* Fetch a list of channels
* @param respond function(errStr, channels) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.channels The list of channels for this guild
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-channels', [ 'function' ],
async (respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching channels`);
let channels = await DB.getChannels(identity.guildId);
2021-10-30 17:26:41 +00:00
respond(null, channels);
}
);
/*
* Fetch the most recent messages for a specific channel
* @param channelId The channel uuid of the channel
* @param number The maximum number of messages to get
* @param respond function(errStr, messages) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.messages List of message data objects in the channel from PostgreSQL
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-messages-recent', [ 'string', 'number', 'function' ],
async (channelId, number, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching recent messages for ch#${channelId} number: ${number}`);
let messages = await DB.getMessagesRecent(identity.guildId, channelId, number);
2021-10-30 17:26:41 +00:00
respond(null, messages);
}
);
/*
* client.on('fetch-messages-before', (channelId, messageId, number, respond(errStr, messages)))
* Fetch messages coming before the specified message
* @param channelId The channel uuid of the channel
* @param messageId The id of the base message (will not be included in results)
* @param number The maximum number of messages to get
* @param respond function(errStr, messages) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.messages List of message data objects in the channel from PostgreSQL
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-messages-before', [ 'string', 'string', 'number', 'function' ],
async (channelId, messageId, number, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching messages before ch#${channelId} m#${messageId} number: ${number}`);
let messages = await DB.getMessagesBefore(identity.guildId, channelId, messageId, number);
2021-10-30 17:26:41 +00:00
respond(null, messages);
}
);
/*
* Fetch messages coming after the specified message
* @param channelId The channel uuid of the channel
* @param messageId The id of the base message (will not be included in results)
* @param number The maximum number of messages to get
* @param respond function(errStr, messages) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.messages List of message data objects in the channel from PostgreSQL
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-messages-after', [ 'string', 'string', 'number', 'function' ],
async (channelId, messageId, number, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching messages after ch#${channelId} m#${messageId} number: ${number}`);
let messages = await DB.getMessagesAfter(identity.guildId, channelId, messageId, number);
2021-10-30 17:26:41 +00:00
respond(null, messages);
}
);
/*
* @param resourceId The id of the resource to fetch
* @param respond function(errStr, resource) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.resource Resource object { id, guild_id, hash, data }
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-resource', [ 'string', 'function' ],
async (resourceId, respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching r#${resourceId}`);
let resource = await DB.getResource(identity.guildId, resourceId);
2021-10-30 17:26:41 +00:00
respond(null, resource);
}
);
/*
* Fetch members in the guild
2021-10-30 17:26:41 +00:00
* @param respond function(errStr, members) as a socket.io response function
* @param respond.errStr Error string if an error, else null
* @param respond.members List of member data objects in the guild from PostgreSQL
2021-10-30 17:26:41 +00:00
*/
bindEvent(
client, identity,
{ verified: true },
'fetch-members', [ 'function' ],
async (respond) => {
if (!identity.guildId) throw new EventError('identity no guildId');
2021-10-30 17:26:41 +00:00
if (!identity.memberId) throw new EventError('identity no memberId');
LOG.info(`u#${identity.memberId}: fetching members`);
let members = await DB.getMembers(identity.guildId);
2021-10-30 17:26:41 +00:00
respond(null, members);
}
);
}
export function bindSocketEvents(io: socketio.Server): void {
io.on('connection', client => {
LOG.info(`c#${client.id}: connected`);
let identity: IIdentity = {
guildId: null,
2021-10-30 17:26:41 +00:00
memberId: null,
verified: false,
publicKey: null,
challenge: null
};
connected.push(identity);
//bindIdentificationEvents(io, keypair, client);
bindRegistrationEvents(io, client);
bindChallengeVerificationEvents(io, client, identity);
bindAdminEvents(io, client, identity);
bindActionEvents(io, client, identity);
bindFetchEvents(client, identity);
client.on('disconnect', () => {
connected.splice(connected.findIndex(i => i.memberId == identity.memberId), 1);
if (identity.verified) {
LOG.info(`c#${client.id}: disconnected (was u#${identity.memberId})`);
(async () => {
if (!identity.guildId || !identity.memberId) return;
2021-10-30 17:26:41 +00:00
try {
await DB.setMemberStatus(identity.guildId, identity.memberId, 'offline');
2021-10-30 17:26:41 +00:00
} catch (e) {
LOG.error('Error updating member status on disconnect', e);
}
})();
} else {
LOG.info(`c#${client.id}: disconnected (was unverified)`);
}
});
});
}