detritus-client
Version:
A Typescript NodeJS library to interact with Discord's API, both Rest and Gateway.
388 lines (387 loc) • 16 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShardClient = void 0;
const Crypto = require("crypto");
const detritus_client_rest_1 = require("detritus-client-rest");
const detritus_client_socket_1 = require("detritus-client-socket");
const detritus_utils_1 = require("detritus-utils");
const rest_1 = require("./rest");
const constants_1 = require("./constants");
const handler_1 = require("./gateway/handler");
const basecollection_1 = require("./collections/basecollection");
const baseset_1 = require("./collections/baseset");
const collections_1 = require("./collections");
const voiceconnection_1 = require("./media/voiceconnection");
const structures_1 = require("./structures");
const utils_1 = require("./utils");
/**
* Shard Client, represents one gateway connection
* @category Clients
*/
class ShardClient extends detritus_utils_1.EventSpewer {
constructor(token, options = {}) {
super();
/**
* @ignore
*/
this._isBot = true;
this._killed = false;
this.application = null;
this.cluster = null;
this.commandClient = null;
this.interactionCommandClient = null;
/** Default Image Format to use for any url getters*/
this.imageFormat = constants_1.ImageFormats.PNG;
/**
* If this is a bot, this will be filled with it's application owner or all of the application's team owners
* If this is a user, this will only include the user object
* Only fills once we receive the Ready payload
*/
this.owners = new basecollection_1.BaseCollection();
/** If the client is ran or not */
this.ran = false;
/** Us, only fills once we received the Ready payload from the gateway */
this.user = null;
if (!token) {
throw new Error('Token is required for this library to work.');
}
this.token = token;
options = Object.assign({}, options);
if (options.cache === undefined) {
options.cache = {};
}
if (options.pass === undefined) {
options.pass = {};
}
this.cluster = options.pass.cluster || this.cluster;
this.commandClient = options.pass.commandClient || this.commandClient;
this.interactionCommandClient = options.pass.interactionCommandClient || this.interactionCommandClient;
this.gateway = new detritus_client_socket_1.Gateway.Socket(token, options.gateway);
this.gatewayHandler = new handler_1.GatewayHandler(this, options.gateway);
this.rest = new rest_1.RestClient(token, Object.assign({
authType: (options.isBot) ? constants_1.AuthTypes.BOT : constants_1.AuthTypes.USER,
}, options.rest), this);
if (options.isBot !== undefined) {
this._isBot = !!options.isBot;
}
if (options.imageFormat) {
const imageFormat = options.imageFormat.toLowerCase();
if (!constants_1.IMAGE_FORMATS.includes(imageFormat)) {
throw new Error(`Image format must be one of ${JSON.stringify(constants_1.IMAGE_FORMATS)}`);
}
this.imageFormat = imageFormat;
}
Object.defineProperties(this, {
_isBot: { configurable: true, enumerable: false, writable: false },
_killed: { configurable: true, enumerable: false, writable: false },
cluster: { enumerable: false, writable: false },
commandClient: { configurable: true, enumerable: false, writable: false },
gateway: { enumerable: false, writable: false },
ran: { configurable: true, writable: false },
rest: { enumerable: false, writable: false },
interactionCommandClient: { configurable: true, enumerable: false, writable: false },
token: { enumerable: false, writable: false },
});
if (typeof (options.cache) === 'boolean') {
const enabled = options.cache;
options.cache = {
applications: { enabled },
channels: { enabled },
connectedAccounts: { enabled },
emojis: { enabled },
guilds: { enabled },
interactions: { enabled },
members: { enabled },
messages: { enabled },
notes: { enabled },
presences: { enabled },
relationships: { enabled },
roles: { enabled },
sessions: { enabled },
stageInstances: { enabled },
stickers: { enabled },
typings: { enabled },
users: { enabled },
voiceCalls: { enabled },
voiceConnections: { enabled },
voiceStates: { enabled },
};
}
this.applications = options.pass.applications || new collections_1.Applications(this, options.cache.applications);
this.channels = options.pass.channels || new collections_1.Channels(this, options.cache.channels);
this.connectedAccounts = options.pass.connectedAccounts || new collections_1.ConnectedAccounts(this, options.cache.connectedAccounts);
this.emojis = options.pass.emojis || new collections_1.Emojis(this, options.cache.emojis);
this.guilds = options.pass.guilds || new collections_1.Guilds(this, options.cache.guilds);
this.interactions = options.pass.interactions || new collections_1.Interactions(this, options.cache.interactions);
this.members = options.pass.members || new collections_1.Members(this, options.cache.members);
this.messages = options.pass.messages || new collections_1.Messages(this, options.cache.messages);
this.notes = options.pass.notes || new collections_1.Notes(this, options.cache.notes);
this.presences = options.pass.presences || new collections_1.Presences(this, options.cache.presences);
this.relationships = options.pass.relationships || new collections_1.Relationships(this, options.cache.relationships);
this.roles = options.pass.roles || new collections_1.Roles(this, options.cache.roles);
this.sessions = options.pass.sessions || new collections_1.Sessions(this, options.cache.sessions);
this.stageInstances = options.pass.stageInstances || new collections_1.StageInstances(this, options.cache.stageInstances);
this.stickers = options.pass.stickers || new collections_1.Stickers(this, options.cache.stickers);
this.typings = options.pass.typings || new collections_1.TypingCollection(this, options.cache.typings);
this.users = options.pass.users || new collections_1.Users(this, options.cache.users);
this.voiceCalls = options.pass.voiceCalls || new collections_1.VoiceCalls(this, options.cache.voiceCalls);
this.voiceConnections = options.pass.voiceConnections || new collections_1.VoiceConnections(this, options.cache.voiceConnections);
this.voiceStates = options.pass.voiceStates || new collections_1.VoiceStates(this, options.cache.voiceStates);
}
get applicationId() {
if (this.application) {
return this.application.id;
}
return this.userId;
}
get clientId() {
return this.applicationId;
}
get isBot() {
if (this.user) {
return this.user.bot;
}
return this._isBot;
}
get killed() {
return this._killed && this.gateway.killed;
}
get shardCount() {
return this.gateway.shardCount;
}
get shardId() {
return this.gateway.shardId;
}
get userId() {
return this.gateway.userId || '';
}
_mergeOauth2Application(data) {
let oauth2Application;
if (this.application) {
oauth2Application = this.application;
oauth2Application.merge(data);
}
else {
oauth2Application = new structures_1.Oauth2Application(this, data);
this.application = oauth2Application;
}
if (oauth2Application.owner) {
this.owners.clear();
this.owners.set(oauth2Application.owner.id, oauth2Application.owner);
if (oauth2Application.team) {
for (let [userId, member] of oauth2Application.team.members) {
this.owners.set(userId, member.user);
}
}
}
return oauth2Application;
}
hookComponents(listenerId, components, timeout) {
if (components instanceof utils_1.Components) {
components.id = listenerId;
}
else {
components = new utils_1.Components({ components, id: listenerId, timeout: timeout || 0 });
}
this.gatewayHandler._componentHandler.insert(components);
return components;
}
isOwner(userId) {
return this.owners.has(userId);
}
kill(error) {
if (!this.killed) {
Object.defineProperty(this, '_killed', { value: true });
this.gateway.kill(error);
this.reset(true);
if (this.cluster) {
// must be a better way to handle this
// maybe kill the entire cluster?
this.cluster.shards.delete(this.shardId);
}
this.emit(constants_1.ClientEvents.KILLED, { error });
this.rest.raw.removeAllListeners();
this.removeAllListeners();
}
}
async ping() {
const [gateway, response] = await Promise.all([
this.gateway.ping(),
this.rest.request({
dataOnly: false,
route: {
path: detritus_client_rest_1.Endpoints.Api.ME,
},
}),
]);
return { gateway, rest: response.took };
}
async requestGuildMembers(guildId, oldOptions) {
const options = Object.assign({
limit: 0,
timeout: 1500,
}, oldOptions);
let key = `${guildId}:${options.limit}:${options.query}:${options.presences}`;
if (options.userIds && options.userIds.length) {
if (options.userIds.length <= 10) {
key += `:${options.userIds.join('.')}`;
}
else {
key += `:amount.${options.userIds.length}`;
}
}
const nonce = options.nonce = Crypto.createHash('md5').update(key).digest('hex');
let cache;
if (this.gatewayHandler._chunksWaiting.has(nonce)) {
cache = this.gatewayHandler._chunksWaiting.get(nonce);
}
else {
const promise = {};
promise.wait = new Promise((res, rej) => {
promise.resolve = res;
promise.reject = rej;
});
cache = {
members: new basecollection_1.BaseCollection(),
notFound: new baseset_1.BaseSet(),
presences: new basecollection_1.BaseCollection(),
promise,
waiting: 0,
};
this.gatewayHandler._chunksWaiting.set(nonce, cache);
this.gateway.requestGuildMembers(guildId, options);
}
cache.waiting++;
const timeout = new detritus_utils_1.Timers.Timeout();
return new Promise((resolve, reject) => {
cache.promise.wait.then(resolve).catch(reject);
timeout.start(options.timeout, () => {
reject(new Error(`Guild chunking took longer than ${options.timeout}ms.`));
cache.waiting--;
if (cache.waiting <= 0) {
this.gatewayHandler._chunksWaiting.delete(nonce);
}
});
}).then(() => {
timeout.stop();
this.gatewayHandler._chunksWaiting.delete(nonce);
return {
members: cache.members,
nonce,
notFound: cache.notFound,
presences: cache.presences,
};
});
}
reset(applications = true) {
if (applications) {
this.applications.clear();
}
this.channels.clear();
this.connectedAccounts.clear();
this.emojis.clear();
this.guilds.clear();
this.interactions.clear();
this.members.clear();
this.messages.clear();
this.notes.clear();
this.presences.clear();
this.relationships.clear();
this.roles.clear();
this.sessions.clear();
this.stageInstances.clear();
this.stickers.clear();
this.typings.clear();
this.users.clear();
this.voiceCalls.clear();
this.voiceConnections.clear();
this.voiceStates.clear();
}
async run(options = {}) {
if (this.ran) {
return this;
}
Object.defineProperty(this, 'ran', { value: true });
const wait = options.wait || options.wait === undefined;
let url;
if (options.url) {
url = options.url;
}
else {
const data = await this.rest.fetchGateway();
url = data.url;
}
this.gateway.connect(url);
if (wait) {
await new Promise((resolve, reject) => {
this.once(constants_1.ClientEvents.GATEWAY_READY, resolve);
this.once(constants_1.ClientEvents.KILLED, ({ error }) => reject(error));
});
}
return this;
}
/**
*
* @param guildId Guild Id you want to connect to, if a user and wanting to connect to a Dm Channel, keep this blank
* @param channelId Channel Id you want to connect to or move to
* @param options Options to pass into the `detritus-client-socket`'s gateway's voiceConnect
* @returns Returns a promise that resolves into a Voice Connection object and an isNew variable.
* isNew is used to see if the connection was reused (e.g. changing channels) so you can put listeners on or not
*/
async voiceConnect(guildId, channelId, options = {}) {
options.selfDeaf = options.selfDeaf || options.deaf;
options.selfMute = options.selfMute || options.mute;
const gateway = await this.gateway.voiceConnect(guildId, channelId, options);
const serverId = (guildId || channelId);
if (gateway) {
if (this.voiceConnections.has(serverId)) {
return {
connection: this.voiceConnections.get(serverId),
isNew: false,
};
}
try {
const payload = {
connection: new voiceconnection_1.VoiceConnection(this, gateway, options),
isNew: true,
};
this.voiceConnections.insert(payload.connection);
if (options.wait || options.wait === undefined) {
return new Promise((resolve) => {
payload.connection.once('ready', () => {
resolve(payload);
});
});
}
else {
return payload;
}
}
catch (error) {
gateway.kill(error);
throw error;
}
}
else {
if (this.voiceConnections.has(serverId)) {
this.voiceConnections.get(serverId).kill();
}
}
return null;
}
toString() {
return `Detritus Client (Shard ${this.shardId})`;
}
on(event, listener) {
super.on(event, listener);
return this;
}
once(event, listener) {
super.once(event, listener);
return this;
}
subscribe(event, listener) {
return super.subscribe(event, listener);
}
}
exports.ShardClient = ShardClient;