Skip to content

Instantly share code, notes, and snippets.

@timkinnane
Forked from mikaelmello/clientcommands.md
Last active June 26, 2018 00:28
Show Gist options
  • Save timkinnane/d7f9dce8f437bd6a550b304c973437ec to your computer and use it in GitHub Desktop.
Save timkinnane/d7f9dce8f437bd6a550b304c973437ec to your computer and use it in GitHub Desktop.
Client Commands

I'd like to introduce a new utility called Client Commands, a solution created to allow the Rocket.Chat server to trigger actions in subscriber clients (bots and possibly other websocket clients). This is handled at the adapter and/or SDK level, not by final users (e.g. normal bot developers).

The problem

Bots subscribe to a message stream and respond to message events, but there's no way for the server to prompt them to do anything other than that.

In order to provide a range of new management features for administrating bot clients, getting data or triggering any non message response action, we need to send data to be interpreted by the client as a command. Such data, identified here by ClientCommands, could not be sent through the normal message stream, because:

  • a) it would go to everyone, not just bot clients
  • b) it would need hack workarounds to filter commands from normal message data
  • c) it would be kept forever, bloating message collection storage

While working on my GSoC project, improving the integration of Rocket.Chat with bots, I needed to find a way to send commands directly from the server to a specific bot, without being accessible by other accounts, in order to get data from the bot and make it possible to manage the bot from an admin screen inside Rocket.Chat, not related to the normal commands used by users in the chat rooms.

^ Last paragraph is now mostly covered already. Maybe cut it down, or remove?

The solution

While some features could be added with specific implementations, ClientCommands provides flexibility, making it useful for multiple cases. It keeps code DRY by providng a core utility to developers so they're not re-inventing a range of inconsistent solutions. It should be a fairly obvious architecture to learn as well, accelerating the pace of development for new client management features.

Some management features that can be implemented with the ClientCommands are:

  1. Check bot's aliveness - As commented, it could be a simple endpoint, probably HTTP. But we could send instead a simple 'heartbeat' ClientCommand and the client will send a response indicating that it is alive.
  2. Pause/resume bots - Providing a single interface for an admin to pause the operation of bots can be done by sending a 'pauseMessageStream' command, it will be handled in the SDK that will stop receiving any messages from the server. This can be useful for admins that don't have access to where the bot is being hosted and need to stop it.
  3. Clear cache / reset memory - This is theoretical, but could be something useful for adapters that have cache that might not be working correctly or contain data that needs to be deleted (e.g. user was removed from server, for GDPR compliance), so the an admin can send a command to the adapter, that will then clear its cache and reply indicating success or failure. While this particular management option might not be useful to all, it shows that you can manage anything implemented by the SDK/Adapter, as long as it listens to the specified command.

Useful features that improve UX and can be added with ClientCommands:

  1. Autocomplete bots commands - Since each bot framework behaves differently when asking for the available commands, even if they all use the 'help' message, the response format is different. Each adapter can listen to the 'availableCommands' ClientCommand, and knowing its architecture, get the commands in an array and reply. On server-side, by sending the ClientCommand 'availableCommands', we can store the response in the bot User model with a common syntax for all adapters, making it possible for the server to know which commands the bot can respond to.
  2. Callbacks - An adapter might provide an interface to add callbacks to be called upon conversational context. As approached by Tim here

The implementation

While a simple publication and collection would be enough to send ClientCommands, there is an overhead when dealing with collections that might become a problem in larger systems.

Therefore, after discussing with Sing Li, meteor-streamer was picked to handle the communication from server to clients, it does not use a collection on server-side so it reduces a lot of overhead.

The package rocketchat-client-commands has a function to send ClientCommands, a method to reply to ClientCommands (called by the client) and a startup file to set up the stream.

Sending a command (in the server)

To send a ClientCommand all you need to do is to call the function via RocketChat.sendClientCommand(user, command [, timeout]), where:

  • user: Object of the target user, containing the _id and username properties
  • command: Object of the command, where it must have at least the key property, a unique string
  • timeout: Optional parameter of the timeout for the client to reply the command, defaults to 5 seconds.
  • It returns a promise that resolves with a reply or rejects with a timeout error

This function adds a listener for a 'client-command-response-<_id of the command>' event in the internel EventEmitter called RocketChat and initiates a timeout.

If the listener is called, the timeout is cleared and the function resolves with the event's first parameter, the ClientCommand response's object.

If the listener is not called within the timeout period, the timeout removes the listener and rejects the promise.

Example:

const bot = {
  _id: 'e9e89dqw823d81',
  username: 'bot'
};

RocketChat.sendClientCommand(bot, { key: 'pauseMessageStream' })
  .then((response) => {
    // client replied the command
    console.log(response);
  })
  .catch((err) => {
    // client did not reply within 5 seconds
    console.log(err);
  });

Replying to commands (in the client)

Once the client receives the command and acts according to it, it should call the replyClientCommand method (on the server, via SDK driver) with two parameters, the _id of the command and the response object.

The method will then emit a 'client-command-response-<_id of the command>' event via RocketChat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment