Commando
Loïc Bersier 6 years ago
commit 7b16ff8092

1
.gitignore vendored

@ -0,0 +1 @@
config.json

@ -0,0 +1,27 @@
exports.run = (client, message, args) => {
function clean(text) {
if (typeof (text) === "string")
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
else
return text;
}
if (message.author.id == "267065637183029248") {
try {
const code = args.join(" ");
let evaled = eval(code);
if (typeof evaled !== "string")
evaled = require("util").inspect(evaled);
message.channel.send(clean(evaled), { code: "xl" });
} catch (err) {
message.channel.send(`\`ERROR\` \`\`\`xl\n${clean(err)}\n\`\`\``);
}
}
else {
message.channel.send("Sick you thought");
}
}

@ -0,0 +1,20 @@
exports.run = (client, message, [mention, ...reason]) => {
const StaffRole = message.guild.roles.find("name", "Staff");
if (!StaffRole)
return console.log("The Staff role does not exist");
if (!message.member.roles.has(StaffRole.id))
return message.reply("You can't use this command.");
if (message.mentions.members.size === 0)
return message.reply("Please mention a user to kick");
if (!message.guild.me.hasPermission("KICK_MEMBERS"))
return message.reply("");
const kickMember = message.mentions.members.first();
kickMember.kick(reason.join(" ")).then(member => {
message.reply(`${member.user.username} was succesfully kicked.`);
});
};

@ -0,0 +1,3 @@
exports.run = (client, message, args) => {
message.channel.send("pong!").catch(console.error);
}

@ -0,0 +1,17 @@
exports.run = (client, message, args) => {
if (message.author.id == "267065637183029248") {
if(!args || args.size < 1) return message.reply("Must provide a command name to reload.");
const commandName = args[0];
if(!client.commands.has(commandName)) {
return message.reply("That command does not exist");
}
delete require.cache[require.resolve(`./${commandName}.js`)];
client.commands.delete(commandName);
const props = require(`./${commandName}.js`);
client.commands.set(commandName, props);
message.reply(`The command ${commandName} has been reloaded`);
};
}

@ -0,0 +1,3 @@
exports.run = (client, message, args) => {
message.channel.send("haha yes").catch(console.error);
}

@ -0,0 +1,14 @@
module.exports = (client, message) => {
if (message.author.bot) return;
if (message.content.indexOf(client.config.prefix) !== 0) return;
const args = message.content.slice(client.config.prefix.length).trim().split(/ +/g);
const command = args.shift().toLowerCase();
const cmd = client.commands.get(command);
if (!cmd) return;
cmd.run(client, message, args);
};

@ -0,0 +1,3 @@
module.exports = (client) => {
console.log(`Ready to server in ${client.channels.size} channels on ${client.guilds.size} servers, for a total of ${client.users.size} users.`);
};

@ -0,0 +1,39 @@
const Discord = require("discord.js");
const Enmap = require("enmap");
const fs = require("fs");
const client = new Discord.Client();
const config = require("./config.json");
const responseObject = require("./reply.json");
client.config = config;
fs.readdir("./events/", (err, files) => {
if (err) return console.error(err);
files.forEach(file => {
const event = require(`./events/${file}`);
let eventName = file.split(".")[0];
client.on(eventName, event.bind(null, client));
});
});
client.commands = new Enmap();
fs.readdir("./commands/", (err, files) => {
if (err) return console.error(err);
files.forEach(file => {
if (!file.endsWith(".js")) return;
let props = require(`./commands/${file}`);
let commandName = file.split(".")[0];
console.log(`Attempting to load command ${commandName}`);
client.commands.set(commandName, props);
});
});
client.on("message", (message) => {
if(responseObject[message.content]) {
message.channel.send(responseObject[message.content]);
}
});
client.login(config.token);

@ -0,0 +1,7 @@
language: node_js
node_js:
- "6"
- "node"
script: npm run travis
cache:
yarn: true

