Skip to content

Commit

Permalink
feat(respond): Outgoing envelope handling
Browse files Browse the repository at this point in the history
- Refactor all user attributes into `meta` arg
- Add envelope class and create methods
- Add room atts to envelope and user
- Refactor imports to be consistent
- Rename `bot` module to `core`
- Limit bot attributes exported in `b` state
- Remove @module doc tags
  • Loading branch information
timkinnane committed May 21, 2018
1 parent bd98c0d commit 631a5f9
Show file tree
Hide file tree
Showing 26 changed files with 392 additions and 312 deletions.
2 changes: 2 additions & 0 deletions md/HubotMigration.md
@@ -1,5 +1,7 @@
[thought]: ./ThoughtProcess.md

Add note this a guide for script developers. To see a feature comparison, go here ... <LINK TO ENGINEERING SECTION>

Requiring/importing the bot...

Async all the things
Expand Down
4 changes: 1 addition & 3 deletions src/index.ts
@@ -1,5 +1,3 @@
/** @module bBot */

export * from './lib/argv'
export * from './lib/events'
export * from './lib/logger'
Expand All @@ -12,5 +10,5 @@ export * from './lib/user'
export * from './lib/message'
export * from './lib/listen'
export * from './lib/bit'
export * from './lib/bot'
export * from './lib/core'
export * from './lib/thought-process'
12 changes: 6 additions & 6 deletions src/lib/adapter-classes/message.spec.ts
Expand Up @@ -43,12 +43,6 @@ describe('message adapter', () => {
sinon.assert.calledWithMatch(log, /emote/, { strings: ['testing'] })
})
})
describe('.emote', () => {
it('logs debug', async () => {
await mockAdapter.emote({ user: { name: 'tester' } }, 'testing')
sinon.assert.calledWithMatch(log, /emote/, { strings: ['testing'] })
})
})
describe('.topic', () => {
it('logs debug', async () => {
await mockAdapter.topic({ user: { name: 'tester' } }, 'testing')
Expand All @@ -67,4 +61,10 @@ describe('message adapter', () => {
sinon.assert.calledWithMatch(log, /play/, { strings: ['testing'] })
})
})
describe('.react', () => {
it('logs debug', async () => {
await mockAdapter.react({ user: { name: 'tester' } }, 'testing')
sinon.assert.calledWithMatch(log, /react/, { strings: ['testing'] })
})
})
})
18 changes: 12 additions & 6 deletions src/lib/adapter-classes/message.ts
@@ -1,4 +1,5 @@
import { Adapter } from './base'
import * as bot from '../..'

/**
* Message Adapter class, extended to connect bBot with messaging platform.
Expand All @@ -9,22 +10,27 @@ export abstract class MessageAdapter extends Adapter {
async receive (message: any, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `receive` called without override', { message, strings })
}
async send (envelope: any, ...strings: string[]): Promise<any> {
async send (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `send` called without override', { envelope, strings })
}
async reply (envelope: any, ...strings: string[]): Promise<any> {
async reply (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `reply` called without override', { envelope, strings })
}
async emote (envelope: any, ...strings: string[]): Promise<any> {
async emote (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `emote` called without override', { envelope, strings })
}
async topic (envelope: any, ...strings: string[]): Promise<any> {
async topic (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `topic` called without override', { envelope, strings })
}
async notify (envelope: any, ...strings: string[]): Promise<any> {
async notify (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `notify` called without override', { envelope, strings })
}
async play (envelope: any, ...strings: string[]): Promise<any> {
async play (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `play` called without override', { envelope, strings })
}
async react (envelope: bot.Envelope, ...strings: string[]): Promise<any> {
this.bot.logger.debug('Message adapter `react` called without override', { envelope, strings })
}
}

export type MessageMethod = 'send' | 'reply' | 'emote' | 'topic' | 'notify' | 'play' | 'react'
3 changes: 2 additions & 1 deletion src/lib/adapter.spec.ts
Expand Up @@ -4,12 +4,13 @@ import { expect } from 'chai'
import { config } from './argv'
import { Adapter } from './adapter-classes/base'
import * as adapter from './adapter'
import * as bot from '..'

class MockAdapter extends Adapter {
name = 'mock-adapter'
async start () { /* mock start */ }
}
const mockAdapter = new MockAdapter()
const mockAdapter = new MockAdapter(bot)
const start = sinon.spy(mockAdapter, 'start')
export const use = sinon.spy(() => mockAdapter)

