1 | [ ![Codeship Status for rbergman/ac-koa-hipchat](https://codeship.io/projects/22bfa080-fbc3-0131-d6f8-5a73486b8860/status)](https://codeship.io/projects/29249)
|
2 |
|
3 | # What is this?
|
4 |
|
5 | A [Node.js](http://nodejs.org) and [Koa.js](http://koajs.com)-based library for building [HipChat Connect add-ons](https://www.hipchat.com/docs/apiv2/addons), built up from lower-level libraries such as [ac-node](rbergman/ac-node), [ac-node-hipchat](rbergman/ac-node-hipchat), and [ac-koa](rbergman/ac-koa).
|
6 |
|
7 | This library is designed to make writing Node.js-based add-ons as simple as possible while improving on the design limitations of its predecessor, [Atlassian Connect Express - HipChat](https://www.npmjs.org/package/atlassian-connect-express-hipchat).
|
8 |
|
9 | This is an early, alpha-quality release, but can be used to build real add-ons today. Future versions may include backward-incompatible changes.
|
10 |
|
11 | # Getting started
|
12 |
|
13 | ## Dependencies
|
14 |
|
15 | If you're already comfortable with Node.js, the main thing you need to know to get started is that Koa (and therefore this library) requires Node v0.11.x. Using [nvm](https://github.com/creationix/nvm) to manage multiple instances of Node is recommended. If you need more help with Node, `nvm`, or `npm`, please start with their public documentation.
|
16 |
|
17 | By default, `ac-koa-hipchat` expects Redis to be available, for the persistence of tenant installations. You can provide your own persistent store implementation if necessary, though that is a more advanced topic that will be treated elsewhere. See the [ac-node](rbergman/ac-node) library for the Redis store implementation and compatibility tests as a guide if you need a custom store, otherwise make sure you have Redis is installed and running locally.
|
18 |
|
19 | ## A first add-on
|
20 |
|
21 | Writing basic HipChat add-ons with `ac-koa-hipchat` requires very little code to get up and running. Here's an example of a simple yet complete add-on, in two files:
|
22 |
|
23 | ### web.js
|
24 |
|
25 | ```
|
26 | #!javascript
|
27 |
|
28 | var ack = require('ac-koa').require('hipchat');
|
29 | var pkg = require('./package.json');
|
30 | var app = ack(pkg);
|
31 |
|
32 | var addon = app.addon()
|
33 | .hipchat()
|
34 | .allowRoom(true)
|
35 | .scopes('send_notification');
|
36 |
|
37 | addon.webhook('room_enter', function *() {
|
38 | yield this.roomClient.sendNotification('Hi, ' + this.sender.name + '!');
|
39 | });
|
40 |
|
41 | app.listen();
|
42 | ```
|
43 |
|
44 | ### package.json
|
45 |
|
46 | ```
|
47 | #!json
|
48 |
|
49 | {
|
50 | "name": "hipchat-greeter-example",
|
51 | "displayName": "HipChat Greeter Example Add-on",
|
52 | "description": "Greets people when they join a HipChat room",
|
53 | "version": "0.1.0",
|
54 | "author": {
|
55 | "name": "Your Name",
|
56 | "url": "http://yourcompany.com"
|
57 | },
|
58 | "license": "Apache 2.0",
|
59 | "engines": {
|
60 | "node": "~0.11.13"
|
61 | },
|
62 | "scripts": {
|
63 | "web": "node --harmony web.js"
|
64 | "web-dev": "nodemon --harmony -e js,json,css,hbs web.js",
|
65 | "tunnel": "ngrok 3000"
|
66 | },
|
67 | "development": {
|
68 | "port": 3000
|
69 | },
|
70 | "production": {
|
71 | "localBaseUrl": "https://hipchat-greeter-example.yourpaas.com",
|
72 | "redisEnv": "NAME_OF_ENV_VAR_CONTAINING_REDIS_URL",
|
73 | "port": "$PORT"
|
74 | },
|
75 | "dependencies": {
|
76 | "ac-koa": "^0.1.4",
|
77 | "ac-koa-hipchat": "^0.1.6"
|
78 | }
|
79 | }
|
80 | ```
|
81 |
|
82 | ## Running the server
|
83 |
|
84 | To run this example yourself, add these files to a new directory and run the following commands there:
|
85 |
|
86 | ```
|
87 | #!bash
|
88 |
|
89 | $ npm install
|
90 | $ npm run web
|
91 | ```
|
92 |
|
93 | If the server started as expected, you'll see something like the following emitted:
|
94 |
|
95 | ```
|
96 | #!bash
|
97 |
|
98 | info: Atlassian Connect add-on started at http://hostname.local:3000
|
99 | ```
|
100 |
|
101 | To double check that the server is running correctly, try requesting it's add-on descriptor:
|
102 |
|
103 | ```
|
104 | #!bash
|
105 |
|
106 | $ curl http://hostname.local:3000/addon/capabilities
|
107 | ```
|
108 |
|
109 | A successful request will return a HipChat capabilities descriptor for the add-on.
|
110 |
|
111 | ## Optimizing your dev loop
|
112 |
|
113 | To get the server to restart automatically when code changes, [nodemon](https://github.com/remy/nodemon) is recommended, and can be installed by simply running the following:
|
114 |
|
115 | ```
|
116 | #!bash
|
117 |
|
118 | $ npm install -g nodemon
|
119 | ```
|
120 |
|
121 | Then you can run this command to start the server for development:
|
122 |
|
123 | ```
|
124 | #!bash
|
125 |
|
126 | $ npm run web-dev
|
127 | ```
|
128 |
|
129 | In the example above, `nodemon` is set up to monitor js, json, css, and hbs (handlebars) files. If your add-on use additional file types, you may want to add them to the `web-dev` script configuration in `package.json`.
|
130 |
|
131 | If you encounter errors while following these steps, double check that you're using Node v0.11.x and that all of the above dependencies installed correctly.
|
132 |
|
133 | ## Preparing the add-on for installation
|
134 |
|
135 | Now that you have a server running, you'll want to try it somehow. The next step is different depending on whether you're going to be developing with hipchat.com or a private HipChat instance being hosted behind your corporate firewall.
|
136 |
|
137 | ### Developing with HipChat.com
|
138 |
|
139 | The easiest way to test with hipchat.com while developing on your local machine is to use [ngrok](https://ngrok.com). Download and install it now if you need to -- it's an amazing tool that will change the way you develop and share web applications.
|
140 |
|
141 | You may also want to set up your own `ngrok` account to access advanced features like being able to specify custom subdomains, but it's not necessary.
|
142 |
|
143 | Now you can start a secure tunnel to your add-on that's accessible by the outside world, which will allow you to install it at hipchat.com while you test it:
|
144 |
|
145 | ```
|
146 | #!bash
|
147 |
|
148 | $ npm run tunnel
|
149 | ```
|
150 |
|
151 | That command will start ngrok and output the address of your new tunnel, which should look something like `https://3a4bfceb.ngrok.com`. This will be the value you use for your "local base url" needed by the Installation step.
|
152 |
|
153 | While ngrok will forward both HTTP and HTTPS, for the protection of you and your HipChat group members, you should always use HTTPS when running your add-on on the public internet.
|
154 |
|
155 | ### Developing with a private server
|
156 |
|
157 | To install your add-on on a private HipChat server, both the add-on server and HipChat server need to be able to connect to each other via HTTP or HTTPS on your local network. Simply determine an HTTP url that your HipChat server can use to connect to your locally running add-on, and use that as the value of your "local base url" needed by the Installation step.
|
158 |
|
159 | If all goes well, you won't have to change anything from the defaults, as `ac-koa-hipchat` will simply attempt to use the OS's hostname to build the local base url, which may already be good enough for your private network.
|
160 |
|
161 | ## Installation
|
162 |
|
163 | ### Configuring the add-on's local base url
|
164 |
|
165 | Now, we need to tell the add-on server where it's running so that it can successfully be installed. You can do this in one of the following ways, using the local base url determined in the prior step, appropriate to your environment:
|
166 |
|
167 | 1. Set the `LOCAL_BASE_URL` environment variable when you start the server:
|
168 |
|
169 | ```
|
170 | #!bash
|
171 |
|
172 | $ LOCAL_BASE_URL=https://3a4bfceb.ngrok.com npm run web-dev
|
173 | ```
|
174 |
|
175 | 2. Add it to your add-on's `package.json` configuration in the `development` section:
|
176 |
|
177 | ```
|
178 | #!json
|
179 |
|
180 | "development": {
|
181 | "localBaseUrl" : "https://3a4bfceb.ngrok.com",
|
182 | "port": 3000
|
183 | },
|
184 | ```
|
185 |
|
186 | The `ac-koa-hipchat` library first looks for the configuration values it needs as environment variables, and then in the current runtime environment section of the configuration. If the local base url isn't not found in either location, it defaults to `http://<hostname>:$PORT`, but when advertising that address it can't be properly installed with hipchat.com.
|
187 |
|
188 | Which section of the configuration is used (e.g. `production`, `development`, `test`, etc) depends on the value of the environment variable `NODE_ENV`, whose value defaults to `development`. When running your server on a PaaS such as [Heroku](http://heroku.com), you'll want to set `NODE_ENV=production`.
|
189 |
|
190 | When properly configured, you'll see the server report the new local base url when it starts up:
|
191 |
|
192 | ```
|
193 | #!bash
|
194 |
|
195 | info: Atlassian Connect add-on started at https://3a4bfceb.ngrok.com
|
196 | ```
|
197 |
|
198 | ### Manually installing the add-on using HipChat's admin UI
|
199 |
|
200 | To install your add-on into HipChat, you have to register your addon's capabilities descriptor.
|
201 |
|
202 | HipChat add-ons can operate inside a room or within the entire account. When developing, you should probably register your add-on inside a room you've created just for testing. Also, you can only register add-ons inside a room where you are an administrator.
|
203 |
|
204 | To register your add-on descriptor, navigate to the rooms administration page at `https://<your-account>.hipchat.com/rooms` (or whatever url your private server is running at, if appropriate). Then select one of your rooms in the list. In the following page, select `Integrations` in the sidebar:
|
205 |
|
206 | ![Installation Screenshot](https://s3.amazonaws.com/uploads.hipchat.com/10804/124261/vSnwkHxO1mNue2C/upload.png)
|
207 |
|
208 | At the bottom of the page, you'll find the `Add new private integration` form. Paste your descriptor url in the `Capabilities URL` field and then click `Add integration`. This will initiate the installation of your add-on for that room.
|
209 |
|
210 | # Example Projects
|
211 |
|
212 | The example illustrated above comes from the following example project:
|
213 |
|
214 | * https://bitbucket.org/rbergman/ac-koa-hipchat-greeter
|
215 |
|
216 | See these additional add-ons for more complete examples:
|
217 |
|
218 | * https://bitbucket.org/rbergman/ac-koa-hipchat-hearsay
|
219 | * __TODO__: more examples :D
|
220 |
|
221 | # Library Features
|
222 |
|
223 | This library, in conjunction with it's product-agnostic base library `ac-koa`, provides help with many aspects of add-on development, such as:
|
224 |
|
225 | * Support for mounting multiple addons in a single Koa app
|
226 | * Configuration of commonly required Koa middleware
|
227 | * Choice of programmatic HipChat add-on descriptor builder or providing a full or partial descriptor object literal
|
228 | * Multitenant registration and data partioning
|
229 | * High-level conveniences for mounting webhook handlers
|
230 | * A REST API client with built-in OAuth2 token acquisition and refresh
|
231 | * JWT authentication validation, refresh, and token generation for web UI routes (e.g. the `configurable` capability)
|
232 | * A tenant-aware API for dynamically adding and removing room webhooks
|
233 |
|
234 | In the documentation below, we use the terms `ctx` and 'Koa context' interchangably to refer to the context object that contains both standard Koa request/response data and objects and this library's request objects and services.
|
235 |
|
236 | ## Multiple add-on support
|
237 |
|
238 | _Note: The builder support described in this section this may not yet be fully implemented, though it should at least be possible when providing object literal descriptors._
|
239 |
|
240 | More than one add-on can be mounted at a time in a single Koa app by giving each a unique mount scope. For example:
|
241 |
|
242 | ```
|
243 | #!javascript
|
244 |
|
245 | var addon1 = app.addon('addon1')
|
246 | .hipchat()
|
247 | .key('addon1-key')
|
248 | .name('Addon 1')
|
249 | .allowRoom(true)
|
250 | .scopes('send_notification');
|
251 |
|
252 | var addon2 = app.addon('addon2')
|
253 | .hipchat()
|
254 | .key('addon2-key')
|
255 | .name('Addon 2')
|
256 | .allowRoom(true)
|
257 | .scopes('send_notification');
|
258 | ```
|
259 |
|
260 | The scope given to each `addon()` method is automatically used for both data partioning and route disambiguation.
|
261 |
|
262 | ## Addon definition
|
263 |
|
264 | __TODO__: Cover both object literal and builder forms of add-on definition, plus the builder API docs and subscription of dynamic webhook listeners.
|
265 |
|
266 | ## Multitenancy
|
267 |
|
268 | One add-on can be installed with multiple HipChat OAuth2 clients, referred to here as 'tenants'. In practice, a tenant is either a HipChat room or group, depending on the installation scope of the add-on.
|
269 |
|
270 | ### Tenant registration and information
|
271 |
|
272 | Tenant installation and uninstallation is handled automatically by the library, which configures the necessary routes and handlers for each mounted add-on. Each installation results in the registration information for that tenant to be verified and stored for later use, including the tenant's shared secret, used for bi-directional authentication.
|
273 |
|
274 | Add-on implementations are given access to the `tenant` information object in every Koa context for routes and webhook listeners in which this library is involved.
|
275 |
|
276 | #### `ctx.tenant`
|
277 |
|
278 | Field | Description
|
279 | -------------------: | --------------------------------------------------------------------------------------------
|
280 | `id` | This tenant's id.
|
281 | `group` | This tenant's group id.
|
282 | `secret` | This tenant's shared secret.
|
283 | `room` | This tenant's room id, if installed in a single room.
|
284 | `webhookToken` | A secret token added to all dynamically generated webhooks, as an extra measure of security.
|
285 | `links` | A collection of this tenant's relevant URLs.
|
286 | `links.capabilities` | This tenant's capabilities descriptor URL.
|
287 | `links.base` | This tenant's base URL.
|
288 | `links.api` | This tenant's base API URL.
|
289 | `links.token` | This tenant's OAuth2 token generation URL.
|
290 |
|
291 | ### Tenant authentication
|
292 |
|
293 | This library handles bi-direction authentication between tenants and add-ons. It provides the following facilities:
|
294 |
|
295 | #### Inbound JWT signature verification
|
296 |
|
297 | For add-on routes that provide web UI to the tenant, such as the one defined in an add-on's `configurable` capabilitity, use the `addon` object's `authenticate()` middleware to protect your route. This middleware will then verify that requests have a valid JWT signature provided either as the `signed_request` query parameter or in a standard HTTP `Authorization` header with the format `Authorization: JWT token=<jwt-token-value>`.
|
298 |
|
299 | For example, one would secure an addon's `/configure` route with this middleware as follows:
|
300 |
|
301 | ```
|
302 | #!javascript
|
303 |
|
304 | addon.get('/configure',
|
305 | addon.authenticate(),
|
306 | function *() {
|
307 | // Normal Koa route handling here...
|
308 | }
|
309 | );
|
310 | ```
|
311 |
|
312 | Requests successfully passing through this middleware will have the following object available on the Koa context:
|
313 |
|
314 | #### `ctx.authentication`
|
315 |
|
316 | Field | Description
|
317 | -------------------: | --------------------------------------------------------------------------------------------
|
318 | `issuer` | The authenticated tenant's id.
|
319 | `issued` | The timestamp at which the token was generated.
|
320 | `userId` | The id of the user making the request.
|
321 | `expiry` | The time at which the token should be expired.
|
322 | `context` | An additional request context object sent by the tenant as part of the signed data. This may contain the current user's timezone in a field named `tz`.
|
323 | `token` | A refreshed version of the JWT token, suitable for use in subsequent requests over Ajax or as form or link parameters. See the [Hearsay](https://bitbucket.org/rbergman/ac-koa-hipchat-hearsay) example add-on for a demonstration of this technique. We prefer this approach to maintaining state for iframed add-on UI over cookies due to some anti-click-jacking browser security models that prevent cookies from being set in cross-domain iframes.
|
324 |
|
325 | #### Outbound request token handling
|
326 |
|
327 | Outbound requests to the tenant's REST APIs require an current OAuth2 bearer token, which must be refreshed via the tenant's token API when it expires. As long as the add-on uses the `ctx.tenantClient` or `ctx.roomClient` APIs, this token management is handled automatically. See below for information about the these services.
|
328 |
|
329 | ### Tenant services
|
330 |
|
331 | For convenient add-on implementation, several service objects are attached to the Koa context that provide tenant-aware operations.
|
332 |
|
333 | #### Tenant data storage
|
334 |
|
335 | In order to support multiple tenants concurrently, a tenant-aware data storage abstraction is used throughout the library to partition tenant data, and a derivative of a tenant's unique storage object is provided with each Koa context, allowing add-ons access to a simple, partioned location to store basic key/value information. Storage of more structured data in alternative data stores is left as an exercise for each add-on, though every Koa context contains the full tenant data model, making such manual data partioning straightforward.
|
336 |
|
337 | #### `ctx.tenantStore`
|
338 |
|
339 | Method | Description
|
340 | -------------------: | --------------------------------------------------------------------------------------------
|
341 | `get(key)` | Gets a value for a given key. Returns a promise.
|
342 | `set(key, value)` | Sets a value for a given key. Returns a promise.
|
343 | `del(key)` | Deletes a value for a given key. Returns a promise.
|
344 | `all()` | Gets all values in the current storage scope. Returns a promise.
|
345 | `narrow(scope)` | Creates and returns a substore narrowed by the given scope.
|
346 |
|
347 | #### Tenant REST Client
|
348 |
|
349 | A tenant REST client is provided that automatically handles the construction of authenticated requests for each of opertions available in the [HipChat API v2](https://www.hipchat.com/docs/apiv2).
|
350 |
|
351 | #### `ctx.tenantClient`
|
352 |
|
353 | A [HipChat API v2](https://www.hipchat.com/docs/apiv2) REST client API with automatic automatic Oauth2 token acquisition and refresh.
|
354 |
|
355 | Method | Description
|
356 | ----------------------------------------------------: | --------------------------------------------------------------------------------------------
|
357 | `sendNotification(roomIdOrName, message, options)` | Sends a notification message to a room by name or id. The optional `options` object may include any or all of `color`, `notify`, or `format` -- see the [API docs](https://www.hipchat.com/docs/apiv2/method/send_room_notification) for valid values. Returns a promise.
|
358 | `createWebhook(roomIdOrName, definition)` | Creates a new webhook in a specific room given a definition -- see the [API docs](https://www.hipchat.com/docs/apiv2/method/create_webhook) for the definition fields. Returns a promise.
|
359 | `deleteWebhook(roomIdOrName, webhookId)` | Deletes an existing webhook by id in a specific room. Returns a promise.
|
360 | __TODO__ | (Implement and document the remaining API methods)
|
361 |
|
362 | #### `ctx.roomClient`
|
363 |
|
364 | A thin wrapper around the `tenantClient` object that's provided in room-specific request contexts. Only the methods of the `tenantClient` that require a room id are exposed on this object, with the room id already partially applied for convenience.
|
365 |
|
366 | Method | Description
|
367 | ----------------------------------------------------: | --------------------------------------------------------------------------------------------
|
368 | `sendNotification(message, options)` | Sends a notification message to the current room. The optional `options` object may include any or all of `color`, `notify`, or `format` -- see the [API docs](https://www.hipchat.com/docs/apiv2/method/send_room_notification) for valid values. Returns a promise.
|
369 | __TODO__ | (Implement and document the remaining API methods)
|
370 |
|
371 | #### `ctx.tenantWebhooks`
|
372 |
|
373 | A high-level API for adding or removing webhooks for a given tenant, in such a way that this library can manage any required routing, authentication, and dispatching necessary to provide a similar level of service as is enjoyed by statically defined webhooks (i.e. those defined as part of the capabilities decriptor when the addon is defined at server startup).
|
374 |
|
375 | See the [Hearsay](https://bitbucket.org/rbergman/ac-koa-hipchat-hearsay) example add-on for a functioning example of the webhook manager API.
|
376 |
|
377 | Method | Description
|
378 | ----------------------------------------------------: | --------------------------------------------------------------------------------------------
|
379 | `get(name)` | __TODO__
|
380 | `get(roomId, name)` | __TODO__
|
381 | `add(event)` | __TODO__
|
382 | `add('room_message', pattern)` | __TODO__
|
383 | `add(definition)` | __TODO__
|
384 | `add(roomId, event)` | __TODO__
|
385 | `add(roomId, 'room_message', pattern)` | __TODO__
|
386 | `add(roomId, definition)` | __TODO__
|
387 | `remove(name)` | __TODO__
|
388 | `remove(roomId, name)` | __TODO__
|
389 |
|
390 | #### Webhook data extraction and normalization
|
391 |
|
392 | Webhook listeners added via either `addon.webhook(...)` (for webhooks defined using the add-on builder API convenience method) or `addon.onWebhook(...)` (for dynamic or non-builder based webhook listeners) are provided with both raw and normalized views of the webhook body. Since not all webhooks organize their data the same way, extracted and normalized webhook fields are attached directly to the Koa context, along side the raw webhook object. Not all webhooks provide all fields and some of these fields are themselves complex objects, so consult the [API docs](https://www.hipchat.com/docs/apiv2/webhooks) for information about what to expect when.
|
393 |
|
394 | Field | Description
|
395 | --------------------: | ----------------------------------------------------------------------------------------------------------
|
396 | `ctx.webhook` | The raw webhook payload.
|
397 | `ctx.webhookId` | The webhook id.
|
398 | `ctx.room` | The relevant room model object.
|
399 | `ctx.sender` | The relevant sender model object.
|
400 | `ctx.message` | The message object, if any. Only in `room_notification` and `room_message` webhooks.
|
401 | `ctx.content` | The message field of the message object, if any. Only in `room_notification` and `room_message` webhooks.
|
402 | `ctx.topic` | The room topic, if any. Only in `room_topic_change` webhooks.
|
403 |
|
404 | # Anatomy of an add-on
|
405 |
|
406 | Here's a closer look at the original example add-on, this time with comments:
|
407 |
|
408 | ```
|
409 | #!javascript
|
410 |
|
411 | // Require the 'ac-koa' module, and then tell it to load the 'hipchat' adapter
|
412 | // from 'ac-koa-hipchat'
|
413 | var ack = require('ac-koa').require('hipchat');
|
414 | // Require our package.json file, which doubles as the configuration from which
|
415 | // we'll generate the add-on descriptor and server's runtime parameters
|
416 | var pkg = require('./package.json');
|
417 | // Create the base Koa app, via an 'ac-koa' factory method that helps preconfigure
|
418 | // and decorate the app object
|
419 | var app = ack(pkg);
|
420 |
|
421 | // Now build and mount an AC add-on on the Koa app; we can either pass a full or
|
422 | // partial descriptor object to the 'addon()' method, or when we provide none, as
|
423 | // in this example, we can instead create the descriptor using a product-specific
|
424 | // builder API
|
425 | var addon = app.addon()
|
426 | // Use the hipchat descriptor builder
|
427 | .hipchat()
|
428 | // Indicate that the descriptor should mark this as installable in rooms
|
429 | .allowRoom(true)
|
430 | // Provide the list of permissions scopes the add-on requires
|
431 | .scopes('send_notification');
|
432 |
|
433 | // Subscribe to the 'room_enter' webhook, and provide an event listener. Under
|
434 | // the covers, this adds a webhook entry to the add-on descriptor, mounts a common
|
435 | // webhook endpoint on the Koa app, and brokers webhook POST requests to the event
|
436 | // listener as appropriate
|
437 | addon.webhook('room_enter', function *() {
|
438 | // 'this' is a Koa context object, containing standard Koa request and response
|
439 | // contextual information as well as hipchat-specific models and services that
|
440 | // make handling the webhook as simple as possible
|
441 | yield this.roomClient.sendNotification('Hi, ' + this.sender.name + '!');
|
442 | });
|
443 |
|
444 | // Now that the descriptor has been defined along with a useful webhook handler,
|
445 | // start the server
|
446 | app.listen();
|
447 | ```
|