#!/usr/bin/env node (Symbol).asyncIterator = Symbol.asyncIterator || Symbol.for("Symbol.asyncIterator"); import * as yargs from 'yargs' import * as readline from 'readline' import * as fs from 'fs' import * as path from'path' import * as http from 'http' import * as assert from'assert' import * as request from 'request' import { parse, format } from 'url' const styles = require('ansi-styles') const EventSource = require('eventsource') const _mkdirp = require('mkdirp') const prompt = require('prompt-promise'); const { HOME } = process.env function mkdirp(dir:any) { return new Promise((pass, fail) => { _mkdirp(dir, (err:any) => { if (err) return fail(err) pass() }) }) } class VirtualIrlApi { public name: string public apiKey: string public apiName: string constructor( public readonly serverUrl: string, public readonly request: any, ) {} private _req( path: string, method: string = 'GET', body: any = undefined, ): Promise { const urls = parse(this.serverUrl) urls.pathname = path return new Promise((pass,fail) => { const url = format(urls) this.request({url, method, json:true, body, headers: { 'API-Key': this.apiKey, 'API-Name': this.apiName, }}, (err:any, res:any, body:any) => { if (err) return fail(err) if (res.statusCode >= 400) { return fail(new Error(`Unexpected Status Code: ${res.statusCode}`)) } pass(body) }) }) } async hello(): Promise { const {error, name} = await this._req(`/home/hello`) if (error || !name) { throw new Error(`You are not logged in`) } return name } async getMessages(): Promise<{messages:any[]}> { return this._req(`/home/messages`, 'GET') } async createAccount(name: string, password: string, signupcode: string): Promise { await this._req(`/account/create`, 'POST', {name, password, signupcode}) console.log(`Account created`) } async authPassword(name: string, password: string): Promise { this.apiKey = '' this.apiName = '' const {key} = await this._req(`/auth/password`, 'POST', {name, password}) this.apiKey = key this.apiName = name fs.writeFileSync(`${HOME}/.virl/config.json`, JSON.stringify({name, key}), 'utf-8') } async * listenStdio(): AsyncIterableIterator { const events = new EventSource(`${this.serverUrl}/home/stdout`, { headers: { 'API-Key': this.apiKey, 'API-Name': this.apiName, } }) var next: () => void const buff:any = [] events.on('message', function ({data}: any) { buff.push(JSON.parse(data)) if (next) next() }) var item while(true) { while(item = buff.shift()) { yield item } await new Promise(ok => next = ok) } } async * listenEvents(): AsyncIterableIterator { const events = new EventSource(`${this.serverUrl}/home/events`, { headers: { 'API-Key': this.apiKey, 'API-Name': this.apiName, } }) var next: () => void const buff:any = [] events.on('message', function ({data}: any) { buff.push(JSON.parse(data)) if (next) next() }) var item while(true) { while(item = buff.shift()) { yield item } await new Promise(ok => next = ok) } } async submitCommand(script: string): Promise { const {exitCode} = await this._req(`/home/eval`, 'POST', {script}) return parseInt(exitCode) } } const blessed = require('blessed') async function connect(api: VirtualIrlApi) { const screen = blessed.screen({ smartCSR: true, cursor: { blink: true }, dockBorders: true, }) screen.title = 'Virtual IRL'; const events = blessed.log({ top: '0', left: "0", width: '50%', height: '100%-3', tags: true, border: { type: 'line' }, label: "Virtual IRL" }) const chat = blessed.log({ left: "50%", width: '50%', height: '100%-3', tags: true, border: { type: 'line' }, label: "Room" }) const input = blessed.textbox({ bottom: '0', left: '0 ', width: '100%', height: 'shrink', bg: 'black', border: { type: 'line' }, label: "Input - type (quit) to exit" }) screen.append(events) screen.append(input) screen.append(chat) function llog(msg:string) { events.log(msg) } function getCli() { input.readInput(async (err:any, line:any) => { if (line === 'quit') process.exit(0) if (line.startsWith('.')) { input.clearValue() return await api.submitCommand(line) } input.clearValue() events.setText('') if (!line) return getCli() llog(styles.color.gray.open + `> ${line}` + styles.color.gray.close) try { await api.submitCommand(line) } catch(e) { llog(e.message) getCli() } }) } getCli() // Render the screen. screen.render() const co = async function() { for await(const event of api.listenEvents()) { const {messages} = await api.getMessages() for(const {body, type} of messages) { chat.log(body) } } }() for await(const {type, body} of api.listenStdio()) { switch(type) { case 'info': { llog(styles.inverse.open + body + styles.inverse.close) break } case 'warn': { llog(styles.color.yellow.open + body + styles.color.yellow.close) break } case 'value': { llog(styles.color.blueBright.open + body + styles.color.blueBright.close) break } case 'abort': { llog(styles.color.red.open + body + styles.color.red.close) break } case 'done': { getCli() break } default: llog(`[${type}] ${body}`) break } } } async function hello(api: VirtualIrlApi) { const name = await api.hello() console.log(`You are Authenticated as ${name}`) } async function auth(api: VirtualIrlApi, user:string, pass:string) { try { const name = await api.authPassword(user, pass) } catch(e) { console.error(e.message) } } async function signup(api: VirtualIrlApi) { const user = await prompt('username: ') const pass = await prompt.password('password: ') const code = await prompt.password('access code: ') console.log(`Creating account for ${user}`) await api.createAccount(user, pass, code) return {user, pass} } async function commander(api: VirtualIrlApi, argv: any) { const [cmd, ...args] = argv._ switch(cmd) { case 'signup': { const {user, pass} = await signup(api) await auth(api, user, pass) await hello(api) return } case 'login': { await hello(api) return connect(api) } case 'hello': { return hello(api) } case 'auth': { const {username} = argv assert.ok(username, 'username required') await auth(api, username, await prompt.password('password: ')) await hello(api) return } default: throw new Error(`Unknown Command ${cmd}`) } } async function main() { await mkdirp(`${HOME}/.virl/`) const argv = yargs .command('auth ', 'login to virtual irl') .command('hello', 'check if you are logged in') .command('login', 'enter Virtual IRL') .command('signup', 'create a new a ccount') .option('V', { alias: 'verbose', desc: 'verbose', boolean: true, default: false, }) .demandCommand(1) .help() .argv const { VIRL_URL = `https://virtualirl.herokuapp.com`, } = process.env console.error(`Connecting to ${VIRL_URL}`) const api = new VirtualIrlApi(VIRL_URL, request) try { const {key, name} = JSON.parse(fs.readFileSync(`${HOME}/.virl/config.json`, 'utf-8')) api.apiKey = process.env.VIRL_KEY || key api.apiName = process.env.VIRL_NAME || name } catch (e) { // } try { await commander(api, argv) } catch(e) { if (argv.V) console.error(e) console.log(`There was an error.`) console.log(`Try the same command with -V for more info`) } } main() .catch(e => { console.error(e.stack) console.error(e) })