@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2017 Samuel Reed <samuel.trace.reed@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1 @@
{"/Users/samuelreed/git/forks/async-throttle/index.js":{"path":"/Users/samuelreed/git/forks/async-throttle/index.js","s":{"1":1,"2":7,"3":1,"4":6,"5":6,"6":6,"7":6,"8":6,"9":6,"10":1,"11":1,"12":3,"13":13,"14":13,"15":13,"16":1,"17":19,"18":1,"19":45,"20":6,"21":39,"22":13,"23":13,"24":13,"25":13,"26":39,"27":18,"28":6,"29":6,"30":1,"31":6,"32":6,"33":6,"34":1,"35":13,"36":13,"37":1},"b":{"1":[1,6],"2":[6,5],"3":[6,5],"4":[6,39],"5":[13,26],"6":[18,21],"7":[6,0]},"f":{"1":7,"2":3,"3":13,"4":19,"5":45,"6":6,"7":13},"fnMap":{"1":{"name":"Queue","line":3,"loc":{"start":{"line":3,"column":0},"end":{"line":3,"column":24}}},"2":{"name":"(anonymous_2)","line":22,"loc":{"start":{"line":22,"column":24},"end":{"line":22,"column":41}}},"3":{"name":"(anonymous_3)","line":23,"loc":{"start":{"line":23,"column":28},"end":{"line":23,"column":39}}},"4":{"name":"(anonymous_4)","line":31,"loc":{"start":{"line":31,"column":7},"end":{"line":31,"column":18}}},"5":{"name":"(anonymous_5)","line":36,"loc":{"start":{"line":36,"column":23},"end":{"line":36,"column":34}}},"6":{"name":"(anonymous_6)","line":55,"loc":{"start":{"line":55,"column":25},"end":{"line":55,"column":38}}},"7":{"name":"done","line":62,"loc":{"start":{"line":62,"column":0},"end":{"line":62,"column":16}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":14,"column":1}},"2":{"start":{"line":4,"column":2},"end":{"line":6,"column":3}},"3":{"start":{"line":5,"column":4},"end":{"line":5,"column":30}},"4":{"start":{"line":8,"column":2},"end":{"line":8,"column":26}},"5":{"start":{"line":9,"column":2},"end":{"line":9,"column":53}},"6":{"start":{"line":10,"column":2},"end":{"line":10,"column":19}},"7":{"start":{"line":11,"column":2},"end":{"line":11,"column":17}},"8":{"start":{"line":12,"column":2},"end":{"line":12,"column":16}},"9":{"start":{"line":13,"column":2},"end":{"line":13,"column":31}},"10":{"start":{"line":16,"column":0},"end":{"line":20,"column":2}},"11":{"start":{"line":22,"column":0},"end":{"line":28,"column":3}},"12":{"start":{"line":23,"column":2},"end":{"line":27,"column":4}},"13":{"start":{"line":24,"column":4},"end":{"line":24,"column":75}},"14":{"start":{"line":25,"column":4},"end":{"line":25,"column":16}},"15":{"start":{"line":26,"column":4},"end":{"line":26,"column":24}},"16":{"start":{"line":30,"column":0},"end":{"line":34,"column":3}},"17":{"start":{"line":32,"column":4},"end":{"line":32,"column":43}},"18":{"start":{"line":36,"column":0},"end":{"line":53,"column":2}},"19":{"start":{"line":37,"column":2},"end":{"line":39,"column":3}},"20":{"start":{"line":38,"column":4},"end":{"line":38,"column":11}},"21":{"start":{"line":40,"column":2},"end":{"line":45,"column":3}},"22":{"start":{"line":41,"column":4},"end":{"line":41,"column":32}},"23":{"start":{"line":42,"column":4},"end":{"line":42,"column":19}},"24":{"start":{"line":43,"column":4},"end":{"line":43,"column":20}},"25":{"start":{"line":44,"column":4},"end":{"line":44,"column":16}},"26":{"start":{"line":47,"column":2},"end":{"line":52,"column":3}},"27":{"start":{"line":48,"column":4},"end":{"line":51,"column":5}},"28":{"start":{"line":49,"column":6},"end":{"line":49,"column":30}},"29":{"start":{"line":50,"column":6},"end":{"line":50,"column":27}},"30":{"start":{"line":55,"column":0},"end":{"line":60,"column":2}},"31":{"start":{"line":56,"column":2},"end":{"line":59,"column":3}},"32":{"start":{"line":57,"column":4},"end":{"line":57,"column":22}},"33":{"start":{"line":58,"column":4},"end":{"line":58,"column":16}},"34":{"start":{"line":62,"column":0},"end":{"line":65,"column":1}},"35":{"start":{"line":63,"column":2},"end":{"line":63,"column":17}},"36":{"start":{"line":64,"column":2},"end":{"line":64,"column":14}},"37":{"start":{"line":67,"column":0},"end":{"line":67,"column":23}}},"branchMap":{"1":{"line":4,"type":"if","locations":[{"start":{"line":4,"column":2},"end":{"line":4,"column":2}},{"start":{"line":4,"column":2},"end":{"line":4,"column":2}}]},"2":{"line":8,"type":"binary-expr","locations":[{"start":{"line":8,"column":12},"end":{"line":8,"column":19}},{"start":{"line":8,"column":23},"end":{"line":8,"column":25}}]},"3":{"line":9,"type":"binary-expr","locations":[{"start":{"line":9,"column":21},"end":{"line":9,"column":40}},{"start":{"line":9,"column":44},"end":{"line":9,"column":52}}]},"4":{"line":37,"type":"if","locations":[{"start":{"line":37,"column":2},"end":{"line":37,"column":2}},{"start":{"line":37,"column":2},"end":{"line":37,"column":2}}]},"5":{"line":40,"type":"if","locations":[{"start":{"line":40,"column":2},"end":{"line":40,"column":2}},{"start":{"line":40,"column":2},"end":{"line":40,"column":2}}]},"6":{"line":47,"type":"if","locations":[{"start":{"line":47,"column":2},"end":{"line":47,"column":2}},{"start":{"line":47,"column":2},"end":{"line":47,"column":2}}]},"7":{"line":56,"type":"if","locations":[{"start":{"line":56,"column":2},"end":{"line":56,"column":2}},{"start":{"line":56,"column":2},"end":{"line":56,"column":2}}]}}}}

@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for async-throttle/</title>
<meta charset="utf-8">
<link rel="stylesheet" href="../prettify.css">
<link rel="stylesheet" href="../base.css">
<style type='text/css'>
div.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class="header high">
<h1>Code coverage report for <span class="entity">async-throttle/</span></h1>
<h2>
Statements: <span class="metric">100% <small>(37 / 37)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Branches: <span class="metric">92.86% <small>(13 / 14)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Functions: <span class="metric">100% <small>(7 / 7)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Lines: <span class="metric">100% <small>(37 / 37)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Ignored: <span class="metric"><span class="ignore-none">none</span></span> &nbsp;&nbsp;&nbsp;&nbsp;
</h2>
<div class="path"><a href="../index.html">All files</a> &#187; async-throttle/</div>
</div>
<div class="body">
<div class="coverage-summary">
<table>
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="index.js"><a href="index.js.html">index.js</a></td>
<td data-value="100" class="pic high"><span class="cover-fill cover-full" style="width: 100px;"></span><span class="cover-empty" style="width:0px;"></span></td>
<td data-value="100" class="pct high">100%</td>
<td data-value="37" class="abs high">(37&nbsp;/&nbsp;37)</td>
<td data-value="92.86" class="pct high">92.86%</td>
<td data-value="14" class="abs high">(13&nbsp;/&nbsp;14)</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="7" class="abs high">(7&nbsp;/&nbsp;7)</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="37" class="abs high">(37&nbsp;/&nbsp;37)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="footer">
<div class="meta">Generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Mon Sep 11 2017 11:14:14 GMT-0500 (CDT)</div>
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="../sorter.js"></script>
</body>
</html>

@ -0,0 +1,246 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for async-throttle/index.js</title>
<meta charset="utf-8">
<link rel="stylesheet" href="../prettify.css">
<link rel="stylesheet" href="../base.css">
<style type='text/css'>
div.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class="header high">
<h1>Code coverage report for <span class="entity">async-throttle/index.js</span></h1>
<h2>
Statements: <span class="metric">100% <small>(37 / 37)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Branches: <span class="metric">92.86% <small>(13 / 14)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Functions: <span class="metric">100% <small>(7 / 7)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Lines: <span class="metric">100% <small>(37 / 37)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Ignored: <span class="metric"><span class="ignore-none">none</span></span> &nbsp;&nbsp;&nbsp;&nbsp;
</h2>
<div class="path"><a href="../index.html">All files</a> &#187; <a href="index.html">async-throttle/</a> &#187; index.js</div>
</div>
<div class="body">
<pre><table class="coverage">
<tr><td class="line-count">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68</td><td class="line-coverage"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">7</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">3</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">19</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">45</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">39</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">39</span>
<span class="cline-any cline-yes">18</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-yes">6</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-yes">13</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">'use strict';
&nbsp;
function Queue(options) {
if (!(this instanceof Queue)) {
return new Queue(options);
}
&nbsp;
options = options || {};
this.concurrency = options.concurrency || Infinity;
this.pending = 0;
this.jobs = [];
this.cbs = [];
this._done = done.bind(this);
}
&nbsp;
var arrayAddMethods = [
'push',
'unshift',
'splice'
];
&nbsp;
arrayAddMethods.forEach(function(method) {
Queue.prototype[method] = function() {
var methodResult = Array.prototype[method].apply(this.jobs, arguments);
this._run();
return methodResult;
};
});
&nbsp;
Object.defineProperty(Queue.prototype, 'length', {
get: function() {
return this.pending + this.jobs.length;
}
});
&nbsp;
Queue.prototype._run = function() {
if (this.pending === this.concurrency) {
return;
}
if (this.jobs.length) {
var job = this.jobs.shift();
this.pending++;
job(this._done);
this._run();
}
&nbsp;
if (this.pending === 0) {
while (this.cbs.length !== 0) {
var cb = this.cbs.pop();
process.nextTick(cb);
}
}
};
&nbsp;
Queue.prototype.onDone = function(cb) {
<span class="missing-if-branch" title="else path not taken" >E</span>if (typeof cb === 'function') {
this.cbs.push(cb);
this._run();
}
};
&nbsp;
function done() {
this.pending--;
this._run();
}
&nbsp;
module.exports = Queue;
&nbsp;</pre></td></tr>
</table></pre>
</div>
<div class="footer">
<div class="meta">Generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Mon Sep 11 2017 11:14:14 GMT-0500 (CDT)</div>
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="../sorter.js"></script>
</body>
</html>

@ -0,0 +1,182 @@
body, html {
margin:0; padding: 0;
}
body {
font-family: Helvetica Neue, Helvetica,Arial;
font-size: 10pt;
}
div.header, div.footer {
background: #eee;
padding: 1em;
}
div.header {
z-index: 100;
position: fixed;
top: 0;
border-bottom: 1px solid #666;
width: 100%;
}
div.footer {
border-top: 1px solid #666;
}
div.body {
margin-top: 10em;
}
div.meta {
font-size: 90%;
text-align: center;
}
h1, h2, h3 {
font-weight: normal;
}
h1 {
font-size: 12pt;
}
h2 {
font-size: 10pt;
}
pre {
font-family: Consolas, Menlo, Monaco, monospace;
margin: 0;
padding: 0;
line-height: 1.3;
font-size: 14px;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
div.path { font-size: 110%; }
div.path a:link, div.path a:visited { color: #000; }
table.coverage { border-collapse: collapse; margin:0; padding: 0 }
table.coverage td {
margin: 0;
padding: 0;
color: #111;
vertical-align: top;
}
table.coverage td.line-count {
width: 50px;
text-align: right;
padding-right: 5px;
}
table.coverage td.line-coverage {
color: #777 !important;
text-align: right;
border-left: 1px solid #666;
border-right: 1px solid #666;
}
table.coverage td.text {
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 40px;
}
table.coverage td span.cline-neutral {
background: #eee;
}
table.coverage td span.cline-yes {
background: #b5d592;
color: #999;
}
table.coverage td span.cline-no {
background: #fc8c84;
}
.cstat-yes { color: #111; }
.cstat-no { background: #fc8c84; color: #111; }
.fstat-no { background: #ffc520; color: #111 !important; }
.cbranch-no { background: yellow !important; color: #111; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
.missing-if-branch {
display: inline-block;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: black;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.entity, .metric { font-weight: bold; }
.metric { display: inline-block; border: 1px solid #333; padding: 0.3em; background: white; }
.metric small { font-size: 80%; font-weight: normal; color: #666; }
div.coverage-summary table { border-collapse: collapse; margin: 3em; font-size: 110%; }
div.coverage-summary td, div.coverage-summary table th { margin: 0; padding: 0.25em 1em; border-top: 1px solid #666; border-bottom: 1px solid #666; }
div.coverage-summary th { text-align: left; border: 1px solid #666; background: #eee; font-weight: normal; }
div.coverage-summary th.file { border-right: none !important; }
div.coverage-summary th.pic { border-left: none !important; text-align: right; }
div.coverage-summary th.pct { border-right: none !important; }
div.coverage-summary th.abs { border-left: none !important; text-align: right; }
div.coverage-summary td.pct { text-align: right; border-left: 1px solid #666; }
div.coverage-summary td.abs { text-align: right; font-size: 90%; color: #444; border-right: 1px solid #666; }
div.coverage-summary td.file { border-left: 1px solid #666; white-space: nowrap; }
div.coverage-summary td.pic { min-width: 120px !important; }
div.coverage-summary a:link { text-decoration: none; color: #000; }
div.coverage-summary a:visited { text-decoration: none; color: #777; }
div.coverage-summary a:hover { text-decoration: underline; }
div.coverage-summary tfoot td { border-top: 1px solid #666; }
div.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
div.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
div.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.high { background: #b5d592 !important; }
.medium { background: #ffe87c !important; }
.low { background: #fc8c84 !important; }
span.cover-fill, span.cover-empty {
display:inline-block;
border:1px solid #444;
background: white;
height: 12px;
}
span.cover-fill {
background: #ccc;
border-right: 1px solid #444;
}
span.cover-empty {
background: white;
border-left: none;
}
span.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }

@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8">
<link rel="stylesheet" href="prettify.css">
<link rel="stylesheet" href="base.css">
<style type='text/css'>
div.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class="header high">
<h1>Code coverage report for <span class="entity">All files</span></h1>
<h2>
Statements: <span class="metric">100% <small>(37 / 37)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Branches: <span class="metric">92.86% <small>(13 / 14)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Functions: <span class="metric">100% <small>(7 / 7)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Lines: <span class="metric">100% <small>(37 / 37)</small></span> &nbsp;&nbsp;&nbsp;&nbsp;
Ignored: <span class="metric"><span class="ignore-none">none</span></span> &nbsp;&nbsp;&nbsp;&nbsp;
</h2>
<div class="path"></div>
</div>
<div class="body">
<div class="coverage-summary">
<table>
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="async-throttle/"><a href="async-throttle/index.html">async-throttle/</a></td>
<td data-value="100" class="pic high"><span class="cover-fill cover-full" style="width: 100px;"></span><span class="cover-empty" style="width:0px;"></span></td>
<td data-value="100" class="pct high">100%</td>
<td data-value="37" class="abs high">(37&nbsp;/&nbsp;37)</td>
<td data-value="92.86" class="pct high">92.86%</td>
<td data-value="14" class="abs high">(13&nbsp;/&nbsp;14)</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="7" class="abs high">(7&nbsp;/&nbsp;7)</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="37" class="abs high">(37&nbsp;/&nbsp;37)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="footer">
<div class="meta">Generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Mon Sep 11 2017 11:14:14 GMT-0500 (CDT)</div>
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="sorter.js"></script>
</body>
</html>

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

@ -0,0 +1,156 @@
var addSorting = (function () {
"use strict";
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() { return document.querySelector('.coverage-summary table'); }
// returns the thead element of the summary table
function getTableHeader() { return getTable().querySelector('thead tr'); }
// returns the tbody element of the summary table
function getTableBody() { return getTable().querySelector('tbody'); }
// returns the th element for nth column
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function (a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function (a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function () {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i =0 ; i < cols.length; i += 1) {
if (cols[i].sortable) {
el = getNthColumn(i).querySelector('.sorter');
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function () {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData(cols);
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

@ -0,0 +1,74 @@
TN:
SF:/Users/samuelreed/git/forks/async-throttle/index.js
FN:3,Queue
FN:22,(anonymous_2)
FN:23,(anonymous_3)
FN:31,(anonymous_4)
FN:36,(anonymous_5)
FN:55,(anonymous_6)
FN:62,done
FNF:7
FNH:7
FNDA:7,Queue
FNDA:3,(anonymous_2)
FNDA:13,(anonymous_3)
FNDA:19,(anonymous_4)
FNDA:45,(anonymous_5)
FNDA:6,(anonymous_6)
FNDA:13,done
DA:3,1
DA:4,7
DA:5,1
DA:8,6
DA:9,6
DA:10,6
DA:11,6
DA:12,6
DA:13,6
DA:16,1
DA:22,1
DA:23,3
DA:24,13
DA:25,13
DA:26,13
DA:30,1
DA:32,19
DA:36,1
DA:37,45
DA:38,6
DA:40,39
DA:41,13
DA:42,13
DA:43,13
DA:44,13
DA:47,39
DA:48,18
DA:49,6
DA:50,6
DA:55,1
DA:56,6
DA:57,6
DA:58,6
DA:62,1
DA:63,13
DA:64,13
DA:67,1
LF:37
LH:37
BRDA:4,1,0,1
BRDA:4,1,1,6
BRDA:8,2,0,6
BRDA:8,2,1,5
BRDA:9,3,0,6
BRDA:9,3,1,5
BRDA:37,4,0,6
BRDA:37,4,1,39
BRDA:40,5,0,13
BRDA:40,5,1,26
BRDA:47,6,0,18
BRDA:47,6,1,21
BRDA:56,7,0,6
BRDA:56,7,1,0
BRF:14
BRH:13
end_of_record

@ -0,0 +1,67 @@
'use strict';
function Queue(options) {
if (!(this instanceof Queue)) {
return new Queue(options);
}
options = options || {};
this.concurrency = options.concurrency || Infinity;
this.pending = 0;
this.jobs = [];
this.cbs = [];
this._done = done.bind(this);
}
var arrayAddMethods = [
'push',
'unshift',
'splice'
];
arrayAddMethods.forEach(function(method) {
Queue.prototype[method] = function() {
var methodResult = Array.prototype[method].apply(this.jobs, arguments);
this._run();
return methodResult;
};
});
Object.defineProperty(Queue.prototype, 'length', {
get: function() {
return this.pending + this.jobs.length;
}
});
Queue.prototype._run = function() {
if (this.pending === this.concurrency) {
return;
}
if (this.jobs.length) {
var job = this.jobs.shift();
this.pending++;
job(this._done);
this._run();
}
if (this.pending === 0) {
while (this.cbs.length !== 0) {
var cb = this.cbs.pop();
process.nextTick(cb);
}
}
};
Queue.prototype.onDone = function(cb) {
if (typeof cb === 'function') {
this.cbs.push(cb);
this._run();
}
};
function done() {
this.pending--;
this._run();
}
module.exports = Queue;

@ -0,0 +1,69 @@
{
"_from": "async-limiter@~1.0.0",
"_id": "async-limiter@1.0.0",
"_inBundle": false,
"_integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
"_location": "/async-limiter",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "async-limiter@~1.0.0",
"name": "async-limiter",
"escapedName": "async-limiter",
"rawSpec": "~1.0.0",
"saveSpec": null,
"fetchSpec": "~1.0.0"
},
"_requiredBy": [
"/ws"
],
"_resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"_shasum": "78faed8c3d074ab81f22b4e985d79e8738f720f8",
"_spec": "async-limiter@~1.0.0",
"_where": "/Users/loic/Documents/DiscordJS/node_modules/ws",
"author": {
"name": "Samuel Reed"
},
"bugs": {
"url": "https://github.com/strml/async-limiter/issues"
},
"bundleDependencies": false,
"dependencies": {},
"deprecated": false,
"description": "asynchronous function queue with adjustable concurrency",
"devDependencies": {
"coveralls": "^2.11.2",
"eslint": "^4.6.1",
"eslint-plugin-mocha": "^4.11.0",
"intelli-espower-loader": "^1.0.1",
"istanbul": "^0.3.2",
"mocha": "^3.5.2",
"power-assert": "^1.4.4"
},
"homepage": "https://github.com/strml/async-limiter#readme",
"keywords": [
"throttle",
"async",
"limiter",
"asynchronous",
"job",
"task",
"concurrency",
"concurrent"
],
"license": "MIT",
"name": "async-limiter",
"repository": {
"type": "git",
"url": "git+https://github.com/strml/async-limiter.git"
},
"scripts": {
"coverage": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | coveralls",
"example": "node example",
"lint": "eslint .",
"test": "mocha --R intelli-espower-loader test/",
"travis": "npm run lint && npm run coverage"
},
"version": "1.0.0"
}

@ -0,0 +1,132 @@
# Async-Limiter
A module for limiting concurrent asynchronous actions in flight. Forked from [queue](https://github.com/jessetane/queue).
[![npm](http://img.shields.io/npm/v/async-limiter.svg?style=flat-square)](http://www.npmjs.org/async-limiter)
[![tests](https://img.shields.io/travis/STRML/async-limiter.svg?style=flat-square&branch=master)](https://travis-ci.org/STRML/async-limiter)
[![coverage](https://img.shields.io/coveralls/STRML/async-limiter.svg?style=flat-square&branch=master)](https://coveralls.io/r/STRML/async-limiter)
This module exports a class `Limiter` that implements some of the `Array` API.
Pass async functions (ones that accept a callback or return a promise) to an instance's additive array methods.
## Motivation
Certain functions, like `zlib`, have [undesirable behavior](https://github.com/nodejs/node/issues/8871#issuecomment-250915913) when
run at infinite concurrency.
In this case, it is actually faster, and takes far less memory, to limit concurrency.
This module should do the absolute minimum work necessary to queue up functions. PRs are welcome that would
make this module faster or lighter, but new functionality is not desired.
Style should confirm to nodejs/node style.
## Example
``` javascript
var Limiter = require('async-limiter')
var t = new Limiter({concurrency: 2});
var results = []
// add jobs using the familiar Array API
t.push(function (cb) {
results.push('two')
cb()
})
t.push(
function (cb) {
results.push('four')
cb()
},
function (cb) {
results.push('five')
cb()
}
)
t.unshift(function (cb) {
results.push('one')
cb()
})
t.splice(2, 0, function (cb) {
results.push('three')
cb()
})
// Jobs run automatically. If you want a callback when all are done,
// call 'onDone()'.
t.onDone(function () {
console.log('all done:', results)
})
```
## Zlib Example
```js
const zlib = require('zlib');
const Limiter = require('async-limiter');
const message = {some: "data"};
const payload = new Buffer(JSON.stringify(message));
// Try with different concurrency values to see how this actually
// slows significantly with higher concurrency!
//
// 5: 1398.607ms
// 10: 1375.668ms
// Infinity: 4423.300ms
//
const t = new Limiter({concurrency: 5});
function deflate(payload, cb) {
t.push(function(done) {
zlib.deflate(payload, function(err, buffer) {
done();
cb(err, buffer);
});
});
}
console.time('deflate');
for(let i = 0; i < 30000; ++i) {
deflate(payload, function (err, buffer) {});
}
q.onDone(function() {
console.timeEnd('deflate');
});
```
## Install
`npm install async-limiter`
## Test
`npm test`
## API
### `var t = new Limiter([opts])`
Constructor. `opts` may contain inital values for:
* `q.concurrency`
## Instance methods
### `q.onDone(fn)`
`fn` will be called once and only once, when the queue is empty.
## Instance methods mixed in from `Array`
Mozilla has docs on how these methods work [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).
### `q.push(element1, ..., elementN)`
### `q.unshift(element1, ..., elementN)`
### `q.splice(index , howMany[, element1[, ...[, elementN]]])`
## Properties
### `q.concurrency`
Max number of jobs the queue should process concurrently, defaults to `Infinity`.
### `q.length`
Jobs pending + jobs to process (readonly).

@ -0,0 +1,3 @@
[submodule "typings"]
path = typings
url = https://github.com/discordjs/discord.js-typings

@ -0,0 +1,21 @@
{
"ecmaVersion": 7,
"libs": [],
"loadEagerly": [
"./src/*.js"
],
"dontLoad": [
"node_modules/**"
],
"plugins": {
"es_modules": {},
"node": {},
"doc_comment": {
"fullDocs": true,
"strong": true
},
"webpack": {
"configPath": "./webpack.config.js",
}
}
}

190
node_modules/discord.js/LICENSE generated vendored

@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2017 Amish Shah
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

87
node_modules/discord.js/README.md generated vendored

@ -0,0 +1,87 @@
<div align="center">
<br />
<p>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
</p>
<br />
<p>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a>
<a href="https://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
</p>
<p>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
</p>
</div>
## About
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
[Discord API](https://discordapp.com/developers/docs/intro) very easily.
- Object-oriented
- Predictable abstractions
- Performant
- 100% coverage of the Discord API
## Installation
**Node.js 6.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
### Audio engines
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
Using opusscript is only recommended for development environments where node-opus is tough to get working.
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws`)
## Example usage
```js
const Discord = require('discord.js');
const client = new Discord.Client();
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
client.login('token');
```
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
* [Documentation](https://discord.js.org/#/docs)
* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/discordjs/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
* [Related libraries](https://discordapi.com/unofficial/libs.html)
### Extensions
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).

@ -0,0 +1,9 @@
const browser = typeof window !== 'undefined';
const webpack = !!process.env.__DISCORD_WEBPACK__;
const Discord = require('./');
module.exports = Discord;
if (browser && webpack) window.Discord = Discord; // eslint-disable-line no-undef
// eslint-disable-next-line no-console
else if (!browser) console.warn('Warning: Attempting to use browser version of Discord.js in a non-browser environment!');

@ -0,0 +1,122 @@
{
"_from": "discord.js",
"_id": "discord.js@11.4.2",
"_inBundle": false,
"_integrity": "sha512-MDwpu0lMFTjqomijDl1Ed9miMQe6kB4ifKdP28QZllmLv/HVOJXhatRgjS8urp/wBlOfx+qAYSXcdI5cKGYsfg==",
"_location": "/discord.js",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "discord.js",
"name": "discord.js",
"escapedName": "discord.js",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.4.2.tgz",
"_shasum": "54586981926521572051f2a30b984aad2b49786e",
"_spec": "discord.js",
"_where": "/Users/loic/Documents/DiscordJS",
"author": {
"name": "Amish Shah",
"email": "amishshah.2k@gmail.com"
},
"browser": {
"ws": false,
"uws": false,
"erlpack": false,
"prism-media": false,
"opusscript": false,
"node-opus": false,
"tweetnacl": false,
"sodium": false,
"src/sharding/Shard.js": false,
"src/sharding/ShardClientUtil.js": false,
"src/sharding/ShardingManager.js": false,
"src/client/voice/dispatcher/StreamDispatcher.js": false,
"src/client/voice/opus/BaseOpusEngine.js": false,
"src/client/voice/opus/NodeOpusEngine.js": false,
"src/client/voice/opus/OpusEngineList.js": false,
"src/client/voice/opus/OpusScriptEngine.js": false,
"src/client/voice/pcm/ConverterEngine.js": false,
"src/client/voice/pcm/ConverterEngineList.js": false,
"src/client/voice/pcm/FfmpegConverterEngine.js": false,
"src/client/voice/player/AudioPlayer.js": false,
"src/client/voice/receiver/VoiceReadable.js": false,
"src/client/voice/receiver/VoiceReceiver.js": false,
"src/client/voice/util/Secretbox.js": false,
"src/client/voice/util/SecretKey.js": false,
"src/client/voice/util/VolumeInterface.js": false,
"src/client/voice/ClientVoiceManager.js": false,
"src/client/voice/VoiceBroadcast.js": false,
"src/client/voice/VoiceConnection.js": false,
"src/client/voice/VoiceUDPClient.js": false,
"src/client/voice/VoiceWebSocket.js": false
},
"bugs": {
"url": "https://github.com/discordjs/discord.js/issues"
},
"bundleDependencies": false,
"dependencies": {
"long": "^4.0.0",
"prism-media": "^0.0.3",
"snekfetch": "^3.6.4",
"tweetnacl": "^1.0.0",
"ws": "^4.0.0"
},
"deprecated": false,
"description": "A powerful library for interacting with the Discord API",
"devDependencies": {
"@types/node": "^9.4.6",
"discord.js-docgen": "github:discordjs/docgen",
"eslint": "^4.18.0",
"parallel-webpack": "^2.2.0",
"uglifyjs-webpack-plugin": "^1.2.0",
"webpack": "^3.11.0"
},
"engines": {
"node": ">=6.0.0"
},
"homepage": "https://github.com/discordjs/discord.js#readme",
"keywords": [
"discord",
"api",
"bot",
"client",
"node",
"discordapp"
],
"license": "Apache-2.0",
"main": "./src/index",
"name": "discord.js",
"peerDependencies": {
"bufferutil": "^3.0.3",
"erlpack": "discordapp/erlpack",
"node-opus": "^0.2.7",
"opusscript": "^0.0.6",
"sodium": "^2.0.3",
"libsodium-wrappers": "^0.7.3",
"uws": "^9.14.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/discordjs/discord.js.git"
},
"runkitExampleFilename": "./docs/examples/ping.js",
"scripts": {
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src",
"lint:fix": "eslint --fix src",
"test": "npm run lint && npm run docs:test",
"webpack": "parallel-webpack"
},
"types": "./typings/index.d.ts",
"version": "11.4.2"
}

@ -0,0 +1,558 @@
const EventEmitter = require('events');
const Constants = require('../util/Constants');
const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
const RESTManager = require('./rest/RESTManager');
const ClientDataManager = require('./ClientDataManager');
const ClientManager = require('./ClientManager');
const ClientDataResolver = require('./ClientDataResolver');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const ActionsManager = require('./actions/ActionsManager');
const Collection = require('../util/Collection');
const Presence = require('../structures/Presence').Presence;
const ShardClientUtil = require('../sharding/ShardClientUtil');
const VoiceBroadcast = require('./voice/VoiceBroadcast');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
* @extends {EventEmitter}
*/
class Client extends EventEmitter {
/**
* @param {ClientOptions} [options] Options for the client
*/
constructor(options = {}) {
super();
// Obtain shard details from environment
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
this._validateOptions();
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this);
/**
* The data manager of the client
* @type {ClientDataManager}
* @private
*/
this.dataManager = new ClientDataManager(this);
/**
* The manager of the client
* @type {ClientManager}
* @private
*/
this.manager = new ClientManager(this);
/**
* The WebSocket manager of the client
* @type {WebSocketManager}
* @private
*/
this.ws = new WebSocketManager(this);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* The action manager of the client
* @type {ActionsManager}
* @private
*/
this.actions = new ActionsManager(this);
/**
* The voice manager of the client (`null` in browsers)
* @type {?ClientVoiceManager}
* @private
*/
this.voice = !this.browser ? new ClientVoiceManager(this) : null;
/**
* The shard helpers for the client
* (only if the process was spawned as a child, such as from a {@link ShardingManager})
* @type {?ShardClientUtil}
*/
this.shard = process.send ? ShardClientUtil.singleton(this) : null;
/**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {Collection<Snowflake, User>}
*/
this.users = new Collection();
/**
* All of the guilds the client is currently handling, mapped by their IDs -
* as long as sharding isn't being used, this will be *every* guild the bot is a member of
* @type {Collection<Snowflake, Guild>}
*/
this.guilds = new Collection();
/**
* All of the {@link Channel}s that the client is currently handling, mapped by their IDs -
* as long as sharding isn't being used, this will be *every* channel in *every* guild, and all DM channels
* @type {Collection<Snowflake, Channel>}
*/
this.channels = new Collection();
/**
* Presences that have been received for the client user's friends, mapped by user IDs
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<Snowflake, Presence>}
*/
this.presences = new Collection();
Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'CLIENT_TOKEN' in process.env) {
/**
* Authorization token for the logged in user/bot
* <warn>This should be kept private at all times.</warn>
* @type {?string}
*/
this.token = process.env.CLIENT_TOKEN;
} else {
this.token = null;
}
/**
* User that the client is logged in as
* @type {?ClientUser}
*/
this.user = null;
/**
* Time at which the client was last regarded as being in the `READY` state
* (each time the client disconnects and successfully reconnects, this will be overwritten)
* @type {?Date}
*/
this.readyAt = null;
/**
* Active voice broadcasts that have been created
* @type {VoiceBroadcast[]}
*/
this.broadcasts = [];
/**
* Previous heartbeat pings of the websocket (most recent first, limited to three elements)
* @type {number[]}
*/
this.pings = [];
/**
* Timeouts set by {@link Client#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link Client#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
if (this.options.messageSweepInterval > 0) {
this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000);
}
}
/**
* Timestamp of the latest ping's start time
* @type {number}
* @private
*/
get _pingTimestamp() {
return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0;
}
/**
* Current status of the client's connection to Discord
* @type {?number}
* @readonly
*/
get status() {
return this.ws.connection.status;
}
/**
* How long it has been since the client last entered the `READY` state in milliseconds
* @type {?number}
* @readonly
*/
get uptime() {
return this.readyAt ? Date.now() - this.readyAt : null;
}
/**
* Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property
* @type {number}
* @readonly
*/
get ping() {
return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length;
}
/**
* All active voice connections that have been established, mapped by guild ID
* @type {Collection<Snowflake, VoiceConnection>}
* @readonly
*/
get voiceConnections() {
if (this.browser) return new Collection();
return this.voice.connections;
}
/**
* All custom emojis that the client has access to, mapped by their IDs
* @type {Collection<Snowflake, Emoji>}
* @readonly
*/
get emojis() {
const emojis = new Collection();
for (const guild of this.guilds.values()) {
for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji);
}
return emojis;
}
/**
* Timestamp of the time the client was last `READY` at
* @type {?number}
* @readonly
*/
get readyTimestamp() {
return this.readyAt ? this.readyAt.getTime() : null;
}
/**
* Whether the client is in a browser environment
* @type {boolean}
* @readonly
*/
get browser() {
return typeof window !== 'undefined';
}
/**
* Creates a voice broadcast.
* @returns {VoiceBroadcast}
*/
createVoiceBroadcast() {
const broadcast = new VoiceBroadcast(this);
this.broadcasts.push(broadcast);
return broadcast;
}
/**
* Logs the client in, establishing a websocket connection to Discord.
* <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever
* possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts.
* Bot accounts also have access to many features that user accounts cannot utilise. Automating a user account is
* considered a violation of the ToS.</info>
* @param {string} token Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token')
* .then(console.log)
* .catch(console.error);
*/
login(token = this.token) {
return this.rest.methods.login(token);
}
/**
* Logs out, terminates the connection to Discord, and destroys the client.
* @returns {Promise}
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
return this.manager.destroy();
}
/**
* Requests a sync of guild data with Discord.
* <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info>
* <warn>This is only available when using a user account.</warn>
* @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
*/
syncGuilds(guilds = this.guilds) {
if (this.user.bot) return;
this.ws.send({
op: 12,
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
});
}
/**
* Obtains a user from Discord, or the user cache if it's already available.
* <warn>This is only available when using a bot account.</warn>
* @param {Snowflake} id ID of the user
* @param {boolean} [cache=true] Whether to cache the new user object if it isn't already
* @returns {Promise<User>}
*/
fetchUser(id, cache = true) {
if (this.users.has(id)) return Promise.resolve(this.users.get(id));
return this.rest.methods.getUser(id, cache);
}
/**
* Obtains an invite from Discord.
* @param {InviteResolvable} invite Invite code or URL
* @returns {Promise<Invite>}
* @example
* client.fetchInvite('https://discord.gg/bRCvFy9')
* .then(invite => console.log(`Obtained invite with code: ${invite.code}`)
* .catch(console.error);
*/
fetchInvite(invite) {
const code = this.resolver.resolveInviteCode(invite);
return this.rest.methods.getInvite(code);
}
/**
* Obtains a webhook from Discord.
* @param {Snowflake} id ID of the webhook
* @param {string} [token] Token for the webhook
* @returns {Promise<Webhook>}
* @example
* client.fetchWebhook('id', 'token')
* .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`))
* .catch(console.error);
*/
fetchWebhook(id, token) {
return this.rest.methods.getWebhook(id, token);
}
/**
* Obtains the available voice regions from Discord.
* @returns {Collection<string, VoiceRegion>}
* @example
* client.fetchVoiceRegions()
* .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`))
* .catch(console.error);
*/
fetchVoiceRegions() {
return this.rest.methods.fetchVoiceRegions();
}
/**
* Sweeps all text-based channels' messages and removes the ones older than the max message lifetime.
* If the message has been edited, the time of the edit is used rather than the time of the original message.
* @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds)
* will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime}
* @returns {number} Amount of messages that were removed from the caches,
* or -1 if the message cache lifetime is unlimited
*/
sweepMessages(lifetime = this.options.messageCacheLifetime) {
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
if (lifetime <= 0) {
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
return -1;
}
const lifetimeMs = lifetime * 1000;
const now = Date.now();
let channels = 0;
let messages = 0;
for (const channel of this.channels.values()) {
if (!channel.messages) continue;
channels++;
messages += channel.messages.sweep(
message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs
);
}
this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`);
return messages;
}
/**
* Obtains the OAuth Application of the bot from Discord.
* <warn>Bots can only fetch their own profile.</warn>
* @param {Snowflake} [id='@me'] ID of application to fetch
* @returns {Promise<OAuth2Application>}
* client.fetchApplication()
* .then(application => console.log(`Obtained application with name: ${application.name}`)
* .catch(console.error);
*/
fetchApplication(id = '@me') {
if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning');
return this.rest.methods.getApplication(id);
}
/**
* Generates a link that can be used to invite the bot to a guild.
* <warn>This is only available when using a bot account.</warn>
* @param {PermissionResolvable} [permissions] Permissions to request
* @returns {Promise<string>}
* @example
* client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'])
* .then(link => console.log(`Generated bot invite link: ${link}`))
* .catch(console.error);
*/
generateInvite(permissions) {
permissions = typeof permissions === 'undefined' ? 0 : Permissions.resolve(permissions);
return this.fetchApplication().then(application =>
`https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot`
);
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Adds a ping to {@link Client#pings}.
* @param {number} startTime Starting time of the ping
* @private
*/
_pong(startTime) {
this.pings.unshift(Date.now() - startTime);
if (this.pings.length > 3) this.pings.length = 3;
this.ws.lastHeartbeatAck = true;
}
/**
* Adds/updates a friend's presence in {@link Client#presences}.
* @param {Snowflake} id ID of the user
* @param {Object} presence Raw presence object from Discord
* @private
*/
_setPresence(id, presence) {
if (this.presences.has(id)) {
this.presences.get(id).update(presence);
return;
}
this.presences.set(id, new Presence(presence, this));
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
* with the client as `this`.
* @param {string} script Script to eval
* @returns {*}
* @private
*/
_eval(script) {
return eval(script);
}
/**
* Validates the client options.
* @param {ClientOptions} [options=this.options] Options to validate
* @private
*/
_validateOptions(options = this.options) {
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
throw new TypeError('The shardCount option must be a number.');
}
if (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
throw new TypeError('The shardId option must be a number.');
}
if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.');
if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.');
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
throw new RangeError('The shardId option must be less than shardCount.');
}
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
throw new TypeError('The messageCacheMaxSize option must be a number.');
}
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
throw new TypeError('The messageCacheLifetime option must be a number.');
}
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('The messageSweepInterval option must be a number.');
}
if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('The fetchAllMembers option must be a boolean.');
}
if (typeof options.disableEveryone !== 'boolean') {
throw new TypeError('The disableEveryone option must be a boolean.');
}
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
throw new TypeError('The restWsBridgeTimeout option must be a number.');
}
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
}
}
module.exports = Client;
/**
* Emitted for general warnings.
* @event Client#warn
* @param {string} info The warning
*/
/**
* Emitted for general debugging information.
* @event Client#debug
* @param {string} info The debug information
*/

@ -0,0 +1,138 @@
const Constants = require('../util/Constants');
const Util = require('../util/Util');
const Guild = require('../structures/Guild');
const User = require('../structures/User');
const CategoryChannel = require('../structures/CategoryChannel');
const DMChannel = require('../structures/DMChannel');
const Emoji = require('../structures/Emoji');
const TextChannel = require('../structures/TextChannel');
const VoiceChannel = require('../structures/VoiceChannel');
const GuildChannel = require('../structures/GuildChannel');
const GroupDMChannel = require('../structures/GroupDMChannel');
class ClientDataManager {
constructor(client) {
this.client = client;
}
get pastReady() {
return this.client.ws.connection.status === Constants.Status.READY;
}
newGuild(data) {
const already = this.client.guilds.has(data.id);
const guild = new Guild(this.client, data);
this.client.guilds.set(guild.id, guild);
if (this.pastReady && !already) {
/**
* Emitted whenever the client joins a guild.
* @event Client#guildCreate
* @param {Guild} guild The created guild
*/
if (this.client.options.fetchAllMembers) {
guild.fetchMembers().then(() => { this.client.emit(Constants.Events.GUILD_CREATE, guild); });
} else {
this.client.emit(Constants.Events.GUILD_CREATE, guild);
}
}
return guild;
}
newUser(data, cache = true) {
if (this.client.users.has(data.id)) return this.client.users.get(data.id);
const user = new User(this.client, data);
if (cache) this.client.users.set(user.id, user);
return user;
}
newChannel(data, guild) {
const already = this.client.channels.has(data.id);
let channel;
if (data.type === Constants.ChannelTypes.DM) {
channel = new DMChannel(this.client, data);
} else if (data.type === Constants.ChannelTypes.GROUP_DM) {
channel = new GroupDMChannel(this.client, data);
} else {
guild = guild || this.client.guilds.get(data.guild_id);
if (already) {
channel = this.client.channels.get(data.id);
} else if (guild) {
if (data.type === Constants.ChannelTypes.TEXT) {
channel = new TextChannel(guild, data);
guild.channels.set(channel.id, channel);
} else if (data.type === Constants.ChannelTypes.VOICE) {
channel = new VoiceChannel(guild, data);
guild.channels.set(channel.id, channel);
} else if (data.type === Constants.ChannelTypes.CATEGORY) {
channel = new CategoryChannel(guild, data);
guild.channels.set(channel.id, channel);
}
}
}
if (channel && !already) {
if (this.pastReady) this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
this.client.channels.set(channel.id, channel);
return channel;
} else if (already) {
return channel;
}
return null;
}
newEmoji(data, guild) {
const already = guild.emojis.has(data.id);
if (data && !already) {
let emoji = new Emoji(guild, data);
this.client.emit(Constants.Events.GUILD_EMOJI_CREATE, emoji);
guild.emojis.set(emoji.id, emoji);
return emoji;
} else if (already) {
return guild.emojis.get(data.id);
}
return null;
}
killEmoji(emoji) {
if (!(emoji instanceof Emoji && emoji.guild)) return;
this.client.emit(Constants.Events.GUILD_EMOJI_DELETE, emoji);
emoji.guild.emojis.delete(emoji.id);
}
killGuild(guild) {
const already = this.client.guilds.has(guild.id);
this.client.guilds.delete(guild.id);
if (already && this.pastReady) this.client.emit(Constants.Events.GUILD_DELETE, guild);
}
killUser(user) {
this.client.users.delete(user.id);
}
killChannel(channel) {
this.client.channels.delete(channel.id);
if (channel instanceof GuildChannel) channel.guild.channels.delete(channel.id);
}
updateGuild(currentGuild, newData) {
const oldGuild = Util.cloneObject(currentGuild);
currentGuild.setup(newData);
if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild);
}
updateChannel(currentChannel, newData) {
currentChannel.setup(newData);
}
updateEmoji(currentEmoji, newData) {
const oldEmoji = Util.cloneObject(currentEmoji);
currentEmoji.setup(newData);
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
return currentEmoji;
}
}
module.exports = ClientDataManager;

@ -0,0 +1,375 @@
const path = require('path');
const fs = require('fs');
const snekfetch = require('snekfetch');
const Constants = require('../util/Constants');
const convertToBuffer = require('../util/Util').convertToBuffer;
const User = require('../structures/User');
const Message = require('../structures/Message');
const Guild = require('../structures/Guild');
const Channel = require('../structures/Channel');
const GuildMember = require('../structures/GuildMember');
const Emoji = require('../structures/Emoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const Role = require('../structures/Role');
/**
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
* extracting a User from a Message object.
* @private
*/
class ClientDataResolver {
/**
* @param {Client} client The client the resolver is for
*/
constructor(client) {
this.client = client;
}
/**
* Data that resolves to give a User object. This can be:
* * A User object
* * A Snowflake
* * A Message object (resolves to the message author)
* * A Guild object (owner of the guild)
* * A GuildMember object
* @typedef {User|Snowflake|Message|Guild|GuildMember} UserResolvable
*/
/**
* Resolves a UserResolvable to a User object.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?User}
*/
resolveUser(user) {
if (user instanceof User) return user;
if (typeof user === 'string') return this.client.users.get(user) || null;
if (user instanceof GuildMember) return user.user;
if (user instanceof Message) return user.author;
if (user instanceof Guild) return user.owner;
return null;
}
/**
* Resolves a UserResolvable to a user ID string.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?Snowflake}
*/
resolveUserID(user) {
if (user instanceof User || user instanceof GuildMember) return user.id;
if (typeof user === 'string') return user || null;
if (user instanceof Message) return user.author.id;
if (user instanceof Guild) return user.ownerID;
return null;
}
/**
* Data that resolves to give a Guild object. This can be:
* * A Guild object
* * A Snowflake
* @typedef {Guild|Snowflake} GuildResolvable
*/
/**
* Resolves a GuildResolvable to a Guild object.
* @param {GuildResolvable} guild The GuildResolvable to identify
* @returns {?Guild}
*/
resolveGuild(guild) {
if (guild instanceof Guild) return guild;
if (typeof guild === 'string') return this.client.guilds.get(guild) || null;
return null;
}
/**
* Data that resolves to give a GuildMember object. This can be:
* * A GuildMember object
* * A User object
* @typedef {GuildMember|User} GuildMemberResolvable
*/
/**
* Resolves a GuildMemberResolvable to a GuildMember object.
* @param {GuildResolvable} guild The guild that the member is part of
* @param {UserResolvable} user The user that is part of the guild
* @returns {?GuildMember}
*/
resolveGuildMember(guild, user) {
if (user instanceof GuildMember) return user;
guild = this.resolveGuild(guild);
user = this.resolveUser(user);
if (!guild || !user) return null;
return guild.members.get(user.id) || null;
}
/**
* Data that can be resolved to a Role object. This can be:
* * A Role
* * A Snowflake
* @typedef {Role|Snowflake} RoleResolvable
*/
/**
* Resolves a RoleResolvable to a Role object.
* @param {GuildResolvable} guild The guild that this role is part of
* @param {RoleResolvable} role The role resolvable to resolve
* @returns {?Role}
*/
resolveRole(guild, role) {
if (role instanceof Role) return role;
guild = this.resolveGuild(guild);
if (!guild) return null;
if (typeof role === 'string') return guild.roles.get(role);
return null;
}
/**
* Data that can be resolved to give a Channel object. This can be:
* * A Channel object
* * A Message object (the channel the message was sent in)
* * A Guild object (the #general channel)
* * A Snowflake
* @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable
*/
/**
* Resolves a ChannelResolvable to a Channel object.
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Channel}
*/
resolveChannel(channel) {
if (channel instanceof Channel) return channel;
if (typeof channel === 'string') return this.client.channels.get(channel) || null;
if (channel instanceof Message) return channel.channel;
if (channel instanceof Guild) return channel.channels.get(channel.id) || null;
return null;
}
/**
* Resolves a ChannelResolvable to a channel ID.
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Snowflake}
*/
resolveChannelID(channel) {
if (channel instanceof Channel) return channel.id;
if (typeof channel === 'string') return channel;
if (channel instanceof Message) return channel.channel.id;
if (channel instanceof Guild) return channel.defaultChannel.id;
return null;
}
/**
* Data that can be resolved to give an invite code. This can be:
* * An invite code
* * An invite URL
* @typedef {string} InviteResolvable
*/
/**
* Resolves InviteResolvable to an invite code.
* @param {InviteResolvable} data The invite resolvable to resolve
* @returns {string}
*/
resolveInviteCode(data) {
const inviteRegex = /discord(?:app\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i;
const match = inviteRegex.exec(data);
if (match && match[1]) return match[1];
return data;
}
/**
* Data that can be resolved to give a string. This can be:
* * A string
* * An array (joined with a new line delimiter to give a string)
* * Any value
* @typedef {string|Array|*} StringResolvable
*/
/**
* Resolves a StringResolvable to a string.
* @param {StringResolvable} data The string resolvable to resolve
* @returns {string}
*/
resolveString(data) {
if (typeof data === 'string') return data;
if (data instanceof Array) return data.join('\n');
return String(data);
}
/**
* Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image.
* @param {BufferResolvable|Base64Resolvable} image The image to be resolved
* @returns {Promise<?string>}
*/
resolveImage(image) {
if (!image) return Promise.resolve(null);
if (typeof image === 'string' && image.startsWith('data:')) {
return Promise.resolve(image);
}
return this.resolveFile(image).then(this.resolveBase64);
}
/**
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
* * A Buffer
* * A base64 string
* @typedef {Buffer|string} Base64Resolvable
*/
/**
* Resolves a Base64Resolvable to a Base 64 image.
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
* @returns {?string}
*/
resolveBase64(data) {
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
return data;
}
/**
* Data that can be resolved to give a Buffer. This can be:
* * A Buffer
* * The path to a local file
* * A URL
* * A Stream
* @typedef {string|Buffer} BufferResolvable
*/
/**
* @external Stream
* @see {@link https://nodejs.org/api/stream.html}
*/
/**
* Resolves a BufferResolvable to a Buffer.
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
* @returns {Promise<Buffer>}
*/
resolveFile(resource) {
if (resource instanceof Buffer) return Promise.resolve(resource);
if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource));
if (typeof resource === 'string') {
if (/^https?:\/\//.test(resource)) {
return snekfetch.get(resource).then(res => res.body instanceof Buffer ? res.body : Buffer.from(res.text));
}
return new Promise((resolve, reject) => {
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
if (err) return reject(err);
if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`));
fs.readFile(file, (err2, data) => {
if (err2) reject(err2);
else resolve(data);
});
return null;
});
});
} else if (resource && resource.pipe && typeof resource.pipe === 'function') {
return new Promise((resolve, reject) => {
const buffers = [];
resource.once('error', reject);
resource.on('data', data => buffers.push(data));
resource.once('end', () => resolve(Buffer.concat(buffers)));
});
}
return Promise.reject(new TypeError('The resource must be a string or Buffer.'));
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * A custom emoji ID
* * An Emoji object
* * A ReactionEmoji object
* @typedef {string|Emoji|ReactionEmoji} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveEmojiIdentifier(emoji) {
if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
if (this.client.emojis.has(emoji)) return this.client.emojis.get(emoji).identifier;
else if (!emoji.includes('%')) return encodeURIComponent(emoji);
else return emoji;
}
return null;
}
/**
* Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following
* ```
* [
* 'DEFAULT',
* 'AQUA',
* 'GREEN',
* 'BLUE',
* 'PURPLE',
* 'LUMINOUS_VIVID_PINK',
* 'GOLD',
* 'ORANGE',
* 'RED',
* 'GREY',
* 'DARKER_GREY',
* 'NAVY',
* 'DARK_AQUA',
* 'DARK_GREEN',
* 'DARK_BLUE',
* 'DARK_PURPLE',
* 'DARK_VIVID_PINK',
* 'DARK_GOLD',
* 'DARK_ORANGE',
* 'DARK_RED',
* 'DARK_GREY',
* 'LIGHT_GREY',
* 'DARK_NAVY',
* 'RANDOM',
* ]
* ```
* or something like
* ```
* [255, 0, 255]
* ```
* for purple
* @typedef {string|number|Array} ColorResolvable
*/
/**
* Resolves a ColorResolvable into a color number.
* @param {ColorResolvable} color Color to resolve
* @returns {number} A color
*/
static resolveColor(color) {
if (typeof color === 'string') {
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
if (color === 'DEFAULT') return 0;
color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16);
} else if (color instanceof Array) {
color = (color[0] << 16) + (color[1] << 8) + color[2];
}
if (color < 0 || color > 0xFFFFFF) {
throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).');
} else if (color && isNaN(color)) {
throw new TypeError('Unable to convert color to a number.');
}
return color;
}
/**
* @param {ColorResolvable} color Color to resolve
* @returns {number} A color
*/
resolveColor(color) {
return this.constructor.resolveColor(color);
}
}
module.exports = ClientDataResolver;

@ -0,0 +1,74 @@
const Constants = require('../util/Constants');
const WebSocketConnection = require('./websocket/WebSocketConnection');
/**
* Manages the state and background tasks of the client.
* @private
*/
class ClientManager {
constructor(client) {
/**
* The client that instantiated this Manager
* @type {Client}
*/
this.client = client;
/**
* The heartbeat interval
* @type {?number}
*/
this.heartbeatInterval = null;
}
/**
* The status of the client
* @type {number}
*/
get status() {
return this.connection ? this.connection.status : Constants.Status.IDLE;
}
/**
* Connects the client to the WebSocket.
* @param {string} token The authorization token
* @param {Function} resolve Function to run when connection is successful
* @param {Function} reject Function to run when connection fails
*/
connectToWebSocket(token, resolve, reject) {
this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`);
this.client.token = token;
const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300);
this.client.rest.methods.getGateway().then(res => {
const protocolVersion = Constants.DefaultOptions.ws.version;
const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`;
this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`);
this.client.ws.connect(gateway);
this.client.ws.connection.once('error', reject);
this.client.ws.connection.once('close', event => {
if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN));
if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD));
if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED));
});
this.client.once(Constants.Events.READY, () => {
resolve(token);
this.client.clearTimeout(timeout);
});
}, reject);
}
destroy() {
this.client.ws.destroy();
this.client.rest.destroy();
if (!this.client.user) return Promise.resolve();
if (this.client.user.bot) {
this.client.token = null;
return Promise.resolve();
} else {
return this.client.rest.methods.logout().then(() => {
this.client.token = null;
});
}
}
}
module.exports = ClientManager;

@ -0,0 +1,118 @@
const Webhook = require('../structures/Webhook');
const RESTManager = require('./rest/RESTManager');
const ClientDataResolver = require('./ClientDataResolver');
const Constants = require('../util/Constants');
const Util = require('../util/Util');
/**
* The webhook client.
* @extends {Webhook}
*/
class WebhookClient extends Webhook {
/**
* @param {Snowflake} id ID of the webhook
* @param {string} token Token of the webhook
* @param {ClientOptions} [options] Options for the client
* @example
* // Create a new webhook and send a message
* const hook = new Discord.WebhookClient('1234', 'abcdef');
* hook.sendMessage('This will send a message').catch(console.error);
*/
constructor(id, token, options) {
super(null, id, token);
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* Timeouts set by {@link WebhookClient#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link WebhookClient#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Destroys the client.
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
}
}
module.exports = WebhookClient;

@ -0,0 +1,23 @@
/*
ABOUT ACTIONS
Actions are similar to WebSocket Packet Handlers, but since introducing
the REST API methods, in order to prevent rewriting code to handle data,
"actions" have been introduced. They're basically what Packet Handlers
used to be but they're strictly for manipulating data and making sure
that WebSocket events don't clash with REST methods.
*/
class GenericAction {
constructor(client) {
this.client = client;
}
handle(data) {
return data;
}
}
module.exports = GenericAction;

@ -0,0 +1,40 @@
class ActionsManager {
constructor(client) {
this.client = client;
this.register(require('./MessageCreate'));
this.register(require('./MessageDelete'));
this.register(require('./MessageDeleteBulk'));
this.register(require('./MessageUpdate'));
this.register(require('./MessageReactionAdd'));
this.register(require('./MessageReactionRemove'));
this.register(require('./MessageReactionRemoveAll'));
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
this.register(require('./ChannelUpdate'));
this.register(require('./GuildDelete'));
this.register(require('./GuildUpdate'));
this.register(require('./GuildMemberGet'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
this.register(require('./GuildRoleUpdate'));
this.register(require('./UserGet'));
this.register(require('./UserUpdate'));
this.register(require('./UserNoteUpdate'));
this.register(require('./GuildSync'));
this.register(require('./GuildEmojiCreate'));
this.register(require('./GuildEmojiDelete'));
this.register(require('./GuildEmojiUpdate'));
this.register(require('./GuildEmojisUpdate'));
this.register(require('./GuildRolesPositionUpdate'));
this.register(require('./GuildChannelsPositionUpdate'));
}
register(Action) {
this[Action.name.replace(/Action$/, '')] = new Action(this.client);
}
}
module.exports = ActionsManager;

@ -0,0 +1,11 @@
const Action = require('./Action');
class ChannelCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.dataManager.newChannel(data);
return { channel };
}
}
module.exports = ChannelCreateAction;

@ -0,0 +1,30 @@
const Action = require('./Action');
class ChannelDeleteAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
let channel = client.channels.get(data.id);
if (channel) {
client.dataManager.killChannel(channel);
this.deleted.set(channel.id, channel);
this.scheduleForDeletion(channel.id);
} else {
channel = this.deleted.get(data.id) || null;
}
if (channel) channel.deleted = true;
return { channel };
}
scheduleForDeletion(id) {
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout);
}
}
module.exports = ChannelDeleteAction;

@ -0,0 +1,34 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
class ChannelUpdateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.id);
if (channel) {
const oldChannel = Util.cloneObject(channel);
channel.setup(data);
client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
return {
old: oldChannel,
updated: channel,
};
}
return {
old: null,
updated: null,
};
}
}
/**
* Emitted whenever a channel is updated - e.g. name change, topic change.
* @event Client#channelUpdate
* @param {Channel} oldChannel The channel before the update
* @param {Channel} newChannel The channel after the update
*/
module.exports = ChannelUpdateAction;

@ -0,0 +1,13 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class GuildBanRemove extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const user = client.dataManager.newUser(data.user);
if (guild && user) client.emit(Constants.Events.GUILD_BAN_REMOVE, guild, user);
}
}
module.exports = GuildBanRemove;

@ -0,0 +1,19 @@
const Action = require('./Action');
class GuildChannelsPositionUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
if (guild) {
for (const partialChannel of data.channels) {
const channel = guild.channels.get(partialChannel.id);
if (channel) channel.position = partialChannel.position;
}
}
return { guild };
}
}
module.exports = GuildChannelsPositionUpdate;

@ -0,0 +1,57 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class GuildDeleteAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
let guild = client.guilds.get(data.id);
if (guild) {
for (const channel of guild.channels.values()) {
if (channel.type === 'text') channel.stopTyping(true);
}
if (guild.available && data.unavailable) {
// Guild is unavailable
guild.available = false;
client.emit(Constants.Events.GUILD_UNAVAILABLE, guild);
// Stops the GuildDelete packet thinking a guild was actually deleted,
// handles emitting of event itself
return {
guild: null,
};
}
for (const channel of guild.channels.values()) this.client.channels.delete(channel.id);
if (guild.voiceConnection) guild.voiceConnection.disconnect();
// Delete guild
client.guilds.delete(guild.id);
this.deleted.set(guild.id, guild);
this.scheduleForDeletion(guild.id);
} else {
guild = this.deleted.get(data.id) || null;
}
if (guild) guild.deleted = true;
return { guild };
}
scheduleForDeletion(id) {
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout);
}
}
/**
* Emitted whenever a guild becomes unavailable, likely due to a server outage.
* @event Client#guildUnavailable
* @param {Guild} guild The guild that has become unavailable
*/
module.exports = GuildDeleteAction;

@ -0,0 +1,17 @@
const Action = require('./Action');
class GuildEmojiCreateAction extends Action {
handle(guild, createdEmoji) {
const client = this.client;
const emoji = client.dataManager.newEmoji(createdEmoji, guild);
return { emoji };
}
}
/**
* Emitted whenever a custom emoji is created in a guild.
* @event Client#emojiCreate
* @param {Emoji} emoji The emoji that was created
*/
module.exports = GuildEmojiCreateAction;

@ -0,0 +1,18 @@
const Action = require('./Action');
class GuildEmojiDeleteAction extends Action {
handle(emoji) {
const client = this.client;
client.dataManager.killEmoji(emoji);
emoji.deleted = true;
return { emoji };
}
}
/**
* Emitted whenever a custom guild emoji is deleted.
* @event Client#emojiDelete
* @param {Emoji} emoji The emoji that was deleted
*/
module.exports = GuildEmojiDeleteAction;

@ -0,0 +1,17 @@
const Action = require('./Action');
class GuildEmojiUpdateAction extends Action {
handle(oldEmoji, newEmoji) {
const emoji = this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
return { emoji };
}
}
/**
* Emitted whenever a custom guild emoji is updated.
* @event Client#emojiUpdate
* @param {Emoji} oldEmoji The old emoji
* @param {Emoji} newEmoji The new emoji
*/
module.exports = GuildEmojiUpdateAction;

@ -0,0 +1,38 @@
const Action = require('./Action');
function mappify(iterable) {
const map = new Map();
for (const x of iterable) map.set(...x);
return map;
}
class GuildEmojisUpdateAction extends Action {
handle(data) {
const guild = this.client.guilds.get(data.guild_id);
if (!guild || !guild.emojis) return;
const deletions = mappify(guild.emojis.entries());
for (const emoji of data.emojis) {
// Determine type of emoji event
const cachedEmoji = guild.emojis.get(emoji.id);
if (cachedEmoji) {
deletions.delete(emoji.id);
if (!cachedEmoji.equals(emoji, true)) {
// Emoji updated
this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
}
} else {
// Emoji added
this.client.actions.GuildEmojiCreate.handle(guild, emoji);
}
}
for (const emoji of deletions.values()) {
// Emoji deleted
this.client.actions.GuildEmojiDelete.handle(emoji);
}
}
}
module.exports = GuildEmojisUpdateAction;

@ -0,0 +1,10 @@
const Action = require('./Action');
class GuildMemberGetAction extends Action {
handle(guild, data) {
const member = guild._addMember(data, false);
return { member };
}
}
module.exports = GuildMemberGetAction;

@ -0,0 +1,41 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class GuildMemberRemoveAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
let member = null;
if (guild) {
member = guild.members.get(data.user.id);
guild.memberCount--;
if (member) {
guild._removeMember(member);
this.deleted.set(guild.id + data.user.id, member);
if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, member);
this.scheduleForDeletion(guild.id, data.user.id);
} else {
member = this.deleted.get(guild.id + data.user.id) || null;
}
if (member) member.deleted = true;
}
return { guild, member };
}
scheduleForDeletion(guildID, userID) {
this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.restWsBridgeTimeout);
}
}
/**
* Emitted whenever a member leaves a guild, or is kicked.
* @event Client#guildMemberRemove
* @param {GuildMember} member The member that has left/been kicked from the guild
*/
module.exports = GuildMemberRemoveAction;

@ -0,0 +1,26 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Role = require('../../structures/Role');
class GuildRoleCreate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
let role;
if (guild) {
const already = guild.roles.has(data.role.id);
role = new Role(guild, data.role);
guild.roles.set(role.id, role);
if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role);
}
return { role };
}
}
/**
* Emitted whenever a role is created.
* @event Client#roleCreate
* @param {Role} role The role that was created
*/
module.exports = GuildRoleCreate;

@ -0,0 +1,42 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class GuildRoleDeleteAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
let role;
if (guild) {
role = guild.roles.get(data.role_id);
if (role) {
guild.roles.delete(data.role_id);
this.deleted.set(guild.id + data.role_id, role);
this.scheduleForDeletion(guild.id, data.role_id);
client.emit(Constants.Events.GUILD_ROLE_DELETE, role);
} else {
role = this.deleted.get(guild.id + data.role_id) || null;
}
if (role) role.deleted = true;
}
return { role };
}
scheduleForDeletion(guildID, roleID) {
this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.restWsBridgeTimeout);
}
}
/**
* Emitted whenever a guild role is deleted.
* @event Client#roleDelete
* @param {Role} role The role that was deleted
*/
module.exports = GuildRoleDeleteAction;

@ -0,0 +1,41 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
class GuildRoleUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
if (guild) {
const roleData = data.role;
let oldRole = null;
const role = guild.roles.get(roleData.id);
if (role) {
oldRole = Util.cloneObject(role);
role.setup(data.role);
client.emit(Constants.Events.GUILD_ROLE_UPDATE, oldRole, role);
}
return {
old: oldRole,
updated: role,
};
}
return {
old: null,
updated: null,
};
}
}
/**
* Emitted whenever a guild role is updated.
* @event Client#roleUpdate
* @param {Role} oldRole The role before the update
* @param {Role} newRole The role after the update
*/
module.exports = GuildRoleUpdateAction;

@ -0,0 +1,19 @@
const Action = require('./Action');
class GuildRolesPositionUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
if (guild) {
for (const partialRole of data.roles) {
const role = guild.roles.get(partialRole.id);
if (role) role.position = partialRole.position;
}
}
return { guild };
}
}
module.exports = GuildRolesPositionUpdate;

@ -0,0 +1,29 @@
const Action = require('./Action');
class GuildSync extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.id);
if (guild) {
if (data.presences) {
for (const presence of data.presences) guild._setPresence(presence.user.id, presence);
}
if (data.members) {
for (const syncMember of data.members) {
const member = guild.members.get(syncMember.user.id);
if (member) {
guild._updateMember(member, syncMember);
} else {
guild._addMember(syncMember, false);
}
}
}
if ('large' in data) guild.large = data.large;
}
}
}
module.exports = GuildSync;

@ -0,0 +1,34 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
class GuildUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.id);
if (guild) {
const oldGuild = Util.cloneObject(guild);
guild.setup(data);
client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
return {
old: oldGuild,
updated: guild,
};
}
return {
old: null,
updated: null,
};
}
}
/**
* Emitted whenever a guild is updated - e.g. name change.
* @event Client#guildUpdate
* @param {Guild} oldGuild The guild before the update
* @param {Guild} newGuild The guild after the update
*/
module.exports = GuildUpdateAction;

@ -0,0 +1,55 @@
const Action = require('./Action');
const Message = require('../../structures/Message');
class MessageCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id);
const user = client.users.get((data instanceof Array ? data[0] : data).author.id);
if (channel) {
const member = channel.guild ? channel.guild.member(user) : null;
if (data instanceof Array) {
const messages = new Array(data.length);
for (let i = 0; i < data.length; i++) {
messages[i] = channel._cacheMessage(new Message(channel, data[i], client));
}
const lastMessage = messages[messages.length - 1];
channel.lastMessageID = lastMessage.id;
channel.lastMessage = lastMessage;
if (user) {
user.lastMessageID = lastMessage.id;
user.lastMessage = lastMessage;
}
if (member) {
member.lastMessageID = lastMessage.id;
member.lastMessage = lastMessage;
}
return {
messages,
};
} else {
const message = channel._cacheMessage(new Message(channel, data, client));
channel.lastMessageID = data.id;
channel.lastMessage = message;
if (user) {
user.lastMessageID = data.id;
user.lastMessage = message;
}
if (member) {
member.lastMessageID = data.id;
member.lastMessage = message;
}
return {
message,
};
}
}
return {
message: null,
};
}
}
module.exports = MessageCreateAction;

@ -0,0 +1,35 @@
const Action = require('./Action');
class MessageDeleteAction extends Action {
constructor(client) {
super(client);
this.deleted = new Map();
}
handle(data) {
const client = this.client;
const channel = client.channels.get(data.channel_id);
let message;
if (channel) {
message = channel.messages.get(data.id);
if (message) {
channel.messages.delete(message.id);
this.deleted.set(channel.id + message.id, message);
this.scheduleForDeletion(channel.id, message.id);
} else {
message = this.deleted.get(channel.id + data.id) || null;
}
if (message) message.deleted = true;
}
return { message };
}
scheduleForDeletion(channelID, messageID) {
this.client.setTimeout(() => this.deleted.delete(channelID + messageID),
this.client.options.restWsBridgeTimeout);
}
}
module.exports = MessageDeleteAction;

@ -0,0 +1,26 @@
const Action = require('./Action');
const Collection = require('../../util/Collection');
const Constants = require('../../util/Constants');
class MessageDeleteBulkAction extends Action {
handle(data) {
const messages = new Collection();
const channel = this.client.channels.get(data.channel_id);
if (channel) {
for (const id of data.ids) {
const message = channel.messages.get(id);
if (message) {
message.deleted = true;
messages.set(message.id, message);
channel.messages.delete(id);
}
}
}
if (messages.size > 0) this.client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);
return { messages };
}
}
module.exports = MessageDeleteBulkAction;

@ -0,0 +1,37 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id' } }
*/
class MessageReactionAdd extends Action {
handle(data) {
const user = this.client.users.get(data.user_id);
if (!user) return false;
// Verify channel
const channel = this.client.channels.get(data.channel_id);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
if (!message) return false;
if (!data.emoji) return false;
// Verify reaction
const reaction = message._addReaction(data.emoji, user);
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_ADD, reaction, user);
return { message, reaction, user };
}
}
/**
* Emitted whenever a reaction is added to a cached message.
* @event Client#messageReactionAdd
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that applied the emoji or reaction emoji
*/
module.exports = MessageReactionAdd;

@ -0,0 +1,37 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id' } }
*/
class MessageReactionRemove extends Action {
handle(data) {
const user = this.client.users.get(data.user_id);
if (!user) return false;
// Verify channel
const channel = this.client.channels.get(data.channel_id);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
if (!message) return false;
if (!data.emoji) return false;
// Verify reaction
const reaction = message._removeReaction(data.emoji, user);
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE, reaction, user);
return { message, reaction, user };
}
}
/**
* Emitted whenever a reaction is removed from a cached message.
* @event Client#messageReactionRemove
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user whose emoji or reaction emoji was removed
*/
module.exports = MessageReactionRemove;

@ -0,0 +1,25 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class MessageReactionRemoveAll extends Action {
handle(data) {
const channel = this.client.channels.get(data.channel_id);
if (!channel || channel.type === 'voice') return false;
const message = channel.messages.get(data.message_id);
if (!message) return false;
message._clearReactions();
this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_ALL, message);
return { message };
}
}
/**
* Emitted whenever all reactions are removed from a cached message.
* @event Client#messageReactionRemoveAll
* @param {Message} message The message the reactions were removed from
*/
module.exports = MessageReactionRemoveAll;

@ -0,0 +1,40 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class MessageUpdateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.channel_id);
if (channel) {
const message = channel.messages.get(data.id);
if (message) {
message.patch(data);
client.emit(Constants.Events.MESSAGE_UPDATE, message._edits[0], message);
return {
old: message._edits[0],
updated: message,
};
}
return {
old: message,
updated: message,
};
}
return {
old: null,
updated: null,
};
}
}
/**
* Emitted whenever a message is updated - e.g. embed or content change.
* @event Client#messageUpdate
* @param {Message} oldMessage The message before the update
* @param {Message} newMessage The message after the update
*/
module.exports = MessageUpdateAction;

@ -0,0 +1,11 @@
const Action = require('./Action');
class UserGetAction extends Action {
handle(data) {
const client = this.client;
const user = client.dataManager.newUser(data);
return { user };
}
}
module.exports = UserGetAction;

@ -0,0 +1,30 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class UserNoteUpdateAction extends Action {
handle(data) {
const client = this.client;
const oldNote = client.user.notes.get(data.id);
const note = data.note.length ? data.note : null;
client.user.notes.set(data.id, note);
client.emit(Constants.Events.USER_NOTE_UPDATE, data.id, oldNote, note);
return {
old: oldNote,
updated: note,
};
}
}
/**
* Emitted whenever a note is updated.
* @event Client#userNoteUpdate
* @param {User} user The user the note belongs to
* @param {string} oldNote The note content before the update
* @param {string} newNote The note content after the update
*/
module.exports = UserNoteUpdateAction;

@ -0,0 +1,33 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
const Util = require('../../util/Util');
class UserUpdateAction extends Action {
handle(data) {
const client = this.client;
if (client.user) {
if (client.user.equals(data)) {
return {
old: client.user,
updated: client.user,
};
}
const oldUser = Util.cloneObject(client.user);
client.user.patch(data);
client.emit(Constants.Events.USER_UPDATE, oldUser, client.user);
return {
old: oldUser,
updated: client.user,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = UserUpdateAction;

@ -0,0 +1,52 @@
const snekfetch = require('snekfetch');
const Constants = require('../../util/Constants');
class APIRequest {
constructor(rest, method, path, auth, data, files, reason) {
this.rest = rest;
this.client = rest.client;
this.method = method;
this.path = path.toString();
this.auth = auth;
this.data = data;
this.files = files;
this.route = this.getRoute(this.path);
this.reason = reason;
}
getRoute(url) {
let route = url.split('?')[0];
if (route.includes('/channels/') || route.includes('/guilds/')) {
const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/');
const majorID = route.substring(startInd).split('/')[2];
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
}
return route;
}
getAuth() {
if (this.client.token && this.client.user && this.client.user.bot) {
return `Bot ${this.client.token}`;
} else if (this.client.token) {
return this.client.token;
}
throw new Error(Constants.Errors.NO_TOKEN);
}
gen() {
const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`;
const request = snekfetch[this.method](`${API}${this.path}`);
if (this.auth) request.set('Authorization', this.getAuth());
if (this.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.reason));
if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent);
if (this.files) {
for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name);
if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data));
} else if (this.data) {
request.send(this.data);
}
return request;
}
}
module.exports = APIRequest;

@ -0,0 +1,60 @@
/**
* Represents an error from the Discord API.
* @extends Error
*/
class DiscordAPIError extends Error {
constructor(path, error, method) {
super();
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
this.name = 'DiscordAPIError';
this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened;
/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = path;
/**
* HTTP error code returned by Discord
* @type {number}
*/
this.code = error.code;
/**
* The HTTP method used for the request
* @type {string}
*/
this.method = method;
}
/**
* Flattens an errors object returned from the API into an array.
* @param {Object} obj Discord errors object
* @param {string} [key] Used internally to determine key names of nested fields
* @returns {string[]}
* @private
*/
static flattenErrors(obj, key = '') {
let messages = [];
for (const k of Object.keys(obj)) {
if (k === 'message') continue;
const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k;
if (obj[k]._errors) {
messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`);
} else if (obj[k].code || obj[k].message) {
messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}: ${obj[k].message}`.trim());
} else if (typeof obj[k] === 'string') {
messages.push(obj[k]);
} else {
messages = messages.concat(this.flattenErrors(obj[k], newKey));
}
}
return messages;
}
}
module.exports = DiscordAPIError;

@ -0,0 +1,57 @@
const UserAgentManager = require('./UserAgentManager');
const RESTMethods = require('./RESTMethods');
const SequentialRequestHandler = require('./RequestHandlers/Sequential');
const BurstRequestHandler = require('./RequestHandlers/Burst');
const APIRequest = require('./APIRequest');
const Constants = require('../../util/Constants');
class RESTManager {
constructor(client) {
this.client = client;
this.handlers = {};
this.userAgentManager = new UserAgentManager(this);
this.methods = new RESTMethods(this);
this.rateLimitedEndpoints = {};
this.globallyRateLimited = false;
}
destroy() {
for (const handlerKey of Object.keys(this.handlers)) {
const handler = this.handlers[handlerKey];
if (handler.destroy) handler.destroy();
}
}
push(handler, apiRequest) {
return new Promise((resolve, reject) => {
handler.push({
request: apiRequest,
resolve,
reject,
});
});
}
getRequestHandler() {
switch (this.client.options.apiRequestMethod) {
case 'sequential':
return SequentialRequestHandler;
case 'burst':
return BurstRequestHandler;
default:
throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD);
}
}
makeRequest(method, url, auth, data, file, reason) {
const apiRequest = new APIRequest(this, method, url, auth, data, file, reason);
if (!this.handlers[apiRequest.route]) {
const RequestHandlerType = this.getRequestHandler();
this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route);
}
return this.push(this.handlers[apiRequest.route], apiRequest);
}
}
module.exports = RESTManager;

@ -0,0 +1,949 @@
const querystring = require('querystring');
const long = require('long');
const Permissions = require('../../util/Permissions');
const Constants = require('../../util/Constants');
const Endpoints = Constants.Endpoints;
const Collection = require('../../util/Collection');
const Util = require('../../util/Util');
const resolvePermissions = require('../../structures/shared/resolvePermissions');
const RichEmbed = require('../../structures/RichEmbed');
const User = require('../../structures/User');
const GuildMember = require('../../structures/GuildMember');
const Message = require('../../structures/Message');
const Role = require('../../structures/Role');
const Invite = require('../../structures/Invite');
const Webhook = require('../../structures/Webhook');
const UserProfile = require('../../structures/UserProfile');
const OAuth2Application = require('../../structures/OAuth2Application');
const Channel = require('../../structures/Channel');
const GroupDMChannel = require('../../structures/GroupDMChannel');
const Guild = require('../../structures/Guild');
const VoiceRegion = require('../../structures/VoiceRegion');
const GuildAuditLogs = require('../../structures/GuildAuditLogs');
class RESTMethods {
constructor(restManager) {
this.rest = restManager;
this.client = restManager.client;
this._ackToken = null;
}
login(token = this.client.token) {
return new Promise((resolve, reject) => {
if (!token || typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN);
token = token.replace(/^Bot\s*/i, '');
this.client.manager.connectToWebSocket(token, resolve, reject);
}).catch(e => {
this.client.destroy();
return Promise.reject(e);
});
}
logout() {
return this.rest.makeRequest('post', Endpoints.logout, true, {});
}
getGateway(bot = false) {
return this.rest.makeRequest('get', bot ? Endpoints.gateway.bot : Endpoints.gateway, true);
}
fetchVoiceRegions(guildID) {
let endpoint;
if (guildID) endpoint = Endpoints.Guild(guildID).voiceRegions;
else endpoint = Endpoints.voiceRegions;
return this.rest.makeRequest('get', endpoint, true).then(res => {
const regions = new Collection();
for (const region of res) regions.set(region.id, new VoiceRegion(region));
return regions;
});
}
sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, files = null) {
return new Promise((resolve, reject) => { // eslint-disable-line complexity
if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
// The nonce has to be a uint64 :<
if (typeof nonce !== 'undefined') {
nonce = parseInt(nonce);
if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.');
}
if (content) {
if (split && typeof split !== 'object') split = {};
// Wrap everything in a code block
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true);
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
if (split) {
split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`;
split.append = '\n```';
}
}
// Add zero-width spaces to @everyone/@here
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}
// Add the reply prefix
if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
const id = this.client.resolver.resolveUserID(reply);
const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
content = `${mention}${content ? `, ${content}` : ''}`;
if (split) split.prepend = `${mention}, ${split.prepend || ''}`;
}
// Split the content
if (split) content = Util.splitMessage(content, split);
} else if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
const id = this.client.resolver.resolveUserID(reply);
content = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
}
const send = chan => {
if (content instanceof Array) {
const messages = [];
(function sendChunk(list, index) {
const options = index === list.length - 1 ? { tts, embed, files } : { tts };
chan.send(list[index], options).then(message => {
messages.push(message);
if (index >= list.length - 1) return resolve(messages);
return sendChunk(list, ++index);
}).catch(reject);
}(content, 0));
} else {
this.rest.makeRequest('post', Endpoints.Channel(chan).messages, true, {
content, tts, nonce, embed,
}, files).then(data => resolve(this.client.actions.MessageCreate.handle(data).message), reject);
}
};
if (channel instanceof User || channel instanceof GuildMember) this.createDM(channel).then(send, reject);
else send(channel);
});
}
updateMessage(message, content, { embed, code, reply } = {}) {
if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
// Wrap everything in a code block
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true);
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
}
// Add the reply prefix
if (reply && message.channel.type !== 'dm') {
const id = this.client.resolver.resolveUserID(reply);
const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
content = `${mention}${content ? `, ${content}` : ''}`;
}
if (embed instanceof RichEmbed) embed = embed._apiTransform();
return this.rest.makeRequest('patch', Endpoints.Message(message), true, {
content, embed,
}).then(data => this.client.actions.MessageUpdate.handle(data).updated);
}
deleteMessage(message) {
return this.rest.makeRequest('delete', Endpoints.Message(message), true)
.then(() =>
this.client.actions.MessageDelete.handle({
id: message.id,
channel_id: message.channel.id,
}).message
);
}
ackMessage(message) {
return this.rest.makeRequest('post', Endpoints.Message(message).ack, true, { token: this._ackToken }).then(res => {
if (res.token) this._ackToken = res.token;
return message;
});
}
ackTextChannel(channel) {
return this.rest.makeRequest('post', Endpoints.Channel(channel).Message(channel.lastMessageID).ack, true, {
token: this._ackToken,
}).then(res => {
if (res.token) this._ackToken = res.token;
return channel;
});
}
ackGuild(guild) {
return this.rest.makeRequest('post', Endpoints.Guild(guild).ack, true).then(() => guild);
}
bulkDeleteMessages(channel, messages) {
return this.rest.makeRequest('post', Endpoints.Channel(channel).messages.bulkDelete, true, {
messages: messages,
}).then(() =>
this.client.actions.MessageDeleteBulk.handle({
channel_id: channel.id,
ids: messages,
}).messages
);
}
search(target, options) {
if (typeof options === 'string') options = { content: options };
if (options.before) {
if (!(options.before instanceof Date)) options.before = new Date(options.before);
options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString();
}
if (options.after) {
if (!(options.after instanceof Date)) options.after = new Date(options.after);
options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString();
}
if (options.during) {
if (!(options.during instanceof Date)) options.during = new Date(options.during);
const t = options.during.getTime() - 14200704e5;
options.minID = long.fromNumber(t).shiftLeft(22).toString();
options.maxID = long.fromNumber(t + 86400000).shiftLeft(22).toString();
}
if (options.channel) options.channel = this.client.resolver.resolveChannelID(options.channel);
if (options.author) options.author = this.client.resolver.resolveUserID(options.author);
if (options.mentions) options.mentions = this.client.resolver.resolveUserID(options.options.mentions);
options = {
content: options.content,
max_id: options.maxID,
min_id: options.minID,
has: options.has,
channel_id: options.channel,
author_id: options.author,
author_type: options.authorType,
context_size: options.contextSize,
sort_by: options.sortBy,
sort_order: options.sortOrder,
limit: options.limit,
offset: options.offset,
mentions: options.mentions,
mentions_everyone: options.mentionsEveryone,
link_hostname: options.linkHostname,
embed_provider: options.embedProvider,
embed_type: options.embedType,
attachment_filename: options.attachmentFilename,
attachment_extension: options.attachmentExtension,
include_nsfw: options.nsfw,
};
for (const key in options) if (options[key] === undefined) delete options[key];
const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
let endpoint;
if (target instanceof Channel) {
endpoint = Endpoints.Channel(target).search;
} else if (target instanceof Guild) {
endpoint = Endpoints.Guild(target).search;
} else {
throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.');
}
return this.rest.makeRequest('get', `${endpoint}?${queryString}`, true).then(body => {
const messages = body.messages.map(x =>
x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))
);
return {
totalResults: body.total_results,
messages,
};
});
}
createChannel(guild, channelName, channelType, overwrites, reason) {
return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, {
name: channelName,
type: channelType ? Constants.ChannelTypes[channelType.toUpperCase()] : 'text',
permission_overwrites: resolvePermissions.call(this, overwrites, guild),
}, undefined, reason).then(data => this.client.actions.ChannelCreate.handle(data).channel);
}
createDM(recipient) {
const dmChannel = this.getExistingDM(recipient);
if (dmChannel) return Promise.resolve(dmChannel);
return this.rest.makeRequest('post', Endpoints.User(this.client.user).channels, true, {
recipient_id: recipient.id,
}).then(data => this.client.actions.ChannelCreate.handle(data).channel);
}
createGroupDM(options) {
const data = this.client.user.bot ?
{ access_tokens: options.accessTokens, nicks: options.nicks } :
{ recipients: options.recipients };
return this.rest.makeRequest('post', Endpoints.User('@me').channels, true, data)
.then(res => new GroupDMChannel(this.client, res));
}
addUserToGroupDM(channel, options) {
const data = this.client.user.bot ?
{ nick: options.nick, access_token: options.accessToken } :
{ recipient: options.id };
return this.rest.makeRequest('put', Endpoints.Channel(channel).Recipient(options.id), true, data)
.then(() => channel);
}
removeUserFromGroupDM(channel, userId) {
return this.rest.makeRequest('delete', Endpoints.Channel(channel).Recipient(userId), true)
.then(() => channel);
}
updateGroupDMChannel(channel, _data) {
const data = {};
data.name = _data.name;
data.icon = _data.icon;
return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(() => channel);
}
getExistingDM(recipient) {
return this.client.channels.find(channel =>
channel.recipient && channel.recipient.id === recipient.id
);
}
deleteChannel(channel, reason) {
if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel);
if (!channel) return Promise.reject(new Error('No channel to delete.'));
return this.rest.makeRequest('delete', Endpoints.Channel(channel), true, undefined, undefined, reason)
.then(data => {
data.id = channel.id;
return this.client.actions.ChannelDelete.handle(data).channel;
});
}
updateChannel(channel, _data, reason) {
const data = {};
data.name = (_data.name || channel.name).trim();
data.topic = typeof _data.topic === 'undefined' ? channel.topic : _data.topic;
data.nsfw = typeof _data.nsfw === 'undefined' ? channel.nsfw : _data.nsfw;
data.position = _data.position || channel.position;
data.bitrate = _data.bitrate || (channel.bitrate ? channel.bitrate * 1000 : undefined);
data.user_limit = typeof _data.userLimit !== 'undefined' ? _data.userLimit : channel.userLimit;
data.parent_id = _data.parent;
data.permission_overwrites = _data.permissionOverwrites ?
resolvePermissions.call(this, _data.permissionOverwrites, channel.guild) : undefined;
return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data, undefined, reason).then(newData =>
this.client.actions.ChannelUpdate.handle(newData).updated
);
}
leaveGuild(guild) {
if (guild.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.'));
return this.rest.makeRequest('delete', Endpoints.User('@me').Guild(guild.id), true).then(() =>
this.client.actions.GuildDelete.handle({ id: guild.id }).guild
);
}
createGuild(options) {
options.icon = this.client.resolver.resolveBase64(options.icon) || null;
options.region = options.region || 'us-central';
return new Promise((resolve, reject) => {
this.rest.makeRequest('post', Endpoints.guilds, true, options).then(data => {
if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id));
const handleGuild = guild => {
if (guild.id === data.id) {
this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild);
this.client.clearTimeout(timeout);
resolve(guild);
}
};
this.client.on(Constants.Events.GUILD_CREATE, handleGuild);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild);
reject(new Error('Took too long to receive guild data.'));
}, 10000);
return undefined;
}, reject);
});
}
// Untested but probably will work
deleteGuild(guild) {
return this.rest.makeRequest('delete', Endpoints.Guild(guild), true).then(() =>
this.client.actions.GuildDelete.handle({ id: guild.id }).guild
);
}
getUser(userID, cache) {
return this.rest.makeRequest('get', Endpoints.User(userID), true).then(data => {
if (cache) return this.client.actions.UserGet.handle(data).user;
else return new User(this.client, data);
});
}
updateCurrentUser(_data, password) {
const user = this.client.user;
const data = {};
data.username = _data.username || user.username;
data.avatar = typeof _data.avatar === 'undefined' ? user.avatar : this.client.resolver.resolveBase64(_data.avatar);
if (!user.bot) {
data.email = _data.email || user.email;
data.password = password;
if (_data.new_password) data.new_password = _data.newPassword;
}
return this.rest.makeRequest('patch', Endpoints.User('@me'), true, data).then(newData =>
this.client.actions.UserUpdate.handle(newData).updated
);
}
updateGuild(guild, data, reason) {
return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data, undefined, reason).then(newData =>
this.client.actions.GuildUpdate.handle(newData).updated
);
}
kickGuildMember(guild, member, reason) {
return this.rest.makeRequest(
'delete', Endpoints.Guild(guild).Member(member), true,
undefined, undefined, reason)
.then(() => member);
}
createGuildRole(guild, data, reason) {
if (data.color) data.color = this.client.resolver.resolveColor(data.color);
if (data.permissions) data.permissions = Permissions.resolve(data.permissions);
return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data, undefined, reason).then(r => {
const { role } = this.client.actions.GuildRoleCreate.handle({
guild_id: guild.id,
role: r,
});
if (data.position) return role.setPosition(data.position, reason);
return role;
});
}
deleteGuildRole(role, reason) {
return this.rest.makeRequest(
'delete', Endpoints.Guild(role.guild).Role(role.id), true,
undefined, undefined, reason)
.then(() =>
this.client.actions.GuildRoleDelete.handle({
guild_id: role.guild.id,
role_id: role.id,
}).role
);
}
setChannelOverwrite(channel, payload) {
return this.rest.makeRequest('put', `${Endpoints.Channel(channel).permissions}/${payload.id}`, true, payload);
}
deletePermissionOverwrites(overwrite, reason) {
return this.rest.makeRequest(
'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`,
true, undefined, undefined, reason
).then(() => overwrite);
}
getChannelMessages(channel, payload = {}) {
const params = [];
if (payload.limit) params.push(`limit=${payload.limit}`);
if (payload.around) params.push(`around=${payload.around}`);
else if (payload.before) params.push(`before=${payload.before}`);
else if (payload.after) params.push(`after=${payload.after}`);
let endpoint = Endpoints.Channel(channel).messages;
if (params.length > 0) endpoint += `?${params.join('&')}`;
return this.rest.makeRequest('get', endpoint, true);
}
getChannelMessage(channel, messageID) {
const msg = channel.messages.get(messageID);
if (msg) return Promise.resolve(msg);
return this.rest.makeRequest('get', Endpoints.Channel(channel).Message(messageID), true);
}
putGuildMember(guild, user, options) {
options.access_token = options.accessToken;
if (options.roles) {
const roles = options.roles;
if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) {
options.roles = roles.map(role => role.id);
}
}
return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(user.id), true, options)
.then(data => this.client.actions.GuildMemberGet.handle(guild, data).member);
}
getGuildMember(guild, user, cache) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(user.id), true).then(data => {
if (cache) return this.client.actions.GuildMemberGet.handle(guild, data).member;
else return new GuildMember(guild, data);
});
}
updateGuildMember(member, data, reason) {
if (data.channel) {
data.channel_id = this.client.resolver.resolveChannel(data.channel).id;
data.channel = null;
}
if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
let endpoint = Endpoints.Member(member);
// Fix your endpoints, discord ;-;
if (member.id === this.client.user.id) {
const keys = Object.keys(data);
if (keys.length === 1 && keys[0] === 'nick') {
endpoint = Endpoints.Member(member).nickname;
}
}
return this.rest.makeRequest('patch', endpoint, true, data, undefined, reason).then(newData =>
member.guild._updateMember(member, newData).mem
);
}
addMemberRole(member, role, reason) {
return new Promise((resolve, reject) => {
if (member._roles.includes(role.id)) return resolve(member);
const listener = (oldMember, newMember) => {
if (newMember.id === member.id && !oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
resolve(newMember);
}
};
this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
reject(new Error('Adding the role timed out.'));
}, 10e3);
return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
});
}
removeMemberRole(member, role, reason) {
return new Promise((resolve, reject) => {
if (!member._roles.includes(role.id)) return resolve(member);
const listener = (oldMember, newMember) => {
if (newMember.id === member.id && oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
resolve(newMember);
}
};
this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
reject(new Error('Removing the role timed out.'));
}, 10e3);
return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
});
}
sendTyping(channelID) {
return this.rest.makeRequest('post', Endpoints.Channel(channelID).typing, true);
}
banGuildMember(guild, member, options) {
const id = this.client.resolver.resolveUserID(member);
if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.'));
const url = `${Endpoints.Guild(guild).bans}/${id}?${querystring.stringify(options)}`;
return this.rest.makeRequest('put', url, true).then(() => {
if (member instanceof GuildMember) return member;
const user = this.client.resolver.resolveUser(id);
if (user) {
member = this.client.resolver.resolveGuildMember(guild, user);
return member || user;
}
return id;
});
}
unbanGuildMember(guild, member, reason) {
return new Promise((resolve, reject) => {
const id = this.client.resolver.resolveUserID(member);
if (!id) throw new Error('Couldn\'t resolve the user ID to unban.');
const listener = (eGuild, eUser) => {
if (eGuild.id === guild.id && eUser.id === id) {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
resolve(eUser);
}
};
this.client.on(Constants.Events.GUILD_BAN_REMOVE, listener);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
reject(new Error('Took too long to receive the ban remove event.'));
}, 10000);
this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
});
}
getGuildBans(guild) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans =>
bans.reduce((collection, ban) => {
collection.set(ban.user.id, {
reason: ban.reason,
user: this.client.dataManager.newUser(ban.user),
});
return collection;
}, new Collection())
);
}
updateGuildRole(role, _data, reason) {
const data = {};
data.name = _data.name || role.name;
data.position = typeof _data.position !== 'undefined' ? _data.position : role.position;
data.color = _data.color === null ? null : this.client.resolver.resolveColor(_data.color || role.color);
data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable;
if (typeof _data.permissions !== 'undefined') data.permissions = Permissions.resolve(_data.permissions);
else data.permissions = role.permissions;
return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data, undefined, reason)
.then(_role =>
this.client.actions.GuildRoleUpdate.handle({
role: _role,
guild_id: role.guild.id,
}).updated
);
}
pinMessage(message) {
return this.rest.makeRequest('put', Endpoints.Channel(message.channel).Pin(message.id), true)
.then(() => message);
}
unpinMessage(message) {
return this.rest.makeRequest('delete', Endpoints.Channel(message.channel).Pin(message.id), true)
.then(() => message);
}
getChannelPinnedMessages(channel) {
return this.rest.makeRequest('get', Endpoints.Channel(channel).pins, true);
}
createChannelInvite(channel, options, reason) {
const payload = {};
payload.temporary = options.temporary;
payload.max_age = options.maxAge;
payload.max_uses = options.maxUses;
payload.unique = options.unique;
return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload, undefined, reason)
.then(invite => new Invite(this.client, invite));
}
deleteInvite(invite, reason) {
return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true, undefined, undefined, reason)
.then(() => invite);
}
getInvite(code) {
return this.rest.makeRequest('get', Endpoints.Invite(code), true).then(invite =>
new Invite(this.client, invite)
);
}
getGuildInvites(guild) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).invites, true).then(inviteItems => {
const invites = new Collection();
for (const inviteItem of inviteItems) {
const invite = new Invite(this.client, inviteItem);
invites.set(invite.code, invite);
}
return invites;
});
}
pruneGuildMembers(guild, days, dry, reason) {
return this.rest.makeRequest(dry ?
'get' :
'post',
`${Endpoints.Guild(guild).prune}?days=${days}`, true, undefined, undefined, reason)
.then(data => data.pruned);
}
createEmoji(guild, image, name, roles, reason) {
const data = { image, name };
if (roles) data.roles = roles.map(r => r.id ? r.id : r);
return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data, undefined, reason)
.then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji);
}
updateEmoji(emoji, _data, reason) {
const data = {};
if (_data.name) data.name = _data.name;
if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r);
return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data, undefined, reason)
.then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji);
}
deleteEmoji(emoji, reason) {
return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, undefined, reason)
.then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data);
}
getGuildAuditLogs(guild, options = {}) {
if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id;
if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id;
if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type];
const queryString = (querystring.stringify({
before: options.before,
after: options.after,
limit: options.limit,
user_id: this.client.resolver.resolveUserID(options.user),
action_type: options.type,
}).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
return this.rest.makeRequest('get', `${Endpoints.Guild(guild).auditLogs}?${queryString}`, true)
.then(data => GuildAuditLogs.build(guild, data));
}
getWebhook(id, token) {
return this.rest.makeRequest('get', Endpoints.Webhook(id, token), !token).then(data =>
new Webhook(this.client, data)
);
}
getGuildWebhooks(guild) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).webhooks, true).then(data => {
const hooks = new Collection();
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
});
}
getChannelWebhooks(channel) {
return this.rest.makeRequest('get', Endpoints.Channel(channel).webhooks, true).then(data => {
const hooks = new Collection();
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
});
}
createWebhook(channel, name, avatar, reason) {
return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar }, undefined, reason)
.then(data => new Webhook(this.client, data));
}
editWebhook(webhook, name, avatar) {
return this.rest.makeRequest('patch', Endpoints.Webhook(webhook.id, webhook.token), false, {
name,
avatar,
}).then(data => {
webhook.name = data.name;
webhook.avatar = data.avatar;
return webhook;
});
}
deleteWebhook(webhook, reason) {
return this.rest.makeRequest(
'delete', Endpoints.Webhook(webhook.id, webhook.token),
false, undefined, undefined, reason);
}
sendWebhookMessage(webhook, content, { avatarURL, tts, embeds, username } = {}, files = null) {
return new Promise((resolve, reject) => {
username = username || webhook.name;
if (content instanceof Array) {
const messages = [];
(function sendChunk(list, index) {
const options = index === list.length - 1 ? { tts, embeds, files } : { tts };
webhook.send(list[index], options).then(message => {
messages.push(message);
if (index >= list.length - 1) return resolve(messages);
return sendChunk(list, ++index);
}).catch(reject);
}(content, 0));
} else {
this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, {
username,
avatar_url: avatarURL,
content,
tts,
embeds,
}, files).then(data => {
if (!this.client.channels) resolve(data);
else resolve(this.client.actions.MessageCreate.handle(data).message);
}, reject);
}
});
}
sendSlackWebhookMessage(webhook, body) {
return this.rest.makeRequest(
'post', `${Endpoints.Webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body
);
}
fetchUserProfile(user) {
return this.rest.makeRequest('get', Endpoints.User(user).profile, true).then(data =>
new UserProfile(user, data)
);
}
fetchMentions(options) {
if (options.guild instanceof Guild) options.guild = options.guild.id;
Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options);
return this.rest.makeRequest(
'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true
).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)));
}
addFriend(user) {
return this.rest.makeRequest('post', Endpoints.User('@me'), true, {
username: user.username,
discriminator: user.discriminator,
}).then(() => user);
}
removeFriend(user) {
return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true)
.then(() => user);
}
blockUser(user) {
return this.rest.makeRequest('put', Endpoints.User('@me').Relationship(user.id), true, { type: 2 })
.then(() => user);
}
unblockUser(user) {
return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true)
.then(() => user);
}
updateChannelPositions(guildID, channels) {
const data = new Array(channels.length);
for (let i = 0; i < channels.length; i++) {
data[i] = {
id: this.client.resolver.resolveChannelID(channels[i].channel),
position: channels[i].position,
};
}
return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, data).then(() =>
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: guildID,
channels,
}).guild
);
}
setRolePositions(guildID, roles) {
return this.rest.makeRequest('patch', Endpoints.Guild(guildID).roles, true, roles).then(() =>
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: guildID,
roles,
}).guild
);
}
setChannelPositions(guildID, channels) {
return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, channels).then(() =>
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: guildID,
channels,
}).guild
);
}
addMessageReaction(message, emoji) {
return this.rest.makeRequest(
'put', Endpoints.Message(message).Reaction(emoji).User('@me'), true
).then(() =>
message._addReaction(Util.parseEmoji(emoji), message.client.user)
);
}
removeMessageReaction(message, emoji, userID) {
const endpoint = Endpoints.Message(message).Reaction(emoji).User(userID === this.client.user.id ? '@me' : userID);
return this.rest.makeRequest('delete', endpoint, true).then(() =>
this.client.actions.MessageReactionRemove.handle({
user_id: userID,
message_id: message.id,
emoji: Util.parseEmoji(emoji),
channel_id: message.channel.id,
}).reaction
);
}
removeMessageReactions(message) {
return this.rest.makeRequest('delete', Endpoints.Message(message).reactions, true)
.then(() => message);
}
getMessageReactionUsers(message, emoji, options) {
const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
return this.rest.makeRequest('get', `${Endpoints.Message(message).Reaction(emoji)}?${queryString}`, true);
}
getApplication(id) {
return this.rest.makeRequest('get', Endpoints.OAUTH2.Application(id), true).then(app =>
new OAuth2Application(this.client, app)
);
}
resetApplication(id) {
return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetToken, true)
.then(() => this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetSecret, true))
.then(app => new OAuth2Application(this.client, app));
}
setNote(user, note) {
return this.rest.makeRequest('put', Endpoints.User(user).note, true, { note }).then(() => user);
}
acceptInvite(code) {
if (code.id) code = code.id;
return new Promise((resolve, reject) =>
this.rest.makeRequest('post', Endpoints.Invite(code), true).then(res => {
const handler = guild => {
if (guild.id === res.id) {
resolve(guild);
this.client.removeListener(Constants.Events.GUILD_CREATE, handler);
}
};
this.client.on(Constants.Events.GUILD_CREATE, handler);
this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_CREATE, handler);
reject(new Error('Accepting invite timed out'));
}, 120e3);
})
);
}
patchUserSettings(data) {
return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data);
}
patchClientUserGuildSettings(guildID, data) {
return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').Guild(guildID).settings, true, data);
}
}
module.exports = RESTMethods;

@ -0,0 +1,84 @@
const RequestHandler = require('./RequestHandler');
const DiscordAPIError = require('../DiscordAPIError');
const { Events: { RATE_LIMIT } } = require('../../../util/Constants');
class BurstRequestHandler extends RequestHandler {
constructor(restManager, endpoint) {
super(restManager, endpoint);
this.client = restManager.client;
this.limit = Infinity;
this.resetTime = null;
this.remaining = 1;
this.timeDifference = 0;
this.resetTimeout = null;
}
push(request) {
super.push(request);
this.handle();
}
execute(item) {
if (!item) return;
item.request.gen().end((err, res) => {
if (res && res.headers) {
this.limit = Number(res.headers['x-ratelimit-limit']);
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.remaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {
this.queue.unshift(item);
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
if (this.resetTimeout) return;
this.resetTimeout = this.client.setTimeout(() => {
this.remaining = this.limit;
this.globalLimit = false;
this.handle();
this.resetTimeout = null;
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
this.resetTimeout = this.client.setTimeout(() => {
this.handle();
this.resetTimeout = null;
}, 1e3 + this.client.options.restTimeOffset);
} else {
item.reject(err.status >= 400 && err.status < 500 ?
new DiscordAPIError(res.request.path, res.body, res.request.method) : err);
this.handle();
}
} else {
if (this.remaining === 0) {
if (this.client.listenerCount(RATE_LIMIT)) {
this.client.emit(RATE_LIMIT, {
limit: this.limit,
timeDifference: this.timeDifference,
path: item.request.path,
method: item.request.method,
});
}
}
this.globalLimit = false;
const data = res && res.body ? res.body : {};
item.resolve(data);
this.handle();
}
});
}
handle() {
super.handle();
if (this.queue.length === 0) return;
if ((this.remaining <= 0 || this.globalLimit) && Date.now() - this.timeDifference < this.resetTime) return;
this.execute(this.queue.shift());
this.remaining--;
this.handle();
}
}
module.exports = BurstRequestHandler;

@ -0,0 +1,54 @@
/**
* A base class for different types of rate limiting handlers for the REST API.
* @private
*/
class RequestHandler {
/**
* @param {RESTManager} restManager The REST manager to use
*/
constructor(restManager) {
/**
* The RESTManager that instantiated this RequestHandler
* @type {RESTManager}
*/
this.restManager = restManager;
/**
* A list of requests that have yet to be processed
* @type {APIRequest[]}
*/
this.queue = [];
}
/**
* Whether or not the client is being rate limited on every endpoint
* @type {boolean}
* @readonly
*/
get globalLimit() {
return this.restManager.globallyRateLimited;
}
set globalLimit(value) {
this.restManager.globallyRateLimited = value;
}
/**
* Push a new API request into this bucket.
* @param {APIRequest} request The new request to push into the queue
*/
push(request) {
this.queue.push(request);
}
/**
* Attempts to get this RequestHandler to process its current queue.
*/
handle() {} // eslint-disable-line no-empty-function
destroy() {
this.queue = [];
}
}
module.exports = RequestHandler;

@ -0,0 +1,126 @@
const RequestHandler = require('./RequestHandler');
const DiscordAPIError = require('../DiscordAPIError');
const { Events: { RATE_LIMIT } } = require('../../../util/Constants');
/**
* Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto
* the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account,
* but it can be slower.
* @extends {RequestHandler}
* @private
*/
class SequentialRequestHandler extends RequestHandler {
/**
* @param {RESTManager} restManager The REST manager to use
* @param {string} endpoint The endpoint to handle
*/
constructor(restManager, endpoint) {
super(restManager, endpoint);
/**
* The client that instantiated this handler
* @type {Client}
*/
this.client = restManager.client;
/**
* The endpoint that this handler is handling
* @type {string}
*/
this.endpoint = endpoint;
/**
* The time difference between Discord's Dates and the local computer's Dates. A positive number means the local
* computer's time is ahead of Discord's
* @type {number}
*/
this.timeDifference = 0;
/**
* Whether the queue is being processed or not
* @type {boolean}
*/
this.busy = false;
}
push(request) {
super.push(request);
this.handle();
}
/**
* Performs a request then resolves a promise to indicate its readiness for a new request.
* @param {APIRequest} item The item to execute
* @returns {Promise<?Object|Error>}
*/
execute(item) {
this.busy = true;
return new Promise(resolve => {
item.request.gen().end((err, res) => {
if (res && res.headers) {
this.requestLimit = Number(res.headers['x-ratelimit-limit']);
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {
this.queue.unshift(item);
this.restManager.client.setTimeout(() => {
this.globalLimit = false;
resolve();
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
this.restManager.client.setTimeout(resolve, 1e3 + this.restManager.client.options.restTimeOffset);
} else {
item.reject(err.status >= 400 && err.status < 500 ?
new DiscordAPIError(res.request.path, res.body, res.request.method) : err);
resolve(err);
}
} else {
this.globalLimit = false;
const data = res && res.body ? res.body : {};
item.resolve(data);
if (this.requestRemaining === 0) {
if (this.client.listenerCount(RATE_LIMIT)) {
/**
* Emitted when the client hits a rate limit while making a request
* @event Client#rateLimit
* @param {Object} rateLimitInfo Object containing the rate limit info
* @param {number} rateLimitInfo.requestLimit Number of requests that can be made to this endpoint
* @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers
* @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
* @param {string} rateLimitInfo.path Path used for request that triggered this event
*/
this.client.emit(RATE_LIMIT, {
limit: this.requestLimit,
timeDifference: this.timeDifference,
path: item.request.path,
method: item.request.method,
});
}
this.restManager.client.setTimeout(
() => resolve(data),
this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
);
} else {
resolve(data);
}
}
});
});
}
handle() {
super.handle();
if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return;
this.execute(this.queue.shift()).then(() => {
this.busy = false;
this.handle();
});
}
}
module.exports = SequentialRequestHandler;

@ -0,0 +1,25 @@
const Constants = require('../../util/Constants');
class UserAgentManager {
constructor() {
this.build(this.constructor.DEFAULT);
}
set({ url, version } = {}) {
this.build({
url: url || this.constructor.DFEAULT.url,
version: version || this.constructor.DEFAULT.version,
});
}
build(ua) {
this.userAgent = `DiscordBot (${ua.url}, ${ua.version}) Node.js/${process.version}`;
}
}
UserAgentManager.DEFAULT = {
url: Constants.Package.homepage.split('#')[0],
version: Constants.Package.version,
};
module.exports = UserAgentManager;

@ -0,0 +1,81 @@
const Collection = require('../../util/Collection');
const VoiceConnection = require('./VoiceConnection');
/**
* Manages all the voice stuff for the client.
* @private
*/
class ClientVoiceManager {
constructor(client) {
/**
* The client that instantiated this voice manager
* @type {Client}
*/
this.client = client;
/**
* A collection mapping connection IDs to the Connection objects
* @type {Collection<Snowflake, VoiceConnection>}
*/
this.connections = new Collection();
this.client.on('self.voiceServer', this.onVoiceServer.bind(this));
this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this));
}
onVoiceServer({ guild_id, token, endpoint }) {
const connection = this.connections.get(guild_id);
if (connection) connection.setTokenAndEndpoint(token, endpoint);
}
onVoiceStateUpdate({ guild_id, session_id, channel_id }) {
const connection = this.connections.get(guild_id);
if (connection) {
connection.channel = this.client.channels.get(channel_id);
connection.setSessionID(session_id);
}
}
/**
* Sets up a request to join a voice channel.
* @param {VoiceChannel} channel The voice channel to join
* @returns {Promise<VoiceConnection>}
*/
joinChannel(channel) {
return new Promise((resolve, reject) => {
if (!channel.joinable) {
if (channel.full) {
throw new Error('You do not have permission to join this voice channel; it is full.');
} else {
throw new Error('You do not have permission to join this voice channel.');
}
}
let connection = this.connections.get(channel.guild.id);
if (connection) {
if (connection.channel.id !== channel.id) {
this.connections.get(channel.guild.id).updateChannel(channel);
}
resolve(connection);
return;
} else {
connection = new VoiceConnection(this, channel);
this.connections.set(channel.guild.id, connection);
}
connection.once('failed', reason => {
this.connections.delete(channel.guild.id);
reject(reason);
});
connection.once('authenticated', () => {
connection.once('ready', () => resolve(connection));
connection.once('error', reject);
connection.once('disconnect', () => this.connections.delete(channel.guild.id));
});
});
}
}
module.exports = ClientVoiceManager;

@ -0,0 +1,366 @@
const VolumeInterface = require('./util/VolumeInterface');
const Prism = require('prism-media');
const OpusEncoders = require('./opus/OpusEngineList');
const Collection = require('../../util/Collection');
const ffmpegArguments = [
'-analyzeduration', '0',
'-loglevel', '0',
'-f', 's16le',
'-ar', '48000',
'-ac', '2',
];
/**
* A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency.
*
* Example usage:
* ```js
* const broadcast = client.createVoiceBroadcast();
* broadcast.playFile('./music.mp3');
* // Play "music.mp3" in all voice connections that the client is in
* for (const connection of client.voiceConnections.values()) {
* connection.playBroadcast(broadcast);
* }
* ```
* @implements {VolumeInterface}
*/
class VoiceBroadcast extends VolumeInterface {
constructor(client) {
super();
/**
* The client that created the broadcast
* @type {Client}
*/
this.client = client;
this._dispatchers = new Collection();
this._encoders = new Collection();
/**
* The audio transcoder that this broadcast uses
* @type {Prism}
*/
this.prism = new Prism();
/**
* The current audio transcoder that is being used
* @type {Object}
*/
this.currentTranscoder = null;
this.tickInterval = null;
this._volume = 1;
}
/**
* An array of subscribed dispatchers
* @type {StreamDispatcher[]}
* @readonly
*/
get dispatchers() {
let d = [];
for (const container of this._dispatchers.values()) {
d = d.concat(Array.from(container.values()));
}
return d;
}
get _playableStream() {
const currentTranscoder = this.currentTranscoder;
if (!currentTranscoder) return null;
const transcoder = currentTranscoder.transcoder;
const options = currentTranscoder.options;
return (transcoder && transcoder.output) || options.stream;
}
unregisterDispatcher(dispatcher, old) {
const volume = old || dispatcher.volume;
/**
* Emitted whenever a stream dispatcher unsubscribes from the broadcast.
* @event VoiceBroadcast#unsubscribe
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
*/
this.emit('unsubscribe', dispatcher);
for (const container of this._dispatchers.values()) {
container.delete(dispatcher);
if (!container.size) {
this._encoders.get(volume).destroy();
this._dispatchers.delete(volume);
this._encoders.delete(volume);
}
}
}
registerDispatcher(dispatcher) {
if (!this._dispatchers.has(dispatcher.volume)) {
this._dispatchers.set(dispatcher.volume, new Set());
this._encoders.set(dispatcher.volume, OpusEncoders.fetch());
}
const container = this._dispatchers.get(dispatcher.volume);
if (!container.has(dispatcher)) {
container.add(dispatcher);
dispatcher.once('end', () => this.unregisterDispatcher(dispatcher));
dispatcher.on('volumeChange', (o, n) => {
this.unregisterDispatcher(dispatcher, o);
if (!this._dispatchers.has(n)) {
this._dispatchers.set(n, new Set());
this._encoders.set(n, OpusEncoders.fetch());
}
this._dispatchers.get(n).add(dispatcher);
});
/**
* Emitted whenever a stream dispatcher subscribes to the broadcast.
* @event VoiceBroadcast#subscribe
* @param {StreamDispatcher} dispatcher The subscribed dispatcher
*/
this.emit('subscribe', dispatcher);
}
}
killCurrentTranscoder() {
if (this.currentTranscoder) {
if (this.currentTranscoder.transcoder) this.currentTranscoder.transcoder.kill();
this.currentTranscoder = null;
this.emit('end');
}
}
/**
* Plays any audio stream across the broadcast.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
* @example
* // Play streams using ytdl-core
* const ytdl = require('ytdl-core');
* const streamOptions = { seek: 0, volume: 1 };
* const broadcast = client.createVoiceBroadcast();
*
* voiceChannel.join()
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* broadcast.playStream(stream);
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
*/
playStream(stream, options = {}) {
this.setVolume(options.volume || 1);
return this._playTranscodable(stream, options);
}
/**
* Play the given file in the voice connection.
* @param {string} file The absolute path to the file
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play files natively
* const broadcast = client.createVoiceBroadcast();
*
* voiceChannel.join()
* .then(connection => {
* broadcast.playFile('C:/Users/Discord/Desktop/music.mp3');
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
*/
playFile(file, options = {}) {
this.setVolume(options.volume || 1);
return this._playTranscodable(`file:${file}`, options);
}
_playTranscodable(media, options) {
this.killCurrentTranscoder();
const transcoder = this.prism.transcode({
type: 'ffmpeg',
media,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
});
/**
* Emitted whenever an error occurs.
* @event VoiceBroadcast#error
* @param {Error} error The error that occurred
*/
transcoder.once('error', e => {
if (this.listenerCount('error') > 0) this.emit('error', e);
/**
* Emitted whenever the VoiceBroadcast has any warnings.
* @event VoiceBroadcast#warn
* @param {string|Error} warning The warning that was raised
*/
else this.emit('warn', e);
});
/**
* Emitted once the broadcast (the audio stream) ends.
* @event VoiceBroadcast#end
*/
transcoder.once('end', () => this.killCurrentTranscoder());
this.currentTranscoder = {
transcoder,
options,
};
transcoder.output.once('readable', () => this._startPlaying());
return this;
}
/**
* Plays a stream of 16-bit signed stereo PCM.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
*/
playConvertedStream(stream, options = {}) {
this.killCurrentTranscoder();
this.setVolume(options.volume || 1);
this.currentTranscoder = { options: { stream } };
stream.once('readable', () => this._startPlaying());
return this;
}
/**
* Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playOpusStream(stream) {
this.currentTranscoder = { options: { stream }, opus: true };
stream.once('readable', () => this._startPlaying());
return this;
}
/**
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
* @param {string} input The arbitrary input
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
*/
playArbitraryInput(input, options = {}) {
this.setVolume(options.volume || 1);
options.input = input;
return this._playTranscodable(input, options);
}
/**
* Pauses the entire broadcast - all dispatchers also pause.
*/
pause() {
this.paused = true;
for (const container of this._dispatchers.values()) {
for (const dispatcher of container.values()) {
dispatcher.pause();
}
}
}
/**
* Resumes the entire broadcast - all dispatchers also resume.
*/
resume() {
this.paused = false;
for (const container of this._dispatchers.values()) {
for (const dispatcher of container.values()) {
dispatcher.resume();
}
}
}
_startPlaying() {
if (this.tickInterval) clearInterval(this.tickInterval);
// Old code?
// this.tickInterval = this.client.setInterval(this.tick.bind(this), 20);
this._startTime = Date.now();
this._count = 0;
this._pausedTime = 0;
this._missed = 0;
this.tick();
}
tick() {
if (!this._playableStream) return;
if (this.paused) {
this._pausedTime += 20;
setTimeout(() => this.tick(), 20);
return;
}
const opus = this.currentTranscoder.opus;
const buffer = this.readStreamBuffer();
if (!buffer) {
this._missed++;
if (this._missed < 5) {
this._pausedTime += 200;
setTimeout(() => this.tick(), 200);
} else {
this.killCurrentTranscoder();
}
return;
}
this._missed = 0;
let packetMatrix = {};
const getOpusPacket = volume => {
if (packetMatrix[volume]) return packetMatrix[volume];
const opusEncoder = this._encoders.get(volume);
const opusPacket = opusEncoder.encode(this.applyVolume(buffer, this._volume * volume));
packetMatrix[volume] = opusPacket;
return opusPacket;
};
for (const dispatcher of this.dispatchers) {
if (opus) {
dispatcher.processPacket(buffer);
continue;
}
const volume = dispatcher.volume;
dispatcher.processPacket(getOpusPacket(volume));
}
const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now());
this._count++;
setTimeout(() => this.tick(), next);
}
readStreamBuffer() {
const opus = this.currentTranscoder.opus;
const bufferLength = (opus ? 80 : 1920) * 2;
let buffer = this._playableStream.read(bufferLength);
if (opus) return buffer;
if (!buffer) return null;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
return buffer;
}
/**
* Stop the current stream from playing without unsubscribing dispatchers.
*/
end() {
this.killCurrentTranscoder();
}
/**
* End the current broadcast, all subscribed dispatchers will also end.
*/
destroy() {
this.end();
for (const container of this._dispatchers.values()) {
for (const dispatcher of container.values()) {
dispatcher.destroy('end', 'broadcast ended');
}
}
}
}
module.exports = VoiceBroadcast;

@ -0,0 +1,530 @@
const VoiceWebSocket = require('./VoiceWebSocket');
const VoiceUDP = require('./VoiceUDPClient');
const Util = require('../../util/Util');
const Constants = require('../../util/Constants');
const AudioPlayer = require('./player/AudioPlayer');
const VoiceReceiver = require('./receiver/VoiceReceiver');
const EventEmitter = require('events').EventEmitter;
const Prism = require('prism-media');
/**
* Represents a connection to a guild's voice server.
* ```js
* // Obtained using:
* voiceChannel.join()
* .then(connection => {
*
* });
* ```
* @extends {EventEmitter}
*/
class VoiceConnection extends EventEmitter {
constructor(voiceManager, channel) {
super();
/**
* The voice manager that instantiated this connection
* @type {ClientVoiceManager}
*/
this.voiceManager = voiceManager;
/**
* The client that instantiated this connection
* @type {Client}
*/
this.client = voiceManager.client;
/**
* @external Prism
* @see {@link https://github.com/hydrabolt/prism-media}
*/
/**
* The audio transcoder for this connection
* @type {Prism}
*/
this.prism = new Prism();
/**
* The voice channel this connection is currently serving
* @type {VoiceChannel}
*/
this.channel = channel;
/**
* The current status of the voice connection
* @type {number}
*/
this.status = Constants.VoiceStatus.AUTHENTICATING;
/**
* Whether we're currently transmitting audio
* @type {boolean}
*/
this.speaking = false;
/**
* An array of Voice Receivers that have been created for this connection
* @type {VoiceReceiver[]}
*/
this.receivers = [];
/**
* The authentication data needed to connect to the voice server
* @type {Object}
* @private
*/
this.authentication = {};
/**
* The audio player for this voice connection
* @type {AudioPlayer}
*/
this.player = new AudioPlayer(this);
this.player.on('debug', m => {
/**
* Debug info from the connection.
* @event VoiceConnection#debug
* @param {string} message The debug message
*/
this.emit('debug', `audio player - ${m}`);
});
this.player.on('error', e => {
/**
* Warning info from the connection.
* @event VoiceConnection#warn
* @param {string|Error} warning The warning
*/
this.emit('warn', e);
});
/**
* Map SSRC to speaking values
* @type {Map<number, boolean>}
* @private
*/
this.ssrcMap = new Map();
/**
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
* @type {Object}
* @private
*/
this.sockets = {};
this.authenticate();
}
/**
* The current stream dispatcher (if any)
* @type {?StreamDispatcher}
* @readonly
*/
get dispatcher() {
return this.player.dispatcher;
}
/**
* Sets whether the voice connection should display as "speaking" or not.
* @param {boolean} value Whether or not to speak
* @private
*/
setSpeaking(value) {
if (this.speaking === value) return;
if (this.status !== Constants.VoiceStatus.CONNECTED) return;
this.speaking = value;
this.sockets.ws.sendPacket({
op: Constants.VoiceOPCodes.SPEAKING,
d: {
speaking: true,
delay: 0,
},
}).catch(e => {
this.emit('debug', e);
});
}
/**
* Sends a request to the main gateway to join a voice channel.
* @param {Object} [options] The options to provide
*/
sendVoiceStateUpdate(options = {}) {
options = Util.mergeDefault({
guild_id: this.channel.guild.id,
channel_id: this.channel.id,
self_mute: false,
self_deaf: false,
}, options);
this.client.ws.send({
op: Constants.OPCodes.VOICE_STATE_UPDATE,
d: options,
});
}
/**
* Set the token and endpoint required to connect to the voice servers.
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @returns {void}
*/
setTokenAndEndpoint(token, endpoint) {
if (!endpoint) {
// Signifies awaiting endpoint stage
return;
}
if (!token) {
this.authenticateFailed('Token not provided from voice server packet.');
return;
}
endpoint = endpoint.match(/([^:]*)/)[0];
if (!endpoint) {
this.authenticateFailed('Invalid endpoint received.');
return;
}
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
this.authentication.token = token;
this.authentication.endpoint = endpoint;
this.checkAuthenticated();
} else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
this.reconnect(token, endpoint);
}
}
/**
* Sets the Session ID for the connection.
* @param {string} sessionID The voice session ID
*/
setSessionID(sessionID) {
if (!sessionID) {
this.authenticateFailed('Session ID not supplied.');
return;
}
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
this.authentication.sessionID = sessionID;
this.checkAuthenticated();
} else if (sessionID !== this.authentication.sessionID) {
this.authentication.sessionID = sessionID;
/**
* Emitted when a new session ID is received.
* @event VoiceConnection#newSession
* @private
*/
this.emit('newSession', sessionID);
}
}
/**
* Checks whether the voice connection is authenticated.
* @private
*/
checkAuthenticated() {
const { token, endpoint, sessionID } = this.authentication;
if (token && endpoint && sessionID) {
clearTimeout(this.connectTimeout);
this.status = Constants.VoiceStatus.CONNECTING;
/**
* Emitted when we successfully initiate a voice connection.
* @event VoiceConnection#authenticated
*/
this.emit('authenticated');
this.connect();
}
}
/**
* Invoked when we fail to initiate a voice connection.
* @param {string} reason The reason for failure
* @private
*/
authenticateFailed(reason) {
clearTimeout(this.connectTimeout);
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
/**
* Emitted when we fail to initiate a voice connection.
* @event VoiceConnection#failed
* @param {Error} error The encountered error
*/
this.emit('failed', new Error(reason));
} else {
/**
* Emitted whenever the connection encounters an error.
* @event VoiceConnection#error
* @param {Error} error The encountered error
*/
this.emit('error', new Error(reason));
}
this.status = Constants.VoiceStatus.DISCONNECTED;
}
/**
* Move to a different voice channel in the same guild.
* @param {VoiceChannel} channel The channel to move to
* @private
*/
updateChannel(channel) {
this.channel = channel;
this.sendVoiceStateUpdate();
}
/**
* Attempts to authenticate to the voice server.
* @private
*/
authenticate() {
this.sendVoiceStateUpdate();
this.connectTimeout = this.client.setTimeout(
() => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000);
}
/**
* Attempts to reconnect to the voice server (typically after a region change).
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @private
*/
reconnect(token, endpoint) {
this.authentication.token = token;
this.authentication.endpoint = endpoint;
this.status = Constants.VoiceStatus.RECONNECTING;
/**
* Emitted when the voice connection is reconnecting (typically after a region change).
* @event VoiceConnection#reconnecting
*/
this.emit('reconnecting');
this.connect();
}
/**
* Disconnect the voice connection, causing a disconnect and closing event to be emitted.
*/
disconnect() {
this.emit('closing');
this.sendVoiceStateUpdate({
channel_id: null,
});
this.player.destroy();
this.cleanup();
this.status = Constants.VoiceStatus.DISCONNECTED;
/**
* Emitted when the voice connection disconnects.
* @event VoiceConnection#disconnect
*/
this.emit('disconnect');
}
/**
* Cleans up after disconnect.
* @private
*/
cleanup() {
const { ws, udp } = this.sockets;
if (ws) {
ws.removeAllListeners('error');
ws.removeAllListeners('ready');
ws.removeAllListeners('sessionDescription');
ws.removeAllListeners('speaking');
}
if (udp) udp.removeAllListeners('error');
this.sockets.ws = null;
this.sockets.udp = null;
}
/**
* Connect the voice connection.
* @private
*/
connect() {
if (this.status !== Constants.VoiceStatus.RECONNECTING) {
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
}
if (this.sockets.ws) this.sockets.ws.shutdown();
if (this.sockets.udp) this.sockets.udp.shutdown();
this.sockets.ws = new VoiceWebSocket(this);
this.sockets.udp = new VoiceUDP(this);
const { ws, udp } = this.sockets;
ws.on('error', err => this.emit('error', err));
udp.on('error', err => this.emit('error', err));
ws.on('ready', this.onReady.bind(this));
ws.on('sessionDescription', this.onSessionDescription.bind(this));
ws.on('speaking', this.onSpeaking.bind(this));
}
/**
* Invoked when the voice websocket is ready.
* @param {Object} data The received data
* @private
*/
onReady({ port, ssrc, ip }) {
this.authentication.port = port;
this.authentication.ssrc = ssrc;
this.sockets.udp.createUDPSocket(ip);
}
/**
* Invoked when a session description is received.
* @param {string} mode The encryption mode
* @param {string} secret The secret key
* @private
*/
onSessionDescription(mode, secret) {
this.authentication.encryptionMode = mode;
this.authentication.secretKey = secret;
this.status = Constants.VoiceStatus.CONNECTED;
/**
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
* the connection will already be ready.
* @event VoiceConnection#ready
*/
this.emit('ready');
}
/**
* Invoked when a speaking event is received.
* @param {Object} data The received data
* @private
*/
onSpeaking({ user_id, ssrc, speaking }) {
const guild = this.channel.guild;
const user = this.client.users.get(user_id);
this.ssrcMap.set(+ssrc, user);
if (!speaking) {
for (const receiver of this.receivers) {
receiver.stoppedSpeaking(user);
}
}
/**
* Emitted whenever a user starts/stops speaking.
* @event VoiceConnection#speaking
* @param {User} user The user that has started/stopped speaking
* @param {boolean} speaking Whether or not the user is speaking
*/
if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking);
guild._memberSpeakUpdate(user_id, speaking);
}
/**
* Options that can be passed to stream-playing methods:
* @typedef {Object} StreamOptions
* @property {number} [seek=0] The time to seek to
* @property {number} [volume=1] The volume to play at
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
* @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
* If set to 'auto', the voice channel's bitrate will be used
*/
/**
* Play the given file in the voice connection.
* @param {string} file The absolute path to the file
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play files natively
* voiceChannel.join()
* .then(connection => {
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
* })
* .catch(console.error);
*/
playFile(file, options) {
return this.player.playUnknownStream(`file:${file}`, options);
}
/**
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
* @param {string} input the arbitrary input
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playArbitraryInput(input, options) {
return this.player.playUnknownStream(input, options);
}
/**
* Plays and converts an audio stream in the voice connection.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play streams using ytdl-core
* const ytdl = require('ytdl-core');
* const streamOptions = { seek: 0, volume: 1 };
* voiceChannel.join()
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* const dispatcher = connection.playStream(stream, streamOptions);
* })
* .catch(console.error);
*/
playStream(stream, options) {
return this.player.playUnknownStream(stream, options);
}
/**
* Plays a stream of 16-bit signed stereo PCM.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playConvertedStream(stream, options) {
return this.player.playPCMStream(stream, options);
}
/**
* Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playOpusStream(stream, options) {
return this.player.playOpusStream(stream, options);
}
/**
* Plays a voice broadcast.
* @param {VoiceBroadcast} broadcast The broadcast to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play a broadcast
* const broadcast = client
* .createVoiceBroadcast()
* .playFile('./test.mp3');
* const dispatcher = voiceConnection.playBroadcast(broadcast);
*/
playBroadcast(broadcast, options) {
return this.player.playBroadcast(broadcast, options);
}
/**
* Creates a VoiceReceiver so you can start listening to voice data.
* It's recommended to only create one of these.
* @returns {VoiceReceiver}
*/
createReceiver() {
const receiver = new VoiceReceiver(this);
this.receivers.push(receiver);
return receiver;
}
}
module.exports = VoiceConnection;

@ -0,0 +1,127 @@
const udp = require('dgram');
const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter;
/**
* Represents a UDP client for a Voice Connection.
* @extends {EventEmitter}
* @private
*/
class VoiceConnectionUDPClient extends EventEmitter {
constructor(voiceConnection) {
super();
/**
* The voice connection that this UDP client serves
* @type {VoiceConnection}
*/
this.voiceConnection = voiceConnection;
/**
* The UDP socket
* @type {?Socket}
*/
this.socket = null;
/**
* The address of the Discord voice server
* @type {?string}
*/
this.discordAddress = null;
/**
* The local IP address
* @type {?string}
*/
this.localAddress = null;
/**
* The local port
* @type {?string}
*/
this.localPort = null;
this.voiceConnection.on('closing', this.shutdown.bind(this));
}
shutdown() {
if (this.socket) {
this.socket.removeAllListeners('message');
try {
this.socket.close();
} finally {
this.socket = null;
}
}
}
/**
* The port of the Discord voice server
* @type {number}
* @readonly
*/
get discordPort() {
return this.voiceConnection.authentication.port;
}
/**
* Send a packet to the UDP client.
* @param {Object} packet The packet to send
* @returns {Promise<Object>}
*/
send(packet) {
return new Promise((resolve, reject) => {
if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.');
if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.');
this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => {
if (error) reject(error); else resolve(packet);
});
});
}
createUDPSocket(address) {
this.discordAddress = address;
const socket = this.socket = udp.createSocket('udp4');
socket.once('message', message => {
const packet = parseLocalPacket(message);
if (packet.error) {
this.emit('error', packet.error);
return;
}
this.localAddress = packet.address;
this.localPort = packet.port;
this.voiceConnection.sockets.ws.sendPacket({
op: Constants.VoiceOPCodes.SELECT_PROTOCOL,
d: {
protocol: 'udp',
data: {
address: packet.address,
port: packet.port,
mode: 'xsalsa20_poly1305',
},
},
});
});
const blankMessage = Buffer.alloc(70);
blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4);
this.send(blankMessage);
}
}
function parseLocalPacket(message) {
try {
const packet = Buffer.from(message);
let address = '';
for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
return { address, port };
} catch (error) {
return { error };
}
}
module.exports = VoiceConnectionUDPClient;

@ -0,0 +1,246 @@
const Constants = require('../../util/Constants');
const SecretKey = require('./util/SecretKey');
const EventEmitter = require('events').EventEmitter;
let WebSocket;
try {
WebSocket = require('uws');
} catch (err) {
WebSocket = require('ws');
}
/**
* Represents a Voice Connection's WebSocket.
* @extends {EventEmitter}
* @private
*/
class VoiceWebSocket extends EventEmitter {
constructor(voiceConnection) {
super();
/**
* The client of this voice WebSocket
* @type {Client}
*/
this.client = voiceConnection.voiceManager.client;
/**
* The Voice Connection that this WebSocket serves
* @type {VoiceConnection}
*/
this.voiceConnection = voiceConnection;
/**
* How many connection attempts have been made
* @type {number}
*/
this.attempts = 0;
this.connect();
this.dead = false;
this.voiceConnection.on('closing', this.shutdown.bind(this));
}
shutdown() {
this.dead = true;
this.reset();
}
/**
* Resets the current WebSocket.
*/
reset() {
if (this.ws) {
if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close();
this.ws = null;
}
this.clearHeartbeat();
}
/**
* Starts connecting to the Voice WebSocket Server.
*/
connect() {
if (this.dead) return;
if (this.ws) this.reset();
if (this.attempts >= 5) {
this.emit('debug', new Error(`Too many connection attempts (${this.attempts}).`));
return;
}
this.attempts++;
/**
* The actual WebSocket used to connect to the Voice WebSocket Server.
* @type {WebSocket}
*/
this.ws = new WebSocket(`wss://${this.voiceConnection.authentication.endpoint}`);
this.ws.onopen = this.onOpen.bind(this);
this.ws.onmessage = this.onMessage.bind(this);
this.ws.onclose = this.onClose.bind(this);
this.ws.onerror = this.onError.bind(this);
}
/**
* Sends data to the WebSocket if it is open.
* @param {string} data The data to send to the WebSocket
* @returns {Promise<string>}
*/
send(data) {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error(`Voice websocket not open to send ${data}.`);
}
this.ws.send(data, null, error => {
if (error) reject(error); else resolve(data);
});
});
}
/**
* JSON.stringify's a packet and then sends it to the WebSocket Server.
* @param {Object} packet The packet to send
* @returns {Promise<string>}
*/
sendPacket(packet) {
try {
packet = JSON.stringify(packet);
} catch (error) {
return Promise.reject(error);
}
return this.send(packet);
}
/**
* Called whenever the WebSocket opens.
*/
onOpen() {
this.sendPacket({
op: Constants.OPCodes.DISPATCH,
d: {
server_id: this.voiceConnection.channel.guild.id,
user_id: this.client.user.id,
token: this.voiceConnection.authentication.token,
session_id: this.voiceConnection.authentication.sessionID,
},
}).catch(() => {
this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.'));
});
}
/**
* Called whenever a message is received from the WebSocket.
* @param {MessageEvent} event The message event that was received
* @returns {void}
*/
onMessage(event) {
try {
return this.onPacket(JSON.parse(event.data));
} catch (error) {
return this.onError(error);
}
}
/**
* Called whenever the connection to the WebSocket server is lost.
*/
onClose() {
if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000);
}
/**
* Called whenever an error occurs with the WebSocket.
* @param {Error} error The error that occurred
*/
onError(error) {
this.emit('error', error);
}
/**
* Called whenever a valid packet is received from the WebSocket.
* @param {Object} packet The received packet
*/
onPacket(packet) {
switch (packet.op) {
case Constants.VoiceOPCodes.READY:
this.setHeartbeat(packet.d.heartbeat_interval);
/**
* Emitted once the voice WebSocket receives the ready packet.
* @param {Object} packet The received packet
* @event VoiceWebSocket#ready
*/
this.emit('ready', packet.d);
break;
case Constants.VoiceOPCodes.SESSION_DESCRIPTION:
/**
* Emitted once the Voice Websocket receives a description of this voice session.
* @param {string} encryptionMode The type of encryption being used
* @param {SecretKey} secretKey The secret key used for encryption
* @event VoiceWebSocket#sessionDescription
*/
this.emit('sessionDescription', packet.d.mode, new SecretKey(packet.d.secret_key));
break;
case Constants.VoiceOPCodes.SPEAKING:
/**
* Emitted whenever a speaking packet is received.
* @param {Object} data
* @event VoiceWebSocket#speaking
*/
this.emit('speaking', packet.d);
break;
default:
/**
* Emitted when an unhandled packet is received.
* @param {Object} packet
* @event VoiceWebSocket#unknownPacket
*/
this.emit('unknownPacket', packet);
break;
}
}
/**
* Sets an interval at which to send a heartbeat packet to the WebSocket.
* @param {number} interval The interval at which to send a heartbeat packet
*/
setHeartbeat(interval) {
if (!interval || isNaN(interval)) {
this.onError(new Error('Tried to set voice heartbeat but no valid interval was specified.'));
return;
}
if (this.heartbeatInterval) {
/**
* Emitted whenver the voice WebSocket encounters a non-fatal error.
* @param {string} warn The warning
* @event VoiceWebSocket#warn
*/
this.emit('warn', 'A voice heartbeat interval is being overwritten');
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval);
}
/**
* Clears a heartbeat interval, if one exists.
*/
clearHeartbeat() {
if (!this.heartbeatInterval) {
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
return;
}
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
/**
* Sends a heartbeat packet.
*/
sendHeartbeat() {
this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }).catch(() => {
this.emit('warn', 'Tried to send heartbeat, but connection is not open');
this.clearHeartbeat();
});
}
}
module.exports = VoiceWebSocket;

