Skip to content

Commit

Permalink
feat(cli): refactor to use yargs, implement seamail read
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjamin Reed committed Feb 11, 2019
1 parent 9c824da commit f2c837a
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 109 deletions.
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,13 @@
"webpack": "^4.29.3",
"webpack-cli": "^3.2.3",
"webpack-closure-compiler": "^2.1.6",
"yargs": "^12.0.0",
"yargs": "^12.0.5",
"yarn": "^1.13.0"
},
"dependencies": {
"@babel/runtime": "^7.3.1",
"axios": "^0.18.0",
"cli-table2": "^0.2.0",
"commander": "^2.19.0",
"lodash.clonedeep": "^4.5.0",
"lodash.startcase": "^4.4.0",
"moment": "^2.24.0",
Expand Down
283 changes: 177 additions & 106 deletions src/CLI.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import * as startCase from 'lodash.startcase';

import { API, Model, Rest, DAO, Client } from './API';
import { API, Rest, Client } from './API';
import { Util } from './internal/Util';
import { TwitarrError } from './api/TwitarrError';
import { User } from './model/User';

const fakeRequire = require('./__fake_require'); // tslint:disable-line

/** @hidden */
const CLI = () => {
const version = global.TWITARR_JS_VERSION || require('../package.json').version || 'unknown';

// tslint:disable
const Table = require('cli-table2');
const colors = require('colors');
const fs = require('fs');
const path = require('path');
const program = require('commander');
// tslint:enable
const Table = fakeRequire('cli-table2'); // tslint:disable-line variable-name
const colors = fakeRequire('colors');
const fs = fakeRequire('fs');
const path = fakeRequire('path');
const yargs = fakeRequire('yargs');

const homedir = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
const defaultConfigFile = path.join(homedir, '.twitarr.config.json');
Expand Down Expand Up @@ -47,8 +46,46 @@ const CLI = () => {
wordWrap: true,
};

const argv = yargs.usage('$0 <cmd> [args]')
.version(version)
.alias('c', 'config')
.describe('c', 'specify a configuration file (default: ~/.twitarr-config.json)')
.string('c')
.describe('debug', 'enable debug output')
.count('debug')
.command('connect <url> <user> <pass>', 'connect to a Twit-arr server')
.command('profile', 'read or edit your profile', (sub) => {
return sub
.alias('d', 'display-name')
.describe('d', 'set your display name')
.string('d')
.alias('h', 'home-location')
.describe('h', 'set your home location')
.string('h')
.alias('r', 'real-name')
.describe('r', 'set your real name')
.string('r')
.alias('p', 'pronouns')
.describe('p', 'set your preferred pronouns')
.string('p')
.alias('n', 'room-number')
.describe('r', 'set your room number')
.string('r');
})
.command('seamail', 'list, read, or create seamail threads', (sub) => {
return sub
.command('list', 'list seamail threads', (y) => {
return y
.alias('n', 'new')
.describe('n', 'list new threads and threads with new messages');
})
.command('read <id>', 'read a seamail thread')
.command('create <title> <users...>', 'create a new seamail thread');
})
.argv;

const readConfig = () => {
const configfile = program.config || defaultConfigFile;
const configfile = argv.config || defaultConfigFile;
let config;
if (fs.existsSync(configfile)) {
config = JSON.parse(fs.readFileSync(configfile));
Expand Down Expand Up @@ -78,7 +115,7 @@ const CLI = () => {
} else if (Object.prototype.toString.call(err) === '[object String]') {
realError = new API.TwitarrError(message + ': ' + err);
}
if (program.debug) {
if (argv.debug > 0) {
console.error(realError.message, realError);
} else {
console.error(realError.message);
Expand All @@ -88,111 +125,145 @@ const CLI = () => {

/* tslint:disable:no-console */

const oldDebug = console.debug;
console.debug = () => { }; // tslint:disable-line no-empty
if (argv.debug === 0) {
console.debug = () => { }; // tslint:disable-line no-empty
}

// global options
program
.option('-d, --debug', 'Enable debug output', () => {
console.debug = oldDebug;
})
.option('-c, --config <file>', 'Specify a configuration file (default: ~/.twitarr.config.json)')
.option('-v, --version', 'Print the twitarr.js version and exit', () => {
console.log(version);
process.exit(0);
});
const doConnect = async (url, username, password) => {
console.log(colors.red('WARNING: This command saves your login'
+ ' information to ~/.twitarr.config.json in clear text.'));
const config = readConfig();
if (url) {
// the user is passing a URL, reset the config
config.url = url;
config.key = undefined;
}
if (Util.isEmpty(username) || Util.isEmpty(password)) {
throw new TwitarrError('A username and password are required!');
}

// connect (validate server and save config)
program
.command('connect [url]')
.description('Connect to a Twitarr server')
.option('-u, --username <username>', 'The username to authenticate as')
.option('-p, --password <password>', 'The password to authenticate with')
.action((url, options) => {
console.log(colors.red('WARNING: This command saves your login'
+ ' information to ~/.twitarr.config.json in clear text.'));
const config = readConfig();
if (url) {
// the user is passing a URL, reset the config
config.url = url;
config.key = undefined;
}
if (Util.isEmpty(options.username) || Util.isEmpty(options.password)) {
throw new TwitarrError('A username and password are required!');
const auth = new API.TwitarrAuthConfig(username, password);
const server = new API.TwitarrServer('Twitarr', config.url, auth);
const http = new Rest.AxiosHTTP(server);

await Client.checkServer(server, http);
console.log(colors.green('* Server is valid.'));

const client = new Client(http);
const key = await client.connect('Twitarr', config.url, username, password);
console.log(colors.green('* Connected to ' + server.name + '.'));

console.warn('Saving configuration to ' + defaultConfigFile);
config.key = key;
fs.writeFileSync(defaultConfigFile, JSON.stringify(config, undefined, 2), { mode: 0o600 });

return config;
};

// tslint:disable-next-line max-line-length
const doProfile = async (displayName?: string, homeLocation?: string, realName?: string, pronouns?: string, roomNumber?: string) => {
const client = getClient();
if (Util.isEmpty(displayName, homeLocation, realName, pronouns, roomNumber)) {
const profile = await client.user().getProfile();
const t = new Table(tableFormat);
for (const key of Object.keys(profile)) {
const name = key? key.replace(/_/g, ' ') : key;
t.push([name + ':', profile[key]]);
}
console.log(t.toString());
console.log('');
} else {
throw new TwitarrError('Not yet implemented!');
}
};

const auth = new API.TwitarrAuthConfig(options.username, options.password);
const server = new API.TwitarrServer('Twitarr', config.url, auth);
const http = new Rest.AxiosHTTP(server);

return Client.checkServer(server, http).then(() => {
console.log(colors.green('Server is valid.'));
return new Client(http).connect('Twitarr', config.url, options.username, options.password)
.then((ret) => {
console.log(colors.green('Login succeeded.'));
config.key = http.getKey();
if (!program.config) { // don't write the config if a config was passed in
console.warn('Saving configuration to ' + defaultConfigFile);
fs.writeFileSync(defaultConfigFile, JSON.stringify(config, undefined, 2), { mode: 0o600 });
}
return ret;
});
}).catch((err) => {
return handleError('Server check failed', err);
});
});
const doSeamailList = async (n = false) => {
const client = getClient();
const seamail = await client.seamail().getMetadata(n);

program.command('profile')
.description('Read or edit your profile')
.option('-d, --display-name <display-name>', 'Set your display name')
.option('-e, --email <email>', 'Set your email address')
.option('-h, --home <home-location>', 'Set your home location')
.option('-r, --real-name <real-name>', 'Set your real name')
.option('-p, --pronouns <pronouns>', 'Set your pronouns')
.option('-n, --room-number <room-number>', 'Set your room number')
.action((options) => {
const client = getClient();
if (options.length > 0) {
console.log('setting options:', options);
throw new TwitarrError('Not yet implemented!');
} else {
return client.user().getProfile().then((profile) => {
const t = new Table(tableFormat);
for (const key of Object.keys(profile)) {
const name = key? key.replace(/_/g, ' ') : key;
t.push([name + ':', profile[key]]);
}
console.log(t.toString());
console.log('');
});
const format = Object.assign({ }, tableFormat);
format.head = [ 'ID', 'Subject', 'Last Updated'];
if (!n) {
format.head.unshift('New');
}
const t = new Table(format);

for (const thread of seamail.threads) {
const row = [thread.id, thread.subject, thread.timestamp.fromNow()];
if (!n) {
row.unshift(thread.is_unread? '*' : '');
}
t.push(row);
}

console.log(t.toString());
console.log('');
};

const getUserString = (user: User) => {
return Util.isEmpty(user.display_name)? '@' + user.username : user.display_name + ' (@' + user.username + ')';
};

const doSeamailRead = async (id: string) => {
const client = getClient();
const seamail = await client.seamail().get(id);
const format = Object.assign({ }, tableFormat);

let t = new Table(tableFormat);

const thread = seamail.threads[0];
t.push(['Subject:', thread.subject]);
t.push(['Last Updated:', thread.timestamp.fromNow()]);
t.push(['Participants:', thread.users.map((user) => user.toString()).join('\n')]);
console.log(t.toString());
console.log('');
t = new Table(tableFormat);
thread.messages.forEach((message) => {
t.push([message.author.getDisplayName(), message.text, message.timestamp.fromNow()]);
});
console.log(t.toString());
console.log('');
};

program.command('seamail <command>')
.description('read or post seamail messages')
.action((command, options) => {
const client = getClient();
if (command === 'list') {
return client.seamail().getMetadata().then((seamail) => {
const format = Object.assign({ }, tableFormat);
format.head = [ 'Unread', 'ID', 'Subject', 'Last Updated'];
const t = new Table(format);
for (const thread of seamail.threads) {
t.push([thread.is_unread? '*' : '', thread.id, thread.subject, thread.timestamp.fromNow()]);
const processArgs = async (args) => {
try {
switch (args._[0]) {
case 'connect': {
await doConnect(args.url, args.user, args.pass);
break;
}
case 'profile': {
await doProfile(args.displayName, args.homeLocation, args.realName, args.pronouns, args.roomNumber);
break;
}
case 'seamail': {
const command = args._[1];
switch (command) {
case 'list': {
await doSeamailList(args.new);
break;
}
case 'read': {
await doSeamailRead(args.id);
break;
}
default: throw new TwitarrError('Unhandled seamail command: ' + command);
}
console.log(t.toString());
console.log('');
});
break;
}
default: {
console.error('Unhandled argument:', args);
throw new TwitarrError('Unhandled argument: ' + args._[0]);
}
}
throw new TwitarrError('Unhandled command: seamail ' + command);
});

program.parse(process.argv);
process.exit(0);
} catch (err) {
console.log(colors.red('Failed: ' + err.message));
process.exit(1);
}
};

if (!process.argv.slice(2).length) {
program.outputHelp();
process.exit(0);
}
processArgs(argv);
};

process.on('unhandledRejection', (reason, p) => {
Expand Down
2 changes: 2 additions & 0 deletions src/__fake_require.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
module.exports = typeof __webpack_require__ !== 'undefined' ? __webpack_require__ : eval('require');

0 comments on commit f2c837a

Please sign in to comment.