"use strict";
const TextChannel = require('../models/TextChannel');
const VoiceChannel = require('../models/VoiceChannel');
const CategoryChannel = require('../models/CategoryChannel');
const DMChannel = require('../models/DMChannel');
const Guild = require('../models/Guild');
const ClientUser = require('../models/ClientUser');
const User = require('../models/User');
const Member = require('../models/Member');
const Role = require('../models/Role');
const Message = require('../models/Message');
const Store = require('../utils/Store');
let Websocket;
try {
Websocket = require('uws');
} catch(err) {
Websocket = require('ws');
};
/**
* @class The Shard for the Client
* @prop {Number} latency The latency of the shard
* @prop {Number|String} id The id of the shard, it will only be a string if it's in the Client#shards
* @prop {Store} guilds A store of Guilds connected to the Shard
*/
class Shard {
constructor(client, id) {
this.client = client;
this.id = id;
// non-enumerable properties
Object.defineProperty(this, 'guildLength', { value: 0, writable: true });
Object.defineProperty(this, 'totalMemberCount', { value: 0, writable: true });
Object.defineProperty(this, 'totalMemberCountOfGuildMemberChunk', { value: 0, writable: true });
this.setup();
}
connect() {
return this.initiate();
}
initiate() {
this.ws = new Websocket(`${this.client.gatewayURL}/?v=6&encoding=json`);
this.ws.on('message', (event) => {
let packet = JSON.parse(event);
this.onMessage(packet);
});
return;
}
setup() {
let data = {
seq: null,
sessionID: null,
heartbeatInterval: 0,
ws: null,
failed: 0,
guilds: new Store(),
latency: Infinity
};
for (var i of Object.entries(data)) {
this[i[0]] = i[1];
};
return;
}
onMessage(packet) {
switch(packet.op) {
case 10:
this.heartbeat(packet.d.heartbeat_interval)
break;
case 0:
this.seq = packet.s;
this.onEvent(packet);
break;
case 11:
this.lastHeartbeatAck = new Date().getTime();
this.latency = this.lastHeartbeatAck - this.lastHeartbeatSent;
break;
case 9:
if (!packet.d) {
this.client.emit('debug', { shard: this.id, message: 'Received Opcode 9 ( Invalid Session ). Will re-login.' });
this.send({
op: 2,
d: {
token: this.client.token,
properties: {
$os: process.platform,
$browser: 'JCord',
$device: 'JCord'
},
shard: [this.id, this.client.shardCount]
}
});
} else {
this.client.emit('debug', { shard: this.id, message: 'Received Opcode 9 ( Invalid Session ). Will resume.' });
setTimeout(() => this.send({ op: 6, seq: this.seq, token: this.client.token, session_id: this.sessionID }), 2500);
};
break;
case 7:
this.client.emit('debug', { shard: this.id, message: 'Received Opcode 7 ( Reconnect ). Will reconnect the shard...' });
this.ws.close(4000);
this.ws = null;
this.ws = new Websocket(this.client.gatewayURL);
this.send({
op: 2,
d: {
token: this.client.token,
properties: {
$os: process.platform,
$browser: 'JCord',
$device: 'JCord'
},
shard: [this.id, this.client.shardCount]
}
});
break;
};
}
onEvent(packet) {
switch(packet.t) {
case 'MESSAGE_CREATE':
var channel = this.client.channels.get(packet.d.channel_id);
if (!this.client.users.has(packet.d.author.id)) {
this.client.users.set(packet.d.author.id, new User(this.client, packet.d.author));
};
let message = new Message(this.client, packet.d);
if (this.client.storeMessages) {
channel.messages.set(message.id, message)
};
this.client.emit('MESSAGE_CREATE', message);
break;
case 'CHANNEL_CREATE':
var guild = this.client.guilds.get(packet.d.guild_id);
switch(packet.d.type) {
case 0:
guild.channels.set(packet.d.id, new TextChannel(this.client, packet.d));
this.client.channels.set(packet.d.id, new TextChannel(this.client, packet.d));
break;
case 1:
this.client.channels.set(packet.d.id, new DMChannel(this.client, packet.d));
break;
case 2:
guild.channels.set(packet.d.id, new VoiceChannel(this.client, packet.d));
this.client.channels.set(packet.d.id, new VoiceChannel(this.client, packet.d));
break;
case 4:
guild.channels.set(packet.d.id, new CategoryChannel(this.client, packet.d));
this.client.channels.set(packet.d.id, new CategoryChannel(this.client, packet.d));
break;
};
this.client.emit('CHANNEL_CREATE', this.client.channels.get(packet.d.id));
break;
case 'READY':
this.client.user = new ClientUser(this.client, packet.d.user);
if (!packet.d.guilds.length) {
this.client.connectedShards.push(this.id);
/**
* Emitted once a shard is ready
* @event Client#SHARD_READY
* @prop {Object} shard The data of the shard
*/
this.client.emit('SHARD_READY', this);
if (this.client.connectedShards.length === this.client.shardCount) {
/**
* Emiited once all shards are ready
* @event Client#READY
*/
this.client.emit('READY');
}
};
this.guildLength = packet.d.guilds.length;
break;
case 'GUILD_CREATE':
packet.d.shard = this;
var guild = new Guild(this.client, packet.d);
this.guilds.set(guild.id, guild);
this.client.guilds.set(guild.id, guild);
this.guildLength--;
if (this.client.getAllMembers) {
this.totalMemberCount += guild.memberCount;
this.client.emit('debug', { shard: this.id, message: `Client#getAllMembers was true! Will request ALL guild members to be recevied and cached for Guild: ${guild.id}` });
this.fetchAllMembers(packet.d.id);
};
if (this.guildLength == 0 && this.status !== 'ready' && !this.client.getAllMembers) {
this.client.startTime = Date.now();
this.client.connectedShards.push(this.id);
/**
* Emitted once a shard is ready
* @event Client#SHARD_READY
* @prop {Object} shard The data of the shard
*/
this.client.emit('SHARD_READY', this);
if (this.client.connectedShards.length === this.client.shardCount) {
this.status = 'ready';
/**
* Emiited once all shards are ready
* @event Client#READY
*/
this.client.emit('READY');
}
};
break;
case 'GUILD_MEMBERS_CHUNK':
var guild = this.client.guilds.get(packet.d.guild_id);
for (var i = 0; i < packet.d.members.length; i++) {
guild.members.set(packet.d.members[i].user.id, packet.d.members[i]);
this.client.users.set(packet.d.members[i].user.id, new User(this.client, packet.d.members[i].user));
};
this.totalMemberCountOfGuildMemberChunk += packet.d.members.length;
this.client.guilds.set(guild.id, guild);
if (this.totalMemberCountOfGuildMemberChunk === this.totalMemberCount && this.status !== 'ready') {
this.client.emit('SHARD_READY', this);
this.client.connectedShards.push(this.id);
this.client.shards.set(this.id.toString(), this);
if (this.client.connectedShards.length === this.client.shardCount) {
this.client.startTime = Date.now();
this.status = 'ready';
/**
* Emiited once all shards are ready
* @event Client#READY
*/
this.client.emit('READY');
}
};
this.client.emit('GUILD_MEMBERS_CHUNK', packet.d);
break;
case 'GUILD_MEMBER_ADD':
packet.d.guild = this.client.guilds.get(packet.d.guild_id);
var member = new Member(this.client, packet.d);
if (!this.client.users.has(member.user.id)) {
this.client.users.set(member.user.id, new User(this.client, member.user));
};
packet.d.guild.members.set(member.user.id, member);
/**
* Emitted when a guild member joins a guild
* @event Client#GUILD_MEMBER_ADD
* @prop {Member} member The member that joined
*/
this.client.emit('GUILD_MEMBER_ADD', member);
break;
case 'GUILD_MEMBER_REMOVE':
var guild = this.client.guilds.get(packet.d.guild_id);
if (!guild.members.has(packet.d.user.id)) {
return this.client.emit('debug', { shard: this.id, message: 'Guild Member left, but not in cache. Will not emit Client#GUILD_MEMBER_ADD' });
};
var member = guild.members.get(packet.d.user.id);
guild.members.delete(member.user.id);
/**
* Emitted when a guild member leaves a guild
* @event Client#GUILD_MEMBER_REMOVE
* @prop {Member} member The member that left
*/
this.client.emit('GUILD_MEMBER_REMOVE', member);
break;
case 'GUILD_MEMBER_UPDATE':
var guild = this.client.guilds.get(packet.d.guild_id);
var member = guild.members.get(packet.d.user.id);
if (!packet.d.roles.includes(guild.id)) packet.d.roles.push(guild.id);
if (packet.d.roles.length > member.roles.size) {
var role = member.guild.roles.get(packet.d.roles[packet.d.roles.length - 2]);
member.roles.set(role.id, role);
};
if (packet.d.roles.length < member.roles.size) {
var oldRoleIDs = member.roles.keyArray();
var removedRoles = oldRoleIDs.filter(val => !packet.d.roles.includes(val));
removedRoles.forEach(role => {
member.roles.delete(role);
});
};
/**
* Emitted when a guild gets updated
* @event Client#GUILD_MEMBER_UPDATE
* @prop {Member} member The member that got updated
*/
this.client.emit('GUILD_MEMBER_UPDATE', member);
break;
case 'PRESENCE_UPDATE':
let data = {};
if (packet.d.user.hasOwnProperty('avatar') && packet.d.user.hasOwnProperty('username') && packet.d.user.hasOwnProperty('discriminator')) {
this.client.users.set(packet.d.user.id, new User(this.client, packet.d.user));
};
data.user = this.client.users.get(packet.d.user.id);
data.status = packet.d.status;
data.activities = packet.d.activities;
data.game = packet.d.game || null
/**
* Emitted when a user changes his presence or user info
* @event Client#PRESENCE_UPDATE
* @prop {Object} data The data of the presence
*/
this.client.emit('PRESENCE_UPDATE', data);
break;
case 'GUILD_ROLE_CREATE':
var guild = packet.d.guild_id ? this.client.guilds.get(packet.d.guild_id) : null;
guild.roles.set(packet.d.role.id, new Role(this.client, packet.d.role));
/**
* @event Client#GUILD_ROLE_CREATE
* @prop {Role} role The new role created
*/
this.client.emit('GUILD_ROLE_CREATE', new Role(this.client, packet.d.role));
break;
case 'GUILD_ROLE_DELETE':
var guild = packet.d.guild_id ? this.client.guilds.get(packet.d.guild_id) : null;
var role = guild.roles.get(packet.d.role_id);
var members = guild.members.filter(member => member.roles.has(role.id));
for (var i = 0; i < members.length; i++) {
members[i].roles.delete(role.id);
};
guild.roles.delete(role.id);
/**
* @event Client#GUILD_ROLE_DELETE
* @prop {Role} role The role deleted
*/
this.client.emit('GUILD_ROLE_DELETE', role);
break;
case 'GUILD_ROLE_UPDATE':
var guild = packet.d.guild_id ? this.client.guilds.get(packet.d.guild_id) : null;
var role = new Role(this.client, packet.d.role);
var members = guild.members.filter(member => member.roles.has(role.id));
for (var i = 0; i < members.length; i++) {
members[i].roles.set(role.id, role);
};
guild.roles.set(role.id, role);
/**
* @event Client#GUILD_ROLE_UPDATE
* @prop {Role} role The role updated
*/
this.client.emit('GUILD_ROLE_UPDATE', role);
break;
};
return packet;
}
heartbeat(interval) {
/**
* Emitted once a Shard is being loaded
* @event Client#SHARD_LOADING
* @prop {Shard<Id>} shard Returns a partial Shard Object, contains an id only
*/
this.client.emit('SHARD_LOADING', { id: this.id });
this.lastHeartbeatSent = new Date().getTime();
this.client.emit('debug', { shard: this.id, message: 'Sent the first heartbeat!' });
this.send({ op: 1, d: null });
this.client.emit('debug', { shard: this.id, message: 'Sent Opcode 2 ( Identify ). Bot will now login' });
this.send({
op: 2,
d: {
token: this.client.token,
properties: {
$os: process.platform,
$browser: 'JCord',
$device: 'JCord'
},
shard: [this.id, this.client.shardCount],
large_threshold: this.largeThreshold
}
});
setInterval(() => {
this.lastHeartbeatSent = new Date().getTime();
this.client.emit('debug', { shard: this.id, message: 'Sent another heartbeat!' });
this.send({ op: 1, d: this.seq })
}, interval);
return;
}
send(data) {
this.ws.send(typeof(data) === 'object' ? JSON.stringify(data) : data);
return data;
}
fetchAllMembers(guilds) {
return this.send({ op: 8, d: {
guild_id: guilds,
query: "",
limit: 0
}
});
}
};
module.exports = Shard;