@ -0,0 +1,331 @@
const VolumeInterface = require('../util/VolumeInterface');
const VoiceBroadcast = require('../VoiceBroadcast');
const Constants = require('../../../util/Constants');
const secretbox = require('../util/Secretbox');
const nonce = Buffer.alloc(24);
nonce.fill(0);
/**
* The class that sends voice packet data to the voice connection.
* ```js
* // Obtained using:
* voiceChannel.join().then(connection => {
* // You can play a file or a stream here:
* const dispatcher = connection.playFile('./file.mp3');
* });
* ```
* @implements {VolumeInterface}
*/
class StreamDispatcher extends VolumeInterface {
constructor(player, stream, streamOptions) {
super(streamOptions);
/**
* The Audio Player that controls this dispatcher
* @type {AudioPlayer}
*/
this.player = player;
/**
* The stream that the dispatcher plays
* @type {ReadableStream|VoiceBroadcast}
*/
this.stream = stream;
if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming();
this.streamOptions = streamOptions;
const data = this.streamingData;
data.length = 20;
data.missed = 0;
/**
* Whether playing is paused
* @type {boolean}
*/
this.paused = false;
/**
* Whether this dispatcher has been destroyed
* @type {boolean}
*/
this.destroyed = false;
this._opus = streamOptions.opus;
}
/**
* How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5
* aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime
* @type {number}
* @readonly
*/
get passes() {
return this.streamOptions.passes || 1;
}
set passes(n) {
this.streamOptions.passes = n;
}
get streamingData() {
return this.player.streamingData;
}
/**
* How long the stream dispatcher has been "speaking" for
* @type {number}
* @readonly
*/
get time() {
return this.streamingData.count * (this.streamingData.length || 0);
}
/**
* The total time, taking into account pauses and skips, that the dispatcher has been streaming for
* @type {number}
* @readonly
*/
get totalStreamTime() {
return this.time + this.streamingData.pausedTime;
}
/**
* Stops sending voice packets to the voice connection (stream may still progress however).
*/
pause() { this.setPaused(true); }
/**
* Resumes sending voice packets to the voice connection (may be further on in the stream than when paused).
*/
resume() { this.setPaused(false); }
/**
* Stops the current stream permanently and emits an `end` event.
* @param {string} [reason='user'] An optional reason for stopping the dispatcher
*/
end(reason = 'user') {
this.destroy('end', reason);
}
setSpeaking(value) {
if (this.speaking === value) return;
if (this.player.voiceConnection.status !== Constants.VoiceStatus.CONNECTED) return;
this.speaking = value;
/**
* Emitted when the dispatcher starts/stops speaking.
* @event StreamDispatcher#speaking
* @param {boolean} value Whether or not the dispatcher is speaking
*/
this.emit('speaking', value);
}
/**
* Set the bitrate of the current Opus encoder.
* @param {number} bitrate New bitrate, in kbps
* If set to 'auto', the voice channel's bitrate will be used
*/
setBitrate(bitrate) {
this.player.setBitrate(bitrate);
}
sendBuffer(buffer, sequence, timestamp, opusPacket) {
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
const packet = this.createPacket(sequence, timestamp, opusPacket);
this.sendPacket(packet);
}
sendPacket(packet) {
let repeats = this.passes;
/**
* Emitted whenever the dispatcher has debug information.
* @event StreamDispatcher#debug
* @param {string} info The debug info
*/
this.setSpeaking(true);
while (repeats-- && this.player.voiceConnection.sockets.udp) {
this.player.voiceConnection.sockets.udp.send(packet)
.catch(e => {
this.setSpeaking(false);
this.emit('debug', `Failed to send a packet ${e}`);
});
}
}
createPacket(sequence, timestamp, buffer) {
const packetBuffer = Buffer.alloc(buffer.length + 28);
packetBuffer.fill(0);
packetBuffer[0] = 0x80;
packetBuffer[1] = 0x78;
packetBuffer.writeUIntBE(sequence, 2, 2);
packetBuffer.writeUIntBE(timestamp, 4, 4);
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
packetBuffer.copy(nonce, 0, 0, 12);
buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
return packetBuffer;
}
processPacket(packet) {
try {
if (this.destroyed || !this.player.voiceConnection.authentication.secretKey) {
this.setSpeaking(false);
return;
}
const data = this.streamingData;
if (this.paused) {
this.setSpeaking(false);
data.pausedTime = data.length * 10;
return;
}
if (!packet) {
data.missed++;
data.pausedTime += data.length * 10;
return;
}
this.started();
this.missed = 0;
this.stepStreamingData();
this.sendBuffer(null, data.sequence, data.timestamp, packet);
} catch (e) {
this.destroy('error', e);
}
}
process() {
try {
if (this.destroyed) {
this.setSpeaking(false);
return;
}
const data = this.streamingData;
if (data.missed >= 5) {
this.destroy('end', 'Stream is not generating quickly enough.');
return;
}
if (this.paused) {
this.setSpeaking(false);
// Old code?
// data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
data.pausedTime += data.length * 10;
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return;
}
this.started();
const buffer = this.readStreamBuffer();
if (!buffer) {
data.missed++;
data.pausedTime += data.length * 10;
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return;
}
data.missed = 0;
this.stepStreamingData();
if (this._opus) {
this.sendBuffer(null, data.sequence, data.timestamp, buffer);
} else {
this.sendBuffer(buffer, data.sequence, data.timestamp);
}
const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now());
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime);
} catch (e) {
this.destroy('error', e);
}
}
readStreamBuffer() {
const data = this.streamingData;
const bufferLength = (this._opus ? 80 : 1920) * data.channels;
let buffer = this.stream.read(bufferLength);
if (this._opus) return buffer;
if (!buffer) return null;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
buffer = this.applyVolume(buffer);
return buffer;
}
started() {
const data = this.streamingData;
if (!data.startTime) {
/**
* Emitted once the dispatcher starts streaming.
* @event StreamDispatcher#start
*/
this.emit('start');
data.startTime = Date.now();
}
}
stepStreamingData() {
const data = this.streamingData;
data.count++;
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
data.timestamp = (data.timestamp + 960) < 4294967295 ? data.timestamp + 960 : 0;
}
destroy(type, reason) {
if (this.destroyed) return;
this.destroyed = true;
this.setSpeaking(false);
this.emit(type, reason);
/**
* Emitted once the dispatcher ends.
* @param {string} [reason] The reason the dispatcher ended
* @event StreamDispatcher#end
*/
if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`);
}
startStreaming() {
if (!this.stream) {
/**
* Emitted if the dispatcher encounters an error.
* @event StreamDispatcher#error
* @param {string} error The error message
*/
this.emit('error', 'No stream');
return;
}
this.stream.on('end', err => this.destroy('end', err || 'stream'));
this.stream.on('error', err => this.destroy('error', err));
const data = this.streamingData;
data.length = 20;
data.missed = 0;
this.stream.once('readable', () => {
data.startTime = null;
data.count = 0;
this.process();
});
}
setPaused(paused) { this.setSpeaking(!(this.paused = paused)); }
}
module.exports = StreamDispatcher;

@ -0,0 +1,60 @@
/**
* The base opus encoding engine.
* @private
*/
class BaseOpus {
/**
* @param {Object} [options] The options to apply to the Opus engine
* @param {number} [options.bitrate=48] The desired bitrate (kbps)
* @param {boolean} [options.fec=false] Whether to enable forward error correction
* @param {number} [options.plp=0] The expected packet loss percentage
*/
constructor({ bitrate = 48, fec = false, plp = 0 } = {}) {
this.ctl = {
BITRATE: 4002,
FEC: 4012,
PLP: 4014,
};
this.samplingRate = 48000;
this.channels = 2;
/**
* The desired bitrate (kbps)
* @type {number}
*/
this.bitrate = bitrate;
/**
* Miscellaneous Opus options
* @type {Object}
*/
this.options = { fec, plp };
}
init() {
try {
this.setBitrate(this.bitrate);
// Set FEC (forward error correction)
if (this.options.fec) this.setFEC(this.options.fec);
// Set PLP (expected packet loss percentage)
if (this.options.plp) this.setPLP(this.options.plp);
} catch (err) {
// Opus engine likely has no support for libopus CTL
}
}
encode(buffer) {
return buffer;
}
decode(buffer) {
return buffer;
}
destroy() {} // eslint-disable-line no-empty-function
}
module.exports = BaseOpus;

@ -0,0 +1,40 @@
const OpusEngine = require('./BaseOpusEngine');
let opus;
class NodeOpusEngine extends OpusEngine {
constructor(player) {
super(player);
try {
opus = require('node-opus');
} catch (err) {
throw err;
}
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}
setPLP(percent) {
this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
}
encode(buffer) {
super.encode(buffer);
return this.encoder.encode(buffer, 1920);
}
decode(buffer) {
super.decode(buffer);
return this.encoder.decode(buffer, 1920);
}
}
module.exports = NodeOpusEngine;

@ -0,0 +1,28 @@
const list = [
require('./NodeOpusEngine'),
require('./OpusScriptEngine'),
];
function fetch(Encoder, engineOptions) {
try {
return new Encoder(engineOptions);
} catch (err) {
if (err.message.includes('Cannot find module')) return null;
// The Opus engine exists, but another error occurred.
throw err;
}
}
exports.add = encoder => {
list.push(encoder);
};
exports.fetch = engineOptions => {
for (const encoder of list) {
const fetched = fetch(encoder, engineOptions);
if (fetched) return fetched;
}
throw new Error('Couldn\'t find an Opus engine.');
};

@ -0,0 +1,45 @@
const OpusEngine = require('./BaseOpusEngine');
let OpusScript;
class OpusScriptEngine extends OpusEngine {
constructor(player) {
super(player);
try {
OpusScript = require('opusscript');
} catch (err) {
throw err;
}
this.encoder = new OpusScript(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}
setPLP(percent) {
this.encoder.encoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
}
encode(buffer) {
super.encode(buffer);
return this.encoder.encode(buffer, 960);
}
decode(buffer) {
super.decode(buffer);
return this.encoder.decode(buffer);
}
destroy() {
super.destroy();
this.encoder.delete();
}
}
module.exports = OpusScriptEngine;

@ -0,0 +1,170 @@
const EventEmitter = require('events').EventEmitter;
const Prism = require('prism-media');
const StreamDispatcher = require('../dispatcher/StreamDispatcher');
const Collection = require('../../../util/Collection');
const OpusEncoders = require('../opus/OpusEngineList');
const ffmpegArguments = [
'-analyzeduration', '0',
'-loglevel', '0',
'-f', 's16le',
'-ar', '48000',
'-ac', '2',
];
/**
* An Audio Player for a Voice Connection.
* @private
* @extends {EventEmitter}
*/
class AudioPlayer extends EventEmitter {
constructor(voiceConnection) {
super();
/**
* The voice connection that the player serves
* @type {VoiceConnection}
*/
this.voiceConnection = voiceConnection;
/**
* The prism transcoder that the player uses
* @type {Prism}
*/
this.prism = new Prism();
this.streams = new Collection();
this.currentStream = {};
this.streamingData = {
channels: 2,
count: 0,
sequence: 0,
timestamp: 0,
pausedTime: 0,
};
this.voiceConnection.once('closing', () => this.destroyCurrentStream());
}
/**
* The current transcoder
* @type {?Object}
* @readonly
*/
get transcoder() {
return this.currentStream.transcoder;
}
/**
* The current dispatcher
* @type {?StreamDispatcher}
* @readonly
*/
get dispatcher() {
return this.currentStream.dispatcher;
}
destroy() {
if (this.opusEncoder) this.opusEncoder.destroy();
this.opusEncoder = null;
}
destroyCurrentStream() {
const transcoder = this.transcoder;
const dispatcher = this.dispatcher;
if (transcoder) transcoder.kill();
if (dispatcher) {
const end = dispatcher.listeners('end')[0];
const error = dispatcher.listeners('error')[0];
if (end) dispatcher.removeListener('end', end);
if (error) dispatcher.removeListener('error', error);
dispatcher.destroy('end');
}
this.currentStream = {};
}
/**
* Set the bitrate of the current Opus encoder.
* @param {number} value New bitrate, in kbps
* If set to 'auto', the voice channel's bitrate will be used
*/
setBitrate(value) {
if (!value) return;
if (!this.opusEncoder) return;
const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value;
this.opusEncoder.setBitrate(bitrate);
}
playUnknownStream(stream, options = {}) {
this.destroy();
this.opusEncoder = OpusEncoders.fetch(options);
const transcoder = this.prism.transcode({
type: 'ffmpeg',
media: stream,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
});
this.destroyCurrentStream();
this.currentStream = {
transcoder: transcoder,
output: transcoder.output,
input: stream,
};
transcoder.on('error', e => {
this.destroyCurrentStream();
if (this.listenerCount('error') > 0) this.emit('error', e);
this.emit('warn', `prism transcoder error - ${e}`);
});
return this.playPCMStream(transcoder.output, options, true);
}
playPCMStream(stream, options = {}, fromUnknown = false) {
this.destroy();
this.opusEncoder = OpusEncoders.fetch(options);
this.setBitrate(options.bitrate);
const dispatcher = this.createDispatcher(stream, options);
if (fromUnknown) {
this.currentStream.dispatcher = dispatcher;
} else {
this.destroyCurrentStream();
this.currentStream = {
dispatcher,
input: stream,
output: stream,
};
}
return dispatcher;
}
playOpusStream(stream, options = {}) {
options.opus = true;
this.destroyCurrentStream();
const dispatcher = this.createDispatcher(stream, options);
this.currentStream = {
dispatcher,
input: stream,
output: stream,
};
return dispatcher;
}
playBroadcast(broadcast, options) {
this.destroyCurrentStream();
const dispatcher = this.createDispatcher(broadcast, options);
this.currentStream = {
dispatcher,
broadcast,
input: broadcast,
output: broadcast,
};
broadcast.registerDispatcher(dispatcher);
return dispatcher;
}
createDispatcher(stream, { seek = 0, volume = 1, passes = 1, opus } = {}) {
const options = { seek, volume, passes, opus };
const dispatcher = new StreamDispatcher(this, stream, options);
dispatcher.on('end', () => this.destroyCurrentStream());
dispatcher.on('error', () => this.destroyCurrentStream());
dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value));
return dispatcher;
}
}
module.exports = AudioPlayer;

@ -0,0 +1,17 @@
const Readable = require('stream').Readable;
class VoiceReadable extends Readable {
constructor() {
super();
this._packets = [];
this.open = true;
}
_read() {} // eslint-disable-line no-empty-function
_push(d) {
if (this.open) this.push(d);
}
}
module.exports = VoiceReadable;

@ -0,0 +1,219 @@
const EventEmitter = require('events').EventEmitter;
const secretbox = require('../util/Secretbox');
const Readable = require('./VoiceReadable');
const OpusEncoders = require('../opus/OpusEngineList');
const nonce = Buffer.alloc(24);
nonce.fill(0);
/**
* Receives voice data from a voice connection.
* ```js
* // Obtained using:
* voiceChannel.join()
* .then(connection => {
* const receiver = connection.createReceiver();
* });
* ```
* @extends {EventEmitter}
*/
class VoiceReceiver extends EventEmitter {
constructor(connection) {
super();
/*
Need a queue because we don't get the ssrc of the user speaking until after the first few packets,
so we queue up unknown SSRCs until they become known, then empty the queue
*/
this.queues = new Map();
this.pcmStreams = new Map();
this.opusStreams = new Map();
this.opusEncoders = new Map();
/**
* Whether or not this receiver has been destroyed
* @type {boolean}
*/
this.destroyed = false;
/**
* The VoiceConnection that instantiated this
* @type {VoiceConnection}
*/
this.voiceConnection = connection;
this._listener = msg => {
const ssrc = +msg.readUInt32BE(8).toString(10);
const user = this.voiceConnection.ssrcMap.get(ssrc);
if (!user) {
if (!this.queues.has(ssrc)) this.queues.set(ssrc, []);
this.queues.get(ssrc).push(msg);
} else {
if (this.queues.get(ssrc)) {
this.queues.get(ssrc).push(msg);
this.queues.get(ssrc).map(m => this.handlePacket(m, user));
this.queues.delete(ssrc);
return;
}
this.handlePacket(msg, user);
}
};
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
}
/**
* If this VoiceReceiver has been destroyed, running `recreate()` will recreate the listener.
* This avoids you having to create a new receiver.
* <info>Any streams that you had prior to destroying the receiver will not be recreated.</info>
*/
recreate() {
if (!this.destroyed) return;
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
this.destroyed = false;
}
/**
* Destroy this VoiceReceiver, also ending any streams that it may be controlling.
*/
destroy() {
this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener);
for (const [id, stream] of this.pcmStreams) {
stream._push(null);
this.pcmStreams.delete(id);
}
for (const [id, stream] of this.opusStreams) {
stream._push(null);
this.opusStreams.delete(id);
}
for (const [id, encoder] of this.opusEncoders) {
encoder.destroy();
this.opusEncoders.delete(id);
}
this.destroyed = true;
}
/**
* Invoked when a user stops speaking.
* @param {User} user The user that stopped speaking
* @private
*/
stoppedSpeaking(user) {
const opusStream = this.opusStreams.get(user.id);
const pcmStream = this.pcmStreams.get(user.id);
const opusEncoder = this.opusEncoders.get(user.id);
if (opusStream) {
opusStream.push(null);
opusStream.open = false;
this.opusStreams.delete(user.id);
}
if (pcmStream) {
pcmStream.push(null);
pcmStream.open = false;
this.pcmStreams.delete(user.id);
}
if (opusEncoder) {
opusEncoder.destroy();
}
}
/**
* Creates a readable stream for a user that provides opus data while the user is speaking. When the user
* stops speaking, the stream is destroyed.
* @param {UserResolvable} user The user to create the stream for
* @returns {ReadableStream}
*/
createOpusStream(user) {
user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user);
if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.');
if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
const stream = new Readable();
this.opusStreams.set(user.id, stream);
return stream;
}
/**
* Creates a readable stream for a user that provides PCM data while the user is speaking. When the user
* stops speaking, the stream is destroyed. The stream is 32-bit signed stereo PCM at 48KHz.
* @param {UserResolvable} user The user to create the stream for
* @returns {ReadableStream}
*/
createPCMStream(user) {
user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user);
if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.');
if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
const stream = new Readable();
this.pcmStreams.set(user.id, stream);
return stream;
}
handlePacket(msg, user) {
msg.copy(nonce, 0, 0, 12);
let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key);
if (!data) {
/**
* Emitted whenever a voice packet experiences a problem.
* @event VoiceReceiver#warn
* @param {string} reason The reason for the warning. If it happened because the voice packet could not be
* decrypted, this would be `decrypt`. If it happened because the voice packet could not be decoded into
* PCM, this would be `decode`
* @param {string} message The warning message
*/
this.emit('warn', 'decrypt', 'Failed to decrypt voice packet');
return;
}
data = Buffer.from(data);
// Strip RTP Header Extensions (one-byte only)
if (data[0] === 0xBE && data[1] === 0xDE && data.length > 4) {
const headerExtensionLength = data.readUInt16BE(2);
let offset = 4;
for (let i = 0; i < headerExtensionLength; i++) {
const byte = data[offset];
offset++;
if (byte === 0) {
continue;
}
offset += 1 + (0b1111 & (byte >> 4));
}
while (data[offset] === 0) {
offset++;
}
data = data.slice(offset);
}
if (this.opusStreams.get(user.id)) this.opusStreams.get(user.id)._push(data);
/**
* Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM).
* @event VoiceReceiver#opus
* @param {User} user The user that is sending the buffer (is speaking)
* @param {Buffer} buffer The opus buffer
*/
this.emit('opus', user, data);
if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) {
if (!this.opusEncoders.get(user.id)) this.opusEncoders.set(user.id, OpusEncoders.fetch());
const { pcm, error } = VoiceReceiver._tryDecode(this.opusEncoders.get(user.id), data);
if (error) {
this.emit('warn', 'decode', `Failed to decode packet voice to PCM because: ${error.message}`);
return;
}
if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm);
/**
* Emits decoded voice data when it's received. For performance reasons, the decoding will only
* happen if there is at least one `pcm` listener on this receiver.
* @event VoiceReceiver#pcm
* @param {User} user The user that is sending the buffer (is speaking)
* @param {Buffer} buffer The decoded buffer
*/
this.emit('pcm', user, pcm);
}
}
static _tryDecode(encoder, data) {
try {
return { pcm: encoder.decode(data) };
} catch (error) {
return { error };
}
}
}
module.exports = VoiceReceiver;

@ -0,0 +1,16 @@
/**
* Represents a Secret Key used in encryption over voice.
* @private
*/
class SecretKey {
constructor(key) {
/**
* The key used for encryption
* @type {Uint8Array}
*/
this.key = new Uint8Array(new ArrayBuffer(key.length));
for (const index in key) this.key[index] = key[index];
}
}
module.exports = SecretKey;

@ -0,0 +1,33 @@
const libs = {
sodium: sodium => ({
open: sodium.api.crypto_secretbox_open_easy,
close: sodium.api.crypto_secretbox_easy,
}),
'libsodium-wrappers': sodium => ({
open: sodium.crypto_secretbox_open_easy,
close: sodium.crypto_secretbox_easy,
}),
tweetnacl: tweetnacl => ({
open: tweetnacl.secretbox.open,
close: tweetnacl.secretbox,
}),
};
exports.methods = {};
for (const libName of Object.keys(libs)) {
try {
const lib = require(libName);
if (libName === 'libsodium-wrappers' && lib.ready) {
lib.ready.then(() => {
exports.methods = libs[libName](lib);
}).catch(() => {
const tweetnacl = require('tweetnacl');
exports.methods = libs.tweetnacl(tweetnacl);
}).catch(() => undefined);
} else {
exports.methods = libs[libName](lib);
}
break;
} catch (err) {} // eslint-disable-line no-empty
}

@ -0,0 +1,86 @@
const EventEmitter = require('events');
/**
* An interface class for volume transformation.
* @extends {EventEmitter}
*/
class VolumeInterface extends EventEmitter {
constructor({ volume = 0 } = {}) {
super();
this.setVolume(volume || 1);
}
/**
* The current volume of the broadcast
* @readonly
* @type {number}
*/
get volume() {
return this._volume;
}
/**
* The current volume of the broadcast in decibels
* @readonly
* @type {number}
*/
get volumeDecibels() {
return Math.log10(this._volume) * 20;
}
/**
* The current volume of the broadcast from a logarithmic scale
* @readonly
* @type {number}
*/
get volumeLogarithmic() {
return Math.pow(this._volume, 1 / 1.660964);
}
applyVolume(buffer, volume) {
volume = volume || this._volume;
if (volume === 1) return buffer;
const out = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))));
out.writeInt16LE(uint, i);
}
return out;
}
/**
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
* @param {number} volume The volume that you want to set
*/
setVolume(volume) {
/**
* Emitted when the volume of this interface changes.
* @event VolumeInterface#volumeChange
* @param {number} oldVolume The old volume of this interface
* @param {number} newVolume The new volume of this interface
*/
this.emit('volumeChange', this._volume, volume);
this._volume = volume;
}
/**
* Set the volume in decibels.
* @param {number} db The decibels
*/
setVolumeDecibels(db) {
this.setVolume(Math.pow(10, db / 20));
}
/**
* Set the volume so that a perceived value of 0.5 is half the perceived volume etc.
* @param {number} value The value for the volume
*/
setVolumeLogarithmic(value) {
this.setVolume(Math.pow(value, 1.660964));
}
}
module.exports = VolumeInterface;

@ -0,0 +1,506 @@
const browser = typeof window !== 'undefined';
const EventEmitter = require('events');
const Constants = require('../../util/Constants');
const zlib = require('zlib');
const PacketManager = require('./packets/WebSocketPacketManager');
const erlpack = (function findErlpack() {
try {
const e = require('erlpack');
if (!e.pack) return null;
return e;
} catch (e) {
return null;
}
}());
const WebSocket = (function findWebSocket() {
if (browser) return window.WebSocket; // eslint-disable-line no-undef
try {
return require('uws');
} catch (e) {
return require('ws');
}
}());
/**
* Abstracts a WebSocket connection with decoding/encoding for the Discord gateway.
* @private
*/
class WebSocketConnection extends EventEmitter {
/**
* @param {WebSocketManager} manager The WebSocket manager
* @param {string} gateway The WebSocket gateway to connect to
*/
constructor(manager, gateway) {
super();
/**
* The WebSocket Manager of this connection
* @type {WebSocketManager}
*/
this.manager = manager;
/**
* The client this belongs to
* @type {Client}
*/
this.client = manager.client;
/**
* The WebSocket connection itself
* @type {WebSocket}
*/
this.ws = null;
/**
* The current sequence of the WebSocket
* @type {number}
*/
this.sequence = -1;
/**
* The current status of the client
* @type {number}
*/
this.status = Constants.Status.IDLE;
/**
* The Packet Manager of the connection
* @type {WebSocketPacketManager}
*/
this.packetManager = new PacketManager(this);
/**
* The last time a ping was sent (a timestamp)
* @type {number}
*/
this.lastPingTimestamp = 0;
/**
* Contains the rate limit queue and metadata
* @type {Object}
*/
this.ratelimit = {
queue: [],
remaining: 120,
total: 120,
time: 60e3,
resetTimer: null,
};
this.connect(gateway);
/**
* Events that are disabled (will not be processed)
* @type {Object}
*/
this.disabledEvents = {};
/**
* The sequence on WebSocket close
* @type {number}
*/
this.closeSequence = 0;
/**
* Whether or not the WebSocket is expecting to be closed
* @type {boolean}
*/
this.expectingClose = false;
for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true;
}
/**
* Causes the client to be marked as ready and emits the ready event.
* @returns {void}
*/
triggerReady() {
if (this.status === Constants.Status.READY) {
this.debug('Tried to mark self as ready, but already ready');
return;
}
/**
* Emitted when the client becomes ready to start working.
* @event Client#ready
*/
this.status = Constants.Status.READY;
this.client.emit(Constants.Events.READY);
this.packetManager.handleQueue();
}
/**
* Checks whether the client is ready to be marked as ready.
* @returns {void}
*/
checkIfReady() {
if (this.status === Constants.Status.READY || this.status === Constants.Status.NEARLY) return false;
let unavailableGuilds = 0;
for (const guild of this.client.guilds.values()) {
if (!guild.available) unavailableGuilds++;
}
if (unavailableGuilds === 0) {
this.status = Constants.Status.NEARLY;
if (!this.client.options.fetchAllMembers) return this.triggerReady();
// Fetch all members before marking self as ready
const promises = this.client.guilds.map(g => g.fetchMembers());
Promise.all(promises)
.then(() => this.triggerReady())
.catch(e => {
this.debug(`Failed to fetch all members before ready! ${e}`);
this.triggerReady();
});
}
return true;
}
// Util
/**
* Emits a debug message.
* @param {string} message Debug message
* @returns {void}
*/
debug(message) {
if (message instanceof Error) message = message.stack;
return this.manager.debug(`[connection] ${message}`);
}
/**
* Attempts to serialise data from the WebSocket.
* @param {string|Object} data Data to unpack
* @returns {Object}
*/
unpack(data) {
if (data instanceof ArrayBuffer) data = Buffer.from(new Uint8Array(data));
if (erlpack && typeof data !== 'string') return erlpack.unpack(data);
else if (data instanceof Buffer) data = zlib.inflateSync(data).toString();
return JSON.parse(data);
}
/**
* Packs an object ready to be sent.
* @param {Object} data Data to pack
* @returns {string|Buffer}
*/
pack(data) {
return erlpack ? erlpack.pack(data) : JSON.stringify(data);
}
/**
* Processes the current WebSocket queue.
*/
processQueue() {
if (this.ratelimit.remaining === 0) return;
if (this.ratelimit.queue.length === 0) return;
if (this.ratelimit.remaining === this.ratelimit.total) {
this.ratelimit.resetTimer = this.client.setTimeout(() => {
this.ratelimit.remaining = this.ratelimit.total;
this.processQueue();
}, this.ratelimit.time);
}
while (this.ratelimit.remaining > 0) {
const item = this.ratelimit.queue.shift();
if (!item) return;
this._send(item);
this.ratelimit.remaining--;
}
}
/**
* Sends data, bypassing the queue.
* @param {Object} data Packet to send
* @returns {void}
*/
_send(data) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.debug(`Tried to send packet ${data} but no WebSocket is available!`);
return;
}
this.ws.send(this.pack(data));
}
/**
* Adds data to the queue to be sent.
* @param {Object} data Packet to send
* @returns {void}
*/
send(data) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.debug(`Tried to send packet ${data} but no WebSocket is available!`);
return;
}
this.ratelimit.queue.push(data);
this.processQueue();
}
/**
* Creates a connection to a gateway.
* @param {string} gateway The gateway to connect to
* @param {number} [after=0] How long to wait before connecting
* @param {boolean} [force=false] Whether or not to force a new connection even if one already exists
* @returns {boolean}
*/
connect(gateway = this.gateway, after = 0, force = false) {
if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line
if (this.ws && !force) {
this.debug('WebSocket connection already exists');
return false;
} else if (typeof gateway !== 'string') {
this.debug(`Tried to connect to an invalid gateway: ${gateway}`);
return false;
}
this.expectingClose = false;
this.gateway = gateway;
this.debug(`Connecting to ${gateway}`);
const ws = this.ws = new WebSocket(gateway);
if (browser) ws.binaryType = 'arraybuffer';
ws.onmessage = this.onMessage.bind(this);
ws.onopen = this.onOpen.bind(this);
ws.onerror = this.onError.bind(this);
ws.onclose = this.onClose.bind(this);
this.status = Constants.Status.CONNECTING;
return true;
}
/**
* Destroys the connection.
* @returns {boolean}
*/
destroy() {
const ws = this.ws;
if (!ws) {
this.debug('Attempted to destroy WebSocket but no connection exists!');
return false;
}
this.heartbeat(-1);
this.expectingClose = true;
ws.close(1000);
this.packetManager.handleQueue();
this.ws = null;
this.status = Constants.Status.DISCONNECTED;
this.ratelimit.remaining = this.ratelimit.total;
return true;
}
/**
* Called whenever a message is received.
* @param {Event} event Event received
* @returns {boolean}
*/
onMessage(event) {
let data;
try {
data = this.unpack(event.data);
} catch (err) {
this.emit('debug', err);
}
return this.onPacket(data);
}
/**
* Sets the current sequence of the connection.
* @param {number} s New sequence
*/
setSequence(s) {
this.sequence = s > this.sequence ? s : this.sequence;
}
/**
* Called whenever a packet is received.
* @param {Object} packet Received packet
* @returns {boolean}
*/
onPacket(packet) {
if (!packet) {
this.debug('Received null packet');
return false;
}
this.client.emit('raw', packet);
switch (packet.op) {
case Constants.OPCodes.HELLO:
return this.heartbeat(packet.d.heartbeat_interval);
case Constants.OPCodes.RECONNECT:
return this.reconnect();
case Constants.OPCodes.INVALID_SESSION:
if (!packet.d) this.sessionID = null;
this.sequence = -1;
this.debug('Session invalidated -- will identify with a new session');
return this.identify(packet.d ? 2500 : 0);
case Constants.OPCodes.HEARTBEAT_ACK:
return this.ackHeartbeat();
case Constants.OPCodes.HEARTBEAT:
return this.heartbeat();
default:
return this.packetManager.handle(packet);
}
}
/**
* Called whenever a connection is opened to the gateway.
* @param {Event} event Received open event
*/
onOpen(event) {
if (event && event.target && event.target.url) this.gateway = event.target.url;
this.debug(`Connected to gateway ${this.gateway}`);
this.identify();
}
/**
* Causes a reconnection to the gateway.
*/
reconnect() {
this.debug('Attemping to reconnect in 5500ms...');
/**
* Emitted whenever the client tries to reconnect to the WebSocket.
* @event Client#reconnecting
*/
this.client.emit(Constants.Events.RECONNECTING);
this.connect(this.gateway, 5500, true);
}
/**
* Called whenever an error occurs with the WebSocket.
* @param {Error} error The error that occurred
*/
onError(error) {
if (error && error.message === 'uWs client connection error') {
this.reconnect();
return;
}
/**
* Emitted whenever the client's WebSocket encounters a connection error.
* @event Client#error
* @param {Error} error The encountered error
*/
this.client.emit(Constants.Events.ERROR, error);
}
/**
* @external CloseEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
*/
/**
* Called whenever a connection to the gateway is closed.
* @param {CloseEvent} event Close event that was received
*/
onClose(event) {
this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`);
this.closeSequence = this.sequence;
// Reset the state before trying to fix anything
this.emit('close', event);
this.heartbeat(-1);
// Should we reconnect?
if (event.code === 1000 ? this.expectingClose : Constants.WSCodes[event.code]) {
this.expectingClose = false;
/**
* Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect.
* @event Client#disconnect
* @param {CloseEvent} event The WebSocket close event
*/
this.client.emit(Constants.Events.DISCONNECT, event);
this.debug(Constants.WSCodes[event.code]);
this.destroy();
return;
}
this.expectingClose = false;
this.reconnect();
}
// Heartbeat
/**
* Acknowledges a heartbeat.
*/
ackHeartbeat() {
this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`);
this.client._pong(this.lastPingTimestamp);
}
/**
* Sends a heartbeat or sets an interval for sending heartbeats.
* @param {number} [time] If -1, clears the interval, any other number sets an interval
* If no value is given, a heartbeat will be sent instantly
*/
heartbeat(time) {
if (!isNaN(time)) {
if (time === -1) {
this.debug('Clearing heartbeat interval');
this.client.clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
} else {
this.debug(`Setting a heartbeat interval for ${time}ms`);
this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time);
}
return;
}
this.debug('Sending a heartbeat');
this.lastPingTimestamp = Date.now();
this.send({
op: Constants.OPCodes.HEARTBEAT,
d: this.sequence,
});
}
// Identification
/**
* Identifies the client on a connection.
* @param {number} [after] How long to wait before identifying
* @returns {void}
*/
identify(after) {
if (after) return this.client.setTimeout(this.identify.bind(this), after);
return this.sessionID ? this.identifyResume() : this.identifyNew();
}
/**
* Identifies as a new connection on the gateway.
* @returns {void}
*/
identifyNew() {
if (!this.client.token) {
this.debug('No token available to identify a new session with');
return;
}
// Clone the generic payload and assign the token
const d = Object.assign({ token: this.client.token }, this.client.options.ws);
// Sharding stuff
const { shardId, shardCount } = this.client.options;
if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)];
// Send the payload
this.debug('Identifying as a new session');
this.send({ op: Constants.OPCodes.IDENTIFY, d });
}
/**
* Resumes a session on the gateway.
* @returns {void}
*/
identifyResume() {
if (!this.sessionID) {
this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead');
return this.identifyNew();
}
this.debug(`Attempting to resume session ${this.sessionID}`);
const d = {
token: this.client.token,
session_id: this.sessionID,
seq: this.sequence,
};
return this.send({
op: Constants.OPCodes.RESUME,
d,
});
}
}
/**
* Encoding the WebSocket connections will use.
* @type {string}
*/
WebSocketConnection.ENCODING = erlpack ? 'etf' : 'json';
WebSocketConnection.WebSocket = WebSocket;
module.exports = WebSocketConnection;

@ -0,0 +1,90 @@
const EventEmitter = require('events').EventEmitter;
const Constants = require('../../util/Constants');
const WebSocketConnection = require('./WebSocketConnection');
/**
* WebSocket Manager of the client.
* @private
*/
class WebSocketManager extends EventEmitter {
constructor(client) {
super();
/**
* The client that instantiated this WebSocketManager
* @type {Client}
*/
this.client = client;
/**
* The WebSocket connection of this manager
* @type {?WebSocketConnection}
*/
this.connection = null;
}
/**
* Sends a heartbeat on the available connection.
* @returns {void}
*/
heartbeat() {
if (!this.connection) return this.debug('No connection to heartbeat');
return this.connection.heartbeat();
}
/**
* Emits a debug event.
* @param {string} message Debug message
* @returns {void}
*/
debug(message) {
return this.client.emit('debug', `[ws] ${message}`);
}
/**
* Destroy the client.
* @returns {void} Whether or not destruction was successful
*/
destroy() {
if (!this.connection) {
this.debug('Attempted to destroy WebSocket but no connection exists!');
return false;
}
return this.connection.destroy();
}
/**
* Send a packet on the available WebSocket.
* @param {Object} packet Packet to send
* @returns {void}
*/
send(packet) {
if (!this.connection) {
this.debug('No connection to websocket');
return;
}
this.connection.send(packet);
}
/**
* Connects the client to a gateway.
* @param {string} gateway The gateway to connect to
* @returns {boolean}
*/
connect(gateway) {
if (!this.connection) {
this.connection = new WebSocketConnection(this, gateway);
return true;
}
switch (this.connection.status) {
case Constants.Status.IDLE:
case Constants.Status.DISCONNECTED:
this.connection.connect(gateway, 5500);
return true;
default:
this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`);
return false;
}
}
}
module.exports = WebSocketManager;

