contact
Version:
private, one-to-one or many-to-many command line chat
132 lines (117 loc) • 3.92 kB
JavaScript
import Client from 'contact'
import CliView from '../lib/cli-view.js'
import Storage from './storage.js'
import { streamReadText } from 'stream-read-all'
import Transmission from 'contact-model/transmission'
class CliApp {
client
storage
view
allCommands = [
'/help',
'/users',
'/rooms',
'/clientSessions',
'/roomSessions',
'/members',
'/nick',
'/join',
'/send'
]
constructor (options = {}) {
this.client = options.client || new Client({ Connection: options.Connection })
this.storage = options.storage || new Storage()
this.view = options.view || new CliView()
}
async start (url, initialCommands) {
const client = this.client
const storage = this.storage
const view = this.view
try {
this.connection = await client.connect(url)
} catch (err) {
if (err?.cause?.code === 'ABORT_ERR') {
console.error('Connection attempt timed out')
} else if (['ERR_INVALID_URL', 'ECONNREFUSED'].includes(err?.cause?.code || err?.code)) {
console.error(`Connection failed, check the URL and that the server is online. [${err?.cause?.code || err?.code}]`)
} else {
console.error(err)
}
await this.connection?.close()
await client.close()
return
}
await storage.init()
view.init({
input: process.stdin,
output: process.stdout,
history: await storage.readHistory(),
completer: line => {
const hits = this.allCommands.filter(c => c.startsWith(line))
return [hits.length ? hits : this.allCommands, line]
}
})
client.addEventListener('close', async e => {
const cause = e.detail
await view.displayMessage(`Connection closed, cause: ${cause}`, { skipPrompt: true })
view.close()
// console.log('Connection to relay closed, shutting down.')
})
client.addEventListener('msg', async e => {
const t = e.detail
if (t.headers.unread) {
await storage.writeUnread(t)
const nt = Transmission.notification('unread-message', t.headers['client-room-session-id'], t.headers['room-id'], t.headers.from)
await view.displayNotification(nt.headers)
} else {
const msg = await streamReadText(t.readable)
await view.displayMessage(`${t.headers.from}: ${msg}`)
}
})
client.addEventListener('notification', async e => {
await view.displayNotification(e.detail)
})
view.addEventListener('line', async e => {
this.onUserInput(e.detail)
})
view.addEventListener('close', async () => {
await this.connection.close()
await client.close()
})
view.addEventListener('history', async e => {
await storage.writeHistory(e.detail)
})
for (const command of initialCommands) {
const res = await this.connection.tui(command.join(' '))
view.displayMessage(res)
}
}
async onUserInput (line) {
if (line.startsWith('/')) {
const res = await this.connection.tui(line)
if (res.status === 200) {
if (res.command.startsWith('/join')) {
const roomId = res.command.split(/\s+/)[1]
this.view.setRoomId(roomId)
const unreadTransmissions = await this.storage.readUnread(roomId)
for (const t of unreadTransmissions) {
const msg = await streamReadText(t.readable)
this.view.displayMessage(`${t.headers.from}: ${msg}`)
}
}
this.view.displayMessage(res)
} else {
this.view.displayMessage('Command failed')
this.view.displayMessage(res)
}
} else {
const res = await this.connection.tui('/send', Transmission.msg(line))
if (res.status === 200) {
this.view.displayMessage(`${res.body.clientSession.nick}: ${line}`)
} else {
this.view.displayMessage(res)
}
}
}
}
export default CliApp