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.
[](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
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.