· 7 years ago · May 06, 2018, 05:24 PM
1"use strict";
2(function discordio(Discord){
3var isNode = typeof(window) === "undefined" && typeof(navigator) === "undefined";
4var CURRENT_VERSION = "2.x.x",
5 GATEWAY_VERSION = 5,
6 LARGE_THRESHOLD = 250,
7 CONNECT_WHEN = null,
8 Endpoints, Payloads;
9
10if (isNode) {
11 var Util = require('util'),
12 FS = require('fs'),
13 UDP = require('dgram'),
14 Zlib = require('zlib'),
15 DNS = require('dns'),
16 Stream = require('stream'),
17 BN = require('path').basename,
18 EE = require('events').EventEmitter,
19 requesters = {
20 http: require('http'),
21 https: require('https')
22 },
23 ChildProc = require('child_process'),
24 URL = require('url'),
25 //NPM Modules
26 NACL = require('tweetnacl'),
27 Opus = null;
28}
29
30/* --- Version Check --- */
31try {
32 CURRENT_VERSION = require('../package.json').version;
33} catch(e) {}
34if (!isNode) CURRENT_VERSION = CURRENT_VERSION + "-browser";
35
36/**
37 * Discord Client constructor
38 * @class
39 * @arg {Object} options
40 * @arg {String} options.token - The token of the account you wish to log in with.
41 * @arg {Boolean} [options.autorun] - If true, the client runs when constructed without calling `.connect()`.
42 * @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.
43 * @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.
44 */
45Discord.Client = function DiscordClient(options) {
46 if (!isNode) Emitter.call(this);
47 if (!options || options.constructor.name !== 'Object') return console.error("An Object is required to create the discord.io client.");
48
49 applyProperties(this, [
50 ["_ws", null],
51 ["_uIDToDM", {}],
52 ["_ready", false],
53 ["_vChannels", {}],
54 ["_messageCache", {}],
55 ["_connecting", false],
56 ["_mainKeepAlive", null],
57 ["_req", APIRequest.bind(this)],
58 ["_shard", validateShard(options.shard)],
59 ["_messageCacheLimit", typeof(options.messageCacheLimit) === 'number' ? options.messageCacheLimit : 50],
60 ]);
61
62 this.presenceStatus = "offline";
63 this.connected = false;
64 this.inviteURL = null;
65 this.connect = this.connect.bind(this, options);
66
67 if (options.autorun === true) this.connect();
68};
69if (isNode) Emitter.call(Discord.Client);
70
71/* - DiscordClient - Methods - */
72var DCP = Discord.Client.prototype;
73/**
74 * Manually initiate the WebSocket connection to Discord.
75 */
76DCP.connect = function () {
77 var opts = arguments[0];
78 if (!this.connected && !this._connecting) return setTimeout(function() {
79 init(this, opts);
80 CONNECT_WHEN = Math.max(CONNECT_WHEN, Date.now()) + 6000;
81 }.bind(this), Math.max( 0, CONNECT_WHEN - Date.now() ));
82};
83
84/**
85 * Disconnect the WebSocket connection to Discord.
86 */
87DCP.disconnect = function () {
88 if (this._ws) return this._ws.close(), log(this, "Manual disconnect called, websocket closed");
89 return log(this, Discord.LogLevels.Warn, "Manual disconnect called with no WebSocket active, ignored");
90};
91
92/**
93 * Retrieve a user object from Discord, Bot only endpoint. You don't have to share a server with this user.
94 * @arg {Object} input
95 * @arg {Snowflake} input.userID
96 */
97DCP.getUser = function(input, callback) {
98 if (!this.bot) return handleErrCB("[getUser] This account is a 'user' type account, and cannot use 'getUser'. Only bots can use this endpoint.", callback);
99 this._req('get', Endpoints.USER(input.userID), function(err, res) {
100 handleResCB("Could not get user", err, res, callback);
101 });
102};
103
104/**
105 * Edit the client's user information.
106 * @arg {Object} input
107 * @arg {String<Base64>} input.avatar - The last part of a Base64 Data URI. `fs.readFileSync('image.jpg', 'base64')` is enough.
108 * @arg {String} input.username - A username.
109 * @arg {String} input.email - [User only] An email.
110 * @arg {String} input.password - [User only] Your current password.
111 * @arg {String} input.new_password - [User only] A new password.
112 */
113DCP.editUserInfo = function(input, callback) {
114 var payload = {
115 avatar: this.avatar,
116 email: this.email,
117 new_password: null,
118 password: null,
119 username: this.username
120 },
121 plArr = Object.keys(payload);
122
123 for (var key in input) {
124 if (plArr.indexOf(key) < 0) return handleErrCB(("[editUserInfo] '" + key + "' is not a valid key. Valid keys are: " + plArr.join(", ")), callback);
125 payload[key] = input[key];
126 }
127 if (input.avatar) payload.avatar = "data:image/jpg;base64," + input.avatar;
128
129 this._req('patch', Endpoints.ME, payload, function(err, res) {
130 handleResCB("Unable to edit user information", err, res, callback);
131 });
132};
133
134/**
135 * Change the client's presence.
136 * @arg {Object} input
137 * @arg {String|null} input.status - Used to set the status. online, idle, dnd, invisible, and offline are the possible states.
138 * @arg {Number|null} input.idle_since - Optional, use a Number before the current point in time.
139 * @arg {Boolean|null} input.afk - Optional, changes how Discord handles push notifications.
140 * @arg {Object|null} input.game - Used to set game information.
141 * @arg {String|null} input.game.name - The name of the game.
142 * @arg {Number|null} input.game.type - Activity type, 0 for game, 1 for Twitch.
143 * @arg {String|null} input.game.url - A URL matching the streaming service you've selected.
144 */
145DCP.setPresence = function(input) {
146 var payload = Payloads.STATUS(input);
147 send(this._ws, payload);
148
149 if (payload.d.idle_since === null) return void(this.presenceStatus = payload.d.status);
150 this.presenceStatus = payload.d.status;
151};
152
153/**
154 * Receive OAuth information for the current client.
155 */
156DCP.getOauthInfo = function(callback) {
157 this._req('get', Endpoints.OAUTH, function(err, res) {
158 handleResCB("Error GETing OAuth information", err, res, callback);
159 });
160};
161
162/**
163 * Receive account settings information for the current client.
164 */
165DCP.getAccountSettings = function(callback) {
166 this._req('get', Endpoints.SETTINGS, function(err, res) {
167 handleResCB("Error GETing client settings", err, res, callback);
168 });
169};
170
171/* - DiscordClient - Methods - Content - */
172
173/**
174 * Upload a file to a channel.
175 * @arg {Object} input
176 * @arg {Snowflake} input.to - The target Channel or User ID.
177 * @arg {Buffer|String} input.file - A Buffer containing the file data, or a String that's a path to the file.
178 * @arg {String|null} input.filename - A filename for the uploaded file, required if you provide a Buffer.
179 * @arg {String|null} input.message - An optional message to provide.
180 */
181DCP.uploadFile = function(input, callback) {
182 /* After like 15 minutes of fighting with Request, turns out Discord doesn't allow multiple files in one message...
183 despite having an attachments array.*/
184 var file, client, multi, message, isBuffer, isString;
185
186 client = this;
187 multi = new Multipart();
188 message = generateMessage(input.message || "");
189 isBuffer = (input.file instanceof Buffer);
190 isString = (type(input.file) === 'string');
191
192 if (!isBuffer && !isString) return handleErrCB("uploadFile requires a String or Buffer as the 'file' value", callback);
193 if (isBuffer) {
194 if (!input.filename) return handleErrCB("uploadFile requires a 'filename' value to be set if using a Buffer", callback);
195 file = input.file;
196 }
197 if (isString) try { file = FS.readFileSync(input.file); } catch(e) { return handleErrCB("File does not exist: " + input.file, callback); }
198
199 [
200 ["content", message.content],
201 ["mentions", ""],
202 ["tts", false],
203 ["nonce", message.nonce],
204 ["file", file, input.filename || BN(input.file)]
205 ].forEach(multi.append, multi);
206 multi.finalize();
207
208 resolveID(client, input.to, function(channelID) {
209 client._req('post', Endpoints.MESSAGES(channelID), multi, function(err, res) {
210 handleResCB("Unable to upload file", err, res, callback);
211 });
212 });
213};
214
215/**
216 * Send a message to a channel.
217 * @arg {Object} input
218 * @arg {Snowflake} input.to - The target Channel or User ID.
219 * @arg {String} input.message - The message content.
220 * @arg {Object} [input.embed] - An embed object to include
221 * @arg {Boolean} [input.tts] - Enable Text-to-Speech for this message.
222 * @arg {Number} [input.nonce] - Number-used-only-ONCE. The Discord client uses this to change the message color from grey to white.
223 * @arg {Boolean} [input.typing] - Indicates whether the message should be sent with simulated typing. Based on message length.
224 */
225DCP.sendMessage = function(input, callback) {
226 var message = generateMessage(input.message || '', input.embed);
227 message.tts = (input.tts === true);
228 message.nonce = input.nonce || message.nonce;
229
230 if (input.typing === true) {
231 return simulateTyping(
232 this,
233 input.to,
234 message,
235 ( (message.content.length * 0.12) * 1000 ),
236 callback
237 );
238 }
239
240 sendMessage(this, input.to, message, callback);
241};
242
243/**
244 * Pull a message object from Discord.
245 * @arg {Object} input
246 * @arg {Snowflake} input.channelID - The channel ID that the message is from.
247 * @arg {Snowflake} input.messageID - The ID of the message.
248 */
249DCP.getMessage = function(input, callback) {
250 this._req('get', Endpoints.MESSAGES(input.channelID, input.messageID), function(err, res) {
251 handleResCB("Unable to get message", err, res, callback);
252 });
253};
254
255/**
256 * Pull an array of message objects from Discord.
257 * @arg {Object} input
258 * @arg {Snowflake} input.channelID - The channel ID to pull the messages from.
259 * @arg {Number} [input.limit] - How many messages to pull, defaults to 50.
260 * @arg {Snowflake} [input.before] - Pull messages before this message ID.
261 * @arg {Snowflake} [input.after] - Pull messages after this message ID.
262 */
263DCP.getMessages = function(input, callback) {
264 var client = this, qs = {}, messages = [], lastMessageID = "";
265 var total = typeof(input.limit) !== 'number' ? 50 : input.limit;
266
267 if (input.before) qs.before = input.before;
268 if (input.after) qs.after = input.after;
269
270 (function getMessages() {
271 if (total > 100) {
272 qs.limit = 100;
273 total = total - 100;
274 } else {
275 qs.limit = total;
276 }
277
278 if (messages.length >= input.limit) return call(callback, [null, messages]);
279
280 client._req('get', Endpoints.MESSAGES(input.channelID) + qstringify(qs), function(err, res) {
281 if (err) return handleErrCB("Unable to get messages", callback);
282 messages = messages.concat(res.body);
283 lastMessageID = messages[messages.length - 1] && messages[messages.length - 1].id;
284 if (lastMessageID) qs.before = lastMessageID;
285 if (!res.body.length < qs.limit) return call(callback, [null, messages]);
286 return setTimeout(getMessages, 1000);
287 });
288 })();
289};
290
291/**
292 * Edit a previously sent message.
293 * @arg {Object} input
294 * @arg {Snowflake} input.channelID
295 * @arg {Snowflake} input.messageID
296 * @arg {String} input.message - The new message content
297 * @arg {Object} [input.embed] - The new Discord Embed object
298 */
299DCP.editMessage = function(input, callback) {
300 this._req('patch', Endpoints.MESSAGES(input.channelID, input.messageID), generateMessage(input.message || '', input.embed), function(err, res) {
301 handleResCB("Unable to edit message", err, res, callback);
302 });
303};
304
305/**
306 * Delete a posted message.
307 * @arg {Object} input
308 * @arg {Snowflake} input.channelID
309 * @arg {Snowflake} input.messageID
310 */
311DCP.deleteMessage = function(input, callback) {
312 this._req('delete', Endpoints.MESSAGES(input.channelID, input.messageID), function(err, res) {
313 handleResCB("Unable to delete message", err, res, callback);
314 });
315};
316
317/**
318 * Delete a batch of messages.
319 * @arg {Object} input
320 * @arg {Snowflake} input.channelID
321 * @arg {Array<Snowflake>} input.messageIDs - An Array of message IDs, with a maximum of 100 indexes.
322 */
323DCP.deleteMessages = function(input, callback) {
324 this._req('post', Endpoints.BULK_DELETE(input.channelID), {messages: input.messageIDs.slice(0, 100)}, function(err, res) {
325 handleResCB("Unable to delete messages", err, res, callback);
326 });
327};
328
329/**
330 * Pin a message to the channel.
331 * @arg {Object} input
332 * @arg {Snowflake} input.channelID
333 * @arg {Snowflake} input.messageID
334 */
335DCP.pinMessage = function(input, callback) {
336 this._req('put', Endpoints.PINNED_MESSAGES(input.channelID, input.messageID), function(err, res) {
337 handleResCB("Unable to pin message", err, res, callback);
338 });
339};
340
341/**
342 * Get an array of pinned messages from a channel.
343 * @arg {Object} input
344 * @arg {Snowflake} input.channelID
345 */
346DCP.getPinnedMessages = function(input, callback) {
347 this._req('get', Endpoints.PINNED_MESSAGES(input.channelID), function(err, res) {
348 handleResCB("Unable to get pinned messages", err, res, callback);
349 });
350};
351
352/**
353 * Delete a pinned message from a channel.
354 * @arg {Object} input
355 * @arg {Snowflake} input.channelID
356 * @arg {Snowflake} input.messageID
357 */
358DCP.deletePinnedMessage = function(input, callback) {
359 this._req('delete', Endpoints.PINNED_MESSAGES(input.channelID, input.messageID), function(err, res) {
360 handleResCB("Unable to delete pinned message", err, res, callback);
361 });
362};
363
364/**
365 * Send 'typing...' status to a channel
366 * @arg {Snowflake} channelID
367 */
368DCP.simulateTyping = function(channelID, callback) {
369 this._req('post', Endpoints.TYPING(channelID), function(err, res) {
370 handleResCB("Unable to simulate typing", err, res, callback);
371 });
372};
373
374/**
375 * Replace Snowflakes with the names if applicable.
376 * @arg {String} message - The message to fix.
377 */
378DCP.fixMessage = function(message) {
379 var client = this;
380 return message.replace(/<@&(\d*)>|<@!(\d*)>|<@(\d*)>|<#(\d*)>/g, function(match, RID, NID, UID, CID) {
381 var k, i;
382 if (UID || CID) {
383 if (client.users[UID]) return "@" + client.users[UID].username;
384 if (client.channels[CID]) return "#" + client.channels[CID].name;
385 }
386 if (RID || NID) {
387 k = Object.keys(client.servers);
388 for (i=0; i<k.length; i++) {
389 if (client.servers[k[i]].roles[RID]) return "@" + client.servers[k[i]].roles[RID].name;
390 if (client.servers[k[i]].members[NID]) return "@" + client.servers[k[i]].members[NID].nick;
391 }
392 }
393 });
394};
395
396/**
397 * Add an emoji reaction to a message.
398 * @arg {Object} input
399 * @arg {Snowflake} input.channelID
400 * @arg {Snowflake} input.messageID
401 * @arg {String} input.reaction - Either the emoji unicode or the emoji name:id/object.
402 */
403DCP.addReaction = function(input, callback) {
404 this._req('put', Endpoints.USER_REACTIONS(input.channelID, input.messageID, stringifyEmoji(input.reaction)), function(err, res) {
405 handleResCB("Unable to add reaction", err, res, callback);
406 });
407};
408
409/**
410 * Get an emoji reaction of a message.
411 * @arg {Object} input
412 * @arg {Snowflake} input.channelID
413 * @arg {Snowflake} input.messageID
414 * @arg {String} input.reaction - Either the emoji unicode or the emoji name:id/object.
415 * @arg {String} [input.limit]
416 */
417DCP.getReaction = function(input, callback) {
418 var qs = { limit: (typeof(input.limit) !== 'number' ? 100 : input.limit) };
419 this._req('get', Endpoints.MESSAGE_REACTIONS(input.channelID, input.messageID, stringifyEmoji(input.reaction)) + qstringify(qs), function(err, res) {
420 handleResCB("Unable to get reaction", err, res, callback);
421 });
422};
423
424/**
425 * Remove an emoji reaction from a message.
426 * @arg {Object} input
427 * @arg {Snowflake} input.channelID
428 * @arg {Snowflake} input.messageID
429 * @arg {Snowflake} [input.userID]
430 * @arg {String} input.reaction - Either the emoji unicode or the emoji name:id/object.
431 */
432DCP.removeReaction = function(input, callback) {
433 this._req('delete', Endpoints.USER_REACTIONS(input.channelID, input.messageID, stringifyEmoji(input.reaction), input.userID), function(err, res) {
434 handleResCB("Unable to remove reaction", err, res, callback);
435 });
436};
437
438/**
439 * Remove all emoji reactions from a message.
440 * @arg {Object} input
441 * @arg {Snowflake} input.channelID
442 * @arg {Snowflake} input.messageID
443 */
444DCP.removeAllReactions = function(input, callback) {
445 this._req('delete', Endpoints.MESSAGE_REACTIONS(input.channelID, input.messageID), function(err, res) {
446 handleResCB("Unable to remove reactions", err, res, callback);
447 });
448};
449
450/* - DiscordClient - Methods - Server Management - */
451
452/**
453 * Remove a user from a server.
454 * @arg {Object} input
455 * @arg {Snowflake} input.serverID
456 * @arg {Snowflake} input.userID
457 */
458DCP.kick = function(input, callback) {
459 this._req('delete', Endpoints.MEMBERS(input.serverID, input.userID), function(err, res) {
460 handleResCB("Could not kick user", err, res, callback);
461 });
462};
463
464/**
465 * Remove and ban a user from a server.
466 * @arg {Object} input
467 * @arg {Snowflake} input.serverID
468 * @arg {Snowflake} input.userID
469 * @arg {String} input.reason
470 * @arg {Number} [input.lastDays] - Removes their messages up until this point, either 1 or 7 days.
471 */
472DCP.ban = function(input, callback) {
473 var url = Endpoints.BANS(input.serverID, input.userID);
474 var opts = {};
475
476 if (input.lastDays) {
477 opts.lastDays = Number(input.lastDays);
478 opts.lastDays = Math.min(opts.lastDays, 7);
479 opts.lastDays = Math.max(opts.lastDays, 1);
480 }
481
482 if (input.reason) opts.reason = input.reason;
483
484 url += qstringify(opts);
485
486 this._req('put', url, function(err, res) {
487 handleResCB("Could not ban user", err, res, callback);
488 });
489}
490
491/**
492 * Unban a user from a server.
493 * @arg {Object} input
494 * @arg {Snowflake} input.serverID
495 * @arg {Snowflake} input.userID
496 */
497DCP.unban = function(input, callback) {
498 this._req('delete', Endpoints.BANS(input.serverID, input.userID), function(err, res) {
499 handleResCB("Could not unban user", err, res, callback);
500 });
501};
502
503/**
504 * Move a user between voice channels.
505 * @arg {Object} input
506 * @arg {Snowflake} input.serverID
507 * @arg {Snowflake} input.userID
508 * @arg {Snowflake} input.channelID
509 */
510DCP.moveUserTo = function(input, callback) {
511 this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {channel_id: input.channelID}, function(err, res) {
512 handleResCB("Could not move the user", err, res, callback);
513 });
514};
515
516/**
517 * Server-mute the user from speaking in all voice channels.
518 * @arg {Object} input
519 * @arg {Snowflake} input.serverID
520 * @arg {Snowflake} input.userID
521 */
522DCP.mute = function(input, callback) {
523 this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {mute: true}, function(err, res) {
524 handleResCB("Could not mute user", err, res, callback);
525 });
526};
527
528/**
529 * Remove the server-mute from a user.
530 * @arg {Object} input
531 * @arg {Snowflake} input.serverID
532 * @arg {Snowflake} input.userID
533 */
534DCP.unmute = function(input, callback) {
535 this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {mute: false}, function(err, res) {
536 handleResCB("Could not unmute user", err, res, callback);
537 });
538};
539
540/**
541 * Server-deafen a user.
542 * @arg {Object} input
543 * @arg {Snowflake} input.serverID
544 * @arg {Snowflake} input.userID
545 */
546DCP.deafen = function(input, callback) {
547 this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {deaf: true}, function(err, res) {
548 handleResCB("Could not deafen user", err, res, callback);
549 });
550};
551
552/**
553 * Remove the server-deafen from a user.
554 * @arg {Object} input
555 * @arg {Snowflake} input.serverID
556 * @arg {Snowflake} input.userID
557 */
558DCP.undeafen = function(input, callback) {
559 this._req('patch', Endpoints.MEMBERS(input.serverID, input.userID), {deaf: false}, function(err, res) {
560 handleResCB("Could not undeafen user", err, res, callback);
561 });
562};
563
564/**
565 * Self-mute the client from speaking in all voice channels.
566 * @arg {Snowflake} serverID
567 */
568DCP.muteSelf = function(serverID, callback) {
569 var server = this.servers[serverID], channelID, voiceSession;
570 if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback);
571
572 server.self_mute = true;
573
574 if (!server.voiceSession) return call(callback, [null]);
575
576 voiceSession = server.voiceSession;
577 voiceSession.self_mute = true;
578 channelID = voiceSession.channelID;
579 if (!channelID) return call(callback, [null]);
580 return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, true, server.self_deaf))]);
581};
582
583/**
584 * Remove the self-mute from the client.
585 * @arg {Snowflake} serverID
586 */
587DCP.unmuteSelf = function(serverID, callback) {
588 var server = this.servers[serverID], channelID, voiceSession;
589 if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback);
590
591 server.self_mute = false;
592
593 if (!server.voiceSession) return call(callback, [null]);
594
595 voiceSession = server.voiceSession;
596 voiceSession.self_mute = false;
597 channelID = voiceSession.channelID;
598 if (!channelID) return call(callback, [null]);
599 return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, false, server.self_deaf))]);
600};
601
602/**
603 * Self-deafen the client.
604 * @arg {Snowflake} serverID
605 */
606DCP.deafenSelf = function(serverID, callback) {
607 var server = this.servers[serverID], channelID, voiceSession;
608 if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback);
609
610 server.self_deaf = true;
611
612 if (!server.voiceSession) return call(callback, [null]);
613
614 voiceSession = server.voiceSession;
615 voiceSession.self_deaf = true;
616 channelID = voiceSession.channelID;
617 if (!channelID) return call(callback, [null]);
618 return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, true))]);
619};
620
621/**
622 * Remove the self-deafen from the client.
623 * @arg {Snowflake} serverID
624 */
625DCP.undeafenSelf = function(serverID, callback) {
626 var server = this.servers[serverID], channelID, voiceSession;
627 if (!server) return handleErrCB(("Cannot find the server provided: " + serverID), callback);
628
629 server.self_deaf = false;
630
631 if (!server.voiceSession) return call(callback, [null]);
632
633 voiceSession = server.voiceSession;
634 voiceSession.self_deaf = false;
635 channelID = voiceSession.channelID;
636 if (!channelID) return call(callback, [null]);
637 return call(callback, [send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, false))]);
638};
639
640/*Bot server management actions*/
641
642/**
643 * Create a server [User only].
644 * @arg {Object} input
645 * @arg {String} input.name - The server's name
646 * @arg {String} [input.region] - The server's region code, check the Gitbook documentation for all of them.
647 * @arg {String<Base64>} [input.icon] - The last part of a Base64 Data URI. `fs.readFileSync('image.jpg', 'base64')` is enough.
648 */
649DCP.createServer = function(input, callback) {
650 var payload, client = this;
651 payload = {icon: null, name: null, region: null};
652 for (var key in input) {
653 if (Object.keys(payload).indexOf(key) < 0) continue;
654 payload[key] = input[key];
655 }
656 if (input.icon) payload.icon = "data:image/jpg;base64," + input.icon;
657
658 client._req('post', Endpoints.SERVERS(), payload, function(err, res) {
659 try {
660 client.servers[res.body.id] = {};
661 copyKeys(res.body, client.servers[res.body.id]);
662 } catch(e) {}
663 handleResCB("Could not create server", err, res, callback);
664 });
665};
666
667/**
668 * Edit server information.
669 * @arg {Object} input
670 * @arg {Snowflake} input.serverID
671 * @arg {String} [input.name]
672 * @arg {String} [input.icon]
673 * @arg {String} [input.region]
674 * @arg {Snowflake} [input.afk_channel_id] - The ID of the voice channel to move a user to after the afk period.
675 * @arg {Number} [input.afk_timeout] - Time in seconds until a user is moved to the afk channel. 60, 300, 900, 1800, or 3600.
676 */
677DCP.editServer = function(input, callback) {
678 var payload, serverID = input.serverID, server, client = this;
679 if (!client.servers[serverID]) return handleErrCB(("[editServer] Server " + serverID + " not found."), callback);
680
681 server = client.servers[serverID];
682 payload = {
683 name: server.name,
684 icon: server.icon,
685 region: server.region,
686 afk_channel_id: server.afk_channel_id,
687 afk_timeout: server.afk_timeout
688 };
689
690 for (var key in input) {
691 if (Object.keys(payload).indexOf(key) < 0) continue;
692 if (key === 'afk_channel_id') {
693 if (server.channels[input[key]] && server.channels[input[key]].type === 'voice') payload[key] = input[key];
694 continue;
695 }
696 if (key === 'afk_timeout') {
697 if ([60, 300, 900, 1800, 3600].indexOf(Number(input[key])) > -1) payload[key] = input[key];
698 continue;
699 }
700 payload[key] = input[key];
701 }
702 if (input.icon) payload.icon = "data:image/jpg;base64," + input.icon;
703
704 client._req('patch', Endpoints.SERVERS(input.serverID), payload, function(err, res) {
705 handleResCB("Unable to edit server", err, res, callback);
706 });
707};
708
709/**
710 * Edit the widget information for a server.
711 * @arg {Object} input
712 * @arg {Snowflake} input.serverID - The ID of the server whose widget you want to edit.
713 * @arg {Boolean} [input.enabled] - Whether or not you want the widget to be enabled.
714 * @arg {Snowflake} [input.channelID] - [Important] The ID of the channel you want the instant invite to point to.
715 */
716DCP.editServerWidget = function(input, callback) {
717 var client = this, payload, url = Endpoints.SERVERS(input.serverID) + "/embed";
718
719 client._req('get', url, function(err, res) {
720 if (err) return handleResCB("Unable to GET server widget settings. Can not edit without retrieving first.", err, res, callback);
721 payload = {
722 enabled: ('enabled' in input ? input.enabled : res.body.enabled),
723 channel_id: ('channelID' in input ? input.channelID : res.body.channel_id)
724 };
725 client._req('patch', url, payload, function(err, res) {
726 handleResCB("Unable to edit server widget", err, res, callback);
727 });
728 });
729};
730
731/**
732 * [User Account] Add an emoji to a server
733 * @arg {Object} input
734 * @arg {Snowflake} input.serverID
735 * @arg {String} input.name - The emoji's name
736 * @arg {String<Base64>} input.image - The emoji's image data in Base64
737 */
738DCP.addServerEmoji = function(input, callback) {
739 var payload = {
740 name: input.name,
741 image: "data:image/png;base64," + input.image
742 };
743 this._req('post', Endpoints.SERVER_EMOJIS(input.serverID), payload, function(err, res) {
744 handleResCB("Unable to add emoji to the server", err, res, callback);
745 });
746}
747
748/**
749 * [User Account] Edit a server emoji data (name only, currently)
750 * @arg {Object} input
751 * @arg {Snowflake} input.serverID
752 * @arg {Snowflake} input.emojiID - The emoji's ID
753 * @arg {String} [input.name]
754 * @arg {Array<Snowflake>} [input.roles] - An array of role IDs you want to limit the emoji's usage to
755 */
756DCP.editServerEmoji = function(input, callback) {
757 var emoji, payload = {};
758 if ( !this.servers[input.serverID] ) return handleErrCB(("[editServerEmoji] Server not available: " + input.serverID), callback);
759 if ( !this.servers[input.serverID].emojis[input.emojiID]) return handleErrCB(("[editServerEmoji] Emoji not available: " + input.emojiID), callback);
760
761 emoji = this.servers[input.serverID].emojis[input.emojiID];
762 payload.name = input.name || emoji.name;
763 payload.roles = input.roles || emoji.roles;
764
765 this._req('patch', Endpoints.SERVER_EMOJIS(input.serverID, input.emojiID), payload, function(err, res) {
766 handleResCB("[editServerEmoji] Could not edit server emoji", err, res, callback);
767 });
768};
769
770/**
771 * [User Account] Remove an emoji from a server
772 * @arg {Object} input
773 * @arg {Snowflake} input.serverID
774 * @arg {Snowflake} input.emojiID
775 */
776DCP.deleteServerEmoji = function(input, callback) {
777 this._req('delete', Endpoints.SERVER_EMOJIS(input.serverID, input.emojiID), function(err, res) {
778 handleResCB("[deleteServerEmoji] Could not delete server emoji", err, res, callback);
779 });
780};
781
782/**
783 * Leave a server.
784 * @arg {Snowflake} serverID
785 */
786DCP.leaveServer = function(serverID, callback) {
787 this._req('delete', Endpoints.SERVERS_PERSONAL(serverID), function(err, res) {
788 handleResCB("Could not leave server", err, res, callback);
789 });
790};
791
792/**
793 * Delete a server owned by the client.
794 * @arg {Snowflake} serverID
795 */
796DCP.deleteServer = function(serverID, callback) {
797 this._req('delete', Endpoints.SERVERS(serverID), function(err, res) {
798 handleResCB("Could not delete server", err, res, callback);
799 });
800};
801
802/**
803 * Transfer ownership of a server to another user.
804 * @arg {Object} input
805 * @arg {Snowflake} input.serverID
806 * @arg {Snowflake} input.userID
807 */
808DCP.transferOwnership = function(input, callback) {
809 this._req('patch', Endpoints.SERVERS(input.serverID), {owner_id: input.userID}, function(err, res) {
810 handleResCB("Could not transfer server ownership", err, res, callback);
811 });
812};
813
814/**
815 * (Used to) Accept an invite to a server [User Only]. Can no longer be used.
816 * @deprecated
817 */
818DCP.acceptInvite = function(NUL, callback) {
819 return handleErrCB("acceptInvite can no longer be used", callback);
820};
821
822/**
823 * (Used to) Generate an invite URL for a channel.
824 * @deprecated
825 */
826DCP.createInvite = function(input, callback) {
827 var payload, client = this;
828
829 payload = {
830 max_age: 0,
831 max_users: 0,
832 temporary: false
833 };
834
835 if ( Object.keys(input).length === 1 && input.channelID ) {
836 payload = {
837 validate: client.internals.lastInviteCode || null
838 };
839 }
840
841 for (var key in input) {
842 if (Object.keys(payload).indexOf(key) < 0) continue;
843 payload[key] = input[key];
844 }
845
846 this._req('post', Endpoints.CHANNEL(input.channelID) + "/invites", payload, function(err, res) {
847 try {client.internals.lastInviteCode = res.body.code;} catch(e) {}
848 handleResCB('Unable to create invite', err, res, callback);
849 });
850};
851
852/**
853 * Delete an invite code.
854 * @arg {String} inviteCode
855 */
856DCP.deleteInvite = function(inviteCode, callback) {
857 this._req('delete', Endpoints.INVITES(inviteCode), function(err, res) {
858 handleResCB('Unable to delete invite', err, res, callback);
859 });
860};
861
862/**
863 * Get information on an invite.
864 * @arg {String} inviteCode
865 */
866DCP.queryInvite = function(inviteCode, callback) {
867 this._req('get', Endpoints.INVITES(inviteCode), function(err, res) {
868 handleResCB('Unable to get information about invite', err, res, callback);
869 });
870};
871
872/**
873 * Get all invites for a server.
874 * @arg {Snowflake} serverID
875 */
876DCP.getServerInvites = function(serverID, callback) {
877 this._req('get', Endpoints.SERVERS(serverID) + "/invites", function(err, res) {
878 handleResCB('Unable to get invite list for server' + serverID, err, res, callback);
879 });
880};
881
882/**
883 * Get all invites for a channel.
884 * @arg {Snowflake} channelID
885 */
886DCP.getChannelInvites = function(channelID, callback) {
887 this._req('get', Endpoints.CHANNEL(channelID) + "/invites", function(err, res) {
888 handleResCB('Unable to get invite list for channel' + channelID, err, res, callback);
889 });
890};
891
892/**
893 * Create a channel.
894 * @arg {Object} input
895 * @arg {Snowflake} input.serverID
896 * @arg {String} input.name
897 * @arg {String} [input.type] - 'text' or 'voice', defaults to 'text.
898 */
899DCP.createChannel = function(input, callback) {
900 var client = this, payload = {
901 name: input.name,
902 type: (['text', 'voice'].indexOf(input.type) < 0) ? 'text' : input.type
903 };
904
905 this._req('post', Endpoints.SERVERS(input.serverID) + "/channels", payload, function(err, res) {
906 try {
907 var serverID = res.body.guild_id;
908 var channelID = res.body.id;
909
910 client.channels[channelID] = new Channel( client, client.servers[serverID], res.body );
911 } catch(e) {}
912 handleResCB('Unable to create channel', err, res, callback);
913 });
914};
915
916/**
917 * Create a Direct Message channel.
918 * @arg {Snowflake} userID
919 */
920DCP.createDMChannel = function(userID, callback) {
921 var client = this;
922 this._req('post', Endpoints.USER(client.id) + "/channels", {recipient_id: userID}, function(err, res) {
923 if (!err && goodResponse(res)) client._uIDToDM[res.body.recipient.id] = res.body.id;
924 handleResCB("Unable to create DM Channel", err, res, callback);
925 });
926};
927
928/**
929 * Delete a channel.
930 * @arg {Snowflake} channelID
931 */
932DCP.deleteChannel = function(channelID, callback) {
933 this._req('delete', Endpoints.CHANNEL(channelID), function(err, res) {
934 handleResCB("Unable to delete channel", err, res, callback);
935 });
936};
937
938/**
939 * Edit a channel's information.
940 * @arg {Object} input
941 * @arg {Snowflake} input.channelID
942 * @arg {String} [input.name]
943 * @arg {String} [input.topic] - The topic of the channel.
944 * @arg {Number} [input.bitrate] - [Voice Only] The bitrate for the channel.
945 * @arg {Number} [input.position] - The channel's position on the list.
946 * @arg {Number} [input.user_limit] - [Voice Only] Imposes a user limit on a voice channel.
947 */
948DCP.editChannelInfo = function(input, callback) {
949 var channel, payload;
950
951 try {
952 channel = this.channels[input.channelID];
953 payload = {
954 name: channel.name,
955 topic: channel.topic,
956 bitrate: channel.bitrate,
957 position: channel.position,
958 user_limit: channel.user_limit
959 };
960
961 for (var key in input) {
962 if (Object.keys(payload).indexOf(key) < 0) continue;
963 if (+input[key]) {
964 if (key === 'bitrate') {
965 payload.bitrate = Math.min( Math.max( input.bitrate, 8000), 96000);
966 continue;
967 }
968 if (key === 'user_limit') {
969 payload.user_limit = Math.min( Math.max( input.user_limit, 0), 99);
970 continue;
971 }
972 }
973 payload[key] = input[key];
974 }
975
976 this._req('patch', Endpoints.CHANNEL(input.channelID), payload, function(err, res) {
977 handleResCB("Unable to edit channel", err, res, callback);
978 });
979 } catch(e) {return handleErrCB(e, callback);}
980};
981
982/**
983 * Edit (or creates) a permission override for a channel.
984 * @arg {Object} input
985 * @arg {Snowflake} input.channelID
986 * @arg {Snowflake} [input.userID]
987 * @arg {Snowflake} [input.roleID]
988 * @arg {Array<Number>} input.allow - An array of permissions to allow. Discord.Permissions.XXXXXX.
989 * @arg {Array<Number>} input.deny - An array of permissions to deny, same as above.
990 * @arg {Array<Number>} input.default - An array of permissions that cancels out allowed and denied permissions.
991 */
992DCP.editChannelPermissions = function(input, callback) { //Will shrink this up later
993 var payload, pType, ID, channel, permissions, allowed_values;
994 if (!input.userID && !input.roleID) return handleErrCB("[editChannelPermissions] No userID or roleID provided", callback);
995 if (!this.channels[input.channelID]) return handleErrCB(("[editChannelPermissions] No channel found for ID: " + input.channelID), callback);
996 if (!input.allow && !input.deny && !input.default) return handleErrCB("[editChannelPermissions] No allow, deny or default array provided.", callback);
997
998 pType = input.userID ? 'user' : 'role';
999 ID = input[pType + "ID"];
1000 channel = this.channels[ input.channelID ];
1001 permissions = channel.permissions[pType][ID] || { allow: 0, deny: 0 };
1002 allowed_values = [0, 4, 28].concat((channel.type === 'text' ?
1003 [10, 11, 12, 13, 14, 15, 16, 17, 18] :
1004 [20, 21, 22, 23, 24, 25] ));
1005
1006 //Take care of allow first
1007 if (type(input.allow) === 'array') {
1008 input.allow.forEach(function(perm) {
1009 if (allowed_values.indexOf(perm) < 0) return;
1010 if (hasPermission(perm, permissions.deny)) {
1011 permissions.deny = removePermission(perm, permissions.deny);
1012 }
1013 permissions.allow = givePermission(perm, permissions.allow);
1014 });
1015 }
1016 //Take care of deny second
1017 if (type(input.deny) === 'array') {
1018 input.deny.forEach(function(perm) {
1019 if (allowed_values.indexOf(perm) < 0) return;
1020 if (hasPermission(perm, permissions.allow)) {
1021 permissions.allow = removePermission(perm, permissions.allow);
1022 }
1023 permissions.deny = givePermission(perm, permissions.deny);
1024 });
1025 }
1026 //Take care of defaulting last
1027 if (type(input.default) === 'array') {
1028 input.default.forEach(function(perm) {
1029 if (allowed_values.indexOf(perm) < 0) return;
1030 permissions.allow = removePermission(perm, permissions.allow);
1031 permissions.deny = removePermission(perm, permissions.deny);
1032 });
1033 }
1034
1035 payload = {
1036 type: (pType === 'user' ? 'member' : 'role'),
1037 id: ID,
1038 deny: permissions.deny,
1039 allow: permissions.allow
1040 };
1041
1042 this._req('put', Endpoints.CHANNEL(input.channelID) + "/permissions/" + ID, payload, function(err, res) {
1043 handleResCB('Unable to edit permission', err, res, callback);
1044 });
1045};
1046
1047/**
1048 * Delete a permission override for a channel.
1049 * @arg {Object} input
1050 * @arg {Snowflake} input.channelID
1051 * @arg {Snowflake} [input.userID]
1052 * @arg {Snowflake} [input.roleID]
1053 */
1054DCP.deleteChannelPermission = function(input, callback) {
1055 var payload, pType, ID;
1056 if (!input.userID && !input.roleID) return handleErrCB("[deleteChannelPermission] No userID or roleID provided", callback);
1057 if (!this.channels[input.channelID]) return handleErrCB(("[deleteChannelPermission] No channel found for ID: " + input.channelID), callback);
1058
1059 pType = input.userID ? 'user' : 'role';
1060 ID = input[pType + "ID"];
1061
1062 payload = {
1063 type: (pType === 'user' ? 'member' : 'role'),
1064 id: ID
1065 };
1066
1067 this._req('delete', Endpoints.CHANNEL(input.channelID) + "/permissions/" + ID, payload, function(err, res) {
1068 handleResCB('Unable to delete permission', err, res, callback);
1069 });
1070};
1071
1072/**
1073 * Create a role for a server.
1074 * @arg {Snowflake} serverID
1075 */
1076DCP.createRole = function(serverID, callback) {
1077 var client = this;
1078 this._req('post', Endpoints.ROLES(serverID), function(err, res) {
1079 try {
1080 client.servers[serverID].roles[res.body.id] = new Role(res.body);
1081 } catch(e) {}
1082 handleResCB("Unable to create role", err, res, callback);
1083 });
1084};
1085
1086/**
1087 * Edit a role.
1088 * @arg {Object} input
1089 * @arg {Snowflake} input.serverID
1090 * @arg {Snowflake} input.roleID - The ID of the role.
1091 * @arg {String} [input.name]
1092 * @arg {String} [input.color] - A color value as a number. Recommend using Hex numbers, as they can map to HTML colors (0xF35353 === #F35353).
1093 * @arg {Boolean} [input.hoist] - Separates the users in this role from the normal online users.
1094 * @arg {Object} [input.permissions] - An Object containing the permission as a key, and `true` or `false` as its value. Read the Permissions doc.
1095 * @arg {Boolean} [input.mentionable] - Toggles if users can @Mention this role.
1096 */
1097DCP.editRole = function(input, callback) {
1098 var role, payload;
1099 try {
1100 role = new Role(this.servers[input.serverID].roles[input.roleID]);
1101 payload = {
1102 name: role.name,
1103 color: role.color,
1104 hoist: role.hoist,
1105 permissions: role._permissions,
1106 mentionable: role.mentionable,
1107 position: role.position
1108 };
1109
1110 for (var key in input) {
1111 if (Object.keys(payload).indexOf(key) < 0) continue;
1112 if (key === 'permissions') {
1113 for (var perm in input[key]) {
1114 role[perm] = input[key][perm];
1115 payload.permissions = role._permissions;
1116 }
1117 continue;
1118 }
1119 if (key === 'color') {
1120 if (String(input[key])[0] === '#') payload.color = parseInt(String(input[key]).replace('#', '0x'), 16);
1121 if (Discord.Colors[input[key]]) payload.color = Discord.Colors[input[key]];
1122 if (type(input[key]) === 'number') payload.color = input[key];
1123 continue;
1124 }
1125 payload[key] = input[key];
1126 }
1127 this._req('patch', Endpoints.ROLES(input.serverID, input.roleID), payload, function(err, res) {
1128 handleResCB("Unable to edit role", err, res, callback);
1129 });
1130 } catch(e) {return handleErrCB(('[editRole] ' + e), callback);}
1131};
1132
1133/**
1134 * Delete a role.
1135 * @arg {Object} input
1136 * @arg {Snowflake} input.serverID
1137 * @arg {Snowflake} input.roleID
1138 */
1139DCP.deleteRole = function(input, callback) {
1140 this._req('delete', Endpoints.ROLES(input.serverID, input.roleID), function(err, res) {
1141 handleResCB("Could not remove role", err, res, callback);
1142 });
1143};
1144
1145/**
1146 * Add a user to a role.
1147 * @arg {Object} input
1148 * @arg {Snowflake} input.serverID
1149 * @arg {Snowflake} input.roleID
1150 * @arg {Snowflake} input.userID
1151 */
1152DCP.addToRole = function(input, callback) {
1153 this._req('put', Endpoints.MEMBER_ROLES(input.serverID, input.userID, input.roleID), function(err, res) {
1154 handleResCB("Could not add role", err, res, callback);
1155 });
1156};
1157
1158/**
1159 * Remove a user from a role.
1160 * @arg {Object} input
1161 * @arg {Snowflake} input.serverID
1162 * @arg {Snowflake} input.roleID
1163 * @arg {Snowflake} input.userID
1164 */
1165DCP.removeFromRole = function(input, callback) {
1166 this._req('delete', Endpoints.MEMBER_ROLES(input.serverID, input.userID, input.roleID), function(err, res) {
1167 handleResCB("Could not remove role", err, res, callback);
1168 });
1169};
1170
1171/**
1172 * Edit a user's nickname.
1173 * @arg {Object} input
1174 * @arg {Snowflake} input.serverID
1175 * @arg {Snowflake} input.userID
1176 * @arg {String} input.nick - The nickname you'd like displayed.
1177 */
1178DCP.editNickname = function(input, callback) {
1179 var payload = {nick: String( input.nick ? input.nick : "" )};
1180 var url = input.userID === this.id ?
1181 Endpoints.MEMBERS(input.serverID) + "/@me/nick" :
1182 Endpoints.MEMBERS(input.serverID, input.userID);
1183
1184 this._req('patch', url, payload, function(err, res) {
1185 handleResCB("Could not change nickname", err, res, callback);
1186 });
1187};
1188
1189/**
1190 * Edit a user's note.
1191 * @arg {Object} input
1192 * @arg {Snowflake} input.userID
1193 * @arg {String} input.note - The note content that you want to use.
1194 */
1195DCP.editNote = function(input, callback) {
1196 this._req('put', Endpoints.NOTE(input.userID), {note: input.note}, function(err, res) {
1197 handleResCB("Could not edit note", err, res, callback);
1198 });
1199};
1200
1201/**
1202 * Retrieve a user object from Discord, the library already caches users, however.
1203 * @arg {Object} input
1204 * @arg {Snowflake} input.serverID
1205 * @arg {Snowflake} input.userID
1206 */
1207DCP.getMember = function(input, callback) {
1208 this._req('get', Endpoints.MEMBERS(input.serverID, input.userID), function(err, res) {
1209 handleResCB("Could not get member", err, res, callback);
1210 });
1211};
1212
1213/**
1214 * Retrieve a group of user objects from Discord.
1215 * @arg {Object} input
1216 * @arg {Number} [input.limit] - The amount of users to pull, defaults to 50.
1217 * @arg {Snowflake} [input.after] - The offset using a user ID.
1218 */
1219DCP.getMembers = function(input, callback) {
1220 var qs = {};
1221 qs.limit = (typeof(input.limit) !== 'number' ? 50 : input.limit);
1222 if (input.after) qs.after = input.after;
1223
1224 this._req('get', Endpoints.MEMBERS(input.serverID) + qstringify(qs), function(err, res) {
1225 handleResCB("Could not get members", err, res, callback);
1226 });
1227};
1228
1229/**
1230 * Get the ban list from a server
1231 * @arg {Snowflake} serverID
1232 */
1233DCP.getBans = function(serverID, callback) {
1234 this._req('get', Endpoints.BANS(serverID), function(err, res) {
1235 handleResCB("Could not get ban list", err, res, callback);
1236 });
1237};
1238
1239/**
1240 * Get all webhooks for a server
1241 * @arg {Snowflake} serverID
1242 */
1243DCP.getServerWebhooks = function(serverID, callback) {
1244 this._req('get', Endpoints.SERVER_WEBHOOKS(serverID), function(err, res) {
1245 handleResCB("Could not get server Webhooks", err, res, callback);
1246 });
1247};
1248
1249/**
1250 * Get webhooks from a channel
1251 * @arg {Snowflake} channelID
1252 */
1253DCP.getChannelWebhooks = function(channelID, callback) {
1254 this._req('get', Endpoints.CHANNEL_WEBHOOKS(channelID), function(err, res) {
1255 handleResCB("Could not get channel Webhooks", err, res, callback);
1256 });
1257};
1258
1259/**
1260 * Create a webhook for a server
1261 * @arg {Snowflake} serverID
1262 */
1263DCP.createWebhook = function(serverID, callback) {
1264 this._req('post', Endpoints.SERVER_WEBHOOKS(serverID), function(err, res) {
1265 handleResCB("Could not create a Webhook", err, res, callback);
1266 });
1267};
1268
1269/**
1270 * Edit a webhook
1271 * @arg {Object} input
1272 * @arg {Snowflake} input.webhookID - The Webhook's ID
1273 * @arg {String} [input.name]
1274 * @arg {String<Base64>} [input.avatar]
1275 * @arg {String} [input.channelID]
1276 */
1277DCP.editWebhook = function(input, callback) {
1278 var client = this, payload = {}, allowed = ['avatar', 'name'];
1279 this._req('get', Endpoints.WEBHOOKS(input.webhookID), function(err, res) {
1280 if (err || !goodResponse(res)) return handleResCB("Couldn't get webhook, do you have permissions to access it?", err, res, callback);
1281 allowed.forEach(function(key) {
1282 payload[key] = (key in input ? input[key] : res.body[key]);
1283 });
1284 payload.channel_id = input.channelID || res.body.channel_id;
1285
1286 client._req('patch', Endpoints.WEBHOOKS(input.webhookID), payload, function(err, res) {
1287 return handleResCB("Couldn't update webhook", err, res, callback);
1288 });
1289 });
1290};
1291
1292/* --- Voice --- */
1293
1294/**
1295 * Join a voice channel.
1296 * @arg {Snowflake} channelID
1297 */
1298DCP.joinVoiceChannel = function(channelID, callback) {
1299 var serverID, server, channel, voiceSession;
1300 try {
1301 serverID = this.channels[channelID].guild_id;
1302 server = this.servers[serverID];
1303 channel = server.channels[channelID];
1304 } catch(e) {}
1305 if (!serverID) return handleErrCB(("Cannot find the server related to the channel provided: " + channelID), callback);
1306 if (channel.type !== 'voice') return handleErrCB(("Selected channel is not a voice channel: " + channelID), callback);
1307 if (this._vChannels[channelID]) return handleErrCB(("Voice channel already active: " + channelID), callback);
1308
1309 voiceSession = getVoiceSession(this, channelID, server);
1310 voiceSession.self_mute = server.self_mute;
1311 voiceSession.self_deaf = server.self_deaf;
1312 checkVoiceReady(voiceSession, callback);
1313 return send(this._ws, Payloads.UPDATE_VOICE(serverID, channelID, server.self_mute, server.self_deaf));
1314};
1315
1316/**
1317 * Leave a voice channel.
1318 * @arg {Snowflake} channelID
1319 */
1320DCP.leaveVoiceChannel = function(channelID, callback) {
1321 if (!this._vChannels[channelID]) return handleErrCB(("Not in the voice channel: " + channelID), callback);
1322 return leaveVoiceChannel(this, channelID, callback);
1323};
1324
1325/**
1326 * Prepare the client for sending/receiving audio.
1327 * @arg {Snowflake|Object} channelObj - Either the channel ID, or an Object with `channelID` as a key and the ID as the value.
1328 * @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.
1329 * @arg {Boolean} [channelObj.stereo] - Sets the audio to be either stereo or mono. Defaults to true.
1330 */
1331DCP.getAudioContext = function(channelObj, callback) {
1332 // #q/qeled gave a proper timing solution. Credit where it's due.
1333 if (!isNode) return handleErrCB("Using audio in the browser is currently not supported.", callback);
1334 var channelID = channelObj.channelID || channelObj, voiceSession = this._vChannels[channelID], encoder = chooseAudioEncoder(['ffmpeg', 'avconv']);
1335
1336 if (!voiceSession) return handleErrCB(("You have not joined the voice channel: " + channelID), callback);
1337 if (voiceSession.ready !== true) return handleErrCB(("The connection to the voice channel " + channelID + " has not been initialized yet."), callback);
1338 if (!encoder) return handleErrCB("You need either 'ffmpeg' or 'avconv' and they need to be added to PATH", callback);
1339
1340 voiceSession.audio = voiceSession.audio || new AudioCB(
1341 voiceSession,
1342 channelObj.stereo === false ? 1 : 2,
1343 encoder,
1344 Math.abs(Number(channelObj.maxStreamSize)));
1345
1346 return call(callback, [null, voiceSession.audio]);
1347};
1348
1349/* --- Misc --- */
1350
1351/**
1352 * Retrieves all offline (and online, if using a user account) users, fires the `allUsers` event when done.
1353 */
1354DCP.getAllUsers = function(callback) {
1355 var servers = Object.keys(this.servers).filter(function(s) {
1356 s = this.servers[s];
1357 if (s.members) return s.member_count !== Object.keys(s.members).length && (this.bot ? s.large : true);
1358 }, this);
1359
1360 if (!servers[0]) {
1361 this.emit('allUsers');
1362 return handleErrCB("There are no users to be collected", callback);
1363 }
1364 if (!this.bot) send(this._ws, Payloads.ALL_USERS(this));
1365
1366 return getOfflineUsers(this, servers, callback);
1367};
1368
1369/* --- Functions --- */
1370function handleErrCB(err, callback) {
1371 if (!err) return false;
1372 return call(callback, [new Error(err)]);
1373}
1374function handleResCB(errMessage, err, res, callback) {
1375 if (typeof(callback) !== 'function') return;
1376 res = res || {};
1377 if (!err && goodResponse(res)) return (callback(null, res.body), true);
1378
1379 var e = new Error( err || errMessage );
1380 e.name = "ResponseError";
1381 e.statusCode = res.statusCode;
1382 e.statusMessage = res.statusMessage;
1383 e.response = res.body;
1384 return (callback(e), false);
1385}
1386function goodResponse(response) {
1387 return (response.statusCode / 100 | 0) === 2;
1388}
1389function stringifyError(response) {
1390 if (!response) return null;
1391 return response.statusCode + " " + response.statusMessage + "\n" + JSON.stringify(response.body);
1392}
1393
1394/* - Functions - Messages - */
1395function sendMessage(client, to, message, callback) {
1396 resolveID(client, to, function(channelID) {
1397 client._req('post', Endpoints.MESSAGES(channelID), message, function(err, res) {
1398 handleResCB("Unable to send messages", err, res, callback);
1399 });
1400 });
1401}
1402function cacheMessage(cache, limit, channelID, message) {
1403 if (!cache[channelID]) cache[channelID] = {};
1404 if (limit === -1) return void(cache[channelID][message.id] = message);
1405 var k = Object.keys(cache[channelID]);
1406 if (k.length > limit) delete(cache[channelID][k[0]]);
1407 cache[channelID][message.id] = message;
1408}
1409function generateMessage(message, embed) {
1410 return {
1411 content: String(message),
1412 nonce: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER),
1413 embed: embed || {}
1414 };
1415}
1416function messageHeaders(client) {
1417 var r = {
1418 "accept": "*/*",
1419 "accept-language": "en-US;q=0.8",
1420 };
1421 if (isNode) {
1422 r["accept-encoding"] = "gzip, deflate";
1423 r.user_agent = "DiscordBot (https://github.com/izy521/discord.io, " + CURRENT_VERSION + ")";
1424 r.dnt = 1;
1425 }
1426 try {
1427 r.authorization = (client.bot ? "Bot " : "") + client.internals.token;
1428 } catch(e) {}
1429 return r;
1430}
1431function simulateTyping(client, to, message, time, callback) {
1432 if (time <= 0) return sendMessage(client, to, message, callback);
1433
1434 client.simulateTyping(to, function() {
1435 setTimeout(simulateTyping, Math.min(time, 5000), client, to, message, time - 5000, callback);
1436 });
1437}
1438function stringifyEmoji(emoji) {
1439 if (typeof emoji === 'object') // if (emoji.name && emoji.id)
1440 return emoji.name + ':' + emoji.id;
1441 if (emoji.indexOf(':') > -1)
1442 return emoji;
1443 return encodeURIComponent(decodeURIComponent(emoji));
1444}
1445
1446/* - Functions - Utils */
1447function APIRequest(method, url) {
1448 var data, callback, opts, req, headers = messageHeaders(this);
1449 callback = ( typeof(arguments[2]) === 'function' ? arguments[2] : (data = arguments[2], arguments[3]) );
1450
1451 if (isNode) {
1452 opts = URL.parse(url);
1453 opts.method = method;
1454 opts.headers = headers;
1455
1456 req = requesters[opts.protocol.slice(0, -1)].request(opts, function(res) {
1457 var chunks = [];
1458 res.on('data', function(c) { chunks[chunks.length] = c; });
1459 res.once('end', function() {
1460 chunks = Buffer.concat(chunks);
1461 Zlib.gunzip(chunks, function(err, uc) {
1462 if (!err) uc = uc.toString();
1463 try { res.body = JSON.parse(uc || chunks); } catch(e) {}
1464 return callback(null, res);
1465 });
1466 });
1467 });
1468 if (type(data) === 'object' || method.toLowerCase() === 'get') req.setHeader("Content-Type", "application/json; charset=utf-8");
1469 if (data instanceof Multipart) req.setHeader("Content-Type", "multipart/form-data; boundary=" + data.boundary);
1470 if (data) req.write( data.result || JSON.stringify(data), data.result ? 'binary' : 'utf-8' );
1471 req.end();
1472
1473 return req.once('error', function(e) { return callback(e.message); });
1474 }
1475
1476 req = new XMLHttpRequest();
1477 req.open(method.toUpperCase(), url, true);
1478 for (var key in headers) {
1479 req.setRequestHeader(key, headers[key]);
1480 }
1481 req.onreadystatechange = function() {
1482 if (req.readyState == 4) {
1483 req.statusCode = req.status;
1484 req.statusMessage = req.statusText;
1485 try {req.body = JSON.parse(req.responseText);} catch (e) { return handleErrCB(e, callback); }
1486 callback(null, req);
1487 }
1488 };
1489 if (type(data) === 'object' || method.toLowerCase() === 'get') req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
1490 if (data instanceof Multipart) req.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + data.boundary);
1491 if (data) return req.send( data.result ? data.result : JSON.stringify(data) );
1492 req.send(null);
1493}
1494function send(ws, data) {
1495 if (ws && ws.readyState == 1) ws.send(JSON.stringify(data));
1496}
1497function copy(obj) {
1498 try {
1499 return JSON.parse( JSON.stringify( obj ) );
1500 } catch(e) {}
1501}
1502function copyKeys(from, to, omit) {
1503 if (!omit) omit = [];
1504 for (var key in from) {
1505 if (omit.indexOf(key) > -1) continue;
1506 to[key] = from[key];
1507 }
1508}
1509function applyProperties(object, properties) {
1510 properties.forEach(function(t) {
1511 Object.defineProperty(object, t[0], {
1512 configurable: true,
1513 writable: true,
1514 value: t[1]
1515 });
1516 }, object);
1517}
1518function type(v) {
1519 return Object.prototype.toString.call(v).match(/ (.*)]/)[1].toLowerCase();
1520}
1521function call(f, a) {
1522 if (typeof(f) != 'function') return;
1523 return f.apply(null, a);
1524}
1525function qstringify(obj) {
1526 //.map + .join is 7x slower!
1527 var i=0, s = "", k = Object.keys(obj);
1528 for (i;i<k.length;i++) {
1529 s += k[i] + "=" + obj[k[i]] + "&";
1530 }
1531 return "?" + s.slice(0, -1);
1532}
1533function emit(client, message) {
1534 if (!message.t) return;
1535 var t = message.t.split("_"), i = 1, args = [t[0].toLowerCase()];
1536
1537 for (i; i<t.length; i++) {
1538 args[0] += t[i][0] + t[i].slice(1).toLowerCase();
1539 }
1540 for (i=2; i<arguments.length; i++) {
1541 args.push(arguments[i]);
1542 }
1543 args.push(message);
1544 client.emit.apply(client, args);
1545}
1546function decompressWSMessage(m, f) {
1547 f = f || {};
1548 return f.binary ? JSON.parse(Zlib.inflateSync(m).toString()) : JSON.parse(m);
1549}
1550function removeAllListeners(emitter, type) {
1551 if (!emitter) return;
1552 var e = emitter._evts, i, k, o, s, z;
1553 if (isNode) return type ? emitter.removeAllListeners(type) : emitter.removeAllListeners();
1554
1555 if (type && e[type]) {
1556 for (i=0; i<e[type].length; i++) {
1557 emitter.removeListener(type, e[type][i]);
1558 }
1559 }
1560
1561 if (!type) {
1562 k = Object.keys(e);
1563 for (o=0; o<k.length; o++) {
1564 s = e[ k[o] ];
1565 for (z=0; z<s.length; z++) {
1566 emitter.removeListener(k[o], s[z]);
1567 }
1568 }
1569 }
1570}
1571function colorFromRole(server, member) {
1572 return member.roles.reduce(function(array, ID) {
1573 var role = server.roles[ID];
1574 if (!role) return array;
1575 return role.position > array[0] && role.color ? [role.position, role.color] : array;
1576 }, [-1, null])[1];
1577}
1578function log(client, level_message) {
1579 var level, message;
1580 if (arguments.length === 2) {
1581 level = Discord.LogLevels.Info;
1582 message = level_message;
1583 } else {
1584 level = level_message;
1585 message = arguments[2];
1586 }
1587 return client.emit('log', level, message);
1588}
1589
1590function givePermission(bit, permissions) {
1591 return permissions | (1 << bit);
1592}
1593function removePermission(bit, permissions) {
1594 return permissions & ~(1 << bit);
1595}
1596function hasPermission(bit, permissions) {
1597 return ((permissions >> bit) & 1) == 1;
1598}
1599//For the Getters and Setters
1600function getPerm(bit) {
1601 return function() {
1602 return ((this._permissions >> bit) & 1) == 1;
1603 };
1604}
1605function setPerm(bit) {
1606 return function(v) {
1607 if (v === true) return this._permissions |= (1 << (bit));
1608 if (v === false) return this._permissions &= ~(1 << bit);
1609 };
1610}
1611
1612function getServerInfo(client, servArr) {
1613 for (var server=0; server<servArr.length; server++) {
1614 client.servers[servArr[server].id] = new Server(client, servArr[server]);
1615 }
1616}
1617function getDirectMessages(client, DMArray) {
1618 for (var DM=0; DM<DMArray.length; DM++) {
1619 client.directMessages[DMArray[DM].id] = new DMChannel(client._uIDToDM, DMArray[DM]);
1620 }
1621}
1622function resolveID(client, ID, callback) {
1623 /*Get channel from ServerID, ChannelID or UserID.
1624 Only really used for sendMessage and uploadFile.*/
1625 //Callback used instead of return because requesting seems necessary.
1626
1627 if (client._uIDToDM[ID]) return callback(client._uIDToDM[ID]);
1628 //If it's a UserID, and it's in the UserID : ChannelID cache, use the found ChannelID
1629
1630 //If the ID isn't in the UserID : ChannelID cache, let's try seeing if it belongs to a user.
1631 if (client.users[ID]) return client.createDMChannel(ID, function(err, res) {
1632 if (err) return console.log("Internal ID resolver error: " + JSON.stringify(err));
1633 callback(res.id);
1634 });
1635
1636 return callback(ID); //Finally, the ID must not belong to a User, so send the message directly to it, as it must be a Channel's.
1637}
1638function resolveEvent(e) {
1639 return e.detail || ([e.data][0] ? [e.data] : [e.code]);
1640}
1641
1642/* --- Initializing --- */
1643function init(client, opts) {
1644 client.servers = {};
1645 client.channels = {};
1646 client.users = {};
1647 client.directMessages = {};
1648 client.internals = {
1649 oauth: {},
1650 version: CURRENT_VERSION,
1651 settings: {}
1652 };
1653 client._connecting = true;
1654
1655 setupPing(client.internals);
1656 return getToken(client, opts);
1657}
1658function getToken(client, opts) {
1659 if (opts.token) return getGateway(client, opts.token);
1660 if (!isNode) {
1661 //Read from localStorage? Sounds like a bad idea, but I'll leave this here.
1662 }
1663}
1664function getGateway(client, token) {
1665 client.internals.token = token;
1666
1667 return APIRequest('get', Endpoints.GATEWAY, function (err, res) {
1668 if (err || !goodResponse(res)) {
1669 client._connecting = false;
1670 return client.emit("disconnect", "Error GETing gateway:\n" + stringifyError(res), 0);
1671 }
1672 return startConnection(client, (res.body.url + "/?encoding=json&v=" + GATEWAY_VERSION));
1673 });
1674}
1675function startConnection(client, gateway) {
1676 client._ws = new Websocket(gateway);
1677 client.internals.gatewayUrl = gateway;
1678
1679 client._ws.once('close', handleWSClose.bind(client));
1680 client._ws.once('error', handleWSClose.bind(client));
1681 client._ws.on('message', handleWSMessage.bind(client));
1682}
1683function getOfflineUsers(client, servArr, callback) {
1684 if (!servArr[0]) return call(callback);
1685
1686 send(client._ws, Payloads.OFFLINE_USERS(servArr));
1687 return setTimeout( getOfflineUsers, 0, client, servArr, callback );
1688}
1689function checkForAllServers(client, ready, message) {
1690 var all = Object.keys(client.servers).every(function(s) {
1691 return !client.servers[s].unavailable;
1692 });
1693 if (all || ready[0]) return void(client._ready = true && client.emit('ready', message));
1694 return setTimeout(checkForAllServers, 0, client, ready, message);
1695}
1696function setupPing(obj) {
1697 applyProperties(obj, [
1698 ["_pings", []],
1699 ["_lastHB", 0]
1700 ]);
1701 Object.defineProperty(obj, 'ping', {
1702 get: function() {
1703 return ((obj._pings.reduce(function(p, c) { return p + c; }, 0) / obj._pings.length) || 0) | 0;
1704 },
1705 set: function() {}
1706 });
1707}
1708function validateShard(shard) {
1709 return (
1710 type(shard) === 'array' &&
1711 shard.length === 2 &&
1712 shard[0] <= shard[1] &&
1713 shard[1] > 1
1714 ) ? shard : null;
1715}
1716function identifyOrResume(client) {
1717 var payload, internals = client.internals;
1718
1719 if (internals.sequence && internals.token && internals.sessionID) {
1720 payload = Payloads.RESUME(client);
1721 } else {
1722 payload = Payloads.IDENTIFY(client);
1723 if (client._shard) payload.d.shard = client._shard;
1724 }
1725 client._connecting = false;
1726
1727 return send(client._ws, payload);
1728}
1729
1730/* - Functions - Websocket Handling - */
1731function handleWSMessage(data, flags) {
1732 var message = decompressWSMessage(data, flags);
1733 var _data = message.d;
1734 var client = this, user, server, member, old,
1735 userID, serverID, channelID, currentVCID,
1736 mute, deaf, self_mute, self_deaf;
1737
1738 switch (message.op) {
1739 case 0:
1740 //Event dispatched - update our sequence number
1741 client.internals.sequence = message.s;
1742 break;
1743 case 9:
1744 //Disconnect after a Invalid Session
1745 //Disconnect the client if the client has received an invalid session id
1746 delete client.internals.sequence;
1747 delete client.internals.sessionID;
1748 client._ws && client._ws.close(1e3, 'Received an invalid session id');
1749 break;
1750 case 10:
1751 //Start keep-alive interval
1752 //Disconnect the client if no ping has been received
1753 //in 15 seconds (I think that's a decent duration)
1754 //Since v3 you the IDENTIFY/RESUME payload here.
1755 identifyOrResume(client);
1756 client.presenceStatus = 'online';
1757 client.connected = true;
1758
1759 client._mainKeepAlive = setInterval(function() {
1760 client.internals.heartbeat = setTimeout(function() {
1761 client._ws && client._ws.close(1e3, 'No heartbeat received');
1762 }, 15e3);
1763 client.internals._lastHB = Date.now();
1764 send(client._ws, Payloads.HEARTBEAT(client));
1765 }, _data.heartbeat_interval);
1766 break;
1767 case 11:
1768 clearTimeout(client.internals.heartbeat);
1769 client.internals._pings.unshift(Date.now() - client.internals._lastHB);
1770 client.internals._pings = client.internals._pings.slice(0, 10);
1771 break;
1772 }
1773
1774 //Events
1775 client.emit('any', message);
1776 //TODO: Remove in v3
1777 client.emit('debug', message);
1778 switch (message.t) {
1779 case "READY":
1780 copyKeys(_data.user, client);
1781 client.internals.sessionID = _data.session_id;
1782
1783 getServerInfo(client, _data.guilds);
1784 getDirectMessages(client, _data.private_channels);
1785
1786 if (client.bot) client.getOauthInfo(function(err, res) {
1787 if (err) return console.log(err);
1788 client.internals.oauth = res;
1789 client.inviteURL = "https://discordapp.com/oauth2/authorize?client_id=" + res.id + "&scope=bot";
1790 });
1791 if (!client.bot) client.getAccountSettings(function(err, res) {
1792 if (err) return console.log(err);
1793 client.internals.settings = res;
1794 });
1795
1796 return (function() {
1797 if (client._ready) return;
1798 var ready = [false];
1799 setTimeout(function() { ready[0] = true; }, 3500);
1800 checkForAllServers(client, ready, message);
1801 })();
1802 case "MESSAGE_CREATE":
1803 client.emit('message', _data.author.username, _data.author.id, _data.channel_id, _data.content, message);
1804 emit(client, message, _data.author.username, _data.author.id, _data.channel_id, _data.content);
1805 return cacheMessage(client._messageCache, client._messageCacheLimit, _data.channel_id, _data);
1806 case "MESSAGE_UPDATE":
1807 try {
1808 emit(client, message, client._messageCache[_data.channel_id][_data.id], _data);
1809 } catch (e) { emit(client, message, undefined, _data); }
1810 return cacheMessage(client._messageCache, client._messageCacheLimit, _data.channel_id, _data);
1811 case "PRESENCE_UPDATE":
1812 if (!_data.guild_id) break;
1813
1814 serverID = _data.guild_id;
1815 userID = _data.user.id;
1816
1817 if (!client.users[userID]) client.users[userID] = new User(_data.user);
1818
1819 user = client.users[userID];
1820 member = client.servers[serverID].members[userID] || {};
1821
1822 copyKeys(_data.user, user);
1823 user.game = _data.game;
1824
1825 copyKeys(_data, member, ['user', 'guild_id', 'game']);
1826 client.emit('presence', user.username, user.id, member.status, user.game, message);
1827 break;
1828 case "USER_UPDATE":
1829 copyKeys(_data, client);
1830 break;
1831 case "USER_SETTINGS_UPDATE":
1832 copyKeys(_data, client.internals);
1833 break;
1834 case "GUILD_CREATE":
1835 /*The lib will attempt to create the server using the response from the
1836 REST API, if the user using the lib creates the server. There are missing keys, however.
1837 So we still need this GUILD_CREATE event to fill in the blanks.
1838 If It's not our created server, then there will be no server with that ID in the cache,
1839 So go ahead and create one.*/
1840 client.servers[_data.id] = new Server(client, _data);
1841 return emit(client, message, client.servers[_data.id]);
1842 case "GUILD_UPDATE":
1843 old = copy(client.servers[_data.id]);
1844 Server.update(client, _data);
1845 return emit(client, message, old, client.servers[_data.id]);
1846 case "GUILD_DELETE":
1847 emit(client, message, client.servers[_data.id]);
1848 return delete(client.servers[_data.id]);
1849 case "GUILD_MEMBER_ADD":
1850 client.users[_data.user.id] = new User(_data.user);
1851 client.servers[_data.guild_id].members[_data.user.id] = new Member(client, client.servers[_data.guild_id], _data);
1852 client.servers[_data.guild_id].member_count += 1;
1853 return emit(client, message, client.servers[_data.guild_id].members[_data.user.id]);
1854 case "GUILD_MEMBER_UPDATE":
1855 old = copy(client.servers[_data.guild_id].members[_data.user.id]);
1856 Member.update(client, client.servers[_data.guild_id], _data);
1857 return emit(client, message, old, client.servers[_data.guild_id].members[_data.user.id]);
1858 case "GUILD_MEMBER_REMOVE":
1859 if (_data.user && _data.user.id === client.id) return;
1860 client.servers[_data.guild_id].member_count -= 1;
1861 emit(client, message, client.servers[_data.guild_id].members[_data.user.id]);
1862 return delete(client.servers[_data.guild_id].members[_data.user.id]);
1863 case "GUILD_ROLE_CREATE":
1864 client.servers[_data.guild_id].roles[_data.role.id] = new Role(_data.role);
1865 return emit(client, message, client.servers[_data.guild_id].roles[_data.role.id]);
1866 case "GUILD_ROLE_UPDATE":
1867 server = client.servers[_data.guild_id];
1868 old = copy(server.roles[_data.role.id]);
1869 Role.update(server, _data);
1870 Object.keys(server.members).forEach(function(memberID) {
1871 var member = server.members[memberID];
1872 if ( member.roles.indexOf(_data.role.id) < 0 ) return;
1873 member.color = colorFromRole(server, member);
1874 });
1875 return emit(client, message, old, server.roles[_data.role.id]);
1876 case "GUILD_ROLE_DELETE":
1877 emit(client, message, client.servers[_data.guild_id].roles[_data.role_id]);
1878 return delete(client.servers[_data.guild_id].roles[_data.role_id]);
1879 case "CHANNEL_CREATE":
1880 channelID = _data.id;
1881
1882 if (_data.is_private) {
1883 if (client.directMessages[channelID]) return;
1884 client.directMessages[channelID] = new DMChannel(client._uIDToDM, _data);
1885 return emit(client, message, client.directMessages[channelID]);
1886 }
1887
1888 if (client.channels[channelID]) return;
1889 client.channels[channelID] = new Channel(client, client.servers[_data.guild_id], _data);
1890 return emit(client, message, client.channels[channelID]);
1891 case "CHANNEL_UPDATE":
1892 old = copy(client.channels[_data.id]);
1893 Channel.update(client, _data);
1894 return emit(client, message, old, client.channels[_data.id]);
1895 case "CHANNEL_DELETE":
1896 if (_data.is_private === true) {
1897 emit(client, message, client.directMessages[_data.id]);
1898 delete(client.directMessages[_data.id]);
1899 return delete(client._uIDToDM[_data.recipient.id]);
1900 }
1901 emit(client, message, client.servers[_data.guild_id].channels[_data.id]);
1902 delete(client.servers[_data.guild_id].channels[_data.id]);
1903 return delete(client.channels[_data.id]);
1904 case "GUILD_EMOJIS_UPDATE":
1905 old = copy(client.servers[_data.guild_id].emojis);
1906 Emoji.update(client.servers[_data.guild_id], _data.emojis);
1907 return emit(client, message, old, client.servers[_data.guild_id].emojis);
1908 case "VOICE_STATE_UPDATE":
1909 serverID = _data.guild_id;
1910 channelID = _data.channel_id;
1911 userID = _data.user_id;
1912 server = client.servers[serverID];
1913 mute = !!_data.mute;
1914 self_mute = !!_data.self_mute;
1915 deaf = !!_data.deaf;
1916 self_deaf = !!_data.self_deaf;
1917
1918 try {
1919 currentVCID = server.members[userID].voice_channel_id;
1920 if (currentVCID) delete( server.channels[currentVCID].members[userID] );
1921 if (channelID) server.channels[channelID].members[userID] = _data;
1922 server.members[userID].mute = (mute || self_mute);
1923 server.members[userID].deaf = (deaf || self_deaf);
1924 server.members[userID].voice_channel_id = channelID;
1925 } catch(e) {}
1926
1927 if (userID === client.id) {
1928 server.self_mute = self_mute;
1929 server.self_deaf = self_deaf;
1930 if (channelID === null) {
1931 if (server.voiceSession) leaveVoiceChannel(client, server.voiceSession.channelID);
1932 server.voiceSession = null;
1933 } else {
1934 if (!server.voiceSession) {
1935 server.voiceSession = getVoiceSession(client, channelID, server);
1936 }
1937 if (channelID !== server.voiceSession.channelID) {
1938 delete( client._vChannels[server.voiceSession.channelID] );
1939 getVoiceSession(client, channelID, server).channelID = channelID;
1940 }
1941
1942 server.voiceSession.session = _data.session_id;
1943 server.voiceSession.self_mute = self_mute;
1944 server.voiceSession.self_deaf = self_deaf;
1945 }
1946 }
1947 break;
1948 case "VOICE_SERVER_UPDATE":
1949 serverID = _data.guild_id;
1950 server = client.servers[serverID];
1951 server.voiceSession.token = _data.token;
1952 server.voiceSession.severID = serverID;
1953 server.voiceSession.endpoint = _data.endpoint;
1954 joinVoiceChannel(client, server.voiceSession);
1955 break;
1956 case "GUILD_MEMBERS_CHUNK":
1957 serverID = _data.guild_id;
1958 if (!client.servers[serverID].members) client.servers[serverID].members = {};
1959
1960 _data.members.forEach(function(member) {
1961 var uID = member.user.id;
1962 var members = client.servers[serverID].members;
1963 if (members[uID]) return;
1964 if (!client.users[uID]) client.users[uID] = new User(member.user);
1965 members[uID] = new Member(client, client.servers[serverID], member);
1966 });
1967 var all = Object.keys(client.servers).every(function(server) {
1968 server = client.servers[server];
1969 return server.member_count === Object.keys(server.members).length;
1970 });
1971
1972 if (all) return client.emit("allUsers");
1973 break;
1974 case "GUILD_SYNC":
1975 _data.members.forEach(function(member) {
1976 var uID = member.user.id;
1977 if (!client.users[uID]) client.users[uID] = new User(member.user);
1978 client.servers[_data.id].members[uID] = new Member(client, client.servers[_data.id], member);
1979 });
1980
1981 _data.presences.forEach(function(presence) {
1982 var uID = presence.user.id;
1983 var members = client.servers[_data.id].members;
1984 if (!members[uID]) return void(new User(presence.user));
1985 delete(presence.user);
1986 copyKeys(presence, members[uID]);
1987 });
1988 client.servers[_data.id].large = _data.large;
1989 break;
1990 }
1991 return emit(client, message);
1992}
1993function handleWSClose(code, data) {
1994 var client = this;
1995 var eMsg = Discord.Codes.WebSocket[code];
1996
1997 clearInterval(client._mainKeepAlive);
1998 client.connected = false;
1999 client.presenceStatus = "offline";
2000 removeAllListeners(client._ws, 'message');
2001
2002 if ([1001, 1006].indexOf(code) > -1) return getGateway(client, client.internals.token);
2003
2004 client._ready = false;
2005 client._ws = null;
2006
2007 client.emit("disconnect", eMsg, code);
2008}
2009
2010/* - Functions - Voice - */
2011function joinVoiceChannel(client, voiceSession) {
2012 var vWS, vUDP, endpoint = voiceSession.endpoint.split(":")[0];
2013 //handleVoiceChannelChange(client, voiceSession);
2014
2015 voiceSession.ws = {};
2016 voiceSession.udp = {};
2017 voiceSession.members = {};
2018 voiceSession.error = null;
2019 voiceSession.ready = false;
2020 voiceSession.joined = false;
2021 voiceSession.translator = {};
2022 voiceSession.wsKeepAlive = null;
2023 voiceSession.udpKeepAlive = null;
2024 voiceSession.keepAlivePackets = 0;
2025 voiceSession.emitter = new Emitter();
2026 if (isNode) voiceSession.keepAliveBuffer = new Buffer(8).fill(0);
2027 vWS = voiceSession.ws.connection = new Websocket("wss://" + endpoint);
2028
2029 if (isNode) return DNS.lookup(endpoint, function(err, address) {
2030 if (err) return void(voiceSession.error = err);
2031
2032 voiceSession.address = address;
2033 vUDP = voiceSession.udp.connection = UDP.createSocket("udp4");
2034
2035 vUDP.bind({exclusive: true});
2036 vUDP.once('message', handleUDPMessage.bind(client, voiceSession));
2037
2038 vWS.once('open', handlevWSOpen.bind(client, voiceSession));
2039 vWS.on('message', handlevWSMessage.bind(client, voiceSession));
2040 vWS.once('close', handlevWSClose.bind(client, voiceSession));
2041 });
2042
2043 vWS.once('open', handlevWSOpen.bind(client, voiceSession));
2044 vWS.on('message', handlevWSMessage.bind(client, voiceSession));
2045 vWS.once('close', handlevWSClose.bind(client, voiceSession));
2046 return void(voiceSession.joined = true);
2047}
2048
2049function leaveVoiceChannel(client, channelID, callback) {
2050 if (!client._vChannels[channelID]) return;
2051
2052 try {
2053 client._vChannels[channelID].ws.connection.close();
2054 client._vChannels[channelID].udp.connection.close();
2055 } catch(e) {}
2056
2057 send(client._ws, Payloads.UPDATE_VOICE(client.channels[channelID].guild_id, null, false, false));
2058
2059 return call(callback, [null]);
2060}
2061
2062function keepUDPAlive(VS) {
2063 if (!VS.keepAliveBuffer) return;
2064
2065 if (VS.keepAlivePackets > 4294967294) {
2066 VS.keepAlivePackets = 0;
2067 VS.keepAliveBuffer.fill(0);
2068 }
2069 VS.keepAliveBuffer.writeUIntLE(++VS.keepAlivePackets, 0, 6);
2070 try {
2071 return VS.udp.connection.send(VS.keepAliveBuffer, 0, VS.keepAliveBuffer.length, VS.ws.port, VS.address);
2072 } catch(e) {}
2073}
2074
2075function getVoiceSession(client, channelID, server) {
2076 if (!channelID) return null;
2077 return client._vChannels[channelID] ?
2078 client._vChannels[channelID] :
2079 client._vChannels[channelID] = (server && server.voiceSession) || {
2080 serverID: (server && server.id) || null,
2081 channelID: channelID,
2082 token: null,
2083 session: null,
2084 endpoint: null,
2085 self_mute: false,
2086 self_deaf: false
2087 };
2088}
2089
2090function checkVoiceReady(voiceSession, callback) {
2091 return setTimeout(function() {
2092 if (voiceSession.error) return call(callback, [voiceSession.error]);
2093 if (voiceSession.joined) return call(callback, [null, voiceSession.emitter]);
2094 return checkVoiceReady(voiceSession, callback);
2095 }, 1);
2096}
2097
2098/* - Functions - Voice - Handling - */
2099
2100function handlevWSOpen(voiceSession) {
2101 return send(voiceSession.ws.connection, Payloads.VOICE_IDENTIFY(this.id, voiceSession));
2102}
2103function handlevWSMessage(voiceSession, vMessage, vFlags) {
2104 var client = this, vData = decompressWSMessage(vMessage, vFlags);
2105 switch (vData.op) {
2106 case 2: //Ready (Actually means you're READY to initiate the UDP connection)
2107 copyKeys(vData.d, voiceSession.ws);
2108 voiceSession.wsKeepAlive = setInterval(send, vData.d.heartbeat_interval, voiceSession.ws.connection, { "op": 3, "d": null });
2109
2110 if (!isNode) return;
2111
2112 var udpDiscPacket = new Buffer(70);
2113 udpDiscPacket.writeUIntBE(vData.d.ssrc, 0, 4);
2114 voiceSession.udp.connection.send(
2115 udpDiscPacket, 0, udpDiscPacket.length, vData.d.port, voiceSession.address,
2116 function(err) { if (err) {leaveVoiceChannel(client, voiceSession.channelID); handleErrCB("UDP discovery error", callback); } }
2117 );
2118
2119 voiceSession.udpKeepAlive = setInterval(keepUDPAlive, 5000, voiceSession);
2120 break;
2121 case 4: //Session Discription (Actually means you're ready to send audio... stupid Discord Devs :I)
2122 voiceSession.selectedMode = vData.d.mode;
2123 voiceSession.secretKey = vData.d.secret_key;
2124 voiceSession.joined = true;
2125 voiceSession.ready = true;
2126 break;
2127 case 5: //Speaking (At least this isn't confusing!)
2128 voiceSession.emitter.emit('speaking', vData.d.user_id, vData.d.ssrc, vData.d.speaking);
2129 break;
2130 }
2131}
2132function handlevWSClose(voiceSession) {
2133 //Emit the disconnect event first
2134 voiceSession.emitter.emit("disconnect", voiceSession.channelID);
2135 voiceSession.emitter = null
2136 //Kill encoder and decoders
2137 var audio = voiceSession.audio, members = voiceSession.members;
2138 if (audio) {
2139 if (audio._systemEncoder) audio._systemEncoder.kill();
2140 if (audio._mixedDecoder) audio._mixedDecoder.destroy();
2141 }
2142
2143 Object.keys(members).forEach(function(ID) {
2144 var member = members[ID];
2145 if (member.decoder) member.decoder.destroy();
2146 });
2147
2148 //Clear intervals and remove listeners
2149 clearInterval(voiceSession.wsKeepAlive);
2150 clearInterval(voiceSession.udpKeepAlive);
2151 removeAllListeners(voiceSession.emitter);
2152 removeAllListeners(voiceSession.udp.connection, 'message');
2153 removeAllListeners(voiceSession.ws.connection, 'message');
2154
2155 return delete(this._vChannels[voiceSession.channelID]);
2156}
2157
2158function handleUDPMessage(voiceSession, msg, rinfo) {
2159 var buffArr = JSON.parse(JSON.stringify(msg)).data, client = this, vDiscIP = "", vDiscPort;
2160 for (var i=4; i<buffArr.indexOf(0, i); i++) {
2161 vDiscIP += String.fromCharCode(buffArr[i]);
2162 }
2163 vDiscPort = msg.readUIntLE(msg.length - 2, 2).toString(10);
2164 return send(voiceSession.ws.connection, Payloads.VOICE_DISCOVERY(vDiscIP, vDiscPort, 'xsalsa20_poly1305'));
2165}
2166
2167/* - Functions - Voice - AudioCallback - */
2168function AudioCB(voiceSession, audioChannels, encoder, maxStreamSize) {
2169 //With the addition of the new Stream API, `playAudioFile`, `stopAudioFile` and `send`
2170 //will be removed. However they're deprecated for now, hence the code repetition.
2171 if (maxStreamSize && !Opus) Opus = require('cjopus');
2172 Stream.Duplex.call(this);
2173 var ACBI = this;
2174
2175 this.audioChannels = audioChannels;
2176 this.members = voiceSession.members;
2177
2178 applyProperties(this, [
2179 ["_sequence", 0],
2180 ["_timestamp", 0],
2181 ["_readable", false],
2182 ["_streamRef", null],
2183 ["_startTime", null],
2184 ["_systemEncoder", null],
2185 ["_playingAudioFile", false],
2186 ["_voiceSession", voiceSession],
2187 ["_port", voiceSession.ws.port],
2188 ["_address", voiceSession.address],
2189 ["_decodeNonce", new Uint8Array(24)],
2190 ["_vUDP", voiceSession.udp.connection],
2191 ["_secretKey", new Uint8Array(voiceSession.secretKey)],
2192 ["_mixedDecoder", Opus && new Opus.OpusEncoder( 48000, audioChannels ) || null]
2193 ]);
2194
2195 createAudioEncoder(this, encoder);
2196
2197 this._write = function _write(chunk, encoding, callback) {
2198 ACBI._systemEncoder.stdin.write(chunk);
2199 return callback();
2200 };
2201 this._read = function _read() {};
2202 this.stop = function stop() {
2203 return this._systemEncoder.stdout.read = function() { return null };
2204 };
2205
2206 if (maxStreamSize) {
2207 voiceSession.ws.connection.on('message', function(data, flags) {
2208 data = decompressWSMessage(data, flags);
2209
2210 if (data.op !== 5) return;
2211 if (!voiceSession.members[data.d.user_id]) {
2212 voiceSession.members[data.d.user_id] = new Stream.Readable({
2213 highWaterMark: maxStreamSize,
2214 read: function(s) {}
2215 });
2216 voiceSession.members[data.d.user_id].decoder = new Opus.OpusEncoder( 48000, ACBI.audioChannels );
2217 ACBI.emit('newMemberStream', data.d.user_id, voiceSession.members[data.d.user_id]);
2218 }
2219
2220 voiceSession.members[data.d.user_id].ssrc = data.d.ssrc;
2221 voiceSession.translator[data.d.ssrc] = voiceSession.members[data.d.user_id];
2222 });
2223 this._vUDP.on('message', handleIncomingAudio.bind(this));
2224 }
2225}
2226if (isNode) Util.inherits(AudioCB, Stream.Duplex);
2227AudioCB.VoicePacket = (function() {
2228 if (!isNode) return;
2229 var header = new Buffer(12), nonce = new Uint8Array(24), output = new Buffer(2048);
2230
2231 header[0] = 0x80;
2232 header[1] = 0x78;
2233
2234 return function(packet, ssrc, sequence, timestamp, key) {
2235 header.writeUIntBE(sequence, 2, 2);
2236 header.writeUIntBE(timestamp, 4, 4);
2237 header.writeUIntBE(ssrc, 8, 4);
2238 //<Buffer 80 78 00 01 00 00 03 c0 00 00 00 01>
2239 nonce.set(header);
2240 //<Buffer 80 78 00 01 00 00 03 c0 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00>
2241
2242 var encrypted = new Buffer(
2243 NACL.secretbox(
2244 new Uint8Array(packet),
2245 nonce,
2246 key
2247 )
2248 );
2249
2250 header.copy(output);
2251 encrypted.copy(output, 12);
2252
2253 return output.slice(0, header.length + encrypted.length);
2254 };
2255})();
2256var ACBP = AudioCB.prototype;
2257//TODO: Remove in v3
2258ACBP.playAudioFile = function(location, callback) {
2259 if (!this._mixedDecoder) {
2260 if (!Opus) Opus = require('cjopus');
2261 this._mixedDecoder = new Opus.OpusEncoder( 48000, this.audioChannels );
2262 }
2263
2264 if (this._playingAudioFile) return handleErrCB("There is already a file being played.", callback);
2265 var encs = ['ffmpeg', 'avconv'], selection, enc, ACBI = this;
2266
2267 this._playingAudioFile = true;
2268 selection = chooseAudioEncoder(encs);
2269
2270 if (!selection) return console.log("You need either 'ffmpeg' or 'avconv' and they need to be added to PATH");
2271
2272 enc = ChildProc.spawn(selection , [
2273 '-i', location,
2274 '-f', 's16le',
2275 '-ar', '48000',
2276 '-ac', ACBI.audioChannels,
2277 'pipe:1'
2278 ], {stdio: ['pipe', 'pipe', 'ignore']});
2279 enc.stdout.once('end', function() {
2280 enc.kill();
2281 send(ACBI._voiceSession.ws.connection, Payloads.VOICE_SPEAK(0));
2282 ACBI._playingAudioFile = false;
2283 ACBI.emit('fileEnd');
2284 });
2285 enc.stdout.once('error', function(e) {
2286 enc.stdout.emit('end');
2287 });
2288 enc.stdout.once('readable', function() {
2289 send(ACBI._voiceSession.ws.connection, Payloads.VOICE_SPEAK(1));
2290 ACBI._startTime = new Date().getTime();
2291 prepareAudioOld(ACBI, enc.stdout, 1);
2292 });
2293 this._streamRef = enc;
2294};
2295//TODO: Remove in v3
2296ACBP.stopAudioFile = function(callback) {
2297 if (!this._playingAudioFile) return handleErrCB("There is no file being played", callback);
2298
2299 this._streamRef.stdout.end();
2300 this._streamRef.kill();
2301 this._playingAudioFile = false;
2302
2303 call(callback);
2304};
2305//TODO: Remove in v3
2306ACBP.send = function(stream) {
2307 if (!this._mixedDecoder) {
2308 if (!Opus) Opus = require('cjopus');
2309 this._mixedDecoder = new Opus.OpusEncoder( 48000, this.audioChannels );
2310 }
2311 send(this._voiceSession.ws.connection, Payloads.VOICE_SPEAK(1));
2312 this._startTime = new Date().getTime();
2313 prepareAudioOld(this, stream, 1);
2314};
2315
2316function prepareAudio(ACBI, readableStream, cnt) {
2317 var data = readableStream.read( 320 ) || readableStream.read(); //(128 [kb] * 20 [frame_size]) / 8 == 320
2318
2319 if (!data) {
2320 send(ACBI._voiceSession.ws.connection, Payloads.VOICE_SPEAK(0));
2321 ACBI._readable = false;
2322 return readableStream.emit('end');
2323 }
2324
2325 return setTimeout(function() {
2326 sendAudio(ACBI, data);
2327 prepareAudio(ACBI, readableStream, cnt + 1);
2328 }, 20 + ( (ACBI._startTime + cnt * 20) - Date.now() ));
2329}
2330
2331//TODO: Remove in v3
2332function prepareAudioOld(ACBI, readableStream) {
2333 var done = false;
2334
2335 readableStream.on('end', function() {
2336 done = true;
2337 send(ACBI._voiceSession.ws.connection, Payloads.VOICE_SPEAK(0));
2338 });
2339
2340 _prepareAudio(ACBI, readableStream, 1);
2341
2342 function _prepareAudio(ACBI, readableStream, cnt) {
2343 if (done) return;
2344 var buffer, encoded;
2345
2346 buffer = readableStream.read( 1920 * ACBI.audioChannels );
2347 encoded = [0xF8, 0xFF, 0xFE];
2348
2349 if ((buffer && buffer.length === 1920 * ACBI.audioChannels) && !ACBI._mixedDecoder.destroyed) {
2350 encoded = ACBI._mixedDecoder.encode(buffer);
2351 }
2352
2353 return setTimeout(function() {
2354 sendAudio(ACBI, encoded);
2355 _prepareAudio(ACBI, readableStream, cnt + 1);
2356 }, 20 + ( (ACBI._startTime + cnt * 20) - Date.now() ));
2357 }
2358}
2359
2360function sendAudio(ACBI, buffer) {
2361 ACBI._sequence = (ACBI._sequence + 1 ) < 0xFFFF ? ACBI._sequence + 1 : 0;
2362 ACBI._timestamp = (ACBI._timestamp + 960) < 0xFFFFFFFF ? ACBI._timestamp + 960 : 0;
2363 if (ACBI._voiceSession.self_mute) return;
2364 var audioPacket = AudioCB.VoicePacket(buffer, ACBI._voiceSession.ws.ssrc, ACBI._sequence, ACBI._timestamp, ACBI._secretKey);
2365
2366 try {
2367 //It throws a synchronous error if it fails (someone leaves the audio channel while playing audio)
2368 ACBI._vUDP.send(audioPacket, 0, audioPacket.length, ACBI._port, ACBI._address);
2369 } catch(e) { return; }
2370}
2371
2372function handleIncomingAudio(msg) {
2373 //The response from the UDP keep alive ping
2374 if (msg.length === 8 || this._voiceSession.self_deaf) return;
2375
2376 var header = msg.slice(0, 12),
2377 audio = msg.slice(12),
2378 ssrc = header.readUIntBE(8, 4),
2379 member = this._voiceSession.translator[ssrc],
2380 decrypted, decoded;
2381
2382 this._decodeNonce.set(header);
2383
2384 try {
2385 decrypted = new Buffer(
2386 NACL.secretbox.open(
2387 new Uint8Array(audio),
2388 this._decodeNonce,
2389 this._secretKey
2390 )
2391 );
2392
2393 if (member) {
2394 decoded = member.decoder.decode(decrypted);
2395 addToStreamBuffer(member, decoded);
2396 } else {
2397 decoded = this._mixedDecoder.decode(decrypted);
2398 }
2399
2400 addToStreamBuffer(this, decoded);
2401 this.emit('incoming', ssrc, decoded );
2402 } catch(e) {}
2403}
2404function addToStreamBuffer(RStream, data) {
2405 return RStream.push(new Buffer(data)) || !!RStream.read(data.length);
2406}
2407
2408function chooseAudioEncoder(players) {
2409 if (!players[0]) return null;
2410 var s = ChildProc.spawnSync(players.shift());
2411 return s.error ? chooseAudioEncoder(players) : s.file;
2412}
2413function createAudioEncoder(ACBI, encoder) {
2414 var enc = ACBI._systemEncoder;
2415 if (enc) {
2416 enc.stdout.emit('end');
2417 enc.kill();
2418 ACBI._systemEncoder = null;
2419 }
2420
2421 enc = ACBI._systemEncoder = ChildProc.spawn(encoder, [
2422 '-hide_banner',
2423 '-loglevel', 'error',
2424 '-i', 'pipe:0',
2425 '-map', '0:a',
2426 '-acodec', 'libopus',
2427 '-f', 'data',
2428 '-sample_fmt', 's16',
2429 '-vbr', 'off',
2430 '-compression_level', '10',
2431 '-ar', '48000',
2432 '-ac', ACBI.audioChannels,
2433 '-b:a', '128000',
2434 'pipe:1'
2435 ], {stdio: ['pipe', 'pipe', 'pipe']});
2436
2437 enc.stderr.once('data', function(d) {
2438 if (ACBI.listeners('error').length > 0) ACBI.emit('error', d.toString());
2439 });
2440
2441 enc.stdin.once('error', function(e) {
2442 enc.stdout.emit('end');
2443 enc.kill();
2444 });
2445
2446 enc.stdout.once('error', function(e) {
2447 enc.stdout.emit('end');
2448 enc.kill();
2449 });
2450 enc.stdout.once('end', function() {
2451 createAudioEncoder(ACBI, encoder);
2452 ACBI.emit('done');
2453 });
2454 enc.stdout.on('readable', function() {
2455 if (ACBI._readable) return;
2456
2457 ACBI._readable = true;
2458 send(ACBI._voiceSession.ws.connection, Payloads.VOICE_SPEAK(1));
2459 ACBI._startTime = new Date().getTime();
2460 prepareAudio(ACBI, enc.stdout, 1);
2461 });
2462}
2463
2464/* - DiscordClient - Classes - */
2465function Resource() {}
2466Object.defineProperty(Resource.prototype, "creationTime", {
2467 get: function() { return (+this.id / 4194304) + 1420070400000; },
2468 set: function() {}
2469});
2470[Server, Channel, DMChannel, User, Member, Role].forEach(function(p) {
2471 p.prototype = Object.create(Resource.prototype);
2472 Object.defineProperty(p.prototype, 'constructor', {value: p, enumerable: false});
2473});
2474
2475function Server(client, data) {
2476 var server = this;
2477 //Accept everything now and trim what we don't need, manually. Any data left in is fine, any data left out could lead to a broken lib.
2478 copyKeys(data, this);
2479 this.large = this.large || this.member_count > LARGE_THRESHOLD;
2480 this.voiceSession = null;
2481 this.self_mute = !!this.self_mute;
2482 this.self_deaf = !!this.self_deaf;
2483 if (data.unavailable) return;
2484
2485 //Objects so we can use direct property accessing without for loops
2486 this.channels = {};
2487 this.members = {};
2488 this.roles = {};
2489 this.emojis = {};
2490
2491 //Copy the data into the objects using IDs as keys
2492 data.channels.forEach(function(channel) {
2493 client.channels[channel.id] = new Channel(client, server, channel);
2494 });
2495 data.roles.forEach(function(role) {
2496 server.roles[role.id] = new Role(role);
2497 });
2498 data.members.forEach(function(member) {
2499 client.users[member.user.id] = new User(member.user);
2500 server.members[member.user.id] = new Member(client, server, member);
2501 });
2502 data.presences.forEach(function(presence) {
2503 var id = presence.user.id;
2504 if (!client.users[id] || !server.members[id]) return;
2505 delete(presence.user);
2506
2507 client.users[id].game = presence.game;
2508 server.members[id].status = presence.status;
2509 });
2510 data.emojis.forEach(function(emoji) {
2511 server.emojis[emoji.id] = new Emoji(emoji);
2512 });
2513 data.voice_states.forEach(function(vs) {
2514 var cID = vs.channel_id;
2515 var uID = vs.user_id;
2516 if (!server.channels[cID] || !server.members[uID]) return;
2517 server.channels[cID].members[uID] = vs;
2518 server.members[uID].voice_channel_id = cID;
2519 });
2520
2521 //Now we can get rid of any of the things we don't need anymore
2522 delete(this.voice_states);
2523 delete(this.presences);
2524}
2525function Channel(client, server, data) {
2526 var channel = this;
2527 this.members = {};
2528 this.permissions = { user: {}, role: {} };
2529 this.guild_id = server.id;
2530 copyKeys(data, this, ['permission_overwrites', 'emojis']);
2531 Object.defineProperty(server.channels, channel.id, {
2532 get: function() { return client.channels[channel.id]; },
2533 set: function(v) { client.channels[channel.id] = v; },
2534 enumerable: true,
2535 configurable: true
2536 });
2537 data.permission_overwrites.forEach(function(p) {
2538 var type = (p.type === 'member' ? 'user' : 'role');
2539 this.permissions[type][p.id] = {allow: p.allow, deny: p.deny};
2540 }, this);
2541
2542
2543 delete(this.is_private);
2544}
2545function DMChannel(translator, data) {
2546 copyKeys(data, this);
2547 translator[data.recipient.id] = data.id;
2548 delete(this.is_private);
2549}
2550function User(data) {
2551 copyKeys(data, this);
2552 this.bot = this.bot || false;
2553}
2554function Member(client, server, data) {
2555 copyKeys(data, this, ['user', 'joined_at',]);
2556 this.id = data.user.id;
2557 this.joined_at = Date.parse(data.joined_at);
2558 this.color = colorFromRole(server, this);
2559 ['username', 'discriminator', 'bot', 'avatar', 'game'].forEach(function(k) {
2560 if (k in Member.prototype) return;
2561
2562 Object.defineProperty(Member.prototype, k, {
2563 get: function() { return client.users[this.id][k]; },
2564 set: function(v) { client.users[this.id][k] = v; },
2565 enumerable: true,
2566 });
2567 });
2568}
2569function Role(data) {
2570 copyKeys(data, this, ['permissions']);
2571 //Use `permissions` from Discord, or `_permissions` if we're making it out of a cache.
2572 this._permissions = data._permissions || data.permissions;
2573}
2574function Emoji(data) {
2575 copyKeys(data, this);
2576}
2577
2578function Multipart() {
2579 this.boundary =
2580 "NodeDiscordIO" + "-" + CURRENT_VERSION;
2581 this.result = "";
2582}
2583Multipart.prototype.append = function(data) {
2584 /* Header */
2585 var str = "\r\n--";
2586 str += this.boundary + "\r\n";
2587 str += 'Content-Disposition: form-data; name="' + data[0] + '"';
2588 if (data[2]) {
2589 str += '; filename="' + data[2] + '"\r\n';
2590 str += 'Content-Type: application/octet-stream';
2591 }
2592 /* Body */
2593 str += "\r\n\r\n" + ( data[1] instanceof Buffer ? data[1] : new Buffer(String(data[1]), 'utf-8') ).toString('binary');
2594 this.result += str;
2595};
2596Multipart.prototype.finalize = function() {
2597 this.result += "\r\n--" + this.boundary + "--";
2598};
2599
2600Server.update = function(client, data) {
2601 if (!client.servers[data.id]) client.servers[data.id] = {}; // new Server(client, data)?
2602 for (var key in data) {
2603 if (key === 'roles') {
2604 data[key].forEach(function(r) {
2605 client.servers[data.id].roles[r.id] = new Role(r);
2606 });
2607 continue;
2608 }
2609 if (key === 'emojis') continue;
2610 client.servers[data.id][key] = data[key];
2611 }
2612};
2613Channel.update = function(client, data) {
2614 if (!client.channels[data.id]) client.channels[data.id] = {}; // new Channel(client, data)?
2615 for (var key in data) {
2616 if (key === 'permission_overwrites') {
2617 data[key].forEach(function(p) {
2618 var type = (p.type === 'member' ? 'user' : 'role');
2619 client.channels[data.id].permissions[type][p.id] = {
2620 allow: p.allow,
2621 deny: p.deny
2622 };
2623 });
2624 continue;
2625 }
2626 client.channels[data.id][key] = data[key];
2627 }
2628 delete(client.channels[data.id].is_private);
2629};
2630Member.update = function(client, server, data) {
2631 if (!server.members[data.user.id]) return server.members[data.user.id] = new Member(client, server, data);
2632 var member = server.members[data.user.id];
2633 copyKeys(data, member, ['user']);
2634 member.color = colorFromRole(server, member);
2635};
2636Role.update = function(server, data) {
2637 if (!server.roles[data.role.id]) server.roles[data.role.id] = {}; // new Role(data)?
2638 server.roles[data.role.id]._permissions = data.role.permissions;
2639 copyKeys(data.role, server.roles[data.role.id], ['permissions']);
2640};
2641Emoji.update = function(server, data) {
2642 server.emojis = {};
2643 data.forEach(function(emoji) {
2644 server.emojis[emoji.id] = new Emoji(emoji);
2645 });
2646}
2647
2648Object.defineProperty(Role.prototype, "permission_values", {
2649 get: function() { return this; },
2650 set: function() {},
2651 enumerable: true
2652});
2653Object.defineProperty(User.prototype, 'avatarURL', {
2654 get: function() {
2655 if (!this.avatar) return null;
2656 var animated = this.avatar.indexOf("a_") > -1;
2657 return Discord.Endpoints.CDN + "/avatars/" + this.id + "/" + this.avatar + (animated ? ".gif" : ".webp");
2658 },
2659 set: function() {}
2660});
2661
2662//Discord.OAuth;
2663Discord.version = CURRENT_VERSION;
2664Discord.Emitter = Emitter;
2665Discord.Codes = {};
2666Discord.Codes.WebSocket = {
2667 "0" : "Gateway Error",
2668 "4000": "Unknown Error",
2669 "4001": "Unknown Opcode",
2670 "4002": "Decode Error",
2671 "4003": "Not Authenticated",
2672 "4004": "Authentication Failed",
2673 "4005": "Already Authenticated",
2674 "4006": "Session Not Valid",
2675 "4007": "Invalid Sequence Number",
2676 "4008": "Rate Limited",
2677 "4009": "Session Timeout",
2678 "4010": "Invalid Shard"
2679};
2680Discord.Colors = {
2681 DEFAULT: 0,
2682 AQUA: 1752220,
2683 GREEN: 3066993,
2684 BLUE: 3447003,
2685 PURPLE: 10181046,
2686 GOLD: 15844367,
2687 ORANGE: 15105570,
2688 RED: 15158332,
2689 GREY: 9807270,
2690 DARKER_GREY: 8359053,
2691 NAVY: 3426654,
2692 DARK_AQUA: 1146986,
2693 DARK_GREEN: 2067276,
2694 DARK_BLUE: 2123412,
2695 DARK_PURPLE: 7419530,
2696 DARK_GOLD: 12745742,
2697 DARK_ORANGE: 11027200,
2698 DARK_RED: 10038562,
2699 DARK_GREY: 9936031,
2700 LIGHT_GREY: 12370112,
2701 DARK_NAVY: 2899536
2702};
2703Discord.Permissions = {
2704 GENERAL_CREATE_INSTANT_INVITE: 0,
2705 GENERAL_KICK_MEMBERS: 1,
2706 GENERAL_BAN_MEMBERS: 2,
2707 GENERAL_ADMINISTRATOR: 3,
2708 GENERAL_MANAGE_CHANNELS: 4,
2709 GENERAL_MANAGE_GUILD: 5,
2710 GENERAL_MANAGE_ROLES: 28,
2711 GENERAL_MANAGE_NICKNAMES: 27,
2712 GENERAL_CHANGE_NICKNAME: 26,
2713 GENERAL_MANAGE_WEBHOOKS: 29,
2714 GENERAL_MANAGE_EMOJIS: 30,
2715
2716 TEXT_ADD_REACTIONS: 6,
2717 TEXT_READ_MESSAGES: 10,
2718 TEXT_SEND_MESSAGES: 11,
2719 TEXT_SEND_TTS_MESSAGE: 12,
2720 TEXT_MANAGE_MESSAGES: 13,
2721 TEXT_EMBED_LINKS: 14,
2722 TEXT_ATTACH_FILES: 15,
2723 TEXT_READ_MESSAGE_HISTORY: 16,
2724 TEXT_MENTION_EVERYONE: 17,
2725 TEXT_EXTERNAL_EMOJIS: 18,
2726
2727 VOICE_CONNECT: 20,
2728 VOICE_SPEAK: 21,
2729 VOICE_MUTE_MEMBERS: 22,
2730 VOICE_DEAFEN_MEMBERS: 23,
2731 VOICE_MOVE_MEMBERS: 24,
2732 VOICE_USE_VAD: 25,
2733};
2734Discord.LogLevels = {
2735 Verbose: 0,
2736 Info: 1,
2737 Warn: 2,
2738 Error: 3
2739};
2740
2741Object.keys(Discord.Permissions).forEach(function(pn) {
2742 Object.defineProperty(Role.prototype, pn, {
2743 get: getPerm( Discord.Permissions[pn] ),
2744 set: setPerm( Discord.Permissions[pn] ),
2745 enumerable: true
2746 });
2747});
2748
2749/* Wrappers */
2750function Emitter() {
2751 var emt = this;
2752 if (isNode) {
2753 EE.call(this);
2754 if (this.prototype) return Util.inherits(this, EE);
2755 return new EE();
2756 }
2757 //Thank you, http://stackoverflow.com/a/24216547
2758 function _Emitter() {
2759 var eventTarget = document.createDocumentFragment();
2760 ["addEventListener", "dispatchEvent", "removeEventListener"].forEach(function(method) {
2761 if (!this[method]) this[method] = eventTarget[method].bind(eventTarget);
2762 }, this);
2763 }
2764 //But I did the rest myself! D:<
2765 _Emitter.call(this);
2766 this._evts = {};
2767 this.on = function(eName, eFunc) {
2768 if (!emt._evts[eName]) emt._evts[eName] = [];
2769 emt._evts[eName].push(eOn);
2770
2771 return this.addEventListener(eName, eOn);
2772
2773 function eOn(e) {
2774 return eFunc.apply(null, resolveEvent(e));
2775 }
2776 };
2777 this.once = function(eName, eFunc) {
2778 if (!emt._evts[eName]) emt._evts[eName] = [];
2779 emt._evts[eName].push(eOnce);
2780
2781 return this.addEventListener(eName, eOnce);
2782
2783 function eOnce(e) {
2784 eFunc.apply(null, resolveEvent(e));
2785 return emt.removeListener(eName, eOnce);
2786 }
2787 };
2788 this.removeListener = function(eName, eFunc) {
2789 if (emt._evts[eName]) emt._evts[eName].splice(emt._evts[eName].lastIndexOf(eFunc), 1);
2790 return this.removeEventListener(eName, eFunc);
2791 };
2792 this.emit = function(eName) {
2793 return this.dispatchEvent( new CustomEvent(eName, {'detail': Array.prototype.slice.call(arguments, 1) }) );
2794 };
2795 return this;
2796}
2797
2798function Websocket(url, opts) {
2799 if (isNode) return new (require('ws'))(url, opts);
2800 return Emitter.call(new WebSocket(url));
2801}
2802
2803/* Endpoints */
2804(function () {
2805 var API = "https://discordapp.com/api";
2806 var CDN = "http://cdn.discordapp.com";
2807 var ME = API + "/users/@me";
2808 Endpoints = Discord.Endpoints = {
2809 API: API,
2810 CDN: CDN,
2811
2812 ME: ME,
2813 NOTE: function(userID) {
2814 return ME + "/notes/" + userID;
2815 },
2816 LOGIN: API + "/auth/login",
2817 OAUTH: API + "/oauth2/applications/@me",
2818 GATEWAY: API + "/gateway",
2819 SETTINGS: ME + "/settings",
2820
2821 SERVERS: function(serverID) {
2822 return API + "/guilds" + (serverID ? "/" + serverID : "");
2823 },
2824 SERVERS_PERSONAL: function(serverID) {
2825 return this.ME + "/guilds" + (serverID ? "/" + serverID : ""); //Method to list personal servers?
2826 },
2827 SERVER_EMOJIS: function(serverID, emojiID) {
2828 return this.SERVERS(serverID) + "/emojis" + (emojiID ? "/" + emojiID : "");
2829 },
2830
2831 CHANNEL: function(channelID) {
2832 return API + "/channels/" + channelID;
2833 },
2834
2835 MEMBERS: function(serverID, userID) {
2836 return this.SERVERS(serverID) + "/members" + (userID ? "/" + userID : "");
2837 },
2838 MEMBER_ROLES: function(serverID, userID, roleID) {
2839 return this.MEMBERS(serverID, userID) + "/roles" + (roleID ? "/" + roleID : "");
2840 },
2841
2842 USER: function(userID) {
2843 return API + "/users/" + userID;
2844 },
2845
2846 ROLES: function(serverID, roleID) {
2847 return this.SERVERS(serverID) + "/roles" + (roleID ? "/" + roleID : "");
2848 },
2849
2850 BANS: function(serverID, userID) {
2851 return this.SERVERS(serverID) + "/bans" + (userID ? "/" + userID : "");
2852 },
2853
2854 MESSAGES: function(channelID, messageID) {
2855 return this.CHANNEL(channelID) + "/messages" + (messageID ? "/" + messageID : "");
2856 },
2857 PINNED_MESSAGES: function(channelID, messageID) {
2858 return this.CHANNEL(channelID) + "/pins" + (messageID ? "/" + messageID : "");
2859 },
2860
2861 MESSAGE_REACTIONS: function(channelID, messageID, reaction) {
2862 return this.MESSAGES(channelID, messageID) + "/reactions" + ( reaction ? ("/" + reaction) : "" );
2863 },
2864 USER_REACTIONS: function(channelID, messageID, reaction, userID) {
2865 return this.MESSAGE_REACTIONS(channelID, messageID, reaction) + '/' + ( (!userID || userID === this.id) ? '@me' : userID );
2866 },
2867
2868 INVITES: function(inviteCode) {
2869 return API + "/invite/" + inviteCode;
2870 },
2871
2872 SERVER_WEBHOOKS: function(serverID) {
2873 return this.SERVERS(serverID) + "/webhooks";
2874 },
2875 CHANNEL_WEBHOOKS: function(channelID) {
2876 return this.CHANNEL(channelID) +"/webhooks";
2877 },
2878
2879 WEBHOOKS: function(webhookID) {
2880 return API + "/webhooks/" + webhookID;
2881 },
2882
2883 BULK_DELETE: function(channelID) {
2884 return this.CHANNEL(channelID) + "/messages/bulk-delete";
2885 },
2886
2887 TYPING: function(channelID) {
2888 return this.CHANNEL(channelID) + "/typing";
2889 }
2890
2891 };
2892})();
2893
2894/* Payloads */
2895(function() {
2896 Payloads = {
2897 IDENTIFY: function(client) {
2898 return {
2899 op: 2,
2900 d: {
2901 token: client.internals.token,
2902 v: GATEWAY_VERSION,
2903 compress: isNode && !!Zlib.inflateSync,
2904 large_threshold: LARGE_THRESHOLD,
2905 properties: {
2906 $os: isNode ? require('os').platform() : navigator.platform,
2907 $browser:"discord.io",
2908 $device:"discord.io",
2909 $referrer:"",
2910 $referring_domain:""
2911 }
2912 }
2913 };
2914 },
2915 RESUME: function(client) {
2916 return {
2917 op: 6,
2918 d: {
2919 seq: client.internals.s,
2920 token: client.internals.token,
2921 session_id: client.internals.sessionID
2922 }
2923 };
2924 },
2925 HEARTBEAT: function(client) {
2926 return {op: 1, d: client.internals.sequence};
2927 },
2928 ALL_USERS: function(client) {
2929 return {op: 12, d: Object.keys(client.servers)};
2930 },
2931 STATUS: function(input) {
2932 return {
2933 op: 3,
2934 d: {
2935 status: type(input.idle_since) === 'number' ? 'idle' : input.status !== undefined ? input.status : null,
2936 afk: !!input.afk,
2937 since: type(input.idle_since) === 'number' || input.status === 'idle' ? Date.now() : null,
2938 game: type(input.game) === 'object' ?
2939 {
2940 name: input.game.name ? String(input.game.name) : null,
2941 type: input.game.type ? Number(input.game.type) : 0,
2942 url: input.game.url ? String(input.game.url) : null
2943 } :
2944 null
2945 }
2946 };
2947 },
2948 UPDATE_VOICE: function(serverID, channelID, self_mute, self_deaf) {
2949 return {
2950 op: 4,
2951 d: {
2952 guild_id: serverID,
2953 channel_id: channelID,
2954 self_mute: self_mute,
2955 self_deaf: self_deaf
2956 }
2957 };
2958 },
2959 OFFLINE_USERS: function(array) {
2960 return {
2961 op: 8,
2962 d: {
2963 guild_id: array.splice(0, 50),
2964 query: "",
2965 limit: 0
2966 }
2967 };
2968 },
2969 VOICE_SPEAK: function(v) {
2970 return {op:5, d:{ speaking: !!v, delay: 0 }};
2971 },
2972 VOICE_IDENTIFY: function(clientID, voiceSession) {
2973 return {
2974 op: 0,
2975 d: {
2976 server_id: voiceSession.serverID,
2977 user_id: clientID,
2978 session_id: voiceSession.session,
2979 token: voiceSession.token
2980 }
2981 };
2982 },
2983 VOICE_DISCOVERY: function(ip, port, mode) {
2984 return {
2985 op:1,
2986 d:{
2987 protocol:"udp",
2988 data:{
2989 address: ip,
2990 port: Number(port),
2991 mode: mode
2992 }
2993 }
2994 };
2995 }
2996 }
2997})();
2998
2999})(typeof exports === 'undefined'? this.Discord = {} : exports);