Expand Down
29 changes: 11 additions & 18 deletions src/lib/adapter.ts
@@ -1,11 +1,4 @@
/**
* @module adapter
* Exports the base class for each adapter type.
* These provide structure for extensions but do nothing internally.
*/
import path from 'path'
import { logger } from './logger'
import { config } from './argv'
import * as bot from '..'

/** Collection of adapter (loose) types and their loaded adapter. */
Expand All @@ -15,44 +8,44 @@ export const adapters: { [key: string]: any | null } = {}
* Require adapter module.
* Resolves local path or NPM package.
* If local path given, attempt to resolve a number of possible locations in
* case bbot running from tests or inherited as sub-module etc.
* case bBot running from tests or inherited as sub-module etc.
*/
export function loadAdapter (adapterPath?: string) {
if (!adapterPath) return null
if (/^(\/|\.|\\)/.test(adapterPath)) {
let modulePath = 'node_modules/bbot/dist'
let sourcePath = 'src'
let modulePath = 'node_modules/bbot/dist'
let mainPath = path.dirname(require.main!.filename)
let mainModule = path.resolve(mainPath, modulePath)
let currentPath = process.cwd()
let currentModule = path.resolve(currentPath, modulePath)
let resolver = {
paths: [ mainPath, mainModule, currentPath, currentModule, sourcePath ]
paths: [ sourcePath, mainPath, mainModule, currentPath, currentModule ]
}
adapterPath = require.resolve(adapterPath, resolver)
}
logger.debug(`Loading adapter from ${adapterPath}`)
bot.logger.debug(`Loading adapter from ${adapterPath}`)
return require(adapterPath).use(bot)
}

/** Load all adapters, but don't yet start them. */
export function loadAdapters () {
adapters.message = loadAdapter(config.messageAdapter)
adapters.language = loadAdapter(config.languageAdapter)
adapters.storage = loadAdapter(config.storageAdapter)
adapters.webhook = loadAdapter(config.webhookAdapter)
adapters.analytics = loadAdapter(config.analyticsAdapter)
adapters.message = loadAdapter(bot.config.messageAdapter)
adapters.language = loadAdapter(bot.config.languageAdapter)
adapters.storage = loadAdapter(bot.config.storageAdapter)
adapters.webhook = loadAdapter(bot.config.webhookAdapter)
adapters.analytics = loadAdapter(bot.config.analyticsAdapter)
}

/** Start each adapter concurrently, to resolve when all ready. */
export function startAdapters () {
return Promise.all(Object.keys(adapters).map((type) => {
let adapter = adapters[type]
if (adapter) {
logger.debug(`Starting ${type} adapter ${adapter.name}`)
bot.logger.debug(`Starting ${type} adapter ${adapter.name}`)
return Promise.resolve(adapter.start())
} else {
logger.debug(`No ${type} adapter defined`)
bot.logger.debug(`No ${type} adapter defined`)
}
}))
}
Expand Down
2 changes: 0 additions & 2 deletions src/lib/argv.ts
@@ -1,5 +1,3 @@
/** @module argv */

import * as yargs from 'yargs'
import * as packageJSON from '../../package.json'

Expand Down
4 changes: 2 additions & 2 deletions src/lib/bit.spec.ts
@@ -1,11 +1,11 @@
import 'mocha'
import sinon from 'sinon'
import { expect } from 'chai'
import * as bot from '..'
import * as bit from './bit'
import * as bot from '..'

