Skip to content

Commit

Permalink
Merge pull request #38 from bra1n/develop
Browse files Browse the repository at this point in the history
Add !mtr command implementation
  • Loading branch information
bra1n authored Jan 23, 2017
2 parents 2c38c77 + 29befdc commit 1424136
Show file tree
Hide file tree
Showing 8 changed files with 2,475 additions and 23 deletions.
1 change: 0 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ rules:
no-case-declarations: 2
no-div-regex: 2
no-else-return: 0
no-empty-label: 2
no-empty-pattern: 2
no-eq-null: 2
no-eval: 2
Expand Down
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Clone the Git repository and run the following commands:
npm install
export DISCORD_TOKEN="<your Discord bot token>"
export CR_ADDRESS="http://media.wizards.com/2016/docs/MagicCompRules_20160930.txt"
export IPG_ADDRESS="https://sites.google.com/site/mtgfamiliar/rules/MagicTournamentRules-light.html"
export IPG_ADDRESS=https://sites.google.com/site/mtgfamiliar/rules/InfractionProcedureGuide-light.html"
export MTR_ADDRESS="https://sites.google.com/site/mtgfamiliar/rules/MagicTournamentRules-light.html"
node server.js
```
Expand All @@ -22,9 +23,5 @@ node server.js
- **!cr \<paragraph number\>**: shows the chosen paragraph from the [Comprehensive Rules](https://rules.wizards.com/rulebook.aspx?game=Magic&category=Game+Rules), *Example: !cr 100.6b*
- **!define \<keyword\>**: shows the chosen keyword definition from the [Comprehensive Rules](https://rules.wizards.com/rulebook.aspx?game=Magic&category=Game+Rules), *Example: !define banding*
- **!ipg \<paragraph number\>**: shows the chosen (sub-)section from the [Infraction Procedure Guide](http://blogs.magicjudges.org/rules/ipg/), *Example: !ipg 2.1, !ipg hce philosophy*
- **!help**: show a list of available commands (in a direct message)

## Coming soon

- **!mtr \<paragraph number\>**
- **!jar \<paragraph number\>**
- **!mtr \<paragraph number\>**: shows the chose section from the [Magic: The Gathering Tournament Rules](http://blogs.magicjudges.org/rules/mtr/), *Example: !mtr 3, !mtr 4.2*
- **!help**: show a list of available commands (in a direct message)
16 changes: 9 additions & 7 deletions modules/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Help {
this.commandList = {
'!card': 'Search for an English Magic card by (partial) name, *Example: !card iona*',
'!ipg': 'Show an entry from the Infraction Procedure Guide, *Example: !ipg 4.2, !ipg grv philosophy*',
'!mtr': 'Show an entry from Magic: The Gathering Tournament Rules, *Example: !mtr 2, !mtr 4.2*',
'!cr': 'Show an entry from the Comprehensive Rulebook, *Example: !cr 100.6b*',
'!define': 'Show a definition from the Comprehensive Rulebook, *Example: !define phasing*'
};
Expand All @@ -14,13 +15,14 @@ class Help {
}

handleMessage(command, parameter, msg) {
let response = "**Available commands:**\n";
for(let cmd in this.commandList) {
response += `:small_blue_diamond: **${cmd}**: ${this.commandList[cmd]}\n`;
}
response += "\nThis judgebot is provided free of charge and can be added to your channel, too!\n";
response += ":link: https://bots.discord.pw/bots/240537940378386442\n";
response += ":link: https://github.com/bra1n/judgebot\n";
const commands = Object.keys(this.commandList).map(cmd => ` :small_blue_diamond: **${cmd}**: ${this.commandList[cmd]}`);
const response = [
'**Available commands:**',
commands.join('\n'),
'\nThis judgebot is provided free of charge and can be added to your channel, too!',
':link: https://bots.discord.pw/bots/240537940378386442',
':link: https://github.com/bra1n/judgebot'
].join('\n');
return msg.author.sendMessage(response);
}
}
Expand Down
156 changes: 150 additions & 6 deletions modules/rules/mtr.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,166 @@
const _ = require('lodash');
const cheerio = require('cheerio');
const rp = require('request-promise-native');
const Table = require('tty-table');
const log = require('log4js').getLogger('mtr');

const MTR_ADDRESS = process.env.MTR_ADDRESS || 'https://sites.google.com/site/mtgfamiliar/rules/MagicTournamentRules-light.html';

class MTR {
constructor() {
this.Location = "http://blogs.magicjudges.org/rules/mtr/";
this.commands = ["mtr"];
constructor(initialize = true) {
this.location = 'http://blogs.magicjudges.org/rules/mtr';
this.maxLength = 1950;
this.commands = ['mtr'];
this.mtrData = {
chapters: {appendices: {title: 'Appendices', sections: []}},
sections: {}
};

if (initialize) {
this.download(MTR_ADDRESS).then(mtrDocument => this.init(mtrDocument));
}
}

download(url) {
return rp({url: url, simple: false, resolveWithFullResponse: true }).then(response => {
if (response.statusCode === 200) {
return response.body;
} else {
log.error('Error loading MTR, server returned status code ' + response.statusCode);
}
}).catch(e => log.error('Error loading MTR: ' + e, e));
}

init(mtrDocument) {
const $ = cheerio.load(mtrDocument);
this.cleanup($);
this.handleChapters($);
this.handleSections($);
log.info('MTR Ready');
}

cleanup($) {
// wrap standalone text nodes in p tags
const nodes = $('body').contents();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// Text Node
if (node.nodeType === 3) {
$(node).wrap('p');
}
}

// strip out p tags containing only whitespace
$('p').filter((i, e) => /^\s*$/.test($(e).text())).remove();

// mark chapter headers
$('h4').filter((i, e) => /^\d+\.\s/.test($(e).text().trim())).addClass('chapter-header');
// mark section headers
$('h4').filter((i, e) => /^(\d+\.\d+\s|Appendix)/.test($(e).text().trim())).addClass('section-header');
}

handleChapters($) {
$('.chapter-header').each((i, e) => {
const title = $(e).text().trim();
const number = title.split('.', 1)[0];
this.mtrData.chapters[number] = {
title: title,
sections: []
};
});
}

handleSections($) {
$('.section-header').each((i, e) => {

const title = $(e).text().trim();
const key = title.startsWith('Appendix') ? _.kebabCase(title.split('-', 1)[0]) : title.split(/\s/, 1)[0];
const chapter = key.startsWith('appendix') ? 'appendices' : key.split('.', 1)[0];
const content = this.handleSectionContent($, $(e), title, key);

this.mtrData.sections[key] = {
title: title,
content: content
};
this.mtrData.chapters[chapter].sections.push(key);
});
}

handleSectionContent($, sectionHeader, title, number) {
/* on most sections we can just use the text, special cases are:
* - banlists (sections ending in deck construction), these are basically long lists of sets and cards
* - some sections containing tables (draft timings and recommended number of rounds)
*/
if (/Format Deck Construction$/.test(title)) {
// Asking a bot for the banlist has to be one of the worst ways to inquire about card legality that I can imagine,
// defer handling this until I'm really bored and redirect people to the annotated mtr in the meantime
return `You can find the full text of ${title} on <${this.location}${number.replace('.', '-')}>`;
}

// there are some headers which are neiter section nor chapter headers interspersed in the secions
const sectionContent = sectionHeader.nextUntil('.section-header,.chapter-header').wrap('<div></div>').parent();
sectionContent.find('h4').replaceWith((i, e) => `<p>\n\n**${$(e).text().trim()}**\n\n</p>`);

// replace tables with an ASCII representation
sectionContent.find('table').replaceWith((i, e) => {
const tableString = this.generateTextTable($, $(e));
return `<p>\n${tableString}\n</p>`;
});
// mark each line as a Codeblock (uses monospace font), otherwise message splitting will mess up the formatting
return sectionContent.text().trim().replace(/\n\s+\n/, '\n\n');
}

generateTextTable($, table) {
const rows = table.find('tr:has("td,th")').map((i, e) => $(e).children()).get();
const data = rows.map(r => r.map((i, e) => $(e).text().trim()).get());
const textTable = new Table(null, data).render();
return textTable.split('\n').filter(l => !/^\s*$/.test(l)).map(l => '`' + l + '`').join('\n');
}

formatChapter(chapter) {
const availableSections = chapter.sections.map(s => `*${_.truncate(this.mtrData.sections[s].title)}* (${s})`).join(', ');
return [
`**MTR - ${chapter.title}**`,
`**Available Sections**: ${availableSections}`
].join('\n\n');
}

formatSection(section) {
return [
`MTR - **${section.title}**`,
section.content
].join('\n\n');
}

getCommands() {
return this.commands;
}

find(parameter) {
//todo
if (parameter.indexOf('-') !== -1 || parameter.indexOf('.') !== -1) {
// looks like a section query
const section = this.mtrData.sections[parameter];
if (section) {
return this.formatSection(section);
}
return 'This section does not exist. Try asking for a chapter to get a list of available sections for that chapter.';
}

const chapter = this.mtrData.chapters[parameter];
if (chapter) {
return this.formatChapter(chapter);
}
const availableChapters = _.values(this.mtrData.chapters).map(c => `*${c.title}*`).join(', ');
return `This chapter does not exist.\n**Available Chapters**: ${availableChapters}`;
}

handleMessage(command, parameter, msg) {
if (parameter) {
return msg.channel.sendMessage(this.find(parameter));
const result = this.find(parameter.trim());
return msg.channel.sendMessage(result, {split: true});
}
return msg.channel.sendMessage('**Magic Tournament Rules**: <' + this.Location + '>');
return msg.channel.sendMessage('**Magic Tournament Rules**: <' + this.location + '>');
}
}

module.exports = MTR;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"lodash": "^4.16.6",
"log4js": "^1.0.0",
"request": "^2.76.0",
"request-promise-native": "^1.0.3"
"request-promise-native": "^1.0.3",
"tty-table": "^2.2.5"
},
"devDependencies": {
"chai": "^3.5.0",
Expand Down
2 changes: 1 addition & 1 deletion test/rules/ipg.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ describe('IPG', function () {
let ipg;

before(function () {
this.timeout = 5000;
this.timeout(5000);
ipg = new IPG(false);
ipg.init(fs.readFileSync(`${__dirname}/ipg.html`, 'utf8'));
});
Expand Down
Loading

0 comments on commit 1424136

Please sign in to comment.