mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-10 08:33:30 +01:00
Merge remote-tracking branch 'origin/indev' into indev-voice
This commit is contained in:
@@ -20,22 +20,19 @@ class Client extends EventEmitter {
|
||||
/**
|
||||
* @param {ClientOptions} [options] Options for the client
|
||||
*/
|
||||
constructor(options) {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
// Obtain shard details from environment
|
||||
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
|
||||
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);
|
||||
|
||||
/**
|
||||
* The options the client was instantiated with
|
||||
* @type {ClientOptions}
|
||||
*/
|
||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||
|
||||
if (!this.options.shardId && 'SHARD_ID' in process.env) {
|
||||
this.options.shardId = Number(process.env.SHARD_ID);
|
||||
}
|
||||
|
||||
if (!this.options.shardCount && 'SHARD_COUNT' in process.env) {
|
||||
this.options.shardCount = Number(process.env.SHARD_COUNT);
|
||||
}
|
||||
this._validateOptions();
|
||||
|
||||
/**
|
||||
* The REST manager of the client
|
||||
@@ -117,11 +114,15 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
this.presences = new Collection();
|
||||
|
||||
/**
|
||||
* The authorization token for the logged in user/bot.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.token = null;
|
||||
if (!this.token && 'CLIENT_TOKEN' in process.env) {
|
||||
/**
|
||||
* The authorization token for the logged in user/bot.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.token = process.env.CLIENT_TOKEN;
|
||||
} else {
|
||||
this.token = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The email, if there is one, for the logged in Client
|
||||
@@ -145,7 +146,7 @@ class Client extends EventEmitter {
|
||||
* The date at which the Client was regarded as being in the `READY` state.
|
||||
* @type {?Date}
|
||||
*/
|
||||
this.readyTime = null;
|
||||
this.readyAt = null;
|
||||
|
||||
this._timeouts = new Set();
|
||||
this._intervals = new Set();
|
||||
@@ -170,7 +171,7 @@ class Client extends EventEmitter {
|
||||
* @readonly
|
||||
*/
|
||||
get uptime() {
|
||||
return this.readyTime ? Date.now() - this.readyTime : null;
|
||||
return this.readyAt ? Date.now() - this.readyAt : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,6 +196,15 @@ class Client extends EventEmitter {
|
||||
return emojis;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp that the client was last ready at
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
get readyTimestamp() {
|
||||
return this.readyAt ? this.readyAt.getTime() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the client in. If successful, resolves with the account's token. <warn>If you're making a bot, it's
|
||||
* much better to use a bot account rather than a user account.
|
||||
@@ -241,13 +251,13 @@ class Client extends EventEmitter {
|
||||
/**
|
||||
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
|
||||
* if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
|
||||
* @param {Guild[]} [guilds=this.guilds.array()] An array of guilds to sync
|
||||
* @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
|
||||
*/
|
||||
syncGuilds(guilds = this.guilds.array()) {
|
||||
syncGuilds(guilds = this.guilds) {
|
||||
if (!this.user.bot) {
|
||||
this.ws.send({
|
||||
op: 12,
|
||||
d: guilds.map(g => g.id),
|
||||
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -265,13 +275,23 @@ class Client extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Fetches an invite object from an invite code.
|
||||
* @param {string} code the invite code.
|
||||
* @param {InviteResolvable} invite An invite code or URL
|
||||
* @returns {Promise<Invite>}
|
||||
*/
|
||||
fetchInvite(code) {
|
||||
fetchInvite(invite) {
|
||||
const code = this.resolver.resolveInviteCode(invite);
|
||||
return this.rest.methods.getInvite(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a webhook by ID.
|
||||
* @param {string} id ID of the webhook
|
||||
* @returns {Promise<Webhook>}
|
||||
*/
|
||||
fetchWebhook(id) {
|
||||
return this.rest.methods.getWebhook(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all channels' messages and removes the ones older than the max message lifetime.
|
||||
* If the message has been edited, the time of the edit is used rather than the time of the original message.
|
||||
@@ -281,7 +301,7 @@ class Client extends EventEmitter {
|
||||
* or -1 if the message cache lifetime is unlimited
|
||||
*/
|
||||
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('Lifetime must be a number.');
|
||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
|
||||
if (lifetime <= 0) {
|
||||
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
|
||||
return -1;
|
||||
@@ -344,6 +364,39 @@ class Client extends EventEmitter {
|
||||
_eval(script) {
|
||||
return eval(script);
|
||||
}
|
||||
|
||||
_validateOptions(options = this.options) {
|
||||
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
|
||||
throw new TypeError('The shardCount option must be a number.');
|
||||
}
|
||||
if (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
|
||||
throw new TypeError('The shardId option must be a number.');
|
||||
}
|
||||
if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.');
|
||||
if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.');
|
||||
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
|
||||
throw new RangeError('The shardId option must be less than shardCount.');
|
||||
}
|
||||
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
|
||||
throw new TypeError('The messageCacheMaxSize option must be a number.');
|
||||
}
|
||||
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
|
||||
throw new TypeError('The messageCacheLifetime option must be a number.');
|
||||
}
|
||||
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
|
||||
throw new TypeError('The messageSweepInterval option must be a number.');
|
||||
}
|
||||
if (typeof options.fetchAllMembers !== 'boolean') {
|
||||
throw new TypeError('The fetchAllMembers option must be a boolean.');
|
||||
}
|
||||
if (typeof options.disableEveryone !== 'boolean') {
|
||||
throw new TypeError('The disableEveryone option must be a boolean.');
|
||||
}
|
||||
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
|
||||
throw new TypeError('The restWsBridgeTimeout option must be a number.');
|
||||
}
|
||||
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
|
||||
@@ -118,6 +118,12 @@ class ClientDataManager {
|
||||
updateChannel(currentChannel, newData) {
|
||||
currentChannel.setup(newData);
|
||||
}
|
||||
|
||||
updateEmoji(currentEmoji, newData) {
|
||||
const oldEmoji = cloneObject(currentEmoji);
|
||||
currentEmoji.setup(newData);
|
||||
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientDataManager;
|
||||
|
||||
@@ -99,23 +99,6 @@ class ClientDataResolver {
|
||||
return guild.members.get(user.id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
||||
* * A Buffer
|
||||
* * A Base64 string
|
||||
* @typedef {Buffer|string} Base64Resolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a Base64Resolvable to a Base 64 image
|
||||
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
resolveBase64(data) {
|
||||
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Channel. This can be:
|
||||
* * An instance of a Channel
|
||||
@@ -138,6 +121,26 @@ class ClientDataResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give an invite code. This can be:
|
||||
* * An invite code
|
||||
* * An invite URL
|
||||
* @typedef {string} InviteResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves InviteResolvable to an invite code
|
||||
* @param {InviteResolvable} data The invite resolvable to resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
resolveInviteCode(data) {
|
||||
const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i;
|
||||
const match = inviteRegex.exec(data);
|
||||
|
||||
if (match && match[1]) return match[1];
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a permission number. This can be:
|
||||
* * A string
|
||||
@@ -205,6 +208,23 @@ class ClientDataResolver {
|
||||
return String(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
||||
* * A Buffer
|
||||
* * A Base64 string
|
||||
* @typedef {Buffer|string} Base64Resolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a Base64Resolvable to a Base 64 image
|
||||
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
resolveBase64(data) {
|
||||
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Buffer. This can be:
|
||||
* * A Buffer
|
||||
|
||||
@@ -49,6 +49,7 @@ class ClientManager {
|
||||
*/
|
||||
setupKeepAlive(time) {
|
||||
this.heartbeatInterval = this.client.setInterval(() => {
|
||||
this.client.emit('debug', 'Sending heartbeat');
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.HEARTBEAT,
|
||||
d: this.client.ws.sequence,
|
||||
|
||||
46
src/client/WebhookClient.js
Normal file
46
src/client/WebhookClient.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const Webhook = require('../structures/Webhook');
|
||||
const RESTManager = require('./rest/RESTManager');
|
||||
const ClientDataResolver = require('./ClientDataResolver');
|
||||
const mergeDefault = require('../util/MergeDefault');
|
||||
const Constants = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* The Webhook Client
|
||||
* @extends {Webhook}
|
||||
*/
|
||||
class WebhookClient extends Webhook {
|
||||
/**
|
||||
* @param {string} id The id of the webhook.
|
||||
* @param {string} token the token of the webhook.
|
||||
* @param {ClientOptions} [options] Options for the client
|
||||
* @example
|
||||
* // create a new webhook and send a message
|
||||
* let hook = new Discord.WebhookClient('1234', 'abcdef')
|
||||
* hook.sendMessage('This will send a message').catch(console.log)
|
||||
*/
|
||||
constructor(id, token, options) {
|
||||
super(null, id, token);
|
||||
|
||||
/**
|
||||
* The options the client was instantiated with
|
||||
* @type {ClientOptions}
|
||||
*/
|
||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||
|
||||
/**
|
||||
* The REST manager of the client
|
||||
* @type {RESTManager}
|
||||
* @private
|
||||
*/
|
||||
this.rest = new RESTManager(this);
|
||||
|
||||
/**
|
||||
* The Data Resolver of the Client
|
||||
* @type {ClientDataResolver}
|
||||
* @private
|
||||
*/
|
||||
this.resolver = new ClientDataResolver(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebhookClient;
|
||||
@@ -1,13 +1,15 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
class GuildEmojiUpdateAction extends Action {
|
||||
handle(data, guild) {
|
||||
const client = this.client;
|
||||
for (let emoji of data.emojis) {
|
||||
const already = guild.emojis.has(emoji.id);
|
||||
emoji = client.dataManager.newEmoji(emoji, guild);
|
||||
if (already) client.emit(Constants.Events.GUILD_EMOJI_UPDATE, guild, emoji);
|
||||
if (already) {
|
||||
client.dataManager.updateEmoji(guild.emojis.get(emoji.id), emoji);
|
||||
} else {
|
||||
emoji = client.dataManager.newEmoji(emoji, guild);
|
||||
}
|
||||
}
|
||||
for (let emoji of guild.emojis) {
|
||||
if (!data.emoijs.has(emoji.id)) client.dataManager.killEmoji(emoji);
|
||||
@@ -21,7 +23,7 @@ class GuildEmojiUpdateAction extends Action {
|
||||
/**
|
||||
* Emitted whenever an emoji is updated
|
||||
* @event Client#guildEmojiUpdate
|
||||
* @param {Guild} guild The guild that the emoji was updated in.
|
||||
* @param {Emoji} emoji The emoji that was updated.
|
||||
* @param {Emoji} oldEmoji The old emoji
|
||||
* @param {Emoji} newEmoji The new emoji
|
||||
*/
|
||||
module.exports = GuildEmojiUpdateAction;
|
||||
|
||||
@@ -7,13 +7,15 @@ const User = requireStructure('User');
|
||||
const GuildMember = requireStructure('GuildMember');
|
||||
const Role = requireStructure('Role');
|
||||
const Invite = requireStructure('Invite');
|
||||
const Webhook = requireStructure('Webhook');
|
||||
|
||||
class RESTMethods {
|
||||
constructor(restManager) {
|
||||
this.rest = restManager;
|
||||
}
|
||||
|
||||
loginToken(token) {
|
||||
loginToken(token = this.rest.client.token) {
|
||||
token = token.replace(/^Bot\s*/i, '');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.client.manager.connectToWebSocket(token, resolve, reject);
|
||||
});
|
||||
@@ -26,7 +28,7 @@ class RESTMethods {
|
||||
this.rest.client.password = password;
|
||||
this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
|
||||
.then(data => {
|
||||
this.rest.client.manager.connectToWebSocket(data.token, resolve, reject);
|
||||
resolve(this.loginToken(data.token));
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
@@ -47,21 +49,26 @@ class RESTMethods {
|
||||
});
|
||||
}
|
||||
|
||||
getBotGateway() {
|
||||
return this.rest.makeRequest('get', Constants.Endpoints.botGateway, true);
|
||||
}
|
||||
|
||||
sendMessage(channel, content, { tts, nonce, disableEveryone, split } = {}, file = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
||||
|
||||
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
||||
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
||||
}
|
||||
if (content) {
|
||||
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
||||
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
||||
}
|
||||
|
||||
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
|
||||
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
|
||||
}
|
||||
|
||||
if (channel instanceof User || channel instanceof GuildMember) {
|
||||
this.createDM(channel).then(chan => {
|
||||
this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
|
||||
})
|
||||
.catch(reject);
|
||||
}).catch(reject);
|
||||
} else {
|
||||
this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
|
||||
}
|
||||
@@ -71,22 +78,24 @@ class RESTMethods {
|
||||
_sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
|
||||
if (content instanceof Array) {
|
||||
const datas = [];
|
||||
const promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||
let promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||
content: content[0], tts, nonce,
|
||||
}, file).catch(reject);
|
||||
|
||||
for (let i = 1; i <= content.length; i++) {
|
||||
if (i < content.length) {
|
||||
promise.then(data => {
|
||||
const i2 = i;
|
||||
promise = promise.then(data => {
|
||||
datas.push(data);
|
||||
return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||
content: content[i], tts, nonce,
|
||||
content: content[i2], tts, nonce,
|
||||
}, file);
|
||||
});
|
||||
}).catch(reject);
|
||||
} else {
|
||||
promise.then(data => {
|
||||
datas.push(data);
|
||||
resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
|
||||
});
|
||||
}).catch(reject);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -535,6 +544,138 @@ class RESTMethods {
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
getWebhook(id, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('get', Constants.Endpoints.webhook(id, token), require('util').isUndefined(token))
|
||||
.then(data => {
|
||||
resolve(new Webhook(this.rest.client, data));
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
getGuildWebhooks(guild) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('get', Constants.Endpoints.guildWebhooks(guild.id), true)
|
||||
.then(data => {
|
||||
const hooks = new Collection();
|
||||
for (const hook of data) {
|
||||
hooks.set(hook.id, new Webhook(this.rest.client, hook));
|
||||
}
|
||||
resolve(hooks);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
getChannelWebhooks(channel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('get', Constants.Endpoints.channelWebhooks(channel.id), true)
|
||||
.then(data => {
|
||||
const hooks = new Collection();
|
||||
for (const hook of data) {
|
||||
hooks.set(hook.id, new Webhook(this.rest.client, hook));
|
||||
}
|
||||
resolve(hooks);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
createWebhook(channel, name, avatar) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('post', Constants.Endpoints.channelWebhooks(channel.id), true, {
|
||||
name,
|
||||
avatar,
|
||||
})
|
||||
.then(data => {
|
||||
resolve(new Webhook(this.rest.client, data));
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
editWebhook(webhook, name, avatar) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('patch', Constants.Endpoints.webhook(webhook.id, webhook.token), false, {
|
||||
name,
|
||||
avatar,
|
||||
}).then(data => {
|
||||
webhook.name = data.name;
|
||||
webhook.avatar = data.avatar;
|
||||
resolve(webhook);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
deleteWebhook(webhook) {
|
||||
return this.rest.makeRequest('delete', Constants.Endpoints.webhook(webhook.id, webhook.token), false);
|
||||
}
|
||||
|
||||
sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds } = {}, file = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
||||
|
||||
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
||||
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
||||
}
|
||||
|
||||
this.rest.makeRequest('post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}?wait=true`, false, {
|
||||
content: content, username: webhook.name, avatar_url: avatarURL, tts: tts, file: file, embeds: embeds,
|
||||
})
|
||||
.then(data => {
|
||||
resolve(data);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
sendSlackWebhookMessage(webhook, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest(
|
||||
'post',
|
||||
`${Constants.Endpoints.webhook(webhook.id, webhook.token)}/slack?wait=true`,
|
||||
false,
|
||||
body
|
||||
).then(data => {
|
||||
resolve(data);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
addFriend(user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('post', Constants.Endpoints.relationships('@me'), true, {
|
||||
discriminator: user.discriminator,
|
||||
username: user.username,
|
||||
}).then(() => {
|
||||
resolve(user);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
removeFriend(user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
|
||||
.then(() => {
|
||||
resolve(user);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
blockUser(user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('put', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true, { type: 2 })
|
||||
.then(() => {
|
||||
resolve(user);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
unblockUser(user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
|
||||
.then(() => {
|
||||
resolve(user);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RESTMethods;
|
||||
|
||||
@@ -59,6 +59,13 @@ class WebSocketManager extends EventEmitter {
|
||||
*/
|
||||
this.ws = null;
|
||||
|
||||
/**
|
||||
* An object with keys that are websocket event names that should be ignored
|
||||
* @type {Object}
|
||||
*/
|
||||
this.disabledEvents = {};
|
||||
for (const event in client.options.disabledEvents) this.disabledEvents[event] = true;
|
||||
|
||||
this.first = true;
|
||||
}
|
||||
|
||||
@@ -69,9 +76,7 @@ class WebSocketManager extends EventEmitter {
|
||||
_connect(gateway) {
|
||||
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
||||
this.normalReady = false;
|
||||
if (this.status !== Constants.Status.RECONNECTING) {
|
||||
this.status = Constants.Status.CONNECTING;
|
||||
}
|
||||
if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING;
|
||||
this.ws = new WebSocket(gateway);
|
||||
this.ws.onopen = () => this.eventOpen();
|
||||
this.ws.onclose = (d) => this.eventClose(d);
|
||||
@@ -216,7 +221,7 @@ class WebSocketManager extends EventEmitter {
|
||||
|
||||
this.client.emit('raw', packet);
|
||||
|
||||
if (packet.op === 10) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
|
||||
if (packet.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
|
||||
return this.packetManager.handle(packet);
|
||||
}
|
||||
|
||||
@@ -258,9 +263,10 @@ class WebSocketManager extends EventEmitter {
|
||||
if (unavailableCount === 0) {
|
||||
this.status = Constants.Status.NEARLY;
|
||||
if (this.client.options.fetchAllMembers) {
|
||||
const promises = this.client.guilds.array().map(g => g.fetchMembers());
|
||||
const promises = this.client.guilds.map(g => g.fetchMembers());
|
||||
Promise.all(promises).then(() => this._emitReady()).catch(e => {
|
||||
this.client.emit(Constants.Event.WARN, `Error on pre-ready guild member fetching - ${e}`);
|
||||
this.client.emit(Constants.Events.WARN, 'Error in pre-ready guild member fetching');
|
||||
this.client.emit(Constants.Events.ERROR, e);
|
||||
this._emitReady();
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -42,6 +42,8 @@ class WebSocketPacketManager {
|
||||
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
|
||||
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
|
||||
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
|
||||
this.register(Constants.WSEvents.RELATIONSHIP_ADD, 'RelationshipAdd');
|
||||
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, 'RelationshipRemove');
|
||||
}
|
||||
|
||||
get client() {
|
||||
@@ -77,6 +79,8 @@ class WebSocketPacketManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) this.ws.client.emit('debug', 'Heartbeat acknowledged');
|
||||
|
||||
if (this.ws.status === Constants.Status.RECONNECTING) {
|
||||
this.ws.reconnecting = false;
|
||||
this.ws.checkIfReady();
|
||||
@@ -84,6 +88,8 @@ class WebSocketPacketManager {
|
||||
|
||||
this.setSequence(packet.s);
|
||||
|
||||
if (this.ws.disabledEvents[packet.t] !== undefined) return false;
|
||||
|
||||
if (this.ws.status !== Constants.Status.READY) {
|
||||
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
|
||||
this.queue.push(packet);
|
||||
|
||||
@@ -10,12 +10,21 @@ class ReadyHandler extends AbstractHandler {
|
||||
|
||||
const clientUser = new ClientUser(client, data.user);
|
||||
client.user = clientUser;
|
||||
client.readyTime = new Date();
|
||||
client.readyAt = new Date();
|
||||
client.users.set(clientUser.id, clientUser);
|
||||
|
||||
for (const guild of data.guilds) client.dataManager.newGuild(guild);
|
||||
for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
|
||||
|
||||
for (const relation of data.relationships) {
|
||||
const user = client.dataManager.newUser(relation.user);
|
||||
if (relation.type === 1) {
|
||||
client.user.friends.set(user.id, user);
|
||||
} else if (relation.type === 2) {
|
||||
client.user.blocked.set(user.id, user);
|
||||
}
|
||||
}
|
||||
|
||||
data.presences = data.presences || [];
|
||||
for (const presence of data.presences) {
|
||||
client.dataManager.newUser(presence.user);
|
||||
|
||||
19
src/client/websocket/packets/handlers/RelationshipAdd.js
Normal file
19
src/client/websocket/packets/handlers/RelationshipAdd.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const AbstractHandler = require('./AbstractHandler');
|
||||
|
||||
class RelationshipAddHandler extends AbstractHandler {
|
||||
handle(packet) {
|
||||
const client = this.packetManager.client;
|
||||
const data = packet.d;
|
||||
if (data.type === 1) {
|
||||
client.fetchUser(data.id).then(user => {
|
||||
client.user.friends.set(user.id, user);
|
||||
});
|
||||
} else if (data.type === 2) {
|
||||
client.fetchUser(data.id).then(user => {
|
||||
client.user.blocked.set(user.id, user);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RelationshipAddHandler;
|
||||
19
src/client/websocket/packets/handlers/RelationshipRemove.js
Normal file
19
src/client/websocket/packets/handlers/RelationshipRemove.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const AbstractHandler = require('./AbstractHandler');
|
||||
|
||||
class RelationshipRemoveHandler extends AbstractHandler {
|
||||
handle(packet) {
|
||||
const client = this.packetManager.client;
|
||||
const data = packet.d;
|
||||
if (data.type === 2) {
|
||||
if (client.user.blocked.has(data.id)) {
|
||||
client.user.blocked.delete(data.id);
|
||||
}
|
||||
} else if (data.type === 1) {
|
||||
if (client.user.friends.has(data.id)) {
|
||||
client.user.friends.delete(data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RelationshipRemoveHandler;
|
||||
@@ -1,9 +1,14 @@
|
||||
module.exports = {
|
||||
Client: require('./client/Client'),
|
||||
WebhookClient: require('./client/WebhookClient'),
|
||||
Shard: require('./sharding/Shard'),
|
||||
ShardClientUtil: require('./sharding/ShardClientUtil'),
|
||||
ShardingManager: require('./sharding/ShardingManager'),
|
||||
|
||||
Collection: require('./util/Collection'),
|
||||
splitMessage: require('./util/SplitMessage'),
|
||||
escapeMarkdown: require('./util/EscapeMarkdown'),
|
||||
getRecommendedShards: require('./util/GetRecommendedShards'),
|
||||
|
||||
Channel: require('./structures/Channel'),
|
||||
ClientUser: require('./structures/ClientUser'),
|
||||
@@ -28,6 +33,7 @@ module.exports = {
|
||||
TextChannel: require('./structures/TextChannel'),
|
||||
User: require('./structures/User'),
|
||||
VoiceChannel: require('./structures/VoiceChannel'),
|
||||
Webhook: require('./structures/Webhook'),
|
||||
|
||||
version: require('../package').version,
|
||||
};
|
||||
|
||||
@@ -10,8 +10,9 @@ class Shard {
|
||||
/**
|
||||
* @param {ShardingManager} manager The sharding manager
|
||||
* @param {number} id The ID of this shard
|
||||
* @param {array} [args=[]] Command line arguments to pass to the script
|
||||
*/
|
||||
constructor(manager, id) {
|
||||
constructor(manager, id, args = []) {
|
||||
/**
|
||||
* Manager that created the shard
|
||||
* @type {ShardingManager}
|
||||
@@ -24,15 +25,22 @@ class Shard {
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The environment variables for the shard
|
||||
* @type {Object}
|
||||
*/
|
||||
this.env = {
|
||||
SHARD_ID: this.id,
|
||||
SHARD_COUNT: this.manager.totalShards,
|
||||
};
|
||||
if (this.manager.token) this.env.CLIENT_TOKEN = this.manager.token;
|
||||
|
||||
/**
|
||||
* Process of the shard
|
||||
* @type {ChildProcess}
|
||||
*/
|
||||
this.process = childProcess.fork(path.resolve(this.manager.file), [], {
|
||||
env: {
|
||||
SHARD_ID: this.id,
|
||||
SHARD_COUNT: this.manager.totalShards,
|
||||
},
|
||||
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
|
||||
env: this.env,
|
||||
});
|
||||
this.process.on('message', this._handleMessage.bind(this));
|
||||
this.process.once('exit', () => {
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
const mergeDefault = require('../util/MergeDefault');
|
||||
const Shard = require('./Shard');
|
||||
const Collection = require('../util/Collection');
|
||||
const getRecommendedShards = require('../util/GetRecommendedShards');
|
||||
|
||||
/**
|
||||
* This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate
|
||||
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
|
||||
* If you do not select an amount of shards, the manager will automatically decide the best amount.
|
||||
* <warn>The Sharding Manager is still experimental</warn>
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class ShardingManager extends EventEmitter {
|
||||
/**
|
||||
* @param {string} file Path to your shard script file
|
||||
* @param {number} [totalShards=1] Number of shards to default to spawning
|
||||
* @param {boolean} [respawn=true] Respawn a shard when it dies
|
||||
* @param {Object} [options] Options for the sharding manager
|
||||
* @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto"
|
||||
* @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting
|
||||
* @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning
|
||||
* @param {string} [options.token] Token to use for automatic shard count and passing to shards
|
||||
*/
|
||||
constructor(file, totalShards = 1, respawn = true) {
|
||||
constructor(file, options = {}) {
|
||||
super();
|
||||
options = mergeDefault({
|
||||
totalShards: 'auto',
|
||||
respawn: true,
|
||||
shardArgs: [],
|
||||
token: null,
|
||||
}, options);
|
||||
|
||||
/**
|
||||
* Path to the shard script file
|
||||
@@ -32,20 +43,36 @@ class ShardingManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Amount of shards that this manager is going to spawn
|
||||
* @type {number}
|
||||
* @type {number|string}
|
||||
*/
|
||||
this.totalShards = totalShards;
|
||||
if (typeof totalShards !== 'number' || isNaN(totalShards)) {
|
||||
throw new TypeError('Amount of shards must be a number.');
|
||||
this.totalShards = options.totalShards;
|
||||
if (this.totalShards !== 'auto') {
|
||||
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
|
||||
throw new TypeError('Amount of shards must be a number.');
|
||||
}
|
||||
if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||
if (this.totalShards !== Math.floor(this.totalShards)) {
|
||||
throw new RangeError('Amount of shards must be an integer.');
|
||||
}
|
||||
}
|
||||
if (totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||
if (totalShards !== Math.floor(totalShards)) throw new RangeError('Amount of shards must be an integer.');
|
||||
|
||||
/**
|
||||
* Whether shards should automatically respawn upon exiting
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.respawn = respawn;
|
||||
this.respawn = options.respawn;
|
||||
|
||||
/**
|
||||
* An array of arguments to pass to shards.
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.shardArgs = options.shardArgs;
|
||||
|
||||
/**
|
||||
* Token to use for obtaining the automatic shard count, and passing to shards
|
||||
* @type {?string}
|
||||
*/
|
||||
this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : null;
|
||||
|
||||
/**
|
||||
* A collection of shards that this manager has spawned
|
||||
@@ -60,7 +87,7 @@ class ShardingManager extends EventEmitter {
|
||||
* @returns {Promise<Shard>}
|
||||
*/
|
||||
createShard(id = this.shards.size) {
|
||||
const shard = new Shard(this, id);
|
||||
const shard = new Shard(this, id, this.shardArgs);
|
||||
this.shards.set(id, shard);
|
||||
/**
|
||||
* Emitted upon launching a shard
|
||||
@@ -78,10 +105,29 @@ class ShardingManager extends EventEmitter {
|
||||
* @returns {Promise<Collection<number, Shard>>}
|
||||
*/
|
||||
spawn(amount = this.totalShards, delay = 5500) {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
||||
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||
if (amount !== Math.floor(amount)) throw new RangeError('Amount of shards must be an integer.');
|
||||
return new Promise((resolve, reject) => {
|
||||
if (amount === 'auto') {
|
||||
getRecommendedShards(this.token).then(count => {
|
||||
this.totalShards = count;
|
||||
resolve(this._spawn(count, delay));
|
||||
}).catch(reject);
|
||||
} else {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
||||
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
|
||||
resolve(this._spawn(amount, delay));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually spawns shards, unlike that poser above >:(
|
||||
* @param {number} amount Number of shards to spawn
|
||||
* @param {number} delay How long to wait in between spawning each shard (in milliseconds)
|
||||
* @returns {Promise<Collection<number, Shard>>}
|
||||
* @private
|
||||
*/
|
||||
_spawn(amount, delay) {
|
||||
return new Promise(resolve => {
|
||||
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
|
||||
this.totalShards = amount;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const User = require('./User');
|
||||
const Collection = require('../util/Collection');
|
||||
|
||||
/**
|
||||
* Represents the logged in client's Discord User
|
||||
@@ -21,6 +22,20 @@ class ClientUser extends User {
|
||||
this.email = data.email;
|
||||
this.localPresence = {};
|
||||
this._typing = new Map();
|
||||
|
||||
/**
|
||||
* A Collection of friends for the logged in user.
|
||||
* <warn>This is only filled for user accounts, not bot accounts!</warn>
|
||||
* @type {Collection<string, User>}
|
||||
*/
|
||||
this.friends = new Collection();
|
||||
|
||||
/**
|
||||
* A Collection of blocked users for the logged in user.
|
||||
* <warn>This is only filled for user accounts, not bot accounts!</warn>
|
||||
* @type {Collection<string, User>}
|
||||
*/
|
||||
this.blocked = new Collection();
|
||||
}
|
||||
|
||||
edit(data) {
|
||||
@@ -118,6 +133,28 @@ class ClientUser extends User {
|
||||
return this.setPresence({ afk });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a friend request
|
||||
* <warn>This is only available for user accounts, not bot accounts!</warn>
|
||||
* @param {UserResolvable} user The user to send the friend request to.
|
||||
* @returns {Promise<User>} The user the friend request was sent to.
|
||||
*/
|
||||
addFriend(user) {
|
||||
user = this.client.resolver.resolveUser(user);
|
||||
return this.client.rest.methods.addFriend(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a friend
|
||||
* <warn>This is only available for user accounts, not bot accounts!</warn>
|
||||
* @param {UserResolvable} user The user to remove from your friends
|
||||
* @returns {Promise<User>} The user that was removed
|
||||
*/
|
||||
removeFriend(user) {
|
||||
user = this.client.resolver.resolveUser(user);
|
||||
return this.client.rest.methods.removeFriend(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the full presence of the current user.
|
||||
* @param {Object} data the data to provide
|
||||
@@ -125,12 +162,12 @@ class ClientUser extends User {
|
||||
*/
|
||||
setPresence(data) {
|
||||
// {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}}
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(resolve => {
|
||||
let status = this.localPresence.status || this.presence.status;
|
||||
let game = this.localPresence.game;
|
||||
let afk = this.localPresence.afk || this.presence.afk;
|
||||
|
||||
if (!game) {
|
||||
if (!game && this.presence.game) {
|
||||
game = {
|
||||
name: this.presence.game.name,
|
||||
type: this.presence.game.type,
|
||||
@@ -139,10 +176,7 @@ class ClientUser extends User {
|
||||
}
|
||||
|
||||
if (data.status) {
|
||||
if (typeof data.status !== 'string') {
|
||||
reject(new TypeError('status must be a string'));
|
||||
return;
|
||||
}
|
||||
if (typeof data.status !== 'string') throw new TypeError('Status must be a string');
|
||||
status = data.status;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class Emoji {
|
||||
* @returns {string}
|
||||
* @example
|
||||
* // send an emoji:
|
||||
* const emoji = guild.emojis.array()[0];
|
||||
* const emoji = guild.emojis.first();
|
||||
* msg.reply(`Hello! ${emoji}`);
|
||||
*/
|
||||
toString() {
|
||||
|
||||
@@ -50,7 +50,7 @@ class EvaluatedPermissions {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPermissions(permissions, explicit = false) {
|
||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
||||
return permissions.every(p => this.hasPermission(p, explicit));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,8 +101,8 @@ class GroupDMChannel extends Channel {
|
||||
this.ownerID === channel.ownerID;
|
||||
|
||||
if (equal) {
|
||||
const thisIDs = this.recipients.array().map(r => r.id);
|
||||
const otherIDs = channel.recipients.map(r => r.id);
|
||||
const thisIDs = this.recipients.keyArray();
|
||||
const otherIDs = channel.recipients.keyArray();
|
||||
return arraysEqual(thisIDs, otherIDs);
|
||||
}
|
||||
|
||||
|
||||
@@ -296,6 +296,14 @@ class Guild {
|
||||
return this.client.rest.methods.getGuildInvites(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all webhooks for the guild.
|
||||
* @returns {Collection<Webhook>}
|
||||
*/
|
||||
fetchWebhooks() {
|
||||
return this.client.rest.methods.getGuildWebhooks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single guild member from a user.
|
||||
* @param {UserResolvable} user The user to fetch the member for
|
||||
|
||||
@@ -111,7 +111,7 @@ class GuildChannel extends Channel {
|
||||
|
||||
/**
|
||||
* Overwrites the permissions for a user or role in this channel.
|
||||
* @param {Role|UserResolvable} userOrRole The user or role to update
|
||||
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
|
||||
* @param {PermissionOverwriteOptions} options The configuration for the update
|
||||
* @returns {Promise}
|
||||
* @example
|
||||
@@ -130,6 +130,9 @@ class GuildChannel extends Channel {
|
||||
|
||||
if (userOrRole instanceof Role) {
|
||||
payload.type = 'role';
|
||||
} else if (this.guild.roles.has(userOrRole)) {
|
||||
userOrRole = this.guild.roles.get(userOrRole);
|
||||
payload.type = 'role';
|
||||
} else {
|
||||
userOrRole = this.client.resolver.resolveUser(userOrRole);
|
||||
payload.type = 'member';
|
||||
@@ -236,12 +239,12 @@ class GuildChannel extends Channel {
|
||||
this.name === channel.name;
|
||||
|
||||
if (equal) {
|
||||
if (channel.permission_overwrites) {
|
||||
const thisIDSet = Array.from(this.permissionOverwrites.keys());
|
||||
const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id);
|
||||
if (this.permissionOverwrites && channel.permissionOverwrites) {
|
||||
const thisIDSet = this.permissionOverwrites.keyArray();
|
||||
const otherIDSet = channel.permissionOverwrites.keyArray();
|
||||
equal = arraysEqual(thisIDSet, otherIDSet);
|
||||
} else {
|
||||
equal = false;
|
||||
equal = !this.permissionOverwrites && !channel.permissionOverwrites;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ class GuildMember {
|
||||
*/
|
||||
hasPermissions(permissions, explicit = false) {
|
||||
if (!explicit && this.user.id === this.guild.ownerID) return true;
|
||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
||||
return permissions.every(p => this.hasPermission(p, explicit));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,7 +407,7 @@ class GuildMember {
|
||||
* console.log(`Hello from ${member}!`);
|
||||
*/
|
||||
toString() {
|
||||
return String(this.user);
|
||||
return `<@${this.nickname ? '!' : ''}${this.user.id}>`;
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||
|
||||
@@ -2,6 +2,7 @@ const Attachment = require('./MessageAttachment');
|
||||
const Embed = require('./MessageEmbed');
|
||||
const Collection = require('../util/Collection');
|
||||
const Constants = require('../util/Constants');
|
||||
const escapeMarkdown = require('../util/EscapeMarkdown');
|
||||
|
||||
/**
|
||||
* Represents a Message on Discord
|
||||
@@ -31,6 +32,12 @@ class Message {
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The type of the message
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = Constants.MessageTypes[data.type];
|
||||
|
||||
/**
|
||||
* The content of the message
|
||||
* @type {string}
|
||||
@@ -332,8 +339,8 @@ class Message {
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
editCode(lang, content) {
|
||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
||||
return this.edit(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``);
|
||||
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||
return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,7 @@ class MessageAttachment {
|
||||
* The Proxy URL to this attachment
|
||||
* @type {string}
|
||||
*/
|
||||
this.proxyURL = data.url;
|
||||
this.proxyURL = data.proxy_url;
|
||||
|
||||
/**
|
||||
* The height of this attachment (if an image)
|
||||
|
||||
@@ -24,6 +24,7 @@ class MessageCollector extends EventEmitter {
|
||||
* @typedef {Object} CollectorOptions
|
||||
* @property {number} [time] Duration for the collector in milliseconds
|
||||
* @property {number} [max] Maximum number of messages to handle
|
||||
* @property {number} [maxMatches] Maximum number of successfully filtered messages to obtain
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -86,7 +87,8 @@ class MessageCollector extends EventEmitter {
|
||||
* @event MessageCollector#message
|
||||
*/
|
||||
this.emit('message', message, this);
|
||||
if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
|
||||
if (this.collected.size >= this.options.maxMatches) this.stop('matchesLimit');
|
||||
else if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -100,9 +102,14 @@ class MessageCollector extends EventEmitter {
|
||||
*/
|
||||
get next() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.ended) {
|
||||
reject(this.collected);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.removeListener(onMessage);
|
||||
this.removeListener(onEnd);
|
||||
this.removeListener('message', onMessage);
|
||||
this.removeListener('end', onEnd);
|
||||
};
|
||||
|
||||
const onMessage = (...args) => {
|
||||
|
||||
@@ -150,7 +150,7 @@ class Role {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPermissions(permissions, explicit = false) {
|
||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
||||
return permissions.every(p => this.hasPermission(p, explicit));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,38 @@ class TextChannel extends GuildChannel {
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all webhooks for the channel.
|
||||
* @returns {Promise<Collection<string, Webhook>>}
|
||||
*/
|
||||
fetchWebhooks() {
|
||||
return this.client.rest.methods.getChannelWebhooks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a webhook for the channel.
|
||||
* @param {string} name The name of the webhook.
|
||||
* @param {FileResolvable} avatar The avatar for the webhook.
|
||||
* @returns {Promise<Webhook>} webhook The created webhook.
|
||||
* @example
|
||||
* channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png')
|
||||
* .then(webhook => console.log(`Created Webhook ${webhook}`))
|
||||
* .catch(console.log)
|
||||
*/
|
||||
createWebhook(name, avatar) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (avatar) {
|
||||
this.client.resolver.resolveFile(avatar).then(file => {
|
||||
let base64 = new Buffer(file, 'binary').toString('base64');
|
||||
let dataURI = `data:;base64,${base64}`;
|
||||
this.client.rest.methods.createWebhook(this, name, dataURI).then(resolve).catch(reject);
|
||||
}).catch(reject);
|
||||
} else {
|
||||
this.client.rest.methods.createWebhook(this, name).then(resolve).catch(reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||
sendMessage() { return; }
|
||||
sendTTSMessage() { return; }
|
||||
|
||||
@@ -135,6 +135,38 @@ class User {
|
||||
return this.client.rest.methods.deleteChannel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a friend request to the user
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
addFriend() {
|
||||
return this.client.rest.methods.addFriend(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the user from your friends
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
removeFriend() {
|
||||
return this.client.rest.methods.removeFriend(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks the user
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
block() {
|
||||
return this.client.rest.methods.blockUser(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unblocks the user
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
unblock() {
|
||||
return this.client.rest.methods.unblockUser(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
|
||||
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
|
||||
|
||||
@@ -45,6 +45,22 @@ class VoiceChannel extends GuildChannel {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the client has permission join the voice channel
|
||||
* @type {boolean}
|
||||
*/
|
||||
get joinable() {
|
||||
return this.permissionsFor(this.client.user).hasPermission('CONNECT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the client has permission to send audio to the voice channel
|
||||
* @type {boolean}
|
||||
*/
|
||||
get speakable() {
|
||||
return this.permissionsFor(this.client.user).hasPermission('SPEAK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bitrate of the channel
|
||||
* @param {number} bitrate The new bitrate
|
||||
|
||||
206
src/structures/Webhook.js
Normal file
206
src/structures/Webhook.js
Normal file
@@ -0,0 +1,206 @@
|
||||
const path = require('path');
|
||||
const escapeMarkdown = require('../util/EscapeMarkdown');
|
||||
|
||||
/**
|
||||
* Represents a Webhook
|
||||
*/
|
||||
class Webhook {
|
||||
constructor(client, dataOrID, token) {
|
||||
if (client) {
|
||||
/**
|
||||
* The client that instantiated the Channel
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
|
||||
if (dataOrID) this.setup(dataOrID);
|
||||
} else {
|
||||
this.id = dataOrID;
|
||||
this.token = token;
|
||||
this.client = this;
|
||||
}
|
||||
}
|
||||
|
||||
setup(data) {
|
||||
/**
|
||||
* The name of the Webhook
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
|
||||
/**
|
||||
* The token for the Webhook
|
||||
* @type {string}
|
||||
*/
|
||||
this.token = data.token;
|
||||
|
||||
/**
|
||||
* The avatar for the Webhook
|
||||
* @type {string}
|
||||
*/
|
||||
this.avatar = data.avatar;
|
||||
|
||||
/**
|
||||
* The ID of the Webhook
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The guild the Webhook belongs to
|
||||
* @type {string}
|
||||
*/
|
||||
this.guildID = data.guild_id;
|
||||
|
||||
/**
|
||||
* The channel the Webhook belongs to
|
||||
* @type {string}
|
||||
*/
|
||||
this.channelID = data.channel_id;
|
||||
|
||||
/**
|
||||
* The owner of the Webhook
|
||||
* @type {User}
|
||||
*/
|
||||
if (data.user) this.owner = data.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be passed into sendMessage, sendTTSMessage, sendFile, sendCode
|
||||
* @typedef {Object} WebhookMessageOptions
|
||||
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
||||
* @property {boolean} [disableEveryone=this.options.disableEveryone] Whether or not @everyone and @here
|
||||
* should be replaced with plain-text
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a message with this webhook
|
||||
* @param {StringResolvable} content The content to send.
|
||||
* @param {WebhookMessageOptions} [options={}] The options to provide.
|
||||
* @returns {Promise<Message|Message[]>}
|
||||
* @example
|
||||
* // send a message
|
||||
* webhook.sendMessage('hello!')
|
||||
* .then(message => console.log(`Sent message: ${message.content}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
sendMessage(content, options = {}) {
|
||||
return this.client.rest.methods.sendWebhookMessage(this, content, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a raw slack message with this webhook
|
||||
* @param {Object} body The raw body to send.
|
||||
* @returns {Promise}
|
||||
* @example
|
||||
* // send a slack message
|
||||
* webhook.sendSlackMessage({
|
||||
* 'username': 'Wumpus',
|
||||
* 'attachments': [{
|
||||
* 'pretext': 'this looks pretty cool',
|
||||
* 'color': '#F0F',
|
||||
* 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png',
|
||||
* 'footer': 'Powered by sneks',
|
||||
* 'ts': new Date().getTime() / 1000
|
||||
* }]
|
||||
* }).catch(console.log);
|
||||
*/
|
||||
sendSlackMessage(body) {
|
||||
return this.client.rest.methods.sendSlackWebhookMessage(this, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a text-to-speech message with this webhook
|
||||
* @param {StringResolvable} content The content to send
|
||||
* @param {WebhookMessageOptions} [options={}] The options to provide
|
||||
* @returns {Promise<Message|Message[]>}
|
||||
* @example
|
||||
* // send a TTS message
|
||||
* webhook.sendTTSMessage('hello!')
|
||||
* .then(message => console.log(`Sent tts message: ${message.content}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
sendTTSMessage(content, options = {}) {
|
||||
Object.assign(options, { tts: true });
|
||||
return this.client.rest.methods.sendWebhookMessage(this, content, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a file with this webhook
|
||||
* @param {FileResolvable} attachment The file to send
|
||||
* @param {string} [fileName="file.jpg"] The name and extension of the file
|
||||
* @param {StringResolvable} [content] Text message to send with the attachment
|
||||
* @param {WebhookMessageOptions} [options] The options to provide
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
sendFile(attachment, fileName, content, options = {}) {
|
||||
if (!fileName) {
|
||||
if (typeof attachment === 'string') {
|
||||
fileName = path.basename(attachment);
|
||||
} else if (attachment && attachment.path) {
|
||||
fileName = path.basename(attachment.path);
|
||||
} else {
|
||||
fileName = 'file.jpg';
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.resolver.resolveFile(attachment).then(file => {
|
||||
this.client.rest.methods.sendWebhookMessage(this, content, options, {
|
||||
file,
|
||||
name: fileName,
|
||||
}).then(resolve).catch(reject);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a code block with this webhook
|
||||
* @param {string} lang Language for the code block
|
||||
* @param {StringResolvable} content Content of the code block
|
||||
* @param {WebhookMessageOptions} options The options to provide
|
||||
* @returns {Promise<Message|Message[]>}
|
||||
*/
|
||||
sendCode(lang, content, options = {}) {
|
||||
if (options.split) {
|
||||
if (typeof options.split !== 'object') options.split = {};
|
||||
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`;
|
||||
if (!options.split.append) options.split.append = '\n```';
|
||||
}
|
||||
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||
return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the Webhook.
|
||||
* @param {string} name The new name for the Webhook
|
||||
* @param {FileResolvable} avatar The new avatar for the Webhook.
|
||||
* @returns {Promise<Webhook>}
|
||||
*/
|
||||
edit(name, avatar) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (avatar) {
|
||||
this.client.resolver.resolveFile(avatar).then(file => {
|
||||
let base64 = new Buffer(file, 'binary').toString('base64');
|
||||
let dataURI = `data:;base64,${base64}`;
|
||||
this.client.rest.methods.editWebhook(this, name, dataURI)
|
||||
.then(resolve).catch(reject);
|
||||
}).catch(reject);
|
||||
} else {
|
||||
this.client.rest.methods.editWebhook(this, name)
|
||||
.then(data => {
|
||||
this.setup(data);
|
||||
}).catch(reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the Webhook
|
||||
* @returns {Promise}
|
||||
*/
|
||||
delete() {
|
||||
return this.client.rest.methods.deleteWebhook(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Webhook;
|
||||
@@ -2,6 +2,7 @@ const path = require('path');
|
||||
const Message = require('../Message');
|
||||
const MessageCollector = require('../MessageCollector');
|
||||
const Collection = require('../../util/Collection');
|
||||
const escapeMarkdown = require('../../util/EscapeMarkdown');
|
||||
|
||||
/**
|
||||
* Interface for classes that have text-channel-like features
|
||||
@@ -111,11 +112,11 @@ class TextBasedChannel {
|
||||
sendCode(lang, content, options = {}) {
|
||||
if (options.split) {
|
||||
if (typeof options.split !== 'object') options.split = {};
|
||||
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang ? lang : ''}\n`;
|
||||
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`;
|
||||
if (!options.split.append) options.split.append = '\n```';
|
||||
}
|
||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
||||
return this.sendMessage(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``, options);
|
||||
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||
return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,17 +316,17 @@ class TextBasedChannel {
|
||||
* @returns {Collection<string, Message>}
|
||||
*/
|
||||
bulkDelete(messages) {
|
||||
if (messages instanceof Collection) messages = messages.array();
|
||||
if (!(messages instanceof Array)) return Promise.reject(new TypeError('Messages must be an Array or Collection.'));
|
||||
const messageIDs = messages.map(m => m.id);
|
||||
if (!(messages instanceof Array || messages instanceof Collection)) {
|
||||
return Promise.reject(new TypeError('Messages must be an Array or Collection.'));
|
||||
}
|
||||
const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id);
|
||||
return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
|
||||
}
|
||||
|
||||
_cacheMessage(message) {
|
||||
const maxSize = this.client.options.maxMessageCache;
|
||||
const maxSize = this.client.options.messageCacheMaxSize;
|
||||
if (maxSize === 0) return null;
|
||||
if (this.messages.size >= maxSize) this.messages.delete(this.messages.keys().next().value);
|
||||
|
||||
if (this.messages.size >= maxSize && maxSize > 0) this.messages.delete(this.messages.firstKey());
|
||||
this.messages.set(message.id, message);
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -3,26 +3,48 @@
|
||||
* @extends {Map}
|
||||
*/
|
||||
class Collection extends Map {
|
||||
constructor(iterable) {
|
||||
super(iterable);
|
||||
this._array = null;
|
||||
this._keyArray = null;
|
||||
}
|
||||
|
||||
set(key, val) {
|
||||
super.set(key, val);
|
||||
this._array = null;
|
||||
this._keyArray = null;
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
super.delete(key);
|
||||
this._array = null;
|
||||
this._keyArray = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ordered array of the values of this collection.
|
||||
* Creates an ordered array of the values of this collection, and caches it internally. The array will only be
|
||||
* reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
|
||||
* @returns {Array}
|
||||
* @example
|
||||
* // identical to:
|
||||
* Array.from(collection.values());
|
||||
*/
|
||||
array() {
|
||||
return Array.from(this.values());
|
||||
if (!this._array || this._array.length !== this.size) this._array = Array.from(this.values());
|
||||
return this._array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ordered array of the keys of this collection.
|
||||
* Creates an ordered array of the keys of this collection, and caches it internally. The array will only be
|
||||
* reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
|
||||
* @returns {Array}
|
||||
* @example
|
||||
* // identical to:
|
||||
* Array.from(collection.keys());
|
||||
*/
|
||||
keyArray() {
|
||||
return Array.from(this.keys());
|
||||
if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = Array.from(this.keys());
|
||||
return this._keyArray;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,11 +205,27 @@ class Collection extends Map {
|
||||
*/
|
||||
filter(fn, thisArg) {
|
||||
if (thisArg) fn = fn.bind(thisArg);
|
||||
const collection = new Collection();
|
||||
const results = new Collection();
|
||||
for (const [key, val] of this) {
|
||||
if (fn(val, key, this)) collection.set(key, val);
|
||||
if (fn(val, key, this)) results.set(key, val);
|
||||
}
|
||||
return collection;
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to
|
||||
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
|
||||
* @param {function} fn Function used to test (should return a boolean)
|
||||
* @param {Object} [thisArg] Value to use as `this` when executing function
|
||||
* @returns {Collection}
|
||||
*/
|
||||
filterArray(fn, thisArg) {
|
||||
if (thisArg) fn = fn.bind(thisArg);
|
||||
const results = [];
|
||||
for (const [key, val] of this) {
|
||||
if (fn(val, key, this)) results.push(val);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,28 +7,33 @@ exports.Package = require('../../package.json');
|
||||
* the order they are triggered, whereas burst runs multiple at a time, and doesn't guarantee a particular order.
|
||||
* @property {number} [shardId=0] The ID of this shard
|
||||
* @property {number} [shardCount=0] The number of shards
|
||||
* @property {number} [maxMessageCache=200] Number of messages to cache per channel
|
||||
* @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel
|
||||
* (-1 for unlimited - don't do this without message sweeping, otherwise memory usage will climb indefinitely)
|
||||
* @property {number} [messageCacheLifetime=0] How long until a message should be uncached by the message sweeping
|
||||
* (in seconds, 0 for forever)
|
||||
* @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than
|
||||
* the max message lifetime (in seconds, 0 for never)
|
||||
* the message cache lifetime (in seconds, 0 for never)
|
||||
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
|
||||
* upon joining a guild
|
||||
* @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone
|
||||
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
|
||||
* corresponding websocket events
|
||||
* @property {string[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be
|
||||
* processed. Disabling useless events such as 'TYPING_START' can result in significant performance increases on
|
||||
* large-scale bots.
|
||||
* @property {WebsocketOptions} [ws] Options for the websocket
|
||||
*/
|
||||
exports.DefaultOptions = {
|
||||
apiRequestMethod: 'sequential',
|
||||
shardId: 0,
|
||||
shardCount: 0,
|
||||
maxMessageCache: 200,
|
||||
messageCacheMaxSize: 200,
|
||||
messageCacheLifetime: 0,
|
||||
messageSweepInterval: 0,
|
||||
fetchAllMembers: false,
|
||||
disableEveryone: false,
|
||||
restWsBridgeTimeout: 5000,
|
||||
disabledEvents: [],
|
||||
|
||||
/**
|
||||
* Websocket options. These are left as snake_case to match the API.
|
||||
@@ -67,6 +72,7 @@ const Endpoints = exports.Endpoints = {
|
||||
login: `${API}/auth/login`,
|
||||
logout: `${API}/auth/logout`,
|
||||
gateway: `${API}/gateway`,
|
||||
botGateway: `${API}/gateway/bot`,
|
||||
invite: (id) => `${API}/invite/${id}`,
|
||||
inviteLink: (id) => `https://discord.gg/${id}`,
|
||||
CDN: 'https://cdn.discordapp.com',
|
||||
@@ -77,6 +83,7 @@ const Endpoints = exports.Endpoints = {
|
||||
avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
|
||||
me: `${API}/users/@me`,
|
||||
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
|
||||
relationships: (userID) => `${Endpoints.user(userID)}/relationships`,
|
||||
|
||||
// guilds
|
||||
guilds: `${API}/guilds`,
|
||||
@@ -103,6 +110,10 @@ const Endpoints = exports.Endpoints = {
|
||||
channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
|
||||
channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
|
||||
channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
|
||||
channelWebhooks: (channelID) => `${Endpoints.channel(channelID)}/webhooks`,
|
||||
|
||||
// webhooks
|
||||
webhook: (webhookID, token) => `${API}/webhooks/${webhookID}${token ? `/${token}` : ''}`,
|
||||
};
|
||||
|
||||
exports.Status = {
|
||||
@@ -131,6 +142,8 @@ exports.OPCodes = {
|
||||
RECONNECT: 7,
|
||||
REQUEST_GUILD_MEMBERS: 8,
|
||||
INVALID_SESSION: 9,
|
||||
HELLO: 10,
|
||||
HEARTBEAT_ACK: 11,
|
||||
};
|
||||
|
||||
exports.VoiceOPCodes = {
|
||||
@@ -178,6 +191,7 @@ exports.Events = {
|
||||
TYPING_STOP: 'typingStop',
|
||||
DISCONNECT: 'disconnect',
|
||||
RECONNECTING: 'reconnecting',
|
||||
ERROR: 'error',
|
||||
WARN: 'warn',
|
||||
DEBUG: 'debug',
|
||||
};
|
||||
@@ -212,6 +226,18 @@ exports.WSEvents = {
|
||||
FRIEND_ADD: 'RELATIONSHIP_ADD',
|
||||
FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
|
||||
RELATIONSHIP_ADD: 'RELATIONSHIP_ADD',
|
||||
RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||
};
|
||||
|
||||
exports.MessageTypes = {
|
||||
0: 'DEFAULT',
|
||||
1: 'RECIPIENT_ADD',
|
||||
2: 'RECIPIENT_REMOVE',
|
||||
3: 'CALL',
|
||||
4: 'CHANNEL_NAME_CHANGE',
|
||||
5: 'CHANNEL_ICON_CHANGE',
|
||||
6: 'PINS_ADD',
|
||||
};
|
||||
|
||||
const PermissionFlags = exports.PermissionFlags = {
|
||||
@@ -242,7 +268,7 @@ const PermissionFlags = exports.PermissionFlags = {
|
||||
CHANGE_NICKNAME: 1 << 26,
|
||||
MANAGE_NICKNAMES: 1 << 27,
|
||||
MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
|
||||
|
||||
MANAGE_WEBHOOKS: 1 << 29,
|
||||
MANAGE_EMOJIS: 1 << 30,
|
||||
};
|
||||
|
||||
|
||||
5
src/util/EscapeMarkdown.js
Normal file
5
src/util/EscapeMarkdown.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = function escapeMarkdown(text, onlyCodeBlock = false, onlyInlineCode = false) {
|
||||
if (onlyCodeBlock) return text.replace(/```/g, '`\u200b``');
|
||||
if (onlyInlineCode) return text.replace(/\\(`|\\)/g, '$1').replace(/(`|\\)/g, '\\$1');
|
||||
return text.replace(/\\(\*|_|`|~|\\)/g, '$1').replace(/(\*|_|`|~|\\)/g, '\\$1');
|
||||
};
|
||||
19
src/util/GetRecommendedShards.js
Normal file
19
src/util/GetRecommendedShards.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const superagent = require('superagent');
|
||||
const botGateway = require('./Constants').Endpoints.botGateway;
|
||||
|
||||
/**
|
||||
* Gets the recommended shard count from Discord
|
||||
* @param {number} token Discord auth token
|
||||
* @returns {Promise<number>} the recommended number of shards
|
||||
*/
|
||||
module.exports = function getRecommendedShards(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!token) throw new Error('A token must be provided.');
|
||||
superagent.get(botGateway)
|
||||
.set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`)
|
||||
.end((err, res) => {
|
||||
if (err) reject(err);
|
||||
resolve(res.body.shards);
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user