Full Code of hipchat/hubot-hipchat for AI

master 19199efee358 cached
9 files
38.1 KB
10.4k tokens
1 requests
Download .txt
Repository: hipchat/hubot-hipchat
Branch: master
Commit: 19199efee358
Files: 9
Total size: 38.1 KB

Directory structure:
gitextract_dlevzx9j/

├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
└── src/
    ├── connector.coffee
    ├── hipchat.coffee
    ├── promises.coffee
    └── test.coffee

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
.DS_Store*


================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and
this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [2.13.0] - 2017-11-21
### Changed
- `message.user` returns an object from the brain.
  - Changed by [Andrew Widdersheim](https://github.com/awiddersheim) in Pull Request [#277](https://github.com/hipchat/hubot-hipchat/pull/277).
- `user.room` always returns a JID.
  - Changed by [Andrew Widdersheim](https://github.com/awiddersheim) in Pull Request [#277](https://github.com/hipchat/hubot-hipchat/pull/277).

### Removed
- `reply_to` from `envelope.user` and `message.user`.
  - Removed by [Andrew Widdersheim](https://github.com/awiddersheim) in Pull Request [#277](https://github.com/hipchat/hubot-hipchat/pull/277).

### Security
- Updated `node-xmpp-client` to 3.2.0.
  - Updated by [Sam](https://github.com/samcday) in Pull Request [#272](https://github.com/hipchat/hubot-hipchat/pull/272).


================================================
FILE: LICENSE
================================================
Copyright (c) HipChat, Inc.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# hubot-hipchat

## Quickstart: Hubot for HipChat on Heroku

### The Easy Way

Try deploying the "[Triatomic](https://github.com/hipchat/triatomic)" starter HipChat Hubot project to Heroku. Once you have it running, simply clone it and customize its scripts as you please.

[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/hipchat/triatomic)

### The "I do it myself!" Way

This is a HipChat-specific version of the more general [instructions in the Hubot wiki](https://github.com/github/hubot/wiki/Deploying-Hubot-onto-Heroku). Some of this guide is derived from Hubot's [general set up instructions](https://hubot.github.com/docs). You may wish to see that guide for more information about the general use and configuration of Hubot, in addition to details for deploying it to environments other than Heroku.

1. From your existing HipChat account add your bot as a [new user](http://help.hipchat.com/knowledgebase/articles/64413-how-do-i-add-invite-new-users-). Stay signed in to the account - we'll need to access its account settings later.

1. If you are using Linux, make sure libexpat is installed:

        % apt-get install libexpat1-dev

1. You will need [node.js](https://nodejs.org). Joyent has an [excellent blog post on how to get those installed](https://www.joyent.com/blog/installing-node-and-npm), so we'll omit those details here.  You'll want node.js 0.12.x or later.

1. Once node and npm are ready, we can install the hubot generator:

        %  npm install -g yo generator-hubot

1. This will give us the hubot yeoman generator. Now we can make a new directory, and generate a new instance of hubot in it, using this Hubot HipChat adapter. For example, if we wanted to make a bot called myhubot:

        % mkdir myhubot
        % cd myhubot
        % yo hubot --adapter hipchat

1. At this point, you'll be asked a few questions about the bot you are creating. When you finish answering, yeoman will download and install the necessary dependencies. (If the generator hangs, a workaround is to re-run without the `--adapter hipchat` argument, accept the default `campfire` value when prompted, and then re-run yet again, again with the hipchat adapter argument, accepting the prompts to overwrite existing files. This appears to be an issue with the generator itself.)

1. Turn your `hubot` directory into a git repository:

        % git init
        % git add .
        % git commit -m "Initial commit"

1. Install the [Heroku command line tools](http://devcenter.heroku.com/articles/heroku-command) if you don't have them installed yet.

1. Create a new Heroku application and (optionally) rename it:

        % heroku create our-company-hubot

1. Add [Redis To Go](http://devcenter.heroku.com/articles/redistogo) to your Heroku app:

        % heroku addons:create redistogo:nano --app our-company-hubot

1. Configure it:

      You will need to set a configuration variable if you are hosting on the free Heroku plan.

        % heroku config:add HEROKU_URL=http://our-company-hubot.herokuapp.com

      Where the URL is your Heroku app's URL (shown after running `heroku create`, or `heroku rename`).

      Set the JID to the "Jabber ID" shown on your bot's [XMPP/Jabber account settings](https://www.hipchat.com/account/xmpp):

        % heroku config:add HUBOT_HIPCHAT_JID="..."

      Set the password to the password chosen when you created the bot's account.

        % heroku config:add HUBOT_HIPCHAT_PASSWORD="..."

      If using HipChat Server Beta, you need to set xmppDomain to btf.hipchat.com.

        % heroku config:add HUBOT_HIPCHAT_XMPP_DOMAIN="btf.hipchat.com"

1. Deploy and start the bot:

        % git push heroku master
        % heroku ps:scale web=1

      This will tell Heroku to run 1 of the `web` process type which is described in the `Procfile`.

1. You should see the bot join all rooms it has access to (or are specified in HUBOT\_HIPCHAT\_ROOMS, see below). If not, check the output of `heroku logs`. You can also use `heroku config` to check the config vars and `heroku restart` to restart the bot. `heroku ps` will show you its current process state.

1. Assuming your bot's name is "Hubot", the bot will respond to commands like "@hubot help". It will also respond in 1-1 chat ("@hubot" must be omitted there, so just use "help" for example).

1. To configure the commands the bot responds to, you'll need to edit the `hubot-scripts.json` file ([valid script names here](https://github.com/github/hubot-scripts/tree/master/src/scripts)) or add scripts to the `scripts/` directory.

1. To deploy an updated version of the bot, simply commit your changes and run `git push heroku master` again.

Bonus: Add a notification hook to Heroku so a notification is sent to a room whenever the bot is updated: https://marketplace.atlassian.com/plugins/com.heroku.hipchat/cloud/overview

## Scripting Gotchas
`robot.messageRoom` syntax is as follows
```
robot.messageRoom("1234_room@conf.hipchat.com", "message");
```

## Adapter configuration

This adapter uses the following environment variables:

### HUBOT\_HIPCHAT\_JID

This is your bot's Jabber ID which can be found in your [XMPP/Jabber account settings](https://www.hipchat.com/account/xmpp). It will look something like `123_456@chat.hipchat.com`

### HUBOT\_HIPCHAT\_PASSWORD

This is the password for your bot's HipChat account.

### HUBOT\_HIPCHAT\_ROOMS

Optional. This is a comma separated list of room JIDs that you want your bot to join. You can leave this blank or set it to "All" to have your bot join every room. Room JIDs look like "123_development@conf.hipchat.com" and can be found in the [XMPP/Jabber account settings](https://www.hipchat.com/account/xmpp) - just add "@conf.hipchat.com" to the end of the room's "XMPP/Jabber Name".

### HUBOT\_HIPCHAT\_ROOMS\_BLACKLIST

Optional. This is a comma separated list of room JIDs that should not be joined.

### HUBOT\_HIPCHAT\_JOIN\_ROOMS\_ON\_INVITE

Optional. Setting to `false` will prevent the HipChat adapter from auto-joining rooms when invited.

### HUBOT\_HIPCHAT\_JOIN\_PUBLIC\_ROOMS

Optional. Setting to `false` will prevent the HipChat adapter from auto-joining rooms that are publicly available (i.e. guest-accessible).

### HUBOT\_HIPCHAT\_HOST

Optional. Use to force the host to open the XMPP connection to.

### HUBOT\_HIPCHAT\_XMPP\_DOMAIN

Optional. Set to btf.hipchat.com if using HipChat Server.

### HUBOT\_LOG\_LEVEL

Optional. Set to `debug` to enable detailed debug logging.

### HUBOT\_HIPCHAT\_RECONNECT

Optional. Seting to `false` will prevent the HipChat adapter from auto-reconnecting if it detects a server error or disconnection.

## Running locally

To run locally on OSX or Linux you'll need to set the required environment variables and run the `bin/hubot` script. An example script to run the bot might look like:

    #!/bin/bash

    export HUBOT_HIPCHAT_JID="..."
    export HUBOT_HIPCHAT_PASSWORD="..."

    bin/hubot --adapter hipchat

But be aware that credentials normally shouldn't be checked into your vcs.


================================================
FILE: package.json
================================================
{
  "name": "hubot-hipchat",
  "version": "2.13.0",
  "author": "HipChat <support@hipchat.com>",
  "description": "A HipChat adapter for Hubot",
  "keywords": [
    "hipchat",
    "hubot",
    "xmpp",
    "chat",
    "bot"
  ],
  "repository": {
    "type": "git",
    "url": "http://github.com/hipchat/hubot-hipchat.git"
  },
  "license": "MIT",
  "main": "./src/hipchat",
  "engines": {
    "node": "~0.12.2"
  },
  "dependencies": {
    "node-xmpp-client": "^3.2.0",
    "rsvp": "~1.2.0",
    "underscore": "~1.4.4"
  }
}


================================================
FILE: src/connector.coffee
================================================
# Modified from [Wobot](https://github.com/cjoudrey/wobot).
#
# Copyright (C) 2011 by Christian Joudrey
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

{EventEmitter} = require "events"
fs = require "fs"
{bind, isString, isRegExp} = require "underscore"
xmpp = require 'node-xmpp-client'

# Parse and cache the node package.json file when this module is loaded
pkg = do ->
  data = fs.readFileSync __dirname + "/../package.json", "utf8"
  JSON.parse(data)

# ##Public Connector API
module.exports = class Connector extends EventEmitter

  # This is the `Connector` constructor.
  #
  # `options` object:
  #
  #   - `jid`: Connector's Jabber ID
  #   - `password`: Connector's HipChat password
  #   - `host`: Force host to make XMPP connection to. Will look up DNS SRV
  #        record on JID's host otherwise.
  #   - `caps_ver`: Name and version of connector. Override if Connector is being used
  #        to power another connector framework (e.g. Hubot).
  #   - `logger`: A logger instance.
  constructor: (options={}) ->
    @once "connect", (->) # listener bug in Node 0.4.2
    @setMaxListeners 0

    @jabber = null
    @keepalive = null
    @name = null
    @plugins = {}
    @iq_count = 1 # current IQ id to use
    @logger = options.logger

    # add a JID resource if none was provided
    jid = new xmpp.JID options.jid
    jid.resource = "hubot-hipchat" if not jid.resource

    @jid = jid.toString()
    @password = options.password
    @host = options.host
    @caps_ver = options.caps_ver or "hubot-hipchat:#{pkg.version}"
    @xmppDomain = options.xmppDomain
    @bosh = options.bosh

    # Multi-User-Conference (rooms) service host. Use when directing stanzas
    # to the MUC service.
    @mucDomain = "conf.#{if @xmppDomain then @xmppDomain else 'hipchat.com'}"

    @disconnecting = false
    @onError @disconnect

  # Connects the connector to HipChat and sets the XMPP event listeners.
  connect: ->
    @jabber = new xmpp.Client
      jid: @jid,
      password: @password,
      host: @host
      bosh: @bosh

    @jabber.on "error", bind(onStreamError, @)
    @jabber.on "online", bind(onOnline, @)
    @jabber.on "stanza", bind(onStanza, @)
    @jabber.on "offline", bind(onOffline, @)
    @jabber.on "close", bind(onClose, @)

    # debug network traffic
    do =>
      @jabber.on "data", (buffer) =>
        @logger.debug "  IN > %s", buffer.toString()
      _send = @jabber.send
      @jabber.send = (stanza) =>
        @logger.debug " OUT > %s", stanza
        _send.call @jabber, stanza

  # Disconnect the connector from HipChat, remove the anti-idle and emit the
  # `disconnect` event.
  disconnect: =>
    # since we're going to emit "disconnect" event in the end, we should prevent ourself from handling it here
    if ! @disconnecting
      @disconnecting = true
      @logger.debug 'Disconnecting here'
      if @keepalive
        clearInterval @keepalive
        delete @keepalive
      @jabber.end()
      @emit "disconnect"
      @disconnecting = false

  # Fetches our profile info
  #
  # - `callback`: Function to be triggered: `function (err, data, stanza)`
  #   - `err`: Error condition (string) if any
  #   - `data`: Object containing fields returned (fn, title, photo, etc)
  #   - `stanza`: Full response stanza, an `xmpp.Element`
  getProfile: (callback) ->
    stanza = new xmpp.Element("iq", type: "get")
      .c("vCard", xmlns: "vcard-temp")
    @sendIq stanza, (err, res) ->
      data = {}
      if not err
        for field in res.getChild("vCard").children
          data[field.name.toLowerCase()] = field.getText()
      callback err, data, res

  # Fetches the rooms available to the connector user. This is equivalent to what
  # would show up in the HipChat lobby.
  #
  # - `callback`: Function to be triggered: `function (err, items, stanza)`
  #   - `err`: Error condition (string) if any
  #   - `rooms`: Array of objects containing room data
  #   - `stanza`: Full response stanza, an `xmpp.Element`
  getRooms: (callback) ->
    iq = new xmpp.Element("iq", to: this.mucDomain, type: "get")
      .c("query", xmlns: "http://jabber.org/protocol/disco#items");
    @sendIq iq, (err, stanza) ->
      rooms = if err then [] else
        # Parse response into objects
        stanza.getChild("query").getChildren("item").map (el) ->
          x = el.getChild "x", "http://hipchat.com/protocol/muc#room"
          # A room
          jid: el.attrs.jid.trim()
          name: el.attrs.name
          id: getInt(x, "id")
          topic: getText(x, "topic")
          privacy: getText(x, "privacy")
          owner: getText(x, "owner")
          guest_url: getText(x, "guest_url")
          is_archived: !!getChild(x, "is_archived")
      callback err, (rooms or []), stanza

  # Fetches the roster (buddy list)
  #
  # - `callback`: Function to be triggered: `function (err, items, stanza)`
  #   - `err`: Error condition (string) if any
  #   - `items`: Array of objects containing user data
  #   - `stanza`: Full response stanza, an `xmpp.Element`
  getRoster: (callback) ->
    iq = new xmpp.Element("iq", type: "get")
      .c("query", xmlns: "jabber:iq:roster")
    @sendIq iq, (err, stanza) ->
      items = if err then [] else usersFromStanza(stanza)
      callback err, (items or []), stanza

  # Updates the connector's availability and status.
  #
  #  - `availability`: Jabber availability codes
  #     - `away`
  #     - `chat` (Free for chat)
  #     - `dnd` (Do not disturb)
  #  - `status`: Status message to display
  setAvailability: (availability, status) ->
    packet = new xmpp.Element "presence", type: "available"
    packet.c("show").t(availability)
    packet.c("status").t(status) if (status)

    # Providing capabilities info (XEP-0115) in presence tells HipChat
    # what type of client is connecting. The rest of the spec is not actually
    # used at this time.
    packet.c "c",
      xmlns: "http://jabber.org/protocol/caps"
      node: "http://hipchat.com/client/bot" # tell HipChat we're a bot
      ver: @caps_ver

    @jabber.send packet

  # Join the specified room.
  #
  # - `roomJid`: Target room, in the form of `????_????@conf.hipchat.com`
  # - `historyStanzas`: Max number of history entries to request
  join: (roomJid, historyStanzas) ->
    historyStanzas = 0 if not historyStanzas
    packet = new xmpp.Element "presence", to: "#{roomJid}/#{@name}"
    packet.c "x", xmlns: "http://jabber.org/protocol/muc"
    packet.c "history",
      xmlns: "http://jabber.org/protocol/muc"
      maxstanzas: String(historyStanzas)
    @jabber.send packet

  # Part the specified room.
  #
  # - `roomJid`: Target room, in the form of `????_????@conf.hipchat.com`
  part: (roomJid) ->
    packet = new xmpp.Element 'presence',
      type: 'unavailable'
      to: "#{roomJid}/#{@name}"
    packet.c 'x', xmlns: 'http://jabber.org/protocol/muc'
    packet.c('status').t('hc-leave')
    @jabber.send packet

  # Send a message to a room or a user.
  #
  # - `targetJid`: Target
  #    - Message to a room: `????_????@conf.hipchat.com`
  #    - Private message to a user: `????_????@chat.hipchat.com`
  # - `message`: Message to be sent to the room
  message: (targetJid, message) ->
    parsedJid = new xmpp.JID targetJid

    if parsedJid.domain is @mucDomain
      packet = new xmpp.Element "message",
        to: "#{targetJid}/#{@name}"
        type: "groupchat"
    else
      packet = new xmpp.Element "message",
        to: targetJid
        type: "chat"
        from: @jid
      packet.c "inactive", xmlns: "http://jabber/protocol/chatstates"
    # we should make sure that the message is properly escaped
    # based on http://unix.stackexchange.com/questions/111899/how-to-strip-color-codes-out-of-stdout-and-pipe-to-file-and-stdout
    message = message.replace(/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]/g, "")  # remove bash color codes
    @logger.debug 'building message'
    @logger.debug message
    packet.c("body").t(message)
    @jabber.send packet

  # Send a topic change message to a room
  #
  # - `targetJid`: Target
  #    - Message to a room: `????_????@conf.hipchat.com`
  # - `message`: Text string that the topic should be set to
  topic: (targetJid, message) ->
    parsedJid = new xmpp.JID targetJid

    packet = new xmpp.Element "message",
      to: "#{targetJid}/#{@name}"
      type: "groupchat"

    packet.c("subject").t(message)
    @jabber.send packet

  # Sends an IQ stanza and stores a callback to be called when its response
  # is received.
  #
  # - `stanza`: `xmpp.Element` to send
  # - `callback`: Function to be triggered: `function (err, stanza)`
  #   - `err`: Error condition (string) if any
  #   - `stanza`: Full response stanza, an `xmpp.Element`
  sendIq: (stanza, callback) ->
    stanza = stanza.root() # work with base element
    id = @iq_count++
    stanza.attrs.id = id;
    @once "iq:#{id}", callback
    @jabber.send stanza

  loadPlugin: (identifier, plugin, options) ->
    if typeof plugin isnt "object"
      throw new Error "Plugin argument must be an object"
    if typeof plugin.load isnt "function"
      throw new Error "Plugin object must have a load function"
    @plugins[identifier] = plugin
    plugin.load @, options
    true

  # ##Events API

  # Emitted whenever the connector connects to the server.
  #
  # - `callback`: Function to be triggered: `function ()`
  onConnect: (callback) -> @on "connect", callback

  # Emitted whenever the connector disconnects from the server.
  #
  # - `callback`: Function to be triggered: `function ()`
  onDisconnect: (callback) -> @on "disconnect", callback

  # Emitted whenever the connector is invited to a room.
  #
  # `onInvite(callback)`
  #
  # - `callback`: Function to be triggered:
  #               `function (roomJid, fromJid, reason, matches)`
  #   - `roomJid`: JID of the room being invited to.
  #   - `fromJid`: JID of the person who sent the invite.
  #   - `reason`: Reason for invite (text)
  onInvite: (callback) -> @on "invite", callback

  # Makes an onMessage impl for the named message event
  onMessageFor = (name) ->
    (condition, callback) ->
      if not callback
        callback = condition
        condition = null
      @on name, ->
        message = arguments[arguments.length - 1]
        if not condition or message is condition
          callback.apply @, arguments
        else if isRegExp condition
          match = message.match condition
          return if not match
          args = [].slice.call arguments
          args.push match
          callback.apply @, args

  # Emitted whenever a message is sent to a channel the connector is in.
  #
  # `onMessage(condition, callback)`
  #
  # `onMessage(callback)`
  #
  # - `condition`: String or RegExp the message must match.
  # - `callback`: Function to be triggered: `function (roomJid, from, message, matches)`
  #   - `roomJid`: Jabber Id of the room in which the message occured.
  #   - `from`: The name of the person who said the message.
  #   - `message`: The message
  #   - `matches`: The matches returned by the condition when it is a RegExp
  onMessage: onMessageFor "message"

  # Emitted whenever a message is sent privately to the connector.
  #
  # `onPrivateMessage(condition, callback)`
  #
  # `onPrivateMessage(callback)`
  #
  # - `condition`: String or RegExp the message must match.
  # - `callback`: Function to be triggered: `function (fromJid, message)`
  onPrivateMessage: onMessageFor "privateMessage"

  # Emitted whenever the connector receives a topic change in a room
  #
  # - `callback`: Function to be triggered: `function ()`
  onTopic: (callback) -> @on "topic", callback

  onEnter: (callback) -> @on "enter", callback

  onLeave: (callback) -> @on "leave", callback

  onRosterChange: (callback) -> @on "rosterChange", callback

  # Emitted whenever the connector pings the server (roughly every 30 seconds).
  #
  # - `callback`: Function to be triggered: `function ()`
  onPing: (callback) -> @on "ping", callback

  # Emitted whenever an XMPP stream error occurs. The `disconnect` event will
  # always be emitted afterwards.
  #
  # Conditions are defined in the XMPP spec:
  #   http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions
  #
  # - `callback`: Function to be triggered: `function(condition, text, stanza)`
  #   - `condition`: XMPP stream error condition (string)
  #   - `text`: Human-readable error message (string)
  #   - `stanza`: The raw `xmpp.Element` error stanza
  onError: (callback) -> @on "error", callback

# ##Private functions

# Whenever an XMPP stream error occurs, this function is responsible for
# triggering the `error` event with the details and disconnecting the connector
# from the server.
#
# Stream errors (http://xmpp.org/rfcs/rfc6120.html#streams-error) look like:
# <stream:error>
#   <system-shutdown xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>
# </stream:error>
onStreamError = (err) ->
  if err instanceof xmpp.Element
    condition = err.children[0].name
    text = err.getChildText "text"
    if not text
      text = "No error text sent by HipChat, see
        http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions
        for error condition descriptions."
    @emit "error", condition, text, err
  else
    @emit "error", null, null, err

# Whenever an XMPP connection is made, this function is responsible for
# triggering the `connect` event and starting the 30s anti-idle. It will
# also set the availability of the connector to `chat`.
onOnline = ->
  @setAvailability "chat"

  ping = =>
    @jabber.send new xmpp.Element('r')
    @emit "ping"

  @keepalive = setInterval ping, 30000

  # Load our profile to get name
  @getProfile (err, data) =>
    if err
      # This isn't technically a stream error which is what the `error`
      # event usually represents, but we want to treat a profile fetch
      # error as a fatal error and disconnect the connector.
      @emit "error", null, "Unable to get profile info: #{err}", null
    else
      # Now that we have our name we can let rooms be joined
      @name = data.fn;
      # This is the name used to @mention us
      @mention_name = data.nickname
      @emit "connect"

# This function is responsible for handling incoming XMPP messages. The
# `data` event will be triggered with the message for custom XMPP
# handling.
#
# The connector will parse the message and trigger the `message`
# event when it is a group chat message or the `privateMessage` event when
# it is a private message.
onStanza = (stanza) ->
  @emit "data", stanza

  if stanza.is "message"
    if stanza.attrs.type is "groupchat"

      return if stanza.getChild "delay"
      fromJid = new xmpp.JID stanza.attrs.from
      fromChannel = fromJid.bare().toString()
      fromNick = fromJid.resource
      # Ignore our own messages
      return if fromNick is @name
      # Look for body msg
      body = stanza.getChildText "body"
      # look for Subject: http://xmpp.org/extensions/xep-0045.html#subject-mod
      subject = stanza.getChildText "subject"
      if body
        # message stanza
        @emit "message", fromChannel, fromNick, body
      else if subject
        # subject stanza
        @emit "topic", fromChannel, fromNick, subject
      else
        # Skip parsing other types and return
        return

    else if stanza.attrs.type is "chat"
      # Message without body is probably a typing notification
      body = stanza.getChildText "body"
      return if not body
      # Ignore chat history
      return if stanza.getChild "delay"
      fromJid = new xmpp.JID stanza.attrs.from
      @emit "privateMessage", fromJid.bare().toString(), body

    else if not stanza.attrs.type
      x = stanza.getChild "x", "http://jabber.org/protocol/muc#user"
      return if not x
      invite = x.getChild "invite"
      return if not invite
      reason = invite.getChildText "reason"
      inviteRoom = new xmpp.JID stanza.attrs.from
      inviteSender = new xmpp.JID invite.attrs.from
      @emit "invite", inviteRoom.bare(), inviteSender.bare(), reason

  else if stanza.is "iq"
    # Handle a response to an IQ request
    event_id = "iq:#{stanza.attrs.id}"
    if stanza.attrs.type is "result"
      @emit event_id, null, stanza
    else if stanza.attrs.type is "set"
      # Check for roster push
      if stanza.getChild("query").attrs.xmlns is "jabber:iq:roster"
        users = usersFromStanza(stanza)
        @emit "rosterChange", users, stanza
    else
      # IQ error response
      # ex: http://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-result
      condition = "unknown"
      error_elem = stanza.getChild "error"
      condition = error_elem.children[0].name if error_elem
      @emit event_id, condition, stanza

  else if stanza.is "presence"
    jid = new xmpp.JID stanza.attrs.from
    room = jid.bare().toString()
    return if not room
    name = stanza.attrs.from.split("/")[1]
    # Fall back to empty string if name isn't reported in presence
    name ?= ""
    type = stanza.attrs.type or "available"
    x = stanza.getChild "x", "http://jabber.org/protocol/muc#user"
    return if not x
    entity = x.getChild "item"
    return if not entity
    from = entity.attrs?.jid
    return if not from
    if type is "unavailable"
      @emit "leave", from, room, name
    else if type is "available" and entity.attrs.role is "participant"
      @emit "enter", from, room, name

onOffline = ->
  @logger.info 'Connection went offline'

onClose = ->
  @logger.info 'Connection was closed'
  @disconnect()

usersFromStanza = (stanza) ->
  # Parse response into objects
  stanza.getChild("query").getChildren("item").map (el) ->
    jid: el.attrs.jid
    name: el.attrs.name
    # Name used to @mention this user
    mention_name: el.attrs.mention_name
    # Email address
    email_address: el.attrs.email

# DOM helpers

getChild = (el, name) ->
  el.getChild name

getText = (el, name) ->
  getChild(el, name).getText()

getInt = (el, name) ->
  parseInt getText(el, name), 10


================================================
FILE: src/hipchat.coffee
================================================
{Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, User} = require "hubot"
HTTPS = require "https"
{inspect} = require "util"
Connector = require "./connector"
promise = require "./promises"

class HipChat extends Adapter

  constructor: (robot) ->
    super robot
    @logger = robot.logger
    reconnectTimer = null

  emote: (envelope, strings...) ->
    @send envelope, strings.map((str) -> "/me #{str}")...

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

  topic: (envelope, message) ->
    {user, room} = envelope
    user = envelope if not user # pre-2.4.2 style

    target_jid =
      # most common case - we're replying to a user in a room or 1-1
      user?.reply_to or
      # allows user objects to be passed in
      user?.jid or
      if user?.search?(/@/) >= 0
        user # allows user to be a jid string
      else
        room # this will happen if someone uses robot.messageRoom(jid, ...)

    if not target_jid
      return @logger.error "ERROR: Not sure who to send to: envelope=#{inspect envelope}"

    @connector.topic target_jid, message

  reply: (envelope, strings...) ->
    user = if envelope.user then envelope.user else envelope
    @send envelope, "@#{user.mention_name} #{str}" for str in strings

  waitAndReconnect: ->
    if !@reconnectTimer
      delay = Math.round(Math.random() * (20 - 5) + 5)
      @logger.info "Waiting #{delay}s and then retrying..."
      @reconnectTimer = setTimeout () =>
         @logger.info "Attempting to reconnect..."
         delete @reconnectTimer
         @connector.connect()
      , delay * 1000

  run: ->
    botjid = process.env.HUBOT_HIPCHAT_JID
    if not botjid
      throw new Error("Environment variable HUBOT_HIPCHAT_JID is required to contain your bot's user JID.")
    botpw = process.env.HUBOT_HIPCHAT_PASSWORD
    if not botpw
      throw new Error("Environment variable HUBOT_HIPCHAT_PASSWORD is required to contain your bot's user password.")
    @options =
      jid: botjid
      password: botpw
      token: process.env.HUBOT_HIPCHAT_TOKEN or null
      rooms: process.env.HUBOT_HIPCHAT_ROOMS or "All"
      rooms_blacklist: process.env.HUBOT_HIPCHAT_ROOMS_BLACKLIST or ""
      rooms_join_public: process.env.HUBOT_HIPCHAT_JOIN_PUBLIC_ROOMS isnt "false"
      host: process.env.HUBOT_HIPCHAT_HOST or null
      bosh: { url: process.env.HUBOT_HIPCHAT_BOSH_URL or null }
      autojoin: process.env.HUBOT_HIPCHAT_JOIN_ROOMS_ON_INVITE isnt "false"
      xmppDomain: process.env.HUBOT_HIPCHAT_XMPP_DOMAIN or null
      reconnect: process.env.HUBOT_HIPCHAT_RECONNECT isnt "false"

    @logger.debug "HipChat adapter options: #{JSON.stringify @options}"

    # create Connector object
    connector = new Connector
      jid: @options.jid
      password: @options.password
      host: @options.host
      logger: @logger
      xmppDomain: @options.xmppDomain
      bosh: @options.bosh
    host = if @options.host then @options.host else "hipchat.com"
    @logger.info "Connecting HipChat adapter..."

    init = promise()

    connector.onTopic (channel, from, message) =>
      @logger.info "Topic change: " + message
      author = getAuthor: => @robot.brain.userForName(from) or new User(from)
      author.room = channel
      @receive new TopicMessage(author, message, 'id')


    connector.onDisconnect =>
      @logger.info "Disconnected from #{host}"

      if @options.reconnect
        @waitAndReconnect()

    connector.onError =>
      @logger.error [].slice.call(arguments).map(inspect).join(", ")

      if @options.reconnect
        @waitAndReconnect()

    firstTime = true
    connector.onConnect =>
      @logger.info "Connected to #{host} as @#{connector.mention_name}"

      # Provide our name to Hubot
      @robot.name = connector.mention_name

      # Tell Hubot we're connected so it can load scripts
      if firstTime
        @emit "connected"
        @logger.debug "Sending connected event"

      saveUsers = (users) =>
        # Save users to brain
        for user in users
          user.id = @userIdFromJid user.jid
          # userForId will not merge to an existing user
          if user.id of @robot.brain.data.users
            oldUser = @robot.brain.data.users[user.id]
            for key, value of oldUser
              unless key of user
                user[key] = value
            delete @robot.brain.data.users[user.id]
          @robot.brain.userForId user.id, user

      joinRoom = (jid) =>
        if jid and typeof jid is "object"
          jid = "#{jid.local}@#{jid.domain}"

        if jid in @options.rooms_blacklist.split(",")
          @logger.info "Not joining #{jid} because it is blacklisted"
          return

        @logger.info "Joining #{jid}"
        connector.join jid

      # Fetch user info
      connector.getRoster (err, users, stanza) =>
        return init.reject err if err
        init.resolve users

      init
        .done (users) =>
          saveUsers(users)
          # Join requested rooms
          if @options.rooms is "All" or @options.rooms is "@All"
            connector.getRooms (err, rooms, stanza) =>
              if rooms
                for room in rooms
                  if !@options.rooms_join_public && room.guest_url != ''
                    @logger.info "Not joining #{room.jid} because it is a public room"
                  else
                    joinRoom(room.jid)
              else
                @logger.error "Can't list rooms: #{errmsg err}"
          # Join all rooms
          else
            for room_jid in @options.rooms.split ","
              joinRoom(room_jid)
        .fail (err) =>
          @logger.error "Can't list users: #{errmsg err}" if err

      connector.onRosterChange (users) =>
        saveUsers(users)

      handleMessage = (opts) =>
        # buffer message events until the roster fetch completes
        # to ensure user data is properly loaded
        init.done =>
          {getAuthor, message, room} = opts
          author = getAuthor() or {}
          author.room = room
          @receive new TextMessage(author, message)

      if firstTime
        connector.onMessage (channel, from, message) =>
          # reformat leading @mention name to be like "name: message" which is
          # what hubot expects
          mention_name = connector.mention_name
          regex = new RegExp "^@#{mention_name}\\b", "i"
          message = message.replace regex, "#{mention_name}: "
          handleMessage
            getAuthor: => @robot.brain.userForName(from) or new User(from)
            message: message
            room: channel

        connector.onPrivateMessage (from, message) =>
          # remove leading @mention name if present and format the message like
          # "name: message" which is what hubot expects
          mention_name = connector.mention_name
          regex = new RegExp "^@?#{mention_name}\\b", "i"
          message = "#{mention_name}: #{message.replace regex, ""}"
          handleMessage
            getAuthor: => @robot.brain.userForId(@userIdFromJid from)
            message: message
            room: from

      changePresence = (PresenceMessage, user_jid, room_jid, currentName) =>
        # buffer presence events until the roster fetch completes
        # to ensure user data is properly loaded
        init.done =>
          user = @robot.brain.userForId(@userIdFromJid(user_jid)) or {}
          if user
            user.room = room_jid
            # If an updated name was sent as part of a presence, update it now
            user.name = currentName if currentName.length
            @receive new PresenceMessage(user)
      if firstTime
        connector.onEnter (user_jid, room_jid, currentName) =>
          changePresence EnterMessage, user_jid, room_jid, currentName

        connector.onLeave (user_jid, room_jid) ->
          changePresence LeaveMessage, user_jid, room_jid

        connector.onInvite (room_jid, from_jid, message) =>
          action = if @options.autojoin then "joining" else "ignoring"
          @logger.info "Got invite to #{room_jid} from #{from_jid} - #{action}"
          joinRoom(room_jid) if @options.autojoin

      firstTime = false
    connector.connect()

    @connector = connector

  userIdFromJid: (jid) ->
    try
      jid.match(/^\d+_(\d+)@chat\./)[1]
    catch e
      @logger.error "Bad user JID: #{jid}"

  # Convenience HTTP Methods for posting on behalf of the token'd user
  get: (path, callback) ->
    @request "GET", path, null, callback

  post: (path, body, callback) ->
    @request "POST", path, body, callback

  request: (method, path, body, callback) ->
    @logger.debug "Request:", method, path, body
    host = @options.host or "api.hipchat.com"
    headers = "Host": host

    unless @options.token
      return callback "No API token provided to Hubot", null

    options =
      agent  : false
      host   : host
      port   : 443
      path   : path += "?auth_token=#{@options.token}"
      method : method
      headers: headers

    if method is "POST"
      headers["Content-Type"] = "application/x-www-form-urlencoded"
      options.headers["Content-Length"] = body.length

    request = HTTPS.request options, (response) =>
      data = ""
      response.on "data", (chunk) ->
        data += chunk
      response.on "end", =>
        if response.statusCode >= 400
          @logger.error "HipChat API error: #{response.statusCode}"
        try
          callback null, JSON.parse(data)
        catch err
          callback null, data or { }
      response.on "error", (err) ->
        callback err, null

    if method is "POST"
      request.end(body, "binary")
    else
      request.end()

    request.on "error", (err) =>
      @logger.error err
      @logger.error err.stack if err.stack
      callback err

errmsg = (err) ->
  err + (if err.stack then '\n' + err.stack else '')

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


================================================
FILE: src/promises.coffee
================================================
{Promise} = require "rsvp"

Promise::done = (done) -> @then done
Promise::fail = (fail) -> @then null, fail

module.exports = -> new Promise()


================================================
FILE: src/test.coffee
================================================

user = id: 1, name: "yannick"
oldUser = role: "lol", name: "bob"
for key, value of oldUser
  unless key of user
    user[key] = value
console.log user, oldUser
Download .txt
gitextract_dlevzx9j/

├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
└── src/
    ├── connector.coffee
    ├── hipchat.coffee
    ├── promises.coffee
    └── test.coffee
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (41K chars).
[
  {
    "path": ".gitignore",
    "chars": 24,
    "preview": "node_modules\n.DS_Store*\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1066,
    "preview": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changel"
  },
  {
    "path": "LICENSE",
    "chars": 1052,
    "preview": "Copyright (c) HipChat, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this softwa"
  },
  {
    "path": "README.md",
    "chars": 7094,
    "preview": "# hubot-hipchat\n\n## Quickstart: Hubot for HipChat on Heroku\n\n### The Easy Way\n\nTry deploying the \"[Triatomic](https://gi"
  },
  {
    "path": "package.json",
    "chars": 525,
    "preview": "{\n  \"name\": \"hubot-hipchat\",\n  \"version\": \"2.13.0\",\n  \"author\": \"HipChat <support@hipchat.com>\",\n  \"description\": \"A Hip"
  },
  {
    "path": "src/connector.coffee",
    "chars": 18988,
    "preview": "# Modified from [Wobot](https://github.com/cjoudrey/wobot).\n#\n# Copyright (C) 2011 by Christian Joudrey\n#\n# Permission i"
  },
  {
    "path": "src/hipchat.coffee",
    "chars": 9949,
    "preview": "{Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, User} = require \"hubot\"\nHTTPS = require \"https\"\n{inspec"
  },
  {
    "path": "src/promises.coffee",
    "chars": 143,
    "preview": "{Promise} = require \"rsvp\"\n\nPromise::done = (done) -> @then done\nPromise::fail = (fail) -> @then null, fail\n\nmodule.expo"
  },
  {
    "path": "src/test.coffee",
    "chars": 161,
    "preview": "\nuser = id: 1, name: \"yannick\"\noldUser = role: \"lol\", name: \"bob\"\nfor key, value of oldUser\n  unless key of user\n    use"
  }
]

About this extraction

This page contains the full source code of the hipchat/hubot-hipchat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (38.1 KB), approximately 10.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!