adds new WebhookClient and allows you to fetch channel webhooks and such without being "over the top" (#768)

* start blocking out client

* proto webhookclient

* wee working webhooks

* it's all working

* run docs

* fix jsdoc issues

* add example for webhookClient

* add example in the examples place

* fix docs
This commit is contained in:
Gus Caplan
2016-10-07 13:09:41 -05:00
committed by Amish Shah
parent f9b7f9c27e
commit 1c4ed4547f
11 changed files with 399 additions and 2 deletions

View File

@@ -0,0 +1,12 @@
/*
Send a message using a webhook
*/
// import the discord.js module
const Discord = require('discord.js');
// create a new webhook
const hook = new Discord.WebhookClient('webhook id', 'webhook token');
// send a message using the webhook
hook.sendMessage('I am now alive!');

10
docs/custom/webhook.js Normal file
View File

@@ -0,0 +1,10 @@
const fs = require('fs');
module.exports = {
category: 'Examples',
name: 'Webhooks',
data:
`\`\`\`js
${fs.readFileSync('./docs/custom/examples/webhook.js').toString('utf-8')}
\`\`\``,
};

File diff suppressed because one or more lines are too long

View 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;

View File

@@ -7,6 +7,7 @@ const User = requireStructure('User');
const GuildMember = requireStructure('GuildMember');
const Role = requireStructure('Role');
const Invite = requireStructure('Invite');
const Webhook = requireStructure('Webhook');
class RESTMethods {
constructor(restManager) {
@@ -537,6 +538,87 @@ class RESTMethods {
}).catch(reject);
});
}
fetchGuildWebhooks(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);
});
}
fetchChannelWebhooks(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);
});
}
fetchWebhook(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);
});
}
createChannelWebhook(channel, name, avatar) {
return new Promise((resolve, reject) => {
this.rest.makeRequest('post', Constants.Endpoints.channelWebhooks(channel.id), true, {
name: name, avatar: avatar,
})
.then(data => {
resolve(new Webhook(this.rest.client, data));
}).catch(reject);
});
}
deleteChannelWebhook(webhook) {
return new Promise((resolve, reject) => {
this.rest.makeRequest('delete', Constants.Endpoints.webhook(webhook.id, webhook.token), false)
.then(resolve).catch(reject);
});
}
editChannelWebhook(webhook, name, avatar) {
return new Promise((resolve, reject) => {
this.rest.makeRequest('patch', Constants.Endpoints.webhook(webhook.id, webhook.token), false, {
name: name, avatar: avatar,
})
.then(data => {
resolve(data);
}).catch(reject);
});
}
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);
});
}
}
module.exports = RESTMethods;

View File

@@ -1,5 +1,6 @@
module.exports = {
Client: require('./client/Client'),
WebhookClient: require('./client/WebhookClient'),
Shard: require('./sharding/Shard'),
ShardClientUtil: require('./sharding/ShardClientUtil'),
ShardingManager: require('./sharding/ShardingManager'),
@@ -28,6 +29,7 @@ module.exports = {
TextChannel: require('./structures/TextChannel'),
User: require('./structures/User'),
VoiceChannel: require('./structures/VoiceChannel'),
Webhook: require('./structures/Webhook'),
version: require('../package').version,
};

View File

@@ -622,6 +622,14 @@ class Guild {
return this.client.rest.methods.deleteGuild(this);
}
/**
* Fetch all webhooks for the guild.
* @returns {Collection<Webhook>}
*/
fetchWebhooks() {
return this.client.rest.methods.fetchGuildWebhooks(this);
}
/**
* Whether this Guild equals another Guild. It compares all properties, so for most operations
* it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often

View File

@@ -57,6 +57,9 @@ class TextChannel extends GuildChannel {
createCollector() { return; }
awaitMessages() { return; }
bulkDelete() { return; }
fetchWebhook() { return; }
fetchWebhooks() { return; }
createWebhook() { return; }
_cacheMessage() { return; }
}

184
src/structures/Webhook.js Normal file
View File

@@ -0,0 +1,184 @@
const path = require('path');
/**
* 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.guild_id = data.guild_id;
/**
* The channel the Webhook belongs to
* @type {string}
*/
this.channel_id = 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} MessageOptions
* @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 {MessageOptions} [options={}] The options to provide
* @returns {Promise<Message|Message[]>}
* @example
* // send a message
* webook.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 text-to-speech message with this webhook
* @param {StringResolvable} content The content to send
* @param {MessageOptions} [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 {MessageOptions} [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 {MessageOptions} 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 ? 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);
}
/**
* Delete the Webhook
* @returns {Promise}
*/
delete() {
return this.client.rest.methods.deleteChannelWebhook(this);
}
/**
* 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.editChannelWebhook(this, name, dataURI)
.then(resolve).catch(reject);
}).catch(reject);
} else {
this.client.rest.methods.editChannelWebhook(this, name)
.then(data => {
this.setup(data);
}).catch(reject);
}
});
}
}
module.exports = Webhook;

View File

@@ -329,6 +329,49 @@ class TextBasedChannel {
this.messages.set(message.id, message);
return message;
}
/**
* Fetch all webhooks for the channel.
* @returns {Collection<Webhook>}
*/
fetchWebhooks() {
return this.client.rest.methods.fetchChannelWebhooks(this);
}
/**
* Fetch a webhook by ID
* @param {string} id The id of the webhook.
* @returns {Promise<Webhook>}
*/
fetchWebhook(id) {
return this.client.rest.methods.fetchWebhook(id);
}
/**
* Create a webhook for the channel.
* @param {string} name The name of the webhook.
* @param {FileResolvable=} avatar The avatar for the webhook.
* @returns {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.createChannelWebhook(this, name, dataURI)
.then(resolve).catch(reject);
}).catch(reject);
} else {
this.client.rest.methods.createChannelWebhook(this, name)
.then(resolve).catch(reject);
}
});
}
}
exports.applyToClass = (structure, full = false) => {
@@ -345,6 +388,9 @@ exports.applyToClass = (structure, full = false) => {
props.push('fetchPinnedMessages');
props.push('createCollector');
props.push('awaitMessages');
props.push('fetchWebhooks');
props.push('fetchWebhook');
props.push('createWebhook');
}
for (const prop of props) applyProp(structure, prop);
};

View File

@@ -103,6 +103,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 = {
@@ -242,7 +246,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,
};