Skip to content

Commit

Permalink
fix(adapter): Inherit settings changes after start
Browse files Browse the repository at this point in the history
- Moved adapters to singleton pattern to accept changes to settings outside adapter
- Fixed method and tests for restoring settings after extending
  • Loading branch information
timkinnane committed Aug 31, 2018
1 parent fba4de6 commit 1464b40
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 75 deletions.
22 changes: 17 additions & 5 deletions src/adapters/mongo.ts
@@ -1,4 +1,4 @@
import { StorageAdapter } from '..'
import * as bBot from '..'
import mongoose from 'mongoose'

export interface IStore extends mongoose.Document {
Expand Down Expand Up @@ -27,7 +27,7 @@ export function getModel (collection: string) {
* Long-term data is stored in sub-collections alongside memory, using either
* a key for key/value pairs, or a key-less array for serial data.
*/
export class Mongo extends StorageAdapter {
export class Mongo extends bBot.StorageAdapter {
name = 'mongo-storage-adapter'
config = {
useNewUrlParser: true,
Expand All @@ -40,8 +40,20 @@ export class Mongo extends StorageAdapter {
model: mongoose.Model<mongoose.Document>
store?: mongoose.Mongoose

/** Create adapter instance with ref to bot instance */
constructor (bot: any) {
/** Singleton pattern instance */
private static instance: Mongo

/** Singleton instance init */
static getInstance (bot: typeof bBot) {
if (!Mongo.instance) Mongo.instance = new Mongo(bot)
return Mongo.instance
}

/**
* Create adapter instance with ref to bot instance.
* Prevent direct access to constructor for singleton adapter
*/
constructor (bot: typeof bBot) {
super(bot)
this.bot.settings.extend({
'db-url': {
Expand Down Expand Up @@ -164,4 +176,4 @@ export class Mongo extends StorageAdapter {
}
}

export const use = (bot: any) => new Mongo(bot)
export const use = (bot: any) => Mongo.getInstance(bot)
54 changes: 33 additions & 21 deletions src/adapters/rocketchat.ts
@@ -1,20 +1,32 @@
import * as bot from '..'
import * as bBot from '..'
import * as sdk from '@rocket.chat/sdk'

/**
* Rocket.Chat adapter processes incoming message stream, providing the
* their this modules as attributes for advanced branch callbacks to use.
* Provides member alias to some SDK methods, to support legacy Hubot scripts.
*/
export class Rocketchat extends bot.MessageAdapter {
export class Rocketchat extends bBot.MessageAdapter {
name = 'rocketchat-message-adapter'
driver = sdk.driver
methodCache = sdk.methodCache
api = sdk.api
settings = sdk.settings

/** Create Rocket.Chat adapter, configure bot to use username as alias */
constructor (bot: any) {
/** Singleton pattern instance */
private static instance: Rocketchat

/** Singleton instance init */
static getInstance (bot: typeof bBot) {
if (!Rocketchat.instance) Rocketchat.instance = new Rocketchat(bot)
return Rocketchat.instance
}

/**
* Create Rocket.Chat adapter, configure bot to use username as alias.
* Prevent direct access to constructor for singleton adapter
*/
private constructor (bot: typeof bBot) {
super(bot)
this.settings.integrationId = 'bBot'
if (this.settings.username !== this.bot.settings.name) this.bot.settings.alias = this.settings.username
Expand All @@ -26,9 +38,9 @@ export class Rocketchat extends bot.MessageAdapter {
/** Connect to Rocket.Chat via DDP driver and setup message subscriptions */
async start () {
this.bot.logger.info(`[rocketchat] responds to name: ${this.bot.settings.name}`)
if (this.bot.settings.alias) bot.logger.info(`[rocketchat] responds to alias: ${this.bot.settings.alias}`)
if (this.bot.settings.alias) this.bot.logger.info(`[rocketchat] responds to alias: ${this.bot.settings.alias}`)

this.driver.useLog(bot.logger)
this.driver.useLog(this.bot.logger)
await this.driver.connect()
await this.driver.login()
await this.driver.subscribeToMessages()
Expand All @@ -47,7 +59,7 @@ export class Rocketchat extends bot.MessageAdapter {
this.bot.logger.info('[rocketchat] filters passed, will hear message')
const isDM = (meta.roomType === 'd')
const isLC = (meta.roomType === 'l')
const user = bot.userById(message.u._id, {
const user = this.bot.userById(message.u._id, {
fullName: message.u.name,
name: message.u.username,
room: {
Expand All @@ -59,34 +71,34 @@ export class Rocketchat extends bot.MessageAdapter {

// Room joins, hear without further detail
if (message.t === 'uj') {
bot.logger.debug('[rocketchat] hear type EnterMessage')
return bot.receive(new bot.EnterMessage(user, message._id))
this.bot.logger.debug('[rocketchat] hear type EnterMessage')
return this.bot.receive(new bBot.EnterMessage(user, message._id))
}

// Room exit, hear without further detail
if (message.t === 'ul') {
bot.logger.debug('[rocketchat] hear type LeaveMessage')
return bot.receive(new bot.LeaveMessage(user, message._id))
this.bot.logger.debug('[rocketchat] hear type LeaveMessage')
return this.bot.receive(new bBot.LeaveMessage(user, message._id))
}

// Direct messages prepend bot's name so bBot can respond directly
const startOfText = (message.msg.indexOf('@') === 0) ? 1 : 0
const robotIsNamed = message.msg.indexOf(bot.settings.name) === startOfText || message.msg.indexOf(bot.settings.alias) === startOfText
if ((isDM || isLC) && !robotIsNamed) message.msg = `${bot.settings.name} ${message.msg}`
const robotIsNamed = message.msg.indexOf(this.bot.settings.name) === startOfText || message.msg.indexOf(this.bot.settings.alias) === startOfText
if ((isDM || isLC) && !robotIsNamed) message.msg = `${this.bot.settings.name} ${message.msg}`

// Attachments, format properties as payload for bBot rich message type
if (Array.isArray(message.attachments) && message.attachments.length) {
bot.logger.debug('[rocketchat] hear type RichMessage')
return bot.receive(new bot.RichMessage(user, {
this.bot.logger.debug('[rocketchat] hear type RichMessage')
return this.bot.receive(new bBot.RichMessage(user, {
attachments: message.attachments,
text: message.text
}, message._id))
}

// Standard text messages, hear as is
let textMessage = new bot.TextMessage(user, message.msg, message._id)
bot.logger.debug(`[rocketchat] hear type TextMessage: ${textMessage.toString()}`)
return bot.receive(textMessage)
let textMessage = new bBot.TextMessage(user, message.msg, message._id)
this.bot.logger.debug(`[rocketchat] hear type TextMessage: ${textMessage.toString()}`)
return this.bot.receive(textMessage)
}

/** Parse any strings before sending to fix for Rocket.Chat syntaxes */
Expand All @@ -95,7 +107,7 @@ export class Rocketchat extends bot.MessageAdapter {
}

/** Parsing envelope content to an array of Rocket.Chat message schemas */
parseEnvelope (envelope: bot.Envelope, roomId?: string) {
parseEnvelope (envelope: bBot.Envelope, roomId?: string) {
const messages: any[] = []
const attachments: any[] = []
const actions: any[] = []
Expand Down Expand Up @@ -163,7 +175,7 @@ export class Rocketchat extends bot.MessageAdapter {
}

/** Dispatch envelope content, mapped to Rocket.Chat SDK methods */
async dispatch (envelope: bot.Envelope) {
async dispatch (envelope: bBot.Envelope) {
switch (envelope.method) {
case 'send':
if (!envelope.room || !envelope.room.id) {
Expand Down Expand Up @@ -206,4 +218,4 @@ export class Rocketchat extends bot.MessageAdapter {
}
}

export const use = (bot: any) => new Rocketchat(bot)
export const use = (bot: any) => Rocketchat.getInstance(bot)
32 changes: 29 additions & 3 deletions src/adapters/shell.spec.ts
@@ -1,13 +1,39 @@
import 'mocha'
import { expect } from 'chai'
import * as bot from '..'
import * as shell from './shell'
import * as shellAdapter from './shell'

let initEnv: any

describe('[adapter-shell]', () => {
before(() => {
initEnv = process.env
process.env.BOT_NAME = 'aston'
process.env.BOT_SHELL_USER = 'carroll'
process.env.BOT_SHELL_ROOM = 'shell'
})
after(() => process.env = initEnv)
afterEach(() => bot.reset())
describe('.use', () => {
it('returns adapter instance', () => {
const store = shell.use(bot)
expect(store).to.be.instanceof(bot.Adapter)
const shell = shellAdapter.use(bot)
expect(shell).to.be.instanceof(bot.Adapter)
})
it('accepts changes in bot settings before startup', async () => {
bot.settings.set('name', 'shelby')
const shell = shellAdapter.use(bot)
shell.debug = true
bot.start()
await new Promise((resolve) => bot.events.on('shell-started', resolve))
expect(shell.bot.settings.name).to.equal('shelby')
})
it('accepts changes in bot settings after startup', async () => {
const shell = shellAdapter.use(bot)
shell.debug = true
bot.settings.name = 'not-shelby'
await bot.start().catch((err) => { throw err })
bot.settings.name = 'shelby'
expect(shell.bot.settings.name).to.equal('shelby')
})
})
})
105 changes: 70 additions & 35 deletions src/adapters/shell.ts
Expand Up @@ -6,27 +6,35 @@ import chalk from 'chalk'
/** Load prompts and render chat in shell, for testing interactions */
export class Shell extends bBot.MessageAdapter {
name = 'shell-message-adapter'
debug: boolean = false
ui: any
logs: string[] = ['']
messages: [string, string][] = []
line = new inquirer.Separator()
settings = {
chatSize: 5
}
// @todo extend bot settings instead...
userName = process.env.BOT_SHELL_USER_NAME
userId = process.env.BOT_SHELL_USER_ID
roomName = process.env.BOT_SHELL_ROOM
transport?: Transport
user?: bBot.User
room?: { id?: string, name?: string }

/** Singleton pattern instance */
private static instance: Shell

/** Prevent direct access to constructor for singleton adapter */
private constructor (bot: typeof bBot) {
super(bot)
}

/** Singleton instance init */
static getInstance (bot: typeof bBot) {
if (!Shell.instance) Shell.instance = new Shell(bot)
return Shell.instance
}

/** Update chat window and return to input prompt */
async render () {
let _ = '\n'
let n = ' '
_ += chalk.cyan('╔═════════════════════════════════════════════════════════▶') + '\n'
for (let m of this.messages.slice(-this.settings.chatSize)) {
for (let m of this.messages.slice(-this.bot.settings.get('shell-size'))) {
_ += chalk.cyan(`║${n.substr(0, n.length - m[0].length) + m[0]} ┆ `) + m[1] + '\n'
}
_ += chalk.cyan('╚═════════════════════════════════════════════════════════▶') + '\n\n'
Expand Down Expand Up @@ -60,12 +68,51 @@ export class Shell extends bBot.MessageAdapter {
callback()
}

/** Register user and room, then render chat with welcome message */
async start () {
this.bot.settings.extend({
'shell-user-name': {
type: 'string',
description: 'Pre-filled username for user in shell chat session.'
},
'shell-user-id': {
type: 'string',
description: 'ID to persist shell user data (or set as "random").',
default: 'shell-user-01'
},
'shell-room-name': {
type: 'string',
description: 'Name for "room" of shell chat session.'
},
'shell-size': {
type: 'number',
description: 'Number of message lines to display in shell chat.',
default: 5
}
})
this.ui = new inquirer.ui.BottomBar()
this.bot.global.enter((b) => b.respond(
`${this.user!.name} Welcome to #${this.room!.name}, I'm ${b.bot.settings.get('name')}`,
`Type "exit" to exit any time.`
), { id: 'shell-enter' })
this.bot.global.text(/^exit$/i, (b) => b.bot.shutdown(1), { id: 'shell-exit' })
this.bot.events.on('started', async () => {
if (!this.debug) {
this.logSetup()
await this.roomSetup()
await this.bot.receive(new this.bot.EnterMessage(this.user!))
await this.render()
}
this.bot.events.emit('shell-started')
})
}

/** Write prompt to collect room and user name, or take from env settings */
async roomSetup () {
if (this.userName && this.roomName) {
this.user = new this.bot.User({ name: this.userName, id: this.userId })
this.room = { name: this.roomName }
} else {
if (
!this.bot.settings.get('shell-user-name') ||
!this.bot.settings.get('shell-room-name')
) {
const registration: any = await inquirer.prompt([{
type: 'input',
name: 'username',
Expand All @@ -74,37 +121,25 @@ export class Shell extends bBot.MessageAdapter {
},{
type: 'input',
name: 'userId',
message: 'Use ID for user, or generate random?',
message: 'Use ID for user - enter "random" to generate',
default: 'random'
},{
type: 'input',
name: 'room',
message: 'And what about this "room"?',
default: 'shell'
}])
if (registration.userId !== 'random') this.userId = registration.userId
this.user = new this.bot.User({
name: registration.username,
id: this.userId
})
this.room = { name: registration.room }
if (registration.userId !== 'random') {
this.bot.settings.set('shell-user-id', registration.userId)
}
this.bot.settings.set('shell-user-name', registration.username)
this.bot.settings.set('shell-room-name', registration.room)
}
}

/** Register user and room, then render chat with welcome message */
async start () {
this.ui = new inquirer.ui.BottomBar()
this.bot.global.enter((b) => b.respond(
`${this.user!.name} Welcome to #${this.room!.name}, I'm ${b.bot.settings.name}`,
`Type "exit" to exit any time.`
), { id: 'shell-enter' })
this.bot.global.text(/^exit$/i, (b) => b.bot.shutdown(1), { id: 'shell-exit' })
this.bot.events.on('started', async () => {
this.logSetup()
await this.roomSetup()
await this.bot.receive(new this.bot.EnterMessage(this.user!))
await this.render()
this.user = new this.bot.User({
name: this.bot.settings.get('shell-user-name'),
id: this.bot.settings.get('shell-user-id')
})
this.room = { name: this.bot.settings.get('shell-room-name') }
}

/** Prompt for message input, recursive after each render */
Expand Down Expand Up @@ -144,4 +179,4 @@ export class Shell extends bBot.MessageAdapter {
}
}

export const use = (bot: typeof bBot) => new Shell(bot)
export const use = (bot: typeof bBot) => Shell.getInstance(bot)
5 changes: 2 additions & 3 deletions src/lib/adapter-classes/message.spec.ts
Expand Up @@ -2,15 +2,14 @@ import 'mocha'
import sinon from 'sinon'
import * as bot from '../..'
import { expect } from 'chai'
import { MessageAdapter } from './message'

let log: sinon.SinonSpy
let mockAdapter: bot.MessageAdapter

describe('[adapter-message]', () => {
before(() => {
log = sinon.spy(bot.logger, 'debug')
class MockAdapter extends MessageAdapter {
class MockAdapter extends bot.MessageAdapter {
name = 'mock-message-adapter'
async start () { return }
async shutdown () { return }
Expand All @@ -22,7 +21,7 @@ describe('[adapter-message]', () => {
after(() => log.restore())
describe('MessageAdapter', () => {
it('allows extending', () => {
expect(mockAdapter).to.be.instanceof(MessageAdapter)
expect(mockAdapter).to.be.instanceof(bot.MessageAdapter)
})
})
})

0 comments on commit 1464b40

Please sign in to comment.