const EventEmitter = require('events');
const Discord = require('discord.js');
const Status = require('./status');
const isValidDomain = require('is-valid-domain');
class Bot extends EventEmitter {
constructor(token, database, error, login) {
super();
this.lCache = {};
this.sCache = {};
this.database = database;
this.discord = new Discord.Client();
this.discord.on('message', message => this.message(message));
this.discord.on('guildCreate', guild => this.join(guild));
this.discord.on('guildDelete', guild => this.leave(guild));
this.discord.on('ready', () => {
this.link = `https://discordapp.com/oauth2/authorize?&client_id=${this.discord.user.id}&scope=bot&permissions=19456`;
login(this.discord.user);
});
this.discord.login(token).catch(e => error(e));
}
join(guild) {
this.emit('join', guild);
}
leave(guild) {
this.emit('leave', guild);
}
message(message) {
if (message.author === this.discord.user) {
this.emit('message', message);
return;
}
if (message.author.bot) {
return;
}
const direct = message.channel.type === 'dm' ||
message.mentions.has(this.discord.user) && !message.mentions.everyone;
if (direct) {
this.command(message);
} else {
const matches = message.content
.replace(/<[^@]*@[^>]+>/g, '')
.replace(/\s+/g, ' ')
.trim()
.match(/^!([a-z]+)/i);
if (matches) {
this.database.isGuildStatusCommand(message.guild.id, matches[1], matched => {
if (matched) {
this.command(message);
}
});
}
}
}
command(message) {
this.emit('message', message);
let content = message.content
.replace(/<[^@]*@[^>]+>/g, '')
.replace(/\s+/g, ' ')
.replace(/^[! ]*/, '')
.trim();
const argv = content.split(' ');
const command = argv[0].toLowerCase();
if (command === 'help' && (message.channel.type === 'dm' || message.mentions.has(this.discord.user))) {
this.help(message);
return;
}
if (command === 'ip' || command === 'set' || command === 'reset' || command === 'get') {
if (message.channel.type !== 'dm') {
message.reply('Pssh! Send me that via direct message, someone could see us. :open_mouth:');
return;
}
const guilds = this.getAdminGuilds(message.author);
if (guilds.length === 0) {
message.reply('You are not an owner on any servers we share.');
return;
}
let guild = null;
if (guilds.length > 1) {
if (argv.length < 2) {
message.reply('Specify Discord server as second argument, e.g.: `' + command + ' <server ID>`');
return;
}
for (let aGuild of guilds) {
if (aGuild.id === argv[1]) {
guild = aGuild;
}
}
if (guild === null) {
message.reply('Invalid Discord server ID specified in second argument, usage: `' + command + ' <server ID>`');
return;
}
argv.splice(1, 1);
} else {
guild = guilds[0];
}
this[command](message, guild, argv.slice(1));
return;
}
if (message.channel.type === 'dm') {
if (command === 'status' || command === 'getstatus' || command === 'serverstatus') {
this.status(message, argv.slice(1));
return;
}
message.react('π').catch(() => {});
return;
}
if (message.guild && (message.mentions.has(this.discord.user) || message.content
.replace(/<[^@]*@[^>]+>/g, '')
.replace(/\s+/, ' ')
.startsWith('!'))) {
this.database.isGuildStatusCommand(message.guild.id, command, matched => {
if (matched) {
this.status(message, argv.slice(1));
} else if (message.mentions.has(this.discord.user)) {
message.react('π').catch(() => {});
}
});
}
}
ip(message, guild, argv) {
switch (argv.length > 0 ? argv[0] : null) {
case 'get':
this.ipGet(message, guild, argv.slice(1));
break;
case 'set':
this.ipSet(message, guild, argv.slice(1));
break;
case 'reset':
this.ipReset(message, guild, argv.slice(1));
break;
default:
message.reply('Usage: **ip** `<get|set|reset> <alias>`');
break;
}
}
ipGet(message, guild, argv) {
if (argv.length === 0) {
this.database.ipGetAliases(guild.id, aliases => {
if (aliases.length > 0) {
message.reply('Usage: **ip get** `<' + aliases.join('|') + '>`');
} else {
message.reply('No servers added so far. Fix that by `ip set <alias>`.');
}
});
return;
}
if (!argv[0].match(/^[a-z0-9_]+$/i)) {
message.reply('Alias can only contain alphanumeric characters and underscores.');
return;
}
this.database.ipGet(guild.id, argv[0], ip => {
if (ip.length > 0) {
message.reply(ip.map(address => {
return address.password ? address.password + '@' + address.address : address.address;
}).join(' '));
} else {
message.reply('This alias is not defined. Fix that by `ip set ' + argv[0] + '`.');
}
});
}
ipSet(message, guild, argv) {
if (argv.length === 0) {
message.reply('Usage: **ip set** `<alias>`');
return;
}
if (!argv[0].match(/^[a-z0-9_]+$/i)) {
message.reply('Alias can only contain alphanumeric characters and underscores.');
return;
}
if (argv.length < 2) {
message.reply('Usage: **ip set ' + argv[0] + '** `ip.or.hostname:port [password@another.ip:port, ...]`');
return;
}
const ip = [];
for (let i = 1; i < argv.length; i++) {
let password = null;
let addr = argv[i];
if (addr.indexOf('@') > 0) {
password = addr.substring(0, addr.indexOf('@'));
addr = addr.substring(addr.indexOf('@') + 1);
}
let address = Bot.parseAddress(addr);
if (address === null) {
message.reply('Address `' + addr + '` seems to be invalid.');
return;
}
if (ip.indexOf(address) === -1) {
ip.push({
address: address,
password: password,
});
}
}
if (ip.length > 12) {
message.reply(`That's too much, buddy! You can have up to 12 servers per alias, consider using multiple aliases instead.`);
return;
}
if (ip.length > 0) {
this.database.ipGetAliases(guild.id, aliases => {
if (aliases.length >= 10 && aliases.indexOf(argv[0].toLowerCase()) === -1) {
message.reply(`You have reached aliases limit (10).`);
return;
}
this.database.ipSet(guild.id, argv[0], ip);
message.reply('OK, saved.');
});
}
}
ipReset(message, guild, argv) {
if (argv.length === 0) {
this.database.ipGetAliases(guild.id, aliases => {
if (aliases.length > 0) {
message.reply('Usage: **ip reset** `<' + aliases.join('|') + '>`');
} else {
message.reply('No servers added so far.');
}
});
return;
}
if (!argv[0].match(/^[a-z0-9_]+$/i)) {
message.reply('Alias can only contain alphanumeric characters and underscores.');
return;
}
this.database.ipGetAliases(guild.id, aliases => {
if (aliases.indexOf(argv[0].toLowerCase()) > -1) {
this.database.ipReset(guild.id, argv[0]);
message.reply('OK, reset.');
} else {
message.reply('That alias doesn\' exist. Usage: **ip reset** `<' + aliases.join('|') + '>`');
}
});
}
get(message, guild, argv) {
if (argv.length === 0) {
message.reply('Usage: **get** `<enabled|command>`');
return;
}
switch (argv[0].toLowerCase()) {
case 'enabled':
this.database.directiveGet(guild.id, 'enabled', value => {
if (value === 0) {
message.reply(`It's disabled.`);
} else {
message.reply(`It's enabled.`);
}
});
break;
case 'command':
this.database.directiveGet(guild.id, 'command', value => {
if (value !== null) {
message.reply('The command is `!' + value + '`.');
} else {
message.reply('No command defined, so it\' the default `!status`, `!getstatus` or `!serverstatus`.');
}
});
break;
default:
message.reply('Unknown directive `' + argv[0] + '`.');
break;
}
}
set(message, guild, argv) {
if (argv.length === 0) {
message.reply('Usage: **set** `<enabled|command>`');
return;
}
switch (argv[0].toLowerCase()) {
case 'enabled':
if (argv.length < 2 || !argv[1].match(/^[01]$/)) {
message.reply('Usage: **set enabled** `<0|1>`.');
return;
}
this.database.directiveSet(guild.id, 'enabled', parseInt(argv[1]));
message.reply('OK, bot is **' + (argv[1] === '1' ? 'enabled' : 'disabled') + '**. Max Hass.');
break;
case 'command':
if (argv.length < 2) {
message.reply('Usage: **set command** `<command>`.');
return;
}
if (!argv[1].match(/^[a-z]+$/)) {
message.reply('Command must only contain lowercase letters.');
return;
}
this.database.directiveSet(guild.id, 'command', argv[1]);
message.reply('OK, command has been changed.');
break;
default:
message.reply('Unknown directive `' + argv[0] + '`.');
break;
}
}
reset(message, guild, argv) {
if (argv.length === 0) {
message.reply('Usage: **reset** `<enabled|command>`');
return;
}
switch (argv[0].toLowerCase()) {
case 'enabled':
this.database.directiveSet(guild.id, 'enabled', 1);
message.reply('OK, bot is **enabled**. Max Hass.');
break;
case 'command':
this.database.directiveSet(guild.id, 'command', null);
message.reply('OK, default command was restored.');
break;
default:
message.reply('Unknown directive `' + argv[0] + '`.');
break;
}
}
status(message, argv) {
if (argv.length > 0) {
const address = Bot.parseAddress(argv[0]);
if (address) {
this.sendStatusFor(message, [{address: address}]);
} else {
if (!message.guild) {
message.react('π').catch(() => {});
return;
}
this.database.ipGet(message.guild.id, argv[0], ip => {
if (ip.length === 0) {
message.react('π').catch(() => {});
} else {
this.sendStatusFor(message, ip);
}
});
}
return;
}
if (message.guild) {
this.database.ipGet(message.guild.id, 'default', ip => {
if (ip.length === 0) {
message.react('π').catch(() => {});
} else {
this.sendStatusFor(message, ip);
}
});
return;
}
const common = [];
for (let shared of this.discord.guilds.cache.array()) {
for (let member of shared.members.cache.array()) {
if (member.user === message.author) {
common.push(shared.id);
break;
}
}
}
this.database.getSharedIP(common, ip => {
if (ip.length === 0) {
message.react('π').catch(() => {});
} else {
this.sendStatusFor(message, ip.map(i => {
return {address: i};
}));
}
});
}
sendStatusFor(message, addresses) {
if (addresses.length === 0) {
return;
}
this.buildResponseMessage(addresses, (text, success) => {
message.reply(text).then(sent => {
if (!success && sent.editable) {
setTimeout(() => {
this.buildResponseMessage(addresses, (textRetry, successRetry) => {
if (text === textRetry) {
return;
}
if (message.channel.type !== 'dm') {
textRetry = '<@' + message.author.id + '>,' + textRetry;
}
sent.edit(textRetry).catch(e => {});
}, 3000);
}, 8000);
}
}).catch(e => {
message.author
.send(text)
.catch(e => {});
message.react('π€').catch(e => {});
});
});
}
buildResponseMessage(addresses, callback, timeout = 1000) {
const responses = {};
let counter = addresses.length;
const create = () => {
const lines = [];
let online = true;
for (let address of addresses) {
const response = responses[address.address] || this.lCache[address.address];
if (!responses[address.address]) {
online = false;
}
let line = '';
if (response && ('g_needpass' in response.info) && parseInt(response.info.g_needpass) > 0 || address.password) {
line += ':lock: ';
}
if (response) {
line += '**' + Discord.Util.escapeMarkdown(response.info.sv_hostname.trim()) + '**, ';
if ('mapname' in response.info) {
line += 'map *' + Discord.Util.escapeMarkdown(response.info.mapname) + '*, ';
}
const privateClients = ('sv_privateclients' in response.info) && response.info.sv_privateclients.match(/^\d+$/)
? parseInt(response.info.sv_privateclients)
: 0;
const maxClients = ('sv_maxclients' in response.info) && response.info.sv_maxclients.match(/^\d+$/)
? parseInt(response.info.sv_maxclients) - privateClients
: 0;
const activeClients = response.clients
.filter(client => client.ping > 0)
.length;
if (activeClients > 0) {
line += `**${activeClients}/${maxClients}**`;
} else {
line += `${activeClients}/${maxClients}`;
}
if (privateClients > 0) {
line += '+' + privateClients;
}
if (!responses[address.address]) {
line = ':x: ' + line;
}
}
if (!response) {
line += `**${address.address}**`;
line = ':x: ' + line;
}
const p = address.address.split(':');
line += ' `connect ';
if (p[1] === '27960') {
line += p[0];
} else {
line += address.address;
}
if (address.password) {
line += ' ; password ' + Discord.Util.escapeMarkdown(address.password);
}
line += '`';
lines.push(line);
}
callback('\n' + lines.join('\n'), online);
};
for (let address of addresses) {
if (address.address in this.sCache) {
responses[address.address] = this.sCache[address.address];
counter--;
continue;
}
const p = address.address.split(':');
Status.status(p[0], parseInt(p[1]), (error, response) => {
responses[address.address] = response;
if (response !== null) {
this.sCache[address.address] = response;
this.lCache[address.address] = response;
setTimeout(() => delete this.sCache[address.address], 30000);
}
counter--;
if (counter === 0) {
create();
}
}, timeout);
}
if (counter === 0) {
create();
}
}
help(message) {
if (message.channel.type !== 'dm') {
this.database.getGuildStatusCommand(message.guild.id, command => {
this.database.ipGetAliases(message.guild.id, aliases => {
if (aliases.length === 0) {
return;
}
const ndAliases = aliases.filter(a => a !== 'default');
let append = '';
if (ndAliases.length > 0) {
append = ndAliases.join('|');
}
if (aliases.indexOf('default') > -1 && append !== '') {
append = '[' + append + ']';
}
if (append !== '') {
append = ' ' + append;
}
message.reply('`!' + command + append + '`');
});
});
return;
}
if (this.getAdminGuilds(message.author).length === 0) {
message.reply('`!status <ip:port>`');
return;
}
message.reply(
'**Servers configuration**\n' +
'`ip set default xx.xx.xx.xx:27xxx [password@xx.xx.xx.xx.xx:27xxy ...]`\n' +
'This will list status of servers (you can add more addresses delimited by a space) on `!status` command without any arguments.\n' +
'You can define other aliases, such as `tj`, just replace the `default` alias:\n' +
'`ip set tj xx.xx.xx.xx:27xxx`\n' +
'That will make `!status tj` command working. Again, you can list multiple addresses.\n' +
'Call the `ip set <alias>` again for change. You can also list addresses of an alias with `ip get <alias>` or remove an alias completely with `ip reset <alias>`.\n\n' +
'**Directives**\n' +
'`set command whatzup` - replaces `!status` with `!whatzup`\n' +
'`set enabled 0` - disables me, use that as a kill switch\n' +
'`reset <directive>` - resets original directive value\n' +
'`get <directive>` - shows current directive value\n' +
'\nSupport: AΠΌΠΊ#6633 (https://klva.cz/code/amk/etwolf-maxhass)'
);
}
getAdminGuilds(user) {
return this.discord.guilds
.cache
.filter(guild => guild.ownerID === user.id || guild.members.cache.some(member => member.user === user && member.hasPermission("ADMINISTRATOR")))
.array();
}
static parseAddress(address) {
address += ':27960';
const parts = address.split(':');
if (!Status.isIP(parts[0]) && !isValidDomain(parts[0])) {
return null;
}
if (!parts[1].match(/^\d+$/)) {
return null;
}
return parts[0] + ':' + parts[1];
}
}
module.exports = Bot;