[
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store*\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and\nthis project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [2.13.0] - 2017-11-21\n### Changed\n- `message.user` returns an object from the brain.\n  - Changed by [Andrew Widdersheim](https://github.com/awiddersheim) in Pull Request [#277](https://github.com/hipchat/hubot-hipchat/pull/277).\n- `user.room` always returns a JID.\n  - Changed by [Andrew Widdersheim](https://github.com/awiddersheim) in Pull Request [#277](https://github.com/hipchat/hubot-hipchat/pull/277).\n\n### Removed\n- `reply_to` from `envelope.user` and `message.user`.\n  - Removed by [Andrew Widdersheim](https://github.com/awiddersheim) in Pull Request [#277](https://github.com/hipchat/hubot-hipchat/pull/277).\n\n### Security\n- Updated `node-xmpp-client` to 3.2.0.\n  - Updated by [Sam](https://github.com/samcday) in Pull Request [#272](https://github.com/hipchat/hubot-hipchat/pull/272).\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) HipChat, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# hubot-hipchat\n\n## Quickstart: Hubot for HipChat on Heroku\n\n### The Easy Way\n\nTry 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.\n\n[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/hipchat/triatomic)\n\n### The \"I do it myself!\" Way\n\nThis 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.\n\n1. 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.\n\n1. If you are using Linux, make sure libexpat is installed:\n\n        % apt-get install libexpat1-dev\n\n1. 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.\n\n1. Once node and npm are ready, we can install the hubot generator:\n\n        %  npm install -g yo generator-hubot\n\n1. 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:\n\n        % mkdir myhubot\n        % cd myhubot\n        % yo hubot --adapter hipchat\n\n1. 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.)\n\n1. Turn your `hubot` directory into a git repository:\n\n        % git init\n        % git add .\n        % git commit -m \"Initial commit\"\n\n1. Install the [Heroku command line tools](http://devcenter.heroku.com/articles/heroku-command) if you don't have them installed yet.\n\n1. Create a new Heroku application and (optionally) rename it:\n\n        % heroku create our-company-hubot\n\n1. Add [Redis To Go](http://devcenter.heroku.com/articles/redistogo) to your Heroku app:\n\n        % heroku addons:create redistogo:nano --app our-company-hubot\n\n1. Configure it:\n\n      You will need to set a configuration variable if you are hosting on the free Heroku plan.\n\n        % heroku config:add HEROKU_URL=http://our-company-hubot.herokuapp.com\n\n      Where the URL is your Heroku app's URL (shown after running `heroku create`, or `heroku rename`).\n\n      Set the JID to the \"Jabber ID\" shown on your bot's [XMPP/Jabber account settings](https://www.hipchat.com/account/xmpp):\n\n        % heroku config:add HUBOT_HIPCHAT_JID=\"...\"\n\n      Set the password to the password chosen when you created the bot's account.\n\n        % heroku config:add HUBOT_HIPCHAT_PASSWORD=\"...\"\n\n      If using HipChat Server Beta, you need to set xmppDomain to btf.hipchat.com.\n\n        % heroku config:add HUBOT_HIPCHAT_XMPP_DOMAIN=\"btf.hipchat.com\"\n\n1. Deploy and start the bot:\n\n        % git push heroku master\n        % heroku ps:scale web=1\n\n      This will tell Heroku to run 1 of the `web` process type which is described in the `Procfile`.\n\n1. 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.\n\n1. 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).\n\n1. 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.\n\n1. To deploy an updated version of the bot, simply commit your changes and run `git push heroku master` again.\n\nBonus: 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\n\n## Scripting Gotchas\n`robot.messageRoom` syntax is as follows\n```\nrobot.messageRoom(\"1234_room@conf.hipchat.com\", \"message\");\n```\n\n## Adapter configuration\n\nThis adapter uses the following environment variables:\n\n### HUBOT\\_HIPCHAT\\_JID\n\nThis 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`\n\n### HUBOT\\_HIPCHAT\\_PASSWORD\n\nThis is the password for your bot's HipChat account.\n\n### HUBOT\\_HIPCHAT\\_ROOMS\n\nOptional. 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\".\n\n### HUBOT\\_HIPCHAT\\_ROOMS\\_BLACKLIST\n\nOptional. This is a comma separated list of room JIDs that should not be joined.\n\n### HUBOT\\_HIPCHAT\\_JOIN\\_ROOMS\\_ON\\_INVITE\n\nOptional. Setting to `false` will prevent the HipChat adapter from auto-joining rooms when invited.\n\n### HUBOT\\_HIPCHAT\\_JOIN\\_PUBLIC\\_ROOMS\n\nOptional. Setting to `false` will prevent the HipChat adapter from auto-joining rooms that are publicly available (i.e. guest-accessible).\n\n### HUBOT\\_HIPCHAT\\_HOST\n\nOptional. Use to force the host to open the XMPP connection to.\n\n### HUBOT\\_HIPCHAT\\_XMPP\\_DOMAIN\n\nOptional. Set to btf.hipchat.com if using HipChat Server.\n\n### HUBOT\\_LOG\\_LEVEL\n\nOptional. Set to `debug` to enable detailed debug logging.\n\n### HUBOT\\_HIPCHAT\\_RECONNECT\n\nOptional. Seting to `false` will prevent the HipChat adapter from auto-reconnecting if it detects a server error or disconnection.\n\n## Running locally\n\nTo 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:\n\n    #!/bin/bash\n\n    export HUBOT_HIPCHAT_JID=\"...\"\n    export HUBOT_HIPCHAT_PASSWORD=\"...\"\n\n    bin/hubot --adapter hipchat\n\nBut be aware that credentials normally shouldn't be checked into your vcs.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hubot-hipchat\",\n  \"version\": \"2.13.0\",\n  \"author\": \"HipChat <support@hipchat.com>\",\n  \"description\": \"A HipChat adapter for Hubot\",\n  \"keywords\": [\n    \"hipchat\",\n    \"hubot\",\n    \"xmpp\",\n    \"chat\",\n    \"bot\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"http://github.com/hipchat/hubot-hipchat.git\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"./src/hipchat\",\n  \"engines\": {\n    \"node\": \"~0.12.2\"\n  },\n  \"dependencies\": {\n    \"node-xmpp-client\": \"^3.2.0\",\n    \"rsvp\": \"~1.2.0\",\n    \"underscore\": \"~1.4.4\"\n  }\n}\n"
  },
  {
    "path": "src/connector.coffee",
    "content": "# Modified from [Wobot](https://github.com/cjoudrey/wobot).\n#\n# Copyright (C) 2011 by Christian Joudrey\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\n{EventEmitter} = require \"events\"\nfs = require \"fs\"\n{bind, isString, isRegExp} = require \"underscore\"\nxmpp = require 'node-xmpp-client'\n\n# Parse and cache the node package.json file when this module is loaded\npkg = do ->\n  data = fs.readFileSync __dirname + \"/../package.json\", \"utf8\"\n  JSON.parse(data)\n\n# ##Public Connector API\nmodule.exports = class Connector extends EventEmitter\n\n  # This is the `Connector` constructor.\n  #\n  # `options` object:\n  #\n  #   - `jid`: Connector's Jabber ID\n  #   - `password`: Connector's HipChat password\n  #   - `host`: Force host to make XMPP connection to. Will look up DNS SRV\n  #        record on JID's host otherwise.\n  #   - `caps_ver`: Name and version of connector. Override if Connector is being used\n  #        to power another connector framework (e.g. Hubot).\n  #   - `logger`: A logger instance.\n  constructor: (options={}) ->\n    @once \"connect\", (->) # listener bug in Node 0.4.2\n    @setMaxListeners 0\n\n    @jabber = null\n    @keepalive = null\n    @name = null\n    @plugins = {}\n    @iq_count = 1 # current IQ id to use\n    @logger = options.logger\n\n    # add a JID resource if none was provided\n    jid = new xmpp.JID options.jid\n    jid.resource = \"hubot-hipchat\" if not jid.resource\n\n    @jid = jid.toString()\n    @password = options.password\n    @host = options.host\n    @caps_ver = options.caps_ver or \"hubot-hipchat:#{pkg.version}\"\n    @xmppDomain = options.xmppDomain\n    @bosh = options.bosh\n\n    # Multi-User-Conference (rooms) service host. Use when directing stanzas\n    # to the MUC service.\n    @mucDomain = \"conf.#{if @xmppDomain then @xmppDomain else 'hipchat.com'}\"\n\n    @disconnecting = false\n    @onError @disconnect\n\n  # Connects the connector to HipChat and sets the XMPP event listeners.\n  connect: ->\n    @jabber = new xmpp.Client\n      jid: @jid,\n      password: @password,\n      host: @host\n      bosh: @bosh\n\n    @jabber.on \"error\", bind(onStreamError, @)\n    @jabber.on \"online\", bind(onOnline, @)\n    @jabber.on \"stanza\", bind(onStanza, @)\n    @jabber.on \"offline\", bind(onOffline, @)\n    @jabber.on \"close\", bind(onClose, @)\n\n    # debug network traffic\n    do =>\n      @jabber.on \"data\", (buffer) =>\n        @logger.debug \"  IN > %s\", buffer.toString()\n      _send = @jabber.send\n      @jabber.send = (stanza) =>\n        @logger.debug \" OUT > %s\", stanza\n        _send.call @jabber, stanza\n\n  # Disconnect the connector from HipChat, remove the anti-idle and emit the\n  # `disconnect` event.\n  disconnect: =>\n    # since we're going to emit \"disconnect\" event in the end, we should prevent ourself from handling it here\n    if ! @disconnecting\n      @disconnecting = true\n      @logger.debug 'Disconnecting here'\n      if @keepalive\n        clearInterval @keepalive\n        delete @keepalive\n      @jabber.end()\n      @emit \"disconnect\"\n      @disconnecting = false\n\n  # Fetches our profile info\n  #\n  # - `callback`: Function to be triggered: `function (err, data, stanza)`\n  #   - `err`: Error condition (string) if any\n  #   - `data`: Object containing fields returned (fn, title, photo, etc)\n  #   - `stanza`: Full response stanza, an `xmpp.Element`\n  getProfile: (callback) ->\n    stanza = new xmpp.Element(\"iq\", type: \"get\")\n      .c(\"vCard\", xmlns: \"vcard-temp\")\n    @sendIq stanza, (err, res) ->\n      data = {}\n      if not err\n        for field in res.getChild(\"vCard\").children\n          data[field.name.toLowerCase()] = field.getText()\n      callback err, data, res\n\n  # Fetches the rooms available to the connector user. This is equivalent to what\n  # would show up in the HipChat lobby.\n  #\n  # - `callback`: Function to be triggered: `function (err, items, stanza)`\n  #   - `err`: Error condition (string) if any\n  #   - `rooms`: Array of objects containing room data\n  #   - `stanza`: Full response stanza, an `xmpp.Element`\n  getRooms: (callback) ->\n    iq = new xmpp.Element(\"iq\", to: this.mucDomain, type: \"get\")\n      .c(\"query\", xmlns: \"http://jabber.org/protocol/disco#items\");\n    @sendIq iq, (err, stanza) ->\n      rooms = if err then [] else\n        # Parse response into objects\n        stanza.getChild(\"query\").getChildren(\"item\").map (el) ->\n          x = el.getChild \"x\", \"http://hipchat.com/protocol/muc#room\"\n          # A room\n          jid: el.attrs.jid.trim()\n          name: el.attrs.name\n          id: getInt(x, \"id\")\n          topic: getText(x, \"topic\")\n          privacy: getText(x, \"privacy\")\n          owner: getText(x, \"owner\")\n          guest_url: getText(x, \"guest_url\")\n          is_archived: !!getChild(x, \"is_archived\")\n      callback err, (rooms or []), stanza\n\n  # Fetches the roster (buddy list)\n  #\n  # - `callback`: Function to be triggered: `function (err, items, stanza)`\n  #   - `err`: Error condition (string) if any\n  #   - `items`: Array of objects containing user data\n  #   - `stanza`: Full response stanza, an `xmpp.Element`\n  getRoster: (callback) ->\n    iq = new xmpp.Element(\"iq\", type: \"get\")\n      .c(\"query\", xmlns: \"jabber:iq:roster\")\n    @sendIq iq, (err, stanza) ->\n      items = if err then [] else usersFromStanza(stanza)\n      callback err, (items or []), stanza\n\n  # Updates the connector's availability and status.\n  #\n  #  - `availability`: Jabber availability codes\n  #     - `away`\n  #     - `chat` (Free for chat)\n  #     - `dnd` (Do not disturb)\n  #  - `status`: Status message to display\n  setAvailability: (availability, status) ->\n    packet = new xmpp.Element \"presence\", type: \"available\"\n    packet.c(\"show\").t(availability)\n    packet.c(\"status\").t(status) if (status)\n\n    # Providing capabilities info (XEP-0115) in presence tells HipChat\n    # what type of client is connecting. The rest of the spec is not actually\n    # used at this time.\n    packet.c \"c\",\n      xmlns: \"http://jabber.org/protocol/caps\"\n      node: \"http://hipchat.com/client/bot\" # tell HipChat we're a bot\n      ver: @caps_ver\n\n    @jabber.send packet\n\n  # Join the specified room.\n  #\n  # - `roomJid`: Target room, in the form of `????_????@conf.hipchat.com`\n  # - `historyStanzas`: Max number of history entries to request\n  join: (roomJid, historyStanzas) ->\n    historyStanzas = 0 if not historyStanzas\n    packet = new xmpp.Element \"presence\", to: \"#{roomJid}/#{@name}\"\n    packet.c \"x\", xmlns: \"http://jabber.org/protocol/muc\"\n    packet.c \"history\",\n      xmlns: \"http://jabber.org/protocol/muc\"\n      maxstanzas: String(historyStanzas)\n    @jabber.send packet\n\n  # Part the specified room.\n  #\n  # - `roomJid`: Target room, in the form of `????_????@conf.hipchat.com`\n  part: (roomJid) ->\n    packet = new xmpp.Element 'presence',\n      type: 'unavailable'\n      to: \"#{roomJid}/#{@name}\"\n    packet.c 'x', xmlns: 'http://jabber.org/protocol/muc'\n    packet.c('status').t('hc-leave')\n    @jabber.send packet\n\n  # Send a message to a room or a user.\n  #\n  # - `targetJid`: Target\n  #    - Message to a room: `????_????@conf.hipchat.com`\n  #    - Private message to a user: `????_????@chat.hipchat.com`\n  # - `message`: Message to be sent to the room\n  message: (targetJid, message) ->\n    parsedJid = new xmpp.JID targetJid\n\n    if parsedJid.domain is @mucDomain\n      packet = new xmpp.Element \"message\",\n        to: \"#{targetJid}/#{@name}\"\n        type: \"groupchat\"\n    else\n      packet = new xmpp.Element \"message\",\n        to: targetJid\n        type: \"chat\"\n        from: @jid\n      packet.c \"inactive\", xmlns: \"http://jabber/protocol/chatstates\"\n    # we should make sure that the message is properly escaped\n    # based on http://unix.stackexchange.com/questions/111899/how-to-strip-color-codes-out-of-stdout-and-pipe-to-file-and-stdout\n    message = message.replace(/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]/g, \"\")  # remove bash color codes\n    @logger.debug 'building message'\n    @logger.debug message\n    packet.c(\"body\").t(message)\n    @jabber.send packet\n\n  # Send a topic change message to a room\n  #\n  # - `targetJid`: Target\n  #    - Message to a room: `????_????@conf.hipchat.com`\n  # - `message`: Text string that the topic should be set to\n  topic: (targetJid, message) ->\n    parsedJid = new xmpp.JID targetJid\n\n    packet = new xmpp.Element \"message\",\n      to: \"#{targetJid}/#{@name}\"\n      type: \"groupchat\"\n\n    packet.c(\"subject\").t(message)\n    @jabber.send packet\n\n  # Sends an IQ stanza and stores a callback to be called when its response\n  # is received.\n  #\n  # - `stanza`: `xmpp.Element` to send\n  # - `callback`: Function to be triggered: `function (err, stanza)`\n  #   - `err`: Error condition (string) if any\n  #   - `stanza`: Full response stanza, an `xmpp.Element`\n  sendIq: (stanza, callback) ->\n    stanza = stanza.root() # work with base element\n    id = @iq_count++\n    stanza.attrs.id = id;\n    @once \"iq:#{id}\", callback\n    @jabber.send stanza\n\n  loadPlugin: (identifier, plugin, options) ->\n    if typeof plugin isnt \"object\"\n      throw new Error \"Plugin argument must be an object\"\n    if typeof plugin.load isnt \"function\"\n      throw new Error \"Plugin object must have a load function\"\n    @plugins[identifier] = plugin\n    plugin.load @, options\n    true\n\n  # ##Events API\n\n  # Emitted whenever the connector connects to the server.\n  #\n  # - `callback`: Function to be triggered: `function ()`\n  onConnect: (callback) -> @on \"connect\", callback\n\n  # Emitted whenever the connector disconnects from the server.\n  #\n  # - `callback`: Function to be triggered: `function ()`\n  onDisconnect: (callback) -> @on \"disconnect\", callback\n\n  # Emitted whenever the connector is invited to a room.\n  #\n  # `onInvite(callback)`\n  #\n  # - `callback`: Function to be triggered:\n  #               `function (roomJid, fromJid, reason, matches)`\n  #   - `roomJid`: JID of the room being invited to.\n  #   - `fromJid`: JID of the person who sent the invite.\n  #   - `reason`: Reason for invite (text)\n  onInvite: (callback) -> @on \"invite\", callback\n\n  # Makes an onMessage impl for the named message event\n  onMessageFor = (name) ->\n    (condition, callback) ->\n      if not callback\n        callback = condition\n        condition = null\n      @on name, ->\n        message = arguments[arguments.length - 1]\n        if not condition or message is condition\n          callback.apply @, arguments\n        else if isRegExp condition\n          match = message.match condition\n          return if not match\n          args = [].slice.call arguments\n          args.push match\n          callback.apply @, args\n\n  # Emitted whenever a message is sent to a channel the connector is in.\n  #\n  # `onMessage(condition, callback)`\n  #\n  # `onMessage(callback)`\n  #\n  # - `condition`: String or RegExp the message must match.\n  # - `callback`: Function to be triggered: `function (roomJid, from, message, matches)`\n  #   - `roomJid`: Jabber Id of the room in which the message occured.\n  #   - `from`: The name of the person who said the message.\n  #   - `message`: The message\n  #   - `matches`: The matches returned by the condition when it is a RegExp\n  onMessage: onMessageFor \"message\"\n\n  # Emitted whenever a message is sent privately to the connector.\n  #\n  # `onPrivateMessage(condition, callback)`\n  #\n  # `onPrivateMessage(callback)`\n  #\n  # - `condition`: String or RegExp the message must match.\n  # - `callback`: Function to be triggered: `function (fromJid, message)`\n  onPrivateMessage: onMessageFor \"privateMessage\"\n\n  # Emitted whenever the connector receives a topic change in a room\n  #\n  # - `callback`: Function to be triggered: `function ()`\n  onTopic: (callback) -> @on \"topic\", callback\n\n  onEnter: (callback) -> @on \"enter\", callback\n\n  onLeave: (callback) -> @on \"leave\", callback\n\n  onRosterChange: (callback) -> @on \"rosterChange\", callback\n\n  # Emitted whenever the connector pings the server (roughly every 30 seconds).\n  #\n  # - `callback`: Function to be triggered: `function ()`\n  onPing: (callback) -> @on \"ping\", callback\n\n  # Emitted whenever an XMPP stream error occurs. The `disconnect` event will\n  # always be emitted afterwards.\n  #\n  # Conditions are defined in the XMPP spec:\n  #   http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions\n  #\n  # - `callback`: Function to be triggered: `function(condition, text, stanza)`\n  #   - `condition`: XMPP stream error condition (string)\n  #   - `text`: Human-readable error message (string)\n  #   - `stanza`: The raw `xmpp.Element` error stanza\n  onError: (callback) -> @on \"error\", callback\n\n# ##Private functions\n\n# Whenever an XMPP stream error occurs, this function is responsible for\n# triggering the `error` event with the details and disconnecting the connector\n# from the server.\n#\n# Stream errors (http://xmpp.org/rfcs/rfc6120.html#streams-error) look like:\n# <stream:error>\n#   <system-shutdown xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>\n# </stream:error>\nonStreamError = (err) ->\n  if err instanceof xmpp.Element\n    condition = err.children[0].name\n    text = err.getChildText \"text\"\n    if not text\n      text = \"No error text sent by HipChat, see\n        http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions\n        for error condition descriptions.\"\n    @emit \"error\", condition, text, err\n  else\n    @emit \"error\", null, null, err\n\n# Whenever an XMPP connection is made, this function is responsible for\n# triggering the `connect` event and starting the 30s anti-idle. It will\n# also set the availability of the connector to `chat`.\nonOnline = ->\n  @setAvailability \"chat\"\n\n  ping = =>\n    @jabber.send new xmpp.Element('r')\n    @emit \"ping\"\n\n  @keepalive = setInterval ping, 30000\n\n  # Load our profile to get name\n  @getProfile (err, data) =>\n    if err\n      # This isn't technically a stream error which is what the `error`\n      # event usually represents, but we want to treat a profile fetch\n      # error as a fatal error and disconnect the connector.\n      @emit \"error\", null, \"Unable to get profile info: #{err}\", null\n    else\n      # Now that we have our name we can let rooms be joined\n      @name = data.fn;\n      # This is the name used to @mention us\n      @mention_name = data.nickname\n      @emit \"connect\"\n\n# This function is responsible for handling incoming XMPP messages. The\n# `data` event will be triggered with the message for custom XMPP\n# handling.\n#\n# The connector will parse the message and trigger the `message`\n# event when it is a group chat message or the `privateMessage` event when\n# it is a private message.\nonStanza = (stanza) ->\n  @emit \"data\", stanza\n\n  if stanza.is \"message\"\n    if stanza.attrs.type is \"groupchat\"\n\n      return if stanza.getChild \"delay\"\n      fromJid = new xmpp.JID stanza.attrs.from\n      fromChannel = fromJid.bare().toString()\n      fromNick = fromJid.resource\n      # Ignore our own messages\n      return if fromNick is @name\n      # Look for body msg\n      body = stanza.getChildText \"body\"\n      # look for Subject: http://xmpp.org/extensions/xep-0045.html#subject-mod\n      subject = stanza.getChildText \"subject\"\n      if body\n        # message stanza\n        @emit \"message\", fromChannel, fromNick, body\n      else if subject\n        # subject stanza\n        @emit \"topic\", fromChannel, fromNick, subject\n      else\n        # Skip parsing other types and return\n        return\n\n    else if stanza.attrs.type is \"chat\"\n      # Message without body is probably a typing notification\n      body = stanza.getChildText \"body\"\n      return if not body\n      # Ignore chat history\n      return if stanza.getChild \"delay\"\n      fromJid = new xmpp.JID stanza.attrs.from\n      @emit \"privateMessage\", fromJid.bare().toString(), body\n\n    else if not stanza.attrs.type\n      x = stanza.getChild \"x\", \"http://jabber.org/protocol/muc#user\"\n      return if not x\n      invite = x.getChild \"invite\"\n      return if not invite\n      reason = invite.getChildText \"reason\"\n      inviteRoom = new xmpp.JID stanza.attrs.from\n      inviteSender = new xmpp.JID invite.attrs.from\n      @emit \"invite\", inviteRoom.bare(), inviteSender.bare(), reason\n\n  else if stanza.is \"iq\"\n    # Handle a response to an IQ request\n    event_id = \"iq:#{stanza.attrs.id}\"\n    if stanza.attrs.type is \"result\"\n      @emit event_id, null, stanza\n    else if stanza.attrs.type is \"set\"\n      # Check for roster push\n      if stanza.getChild(\"query\").attrs.xmlns is \"jabber:iq:roster\"\n        users = usersFromStanza(stanza)\n        @emit \"rosterChange\", users, stanza\n    else\n      # IQ error response\n      # ex: http://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-result\n      condition = \"unknown\"\n      error_elem = stanza.getChild \"error\"\n      condition = error_elem.children[0].name if error_elem\n      @emit event_id, condition, stanza\n\n  else if stanza.is \"presence\"\n    jid = new xmpp.JID stanza.attrs.from\n    room = jid.bare().toString()\n    return if not room\n    name = stanza.attrs.from.split(\"/\")[1]\n    # Fall back to empty string if name isn't reported in presence\n    name ?= \"\"\n    type = stanza.attrs.type or \"available\"\n    x = stanza.getChild \"x\", \"http://jabber.org/protocol/muc#user\"\n    return if not x\n    entity = x.getChild \"item\"\n    return if not entity\n    from = entity.attrs?.jid\n    return if not from\n    if type is \"unavailable\"\n      @emit \"leave\", from, room, name\n    else if type is \"available\" and entity.attrs.role is \"participant\"\n      @emit \"enter\", from, room, name\n\nonOffline = ->\n  @logger.info 'Connection went offline'\n\nonClose = ->\n  @logger.info 'Connection was closed'\n  @disconnect()\n\nusersFromStanza = (stanza) ->\n  # Parse response into objects\n  stanza.getChild(\"query\").getChildren(\"item\").map (el) ->\n    jid: el.attrs.jid\n    name: el.attrs.name\n    # Name used to @mention this user\n    mention_name: el.attrs.mention_name\n    # Email address\n    email_address: el.attrs.email\n\n# DOM helpers\n\ngetChild = (el, name) ->\n  el.getChild name\n\ngetText = (el, name) ->\n  getChild(el, name).getText()\n\ngetInt = (el, name) ->\n  parseInt getText(el, name), 10\n"
  },
  {
    "path": "src/hipchat.coffee",
    "content": "{Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, User} = require \"hubot\"\nHTTPS = require \"https\"\n{inspect} = require \"util\"\nConnector = require \"./connector\"\npromise = require \"./promises\"\n\nclass HipChat extends Adapter\n\n  constructor: (robot) ->\n    super robot\n    @logger = robot.logger\n    reconnectTimer = null\n\n  emote: (envelope, strings...) ->\n    @send envelope, strings.map((str) -> \"/me #{str}\")...\n\n  send: (envelope, strings...) ->\n    for str in strings\n      @connector.message envelope.room, str\n\n  topic: (envelope, message) ->\n    {user, room} = envelope\n    user = envelope if not user # pre-2.4.2 style\n\n    target_jid =\n      # most common case - we're replying to a user in a room or 1-1\n      user?.reply_to or\n      # allows user objects to be passed in\n      user?.jid or\n      if user?.search?(/@/) >= 0\n        user # allows user to be a jid string\n      else\n        room # this will happen if someone uses robot.messageRoom(jid, ...)\n\n    if not target_jid\n      return @logger.error \"ERROR: Not sure who to send to: envelope=#{inspect envelope}\"\n\n    @connector.topic target_jid, message\n\n  reply: (envelope, strings...) ->\n    user = if envelope.user then envelope.user else envelope\n    @send envelope, \"@#{user.mention_name} #{str}\" for str in strings\n\n  waitAndReconnect: ->\n    if !@reconnectTimer\n      delay = Math.round(Math.random() * (20 - 5) + 5)\n      @logger.info \"Waiting #{delay}s and then retrying...\"\n      @reconnectTimer = setTimeout () =>\n         @logger.info \"Attempting to reconnect...\"\n         delete @reconnectTimer\n         @connector.connect()\n      , delay * 1000\n\n  run: ->\n    botjid = process.env.HUBOT_HIPCHAT_JID\n    if not botjid\n      throw new Error(\"Environment variable HUBOT_HIPCHAT_JID is required to contain your bot's user JID.\")\n    botpw = process.env.HUBOT_HIPCHAT_PASSWORD\n    if not botpw\n      throw new Error(\"Environment variable HUBOT_HIPCHAT_PASSWORD is required to contain your bot's user password.\")\n    @options =\n      jid: botjid\n      password: botpw\n      token: process.env.HUBOT_HIPCHAT_TOKEN or null\n      rooms: process.env.HUBOT_HIPCHAT_ROOMS or \"All\"\n      rooms_blacklist: process.env.HUBOT_HIPCHAT_ROOMS_BLACKLIST or \"\"\n      rooms_join_public: process.env.HUBOT_HIPCHAT_JOIN_PUBLIC_ROOMS isnt \"false\"\n      host: process.env.HUBOT_HIPCHAT_HOST or null\n      bosh: { url: process.env.HUBOT_HIPCHAT_BOSH_URL or null }\n      autojoin: process.env.HUBOT_HIPCHAT_JOIN_ROOMS_ON_INVITE isnt \"false\"\n      xmppDomain: process.env.HUBOT_HIPCHAT_XMPP_DOMAIN or null\n      reconnect: process.env.HUBOT_HIPCHAT_RECONNECT isnt \"false\"\n\n    @logger.debug \"HipChat adapter options: #{JSON.stringify @options}\"\n\n    # create Connector object\n    connector = new Connector\n      jid: @options.jid\n      password: @options.password\n      host: @options.host\n      logger: @logger\n      xmppDomain: @options.xmppDomain\n      bosh: @options.bosh\n    host = if @options.host then @options.host else \"hipchat.com\"\n    @logger.info \"Connecting HipChat adapter...\"\n\n    init = promise()\n\n    connector.onTopic (channel, from, message) =>\n      @logger.info \"Topic change: \" + message\n      author = getAuthor: => @robot.brain.userForName(from) or new User(from)\n      author.room = channel\n      @receive new TopicMessage(author, message, 'id')\n\n\n    connector.onDisconnect =>\n      @logger.info \"Disconnected from #{host}\"\n\n      if @options.reconnect\n        @waitAndReconnect()\n\n    connector.onError =>\n      @logger.error [].slice.call(arguments).map(inspect).join(\", \")\n\n      if @options.reconnect\n        @waitAndReconnect()\n\n    firstTime = true\n    connector.onConnect =>\n      @logger.info \"Connected to #{host} as @#{connector.mention_name}\"\n\n      # Provide our name to Hubot\n      @robot.name = connector.mention_name\n\n      # Tell Hubot we're connected so it can load scripts\n      if firstTime\n        @emit \"connected\"\n        @logger.debug \"Sending connected event\"\n\n      saveUsers = (users) =>\n        # Save users to brain\n        for user in users\n          user.id = @userIdFromJid user.jid\n          # userForId will not merge to an existing user\n          if user.id of @robot.brain.data.users\n            oldUser = @robot.brain.data.users[user.id]\n            for key, value of oldUser\n              unless key of user\n                user[key] = value\n            delete @robot.brain.data.users[user.id]\n          @robot.brain.userForId user.id, user\n\n      joinRoom = (jid) =>\n        if jid and typeof jid is \"object\"\n          jid = \"#{jid.local}@#{jid.domain}\"\n\n        if jid in @options.rooms_blacklist.split(\",\")\n          @logger.info \"Not joining #{jid} because it is blacklisted\"\n          return\n\n        @logger.info \"Joining #{jid}\"\n        connector.join jid\n\n      # Fetch user info\n      connector.getRoster (err, users, stanza) =>\n        return init.reject err if err\n        init.resolve users\n\n      init\n        .done (users) =>\n          saveUsers(users)\n          # Join requested rooms\n          if @options.rooms is \"All\" or @options.rooms is \"@All\"\n            connector.getRooms (err, rooms, stanza) =>\n              if rooms\n                for room in rooms\n                  if !@options.rooms_join_public && room.guest_url != ''\n                    @logger.info \"Not joining #{room.jid} because it is a public room\"\n                  else\n                    joinRoom(room.jid)\n              else\n                @logger.error \"Can't list rooms: #{errmsg err}\"\n          # Join all rooms\n          else\n            for room_jid in @options.rooms.split \",\"\n              joinRoom(room_jid)\n        .fail (err) =>\n          @logger.error \"Can't list users: #{errmsg err}\" if err\n\n      connector.onRosterChange (users) =>\n        saveUsers(users)\n\n      handleMessage = (opts) =>\n        # buffer message events until the roster fetch completes\n        # to ensure user data is properly loaded\n        init.done =>\n          {getAuthor, message, room} = opts\n          author = getAuthor() or {}\n          author.room = room\n          @receive new TextMessage(author, message)\n\n      if firstTime\n        connector.onMessage (channel, from, message) =>\n          # reformat leading @mention name to be like \"name: message\" which is\n          # what hubot expects\n          mention_name = connector.mention_name\n          regex = new RegExp \"^@#{mention_name}\\\\b\", \"i\"\n          message = message.replace regex, \"#{mention_name}: \"\n          handleMessage\n            getAuthor: => @robot.brain.userForName(from) or new User(from)\n            message: message\n            room: channel\n\n        connector.onPrivateMessage (from, message) =>\n          # remove leading @mention name if present and format the message like\n          # \"name: message\" which is what hubot expects\n          mention_name = connector.mention_name\n          regex = new RegExp \"^@?#{mention_name}\\\\b\", \"i\"\n          message = \"#{mention_name}: #{message.replace regex, \"\"}\"\n          handleMessage\n            getAuthor: => @robot.brain.userForId(@userIdFromJid from)\n            message: message\n            room: from\n\n      changePresence = (PresenceMessage, user_jid, room_jid, currentName) =>\n        # buffer presence events until the roster fetch completes\n        # to ensure user data is properly loaded\n        init.done =>\n          user = @robot.brain.userForId(@userIdFromJid(user_jid)) or {}\n          if user\n            user.room = room_jid\n            # If an updated name was sent as part of a presence, update it now\n            user.name = currentName if currentName.length\n            @receive new PresenceMessage(user)\n      if firstTime\n        connector.onEnter (user_jid, room_jid, currentName) =>\n          changePresence EnterMessage, user_jid, room_jid, currentName\n\n        connector.onLeave (user_jid, room_jid) ->\n          changePresence LeaveMessage, user_jid, room_jid\n\n        connector.onInvite (room_jid, from_jid, message) =>\n          action = if @options.autojoin then \"joining\" else \"ignoring\"\n          @logger.info \"Got invite to #{room_jid} from #{from_jid} - #{action}\"\n          joinRoom(room_jid) if @options.autojoin\n\n      firstTime = false\n    connector.connect()\n\n    @connector = connector\n\n  userIdFromJid: (jid) ->\n    try\n      jid.match(/^\\d+_(\\d+)@chat\\./)[1]\n    catch e\n      @logger.error \"Bad user JID: #{jid}\"\n\n  # Convenience HTTP Methods for posting on behalf of the token'd user\n  get: (path, callback) ->\n    @request \"GET\", path, null, callback\n\n  post: (path, body, callback) ->\n    @request \"POST\", path, body, callback\n\n  request: (method, path, body, callback) ->\n    @logger.debug \"Request:\", method, path, body\n    host = @options.host or \"api.hipchat.com\"\n    headers = \"Host\": host\n\n    unless @options.token\n      return callback \"No API token provided to Hubot\", null\n\n    options =\n      agent  : false\n      host   : host\n      port   : 443\n      path   : path += \"?auth_token=#{@options.token}\"\n      method : method\n      headers: headers\n\n    if method is \"POST\"\n      headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n      options.headers[\"Content-Length\"] = body.length\n\n    request = HTTPS.request options, (response) =>\n      data = \"\"\n      response.on \"data\", (chunk) ->\n        data += chunk\n      response.on \"end\", =>\n        if response.statusCode >= 400\n          @logger.error \"HipChat API error: #{response.statusCode}\"\n        try\n          callback null, JSON.parse(data)\n        catch err\n          callback null, data or { }\n      response.on \"error\", (err) ->\n        callback err, null\n\n    if method is \"POST\"\n      request.end(body, \"binary\")\n    else\n      request.end()\n\n    request.on \"error\", (err) =>\n      @logger.error err\n      @logger.error err.stack if err.stack\n      callback err\n\nerrmsg = (err) ->\n  err + (if err.stack then '\\n' + err.stack else '')\n\nexports.use = (robot) ->\n  new HipChat robot\n"
  },
  {
    "path": "src/promises.coffee",
    "content": "{Promise} = require \"rsvp\"\n\nPromise::done = (done) -> @then done\nPromise::fail = (fail) -> @then null, fail\n\nmodule.exports = -> new Promise()\n"
  },
  {
    "path": "src/test.coffee",
    "content": "\nuser = id: 1, name: \"yannick\"\noldUser = role: \"lol\", name: \"bob\"\nfor key, value of oldUser\n  unless key of user\n    user[key] = value\nconsole.log user, oldUser\n"
  }
]