Skip to content

Commit

Permalink
Finished MMR support.
Browse files Browse the repository at this point in the history
  • Loading branch information
MeLlamoPablo committed Jan 15, 2017
1 parent 3749e1e commit 35c3bfd
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 19 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,20 @@ used `git` to clone the repository, this is easy:
$ git pull --all
```

If you forked this repo to deploy to Heroku, this will not work, because it's pulling from your repo, and not from this one. To solve that, first [configure this repo (MeLlamoPablo/schedulebot) as a remote for your fork (YOUR_GITHUB_USERNAME/schedulebot)](https://help.github.com/articles/configuring-a-remote-for-a-fork/). Then, [fetch this repo](https://help.github.com/articles/syncing-a-fork/) *(change `master` to `dota` or `heroku-dota`, depending on what branch you're working with)*.
If you forked this repo to deploy to Heroku, this will not work, because it's pulling from your
repo, and not from this one. To solve that, first [configure this repo (MeLlamoPablo/schedulebot)
as a remote for your fork (YOUR_GITHUB_USERNAME/schedulebot)](https://help.github.com/articles/configuring-a-remote-for-a-fork/).
Then, [fetch this repo](https://help.github.com/articles/syncing-a-fork/)
*(change `master` to `dota` or `heroku-dota`, depending on what branch you're working with)*.

However, after this your bot is not ready yet. A database update is also required:
After that, perform another npm install to make sure that you get any new dependencies or update
existing ones:

```
$ npm install
```

However, after all of this your bot is not ready yet. A database update is also required:

```sh
$ npm run update
Expand Down
27 changes: 23 additions & 4 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module.exports = {
// More info: https://discordapp.com/developers/docs/topics/permissions
delete_after_reply: {
enabled: true,
time: 60000, // In milliseconds
time: 60000 // In milliseconds
},

// If true, it will delete any message that is not a command from the master channel.
Expand Down Expand Up @@ -94,17 +94,17 @@ module.exports = {
},

steam: {
// The name that the Steam bot will take
// The name that the Steam bots will take. It will be appended with "#id" as in "#1".
name: "ScheduleBot",

// The bot's profile URL. It's needed to redirect users to it.
// The first bot's profile URL. It's needed to redirect users to it.
profile_url: "http://steamcommunity.com/profiles/YOUR_BOT_ID/"
},

dota: {
// The default inhouse server, which will be used if the user doesn't pass the
// --server flag to the add-inhouse command.
// Go to that command's file (Or type -schedulebot add-ihouse --help)
// Go to that command's file (Or type -schedulebot add-inhouse --help)
// to see possible values.
defaultServer: "Luxembourg",

Expand All @@ -113,6 +113,25 @@ module.exports = {
ticketing: {
enabled: false,
league_id: 12345
},

// If enabled is true, the bot will fetch MMR from OpenDota for every user that links
// their Steam account, and display it in event summaries.
//
// The user must have the "Expose Public Match Data" option enabled, must be displaying
// their MMR on their Dota profile, and must have signed in OpenDota at least once, using
// Steam. If OpenDota doesn't know the user MMR, ScheduleBot won't either, and will display
// a "MMR Unknown message"
mmr: {
enabled: true,

// If enforce is true, the bot will only allow people who have their MMR publicly
// exposed in OpenDota to confirm any events. This is useful for competitive leagues
// who need to control MMR; it's recommended to leave it false otherwise.
enforce: false,

// ScheduleBot will update all users' MMR on each interval.
update_interval: 8 // In hours
}
}
};
Expand Down
24 changes: 24 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ const fs = require('fs')
, SummaryHandler = require('./modules/summaryhandler')
, db = require('./modules/dbhandler')
, cfg = require('../config.js')
, getMmr = require('./modules/dotahandler/mmr')
, loadSteamBot = require('./modules/steambotshandler/loadbot')
, moment = require('moment')
, pkg = require('../package.json')
, Discord = require('discord.js')
, DotaHandler = require('./modules/dotahandler')
, ECreateLobbyError = require('./structures/enums/ECreateLobbyError')
, ELobbyStatus = require('./structures/enums/ELobbyStatus')
, scheduleRequests = require('./modules/helpers/request-scheduler')
, steamVerf = require('steam-verificator')
, bot = new Discord.Client()
;
Expand Down Expand Up @@ -313,6 +315,28 @@ Promise.all(startupPromises).then(values => {
setTimeout(update, cfg.update_interval);
})();

// Execute the updateMmr function now and every update_interval hours
if (cfg.dota.mmr.enabled) {
(function updateMmr(){
db.users.getAllLinked().then(users => {
let requests = users.map(user => user.steam_id);

// Make one call each two seconds so we don't throttle OpenDota's API.
scheduleRequests(2000, getMmr, requests).then(mmrArr => {

for (let i = 0; i < mmrArr.length; i++) {
let mmr = mmrArr[i];

db.users.updateMmr(users[i].steam_id, mmr).catch(console.error);
}

}).catch(console.error);
});

setTimeout(updateMmr, cfg.dota.mmr.update_interval * 60 * 60 * 1000);
})();
}

console.log("[DISCORD] Running!");
console.log("[INFO] ScheduleBot finished loading.")
}).catch(console.error);
Expand Down
14 changes: 7 additions & 7 deletions lib/modules/dbhandler/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,16 +306,16 @@ const confirms = {
* @property {string} user The user Discord's snowflake id. Should not be converted to
* number.
* @property {boolean} attends Whether or not the user will attend the event.
* @property {number} solo_mmr The user's solo MMR, or null if unknown.
*/
getByEvent: function(event) {
return new Promise((fulfill, reject) => {
db.select().from("confirms").where({
event: event.id
}).then(
rows => {
fulfill(rows.length > 0 ? rows : null);
}, reject
);
db.select(["id", "event", "user", "attends", "solo_mmr"])
.from("confirms")
.innerJoin("users", /*on*/ "confirms.user", "=", "users.discord_id")
.where({ event: event.id })
.then(rows => fulfill(rows.length > 0 ? rows : null))
.catch(reject);
});
},

Expand Down
145 changes: 145 additions & 0 deletions lib/modules/helpers/request-scheduler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"use strict";

/**
* Schedule requests to a function that returns a Promise on a given interval. This is similar to
* Promise.all(), except that one request is sent after another, leaving a minimum interval
* (minInterval) between them.
*
* This is useful for making API requests without sending every request at once, in order not to
* exceed the API rate limit.
*
* @param {number} minInterval
* The minimum interval between requests, in milliseconds.
*
* @param {function} fn
* The function to execute. Must return a Promise.
*
* @param {...*} params
* For every parameter required by the function passed to fn, one array is required. Each array
* must be of the same length, and each element contains the parameters to pass to each request.
*
* @returns {Promise<*>}
*
* @example
* // Suppose that we want to call the function makeApiRequest() three times, with a minimum wait
* // of ten seconds between each call.
* //
* // As an example, the signature of makeApiRequest() could be the following:
* // function makeApiRequest(userID: number, username: string): Promise<Result>
* // (using TypeScript syntax to illustrate)
* //
* // Since our function returns a Promise<Result>, scheduleRequest will return Promise<Result[]>,
* // Now, let's make the call:
*
* scheduleRequests(
* 10000,
* makeApiRequest,
* [
* 1,
* 2,
* 3
* ],
* [
* "Foo",
* "Bar",
* "Biz"
* ]
* ).then(results => {
* for (let r of results) {
* // Handle each result individually.
* }
* }).catch(console.error);
*/
function scheduleRequests(minInterval, fn, ...params) {
return new Promise((fulfill, reject) => {
/*
* STEP 1: REORGANIZE PARAMS
*
* In the example, the user passes two params to the function makeApiRequest:
* [1, 2, 3] and ["foo", "bar", "biz"].
*
* This gets sent to us in the params variable in the following form:
* [
* [1, 2, 3],
* ["foo", "bar", "biz"]
* ]
*
* Our goal is to fill the requests variable like this:
* [
* [1, "foo"],
* [2, "bar"],
* [3, "biz"]
* ]
*
* With that form, it's easy to make the call to makeApiRequest.
*/

let numberOfRequests = null;
let requests = [];

for (let i = 0; i < params.length; i++) {
let paramArr = params[i];

if (numberOfRequests === null) {

numberOfRequests = paramArr.length;

} else if (paramArr.length !== numberOfRequests) {

return reject(new Error("Parameter length mismatch: the first parameter array " +
"you passed, has " + numberOfRequests + " elements, but one of the others" +
" has " + paramArr.length + " elements."));

}

for (let j = 0; j < paramArr.length; j++) {
let param = paramArr[j];

if (typeof requests[j] === "undefined") {
requests[j] = [];
}

requests[j][i] = param;
}
}

/*
* STEP 2: MAKE THE CALLS
*
* Once we have the requests in that format, making the calls is easy.
*
* We define a function called makeNextCall which will send the first request. Then, it
* keep calling itself until no more requests are defined, at which point the function
* will end by fulfilling the results array.
*/

let currentCall = 0;
let results = [];

let makeNextCall = function() {
let start = Date.now();

fn(...requests[currentCall]).then(result => {
results.push(result);

currentCall++;

if (requests[currentCall]) {
// If the elapsed time is less than the minimum interval,
// wait before making the next call.
let elapsed = Date.now() - start;

setTimeout(makeNextCall, minInterval > elapsed ? minInterval - elapsed : 0);
} else {
fulfill(results);
}

}).catch(reject);
};

makeNextCall();

});
}

module.exports = scheduleRequests;
49 changes: 49 additions & 0 deletions lib/modules/helpers/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<a name="scheduleRequests"></a>

## scheduleRequests(minInterval, fn, ...params) ⇒ <code>Promise.&lt;\*&gt;</code>
Schedule requests to a function that returns a Promise on a given interval. This is similar to
Promise.all(), except that one request is sent after another, leaving a minimum interval
(minInterval) between them.

This is useful for making API requests without sending every request at once, in order not to
exceed the API rate limit.

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| minInterval | <code>number</code> | The minimum interval between requests, in milliseconds. |
| fn | <code>function</code> | The function to execute. Must return a Promise. |
| ...params | <code>\*</code> | For every parameter required by the function passed to fn, one array is required. Each array must be of the same length, and each element contains the parameters to pass to each request. |

**Example**
```js
// Suppose that we want to call the function makeApiRequest() three times, with a minimum wait
// of ten seconds between each call.
//
// As an example, the signature of makeApiRequest() could be the following:
// function makeApiRequest(userID: number, username: string): Promise<Result>
// (using TypeScript syntax to illustrate)
//
// Since our function returns a Promise<Result>, scheduleRequest will return Promise<Result[]>,
// Now, let's make the call:

scheduleRequests(
10000,
makeApiRequest,
[
1,
2,
3
],
[
"Foo",
"Bar",
"Biz"
]
).then(results => {
for (let r of results) {
// Handle each result individually.
}
}).catch(console.error);
```
16 changes: 14 additions & 2 deletions lib/modules/summaryhandler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,13 @@ class SummaryHandler {
+ people.confirmed.length + "/" + event.limit + "):\n\n";
if (people.confirmed.length > 0) {
for (let i = 0; i < people.confirmed.length; i++) {
summary += "- <@" + people.confirmed[i] + ">\n"; // TODO show mmr
summary += "- <@" + people.confirmed[i].user + ">";

if (cfg.dota.mmr.enabled) {
summary += ` | \`${people.confirmed[i].solo_mmr || "Unknown"} MMR\``;
}

summary += "\n";
}
} else {
summary += "- None yet.\n";
Expand All @@ -203,7 +209,13 @@ class SummaryHandler {
summary += `\n**${!expired ? "Will not attend" : "Did not attend"}**:\n\n`;
if (people.rejected.length > 0) {
for (let i = 0; i < people.rejected.length; i++) {
summary += "- <@" + people.rejected[i] + ">\n";
summary += "- <@" + people.rejected[i].user + ">";

if (cfg.dota.mmr.enabled) {
summary += ` | \`${people.rejected[i].solo_mmr || "Unknown"} MMR\``;
}

summary += "\n";
}
} else {
summary += "- None yet.\n";
Expand Down
8 changes: 4 additions & 4 deletions lib/structures/ScheduledEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ class ScheduledEvent {
* with the error.
*
* @typedef {Object} people
* @property {string[]} confirmed An array with the Discord's snowflake user ids of
* @property {confirm[]} confirmed An array with the Discord's snowflake user ids of
* confirmed people.
* @property {string[]} rejected An array with the Discord's snowflake user ids of
* @property {confirm[]} rejected An array with the Discord's snowflake user ids of
* people who have declined the event.
* @property {string[]} waiting An array with the Discord's snowflake user ids of
* people who are waiting to confirm or reject.
Expand All @@ -123,9 +123,9 @@ class ScheduledEvent {
if (confirms !== null) {
for (let i = 0; i < confirms.length; i++) {
if (confirms[i].attends) {
confirmed.push(confirms[i].user);
confirmed.push(confirms[i]);
} else {
rejected.push(confirms[i].user);
rejected.push(confirms[i]);
}
}
}
Expand Down

0 comments on commit 35c3bfd

Please sign in to comment.