@ -0,0 +1,108 @@
const Constants = require('../../../util/Constants');
const BeforeReadyWhitelist = [
Constants.WSEvents.READY,
Constants.WSEvents.RESUMED,
Constants.WSEvents.GUILD_CREATE,
Constants.WSEvents.GUILD_DELETE,
Constants.WSEvents.GUILD_MEMBERS_CHUNK,
Constants.WSEvents.GUILD_MEMBER_ADD,
Constants.WSEvents.GUILD_MEMBER_REMOVE,
];
class WebSocketPacketManager {
constructor(connection) {
this.ws = connection;
this.handlers = {};
this.queue = [];
this.register(Constants.WSEvents.READY, require('./handlers/Ready'));
this.register(Constants.WSEvents.RESUMED, require('./handlers/Resumed'));
this.register(Constants.WSEvents.GUILD_CREATE, require('./handlers/GuildCreate'));
this.register(Constants.WSEvents.GUILD_DELETE, require('./handlers/GuildDelete'));
this.register(Constants.WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate'));
this.register(Constants.WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd'));
this.register(Constants.WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove'));
this.register(Constants.WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd'));
this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove'));
this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate'));
this.register(Constants.WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate'));
this.register(Constants.WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete'));
this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate'));
this.register(Constants.WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate'));
this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk'));
this.register(Constants.WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate'));
this.register(Constants.WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete'));
this.register(Constants.WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate'));
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate'));
this.register(Constants.WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate'));
this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate'));
this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate'));
this.register(Constants.WSEvents.USER_SETTINGS_UPDATE, require('./handlers/UserSettingsUpdate'));
this.register(Constants.WSEvents.USER_GUILD_SETTINGS_UPDATE, require('./handlers/UserGuildSettingsUpdate'));
this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate'));
this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart'));
this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate'));
this.register(Constants.WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete'));
this.register(Constants.WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate'));
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk'));
this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate'));
this.register(Constants.WSEvents.GUILD_SYNC, require('./handlers/GuildSync'));
this.register(Constants.WSEvents.RELATIONSHIP_ADD, require('./handlers/RelationshipAdd'));
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, require('./handlers/RelationshipRemove'));
this.register(Constants.WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd'));
this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove'));
this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll'));
}
get client() {
return this.ws.client;
}
register(event, Handler) {
this.handlers[event] = new Handler(this);
}
handleQueue() {
this.queue.forEach((element, index) => {
this.handle(this.queue[index], true);
this.queue.splice(index, 1);
});
}
handle(packet, queue = false) {
if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) {
this.ws.client._pong(this.ws.client._pingTimestamp);
this.ws.lastHeartbeatAck = true;
this.ws.client.emit('debug', 'Heartbeat acknowledged');
} else if (packet.op === Constants.OPCodes.HEARTBEAT) {
this.client.ws.send({
op: Constants.OPCodes.HEARTBEAT,
d: this.client.ws.sequence,
});
this.ws.client.emit('debug', 'Received gateway heartbeat');
}
if (this.ws.status === Constants.Status.RECONNECTING) {
this.ws.reconnecting = false;
this.ws.checkIfReady();
}
this.ws.setSequence(packet.s);
if (this.ws.disabledEvents[packet.t] !== undefined) return false;
if (this.ws.status !== Constants.Status.READY) {
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
this.queue.push(packet);
return false;
}
}
if (!queue && this.queue.length > 0) this.handleQueue();
if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet);
return false;
}
}
module.exports = WebSocketPacketManager;

@ -0,0 +1,11 @@
class AbstractHandler {
constructor(packetManager) {
this.packetManager = packetManager;
}
handle(packet) {
return packet;
}
}
module.exports = AbstractHandler;

@ -0,0 +1,17 @@
const AbstractHandler = require('./AbstractHandler');
class ChannelCreateHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
client.actions.ChannelCreate.handle(data);
}
}
/**
* Emitted whenever a channel is created.
* @event Client#channelCreate
* @param {Channel} channel The channel that was created
*/
module.exports = ChannelCreateHandler;

@ -0,0 +1,20 @@
const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants');
class ChannelDeleteHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const response = client.actions.ChannelDelete.handle(data);
if (response.channel) client.emit(Constants.Events.CHANNEL_DELETE, response.channel);
}
}
/**
* Emitted whenever a channel is deleted.
* @event Client#channelDelete
* @param {Channel} channel The channel that was deleted
*/
module.exports = ChannelDeleteHandler;

@ -0,0 +1,31 @@
const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants');
/*
{ t: 'CHANNEL_PINS_UPDATE',
s: 666,
op: 0,
d:
{ last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00',
channel_id: '314866471639044027' } }
*/
class ChannelPinsUpdate extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const channel = client.channels.get(data.channel_id);
const time = new Date(data.last_pin_timestamp);
if (channel && time) client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time);
}
}
/**
* Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, not much information
* can be provided easily here - you need to manually check the pins yourself.
* @event Client#channelPinsUpdate
* @param {Channel} channel The channel that the pins update occured in
* @param {Date} time The time of the pins update
*/
module.exports = ChannelPinsUpdate;

@ -0,0 +1,11 @@
const AbstractHandler = require('./AbstractHandler');
class ChannelUpdateHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
client.actions.ChannelUpdate.handle(data);
}
}
module.exports = ChannelUpdateHandler;

@ -0,0 +1,23 @@
// ##untested handler##
const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants');
class GuildBanAddHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id);
const user = client.users.get(data.user.id);
if (guild && user) client.emit(Constants.Events.GUILD_BAN_ADD, guild, user);
}
}
/**
* Emitted whenever a member is banned from a guild.
* @event Client#guildBanAdd
* @param {Guild} guild The guild that the ban occurred in
* @param {User} user The user that was banned
*/
module.exports = GuildBanAddHandler;

@ -0,0 +1,20 @@
// ##untested handler##
const AbstractHandler = require('./AbstractHandler');
class GuildBanRemoveHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildBanRemove.handle(data);
}
}
/**
* Emitted whenever a member is unbanned from a guild.
* @event Client#guildBanRemove
* @param {Guild} guild The guild that the unban occurred in
* @param {User} user The user that was unbanned
*/
module.exports = GuildBanRemoveHandler;

@ -0,0 +1,22 @@
const AbstractHandler = require('./AbstractHandler');
class GuildCreateHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.id);
if (guild) {
if (!guild.available && !data.unavailable) {
// A newly available guild
guild.setup(data);
this.packetManager.ws.checkIfReady();
}
} else {
// A new guild
client.dataManager.newGuild(data);
}
}
}
module.exports = GuildCreateHandler;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save