UNPKG

discord.io

Version:

JavaScript interface for Discord.

1,422 lines (1,272 loc) 101 kB
"use strict"; (function discordio(Discord){ var isNode = typeof(window) === "undefined" && typeof(navigator) === "undefined"; var CURRENT_VERSION = "2.x.x", GATEWAY_VERSION = 5, LARGE_THRESHOLD = 250, CONNECT_WHEN = null, Endpoints, Payloads; if (isNode) { var Util = require('util'), FS = require('fs'), UDP = require('dgram'), Zlib = require('zlib'), DNS = require('dns'), Stream = require('stream'), BN = require('path').basename, EE = require('events').EventEmitter, requesters = { http: require('http'), https: require('https') }, ChildProc = require('child_process'), URL = require('url'), //NPM Modules NACL = require('tweetnacl'), Opus = null; } /* --- Version Check --- */ try { CURRENT_VERSION = require('../package.json').version; } catch(e) {} if (!isNode) CURRENT_VERSION = CURRENT_VERSION + "-browser"; /** * Discord Client constructor * @class * @arg {Object} options * @arg {String} options.token - The token of the account you wish to log in with. * @arg {Boolean} [options.autorun] - If true, the client runs when constructed without calling `.connect()`. * @arg {Number} [options.messageCacheLimit] - The amount of messages to cache in memory, per channel. Used for information on deleted/updated messages. The default is 50. * @arg {Array<Number>} [options.shard] - The shard array. The first index is the current shard ID, the second is the amount of shards that should be running. */ Discord.Client = function DiscordClient(options) { if (!isNode) Emitter.call(this); if (!options || options.constructor.name !== 'Object') return console.error("An Object is required to create the discord.io client."); applyProperties(this, [ ["_ws", null], ["_uIDToDM", {}], ["_ready", false], ["_vChannels", {}], ["_messageCache", {}], ["_connecting", false], ["_mainKeepAlive", null], ["_req", APIRequest.bind(this)], ["_shard", validateShard(options.shard)], ["_messageCacheLimit", typeof(options.messageCacheLimit) === 'number' ? options.messageCacheLimit : 50], ]); this.presenceStatus = "offline"; this.connected = false; this.inviteURL = null; this.connect = this.connect.bind(this, options); if (options.autorun === true) this.connect(); }; if (isNode) Emitter.call(Discord.Client); /* - DiscordClient - Methods - */ var DCP = Discord.Client.prototype; /** * Manually initiate the WebSocket connection to Discord. */ DCP.connect = function () { var opts = arguments[0]; if (!this.connected && !this._connecting) return setTimeout(function() { init(this, opts); CONNECT_WHEN = Math.max(CONNECT_WHEN, Date.now()) + 6000; }.bind(this), Math.max( 0, CONNECT_WHEN - Date.now() )); }; /** * Disconnect the WebSocket connection to Discord. */ DCP.disconnect = function () { if (this._ws) return this._ws.close(), log(this, "Manual disconnect called, websocket closed"); return log(this, Discord.LogLevels.Warn, "Manual disconnect called with no WebSocket active, ignored"); }; /** * Retrieve a user object from Discord, Bot only endpoint. You don't have to share a server with this user. * @arg {Object} input * @arg {Snowflake} input.userID */ DCP.getUser = function(input, callback) { if (!this.bot) return handleErrCB("[getUser] This account is a 'user' type account, and cannot use 'getUser'. Only bots can use this endpoint.", callback); this._req('get', Endpoints.USER(input.userID), function(err, res) { handleResCB("Could not get user", err, res, callback); }); }; /** * Edit the client's user information. * @arg {Object} input * @arg {String<Base64>} input.avatar - The last part of a Base64 Data URI. `fs.readFileSync('image.jpg', 'base64')` is enough. * @arg {String} input.username - A username. * @arg {String} input.email - [User only] An email. * @arg {String} input.password - [User only] Your current password. * @arg {String} input.new_password - [User only] A new password. */ DCP.editUserInfo = function(input, callback) { var payload = { avatar: this.avatar, email: this.email, new_password: null, password: null, username: this.username }, plArr = Object.keys(payload); for (var key in input) { if (plArr.indexOf(key) < 0) return handleErrCB(("[editUserInfo] '" + key + "' is not a valid key. Valid keys are: " + plArr.join(", ")), callback); payload[key] = input[key]; } if (input.avatar) payload.avatar = "data:image/jpg;base64," + input.avatar; this._req('patch', Endpoints.ME, payload, function(err, res) { handleResCB("Unable to edit user information", err, res, callback); }); }; /** * Change the client's presence. * @arg {Object} input * @arg {String|null} input.status - Used to set the status. online, idle, dnd, invisible, and offline are the possible states. * @arg {Number|null} input.idle_since - Optional, use a Number before the current point in time. * @arg {Boolean|null} input.afk - Optional, changes how Discord handles push notifications. * @arg {Object|null} input.game - Used to set game information. * @arg {String|null} input.game.name - The name of the game. * @arg {Number|null} input.game.type - Activity type, 0 for game, 1 for Twitch. * @arg {String|null} input.game.url - A URL matching the streaming service you've selected. */ DCP.setPresence = function(input) { var payload = Payloads.STATUS(input); send(this._ws, payload); if (payload.d.idle_since === null) return void(this.presenceStatus = payload.d.status); this.presenceStatus = payload.d.status; }; /** * Receive OAuth information for the current client. */ DCP.getOauthInfo = function(callback) { this._req('get', Endpoints.OAUTH, function(err, res) { handleResCB("Error GETing OAuth information", err, res, callback); }); }; /** * Receive account settings information for the current client. */ DCP.getAccountSettings = function(callback) { this._req('get', Endpoints.SETTINGS, function(err, res) { handleResCB("Error GETing client settings", err, res, callback); }); }; /* - DiscordClient - Methods - Content - */ /** * Upload a file to a channel. * @arg {Object} input * @arg {Snowflake} input.to - The target Channel or User ID. * @arg {Buffer|String} input.file - A Buffer containing the file data, or a String that's a path to the file. * @arg {String|null} input.filename - A filename for the uploaded file, required if you provide a Buffer. * @arg {String|null} input.message - An optional message to provide. */ DCP.uploadFile = function(input, callback) { /* After like 15 minutes of fighting with Request, turns out Discord doesn't allow multiple files in one message... despite having an attachments array.*/ var file, client, multi, message, isBuffer, isString; client = this; multi = new Multipart(); message = generateMessage(input.message || ""); isBuffer = (input.file instanceof Buffer); isString = (type(input.file) === 'string'); if (!isBuffer && !isString) return handleErrCB("uploadFile requires a String or Buffer as the 'file' value", callback); if (isBuffer) { if (!input.filename) return handleErrCB("uploadFile requires a 'filename' value to be set if using a Buffer", callback); file = input.file; } if (isString) try { file = FS.readFileSync(input.file); } catch(e) { return handleErrCB("File does not exist: " + input.file, callback); } [ ["content", message.content], ["mentions", ""], ["tts", false], ["nonce", message.nonce], ["file", file, input.filename || BN(input.file)] ].forEach(multi.append, multi); multi.finalize(); resolveID(client, input.to, function(channelID) { client._req('post', Endpoints.MESSAGES(channelID), multi, function(err, res) { handleResCB("Unable to upload file", err, res, callback); }); }); }; /** * Send a message to a channel. * @arg {Object} input * @arg {Snowflake} input.to - The target Channel or User ID. * @arg {String} input.message - The message content. * @arg {Object} [input.embed] - An embed object to include * @arg {Boolean} [input.tts] - Enable Text-to-Speech for this message. * @arg {Number} [input.nonce] - Number-used-only-ONCE. The Discord client uses this to change the message color from grey to white. * @arg {Boolean} [input.typing] - Indicates whether the message should be sent with simulated typing. Based on message length. */ DCP.sendMessage = function(input, callback) { var message = generateMessage(input.message || '', input.embed); message.tts = (input.tts === true); message.nonce = input.nonce || message.nonce; if (input.typing === true) { return simulateTyping( this, input.to, message, ( (message.content.length * 0.12) * 1000 ), callback ); } sendMessage(this, input.to, message, callback); }; /** * Pull a message object from Discord. * @arg {Object} input * @arg {Snowflake} input.channelID - The channel ID that the message is from. * @arg {Snowflake} input.messageID - The ID of the message. */ DCP.getMessage = function(input, callback) { this._req('get', Endpoints.MESSAGES(input.channelID, input.messageID), function(err, res) { handleResCB("Unable to get message", err, res, callback); }); }; /** * Pull an array of message objects from Discord. * @arg {Object} input * @arg {Snowflake} input.channelID - The channel ID to pull the messages from. * @arg {Number} [input.limit] - How many messages to pull, defaults to 50. * @arg {Snowflake} [input.before] - Pull messages before this message ID. * @arg {Snowflake} [input.after] - Pull messages after this message ID. */ DCP.getMessages = function(input, callback) { var client = this, qs = {}, messages = [], lastMessageID = ""; var total = typeof(input.limit) !== 'number' ? 50 : input.limit; if (input.before) qs.before = input.before; if (input.after) qs.after = input.after; (function getMessages() { if (total > 100) { qs.limit = 100; total = total - 100; } else { qs.limit = total; } if (messages.length >= input.limit) return call(callback, [null, messages]); client._req('get', Endpoints.MESSAGES(input.channelID) + qstringify(qs), function(err, res) { if (err) return handleErrCB("Unable to get messages", callback); messages = messages.concat(res.body); lastMessageID = messages[messages.length - 1] && messages[messages.length - 1].id; if (lastMessageID) qs.before = lastMessageID; if (!res.body.length < qs.limit) return call(callback, [null, messages]); return setTimeout(getMessages, 1000); }); })(); }; /** * Edit a previously sent message. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID * @arg {String} input.message - The new message content * @arg {Object} [input.embed] - The new Discord Embed object */ DCP.editMessage = function(input, callback) { this._req('patch', Endpoints.MESSAGES(input.channelID, input.messageID), generateMessage(input.message || '', input.embed), function(err, res) { handleResCB("Unable to edit message", err, res, callback); }); }; /** * Delete a posted message. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID */ DCP.deleteMessage = function(input, callback) { this._req('delete', Endpoints.MESSAGES(input.channelID, input.messageID), function(err, res) { handleResCB("Unable to delete message", err, res, callback); }); }; /** * Delete a batch of messages. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Array<Snowflake>} input.messageIDs - An Array of message IDs, with a maximum of 100 indexes. */ DCP.deleteMessages = function(input, callback) { this._req('post', Endpoints.BULK_DELETE(input.channelID), {messages: input.messageIDs.slice(0, 100)}, function(err, res) { handleResCB("Unable to delete messages", err, res, callback); }); }; /** * Pin a message to the channel. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID */ DCP.pinMessage = function(input, callback) { this._req('put', Endpoints.PINNED_MESSAGES(input.channelID, input.messageID), function(err, res) { handleResCB("Unable to pin message", err, res, callback); }); }; /** * Get an array of pinned messages from a channel. * @arg {Object} input * @arg {Snowflake} input.channelID */ DCP.getPinnedMessages = function(input, callback) { this._req('get', Endpoints.PINNED_MESSAGES(input.channelID), function(err, res) { handleResCB("Unable to get pinned messages", err, res, callback); }); }; /** * Delete a pinned message from a channel. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID */ DCP.deletePinnedMessage = function(input, callback) { this._req('delete', Endpoints.PINNED_MESSAGES(input.channelID, input.messageID), function(err, res) { handleResCB("Unable to delete pinned message", err, res, callback); }); }; /** * Send 'typing...' status to a channel * @arg {Snowflake} channelID */ DCP.simulateTyping = function(channelID, callback) { this._req('post', Endpoints.TYPING(channelID), function(err, res) { handleResCB("Unable to simulate typing", err, res, callback); }); }; /** * Replace Snowflakes with the names if applicable. * @arg {String} message - The message to fix. */ DCP.fixMessage = function(message) { var client = this; return message.replace(/<@&(\d*)>|<@!(\d*)>|<@(\d*)>|<#(\d*)>/g, function(match, RID, NID, UID, CID) { var k, i; if (UID || CID) { if (client.users[UID]) return "@" + client.users[UID].username; if (client.channels[CID]) return "#" + client.channels[CID].name; } if (RID || NID) { k = Object.keys(client.servers); for (i=0; i<k.length; i++) { if (client.servers[k[i]].roles[RID]) return "@" + client.servers[k[i]].roles[RID].name; if (client.servers[k[i]].members[NID]) return "@" + client.servers[k[i]].members[NID].nick; } } }); }; /** * Add an emoji reaction to a message. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID * @arg {String} input.reaction - Either the emoji unicode or the emoji name:id/object. */ DCP.addReaction = function(input, callback) { this._req('put', Endpoints.USER_REACTIONS(input.channelID, input.messageID, stringifyEmoji(input.reaction)), function(err, res) { handleResCB("Unable to add reaction", err, res, callback); }); }; /** * Get an emoji reaction of a message. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID * @arg {String} input.reaction - Either the emoji unicode or the emoji name:id/object. * @arg {String} [input.limit] */ DCP.getReaction = function(input, callback) { var qs = { limit: (typeof(input.limit) !== 'number' ? 100 : input.limit) }; this._req('get', Endpoints.MESSAGE_REACTIONS(input.channelID, input.messageID, stringifyEmoji(input.reaction)) + qstringify(qs), function(err, res) { handleResCB("Unable to get reaction", err, res, callback); }); }; /** * Remove an emoji reaction from a message. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID * @arg {Snowflake} [input.userID] * @arg {String} input.reaction - Either the emoji unicode or the emoji name:id/object. */ DCP.removeReaction = function(input, callback) { this._req('delete', Endpoints.USER_REACTIONS(input.channelID, input.messageID, stringifyEmoji(input.reaction), input.userID), function(err, res) { handleResCB("Unable to remove reaction", err, res, callback); }); }; /** * Remove all emoji reactions from a message. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID */ DCP.removeAllReactions = function(input, callback) { this._req('delete', Endpoints.MESSAGE_REACTIONS(input.channelID, input.messageID), function(err, res) { handleResCB("Unable to remove reactions", err, res, callback); }); }; /* - DiscordClient - Methods - Server Management - */ /** * Remove a user from a server. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.kick = function(input, callback) { this._req('delete', Endpoints.MEMBERS(input.serverID, input.userID), function(err, res) { handleResCB("Could not kick user", err, res, callback); }); }; /** * Remove and ban a user from a server. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID * @arg {String} input.reason * @arg {Number} [input.lastDays] - Removes their messages up until this point, either 1 or 7 days. */ DCP.ban = function(input, callback) { var url = Endpoints.BANS(input.serverID, input.userID); var opts = {}; if (input.lastDays) { opts.lastDays = Number(input.lastDays); opts.lastDays = Math.min(opts.lastDays, 7); opts.lastDays = Math.max(opts.lastDays, 1); } if (input.reason) opts.reason = input.reason; url += qstringify(opts); this._req('put', url, function(err, res) { handleResCB("Could not ban user", err, res, callback); }); } /** * Unban a user from a server. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.unban = function(input, callback) { this._req('delete', Endpoints.BANS(input.serverID, input.userID), function(err, res) { handleResCB("Could not unban user", err, res, callback); }); }; /** * Move a user between voice channels. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID * @arg {Snowflake} input.channelID */ DCP.moveUserTo = function(input, callback) { this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {channel_id: input.channelID}, function(err, res) { handleResCB("Could not move the user", err, res, callback); }); }; /** * Server-mute the user from speaking in all voice channels. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.mute = function(input, callback) { this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {mute: true}, function(err, res) { handleResCB("Could not mute user", err, res, callback); }); }; /** * Remove the server-mute from a user. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.unmute = function(input, callback) { this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {mute: false}, function(err, res) { handleResCB("Could not unmute user", err, res, callback); }); }; /** * Server-deafen a user. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.deafen = function(input, callback) { this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {deaf: true}, function(err, res) { handleResCB("Could not deafen user", err, res, callback); }); }; /** * Remove the server-deafen from a user. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.undeafen = function(input, callback) { this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {deaf: false}, function(err, res) { handleResCB("Could not undeafen user", err, res, callback); }); }; /** * Self-mute the client from speaking in all voice channels. * @arg {Snowflake} serverID */ DCP.muteSelf = function(serverID, callback) { var server = this.servers[serverID], channelID, voiceSession; if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback); server.self_mute = true; if (!server.voiceSession) return call(callback, [null]); voiceSession = server.voiceSession; voiceSession.self_mute = true; channelID = voiceSession.channelID; if (!channelID) return call(callback, [null]); return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, true, server.self_deaf))]); }; /** * Remove the self-mute from the client. * @arg {Snowflake} serverID */ DCP.unmuteSelf = function(serverID, callback) { var server = this.servers[serverID], channelID, voiceSession; if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback); server.self_mute = false; if (!server.voiceSession) return call(callback, [null]); voiceSession = server.voiceSession; voiceSession.self_mute = false; channelID = voiceSession.channelID; if (!channelID) return call(callback, [null]); return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, false, server.self_deaf))]); }; /** * Self-deafen the client. * @arg {Snowflake} serverID */ DCP.deafenSelf = function(serverID, callback) { var server = this.servers[serverID], channelID, voiceSession; if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback); server.self_deaf = true; if (!server.voiceSession) return call(callback, [null]); voiceSession = server.voiceSession; voiceSession.self_deaf = true; channelID = voiceSession.channelID; if (!channelID) return call(callback, [null]); return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, true))]); }; /** * Remove the self-deafen from the client. * @arg {Snowflake} serverID */ DCP.undeafenSelf = function(serverID, callback) { var server = this.servers[serverID], channelID, voiceSession; if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback); server.self_deaf = false; if (!server.voiceSession) return call(callback, [null]); voiceSession = server.voiceSession; voiceSession.self_deaf = false; channelID = voiceSession.channelID; if (!channelID) return call(callback, [null]); return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, false))]); }; /*Bot server management actions*/ /** * Create a server [User only]. * @arg {Object} input * @arg {String} input.name - The server's name * @arg {String} [input.region] - The server's region code, check the Gitbook documentation for all of them. * @arg {String<Base64>} [input.icon] - The last part of a Base64 Data URI. `fs.readFileSync('image.jpg', 'base64')` is enough. */ DCP.createServer = function(input, callback) { var payload, client = this; payload = {icon: null, name: null, region: null}; for (var key in input) { if (Object.keys(payload).indexOf(key) < 0) continue; payload[key] = input[key]; } if (input.icon) payload.icon = "data:image/jpg;base64," + input.icon; client._req('post', Endpoints.SERVERS(), payload, function(err, res) { try { client.servers[res.body.id] = {}; copyKeys(res.body, client.servers[res.body.id]); } catch(e) {} handleResCB("Could not create server", err, res, callback); }); }; /** * Edit server information. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {String} [input.name] * @arg {String} [input.icon] * @arg {String} [input.region] * @arg {Snowflake} [input.afk_channel_id] - The ID of the voice channel to move a user to after the afk period. * @arg {Number} [input.afk_timeout] - Time in seconds until a user is moved to the afk channel. 60, 300, 900, 1800, or 3600. */ DCP.editServer = function(input, callback) { var payload, serverID = input.serverID, server, client = this; if (!client.servers[serverID]) return handleErrCB(("[editServer] Server " + serverID + " not found."), callback); server = client.servers[serverID]; payload = { name: server.name, icon: server.icon, region: server.region, afk_channel_id: server.afk_channel_id, afk_timeout: server.afk_timeout }; for (var key in input) { if (Object.keys(payload).indexOf(key) < 0) continue; if (key === 'afk_channel_id') { if (server.channels[input[key]] && server.channels[input[key]].type === 'voice') payload[key] = input[key]; continue; } if (key === 'afk_timeout') { if ([60, 300, 900, 1800, 3600].indexOf(Number(input[key])) > -1) payload[key] = input[key]; continue; } payload[key] = input[key]; } if (input.icon) payload.icon = "data:image/jpg;base64," + input.icon; client._req('patch', Endpoints.SERVERS(input.serverID), payload, function(err, res) { handleResCB("Unable to edit server", err, res, callback); }); }; /** * Edit the widget information for a server. * @arg {Object} input * @arg {Snowflake} input.serverID - The ID of the server whose widget you want to edit. * @arg {Boolean} [input.enabled] - Whether or not you want the widget to be enabled. * @arg {Snowflake} [input.channelID] - [Important] The ID of the channel you want the instant invite to point to. */ DCP.editServerWidget = function(input, callback) { var client = this, payload, url = Endpoints.SERVERS(input.serverID) + "/embed"; client._req('get', url, function(err, res) { if (err) return handleResCB("Unable to GET server widget settings. Can not edit without retrieving first.", err, res, callback); payload = { enabled: ('enabled' in input ? input.enabled : res.body.enabled), channel_id: ('channelID' in input ? input.channelID : res.body.channel_id) }; client._req('patch', url, payload, function(err, res) { handleResCB("Unable to edit server widget", err, res, callback); }); }); }; /** * [User Account] Add an emoji to a server * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {String} input.name - The emoji's name * @arg {String<Base64>} input.image - The emoji's image data in Base64 */ DCP.addServerEmoji = function(input, callback) { var payload = { name: input.name, image: "data:image/png;base64," + input.image }; this._req('post', Endpoints.SERVER_EMOJIS(input.serverID), payload, function(err, res) { handleResCB("Unable to add emoji to the server", err, res, callback); }); } /** * [User Account] Edit a server emoji data (name only, currently) * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.emojiID - The emoji's ID * @arg {String} [input.name] * @arg {Array<Snowflake>} [input.roles] - An array of role IDs you want to limit the emoji's usage to */ DCP.editServerEmoji = function(input, callback) { var emoji, payload = {}; if ( !this.servers[input.serverID] ) return handleErrCB(("[editServerEmoji] Server not available: " + input.serverID), callback); if ( !this.servers[input.serverID].emojis[input.emojiID]) return handleErrCB(("[editServerEmoji] Emoji not available: " + input.emojiID), callback); emoji = this.servers[input.serverID].emojis[input.emojiID]; payload.name = input.name || emoji.name; payload.roles = input.roles || emoji.roles; this._req('patch', Endpoints.SERVER_EMOJIS(input.serverID, input.emojiID), payload, function(err, res) { handleResCB("[editServerEmoji] Could not edit server emoji", err, res, callback); }); }; /** * [User Account] Remove an emoji from a server * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.emojiID */ DCP.deleteServerEmoji = function(input, callback) { this._req('delete', Endpoints.SERVER_EMOJIS(input.serverID, input.emojiID), function(err, res) { handleResCB("[deleteServerEmoji] Could not delete server emoji", err, res, callback); }); }; /** * Leave a server. * @arg {Snowflake} serverID */ DCP.leaveServer = function(serverID, callback) { this._req('delete', Endpoints.SERVERS_PERSONAL(serverID), function(err, res) { handleResCB("Could not leave server", err, res, callback); }); }; /** * Delete a server owned by the client. * @arg {Snowflake} serverID */ DCP.deleteServer = function(serverID, callback) { this._req('delete', Endpoints.SERVERS(serverID), function(err, res) { handleResCB("Could not delete server", err, res, callback); }); }; /** * Transfer ownership of a server to another user. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.transferOwnership = function(input, callback) { this._req('patch', Endpoints.SERVERS(input.serverID), {owner_id: input.userID}, function(err, res) { handleResCB("Could not transfer server ownership", err, res, callback); }); }; /** * (Used to) Accept an invite to a server [User Only]. Can no longer be used. * @deprecated */ DCP.acceptInvite = function(NUL, callback) { return handleErrCB("acceptInvite can no longer be used", callback); }; /** * (Used to) Generate an invite URL for a channel. * @deprecated */ DCP.createInvite = function(input, callback) { var payload, client = this; payload = { max_age: 0, max_users: 0, temporary: false }; if ( Object.keys(input).length === 1 && input.channelID ) { payload = { validate: client.internals.lastInviteCode || null }; } for (var key in input) { if (Object.keys(payload).indexOf(key) < 0) continue; payload[key] = input[key]; } this._req('post', Endpoints.CHANNEL(input.channelID) + "/invites", payload, function(err, res) { try {client.internals.lastInviteCode = res.body.code;} catch(e) {} handleResCB('Unable to create invite', err, res, callback); }); }; /** * Delete an invite code. * @arg {String} inviteCode */ DCP.deleteInvite = function(inviteCode, callback) { this._req('delete', Endpoints.INVITES(inviteCode), function(err, res) { handleResCB('Unable to delete invite', err, res, callback); }); }; /** * Get information on an invite. * @arg {String} inviteCode */ DCP.queryInvite = function(inviteCode, callback) { this._req('get', Endpoints.INVITES(inviteCode), function(err, res) { handleResCB('Unable to get information about invite', err, res, callback); }); }; /** * Get all invites for a server. * @arg {Snowflake} serverID */ DCP.getServerInvites = function(serverID, callback) { this._req('get', Endpoints.SERVERS(serverID) + "/invites", function(err, res) { handleResCB('Unable to get invite list for server' + serverID, err, res, callback); }); }; /** * Get all invites for a channel. * @arg {Snowflake} channelID */ DCP.getChannelInvites = function(channelID, callback) { this._req('get', Endpoints.CHANNEL(channelID) + "/invites", function(err, res) { handleResCB('Unable to get invite list for channel' + channelID, err, res, callback); }); }; /** * Create a channel. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {String} input.name * @arg {String} [input.type] - 'text' or 'voice', defaults to 'text. */ DCP.createChannel = function(input, callback) { var client = this, payload = { name: input.name, type: (['text', 'voice'].indexOf(input.type) < 0) ? 'text' : input.type }; this._req('post', Endpoints.SERVERS(input.serverID) + "/channels", payload, function(err, res) { try { var serverID = res.body.guild_id; var channelID = res.body.id; client.channels[channelID] = new Channel( client, client.servers[serverID], res.body ); } catch(e) {} handleResCB('Unable to create channel', err, res, callback); }); }; /** * Create a Direct Message channel. * @arg {Snowflake} userID */ DCP.createDMChannel = function(userID, callback) { var client = this; this._req('post', Endpoints.USER(client.id) + "/channels", {recipient_id: userID}, function(err, res) { if (!err && goodResponse(res)) client._uIDToDM[res.body.recipient.id] = res.body.id; handleResCB("Unable to create DM Channel", err, res, callback); }); }; /** * Delete a channel. * @arg {Snowflake} channelID */ DCP.deleteChannel = function(channelID, callback) { this._req('delete', Endpoints.CHANNEL(channelID), function(err, res) { handleResCB("Unable to delete channel", err, res, callback); }); }; /** * Edit a channel's information. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {String} [input.name] * @arg {String} [input.topic] - The topic of the channel. * @arg {Number} [input.bitrate] - [Voice Only] The bitrate for the channel. * @arg {Number} [input.position] - The channel's position on the list. * @arg {Number} [input.user_limit] - [Voice Only] Imposes a user limit on a voice channel. */ DCP.editChannelInfo = function(input, callback) { var channel, payload; try { channel = this.channels[input.channelID]; payload = { name: channel.name, topic: channel.topic, bitrate: channel.bitrate, position: channel.position, user_limit: channel.user_limit }; for (var key in input) { if (Object.keys(payload).indexOf(key) < 0) continue; if (+input[key]) { if (key === 'bitrate') { payload.bitrate = Math.min( Math.max( input.bitrate, 8000), 96000); continue; } if (key === 'user_limit') { payload.user_limit = Math.min( Math.max( input.user_limit, 0), 99); continue; } } payload[key] = input[key]; } this._req('patch', Endpoints.CHANNEL(input.channelID), payload, function(err, res) { handleResCB("Unable to edit channel", err, res, callback); }); } catch(e) {return handleErrCB(e, callback);} }; /** * Edit (or creates) a permission override for a channel. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} [input.userID] * @arg {Snowflake} [input.roleID] * @arg {Array<Number>} input.allow - An array of permissions to allow. Discord.Permissions.XXXXXX. * @arg {Array<Number>} input.deny - An array of permissions to deny, same as above. * @arg {Array<Number>} input.default - An array of permissions that cancels out allowed and denied permissions. */ DCP.editChannelPermissions = function(input, callback) { //Will shrink this up later var payload, pType, ID, channel, permissions, allowed_values; if (!input.userID && !input.roleID) return handleErrCB("[editChannelPermissions] No userID or roleID provided", callback); if (!this.channels[input.channelID]) return handleErrCB(("[editChannelPermissions] No channel found for ID: " + input.channelID), callback); if (!input.allow && !input.deny && !input.default) return handleErrCB("[editChannelPermissions] No allow, deny or default array provided.", callback); pType = input.userID ? 'user' : 'role'; ID = input[pType + "ID"]; channel = this.channels[ input.channelID ]; permissions = channel.permissions[pType][ID] || { allow: 0, deny: 0 }; allowed_values = [0, 4, 28].concat((channel.type === 'text' ? [10, 11, 12, 13, 14, 15, 16, 17, 18] : [20, 21, 22, 23, 24, 25] )); //Take care of allow first if (type(input.allow) === 'array') { input.allow.forEach(function(perm) { if (allowed_values.indexOf(perm) < 0) return; if (hasPermission(perm, permissions.deny)) { permissions.deny = removePermission(perm, permissions.deny); } permissions.allow = givePermission(perm, permissions.allow); }); } //Take care of deny second if (type(input.deny) === 'array') { input.deny.forEach(function(perm) { if (allowed_values.indexOf(perm) < 0) return; if (hasPermission(perm, permissions.allow)) { permissions.allow = removePermission(perm, permissions.allow); } permissions.deny = givePermission(perm, permissions.deny); }); } //Take care of defaulting last if (type(input.default) === 'array') { input.default.forEach(function(perm) { if (allowed_values.indexOf(perm) < 0) return; permissions.allow = removePermission(perm, permissions.allow); permissions.deny = removePermission(perm, permissions.deny); }); } payload = { type: (pType === 'user' ? 'member' : 'role'), id: ID, deny: permissions.deny, allow: permissions.allow }; this._req('put', Endpoints.CHANNEL(input.channelID) + "/permissions/" + ID, payload, function(err, res) { handleResCB('Unable to edit permission', err, res, callback); }); }; /** * Delete a permission override for a channel. * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} [input.userID] * @arg {Snowflake} [input.roleID] */ DCP.deleteChannelPermission = function(input, callback) { var payload, pType, ID; if (!input.userID && !input.roleID) return handleErrCB("[deleteChannelPermission] No userID or roleID provided", callback); if (!this.channels[input.channelID]) return handleErrCB(("[deleteChannelPermission] No channel found for ID: " + input.channelID), callback); pType = input.userID ? 'user' : 'role'; ID = input[pType + "ID"]; payload = { type: (pType === 'user' ? 'member' : 'role'), id: ID }; this._req('delete', Endpoints.CHANNEL(input.channelID) + "/permissions/" + ID, payload, function(err, res) { handleResCB('Unable to delete permission', err, res, callback); }); }; /** * Create a role for a server. * @arg {Snowflake} serverID */ DCP.createRole = function(serverID, callback) { var client = this; this._req('post', Endpoints.ROLES(serverID), function(err, res) { try { client.servers[serverID].roles[res.body.id] = new Role(res.body); } catch(e) {} handleResCB("Unable to create role", err, res, callback); }); }; /** * Edit a role. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.roleID - The ID of the role. * @arg {String} [input.name] * @arg {String} [input.color] - A color value as a number. Recommend using Hex numbers, as they can map to HTML colors (0xF35353 === #F35353). * @arg {Boolean} [input.hoist] - Separates the users in this role from the normal online users. * @arg {Object} [input.permissions] - An Object containing the permission as a key, and `true` or `false` as its value. Read the Permissions doc. * @arg {Boolean} [input.mentionable] - Toggles if users can @Mention this role. */ DCP.editRole = function(input, callback) { var role, payload; try { role = new Role(this.servers[input.serverID].roles[input.roleID]); payload = { name: role.name, color: role.color, hoist: role.hoist, permissions: role._permissions, mentionable: role.mentionable, position: role.position }; for (var key in input) { if (Object.keys(payload).indexOf(key) < 0) continue; if (key === 'permissions') { for (var perm in input[key]) { role[perm] = input[key][perm]; payload.permissions = role._permissions; } continue; } if (key === 'color') { if (String(input[key])[0] === '#') payload.color = parseInt(String(input[key]).replace('#', '0x'), 16); if (Discord.Colors[input[key]]) payload.color = Discord.Colors[input[key]]; if (type(input[key]) === 'number') payload.color = input[key]; continue; } payload[key] = input[key]; } this._req('patch', Endpoints.ROLES(input.serverID, input.roleID), payload, function(err, res) { handleResCB("Unable to edit role", err, res, callback); }); } catch(e) {return handleErrCB(('[editRole] ' + e), callback);} }; /** * Delete a role. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.roleID */ DCP.deleteRole = function(input, callback) { this._req('delete', Endpoints.ROLES(input.serverID, input.roleID), function(err, res) { handleResCB("Could not remove role", err, res, callback); }); }; /** * Add a user to a role. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.roleID * @arg {Snowflake} input.userID */ DCP.addToRole = function(input, callback) { this._req('put', Endpoints.MEMBER_ROLES(input.serverID, input.userID, input.roleID), function(err, res) { handleResCB("Could not add role", err, res, callback); }); }; /** * Remove a user from a role. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.roleID * @arg {Snowflake} input.userID */ DCP.removeFromRole = function(input, callback) { this._req('delete', Endpoints.MEMBER_ROLES(input.serverID, input.userID, input.roleID), function(err, res) { handleResCB("Could not remove role", err, res, callback); }); }; /** * Edit a user's nickname. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID * @arg {String} input.nick - The nickname you'd like displayed. */ DCP.editNickname = function(input, callback) { var payload = {nick: String( input.nick ? input.nick : "" )}; var url = input.userID === this.id ? Endpoints.MEMBERS(input.serverID) + "/@me/nick" : Endpoints.MEMBERS(input.serverID, input.userID); this._req('patch', url, payload, function(err, res) { handleResCB("Could not change nickname", err, res, callback); }); }; /** * Edit a user's note. * @arg {Object} input * @arg {Snowflake} input.userID * @arg {String} input.note - The note content that you want to use. */ DCP.editNote = function(input, callback) { this._req('put', Endpoints.NOTE(input.userID), {note: input.note}, function(err, res) { handleResCB("Could not edit note", err, res, callback); }); }; /** * Retrieve a user object from Discord, the library already caches users, however. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID */ DCP.getMember = function(input, callback) { this._req('get', Endpoints.MEMBERS(input.serverID, input.userID), function(err, res) { handleResCB("Could not get member", err, res, callback); }); }; /** * Retrieve a group of user objects from Discord. * @arg {Object} input * @arg {Number} [input.limit] - The amount of users to pull, defaults to 50. * @arg {Snowflake} [input.after] - The offset using a user ID. */ DCP.getMembers = function(input, callback) { var qs = {}; qs.limit = (typeof(input.limit) !== 'number' ? 50 : input.limit); if (input.after) qs.after = input.after; this._req('get', Endpoints.MEMBERS(input.serverID) + qstringify(qs), function(err, res) { handleResCB("Could not get members", err, res, callback); }); }; /** * Get the ban list from a server * @arg {Snowflake} serverID */ DCP.getBans = function(serverID, callback) { this._req('get', Endpoints.BANS(serverID), function(err, res) { handleResCB("Could not get ban list", err, res, callback); }); }; /** * Get all webhooks for a server * @arg {Snowflake} serverID */ DCP.getServerWebhooks = function(serverID, callback) { this._req('get', Endpoints.SERVER_WEBHOOKS(serverID), function(err, res) { handleResCB("Could not get server Webhooks", err, res, callback); }); }; /** * Get webhooks from a channel * @arg {Snowflake} channelID */ DCP.getChannelWebhooks = function(channelID, callback) { this._req('get', Endpoints.CHANNEL_WEBHOOKS(channelID), function(err, res) { handleResCB("Could not get channel Webhooks", err, res, callback); }); }; /** * Create a webhook for a server * @arg {Snowflake} serverID */ DCP.createWebhook = function(serverID, callback) { this._req('post', Endpoints.SERVER_WEBHOOKS(serverID), function(err, res) { handleResCB("Could not create a Webhook", err, res, callback); }); }; /** * Edit a webhook * @arg {Object} input * @arg {Snowflake} input.webhookID - The Webhook's ID * @arg {String} [input.name] * @arg {String<Base64>} [input.avatar] * @arg {String} [input.channelID] */ DCP.editWebhook = function(input, callback) { var client = this, payload = {}, allowed = ['avatar', 'name']; this._req('get', Endpoints.WEBHOOKS(input.webhookID), function(err, res) { if (err || !goodResponse(res)) return handleResCB("Couldn't get webhook, do you have permissions to access it?", err, res, callback); allowed.forEach(function(key) { payload[key] = (key in input ? input[key] : res.body[key]); }); payload.channel_id = input.channelID || res.body.channel_id; client._req('patch', Endpoints.WEBHOOKS(input.webhookID), payload, function(err, res) { return handleResCB("Couldn't update webhook", err, res, callback); }); }); }; /* --- Voice --- */ /** * Join a voice channel. * @arg {Snowflake} channelID */ DCP.joinVoiceChannel = function(channelID, callback) { var serverID, server, channel, voiceSession; try { serverID = this.channels[channelID].guild_id; server = this.servers[serverID]; channel = server.channels[channelID]; } catch(e) {} if (!serverID) return handleErrCB(("Cannot find the server related to the channel provided: " + channelID), callback); if (channel.type !== 'voice') return handleErrCB(("Selected channel is not a voice channel: " + channelID), callback); if (this._vChannels[channelID]) return handleErrCB(("Voice channel already active: " + channelID), callback); voiceSession = getVoiceSession(this, channelID, server); voiceSession.self_mute = server.self_mute; voiceSession.self_deaf = server.self_deaf; checkVoiceReady(voiceSession, callback); return send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, server.self_deaf)); }; /** * Leave a voice channel. * @arg {Snowflake} channelID */ DCP.leaveVoiceChannel = function(channelID, callback) { if (!this._vChannels[channelID]) return handleErrCB(("Not in the voice channel: " + channelID), callback); return leaveVoiceChannel(this, channelID, callback); }; /** * Prepare the client for sending/receiving audio. * @arg {Snowflake|Object} channelObj - Either the channel ID, or an Object with `channelID` as a key and the ID as the value. * @arg {Number} [channelObj.maxStreamSize] - The size in KB that you wish to receive before pushing out earlier data. Required if you want to store or receive incoming audio. * @arg {Boolean} [channelObj.stereo] - Sets the audio to be either stereo or mono. Defaults to true. */ DCP.getAudioContext = function(channelObj, callback) { // #q/qeled gave a proper timing solution. Credit where it's due. if (!isNode) return handleErrCB("Using audio in the browser is currently not supported.", callback); var channelID = channelObj.channelID || channelObj, voiceSession = this._vChannels[channelID], encoder = chooseAudioEncoder(['ffmpeg', 'avconv']); if (!voiceSession) return handleErrCB(("You have not joined the voice channel: " + channelID), callback); if (voiceSession.ready !== true) return handleErrCB(("The connection to the voice channel " + channelID + " has not been initialized yet."), callback); if (!encoder) return handleErrCB("You need either 'ffmpeg' or 'avconv' and they need to be added to PATH", callback); voiceSession.audio = voiceSession.audio || new AudioCB( voiceSession, channelObj.stereo === false ? 1 : 2, encoder, Math.abs(Number(channelObj.maxStreamSize))); return call(callback, [null, voiceSession.audio]); }; /* --- Misc --- */ /** * Retrieves all offline (and online, if using a user account) users, fires the `allUsers` event when done. */ DCP.getAllUsers = function(callback) { var servers = Object.keys(this.servers).filter(function(s) { s = this.servers[s]; if (s.members) return s.member_count !== Object.keys(s.members).length && (this.bot ? s.large : true); }, this); if (!servers[0]) { this.emit('allUsers'); return handleErrCB("There are no users to be collected", callback); } if (!this.bot) send(this._ws, Payloads.ALL_USERS(this)); return getOfflineUsers(this, servers, callback); }; /* --- Functions --- */ function handleErrCB(err, callback) { if (!err) return false; return call(callback, [new Error(err)]); } function handleResCB(errMessage, err, res, callback) { if (typeof(callback) !== 'function') return; res = res || {}; if (!err && goodResponse(res)) return (callback(null, res.body), true); var e = new Error( err || errMessage ); e.name = "ResponseError"; e.statusCode = res.statusCode; e.statusMessage = res.statusMessage; e.response = res.body; return (callback(e), false); } function goodResponse(response) { return (response.statusCode / 100 | 0) === 2; } function stringifyError(response) { if (!response) return null; return response.statusCode + " " + response.statusMessage + "\n" + JSON.stringify(response.body); } /* - Functions - Messages - */ function sendMessage(client, to, message, callback) { resolveID(client, to, function(channelID) { client._req('post', Endpoints.MESSAGES(channelID), message, function(err, res) { handleResCB("Unable to send messages", err, res, callback); }); }); } function cacheMessage(cache, limit, channelID, message) { if (!cache[channelID]) cache[channelID] = {}; if (limit === -1) return void(cache[channelID][message.id] = message); var k = Object.keys(cache[channelID]); if (k.length > limit) delete(cache[channelID][k[0]]); cache[channelID][message.id] = message; } function generateMessage(message, embed) { return { content: String(message), nonce: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER), embed: embed || {} }; } function messageHeaders(client) { var r = { "accept": "*/*", "accept-language": "en-US;q=0.8", }; if (isNode) { r["accept-encoding"] = "gzip, defla