diff --git a/.gitignore b/.gitignore index 8b137891..360ea127 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ - +.vs diff --git a/README.md b/README.md index f81be44c..5843e698 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@

discord.io

+ A small, single-file, fully featured [Discordapp](https://discordapp.com) library for Node.js and browsers. [![Discord](https://discordapp.com/api/guilds/66192955777486848/widget.png)](https://discord.gg/0MvHMfHcTKVVmIGP) [![NPM](https://img.shields.io/npm/v/discord.io.svg)](https://img.shields.io/npm/v/gh-badges.svg) +**You are probably here for the gateway_v6 branch. (https://github.com/Woor/discord.io/tree/gateway_v6) ** + ### Requirements **Required**: * **Node.js 0.10.x** or greater diff --git a/lib/index.js b/lib/index.js index 45d682f9..7f4752d3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -73,21 +73,20 @@ var DCP = Discord.Client.prototype; /** * Manually initiate the WebSocket connection to Discord. */ -DCP.connect = function() { - if (!this.connected && !this._connecting) return setTimeout( - init, - Math.max(0, CONNECT_WHEN - Date.now()), - this, - arguments[0], - void( CONNECT_WHEN = Math.max(CONNECT_WHEN, Date.now()) + 6000 ) - ); +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) this._ws.close(); +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"); }; /** @@ -135,18 +134,20 @@ DCP.editUserInfo = function(input, callback) { /** * Change the client's presence. * @arg {Object} input - * @arg {Number|null} input.idle_since - Use a Number before the current point in time. + * @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 - Streaming activity, 0 for nothing, 1 for Twitch. + * @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 = 'online'); - this.presenceStatus = 'idle'; + if (payload.d.idle_since === null) return void(this.presenceStatus = payload.d.status); + this.presenceStatus = payload.d.status; }; /** @@ -180,13 +181,20 @@ DCP.getAccountSettings = function(callback) { 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 = this, multi = new Multipart(), message = generateMessage(input.message || ""), - isBuffer = (input.file instanceof Buffer), isString = (type(input.file) === 'string'); - - if (!isBuffer && !isString) return handleErrCB("[uploadFile] uploadFile requires a String or Buffer as the 'file' value", callback); - if (isBuffer) if (input.filename) file = input.file; else return handleErrCB("[uploadFile] uploadFile requires a 'filename' value to be set if using a Buffer", callback); - if (isString) try { file = FS.readFileSync(input.file); } catch(e) { return handleErrCB("[uploadFile] File does not exist: " + input.file, callback); } + 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], @@ -219,7 +227,7 @@ DCP.sendMessage = function(input, callback) { message.tts = (input.tts === true); message.nonce = input.nonce || message.nonce; - if (input.typing === true) { + if (typeof input.typing === "boolean" && input.typing === true) { return simulateTyping( this, input.to, @@ -227,6 +235,14 @@ DCP.sendMessage = function(input, callback) { ( (message.content.length * 0.12) * 1000 ), callback ); + } else if (typeof input.typing === "number") { + return simulateTyping( + this, + input.to, + message, + input.typing, + callback + ); } sendMessage(this, input.to, message, callback); @@ -285,7 +301,7 @@ DCP.getMessages = function(input, callback) { * @arg {Object} input * @arg {Snowflake} input.channelID * @arg {Snowflake} input.messageID - * @arg {Snowflake} input.message - The new message content + * @arg {String} input.message - The new message content * @arg {Object} [input.embed] - The new Discord Embed object */ DCP.editMessage = function(input, callback) { @@ -458,19 +474,27 @@ DCP.kick = function(input, callback) { * @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) { - if (input.lastDays) { - input.lastDays = Number(input.lastDays); - input.lastDays = Math.min(input.lastDays, 7); - input.lastDays = Math.max(input.lastDays, 1); + 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); } - this._req('put', Endpoints.BANS(input.serverID, input.userID) + (input.lastDays ? "?delete-message-days=" + input.lastDays : ""), function(err, res) { - handleResCB("Could not ban user", err, res, callback); - }); -}; + 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. @@ -522,7 +546,7 @@ DCP.unmute = function(input, callback) { }; /** - * Server-deafan a user. + * Server-deafen a user. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID @@ -534,7 +558,7 @@ DCP.deafen = function(input, callback) { }; /** - * Remove the server-deafan from a user. + * Remove the server-deafen from a user. * @arg {Object} input * @arg {Snowflake} input.serverID * @arg {Snowflake} input.userID @@ -545,6 +569,82 @@ DCP.undeafen = function(input, 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*/ /** @@ -720,38 +820,16 @@ DCP.transferOwnership = function(input, callback) { }; /** - * Accept an invite to a server [User Only] - * @arg {String} inviteCode - The code part of an invite URL (e.g. 0MvHMfHcTKVVmIGP) + * (Used to) Accept an invite to a server [User Only]. Can no longer be used. + * @deprecated */ -DCP.acceptInvite = function(inviteCode, callback) { - if (this.bot) return handleErrCB("[acceptInvite] This account is a 'bot' type account, and cannot use 'acceptInvite'. Please use the client's inviteURL property instead.", callback); - var client = this, joinedServers = Object.keys(client.servers); - this._req('post', Endpoints.INVITES(inviteCode), function(err, res) { - try { - //Try to create the server with the small amount of data - //that Discord provides directly from the HTTP response - //since the websocket event may take a second to show. - if (!client.servers[res.body.guild.id]) { - client.servers[res.body.guild.id] = res.body.guild; - client.servers[res.body.guild.id].channels = {}; - client.servers[res.body.guild.id].channels[res.body.channel.id] = res.body.channel; - } else { - if (joinedServers.indexOf(res.body.guild.id) > -1) { - return handleErrCB(("Already joined server: " + res.body.guild.id), callback); - } - } - } catch(e) {} - handleResCB(("The invite code provided " + inviteCode + " is incorrect."), err, res, callback); - }); +DCP.acceptInvite = function(NUL, callback) { + return handleErrCB("acceptInvite can no longer be used", callback); }; /** - * Generate an invite URL for a channel. - * @arg {Object} input - * @arg {Snowflake} input.channelID - * @arg {Number} [input.max_age] - Time in seconds. - * @arg {Number} [input.max_users] - The amount of times the invite code can be used. - * @arg {Boolean} [input.temporary] - Any users who use this invite will be removed when they disconnect, unless given a role. + * (Used to) Generate an invite URL for a channel. + * @deprecated */ DCP.createInvite = function(input, callback) { var payload, client = this; @@ -850,7 +928,7 @@ DCP.createChannel = function(input, callback) { 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; + if (!err && goodResponse(res)) client._uIDToDM[res.body.recipient_id] = res.body.id; handleResCB("Unable to create DM Channel", err, res, callback); }); }; @@ -912,12 +990,12 @@ DCP.editChannelInfo = function(input, callback) { /** * Edit (or creates) a permission override for a channel. * @arg {Object} input - * @arg {Snowflake} channelID - * @arg {Snowflake} [userID] - * @arg {Snowflake} [roleID] - * @arg {Array} allow - An array of permissions to allow. Discord.Permissions.XXXXXX. - * @arg {Array} deny - An array of permissions to deny, same as above. - * @arg {Array} default - An array of permissions that cancels out allowed and denied permissions. + * @arg {Snowflake} input.channelID + * @arg {Snowflake} [input.userID] + * @arg {Snowflake} [input.roleID] + * @arg {Array} input.allow - An array of permissions to allow. Discord.Permissions.XXXXXX. + * @arg {Array} input.deny - An array of permissions to deny, same as above. + * @arg {Array} 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; @@ -930,8 +1008,8 @@ DCP.editChannelPermissions = function(input, callback) { //Will shrink this up l 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] )); + [6, 10, 11, 12, 13, 14, 15, 16, 17, 18] : + [20, 21, 22, 23, 24, 25, 29] )); //Take care of allow first if (type(input.allow) === 'array') { @@ -977,9 +1055,9 @@ DCP.editChannelPermissions = function(input, callback) { //Will shrink this up l /** * Delete a permission override for a channel. * @arg {Object} input - * @arg {Snowflake} channelID - * @arg {Snowflake} [userID] - * @arg {Snowflake} [roleID] + * @arg {Snowflake} input.channelID + * @arg {Snowflake} [input.userID] + * @arg {Snowflake} [input.roleID] */ DCP.deleteChannelPermission = function(input, callback) { var payload, pType, ID; @@ -1059,7 +1137,52 @@ DCP.editRole = function(input, callback) { }); } catch(e) {return handleErrCB(('[editRole] ' + e), callback);} }; + +/** + * Move a role up or down relative to it's current position. + * @arg {Object} input + * @arg {Snowflake} input.serverID + * @arg {Snowflake} input.roleID - The ID of the role. + * @arg {Number} input.position - A relative number to move the role up or down. + */ +DCP.moveRole = function(input,callback){ + if(input.position===0)return handleErrCB("Desired role position is same as current", callback); //Don't do anything if they dont want it to move + try { + var role = new Role(this.servers[input.serverID].roles[input.roleID]); + var curPos = role.position; + var newPos = curPos + input.position; + + if(newPos < 1) + newPos = 1; //make sure it doesn't go under the possible positions + + if(newPos > Object.keys(this.servers[input.serverID].roles).length-1) + newPos = Object.keys(this.servers[input.serverID].roles).length-1; //make sure it doesn't go above the possible positions + + var currentOrder = []; + for(var roleID in this.servers[input.serverID].roles){ + if(roleID == input.serverID) continue; //the everyone role cannot be changed, so best to not try. + var _role = new Role(this.servers[input.serverID].roles[roleID]); + currentOrder[_role.position] = roleID; + } + var payload = []; + + + for(var pos in currentOrder){ + var roleID = currentOrder[pos]; + if(newPos>curPos && curPos < pos && pos <= newPos) + payload.push({id:roleID, position:pos-1}); + if(curPos>newPos && curPos > pos && pos >= newPos) + payload.push({id:roleID, position:(pos-(-1))}); + } + payload.push({id:input.roleID,position:newPos}); + + this._req('patch', Endpoints.ROLES(input.serverID), payload, function(err, res) { + handleResCB("Unable to move role", err, res, callback); + }); + } catch(e) {return handleErrCB(e, callback);} +}; + /** * Delete a role. * @arg {Object} input @@ -1237,8 +1360,10 @@ DCP.joinVoiceChannel = function(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)); + return send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, server.self_deaf)); }; /** @@ -1393,7 +1518,7 @@ function APIRequest(method, url) { }); }); }); - if (type(data) === 'object' || method.toLowerCase() === 'get') req.setHeader("Content-Type", "application/json; charset=utf-8"); + if (typeof(data) == 'object' || method.toLowerCase() === 'get') req.setHeader("Content-Type", "application/json; charset=utf-8"); if (data instanceof Multipart) req.setHeader("Content-Type", "multipart/form-data; boundary=" + data.boundary); if (data) req.write( data.result || JSON.stringify(data), data.result ? 'binary' : 'utf-8' ); req.end(); @@ -1414,9 +1539,9 @@ function APIRequest(method, url) { callback(null, req); } }; - if (type(data) === 'object' || method.toLowerCase() === 'get') req.setRequestHeader("Content-Type", "application/json; charset=utf-8"); + if (typeof(data) == 'object' || method.toLowerCase() === 'get') req.setRequestHeader("Content-Type", "application/json; charset=utf-8"); if (data instanceof Multipart) req.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + data.boundary); - if (data) return req[ (data.result ? "sendAsBinary" : "send") ]( data.result ? data.result : JSON.stringify(data) ); + if (data) return req.send( data.result ? data.result : JSON.stringify(data) ); req.send(null); } function send(ws, data) { @@ -1503,6 +1628,17 @@ function colorFromRole(server, member) { return role.position > array[0] && role.color ? [role.position, role.color] : array; }, [-1, null])[1]; } +function log(client, level_message) { + var level, message; + if (arguments.length === 2) { + level = Discord.LogLevels.Info; + message = level_message; + } else { + level = level_message; + message = arguments[2]; + } + return client.emit('log', level, message); +} function givePermission(bit, permissions) { return permissions | (1 << bit); @@ -1649,10 +1785,21 @@ function handleWSMessage(data, flags) { var message = decompressWSMessage(data, flags); var _data = message.d; var client = this, user, server, member, old, - userID, serverID, channelID, currentVCID; - client.internals.sequence = message.s; + userID, serverID, channelID, currentVCID, + mute, deaf, self_mute, self_deaf; switch (message.op) { + case 0: + //Event dispatched - update our sequence number + client.internals.sequence = message.s; + break; + case 9: + //Disconnect after a Invalid Session + //Disconnect the client if the client has received an invalid session id + delete client.internals.sequence; + delete client.internals.sessionID; + client._ws && client._ws.close(1e3, 'Received an invalid session id'); + break; case 10: //Start keep-alive interval //Disconnect the client if no ping has been received @@ -1663,7 +1810,9 @@ function handleWSMessage(data, flags) { client.connected = true; client._mainKeepAlive = setInterval(function() { - client.internals.heartbeat = setTimeout(client._ws.close.bind(client._ws, 1e3, 'No heartbeat received'), 15e3); + client.internals.heartbeat = setTimeout(function() { + client._ws && client._ws.close(1e3, 'No heartbeat received'); + }, 15e3); client.internals._lastHB = Date.now(); send(client._ws, Payloads.HEARTBEAT(client)); }, _data.heartbeat_interval); @@ -1814,28 +1963,39 @@ function handleWSMessage(data, flags) { channelID = _data.channel_id; userID = _data.user_id; server = client.servers[serverID]; + mute = !!_data.mute; + self_mute = !!_data.self_mute; + deaf = !!_data.deaf; + self_deaf = !!_data.self_deaf; try { currentVCID = server.members[userID].voice_channel_id; if (currentVCID) delete( server.channels[currentVCID].members[userID] ); if (channelID) server.channels[channelID].members[userID] = _data; + server.members[userID].mute = (mute || self_mute); + server.members[userID].deaf = (deaf || self_deaf); server.members[userID].voice_channel_id = channelID; } catch(e) {} if (userID === client.id) { + server.self_mute = self_mute; + server.self_deaf = self_deaf; if (channelID === null) { if (server.voiceSession) leaveVoiceChannel(client, server.voiceSession.channelID); - return void(server.voiceSession = null); - } - if (!server.voiceSession) { - server.voiceSession = getVoiceSession(client, channelID, server); - } - if (channelID !== server.voiceSession.channelID) { - delete( client._vChannels[server.voiceSession.channelID] ); - getVoiceSession(client, channelID, server).channelID = channelID; - } + server.voiceSession = null; + } else { + if (!server.voiceSession) { + server.voiceSession = getVoiceSession(client, channelID, server); + } + if (channelID !== server.voiceSession.channelID) { + delete( client._vChannels[server.voiceSession.channelID] ); + getVoiceSession(client, channelID, server).channelID = channelID; + } - server.voiceSession.session = _data.session_id; + server.voiceSession.session = _data.session_id; + server.voiceSession.self_mute = self_mute; + server.voiceSession.self_deaf = self_deaf; + } } break; case "VOICE_SERVER_UPDATE": @@ -1947,7 +2107,7 @@ function leaveVoiceChannel(client, channelID, callback) { client._vChannels[channelID].udp.connection.close(); } catch(e) {} - send(client._ws, Payloads.UPDATE_VOICE(client.channels[channelID].guild_id, null)); + send(client._ws, Payloads.UPDATE_VOICE(client.channels[channelID].guild_id, null, false, false)); return call(callback, [null]); } @@ -1974,13 +2134,15 @@ function getVoiceSession(client, channelID, server) { channelID: channelID, token: null, session: null, - endpoint: null + endpoint: null, + self_mute: false, + self_deaf: false }; } function checkVoiceReady(voiceSession, callback) { return setTimeout(function() { - if (voiceSession.error) return call(callback, [error]); + if (voiceSession.error) return call(callback, [voiceSession.error]); if (voiceSession.joined) return call(callback, [null, voiceSession.emitter]); return checkVoiceReady(voiceSession, callback); }, 1); @@ -2082,7 +2244,7 @@ function AudioCB(voiceSession, audioChannels, encoder, maxStreamSize) { ["_secretKey", new Uint8Array(voiceSession.secretKey)], ["_mixedDecoder", Opus && new Opus.OpusEncoder( 48000, audioChannels ) || null] ]); - + createAudioEncoder(this, encoder); this._write = function _write(chunk, encoding, callback) { @@ -2251,6 +2413,7 @@ function prepareAudioOld(ACBI, readableStream) { function sendAudio(ACBI, buffer) { ACBI._sequence = (ACBI._sequence + 1 ) < 0xFFFF ? ACBI._sequence + 1 : 0; ACBI._timestamp = (ACBI._timestamp + 960) < 0xFFFFFFFF ? ACBI._timestamp + 960 : 0; + if (ACBI._voiceSession.self_mute) return; var audioPacket = AudioCB.VoicePacket(buffer, ACBI._voiceSession.ws.ssrc, ACBI._sequence, ACBI._timestamp, ACBI._secretKey); try { @@ -2261,7 +2424,7 @@ function sendAudio(ACBI, buffer) { function handleIncomingAudio(msg) { //The response from the UDP keep alive ping - if (msg.length === 8) return; + if (msg.length === 8 || this._voiceSession.self_deaf) return; var header = msg.slice(0, 12), audio = msg.slice(12), @@ -2368,6 +2531,8 @@ function Server(client, data) { copyKeys(data, this); this.large = this.large || this.member_count > LARGE_THRESHOLD; this.voiceSession = null; + this.self_mute = !!this.self_mute; + this.self_deaf = !!this.self_deaf; if (data.unavailable) return; //Objects so we can use direct property accessing without for loops @@ -2539,10 +2704,10 @@ Object.defineProperty(Role.prototype, "permission_values", { enumerable: true }); Object.defineProperty(User.prototype, 'avatarURL', { - get: function() { + get: function() { if (!this.avatar) return null; var animated = this.avatar.indexOf("a_") > -1; - return Discord.Endpoints.CDN + "/avatars/" + this.id + "/" + this.avatar + (animated ? ".gif" : ".jpg"); + return Discord.Endpoints.CDN + "/avatars/" + this.id + "/" + this.avatar + (animated ? ".gif" : ".webp"); }, set: function() {} }); @@ -2619,6 +2784,12 @@ Discord.Permissions = { VOICE_MOVE_MEMBERS: 24, VOICE_USE_VAD: 25, }; +Discord.LogLevels = { + Verbose: 0, + Info: 1, + Warn: 2, + Error: 3 +}; Object.keys(Discord.Permissions).forEach(function(pn) { Object.defineProperty(Role.prototype, pn, { @@ -2814,25 +2985,27 @@ function Websocket(url, opts) { return { op: 3, d: { - idle_since: input.idle_since || null, + status: type(input.idle_since) === 'number' ? 'idle' : input.status !== undefined ? input.status : null, + afk: !!input.afk, + since: type(input.idle_since) === 'number' || input.status === 'idle' ? Date.now() : null, game: type(input.game) === 'object' ? { name: input.game.name ? String(input.game.name) : null, - type: input.game.type ? Number(input.game.type) : null, + type: input.game.type ? Number(input.game.type) : 0, url: input.game.url ? String(input.game.url) : null } : null } }; }, - UPDATE_VOICE: function(serverID, channelID) { + UPDATE_VOICE: function(serverID, channelID, self_mute, self_deaf) { return { op: 4, d: { guild_id: serverID, channel_id: channelID, - self_mute: false, - self_deaf: false + self_mute: self_mute, + self_deaf: self_deaf } }; }, diff --git a/package.json b/package.json index c2ff8452..c1176136 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord.io", - "version": "2.5.1", + "version": "2.5.3", "description": "JavaScript interface for Discord.", "main": "./lib/index.js", "typings": "./typings/index.d.ts", diff --git a/typings/index.d.ts b/typings/index.d.ts index 5f1b000e..e1860d9b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -97,7 +97,9 @@ declare interface permissions { GENERAL_MANAGE_ROLES?: boolean; GENERAL_MANAGE_NICKNAMES?: boolean; GENERAL_CHANGE_NICKNAME?: boolean; + GENERAL_MANAGE_WEBHOOKS?: boolean; + TEXT_ADD_REACTIONS?: boolean; TEXT_READ_MESSAGES?: boolean; TEXT_SEND_MESSAGES?: boolean; TEXT_SEND_TTS_MESSAGE?: boolean; @@ -120,15 +122,41 @@ declare interface permissions { */ declare type sendMessageOpts = { to: string, - message: string, + message?: string, tts?: boolean, nonce?: string, - typing?: boolean + typing?: boolean, + embed?: embedMessageOpts +} + +declare type embedMessageOpts = { + author?: { + icon_url?: string, + name: string, + url?: string + }, + color?: number, + description?: string, + fields?: [{ + name: string, + value?: string, + inline?: boolean + }], + thumbnail?: { + url: string + }, + title: string, + timestamp?: Date + url?: string, + footer?: { + icon_url?: string, + text: string + } } declare type uploadFileOpts = { to: string, - file: string, + file: string|Buffer, filename?: string, message?: string } @@ -325,6 +353,12 @@ declare type getMembersOpts = { after: string } +declare type reactionOpts = { + channelID: string, + messageID: string, + reaction: string +} + /** * CLASSES */ @@ -373,11 +407,18 @@ declare namespace Discord { } export class DMChannel extends Resource { - recipient: Object; + recipient: DMRecipient; last_message_id: string; id: string; } + export class DMRecipient extends Resource { + username: string; + id: string; + discriminator: string; + avatar: string; + } + export class User extends Resource { username: string; id: string; @@ -506,7 +547,7 @@ declare namespace Discord { pinMessage(options: pinMessageOpts, callback?: callbackFunc): void deletePinnedMessage(options: deletePinnedMessageOpts, callback?: callbackFunc): void getPinnedMessages(options: getPinnedMessagesOpts, callback?: callbackFunc): void - + addReaction(options: reactionOpts, callback?: callbackFunc): void /** * VOICE CHANNELS */