"use strict";
const EventEmitter = require('events').EventEmitter;
const Shard = require('../gateway/Shard');
const Store = require('../utils/Store');
const RestHandler = require('../rest/RestHandler');
const User = require('../models/User');
const { ENDPOINTS } = require('../utils/Constants').HTTP;
const DMChannel = require('../models/DMChannel');
const Message = require('../models/Message');
const { PERMISSIONS } = require('../utils/Constants');
/**
* @extends EventEmitter Represents a Discord Client
* @prop {Store} channels Where channels are being cached
* @prop {Store} connectedShards A store of **connected** shards
* @prop {Store} guilds Where guilds are being cached
* @prop {String} token The token of the client
* @prop {Store} users Where users are being cached
* @prop {Store} shards A store of Shards
* @prop {Number} shardCount The amount of shards to connect to Discord. ( useful for by shard status )
* @prop {Object} [options] Options for the Discord Client
* @prop {Number|String} [options.shardCount=1] The amount of shards to use
* @prop {Boolean} [options.disableEveryone=true] Whether to disable the @everyone ping
* @prop {Boolean} [options.getAllMembers=false] Whether to fetch all members on each guild regardless of being offline or not
* @prop {Boolean} [options.storeMessages=false] Whether to store messages in a cache, once the bot restarts the messages in the cache will be gone and can't be re-added automatically
* @prop {Number} [options.loginDelay=6500] The amount of time in ms to add a delay when connecting shards
* @prop {Boolean} [options.logger=false] Whether to use our custom logging
* @prop {Number} [options.largeThreshold=250] The amount of members needed to consider a guild large
*/
class Client extends EventEmitter {
constructor(options = {}) {
super(options);
this.shardCount = options.shardCount || 1;
this.firstShardSent = false;
this.getAllMembers = options.getAllMembers || false;
this.storeMessages = options.storeMessages || false;
this.loginDelay = options.loginDelay || 6500;
this.logger = options.logger || false;
this.largeThreshold = options.largeThreshold || 250;
this.disableEveryone = options.disableEveryone || true;
this.chalk = null;
this.status = null;
if (this.logger) {
try {
this.chalk = require('chalk');
} catch(error) {
this.emit('error', new Error(`Module "chalk" is missing! Install it by typing "npm install chalk"`));
}
};
if (this.shards < 1 || (typeof this.shards === 'string' && this.shards !== 'auto')) this.emit('error', new Error('Invalid amount of shards! Must be more than one or use \'auto\''));
this.channels = new Store();
this.guilds = new Store();
this.shards = new Store();
this.users = new Store();
this.user = null;
this.rest = new RestHandler(this);
this.gatewayURL = null;
let client_activity = {
game: null,
since: null,
status: 'online',
afk: false
};
Object.defineProperty(this, 'permissions', { value: new Store() });
Object.defineProperty(this, 'connectedShards', { value: new Store() });
Object.defineProperty(this, 'token', { value: null, writable: true });
Object.defineProperty(this, 'presence', { value: client_activity, writable: true });
Object.defineProperty(this, '_presences', { value: new Store() });
Object.defineProperty(this, 'deprecator', { value: require('../utils/Deprecator') });
for (var i of Object.entries(PERMISSIONS)) {
this.permissions.set(i[0], i[1]);
}
}
get uptime() {
return this.startTime ? Date.now() - this.startTime : null;
}
/**
* Creates a DMChannel between a user
* @param {Snowflake} user The id of the recipient for the DM
* @returns {Promise<DMChannel>}
*/
createDM(user) {
return this.rest.request("POST", ENDPOINTS.USER_CHANNELS('@me'), {
data: {
recipient_id: user
}
}).then(res => {
return new DMChannel(this, res.data);
});
}
/**
* Creates a guild
* @param {String} name The name for the new guild
* @param {Object} [options] Options for the new guild
* @param {String} [options.region] The region for the guild
* @param {String} [options.icon] A base64 128x128 jpeg image for the guild icon
* @returns {Promise<Guild>}
*/
createGuild(name, options = {}) {
return this.rest.request("POST", ENDPOINTS.GUILDS, {
data: {
name,
region: options.region,
icon: options.icon
}
}).then(res => {
return this.guilds.get(res.data.id);
});
}
/**
* Creates an embed to a channel
* @deprecated Use Client#sendMessage() instead.
* @param {Snowflake} channel The id of the channel to send a message to
* @param {Object} embed The embed to send
* @returns {Promise<Message>}
*/
createEmbed(channel, embed) {
this.deprecator.deprecate('Client', 'createEmbed', 'Client', 'sendMessage');
return this.rest.request("POST", ENDPOINTS.CHANNEL_MESSAGES(channel), {
data: {
embed: embed.hasOwnProperty('embed') ? embed.embed : embed
}
}).then(res => {
return new Message(this, res.data);
});
}
/**
* Creates a message to a channel
* @deprecated Use Client#sendMessage() instead.
* @param {Snowflake} channel The id of the channel to send a message to
* @param {String} content The content to send
* @returns {Promise<Message>}
*/
createMessage(channel, content) {
this.deprecator.deprecate('Client', 'createMessage', 'Client', 'sendMessage');
if (content && content.length > 2000) return this.emit('error', new Error('Message length must be equal to or less than 2000 Characters!'));
if (this.disableEveryone) {
content = content.replace(/@everyone/g, '@\u200beveryone');
};
return this.rest.request("POST", ENDPOINTS.CHANNEL_MESSAGES(channel), {
data: {
content
}
}).then(res => {
return new Message(this, res.data);
});
}
/**
* Fetches the user from cache, if it doesn't exist use the REST API to fetch it and add to the cache
* @param {Snowflake} user The id of the user to fetch
* @returns {Promise<User>}
*/
getUser(user) {
if (!this.users.has(user)) {
return this.rest.request("GET", ENDPOINTS.USER(user))
.then(res => {
return this.users.set(res.data.id, new User(this, res.data));
});
} else {
return new Promise((resolve, reject) => {
return resolve(this.users.get(user));
});
};
}
/**
* Makes the bot leave the guild
* @param {Snowflake} guild The id of the guild
* @returns {Promise<Boolean>} Will return true if it's a success
*/
leaveGuild(guild) {
return this.rest.request("DELETE", ENDPOINTS.GUILD(guild))
.then(() => {
return true;
});
}
/**
* Spawns a shard
* @param {Number} id The id of the shard
* @returns {Boolean}
*/
spawn(id) {
return new Shard(this, id).connect();
}
/**
* This will start connecting to the gateway using the given bot token
* @param {String} token The token of the user
* @returns {Void}
*/
async initiate(token) {
this.token = token;
let data = await this.rest.request("GET", '/gateway/bot');
this.gatewayURL = data.data.url;
if (this.shardCount === 'auto') {
let res = await this.rest.request("GET", '/gateway/bot');
this.shardCount = res.data.shards;
};
this.emit('debug', { shard: 'Global', message: `Connecting to Discord... Shards: ${this.shardCount}` });
for (let i = 0; i < this.shardCount; i++) {
this.shards.set(i.toString(), null);
setTimeout(() => {
this.spawn(i);
}, i * this.loginDelay);
};
}
/**
* Logs something to the console that is colored.
* @param {String} type The type of the log
* @param {String} message The message to log
* @returns {Object}
*/
log(type, message) {
if (!this.logger) return this.emit('error', new Error('You need to install the "chalk" package!'));
let types = ['success', 'warn', 'error', 'loading', 'retrying'];
if (!type || type && !types.includes(type.toLowerCase())) this.emit('Invalid Type of log!');
switch(type) {
case 'error':
console.log(`[${this.chalk.red('ERROR')}]: ${message}`);
break;
case 'loading':
console.log(`[${this.chalk.cyan('LOADING')}]: ${message}`);
break;
case 'success':
console.log(`[${this.chalk.green('SUCCESS')}]: ${message}`);
break;
case 'warn':
console.log(`[${this.chalk.yellow('WARNING')}]: ${message}`);
break;
case 'retrying':
console.log(`[${this.chalk.blue('RETRYING')}]: ${message}`);
break;
}
return { type, message };
}
/**
* Edits a message
* @param {Snowflake} channel The id of the channel
* @param {Object} options Options for the message editing
* @param {String} options.content The content of the message
* @param {Embed} options.embed The embed for the message
* @param {Snowflake} options.message The id of the message
* @returns {Promise<Message>}
*/
patchMessage(channel, options = {}) {
return this.rest.request("PATCH", ENDPOINTS.CHANNEL_MESSAGE(channel, options.message), {
data: {
content: options.content || null,
embed: (embed.hasOwnProperty('embed') ? embed.embed : embed) || null
}
}).then(res => {
return new Message(this.client, res.data);
});
}
/**
* Make your own log with your own type and color
* @param {String} type The type of the log, can be anything
* @param {String} color The color of the type
* @param {String} message The message to log
* @returns {Object}
*/
customLog(type, color, message) {
if (!this.logger) return this.emit('error', new Error('You need to install the "chalk" package!'));
if (!type) type = 'info';
if (!color) color = 'white';
console.log(`[${this.chalk[color.toLowerCase()](type)}]: ${message}`);
return { type, color, message };
}
/**
* Make your own log with your own type and color using RGB
* @param {String} type The type of the log, can be anything
* @param {String} message The message to log
* @param {Object} [options] The options for the RGB Color
* @param {Number} [options.red=0] The number for the color red in the rgb data
* @param {Number} [options.green=0] The number for the color green in the rgb data
* @param {Number} [options.blue=0] The number for the color blue in the rgb data
* @returns {Object}
*/
rgbLog(type, message, options = { red: 0, green: 0, blue: 0 }) {
if (!this.logger) return this.emit('error', new Error('You need to install the "chalk" package!'));
console.log(`[${this.chalk.rgb(options.red, options.green, options.blue)(type)}]: ${message}`);
return options;
}
/**
* Sends a message to a channel
* @param {Snowflake} channel
* @param {Object} options
* @param {String} [options.content] The content of the message
* @param {Embed} [options.embed] The embed object of the message
* @returns {Promise<Message>}
*/
sendMessage(channel, options = {}) {
if (options.content && typeof options.content === 'string' && options.content.length > 2000)
return this.emit('error', new RangeError('Maximum of 2000 Characters for Content has been reached!'));
return this.rest.request('POST', ENDPOINTS.CHANNEL_MESSAGES(channel), {
data: {
content: options.content || null,
embed: (embed.hasOwnProperty('embed') ? embed.embed : embed) || null
}
}).then(res => {
return new Message(this, res.data);
});
}
/**
* Sends an activity to the shard
* @param {String} shard The id of the shard, or `'all'` for all shards
* @param {Object} [options] The options for the Activity
* @param {Number} [options.since=null] Unix time (in milliseconds) of when the client went idle, or null if the client is not idle
* @param {Object} [options.game=null] The user's new activity
* @param {String} [options.game.name=null] The activity's name
* @param {Number} [options.game.type=0] The [activity's type](https://discordapp.com/developers/docs/topics/gateway#activity-object-activity-types)
* @param {String} [options.game.url=null] The url of the activity ( Only for game type 1 )
* @param {String} [options.status='online'] The new status of the client
* @param {Boolean} [options.afk=false] Whether or not the client is afk
* @returns {Object} The options for the Activity
*/
setActivity(shard, options = {}) {
if (!shard || (shard && (shard !== 'all' || shard !== 'all' && !this.connectedShards.has(shard))))
return this.emit('error', new Error('Invalid Shard!'));
shard = typeof shard === 'number' ? shard.toString() : shard;
if (shard === 'all') {
for (var i = 0; i < this.connectedShards.size; i++)
this.connectedShards.get(i.toString()).setActivity(options);
} else {
this.connectedShards.get(shard).setActivity(options);
}
return options;
}
};
module.exports = Client;