Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add !mtr command implementation #38

Merged
merged 4 commits into from
Jan 23, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 IPG_ADDRESS = process.env.IPG_ADDRESS || 'https://sites.google.com/site/mtgfamiliar/rules/MagicTournamentRules-light.html';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPG_ADDRESS?


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(IPG_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('.', '-')}>`;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, very nice. 😁 👍

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Come to think of it, this is actually a separate issue: #18

}

// 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;
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