Writing a Hubot Adapter for Bitrix

Back in the day, we installed hubot on our Hipchat developer chat. When hipchat got sold to Atlassian we jumped ship and chose Slack instead. Years later the company chose to invest heavily in having a shared system across all division handling tasks and CRM primarily - but since this tool also had a chat function, we dropped the slack instance, losing access to all the nice Hubot integrations we wrote over the years. Photo by Rock’n Roll Monkey / Unsplash As you may have already guessed the system we chose was Bitrix.

Today we added yet more developers into our shared chat; I got to think about dead old Grottebot (the name for our Hubot instance).

Inspecting requests from Bitrix

I started by setting up a chatbot in our system and sent its requests to a requestbin so that I could inspect them.

Then I added the bot to a chat, send it some messages, and inspected the data, to get acquainted with which data we needed to manage.

Looking into existing adapters

I then took a look at the existing adapters to try to commonalities and differences. And quickly found that this might actually be quite simple.

We need a way to get messages from the bitrix chats; this just requires we set up a webhook in our hubot instance, that can read the webhook calls from Bitrix; and then we need to implement the reply and send methods of the adapter to send stuff back to the chats.

Initial implementation

Since this is a simple one-off adapter for use in an internal tool, I found it overkill to create a separate package; however making a local package which conforms to the hubot adapter naming conventions proved to take a little time.

I ended up with the followinging entry in package.json

{
  "dependencies": {
    "hubot": "^3.3.2",
    "hubot-bitrix": "file:hubot-bitrix",
	.... other dependencies
  }
}

I then implemented the required methods in the adapter

{Robot,Adapter,TextMessage,User} = require 'hubot'

BitrixBotClient = require './bitrix_bot_client'

class BitrixAdapter extends Adapter

  constructor: ->
    super
    @robot.logger.info "Constructor"
    @client = new BitrixBotClient(@robot)

  send: (envelope, strings...) ->
    @client.send(envelope, strings...)

  reply: (envelope, strings...) ->
    @client.reply(envelope, strings...)

  run: ->
    @robot.logger.info "Run"
    @emit "connected"
    @robot.router.post '/some/secret/path/to/the/webhook/endpoint', (req, res) =>
      @client.parseMessage req

      res.status(200).send ok: true

exports.use = (robot) ->
  new BitrixAdapter robot

And I just needed to implement the BitrixBotClient class, which does all of the heavy lifting.

{Robot, Adapter, TextMessage} = require "hubot"

class BitrixBotClient
  constructor: (robot) ->
    @robot = robot

  parseMessage: (request) ->
    if request.body.event == 'ONIMBOTJOINCHAT'
      return @parseJoinRequest(request)
    if request.body.event == 'ONIMBOTMESSAGEADD'
      return @parseMessageRequest(request)

  parseJoinRequest: (request) ->
    @robot.brain.set 'bitrix', {
      base_url: process.env.BITRIX_REST_URL, # This will be linked to some user; so beware
      client_id: request.body.auth.application_token,
      bot_id: request.body.data.PARAMS.BOT_ID
    }
    @sendMessage(request.body.data.PARAMS.DIALOG_ID, "Bot entered the building")
    return undefined

  parseMessageRequest: (request) ->
    text = request.body.data.PARAMS.MESSAGE
    messageId = request.body.ts
    @createUser request, (user) =>
      message = new TextMessage user, text.trim(), messageId
      @robot.receive(message) if message?

  createUser: (request, cb) ->
    userId = request.body.data.USER.ID
    userName = request.body.data.USER.NAME
    roomId = request.body.data.PARAMS.DIALOG_ID

    cb @robot.brain.userForId(userId, name: userName, room: roomId)


  send: (envelope, strings...) ->
    for message in strings
      @sendMessage envelope.room, message

  reply: (envelope, strings...) ->
    for message in strings
      @sendMessage envelope.room, "<@#{envelope.user.name}> #{message}"

  sendMessage: (chatId, message) ->
    bitrix = @robot.brain.get('bitrix')
    url = bitrix.base_url + '/imbot.message.add'
    data = JSON.stringify({
      'BOT_ID': bitrix.bot_id,
      'DIALOG_ID': chatId,
      'MESSAGE': message, # Message text
      "CLIENT_ID": bitrix.client_id,
    })

    console.log("SEND MESSAGE", data)

    @robot.http(url)
      .header('Content-Type', 'application/json')
        .post(data) (err, res, body) ->
          result = JSON.parse(body)
          console.log(result)


module.exports = BitrixBotClient

That is the complete solution currently.

Missing features

Currently this sollution has some minor problems which make it work differently than hubot normally does.

The chatbot api in Bitrix does not allow the chatbot to get all messages in a groupchat, only the ones specifically aimed at it. So (assuming the botname in bitrix is called Hubot)

When want a mention in hubot terms (the respond function in plugins) you need to first mention the bot so that the bot gets the message, and then mention it again, so that the bot knows it is a respond-function that should be called.

We have solved this by making all apis use the hear-method instead.

That also means that the bot can never “hear” anything without getting mentioned. Sadly; because it has it’s uses. Even though most of the uses we had for it in our internal chats was to annoy the hell out of people using specific phrases.

Related Posts