// Mock for initial state object
const message = new bot.TextMessage(new bot.User('test-user'), 'foo')
const message = new bot.TextMessage(new bot.User({ id: 'test-user' }), 'foo')

describe('bit', () => {
describe('Bit', () => {
Expand Down
22 changes: 10 additions & 12 deletions src/lib/bit.ts
@@ -1,6 +1,4 @@
import {
IListenerCallback, counter, logger, B
} from '..'
import * as bot from '..'

/** Keep all created bits, for getting by their ID as key */
export const bits: {
Expand All @@ -16,8 +14,8 @@ export interface IBit {
id?: string,
send?: string | string[],
catch?: string,
callback?: IListenerCallback,
catchCallback?: IListenerCallback,
callback?: bot.IListenerCallback,
catchCallback?: bot.IListenerCallback,
condition?: RegExp | string,
intent?: string,
listen?: string,
Expand All @@ -41,9 +39,9 @@ export class Bit implements IBit {
/** To send if response unmatched by listeners */
catch?: string
/** Function to call when executing bit (after any defined sends) */
callback?: IListenerCallback
callback?: bot.IListenerCallback
/** Function to call when response unmatched by listeners */
catchCallback?: IListenerCallback
catchCallback?: bot.IListenerCallback
/** Regex or string converted to regex for listener to trigger bit */
condition?: RegExp | string
/** Key for language processed intent to match for execution */
Expand Down Expand Up @@ -71,18 +69,18 @@ export class Bit implements IBit {
* that does something outside chat, but can be triggered by chat scripts.
*/
constructor (options: IBit) {
this.id = (options.id) ? options.id : counter('bit')
this.id = (options.id) ? options.id : bot.counter('bit')
Object.keys(options).forEach((key: string) => this[key] = options[key])
if (!this.send && !this.callback) {
logger.warn('Bit won\'t work without a send or callback attribute.')
bot.logger.warn('Bit won\'t work without a send or callback attribute.')
}
}

/**
* Do stuff with current bot state (e.g. send replies and/or call callbacks)
* @todo Do send if has `send` property.
*/
async execute (b: B): Promise<any> {
async execute (b: bot.B): Promise<any> {
if (this.callback) await Promise.resolve(this.callback(b))
}
}
Expand All @@ -95,10 +93,10 @@ export function setupBit (options: IBit) {
}

/** Execute a bit using its ID, providing current bot state */
export async function doBit (id: string, b: B): Promise<void> {
export async function doBit (id: string, b: bot.B): Promise<void> {
const bit = bits[id]
if (!bit) {
logger.error('Attempted to do bit with unknown ID')
bot.logger.error('Attempted to do bit with unknown ID')
return
}
await Promise.resolve(bit.execute(b))
Expand Down
File renamed without changes.
71 changes: 28 additions & 43 deletions src/lib/bot.ts → src/lib/core.ts
@@ -1,26 +1,5 @@
/**
* @module bot
* The core bBot methods. Manages operational aspects like start/stopping,
* logging, event emitting, the internal server and external connections as well
* as managing middleware and executing the high level "thought process".
*/
import { promisify } from 'util'
import {
events,
config,
name,
logger,
unloadListeners,
loadMiddleware,
unloadMiddleware,
loadAdapters,
startAdapters,
unloadAdapters,
Message,
ICallback,
hear,
B
} from '..'
import * as bot from '..'

/** Await helper, pauses for event loop */
export const eventDelay = promisify(setImmediate)
Expand All @@ -34,11 +13,11 @@ const status: { [key: string]: 0 | 1 } = {
function setStatus (set: 'waiting' | 'loading' | 'loaded' | 'starting' | 'started' | 'shutdown') {
for (let key of Object.keys(status)) status[key] = (set === key) ? 1 : 0
if (set === 'loading') {
logger.info(`${name} loading . . . . . ~(0_0)~`)
bot.logger.info(`${bot.name} loading . . . . . ~(0_0)~`)
} else if (set === 'starting') {
logger.info(`${name} starting . . . . . ┌(O_O)┘ bzzzt whirr`)
bot.logger.info(`${bot.name} starting . . . . . ┌(O_O)┘ bzzzt whirr`)
} else if (set === 'started') {
logger.info(`${name} started . . . . . ~(O_O)~ bleep bloop`)
bot.logger.info(`${bot.name} started . . . . . ~(O_O)~ bleep bloop`)
}
}

Expand All @@ -55,13 +34,13 @@ export function getStatus (): string {
export async function load (): Promise<void> {
if (getStatus() !== 'waiting') await reset()
setStatus('loading')
logger.debug('Using config:', config)
loadMiddleware()
loadAdapters()
bot.logger.debug('Using config:', bot.config)
bot.loadMiddleware()
bot.loadAdapters()
// loadServer()
await eventDelay()
setStatus('loaded')
events.emit('loaded')
bot.events.emit('loaded')
}

/**
Expand All @@ -73,11 +52,11 @@ export async function load (): Promise<void> {
export async function start (): Promise<void> {
if (getStatus() !== 'loaded') await load()
setStatus('starting')
await startAdapters()
await bot.startAdapters()
// await startSever()
await eventDelay()
setStatus('started')
events.emit('started')
bot.events.emit('started')
}

/**
Expand All @@ -92,9 +71,9 @@ export async function shutdown (): Promise<void> {
const status = getStatus()
if (status === 'shutdown') return
if (status === 'loading') {
await new Promise((resolve) => events.on('loaded', () => resolve()))
await new Promise((resolve) => bot.events.on('loaded', () => resolve()))
} else if (status === 'starting') {
await new Promise((resolve) => events.on('started', () => resolve()))
await new Promise((resolve) => bot.events.on('started', () => resolve()))
}
// shutdown server
// stop thought process
Expand All @@ -110,7 +89,7 @@ export async function pause (): Promise<void> {
await shutdown()
await eventDelay()
setStatus('loaded')
events.emit('paused')
bot.events.emit('paused')
}

/**
Expand All @@ -121,34 +100,40 @@ export async function reset (): Promise<void> {
const status = getStatus()
if (status === 'waiting') return
if (status !== 'shutdown') await shutdown()
unloadAdapters()
unloadMiddleware()
unloadListeners()
bot.unloadAdapters()
bot.unloadMiddleware()
bot.unloadListeners()
// unloadServer()
await eventDelay()
setStatus('waiting')
events.emit('waiting')
bot.events.emit('waiting')
}

// Primary adapter interfaces...

/** Input message to put through thought process (alias for 'hear' stage) */
export function receive (message: Message, callback?: ICallback): Promise<B> {
return hear(message, callback)
export function receive (
message: bot.Message,
callback?: bot.ICallback
): Promise<bot.B> {
return bot.hear(message, callback)
}

/** Output message either from thought process callback or self initiated */
/** @todo Send via adapter and resolve with sent state */
export function send (message: Message, callback?: ICallback): Promise<B> {
export function send (
message: bot.Message,
callback?: bot.ICallback
): Promise<bot.B> {
console.log('"Sending"', message)
const b = new B({ message })
const b = new bot.B({ message })
const promise = (callback) ? Promise.resolve(callback()) : Promise.resolve()
return promise.then(() => b)
}

/** Store data via adapter, from thought process conclusion or self initiated */
/** @todo Store via adapter and resolve with storage result */
export function store (data: any, callback?: ICallback): Promise<any> {
export function store (data: any, callback?: bot.ICallback): Promise<any> {
console.log('"Storing"', data)
const result = {}
const promise = (callback) ? Promise.resolve(callback()) : Promise.resolve()
Expand Down

0 comments on commit 631a5f9

Please sign in to comment.