Skip to content

Commit

Permalink
feat(rocketchat): Add adapter and fix processes
Browse files Browse the repository at this point in the history
- act processes catch-all listeners instead of listen
- pending tests and use npm published sdk
- fix init log level
- fix log formats for single line per event
- fix log syntax consistency
- fix thought process respond and store
  • Loading branch information
timkinnane committed Aug 12, 2018
1 parent b16aa8e commit 0169337
Show file tree
Hide file tree
Showing 21 changed files with 198 additions and 138 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -73,7 +73,7 @@
"typescript": "^2.7.2"
},
"dependencies": {
"@rocket.chat/sdk": "^0.2.1",
"@rocket.chat/sdk": "rocketchat/rocket.chat.js.sdk",
"@types/mongoose": "^5.0.14",
"@types/node": "^9.4.6",
"@types/winston": "^2.3.8",
Expand Down
35 changes: 22 additions & 13 deletions src/adapters/mongo.ts
Expand Up @@ -50,14 +50,15 @@ export class Mongo extends StorageAdapter {
constructor (bot: any) {
super(bot)
this.model = Model.get(this.config.collection)
this.bot.logger.info(`Using Mongo as storage adapter.`)
this.bot.logger.debug(`Storing to '${this.config.collection}' collection at ${this.config.url}`)
this.bot.logger.info(`[mongo] using Mongo as storage adapter.`)
this.bot.logger.debug(`[mongo] storing to '${this.config.collection}' collection at ${this.config.url}`)
}

/** Connect to Mongo */
async start () {
this.bot.logger.info(`Connecting to Mongo DB at ${this.config.url}`)
this.bot.logger.info(`[mongo] connecting to Mongo DB at ${this.config.url}`)
this.store = await mongoose.connect(this.config.url, this.config.connection)
this.bot.logger.debug(`[mongo] connected to Mongo DB`)
return
}

Expand All @@ -70,7 +71,7 @@ export class Mongo extends StorageAdapter {
/** Put memory data in documents by sub-collection */
/** @todo compare to copy from last save and only update difference */
async saveMemory (data: any) {
this.bot.logger.debug(`Saving memory data to DB`)
this.bot.logger.debug(`[mongo] saving memory data to DB`)
for (let sub in data) {
const query = { sub, type: 'memory' }
const doc = { data: data[sub] }
Expand All @@ -82,7 +83,7 @@ export class Mongo extends StorageAdapter {

/** Get all the memory document data */
async loadMemory () {
this.bot.logger.debug(`Loading memory data from DB`)
this.bot.logger.debug(`[mongo] loading memory data from DB`)
const query = { type: 'memory' }
const fields = { _id: 0, 'data': 1, 'sub': 1 }
const opts = { lean: true }
Expand All @@ -105,19 +106,25 @@ export class Mongo extends StorageAdapter {
/** Add item to serial store data */
/** @todo Add class to model and store constructor ref, to restore on find */
async keep (sub: string, data: any) {
this.bot.logger.debug(`Adding a ${sub} value to DB`)
const query = { sub, type: 'store' }
const update = { $push: { data } }
const options = { upsert: true, lean: true }
await this.model.findOneAndUpdate(query, update, options).exec()
try {
this.bot.logger.debug(`[mongo] keep ${sub} value in DB`)
const query = { sub, type: 'store' }
const update = { $push: { data } }
const options = { upsert: true, lean: true }
// console.log(data) /** @todo FIX MONGOOSE - hangs until memory crash */
await this.model.findOneAndUpdate(query, update, options).exec()
this.bot.logger.debug(`[mongo] kept ${sub}: ${JSON.stringify(update)}`)
} catch (err) {
this.bot.logger.error(`[mongo] keep error for ${sub}`, err)
}
}

/** Find certain stuff in Mongo */
/** @todo Refactor model with data as sub-document so it can be queried */
/** @todo Add note in docs recommending not to use find on large data sets */
/** @todo Use class from model to reinitialise with `new bot[constructor]` */
async find (sub: string, params: any) {
this.bot.logger.debug(`Finding any ${sub} matching ${params}`)
this.bot.logger.debug(`[mongo] finding any ${sub} matching ${params}`)
const query = { sub, data: { $elemMatch: params }, type: 'store' }
const fields = { _id: 0, 'data': 1 }
const opts = { lean: true }
Expand All @@ -128,23 +135,25 @@ export class Mongo extends StorageAdapter {
for (let key in params) match = (item[key] === params[key])
return match
})
this.bot.logger.debug(`[mongo] found matching ${sub}s: ${JSON.stringify(matching)}`)
return matching
}

/** Find a thing in Mongo */
async findOne (sub: string, params: any) {
this.bot.logger.debug(`Finding a ${sub} matching ${params}`)
this.bot.logger.debug(`[mongo] finding a ${sub} matching ${params}`)
const query = { sub, data: { $elemMatch: params }, type: 'store' }
const fields = { _id: 0, 'data.$': 1 }
const opts = { lean: true }
const doc = await this.model.findOne(query, fields, opts).exec() as IStore
if (!doc) return undefined
this.bot.logger.debug(`[mongo] found a ${sub}: ${JSON.stringify(doc.data[0])}`)
return doc.data[0]
}

/** Get rid of stuff in Mongo */
async lose (sub: string, params: any) {
this.bot.logger.debug(`Losing a ${sub} matching ${params}`)
this.bot.logger.debug(`[mongo] losing a ${sub} matching ${params}`)
const query = { sub, type: 'store' }
const update = { $pull: { data: params } }
const options = { upsert: true, lean: true }
Expand Down
88 changes: 43 additions & 45 deletions src/adapters/rocketchat.ts
Expand Up @@ -6,11 +6,11 @@ export class Rocketchat extends bot.MessageAdapter {
constructor (bot: any) {
super(bot)
sdk.settings.integrationId = 'bBot'
this.bot.logger.info('Using Rocket.Chat as message adapter')
bot.logger.info('[rocketchat] using Rocket.Chat as message adapter')
}

async start () {
this.bot.logger.info(`[startup] Rocket.Chat adapter in use`)
bot.logger.info(`[rocketchat] Rocket.Chat adapter in use`)

// Make SDK modules available to scripts, via `adapter.`
this.driver = sdk.driver
Expand All @@ -19,69 +19,68 @@ export class Rocketchat extends bot.MessageAdapter {
this.settings = sdk.settings

// Print logs with current configs
this.bot.logger.info(`[startup] Respond to name: ${this.bot.name}`)
this.bot.alias = (this.bot.name === sdk.settings.username || this.bot.alias)
? this.bot.alias
: sdk.settings.username
if (this.bot.alias) {
this.bot.logger.info(`[startup] Respond to alias: ${this.bot.alias}`)
}
bot.logger.info(`[rocketchat] responds to name: ${bot.name}`)
if (bot.alias) bot.logger.info(`[rocketchat] responds to alias: ${bot.alias}`)

sdk.driver.useLog(bot.logger)
await sdk.driver.connect()
await sdk.driver.login()
await sdk.driver.subscribeToMessages()
await sdk.driver.respondToMessages(this.process.bind(this)) // reactive callback
await sdk.driver.respondToMessages(this.process.bind(this))
bot.logger.debug(`[rocketchat] connected via DDP`)
}

/** Process every incoming message in subscription */
/** @todo Add proper message and meta types from SDK exported interfaces */
process (err: Error, message: any, meta: any) {
process (err: Error | null, message: any, meta: any) {
if (err) throw err
// Prepare message type for bBot to receive...
this.bot.logger.info('Filters passed, will receive message')
bot.logger.info('[rocketchat] filters passed, will hear message')

// Collect required attributes from message meta
/** @todo confirm user alias is on message schema not user */
const isDM = (meta.roomType === 'd')
const isLC = (meta.roomType === 'l')
const user = this.bot.brain.userForId(message.u._id, {
const user = bot.userById(message.u._id, {
name: message.u.username,
alias: message.alias
alias: message.alias,
room: {
id: message.rid,
type: message.roomType,
name: meta.roomName
}
})
user.roomID = message.rid
user.roomType = meta.roomType
user.room = meta.roomName || message.rid

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

// Room exit, receive without further detail
// Room exit, hear without further detail
if (message.t === 'ul') {
this.bot.logger.debug('Message type LeaveMessage')
return this.bot.receive(new bot.LeaveMessage(user, message._id))
bot.logger.debug('[rocketchat] hear type LeaveMessage')
return bot.hear(new bot.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(this.bot.name) === startOfText || message.msg.indexOf(this.bot.alias) === startOfText
if ((isDM || isLC) && !robotIsNamed) message.msg = `${this.bot.name} ${message.msg}`
const robotIsNamed = message.msg.indexOf(bot.name) === startOfText || message.msg.indexOf(bot.alias) === startOfText
if ((isDM || isLC) && !robotIsNamed) message.msg = `${bot.name} ${message.msg}`

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

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

async respond (envelope: bot.Envelope, method: string) {
Expand All @@ -94,16 +93,16 @@ export class Rocketchat extends bot.MessageAdapter {
}
break
case 'dm':
if (!envelope.strings) throw new Error('Sending without strings')
if (!envelope.user) throw new Error('Sending direct without user')
if (!envelope.strings) throw new Error('DM without strings')
if (!envelope.user) throw new Error('DM without user')
for (let text in envelope.strings) {
await sdk.driver.sendDirectToUser(text, envelope.user.username)
}
break
case 'reply':
if (!envelope.strings) throw new Error('Sending without strings')
if (!envelope.strings) throw new Error('Reply without strings')
if (!envelope.user) throw new Error('Reply without user')
if (!envelope.room.id) throw new Error('Sending without room ID')
if (!envelope.room.id) throw new Error('Reply without room ID')
if (envelope.room.id.indexOf(envelope.user.id) === -1) {
envelope.strings = envelope.strings.map((s) => `@${envelope.user!.username} ${s}`)
}
Expand All @@ -112,8 +111,17 @@ export class Rocketchat extends bot.MessageAdapter {
}
break
case 'react':
console.log('TODO: Add Rocket.Chat react method', envelope.strings)
if (!envelope.strings) throw new Error('React without strings')
if (!envelope.message) throw new Error('React without message')
for (let emoji in envelope.strings) {
if (!emoji.startsWith(':')) emoji = `:${emoji}`
if (!emoji.endsWith(':')) emoji = `${emoji}:`
emoji = emoji.replace('-', '_') // Rocket.Chat syntax
await sdk.driver.setReaction(emoji, envelope.message.id)
}
break
default:
throw new Error(`Rocket.Chat adapter has no ${method} handler`)
}
}

Expand All @@ -129,13 +137,3 @@ export class Rocketchat extends bot.MessageAdapter {
}

export const use = (bot: any) => new Rocketchat(bot)

/** Define new message type for handling attachments */
class AttachmentMessage extends bot.TextMessage {
constructor (user: bot.User, public attachment: any, text: string, id: string) {
super(user, text, id)
}
toString () {
return this.attachment
}
}
2 changes: 1 addition & 1 deletion src/adapters/shell.ts
Expand Up @@ -4,7 +4,7 @@ export class Shell extends MessageAdapter {
name = 'shell-message-adapter'
constructor (bot: any) {
super(bot)
this.bot.logger.info('Using Shell as message adapter')
this.bot.logger.info('[shell] using Shell as message adapter')
}
}

Expand Down
15 changes: 12 additions & 3 deletions src/demo/rocketchat.ts
Expand Up @@ -4,14 +4,23 @@
* Requires config: https://rocket.chat/docs/bots/configure-bot-environment/
* Shows different listener types and adapter respond methods.
* Run demo from project root: `ts-node src/demo/rocketchat`
* @todo Move this and adapter into it's own repo to lighten dependencies.
*/
import * as bot from '..'
process.env.BOT_MESSAGE_ADAPTER = '../adapters/rocketchat'
process.env.BOT_STORAGE_ADAPTER = '../adapters/mongo'
process.env.BOT_LOG_LEVEL = 'debug'
process.env.RESPOND_TO_DM = 'true'
process.env.RESPOND_TO_EDITED = 'true'

bot.config.messageAdapter = '../adapters/rocketchat'
bot.config.storageAdapter = '../adapters/mongo'
import * as bot from '..'

const start = async () => {
await bot.start()
/** @todo Test that states within subsequent listeners is isolated - I think it's inheriting */
/** @todo Use https://github.com/omnidan/node-emoji to match any emoji key */
/** @todo Add middleware parsing actual emoji in strings into their key */
bot.listenText(/\b(sup|boom)\b/i, (b) => b.write(':punch:').respond('react'))
bot.listenText(/\b(shit|crap)\b/i, (b) => b.write(':poop:').respond('react'))
}

start().catch((err) => bot.logger.error(err))
4 changes: 2 additions & 2 deletions src/lib/adapter-classes/base.ts
Expand Up @@ -16,11 +16,11 @@ export abstract class Adapter {

/** Extend to add any bot startup requirements in adapter environment */
async start () {
this.bot.logger.info('Generic adapter `start` called without override')
this.bot.logger.info('[adapter] `start` called without override')
}

/** Extend to add any bot shutdown requirements in adapter environment */
async shutdown () {
this.bot.logger.info('Generic adapter `shutdown` called without override')
this.bot.logger.info('[adapter] `shutdown` called without override')
}
}
12 changes: 6 additions & 6 deletions src/lib/adapter-classes/message.ts
Expand Up @@ -8,22 +8,22 @@ import * as bot from '../..'
export abstract class MessageAdapter extends Adapter {
name = 'message-adapter'
/** Open connection to messaging platform */
async open () {
this.bot.logger.debug('Message adapter `open` called without override')
async start () {
this.bot.logger.debug('[message-adapter] `start` called without override')
}
/** Close connection to messaging platform */
async close () {
this.bot.logger.debug('Storage adapter `close` called without override')
async shutdown () {
this.bot.logger.debug('[message-adapter] `shutdown` called without override')
}
/** Process an incoming message from platform */
async hear (message: any): Promise<any> {
this.bot.logger.debug('Message adapter `hear` called without override', {
this.bot.logger.debug('[message-adapter] `hear` called without override', {
message
})
}
/** Take addressed envelope to action in platform, per given method */
async respond (envelope: bot.Envelope, method: string): Promise<any> {
this.bot.logger.debug(`Message adapter ${method} called without override`, {
this.bot.logger.debug(`[message-adapter] respond via ${method} called without override`, {
envelope
})
}
Expand Down

0 comments on commit 0169337

Please sign in to comment.