discord.io
Version:
JavaScript interface for Discord.
1,422 lines (1,272 loc) • 101 kB
JavaScript
"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