var request = require("superagent"); var Endpoints = require("./lib/endpoints.js"); var Server = require("./lib/server.js").Server; var Message = require("./lib/message.js").Message; var User = require("./lib/user.js").User; var Channel = require("./lib/channel.js").Channel; var List = require("./lib/list.js").List; var Invite = require("./lib/invite.js").Invite; var PMChannel = require("./lib/PMChannel.js").PMChannel; var WebSocket = require('ws'); var Internal = require("./lib/internal.js").Internal; var TokenManager = require("./lib/TokenManager.js").TokenManager; var serverCreateRequests = [], globalLoginTime = Date.now(); function tp(time) { return Date.now() - time; } /** * The wrapper module for the Discord Client, also provides some helpful objects. * * @module Discord */ exports; exports.Endpoints = Endpoints; exports.Server = Server; exports.Message = Message; exports.User = User; exports.Channel = Channel; exports.List = List; exports.Invite = Invite; exports.PMChannel = PMChannel; /** * The Discord Client used to interface with the Discord API. Instantiate this to start writing code * with discord.js * @class Client * @constructor * @param {Object} [options] An object containing configurable options. * @param {Number} [options.maxmessage=5000] The maximum amount of messages to be stored per channel. */ exports.Client = function (shouldUseTokenManager) { /** * Contains the options of the client * @attribute options * @type {Object} */ if (shouldUseTokenManager) this.tokenManager = new TokenManager("./", "tokencache.json"); /** * Contains the token used to authorise HTTP requests and WebSocket connection. Received when login was successful. * @attribute token * @readonly * @type {String} */ this.token = ""; /** * Indicates whether the client is logged in or not. Does not indicate whether the client is ready. * @attribute loggedIn * @readonly * @type {Boolean} */ this.loggedIn = false; /** * The WebSocket used when receiving message and other event updates. * @type {WebSocket} * @attribute websocket * @readonly */ this.websocket = null; /** * An Object containing the functions tied to events. These should be set using Client.on(); * @type {Object} * @attribute events */ this.events = {}; /** * The User of the Client class, set when the initial startup is complete. * @attribute user * @type {User} * @readonly */ this.user = null; /** * Indicates whether the Client is ready and has cached all servers it as aware of. * @type {Boolean} * @attribute ready * @readonly */ this.ready = false; /** * A List containing all the Servers the Client has access to. * @attribute serverList * @type {List} * @readonly */ this.serverList = new List("id"); /** * A List containing all the PMChannels the Client has access to. * @attribute PMList * @type {List} * @readonly */ this.PMList = new List("id"); } /** * Returns a list of all servers that the Discord Client has access to. * * @method getServers * @return {List} ServerList */ exports.Client.prototype.getServers = function () { return this.serverList; } /** * Returns a list of all servers that the Discord Client has access to. * @method getChannels * @return {List} Channelist */ exports.Client.prototype.getChannels = function () { return this.serverList.concatSublists("channels", "id"); } /** * Returns a Server matching the given id, or false if not found. Will return false if the server is not cached or not available. * @method getServer * @param {String/Number} id The ID of the Server * @return {Server} The Server matching the ID */ exports.Client.prototype.getServer = function (id) { return this.getServers().filter("id", id, true); } /** * Returns a Channel matching the given id, or false if not found. Will return false if the Channel is not cached or not available. * @method getChannel * @param {String/Number} id The ID of the Channel * @return {Server} The Channel matching the ID */ exports.Client.prototype.getChannel = function (id) { return this.getChannels().filter("id", id, true); } /** * Returns a Channel matching the given name, or false if not found. Will return false if the Channel is not cached or not available. * @method getChannelByName * @param {String/Number} name The Name of the Channel * @return {Server} The Channel matching the Name */ exports.Client.prototype.getChannelByName = function (name) { return this.getChannels().filter("name", name, true); } /** * Triggers an .on() event. * @param {String} event The event to be triggered * @param {Array} args The arguments to be passed onto the method * @return {Boolean} whether the event was triggered successfully. * @method triggerEvent * @private */ exports.Client.prototype.triggerEvent = function (event, args) { if (!this.ready && event !== "raw" && event !== "disconnected" && event !== "debug") { //if we're not even loaded yet, don't try doing anything because it always ends badly! return false; } if (this.events[event]) { this.events[event].apply(this, args); return true; } else { return false; } } /** * Binds a function to an event * @param {String} name The event name which the function should be bound to. * @param {Function} fn The function that should be bound to the event. * @method on */ exports.Client.prototype.on = function (name, fn) { this.events[name] = fn; } /** * Unbinds a function from an event * @param {String} name The event name which should be cleared * @method off */ exports.Client.prototype.off = function (name) { this.events[name] = function () { }; } exports.Client.prototype.cacheServer = function (id, cb, members) { var self = this; var serverInput = {}; if (typeof id === 'string' || id instanceof String) { //actually an ID if (this.serverList.filter("id", id).length > 0) { return; } Internal.XHR.getServer(self.token, id, function (err, data) { if (!err) { makeServer(data); } }) } else { // got objects because SPEEEDDDD if (this.serverList.filter("id", id.id).length > 0) { return; } serverInput = id; id = id.id; makeServer(serverInput); } function channelsFromHTTP() { Internal.XHR.getChannel(self.token, id, function (err) { if (!err) cacheChannels(res.body); }) } var server; function makeServer(dat) { server = new Server(dat.region, dat.owner_id, dat.name, id, serverInput.members || dat.members, dat.icon, dat.afk_timeout, dat.afk_channel_id); if (dat.channels) cacheChannels(dat.channels); else channelsFromHTTP(); } function cacheChannels(dat) { var channelList = dat; for (var channel of channelList) { server.channels.add(new Channel(channel, server)); } self.serverList.add(server); cb(server); } } /** * Logs the Client in with the specified credentials and begins initialising it. * @async * @method login * @param {String} email The Discord email. * @param {String} password The Discord password. * @param {Function} [callback] Called when received reply from authentication server. * @param {Object} callback.error Set to null if there was no error logging in, otherwise is an Object that * can be evaluated as true. * @param {String} callback.error.reason The reason why there was an error * @param {Object} callback.error.error The raw XHR error. * @param {String} callback.token The token received when logging in */ exports.Client.prototype.login = function (email, password, callback, noCache) { globalLoginTime = Date.now(); this.debug("login called at " + globalLoginTime); var self = this; callback = callback || function () { }; if (noCache == undefined || noCache == null) { noCache = false; } self.connectWebsocket(); if (this.tokenManager) { if (this.tokenManager.exists(email) && !noCache) { var token = this.tokenManager.getToken(email, password); if (!token.match(/[^\w.-]+/g)) { done(this.tokenManager.getToken(email, password)); self.debug("loaded token from caches in " + tp(globalLoginTime)); return; } else { self.debug("error getting token from caches, using default auth"); } } } var time = Date.now(); Internal.XHR.login(email, password, function (err, token) { if (err) { self.triggerEvent("disconnected", [{ reason: "failed to log in", error: err }]); self.websocket.close(); self.debug("failed to login in " + tp(globalLoginTime)); } else { if (!noCache) { self.tokenManager.addToken(email, token, password); } self.debug("loaded token from auth servers in " + tp(globalLoginTime)); done(token); } }); function done(token) { self.email = email; self.password = password; self.debug("using token " + token); self.token = token; self.websocket.sendData(); self.loggedIn = true; callback(null, token); } } /** * Replies to a message with a given message * @param {Message/User/Channel/Server/String} destination Where the message should be sent. Channel IDs can also be used here. * @param {String/Message/Array} toSend If a message, the message's content will be sent. If an array, a message will be sent of * the array seperated by a newline. If a String, the string will be sent. * @param {Function} callback Called when a response from the API has been received, the message has either been sent or not. * @param {Object} callback.error If there was an error, this would be an XHR Error object. Otherwise, it will be null. * @param {Message} callback.message If there were no errors, this will be the sent message in Message form. * @param {Object} options see sendMessage(options) * @method reply */ exports.Client.prototype.reply = function (destination, toSend, callback, options) { if (toSend instanceof Array) { toSend = toSend.join("\n"); } toSend = destination.author.mention() + ", " + toSend; this.sendMessage(destination, toSend, callback, options); } exports.Client.prototype.connectWebsocket = function (cb) { var self = this; var sentInitData = false; this.websocket = new WebSocket(Endpoints.WEBSOCKET_HUB); this.websocket.onclose = function (e) { self.triggerEvent("disconnected", [{ reason: "websocket disconnected", error: e }]); }; this.websocket.onmessage = function (e) { self.triggerEvent("raw", [e]); var dat = JSON.parse(e.data); var webself = this; switch (dat.op) { case 0: if (dat.t === "READY") { self.debug("got ready packet"); var data = dat.d; self.user = new User(data.user); var _servers = data.guilds, servers = []; var cached = 0, toCache = _servers.length; for (x in data.private_channels) { self.PMList.add(new PMChannel(data.private_channels[x].recipient, data.private_channels[x].id)); } for (x in _servers) { _server = _servers[x]; self.cacheServer(_server, function (server) { cached++; if (cached === toCache) { self.ready = true; self.triggerEvent("ready"); self.debug("ready triggered"); } }); } setInterval(function () { webself.keepAlive.apply(webself); }, data.heartbeat_interval); } else if (dat.t === "MESSAGE_CREATE") { var data = dat.d; var channel = self.getChannel(data.channel_id); var message = new Message(data, channel); self.triggerEvent("message", [message]); if (channel.messages) channel.messages.add(message); } else if (dat.t === "MESSAGE_DELETE") { var data = dat.d; var channel = self.getChannel(data.channel_id); if (!channel.messages) return; var _msg = channel.messages.filter("id", data.id, true); if (_msg) { self.triggerEvent("messageDelete", [_msg]); channel.messages.removeElement(_msg); } } else if (dat.t === "MESSAGE_UPDATE") { var data = dat.d; if (!self.ready) return; var formerMessage = false; var channel = self.getChannel(data.channel_id); if (channel) { formerMessage = channel.messages.filter("id", data.id, true); var newMessage; data.author = data.author || formerMessage.author; data.timestamp = data.time || formerMessage.time; data.content = data.content || formerMessage.content; data.channel = data.channel || formerMessage.channel; data.id = data.id || formerMessage.id; data.mentions = data.mentions || formerMessage.mentions; data.mention_everyone = data.mention_everyone || formerMessage.everyoneMentioned; data.embeds = data.embeds || formerMessage.embeds; try { newMessage = new Message(data, channel); } catch (e) { self.debug("dropped a message update packet"); return; } self.triggerEvent("messageUpdate", [formerMessage, newMessage]); if (formerMessage) channel.messages.updateElement(formerMessage, newMessage); else channel.messages.add(newMessage); } } else if (dat.t === "PRESENCE_UPDATE") { var data = dat.d; var getUser = self.getUser(data.user.id); if (getUser) { //user already exists var usr = new User(data.user); if (usr.equalsStrict(getUser)) { //no changes, actually a presence. } else { if (self.updateUserReferences(data.user.id, getUser, new User(data.user))) { self.triggerEvent("userupdate", [getUser, usr]); } } } self.triggerEvent("presence", [new User(data.user), data.status, self.serverList.filter("id", data.guild_id, true)]); } else if (dat.t === "GUILD_DELETE") { var deletedServer = self.serverList.filter("id", dat.d.id, true); if (deletedServer) { self.triggerEvent("serverDelete", [deletedServer]); } } else if (dat.t === "CHANNEL_DELETE") { var delServer = self.serverList.filter("id", dat.d.guild_id, true); if (delServer) { var channel = delServer.channels.filter("id", dat.d.id, true); if (channel) { self.triggerEvent("channelDelete", [channel]); } } } else if (dat.t === "GUILD_CREATE") { if (!self.serverList.filter("id", dat.d.id, true)) { self.cacheServer(dat.d, function (server) { if (serverCreateRequests[server.id]) { serverCreateRequests[server.id](null, server); serverCreateRequests[server.id] = null; } else { self.triggerEvent("serverJoin", [server]); } }); } } else if (dat.t === "CHANNEL_CREATE") { var srv = self.serverList.filter("id", dat.d.guild_id, true); if (srv) { if (!srv.channels.filter("id", dat.d.d, true)) { var chann = new Channel(dat.d, srv); srv.channels.add(new Channel(dat.d, srv)); self.triggerEvent("channelCreate", [chann]); } } } else if (dat.t === "USER_UPDATE") { if (dat.d.id === self.user.id) { var newUsr = new User(dat.d); self.triggerEvent("userupdate", [self.user, newUsr]); self.user = newUsr; } } else if (dat.t === "GUILD_MEMBER_ADD") { var srv = self.getServer(dat.d.guild_id); if (srv) { var usr = new User(dat.d.user); srv.members.add(usr); self.triggerEvent("serverMemberAdd", [usr]); } } else if (dat.t === "GUILD_MEMBER_REMOVE") { var srv = self.getServer(dat.d.guild_id); if (srv) { var usr = new User(dat.d.user); srv.members.removeElement(usr); self.triggerEvent("serverMemberRemove", [usr]); } } break; } }; this.websocket.sendPacket = function (p) { this.send(JSON.stringify(p)); } this.websocket.keepAlive = function () { if (this.readyState !== 1) return false; this.sendPacket({ op: 1, d: Date.now() }); } this.websocket.onopen = function () { self.debug("websocket conn open"); this.sendData("onopen"); } this.websocket.sendData = function (why) { if (this.readyState == 1 && !sentInitData && self.token) { sentInitData = true; var connDat = { op: 2, d: { token: self.token, v: 2 } }; connDat.d.properties = Internal.WebSocket.properties; this.sendPacket(connDat); } } } exports.Client.prototype.debug = function (msg) { this.triggerEvent("debug", ["[SL " + tp(globalLoginTime) + "] " + msg]); } /** * Logs the current Client out of Discord and closes any connections. * @param {Function} callback Called after a response is obtained. * @param {Object} callback.error Null unless there was an error, in which case is an XHR error. * @method logout */ exports.Client.prototype.logout = function (callback) { callback = callback || function () { }; var self = this; Internal.XHR.logout(self.token, function (err) { if (err) { callback(err); } self.loggedIn = false; self.websocket.close(); }); } /** * Creates a server with the specified name and region and returns it * @param {String} name The name of the server * @param {String} region The region of the server * @param {Function} callback Called when the request is made. * @param {Object} callback.error An XHR error or null if there were no errors. * @param {Server} callback.server A Server object representing the created server. * @method createServer * @async */ exports.Client.prototype.createServer = function (name, region, cb) { var self = this; Internal.XHR.createServer(self.token, name, region, function (err, data) { if (err) { cb(err); } else { self.cacheServer(data, function (server) { cb(null, server); }); } }); } /** * Makes the Client leave the Server * @param {Server} server A server object. The server you want to leave. * @param {Function} callback Called when the leave request is made. * @param {Object} callback.error An XHR error or null if there were no errors. * @param {Server} callback.server A Server object representing the deleted server. * @method leaveServer * @async */ exports.Client.prototype.leaveServer = function (server, callback) { var self = this; // callback is not necessary for this function callback = callback || function () { }; Internal.XHR.leaveServer(self.token, server.id, function (err) { if (err) { callback(err); } else { self.serverList.removeElement(server); callback(null, server); } }); } /** * Creates an Invite to the specified channel/server with the specified options * @param {Channel/Server} channel The channel/server the invite is to be made to. * @param {Object} [options] The options for the invite * @param {Number} [options.max_age=0] When the invite will expire in seconds * @param {Number} [options.max_uses=0] How many uses the invite has * @param {Boolean} [options.temporary=false] Whether the invite is temporary * @param {Boolean} [options.xkcdpass=false] Whether the invite's code should be composed of words. * @param {Function} callback Called when the invite request has been made * @param {Object} callback.error An XHR Error or null if there were no errors. * @param {Invite} callback.invite An invite object representing the created invite. * @method createInvite */ exports.Client.prototype.createInvite = function (channel, options, callback) { var self = this; var options = options || {}; // callback is not necessary for this function callback = callback || function () { }; if (channel instanceof Server) { channel = channel.getDefaultChannel(); } options.max_age = options.max_age || 0; options.max_uses = options.max_uses || 0; options.temporary = options.temporary || false; options.xkcdpass = options.xkcd || false; Internal.XHR.createInvite(self.token, channel.id, options, function (err, data) { if (err) { callback(err); } else { callback(null, new Invite(data)); } }); } exports.Client.prototype.startPM = function (user, callback) { var self = this; callback = callback || function () { }; Internal.XHR.startPM(self.token, self.user.id, user.id, function (err, data) { if (err) { callback(err); } else { var channel = new PMChannel(data.recipient, data.id); self.PMList.add(channel); callback(null, channel); } }); } /** * Sends a message to the specified destination. * @param {Server/Channel/PMChannel/Message/User/String} destination Where the message should be sent. If this is a String, the String should be a channel ID. * @param {String/Array/Message} toSend The message to send. If an array, the array will be seperated into new lines and then sent. * @param {Function} callback Called when the message has been sent. * @param {Object} error An XHR Error or null if there were no errors. * @param {Message} message A message object representing the sent object. * @param {Object} [options] An object containing options for the message. * @param {Array/Boolean/String} [options.mentions=true] If an Array, it should be an array of User IDs. If a boolean, false will * notify no-one, and true will figure out who should be mentioned based on the message. If a String, should be a User * ID. * @param {Number} [options.selfDestruct=false] If specified, should be the amount of milliseconds at which the message should * delete itself after being sent. * @method sendMessage */ exports.Client.prototype.sendFile = function (destination, toSend, fileName, callback, options) { this.sendMessage(destination, toSend, callback, options, fileName); } exports.Client.prototype.sendMessage = function (destination, toSend, callback, options, fileName) { options = options || {}; callback = callback || function () { }; var channel_id, message, mentions, self = this; channel_id = resolveChannel(destination, self); if (!fileName) { message = resolveMessage(toSend); mentions = resolveMentions(message, options.mention); } if (channel_id) { send(); } else { //a channel is being sorted } function send() { if (fileName) { Internal.XHR.sendFile(self.token, channel_id, toSend, fileName, function (err) { callback(err); }); return; } Internal.XHR.sendMessage(self.token, channel_id, { content: message, mentions: mentions }, function (err, data) { if (err) { callback(err); } else { var msg = new Message(data, self.getChannel(data.channel_id)); if (options.selfDestruct) { setTimeout(function () { self.deleteMessage(msg); }, options.selfDestruct); } callback(null, msg); } }); } function setChannId(id) { channel_id = id; } function resolveChannel(destination, self) { var channel_id = false; if (destination instanceof Server) { channel_id = destination.getDefaultChannel().id; } else if (destination instanceof Channel) { channel_id = destination.id; } else if (destination instanceof PMChannel) { channel_id = destination.id; } else if (destination instanceof Message) { channel_id = destination.channel.id; } else if (destination instanceof User) { var destId = self.PMList.deepFilter(["user", "id"], destination.id, true); if (destId) { channel_id = destId.id; } else { //start a PM and then get that use that self.startPM(destination, function (err, channel) { if (err) { callback(err); } else { self.PMList.add(new PMChannel(channel.user, channel.id)); setChannId(channel.id); send(); } }); return false; } } else { channel_id = destination; } return channel_id; } function resolveMessage(toSend) { var message; if (typeof toSend === "string" || toSend instanceof String) message = toSend; else if (toSend instanceof Array) message = toSend.join("\n"); else if (toSend instanceof Message) message = toSend.content; else message = toSend; return message.substring(0, 2000); } function resolveMentions(message, mentionsOpt) { var mentions = []; if (mentionsOpt === false) { } else if (mentionsOpt || mentionsOpt === "auto" || mentionsOpt == null || mentionsOpt == undefined) { for (mention of (message.match(/<@[^>]*>/g) || [])) { mentions.push(mention.substring(2, mention.length - 1)); } } else if (mentionsOpt instanceof Array) { for (mention of mentionsOpt) { if (mention instanceof User) { mentions.push(mention.id); } else { mentions.push(mention); } } } return mentions; } } /** * Deletes the specified message if the bot has authority * @param {Message} message The message to delete * @param {Function} callback Called after the message deletion request is sent. * @param {Object} callback.error If there was an error, this would be an XHR Error object. Otherwise, it will be null. * @param {Message} callback.message A message object representing the deleted object. * @method deleteMessage */ exports.Client.prototype.deleteMessage = function (message, callback) { callback = callback || function () { }; var self = this; Internal.XHR.deleteMessage(self.token, message.channel.id, message.id, callback); } exports.Client.prototype.updateMessage = function (oldMessage, newContent, callback, options) { var self = this; var channel = oldMessage.channel; options = options || {}; Internal.XHR.updateMessage(self.token, channel.id, oldMessage.id, { content: newContent, mentions: [] }, function (err, data) { if (err) { callback(err); return; } var msg = new Message(data, self.getChannel(data.channel_id)); if (options.selfDestruct) { setTimeout(function () { self.deleteMessage(msg); }, options.selfDestruct); } callback(null, msg); }); } exports.Client.prototype.setUsername = function (username, callback) { var self = this; Internal.XHR.setUsername(self.token, self.user.avatar, self.email, null, self.password, username, function (err) { callback(err); }); } exports.Client.prototype.getChannelLogs = function (channel, amount, callback) { var self = this; Internal.XHR.getChannelLogs(self.token, channel.id, (amount || 50), function (err, data) { if (err) { callback(err); return; } var logs = new List("id"); for (message of data) { logs.add(new Message(message, channel)); } callback(null, logs); }); } exports.Client.prototype.createChannel = function (server, name, type, callback) { var self = this; Internal.XHR.createChannel(self.token, server.id, name, type, function (err, data) { if (err) { callback(err); } else { var chann = new Channel(data, server); server.channels.add(chann); callback(null, chann); } }); } exports.Client.prototype.deleteChannel = function (channel, callback) { var self = this; Internal.XHR.deleteChannel(self.token, channel.id, function (err) { channel.server.channels.removeElement(channel); self.triggerEvent("channelDelete", [channel]); callback(null); }); } exports.Client.prototype.deleteServer = function (server, callback) { var self = this; Internal.XHR.deleteServer(self.token, server.id, function (err) { self.serverList.removeElement(server); self.triggerEvent("serverDelete", [server]); callback(null); }); } exports.Client.prototype.joinServer = function (invite, callback) { var self = this; var code = (invite instanceof Invite ? invite.code : invite); Internal.XHR.acceptInvite(self.token, code, function (err, inviteData) { if (err) { callback(err); } else { serverCreateRequests[inviteData.guild.id] = callback; } }); } exports.Client.prototype.getServers = function () { return this.serverList; } exports.Client.prototype.getChannels = function () { return this.serverList.concatSublists("channels", "id"); } exports.Client.prototype.getUsers = function () { return this.getServers().concatSublists("members", "id"); } exports.Client.prototype.getServer = function (id) { return this.getServers().filter("id", id, true); } exports.Client.prototype.getChannel = function (id) { var normalChan = this.getChannels().filter("id", id, true); return normalChan || this.PMList.filter("id", id, true); } exports.Client.prototype.getChannelByName = function (name) { return this.getChannels().filter("name", name, true); } exports.Client.prototype.getUser = function (id) { return this.getUsers().filter("id", id, true); } exports.isUserID = function (id) { return ((id + "").length === 17 && !isNaN(id)); } exports.Client.prototype.updateUserReferences = function (id, oldUser, user) { if (oldUser.equalsStrict(user)) { return false; } for (server of this.serverList.contents) { server.members.updateElement(oldUser, user); } this.debug("Updated references to " + oldUser.username + " to " + this.getUser(id).username); return true; } exports.Client.prototype.addPM = function (pm) { this.PMList.add(pm); }