Skip to content

Instantly share code, notes, and snippets.

@Alwinfy
Last active October 21, 2018 17:12
Show Gist options
  • Save Alwinfy/164e4b79ff054b0991e2244c01a09485 to your computer and use it in GitHub Desktop.
Save Alwinfy/164e4b79ff054b0991e2244c01a09485 to your computer and use it in GitHub Desktop.
A Simple(?) CLI client for Discord.
const TOKEN = null; // change this for private use
const PREFIX = '!';
const colors = +process.stdout.isTTY && process.stdout.getColorDepth();
const formats = colors ? {
'\\*\\*': [1, 22], // escape for the regex
'\\*': [3, 23],
'~~': [9, 29],
'`': [7, 27],
'__': [4, 24],
// '_': [3, 23]
} : {}; // comment if unsupported
const pager = require('default-pager');
const {readFileSync} = require('fs');
const rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
prompt: ''
}),
client = new (require('discord.js').Client)();
let guild = null, all = {guild: true, chan: true}, chan = null, last = {sent: null, got: null}, unread = {};
const out = data => {
process.stdout.cork();
pager(() => {
process.stdin.resume();
process.stdout.uncork();
if(process.stdin.setRawMode)
process.stdin.setRawMode(true);
rl.resume();
rl.prompt();
}).end(`${data}`);
};
const markdown = content => Object.keys(formats).reduce((content, fmt) =>
content.replace(new RegExp(`(?<!\\\\)${fmt}(.+?)${fmt}`, 'g'), `\\0${formats[fmt][0]}\\0$1\\0${formats[fmt][1]}\\0`)
.replace(new RegExp(`\\\\${fmt}`, 'g'), fmt.replace(/\\/g, '')), content);
const fmts = {
'text': msg => `\\035\\0[${msg.guild.name}]\\034\\0[#${msg.channel.name}] \\00;1\\0${colors >= 4 && msg.member && msg.member.colorRole ? `\\038;2;${0xff & (msg.member.colorRole.color >> 16)};${0xff & (msg.member.colorRole.color >> 8)};${0xff & msg.member.colorRole.color}\\0` : ''}<${(msg.member && msg.member.nickname) || msg.author.username}>\\00\\0 ${markdown(msg.content)}`,
'dm': msg => `\\01\\0<${msg.author.username} -> ${msg.author.id === client.user.id ? msg.channel.recipient.username : client.user.username}>\\00\\0 ${markdown(msg.content)}`
};
const fmtmsg = msg => ((fmts[msg.channel.type] ||
(msg => `Unsupported msg type ${msg.channel.type}`))(msg)
.replace(/<@!?(\d+)>|@(everyon|her)e\b/g, (match, one) => {
let name = null, user;
if(match.endsWith('e'))
name = match.substr(1);
if(msg.guild && (user = msg.guild.members.get(one)))
name = user.nickname || user.user.username;
else if(user = client.users.get(one)) // not a typo
name = user.username;
return (name && `\\094\\0@${name}\\039\\0`) || match;
}).replace(/<#(\d+)>/g, (match, one) => {
if(!msg.guild) return match;
let user = msg.guild.channels.get(one);
return user ? `\\094\\0#${user.name}\\039\\0` : match;
}) +
msg.attachments.map(att => `\n\t{A: ${att.url}}`).join('') +
msg.embeds.map(emb => `\n\t{E:\\01\\0${emb.title ? ' ' + emb.title : ''}\\00\\0${emb.description ? ' ' + emb.description : ''}${emb.image ? ` (I: ${emb.image.url})` : ''}${emb.fields.map(f => `\n\t\t${f.name}: ${f.value.replace(/\n/g, '\n\t\t')}`)}${emb.fields.length ? '\n\t' : ''}}`).join('') +
(msg.edits.length > 1 ? '\\02\\0 (edited)\\00\\0' : ''))
.replace(/\\0([0-9;]+?)\\0/g, colors > 1 ? '\x1b[$1m' : '');
const fmtout = out => out.trim()
.replace(/\\n/g, '\n')
.replace(/@[^@#]+?#\d{4}/g, match => client.users.find('tag', match.substring(1)) || match)
.replace(/#[a-z_-]+/ig, match => (guild && guild.channels.find('name', match.substring(1))) || match)
.replace(/:[a-z_]+:/ig, match => ((client.user.bot ? client : guild).emojis.find('name', match.substring(1, match.length - 1))) || match)
const help = `Discord CLIent v1.0.0
Prefix all commands below with ${PREFIX}. No prefix sends the message to the current channel.
guild all|one|ls|<abbr> - Choose a guild by abbreviation. 'guild one' makes you only listen to that guild.
chan all|one|ls|<name> - Choose a channel in the current guild.
dm ls|<tag> - Choose a DM channel.
help - Display this help.
backlog <n> - Display the last <n> messages of the current channel.
del - Delete the last message. Can't use consecutively.
edit - Edit the last message.
whois - Lookup a user's nick.
last - Jump to the channel of the last-recieved message.
quit - Exit the application.`;
const cmds = {
'': args => {}, // noop
'eval': args => {
let res;
try {
res = eval(args.join(' '));
}
catch(e) {
console.log(`Invalid eval: ${e}`);
return;
}
res ? out(res) : console.log('Got falsey result.');
},
'guild': args => {
if(!args.length) {
console.log(guild ? guild.name : 'Unselected');
return;
}
switch(args[0]) {
case 'ls':
out(client.guilds
.map(guild => `${guild.name} (${guild.nameAcronym})`)
.join('\n'));
break;
case 'all':
all.guild = true;
break;
case 'one':
all.guild = false;
break;
default:
const ng = client.guilds.findAll('nameAcronym', args[0])[+args[1] || 0];
if(!ng) {
console.log(`Unknown guild ${args[0]}`);
return;
}
guild = ng;
chan = null;
}
},
'mark': args => {
if(client.user.bot) return;
if(!guild) {
console.log('Pick a guild first!');
return;
}
guild.acknowledge().then(g => {
for(let id of guild.channels.keys())
delete unread[id];
console.log(`Marked all channels in ${g.name} as read!`)
}).catch(e => console.log('Failed to mark as read!'));
},
'nick': args => {
if(!guild) {
console.log('Pick a guild first!');
return;
}
guild.members.get(client.user.id).setNickname(args.join(' ') || client.user.username)
.then(g => console.log((args.length ? 'Set' : 'Cleared') + ' nickname!'))
.catch(e => console.log(`Failed to ${args.length ? 'set' : 'clear'} nickname!`));
},
'chan': args => {
if(!args.length) {
console.log(chan ? (chan.guild ? `#${chan.name} in ${chan.guild.name}` : `DM with ${chan.recipient.tag}`) : 'Unselected');
return;
}
if(!guild) {
console.log('Pick a guild first!');
return;
}
switch(args[0]) {
case 'ls':
out(guild.channels.filter(chan => chan.type === 'text')
.map(chan => chan.name).join('\n'));
break;
case 'all':
all.chan = true;
break;
case 'one':
all.chan = false;
break;
default:
const name = args[0].toLowerCase();
const nc = guild.channels.findAll('name', name)[+args[1] || 0];
if(!nc) {
console.log(`Unknown channel ${name}`);
return;
}
chan = nc;
}
},
'dm': args => {
if(!args.length) {
cmds.chan([]);
return;
}
switch(args[0]) {
case 'ls':
out(client.users.map(chan => chan.tag).join('\n'));
break;
default:
const nc = client.users.find('tag', args.join(' '));
if(!nc) {
console.log(`Unknown user ${args.join(' ')}`);
return;
}
guild = null;
chan = nc.dmChannel;
}
},
'whois': args => {
if(!guild) {
console.log('Cannot lookup by username without a guild!');
return;
}
const name = args.join(' ').replace(/^@?/, '');
const users = guild.members.findAll('nickname', name)
.concat(guild.members.filterArray(u => u.user.username === name));
if(!users.length) {
console.log('No users by that nickname found!');
return;
}
out(`Found users: ${users.map(u => `\n${u.user.tag}`)}`);
},
'backlog': args => {
if(!chan) {
console.log('Pick a channel first!');
return;
}
chan.fetchMessages({limit: +args[0] || 10})
.then(msgs => out(msgs.map(fmtmsg).reverse().join('\n')))
//.catch(console.log('Failed to fetch messages!'));
.catch(console.error);
},
'unread': args => {
let output = '';
for(let i in unread) {
let chan = client.channels.get(i);
if(!chan) {
console.log(`Unknown chan ${chan}`);
continue;
}
output += `${unread[i]} in ` + (chan.guild ? `#${chan.name} in ${chan.guild.name}` : `DM with ${chan.recipient.tag}`) + '\n';
}
out(output);
},
'edit': args => {
if(!last.sent) {
console.log('Cannot edit without a last-sent!');
return;
}
if(!args.length) {
console.log(`No edit made! Use ${PREFIX}del to delete.`);
return;
}
last.sent.edit(fmtout(args.join(' '))).catch(e => console.log('Edit failed!'));
},
'del': args => {
if(!last.sent) {
console.log('Cannot delete without a last-sent!');
return;
}
last.sent.delete().then(m => 'Message deleted!')
.catch(e => console.log('Delete failed!'));
},
'last': args => {
if(!last.got) {
console.log('Wait for a message first!');
return;
}
guild = last.got.guild || null;
chan = last.got.channel;
},
'help': () => out(help),
'quit': () => rl.close()
};
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}! Type ${PREFIX}help for help.`);
rl.prompt();
});
client.on('message', msg => {
if(!msg.mentions.users.has(client.user.id) && (
(!all.guild && guild && msg.guild.id !== guild.id) ||
(!all.chan && chan && msg.channel.id !== chan.id) ||
(msg.guild && (!guild || msg.guild.id !== guild.id) && msg.guild.muted) ||
((!chan || msg.channel.id != chan.id) && msg.channel.muted)))
return;
//console.log(msg.member.highestRole.hexColor);
console.log(fmtmsg(msg));
unread[msg.channel.id] = 1 + (unread[msg.channel.id] || 0);
if(msg.author.id === client.user.id) delete unread[msg.channel.id];
last.got = msg;
});
client.on('messageUpdate', (oldmsg, newmsg) => {
if(oldmsg.content !== newmsg.content)
client.emit('message', newmsg);
});
rl.on('line', line => {
if(!line.startsWith(PREFIX)) {
if(!line.replace(/\s+/, '')) {
return;
}
if(!chan) {
console.log('Pick a channel first!');
return;
}
const sub = /^\s*s\/((?:[^\/]|\\\/)+)\/((?:[^\/]|\\\/)*)\/(\w*)\s*$/;
rl.write(null, { ctrl: true, name: 'u' });
if(last.sent && sub.test(line)) {
let pat, rep, flags, newstr;
line.replace(sub, (match, one, two, three) => {
[pat, rep, flags] = [one, two, three].map(word =>
word.replace(/(?<!\\)\\\//g, '/'));
});
try {
newstr = last.sent.content.replace(new RegExp(pat, flags), rep);
}
catch(e) {
console.log('Invalid substitution!');
return;
}
cmds.edit([newstr]);
return;
}
chan.send(fmtout(line))
.then(msg => last.sent = msg);
return;
}
const commands = line.split(new RegExp(`(?<!\\\\)${PREFIX}`))
for(let i=1; i<commands.length; i++) {
let args = commands[i]
.replace(new RegExp(`\\\\(\\\\|${PREFIX})`), '$1')
.split(/\s+/);
const cmd = args.shift();
if(!cmd) return;
if(!cmds[cmd]) {
console.log(`Unknown cmd ${cmd}.`);
return;
}
cmds[cmd](args);
if(chan && !client.user.bot) {
chan.acknowledge();
delete unread[chan.id];
}
}
});
rl.on('close', () => {
client.destroy();
console.log(`${PREFIX}quit`);
});
client.login(TOKEN || process.env.TOKEN);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment