Last active
October 21, 2018 17:12
-
-
Save Alwinfy/164e4b79ff054b0991e2244c01a09485 to your computer and use it in GitHub Desktop.
A Simple(?) CLI client for Discord.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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