From 2e6073d21cf3bf3b7d5f6d2c10f4005f2e980dcc Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 20 Jun 2018 00:05:55 -0300 Subject: [PATCH 01/16] Change to reflect moving to streams in RC-side of development Replies to commands are temporarily commented --- src/lib/driver.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 08b9688..458caba 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -25,8 +25,8 @@ import { /** Collection names */ const _messageCollectionName = 'stream-room-messages' const _messageStreamName = '__my_messages__' -const _clientCommandsCollectionName = 'rocketchat_clientcommand' -const _clientCommandsSubscriptionName = 'clientCommands' +const _clientCommandsCollectionName = 'stream-client-commands' +const _clientCommandsStreamName = '__my_commands__' /** * Asteroid ^v2 interface below, suspended for work on future branch @@ -488,10 +488,8 @@ export function respondToMessages (callback: ICallback, options: IRespondOptions * Begin subscription to clientCommands for user and returns the collection */ async function subscribeToCommands (): Promise { - await subscribe(_clientCommandsSubscriptionName) + const subscription = await subscribe(_clientCommandsCollectionName, _clientCommandsStreamName, true) clientCommands = asteroid.getCollection(_clientCommandsCollectionName) - // v2 - // clientCommands = asteroid.collections.get(_clientCommandsCollectionName) || Map() return clientCommands } @@ -513,7 +511,12 @@ async function reactToCommands (callback: ICallback): Promise { const changedCommandQuery = clientCommands.reactiveQuery({ _id }) if (changedCommandQuery.result && changedCommandQuery.result.length > 0) { const changedCommand = changedCommandQuery.result[0] - callback(null, changedCommand) + if (Array.isArray(changedCommand.args)) { + logger.info(`[received] Command ${ changedCommand.args[0].cmd.key }`) + callback(null, changedCommand.args[0]) + } else { + logger.debug('[received] Update without message args') + } } }) } @@ -555,19 +558,19 @@ async function commandHandler (command: IClientCommand): Promise // SDK-level command to pause the message stream, interrupting all messages from the server case 'pauseMessageStream': subscriptions.map((s: ISubscription) => (s._name === _messageCollectionName ? unsubscribe(s) : undefined)) - await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) + // await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) break // SDK-level command to resubscribe to the message stream case 'resumeMessageStream': await subscribeToMessages() messageLastReadTime = new Date() // reset time of last read message - await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) + // await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) break // SDK-level command to check for aliveness of the bot regarding commands case 'heartbeat': - await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) + // await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) break // If command is not at the SDK-level, it tries to call a handler added by the user @@ -575,7 +578,7 @@ async function commandHandler (command: IClientCommand): Promise const handler = commandHandlers[command.cmd.key] if (handler) { const result = await handler(command) - await asyncCall('replyClientCommand', [command._id, result]) + // await asyncCall('replyClientCommand', [command._id, result]) } } } From 2fdb90fdf0704a51d3c43809511b745ec7f6e5cc Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 20 Jun 2018 00:43:30 -0300 Subject: [PATCH 02/16] Stream for clientCommands takes userId as argument --- src/lib/driver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 458caba..02525a4 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -26,7 +26,7 @@ import { const _messageCollectionName = 'stream-room-messages' const _messageStreamName = '__my_messages__' const _clientCommandsCollectionName = 'stream-client-commands' -const _clientCommandsStreamName = '__my_commands__' +let _clientCommandsStreamName: string /** * Asteroid ^v2 interface below, suspended for work on future branch @@ -280,6 +280,7 @@ export function login (credentials: ICredentials = { return login .then((loggedInUserId) => { userId = loggedInUserId + _clientCommandsStreamName = loggedInUserId return loggedInUserId }) .then(() => { From 78b8670b157b4ddf1681970e77adc18911d16d4d Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Thu, 21 Jun 2018 20:37:12 -0300 Subject: [PATCH 03/16] Reply to commands with a default response --- src/config/commandInterfaces.ts | 2 +- src/lib/driver.ts | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/config/commandInterfaces.ts b/src/config/commandInterfaces.ts index 62c1f86..aa8e9a8 100644 --- a/src/config/commandInterfaces.ts +++ b/src/config/commandInterfaces.ts @@ -17,7 +17,7 @@ export interface IClientCommand { * Structure of the object to reply to a clientCommand */ export interface IClientCommandResponse { - status?: number, + success: boolean, msg: string } diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 02525a4..4b3966f 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -489,7 +489,7 @@ export function respondToMessages (callback: ICallback, options: IRespondOptions * Begin subscription to clientCommands for user and returns the collection */ async function subscribeToCommands (): Promise { - const subscription = await subscribe(_clientCommandsCollectionName, _clientCommandsStreamName, true) + await subscribe(_clientCommandsCollectionName, _clientCommandsStreamName, true) clientCommands = asteroid.getCollection(_clientCommandsCollectionName) return clientCommands } @@ -555,23 +555,28 @@ async function respondToCommands (): Promise { * @param command Command object */ async function commandHandler (command: IClientCommand): Promise { + const okResponse: IClientCommandResponse = { + success: true, + msg: 'Ok' + } + switch (command.cmd.key) { // SDK-level command to pause the message stream, interrupting all messages from the server case 'pauseMessageStream': subscriptions.map((s: ISubscription) => (s._name === _messageCollectionName ? unsubscribe(s) : undefined)) - // await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) + await asyncCall('replyClientCommand', [command._id, okResponse]) break // SDK-level command to resubscribe to the message stream case 'resumeMessageStream': await subscribeToMessages() messageLastReadTime = new Date() // reset time of last read message - // await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) + await asyncCall('replyClientCommand', [command._id, okResponse]) break // SDK-level command to check for aliveness of the bot regarding commands case 'heartbeat': - // await asyncCall('replyClientCommand', [command._id, { msg: 'OK' }]) + await asyncCall('replyClientCommand', [command._id, okResponse]) break // If command is not at the SDK-level, it tries to call a handler added by the user @@ -579,7 +584,7 @@ async function commandHandler (command: IClientCommand): Promise const handler = commandHandlers[command.cmd.key] if (handler) { const result = await handler(command) - // await asyncCall('replyClientCommand', [command._id, result]) + await asyncCall('replyClientCommand', [command._id, result]) } } } From 91e37e8790a3808eb9e36e25df6c71fa645bfd08 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Thu, 28 Jun 2018 23:04:22 -0300 Subject: [PATCH 04/16] Improve dealing with ClientCommands - Better logs - No more using of a global variable to set a dynamic userId to subscribe to the client commands stream, pass it as a parameter instead - SDK will not halt if server does not have client-commands stream --- src/lib/driver.ts | 28 ++++++++++++---------------- src/utils/start.ts | 5 ++--- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 4b3966f..7fecbc0 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -25,8 +25,7 @@ import { /** Collection names */ const _messageCollectionName = 'stream-room-messages' const _messageStreamName = '__my_messages__' -const _clientCommandsCollectionName = 'stream-client-commands' -let _clientCommandsStreamName: string +const _clientCommandsStreamName = 'stream-client-commands' /** * Asteroid ^v2 interface below, suspended for work on future branch @@ -280,12 +279,9 @@ export function login (credentials: ICredentials = { return login .then((loggedInUserId) => { userId = loggedInUserId - _clientCommandsStreamName = loggedInUserId - return loggedInUserId - }) - .then(() => { // Calling function to listen to commands and answer to them - return respondToCommands() + respondToCommands(loggedInUserId) + return loggedInUserId }) .catch((err: Error) => { logger.info('[login] Error:', err) @@ -313,7 +309,7 @@ export function subscribe (subscriptionName: string, ...params: any[]): Promise< const subscription = asteroid.subscribe(subscriptionName, ...params) subscriptions.push(subscription) return subscription.ready.then((id) => { - logger.info(`[subscribe] Stream ready: ${id}`) + logger.info(`[subscribe] Subscription ${subscriptionName} ready: ${id}`) resolve(subscription) }) // Asteroid ^v2 interface... @@ -488,9 +484,9 @@ export function respondToMessages (callback: ICallback, options: IRespondOptions /** * Begin subscription to clientCommands for user and returns the collection */ -async function subscribeToCommands (): Promise { - await subscribe(_clientCommandsCollectionName, _clientCommandsStreamName, true) - clientCommands = asteroid.getCollection(_clientCommandsCollectionName) +async function subscribeToCommands (userId: string): Promise { + await subscribe(_clientCommandsStreamName, userId, true) + clientCommands = asteroid.getCollection(_clientCommandsStreamName) return clientCommands } @@ -502,8 +498,8 @@ async function subscribeToCommands (): Promise { * - Uses error-first callback pattern * - Second argument is the the command received */ -async function reactToCommands (callback: ICallback): Promise { - const clientCommands = await subscribeToCommands() +async function reactToCommands (userId: string, callback: ICallback): Promise { + const clientCommands = await subscribeToCommands(userId) await asyncCall('setCustomClientData', customClientData) @@ -525,9 +521,9 @@ async function reactToCommands (callback: ICallback): Promise { /** * Calls reactToCommands with a callback to read latest clientCommands and reply to them */ -async function respondToCommands (): Promise { +async function respondToCommands (userId: string): Promise { commandLastReadTime = new Date() // init before any message read - await reactToCommands(async (err, command) => { + await reactToCommands(userId, async (err, command) => { if (err) { logger.error(`Unable to receive commands ${JSON.stringify(err)}`) throw err @@ -599,7 +595,7 @@ export function registerCommandHandler (key: string, callback: IClientCommandHan const currentHandler = commandHandlers[key] if (currentHandler) { logger.error(`[Command] Command '${key}' already has a handler`) - throw Error('Command in use') + throw Error(`[Command] Command '${key}' already has a handler`) } logger.info(`[Command] Registering handler for command '${key}'`) diff --git a/src/utils/start.ts b/src/utils/start.ts index d044a2b..3b3892e 100644 --- a/src/utils/start.ts +++ b/src/utils/start.ts @@ -23,9 +23,8 @@ async function start () { edited: true, livechat: false }) - driver.registerCommandHandler('xdlol', async (command) => { - console.log('testing') - return { msg: 'OK' } + driver.registerCommandHandler('testCommand', async (command) => { + return { success: true, msg: 'OK' } }) } From acddb68cc8ff578869ea1b437cafec228fd04f70 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Tue, 3 Jul 2018 21:48:29 -0300 Subject: [PATCH 05/16] Improve handling of ClientCommands and add support to getStatistics --- src/config/commandInterfaces.ts | 3 +- src/config/driverInterfaces.ts | 4 ++ src/lib/driver.spec.ts | 4 ++ src/lib/driver.ts | 95 +++++++++++++++++++++------------ src/lib/settings.ts | 2 + 5 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/config/commandInterfaces.ts b/src/config/commandInterfaces.ts index aa8e9a8..c8cc331 100644 --- a/src/config/commandInterfaces.ts +++ b/src/config/commandInterfaces.ts @@ -18,7 +18,8 @@ export interface IClientCommand { */ export interface IClientCommandResponse { success: boolean, - msg: string + error?: Error, + [key: string]: any } /* diff --git a/src/config/driverInterfaces.ts b/src/config/driverInterfaces.ts index e9f6503..8b04fef 100644 --- a/src/config/driverInterfaces.ts +++ b/src/config/driverInterfaces.ts @@ -43,3 +43,7 @@ export interface ILogger { export interface ICallback { (error: Error | null, ...args: any[]): void } + +export interface ISessionStatistics { + interceptedMessages: number +} diff --git a/src/lib/driver.spec.ts b/src/lib/driver.spec.ts index ffb707b..2019935 100644 --- a/src/lib/driver.spec.ts +++ b/src/lib/driver.spec.ts @@ -116,6 +116,10 @@ describe('driver', () => { }) }) describe('.clientCommands', () => { + /** + * Note: For them to work you have to wait for client-commands subscription + * to be fully initialized, so set ROCKETCHAT_WAIT_CLIENT_COMMANDS to true + */ it('sets customClientData from the SDK with no customizations', async () => { await driver.connect() await driver.login() diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 7fecbc0..5763bcf 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -10,7 +10,7 @@ import immutableCollectionMixin from 'asteroid-immutable-collections-mixin' import * as settings from './settings' import * as methodCache from './methodCache' import { Message } from './message' -import { IConnectOptions, IRespondOptions, ICallback, ILogger } from '../config/driverInterfaces' +import { IConnectOptions, IRespondOptions, ICallback, ILogger, ISessionStatistics } from '../config/driverInterfaces' import { IAsteroid, ICredentials, ISubscription, ICollection } from '../config/asteroidInterfaces' import { IMessage } from '../config/messageInterfaces' import { logger, replaceLog } from './log' @@ -95,13 +95,21 @@ export let clientCommands: ICollection */ export let commandHandlers: IClientCommandHandlerMap = {} +/** + * Map of session statistics collected by the SDK + */ +const sessionStatistics: ISessionStatistics = { + interceptedMessages: 0 +} + /** * Custom Data set by the client that is using the SDK */ export let customClientData: object = { framework: 'Rocket.Chat JS SDK', canPauseResumeMsgStream: true, - canListenToHeartbeat: true + canListenToHeartbeat: true, + canGetStatistics: true } /** @@ -280,7 +288,14 @@ export function login (credentials: ICredentials = { .then((loggedInUserId) => { userId = loggedInUserId // Calling function to listen to commands and answer to them - respondToCommands(loggedInUserId) + return loggedInUserId + }) + .then(async (loggedInUserId) => { + if (settings.waitForClientCommands) { + await respondToCommands(loggedInUserId) + } else { + respondToCommands(loggedInUserId).catch(() => {/**/}) + } return loggedInUserId }) .catch((err: Error) => { @@ -476,6 +491,7 @@ export function respondToMessages (callback: ICallback, options: IRespondOptions // if (!isDM && !isLC) meta.roomName = await getRoomName(message.rid) // Processing completed, call callback to respond to message + sessionStatistics.interceptedMessages += 1 callback(null, message, meta) }) return promise @@ -500,7 +516,6 @@ async function subscribeToCommands (userId: string): Promise { */ async function reactToCommands (userId: string, callback: ICallback): Promise { const clientCommands = await subscribeToCommands(userId) - await asyncCall('setCustomClientData', customClientData) logger.info(`[reactive] Listening for change events in collection ${clientCommands.name}`) @@ -551,38 +566,50 @@ async function respondToCommands (userId: string): Promise { * @param command Command object */ async function commandHandler (command: IClientCommand): Promise { - const okResponse: IClientCommandResponse = { - success: true, - msg: 'Ok' + let result: IClientCommandResponse = { + success: true } - - switch (command.cmd.key) { - // SDK-level command to pause the message stream, interrupting all messages from the server - case 'pauseMessageStream': - subscriptions.map((s: ISubscription) => (s._name === _messageCollectionName ? unsubscribe(s) : undefined)) - await asyncCall('replyClientCommand', [command._id, okResponse]) - break - - // SDK-level command to resubscribe to the message stream - case 'resumeMessageStream': - await subscribeToMessages() - messageLastReadTime = new Date() // reset time of last read message - await asyncCall('replyClientCommand', [command._id, okResponse]) - break - - // SDK-level command to check for aliveness of the bot regarding commands - case 'heartbeat': - await asyncCall('replyClientCommand', [command._id, okResponse]) - break - - // If command is not at the SDK-level, it tries to call a handler added by the user - default: - const handler = commandHandlers[command.cmd.key] - if (handler) { - const result = await handler(command) - await asyncCall('replyClientCommand', [command._id, result]) - } + try { + const handler = commandHandlers[command.cmd.key] + switch (command.cmd.key) { + // SDK-level command to check for aliveness of the bot regarding commands + case 'heartbeat': + break + + // SDK-level command to pause the message stream, interrupting all messages from the server + case 'pauseMessageStream': + subscriptions.map((s: ISubscription) => (s._name === _messageCollectionName ? unsubscribe(s) : undefined)) + break + + // SDK-level command to resubscribe to the message stream + case 'resumeMessageStream': + await subscribeToMessages() + messageLastReadTime = new Date() // reset time of last read message + break + + case 'getStatistics': + const statistics: any = {} + statistics.sdk = sessionStatistics + if (handler) { + statistics.adapter = await handler(command) + } + result.statistics = statistics + break + + // If command is not at the SDK-level, it tries to call a handler added by the user + default: + if (handler) { + result = await handler(command) + } else { + throw Error('Handler not found') + } + } + } catch (err) { + result.success = false + result.error = err } + + asyncCall('replyClientCommand', [command._id, result]).catch(() => {/**/}) } /** diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 816fdb0..9374d0f 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -28,3 +28,5 @@ export let roomCacheMaxSize = parseInt(process.env.ROOM_CACHE_SIZE || '10', 10) export let roomCacheMaxAge = 1000 * parseInt(process.env.ROOM_CACHE_MAX_AGE || '300', 10) export let dmCacheMaxSize = parseInt(process.env.DM_ROOM_CACHE_SIZE || '10', 10) export let dmCacheMaxAge = 1000 * parseInt(process.env.DM_ROOM_CACHE_MAX_AGE || '100', 10) + +export let waitForClientCommands = process.env.ROCKETCHAT_WAIT_CLIENT_COMMANDS From 1d21a51df5322e5a9c58c6bd5cd927e7a58a6762 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 4 Jul 2018 21:27:16 -0300 Subject: [PATCH 06/16] Add new stat to reply to getStatistics ClientCommand --- src/config/driverInterfaces.ts | 2 +- src/lib/driver.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config/driverInterfaces.ts b/src/config/driverInterfaces.ts index 8b04fef..e4a49d2 100644 --- a/src/config/driverInterfaces.ts +++ b/src/config/driverInterfaces.ts @@ -45,5 +45,5 @@ export interface ICallback { } export interface ISessionStatistics { - interceptedMessages: number + [key: string]: any } diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 5763bcf..7d5152c 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -99,7 +99,7 @@ export let commandHandlers: IClientCommandHandlerMap = {} * Map of session statistics collected by the SDK */ const sessionStatistics: ISessionStatistics = { - interceptedMessages: 0 + Bot_Stats_Read_Messages: 0 } /** @@ -491,7 +491,7 @@ export function respondToMessages (callback: ICallback, options: IRespondOptions // if (!isDM && !isLC) meta.roomName = await getRoomName(message.rid) // Processing completed, call callback to respond to message - sessionStatistics.interceptedMessages += 1 + sessionStatistics.Bot_Stats_Read_Messages += 1 callback(null, message, meta) }) return promise @@ -590,6 +590,7 @@ async function commandHandler (command: IClientCommand): Promise case 'getStatistics': const statistics: any = {} statistics.sdk = sessionStatistics + statistics.sdk.Bot_Stats_Latest_Read = messageLastReadTime.toUTCString() if (handler) { statistics.adapter = await handler(command) } From 6432d0d008f883cc9762289935b987a54cf83df8 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Sun, 8 Jul 2018 23:04:36 -0300 Subject: [PATCH 07/16] Add new statistic indicating how many times the bot reconnected --- src/lib/driver.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 7d5152c..cc70e39 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -99,7 +99,8 @@ export let commandHandlers: IClientCommandHandlerMap = {} * Map of session statistics collected by the SDK */ const sessionStatistics: ISessionStatistics = { - Bot_Stats_Read_Messages: 0 + Bot_Stats_Read_Messages: 0, + Bot_Stats_Reconnect_Count: 0 } /** @@ -171,6 +172,10 @@ export function connect (options: IConnectOptions = {}, callback?: ICallback): P if (callback) callback(null, asteroid) resolve(asteroid) }) + + events.on('reconnected', () => { + sessionStatistics.Bot_Stats_Reconnect_Count += 1 + }) } }) } From fbf5dd145834d6ceb95023cd4ac4f00aa6b0f42a Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Mon, 9 Jul 2018 22:35:49 -0300 Subject: [PATCH 08/16] Add test of custom command handler and fix bug on getStatistics handler --- src/lib/driver.spec.ts | 13 ++++++++++++- src/lib/driver.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/driver.spec.ts b/src/lib/driver.spec.ts index 2019935..69dd2f3 100644 --- a/src/lib/driver.spec.ts +++ b/src/lib/driver.spec.ts @@ -3,7 +3,7 @@ import sinon from 'sinon' import { expect } from 'chai' import { silence } from './log' import { botUser, mockUser, apiUser } from '../utils/config' -import { logout } from './api' +import { get, login, logout } from './api' import * as utils from '../utils/testing' import * as driver from './driver' import * as methodCache from './methodCache' @@ -136,6 +136,17 @@ describe('driver', () => { expect(result.user.customClientData).to.deep.include(driver.customClientData) expect(result.user.customClientData.framework).to.equal('Testing') }) + it('custom handler is called once', async () => { + driver.setCustomClientData({ framework: 'Testing' }) + const callback = sinon.spy() + driver.registerCommandHandler('getStatistics', callback) + await driver.connect() + await driver.login() + // Login as admin and request stats from the bot + await login({ username: apiUser.username, password: apiUser.password }) + await get('bots.getLiveStats', { username: botUser.username }) + sinon.assert.calledOnce(callback) + }) }) describe('.subscribeToMessages', () => { it('resolves with subscription object', async () => { diff --git a/src/lib/driver.ts b/src/lib/driver.ts index cc70e39..35eee06 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -595,7 +595,7 @@ async function commandHandler (command: IClientCommand): Promise case 'getStatistics': const statistics: any = {} statistics.sdk = sessionStatistics - statistics.sdk.Bot_Stats_Latest_Read = messageLastReadTime.toUTCString() + statistics.sdk.Bot_Stats_Latest_Read = messageLastReadTime ? messageLastReadTime.toUTCString() : undefined if (handler) { statistics.adapter = await handler(command) } From 05cbbb00324ff834154248567a890770e11f92d0 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 11 Jul 2018 23:34:40 -0300 Subject: [PATCH 09/16] Add response to getLogs request Improve use of logs regarding ClientCommands --- src/lib/driver.ts | 78 ++++++++++++++++++++++++++++------- src/types/intercept-stdout.ts | 1 + 2 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 src/types/intercept-stdout.ts diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 35eee06..4ef8510 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events' import Asteroid from 'asteroid' +import intercept from 'intercept-stdout' // Asteroid v2 imports /* import { createClass } from 'asteroid' @@ -38,6 +39,21 @@ const Asteroid: IAsteroid = createClass([immutableCollectionMixin]) // CONNECTION SETUP AND CONFIGURE // ----------------------------------------------------------------------------- +/** + * Intercept all logging going to stdout and store the last 100 entries + * That is the array sent to the server when the client receives a ClientCommand + * getLogs + */ +export let logs: Array = [] +export let maxLogSize: number = 100 +intercept((log: string) => { + logs.push(log) + if (logs.length > maxLogSize) { + logs.splice(logs.length - maxLogSize, logs.length) + } + return log +}) + /** Internal for comparing message and command update timestamps */ export let messageLastReadTime: Date export let commandLastReadTime: Date @@ -96,12 +112,14 @@ export let clientCommands: ICollection export let commandHandlers: IClientCommandHandlerMap = {} /** - * Map of session statistics collected by the SDK + * ClientCommands that should not be logged */ -const sessionStatistics: ISessionStatistics = { - Bot_Stats_Read_Messages: 0, - Bot_Stats_Reconnect_Count: 0 -} +export let silentClientCommands: Array = ['heartbeat', 'getLogs'] + +/** + * Method calls that should not be logged + */ +export let silentMethods: Array = ['replyClientCommand'] /** * Custom Data set by the client that is using the SDK @@ -110,7 +128,16 @@ export let customClientData: object = { framework: 'Rocket.Chat JS SDK', canPauseResumeMsgStream: true, canListenToHeartbeat: true, - canGetStatistics: true + canGetStatistics: true, + canGetLogs: true +} + +/** + * Map of session statistics collected by the SDK + */ +const sessionStatistics: ISessionStatistics = { + Bot_Stats_Read_Messages: 0, + Bot_Stats_Reconnect_Count: 0 } /** @@ -219,16 +246,21 @@ function setupMethodCache (asteroid: IAsteroid): void { */ export function asyncCall (method: string, params: any | any[]): Promise { if (!Array.isArray(params)) params = [params] // cast to array for apply - logger.info(`[${method}] Calling (async): ${JSON.stringify(params)}`) + + const shouldLog: boolean = silentMethods.indexOf(method) === -1 + if (shouldLog) logger.info(`[${method}] Calling (async): ${JSON.stringify(params)}`) + return Promise.resolve(asteroid.apply(method, params).result) .catch((err: Error) => { logger.error(`[${method}] Error:`, err) throw err // throw after log to stop async chain }) .then((result: any) => { - (result) - ? logger.debug(`[${method}] Success: ${JSON.stringify(result)}`) - : logger.debug(`[${method}] Success`) + if (shouldLog) { + (result) + ? logger.debug(`[${method}] Success: ${JSON.stringify(result)}`) + : logger.debug(`[${method}] Success`) + } return result }) } @@ -529,7 +561,6 @@ async function reactToCommands (userId: string, callback: ICallback): Promise 0) { const changedCommand = changedCommandQuery.result[0] if (Array.isArray(changedCommand.args)) { - logger.info(`[received] Command ${ changedCommand.args[0].cmd.key }`) callback(null, changedCommand.args[0]) } else { logger.debug('[received] Update without message args') @@ -545,7 +576,7 @@ async function respondToCommands (userId: string): Promise { commandLastReadTime = new Date() // init before any message read await reactToCommands(userId, async (err, command) => { if (err) { - logger.error(`Unable to receive commands ${JSON.stringify(err)}`) + logger.error(`Unable to receive command ${command.cmd.key}. ${JSON.stringify(err)}`) throw err } @@ -555,8 +586,12 @@ async function respondToCommands (userId: string): Promise { // Ignore commands in stream that aren't new if (currentReadTime <= commandLastReadTime) return + // Only log the command when needed + if (silentClientCommands.indexOf(command.cmd.key) === -1) { + logger.info(`[Command] Received command '${command.cmd.key}' at ${currentReadTime}`) + } + // At this point, command has passed checks and can be responded to - logger.info(`[Command] Received command '${command.cmd.key}' at ${currentReadTime}`) commandLastReadTime = currentReadTime // Processing completed, call callback to respond to command @@ -574,6 +609,9 @@ async function commandHandler (command: IClientCommand): Promise let result: IClientCommandResponse = { success: true } + // Only log the command when needed + const shouldLog: boolean = silentClientCommands.indexOf(command.cmd.key) === -1 + try { const handler = commandHandlers[command.cmd.key] switch (command.cmd.key) { @@ -581,6 +619,10 @@ async function commandHandler (command: IClientCommand): Promise case 'heartbeat': break + case 'getLogs': + result.logs = logs + break + // SDK-level command to pause the message stream, interrupting all messages from the server case 'pauseMessageStream': subscriptions.map((s: ISubscription) => (s._name === _messageCollectionName ? unsubscribe(s) : undefined)) @@ -605,17 +647,25 @@ async function commandHandler (command: IClientCommand): Promise // If command is not at the SDK-level, it tries to call a handler added by the user default: if (handler) { + if (shouldLog) logger.info(`[Command] Calling custom handler of command '${command.cmd.key}'`) result = await handler(command) } else { throw Error('Handler not found') } } } catch (err) { + logger.info(`[Command] Error on handling of '${command.cmd.key}'. ${JSON.stringify(err)}`) result.success = false result.error = err } - asyncCall('replyClientCommand', [command._id, result]).catch(() => {/**/}) + try { + if (shouldLog) logger.info(`[Command] Replying to command '${command.cmd.key}' with result ${JSON.stringify(result)}`) + await asyncCall('replyClientCommand', [command._id, result]) + if (shouldLog) logger.info(`[Command] Successful reply to command '${command.cmd.key}'`) + } catch (err) { + logger.info(`[Command] Failed to reply to command'${command.cmd.key}'. Error: ${JSON.stringify(err)}`) + } } /** diff --git a/src/types/intercept-stdout.ts b/src/types/intercept-stdout.ts new file mode 100644 index 0000000..84477c6 --- /dev/null +++ b/src/types/intercept-stdout.ts @@ -0,0 +1 @@ +declare module 'intercept-stdout' \ No newline at end of file From a0a55acc6be9f7a23f5799fb8de44fb180c8d504 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 11 Jul 2018 23:43:02 -0300 Subject: [PATCH 10/16] Fix lint error by adding new line to end of file --- src/types/intercept-stdout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/intercept-stdout.ts b/src/types/intercept-stdout.ts index 84477c6..31c6f64 100644 --- a/src/types/intercept-stdout.ts +++ b/src/types/intercept-stdout.ts @@ -1 +1 @@ -declare module 'intercept-stdout' \ No newline at end of file +declare module 'intercept-stdout' From 699d360bb6fd3f756046db94f13b2021652fe063 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Thu, 12 Jul 2018 19:55:17 -0300 Subject: [PATCH 11/16] Change log tag used on ClientCommand related logs --- src/lib/driver.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 4ef8510..3726f94 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -563,7 +563,7 @@ async function reactToCommands (userId: string, callback: ICallback): Promise { // Only log the command when needed if (silentClientCommands.indexOf(command.cmd.key) === -1) { - logger.info(`[Command] Received command '${command.cmd.key}' at ${currentReadTime}`) + logger.info(`[ClientCommand] Received '${command.cmd.key}' at ${currentReadTime}`) } // At this point, command has passed checks and can be responded to @@ -647,24 +647,24 @@ async function commandHandler (command: IClientCommand): Promise // If command is not at the SDK-level, it tries to call a handler added by the user default: if (handler) { - if (shouldLog) logger.info(`[Command] Calling custom handler of command '${command.cmd.key}'`) + if (shouldLog) logger.info(`[ClientCommand] Calling custom handler of command '${command.cmd.key}'`) result = await handler(command) } else { throw Error('Handler not found') } } } catch (err) { - logger.info(`[Command] Error on handling of '${command.cmd.key}'. ${JSON.stringify(err)}`) + logger.info(`[ClientCommand] Error on handling of '${command.cmd.key}'. ${JSON.stringify(err)}`) result.success = false result.error = err } try { - if (shouldLog) logger.info(`[Command] Replying to command '${command.cmd.key}' with result ${JSON.stringify(result)}`) + if (shouldLog) logger.info(`[ClientCommand] Replying to '${command.cmd.key}' with result ${JSON.stringify(result)}`) await asyncCall('replyClientCommand', [command._id, result]) - if (shouldLog) logger.info(`[Command] Successful reply to command '${command.cmd.key}'`) + if (shouldLog) logger.info(`[ClientCommand] Successful reply to command '${command.cmd.key}'`) } catch (err) { - logger.info(`[Command] Failed to reply to command'${command.cmd.key}'. Error: ${JSON.stringify(err)}`) + logger.info(`[ClientCommand] Failed to reply to command'${command.cmd.key}'. Error: ${JSON.stringify(err)}`) } } From dcef2dde2020fecc12d30531fa9e1b2da8655218 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Mon, 16 Jul 2018 16:51:01 -0300 Subject: [PATCH 12/16] Calls setCustomClientData on each client-commands stream reconnect --- src/lib/driver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 3726f94..5e8771e 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -563,7 +563,9 @@ async function reactToCommands (userId: string, callback: ICallback): Promise Date: Mon, 16 Jul 2018 21:13:33 -0300 Subject: [PATCH 13/16] Improve comments and logs regarding ClientCommands --- src/lib/driver.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 5e8771e..e1a5dbe 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -40,7 +40,7 @@ const Asteroid: IAsteroid = createClass([immutableCollectionMixin]) // ----------------------------------------------------------------------------- /** - * Intercept all logging going to stdout and store the last 100 entries + * Intercept all logging going to stdout and store the last maxLogSize entries * That is the array sent to the server when the client receives a ClientCommand * getLogs */ @@ -563,8 +563,8 @@ async function reactToCommands (userId: string, callback: ICallback): Promise { commandLastReadTime = new Date() // init before any message read await reactToCommands(userId, async (err, command) => { if (err) { - logger.error(`Unable to receive command ${command.cmd.key}. ${JSON.stringify(err)}`) + logger.error(`[ClientCommand] Unable to receive command ${command.cmd.key}. ${JSON.stringify(err)}`) throw err } @@ -621,6 +621,7 @@ async function commandHandler (command: IClientCommand): Promise case 'heartbeat': break + // SDK-level command to reply with the latest maxLogSize logs case 'getLogs': result.logs = logs break @@ -679,11 +680,11 @@ async function commandHandler (command: IClientCommand): Promise export function registerCommandHandler (key: string, callback: IClientCommandHandler) { const currentHandler = commandHandlers[key] if (currentHandler) { - logger.error(`[Command] Command '${key}' already has a handler`) - throw Error(`[Command] Command '${key}' already has a handler`) + logger.error(`[ClientCommand] Command '${key}' already has a handler`) + throw Error(`[ClientCommand] Command '${key}' already has a handler`) } - logger.info(`[Command] Registering handler for command '${key}'`) + logger.info(`[ClientCommand] Registering handler for command '${key}'`) commandHandlers[key] = callback } From 354c97e3c53938f998578993e372b50563532fd8 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Tue, 24 Jul 2018 19:19:56 -0300 Subject: [PATCH 14/16] Add stack of clients to send as client info to the server Each client has a name and a version Gets version of SDK from package.json --- src/lib/driver.ts | 5 ++++- src/lib/settings.ts | 2 ++ src/types/Package.d.ts | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/types/Package.d.ts diff --git a/src/lib/driver.ts b/src/lib/driver.ts index e1a5dbe..396b7cb 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -125,7 +125,10 @@ export let silentMethods: Array = ['replyClientCommand'] * Custom Data set by the client that is using the SDK */ export let customClientData: object = { - framework: 'Rocket.Chat JS SDK', + stack: [{ + name: 'Rocket.Chat js.SDK', + version: settings.version + }], canPauseResumeMsgStream: true, canListenToHeartbeat: true, canGetStatistics: true, diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 9374d0f..6260508 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -1,3 +1,5 @@ +// Version of the package +export { version } from '../../package.json' // Login settings - LDAP needs to be explicitly enabled export let username = process.env.ROCKETCHAT_USER || 'bot' diff --git a/src/types/Package.d.ts b/src/types/Package.d.ts new file mode 100644 index 0000000..105518f --- /dev/null +++ b/src/types/Package.d.ts @@ -0,0 +1,3 @@ +declare module '*.json' { + export let version: string +} From 67e32864dacb0c8f6fda24110b2a4cf4b3872db5 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 25 Jul 2018 17:16:40 -0300 Subject: [PATCH 15/16] Update documentation of ClientCommands --- README.md | 19 ++++++++++++++----- src/lib/settings.ts | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 70516e1..d86b3fc 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,10 @@ If `allPublic` is true, the `rooms` option will be ignored. Set additional data about the client using the SDK to be sent to the server. -It must be called before the `driver.login()` function, otherwise it will have no effect. +In order to add information about your client on the clients array, you must push directly to the +variable before calling the function. + +It must be called before the `driver.login()` function, since the data is sent right after login. Useful only when adding new features in Rocket.Chat that depend on the client. @@ -282,14 +285,19 @@ do not have this feature, so the bot manager interface in Rocket.Chat will have to differentiate between them, hence the need to define its data. ``` +driver.customClientData.clients.push({ + name: 'ExampleBotFramework Rocket.Chat Adapter', + version: '0.4.2' +}) + driver.setCustomClientData({ framework: 'ExampleBotFramework', canResetConnection: true -}); +}) ``` -Then, Rocket.Chat's interface will check if the bot is able to reset its connection and -show an UI to allow the admin to do that. +Then, Rocket.Chat's interface can check if the bot is able to reset its connection before enabling +an UI with that feature. ### `driver.registerCommandHandler(key, callback)` @@ -310,7 +318,7 @@ The `callback` receives a `ClientCommand` object as the first parameter and retu `ClientCommandResponse` object structure: - ClientCommandResponse.success: Boolean indicating the success status of the command -- ClientCommandResponse.msg: Message response to the command +- Along with any other relevant property to the response ### `driver.asyncCall(method, params)` @@ -601,6 +609,7 @@ rocketchat.driver.connect({ host: 'localhost:3000' }, function (err, asteroid) { | `ROOM_CACHE_MAX_AGE` | Max age of cache for room lookups | | `DM_ROOM_CACHE_SIZE` | Size of cache for Direct Message room lookups | | `DM_ROOM_CACHE_MAX_AGE`| Max age of cache for DM lookups | +| `WAIT_CLIENT_COMMANDS` | Wait subscription of ClientCommands before login finishes. | **Test configs** | | | `ADMIN_USERNAME` | Admin user password for API | | `ADMIN_PASS` | Admin user password for API | diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 6260508..fe60b51 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -31,4 +31,4 @@ export let roomCacheMaxAge = 1000 * parseInt(process.env.ROOM_CACHE_MAX_AGE || ' export let dmCacheMaxSize = parseInt(process.env.DM_ROOM_CACHE_SIZE || '10', 10) export let dmCacheMaxAge = 1000 * parseInt(process.env.DM_ROOM_CACHE_MAX_AGE || '100', 10) -export let waitForClientCommands = process.env.ROCKETCHAT_WAIT_CLIENT_COMMANDS +export let waitForClientCommands = process.env.WAIT_CLIENT_COMMANDS From a0dac2d1a281cc93cf55ddff6c9eb1b611ac22d1 Mon Sep 17 00:00:00 2001 From: Mikael Mello Date: Wed, 25 Jul 2018 18:44:02 -0300 Subject: [PATCH 16/16] Improvoe how the client details are added to the client stack --- README.md | 20 ++++++++++++++++---- src/config/commandInterfaces.ts | 13 +++++++++++++ src/lib/driver.ts | 14 ++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d86b3fc..fb46280 100644 --- a/README.md +++ b/README.md @@ -268,13 +268,25 @@ but have not been joined yet this method will join to those rooms automatically. If `allPublic` is true, the `rooms` option will be ignored. +### `driver.addClientToStack(clientDetails)` + +Adds details about the client using the SDK to the stack of clients. + +It must be called before the `driver.login()` function, since the data is sent right after login. + +clientDetails has a `name` and a `version` field, both strings. + +``` +driver.addClientToStack({ + name: 'ExampleBotFramework Rocket.Chat Adapter', + version: '0.4.2' +}) +``` + ### `driver.setCustomClientData(clientData)` Set additional data about the client using the SDK to be sent to the server. -In order to add information about your client on the clients array, you must push directly to the -variable before calling the function. - It must be called before the `driver.login()` function, since the data is sent right after login. Useful only when adding new features in Rocket.Chat that depend on the client. @@ -285,7 +297,7 @@ do not have this feature, so the bot manager interface in Rocket.Chat will have to differentiate between them, hence the need to define its data. ``` -driver.customClientData.clients.push({ +driver.addClientToStack({ name: 'ExampleBotFramework Rocket.Chat Adapter', version: '0.4.2' }) diff --git a/src/config/commandInterfaces.ts b/src/config/commandInterfaces.ts index c8cc331..36c655d 100644 --- a/src/config/commandInterfaces.ts +++ b/src/config/commandInterfaces.ts @@ -35,3 +35,16 @@ export interface IClientCommandHandler { export interface IClientCommandHandlerMap { [key: string]: IClientCommandHandler } + +/* + * Structure of the object of client data + */ +export interface ICustomClientData { + stack: Array, + [key: string]: any +} + +export interface IClientDetails { + name: string, + version: string +} \ No newline at end of file diff --git a/src/lib/driver.ts b/src/lib/driver.ts index 396b7cb..bda312a 100644 --- a/src/lib/driver.ts +++ b/src/lib/driver.ts @@ -20,7 +20,9 @@ import { IClientCommand, IClientCommandResponse, IClientCommandHandler, - IClientCommandHandlerMap + IClientCommandHandlerMap, + ICustomClientData, + IClientDetails } from '../config/commandInterfaces' /** Collection names */ @@ -124,7 +126,7 @@ export let silentMethods: Array = ['replyClientCommand'] /** * Custom Data set by the client that is using the SDK */ -export let customClientData: object = { +export let customClientData: ICustomClientData = { stack: [{ name: 'Rocket.Chat js.SDK', version: settings.version @@ -699,6 +701,14 @@ export function setCustomClientData (clientData: object) { Object.assign(customClientData, clientData) } +/** + * Add client information to the client stack + * @param clientData Object containing additional data about the client using the SDK + */ +export function addClientToStack (clientDetails: IClientDetails) { + customClientData.stack.push(clientDetails) +} + /** * Get every new element added to DDP in Asteroid (v2) * @todo Resolve this functionality within Rocket.Chat with team