diff --git a/.gitignore b/.gitignore index 194c116..9f91844 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules npm-debug.log .env +output diff --git a/Makefile b/Makefile deleted file mode 100644 index 69da9a3..0000000 --- a/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -.PHONY: all npm validate test security-check clean - -ci: clean npm validate test - -clean: - rm -rf output/ - -npm: - npm prune - npm install - -validate: npm security-check - -test: - npm test - -security-check: - ./node_modules/.bin/retire -n diff --git a/README.md b/README.md index a6c3ea3..6345019 100644 --- a/README.md +++ b/README.md @@ -11,126 +11,11 @@ $ npm install --save node-gitter ## Basics -```js -var Gitter = require('node-gitter'); - -var gitter = new Gitter(token); - -gitter.currentUser() -.then(function(user) { - console.log('You are logged in as:', user.username); -}); -``` ### Authentication It's mandatory to provide a valid Gitter OAuth token in order to use the client. You can obtain one from [https://developer.gitter.im/apps](https://developer.gitter.im/apps). -### Promises or Callbacks - -The client implements both. The following code is equivalent: - -Using promises: - -```js -gitter.rooms.join('gitterhq/sandbox') -.then(function(room) { - console.log('Joined room: ', room.name); -}) -.fail(function(err) { - console.log('Not possible to join the room: ', err); -}) -``` - -Using node-style callbacks: - -```js -gitter.rooms.join('gitterhq/sandbox', function(err, room) { - if (err) { - console.log('Not possible to join the room: ', err); - return; - } - - console.log('Joined room: ', room.name); -}); - -``` - -## Users - -### Current user -```js -gitter.currentUser() -``` - -### Current user rooms, repos, orgs and channels -```js -gitter.currentUser() -.then(function(user) { - user.rooms() - user.repos() - user.orgs() - user.channels() -}) -``` - -### Find a user -```js -gitter.users.find(userId) -``` - -## Rooms - -### Join a room -```js -gitter.rooms.join('gitterhq/sandbox') -``` - -### Post a message to a room -```js -gitter.rooms.join('gitterhq/sandbox') -.then(function(room) { - room.send('Hello world!'); -}); - -``` - -### Listen for chatMessages, Events or Users in a room -```js -gitter.rooms.find(roomId).then(function(room) { - - var events = room.streaming().chatMessages(); - - // The 'snapshot' event is emitted once, with the last messages in the room - events.on('snapshot', function(snapshot) { - console.log(snapshot.length + ' messages in the snapshot'); - }); - - // The 'chatMessages' event is emitted on each new message - events.on('chatMessages', function(message) { - console.log('A message was ' + message.operation); - console.log('Text: ', message.model.text); - }); -}); -``` - -### Room users, channels and messages -```js -gitter.rooms.find(roomId) -.then(function(room) { - room.users() - room.channels() - room.chatMessages() -}); -``` - -### Leave a room -```js -gitter.rooms.find(roomId) -.then(function(room) { - room.leave() -}); -``` # License diff --git a/VERSION b/VERSION deleted file mode 100644 index 3c43790..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.2.6 diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..b05a00a --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,21 @@ +var gulp = require('gulp'); +var mocha = require('gulp-spawn-mocha'); + +/** + * test + */ +gulp.task('test', function() { + return gulp.src(['./test/**/*.js'], { read: false }) + .pipe(mocha({ + reporter: 'spec', + timeout: 10000, + istanbul: { + dir: 'output/coverage-reports/' + }, + env: { + Q_DEBUG: 1 + } + })); +}); + +gulp.task('default', ['test']); diff --git a/lib/client.js b/lib/client.js index 8ef90e3..dd6040f 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,162 +1,165 @@ -/* jshint node:true, unused:true */ - -var https = require('https'); -var http = require('http'); -var qs = require('qs'); -var Q = require('q'); -var url = require('url'); -var debug = require('debug')('node-gitter'); - -var Client = function(token, opts) { - opts = opts || {}; - this.token = token; - - // Will be set once authenticated - this.currentUser = null; - - if(opts.apiEndpoint) { - var parsed = url.parse(opts.apiEndpoint); - - this.host = parsed.hostname; - this.port = parsed.port; - this.protocol = parsed.protocol.replace(/:\/?\/?$/,''); - - var pathname = parsed.pathname; - if(pathname[pathname.length - 1] === '/') { - pathname = pathname.substring(0, pathname.length - 1); - } - - this.pathPrefix = pathname; - } else { - this.host = opts.host || 'api.gitter.im'; - this.port = opts.port || 443; - this.protocol = this.port === 443 ? 'https' : 'http'; - this.pathPrefix = (opts.prefix ? '/api/' : '/') + (opts.version || 'v1'); +'use strict'; + +var _ = require('lodash'); +var request = require('request'); +var Promise = require('bluebird'); +var url = require('url'); +var errors = require('./errors'); +var requestExt = require('request-extensible'); +Client.PACKAGE_VERSION = require('../package.json').version; +var resources = require('./resources'); +Client.resources = resources; +var debug = require('debug')('node-gitter:client'); + +/* Constructs the request which the client will use */ +function getRequestLib(options) { + var underlyingRequest = options.request || request; + /* If extensions have been specified, use request-extensible */ + if (options.extensions) { + return requestExt({ + request: underlyingRequest, + extensions: options.extensions + }); } -}; - -['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach(function(method) { - Client.prototype[method.toLowerCase()] = function(path, opts) { - return this.request(method, path, opts); - }; -}); -Client.prototype.request = function(method, path, opts) { - opts = opts || {}; - var self = this; - var defer = Q.defer(); - - var headers = { - 'Authorization': 'Bearer ' + this.token, - 'Accept': 'application/json' - }; + return underlyingRequest; +} + +function Client(options) { + if (!options) options = {}; + this.spread = options.spread; + this.accessToken = options.accessToken; + this.requestLib = getRequestLib(options); + this.defaultHeaders = _.extend({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'User-Agent': 'NodeGitter/' + Client.PACKAGE_VERSION + }, options.headers); + + this._prepResources(); +} + +Client.prototype = { + _prepResources: function() { + + for (var name in resources) { + this[ + name[0].toLowerCase() + name.substring(1) + ] = new resources[name](this); + } - if(opts.body) { - headers['Content-Type'] = 'application/json'; - } + }, - var req_opts = { - hostname: this.host, - port: this.port, - method: method, - path: this.pathPrefix + (opts.query ? path + '?' + qs.stringify(opts.query) : path), - headers: headers - }; + _request: function(method, path, data, spec, options) { + var self = this; - var scheme = { http: http, https: https}[this.protocol]; + var headers = _.extend({}, this.defaultHeaders, spec.headers, options.headers); - debug('%s %s', req_opts.method, req_opts.path); + headers.Authorization = 'Bearer ' + (options.accessToken || this.accessToken); - var req = scheme.request(req_opts, function(res) { - // Accommodate webpack/browserify - if(res.setEncoding) { - res.setEncoding('utf-8'); - } + return new Promise(_.bind(function(resolve, reject) { + var uri = url.format({ + protocol: "https:", + host: "api.gitter.im", + pathname: path, + query: options.query + }); - self.rateLimit = res.headers['x-ratelimit-limit']; - self.remaining = res.headers['x-ratelimit-remaining']; + debug(method+' '+uri, headers); - var data = ''; - res.on('data' , function(chunk) { - data += chunk; - }); + var useSpread = options.spread !== undefined ? options.spread : this.spread; - res.on('end', function() { - var body; - try { - body = JSON.parse(data); - } catch(err) { - defer.reject(new Error(res.statusCode + ' ' + data)); + function spreadResponse(body, response) { + resolve([body, response]); } - if (res.statusCode !== 200) { - defer.reject(new Error(res.statusCode + ' ' + data)); - } else { - defer.resolve(body); + function noSpreadResponse(body) { + resolve(body); } - }); - }); - req.on('error', function(err) { - defer.reject(err); - }); - - if (opts.body) { - req.write(JSON.stringify(opts.body)); - } - - req.end(); - - return defer.promise; -}; - -Client.prototype.stream = function(path, cb) { - var headers = { - 'Authorization': 'Bearer ' + this.token, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }; - - var opts = { - host: 'stream.gitter.im', - port: 443, - method: 'GET', - path: this.pathPrefix + path, - headers: headers - }; - - debug('%s %s', opts.method, opts.path); - - var heartbeat = " \n"; - - var req = https.request(opts, function(res) { - var msg = ''; - - res.setEncoding('utf-8'); - - res.on('data' , function(chunk) { - var m = chunk.toString(); - if (m === heartbeat) { - msg = ''; - return; + var resolver = useSpread ? spreadResponse : noSpreadResponse; + + // Pass the options through to the extensions, but don't include the options + // that have been mutated by this method + var passThroughOptions = _.omit(options, 'query', 'accessToken', 'spread', 'headers', 'body', 'url', 'json', 'followRedirect', 'followAllRedirects'); + + var requestOptions = _.defaults({ + method: method, + uri: uri, + headers: headers, + body: data, + json: !!data, + gzip: true, + encoding: options.encoding === undefined ? 'utf8' : options.encoding, + followRedirect: true, + followAllRedirects: true, // Redirects for non-GET requests + }, passThroughOptions); + + this.requestLib(requestOptions, function(err, response, body) { + if (err) { + err.response = response; + return reject(err); + } + + if (spec.checkOperation) { + /* 404 means "false" */ + if (response.statusCode === 404) { + return resolver(false, response); + } + + /* 2XX means "true" */ + if (response.statusCode < 300) { + return resolver(true, response); + } + } + + if (options.treat404asEmpty && response.statusCode === 404) { + return resolver(null, response); + } + + try { + var parsedBody = self._parseBody(response, body); + + if (response.statusCode >= 400) { + var message = typeof parsedBody.message === 'string' ? parsedBody.message.replace(/\n+/g, ' ') : "HTTP " + response.statusCode; + debug(message, parsedBody || body); + return reject(new errors.GitterError(message, { + url: uri, + statusCode: response.statusCode, + headers: response.headers, + body: parsedBody || body + })); + } + + return resolver(parsedBody, response); + + } catch(e) { + return reject(e); + } + + }); + + }, this)); + + }, + + _parseBody: function(response, body) { + if (typeof body !== 'string') return body; + + // TODO: deal with various types + var contentType = response.headers['content-type']; + if (contentType) { + var ct = contentType.split(';')[0]; + + if (ct === 'application/json') { + return JSON.parse(body); } - msg += m; - try { - var evt = JSON.parse(msg); - msg = ''; - cb(evt); - } catch (err) { - // Partial message. Ignore it. - } - }); - }); + } - req.on('error', function(err) { - console.error('[stream]', err); - }); + return body; + } - req.end(); }; module.exports = Client; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..176cb7e --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,13 @@ +'use strict'; + +var createError = require('create-error'); + +// an error this library threw +var NodeGitterError = createError('NodeGitterError'); +// gitter's threw this error +var GitterError = createError(NodeGitterError, 'GitterError'); + +module.exports = { + NodeGitterError: NodeGitterError, + GitterError: GitterError +}; diff --git a/lib/faye.js b/lib/faye.js deleted file mode 100644 index 82df014..0000000 --- a/lib/faye.js +++ /dev/null @@ -1,75 +0,0 @@ -/* jshint node:true, unused:true */ - -var Faye = require('gitter-faye'); -var EventEmitter = require('eventemitter3'); - -// Authentication extension for Faye -var ClientAuthExt = function(opts) { - this.token = opts.token; - this.clientType = opts.clientType; -}; - -ClientAuthExt.prototype.outgoing = function(message, callback) { - if (message.channel == '/meta/handshake') { - if (!message.ext) message.ext = {}; - if (this.clientType) message.ext.client = this.clientType; - message.ext.token = this.token; - } - - callback(message); -}; - -// Snapshot extension for Faye -var SnapshotExt = function(opts) { - this.subscriptions = opts.subscriptions; -}; - -SnapshotExt.prototype.incoming = function(message, callback) { - if(message.channel == '/meta/subscribe' && message.ext && message.ext.snapshot) { - var sub = this.subscriptions[message.subscription]; - if (sub) sub.emitter.emit('snapshot', message.ext.snapshot); - } - - callback(message); -}; - -// Client wrapper - -var FayeClient = function(token, opts) { - opts = opts || {}; - var host = opts.host || 'https://ws.gitter.im/faye'; - - this.subscriptions = {}; - - this.client = new Faye.Client(host, {timeout: 60, retry: 5, interval: 1}); - this.client.addExtension(new ClientAuthExt({token: token, clientType: opts.clientType})); - this.client.addExtension(new SnapshotExt({subscriptions: this.subscriptions})); -}; - -FayeClient.prototype.subscribeTo = function(resource, eventName) { - if (this.subscriptions[resource]) return this.subscriptions[resource].emitter; - - var emitter = new EventEmitter(); - var subscription = this.client.subscribe(resource, function(msg) { - emitter.emit(eventName, msg); - }); - - this.subscriptions[resource] = { - eventName: eventName, - emitter: emitter, - subscription: subscription - }; - - return emitter; -}; - -FayeClient.prototype.disconnect = function() { - var self = this; - - Object.keys(this.subscriptions).forEach(function(sub) { - self.subscriptions[sub].subscription.cancel(); - self.subscriptions[sub].emitter.removeAllListeners(); - }); -}; - -module.exports = FayeClient; diff --git a/lib/gitter.js b/lib/gitter.js deleted file mode 100644 index 3e562e5..0000000 --- a/lib/gitter.js +++ /dev/null @@ -1,22 +0,0 @@ -/* jshint node:true, unused:true */ - -var Client = require('./client.js'); -var Users = require('./users.js'); -var Rooms = require('./rooms.js'); -var FayeClient = require('./faye.js'); - -var Gitter = function(token, opts) { - opts = opts || {}; - - this.client = new Client(token, opts.client); - this.faye = new FayeClient(token, opts.faye); - - this.users = new Users(null, this.client, this.faye); - this.rooms = new Rooms(null, this.client, this.faye); -}; - -Gitter.prototype.currentUser = function(cb) { - return cb ? this.users.current().nodeify(cb) : this.users.current(); -}; - -module.exports = Gitter; diff --git a/lib/resources/channels.js b/lib/resources/channels.js new file mode 100644 index 0000000..9d99a50 --- /dev/null +++ b/lib/resources/channels.js @@ -0,0 +1,19 @@ + +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + listForRoom: method({ + method: 'GET', + path: '/v1/rooms/:roomId/channels', + urlParams: ['roomId'] + }), + + listForUser: method({ + method: 'GET', + path: '/v1/user/:userId/channels', + urlParams: ['userId'] + }) +}); diff --git a/lib/resources/index.js b/lib/resources/index.js new file mode 100644 index 0000000..ba687c2 --- /dev/null +++ b/lib/resources/index.js @@ -0,0 +1,5 @@ +'use strict'; + +var requireDirectory = require('require-directory'); + +module.exports = requireDirectory(module); diff --git a/lib/resources/messages.js b/lib/resources/messages.js new file mode 100644 index 0000000..2cfce47 --- /dev/null +++ b/lib/resources/messages.js @@ -0,0 +1,28 @@ +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + listForRoom: method({ + method: 'GET', + path: '/v1/rooms/:roomId/chatMessages', + urlParams: ['roomId'] + }), + + create: method({ + method: 'POST', + path: '/v1/rooms/:roomId/chatMessages', + urlParams: ['roomId'] + }), + + update: method({ + method: 'PUT', + path: '/v1/rooms/:roomId/chatMessages/:chatMessageId', + urlParams: ['roomId', 'chatMessageId'] + }), + + streamForRoom: function() { + // TODO + } +}); diff --git a/lib/resources/orgs.js b/lib/resources/orgs.js new file mode 100644 index 0000000..708c31d --- /dev/null +++ b/lib/resources/orgs.js @@ -0,0 +1,12 @@ +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + listForUser: method({ + method: 'GET', + path: '/v1/user/:userId/orgs', + urlParams: ['userId'] + }) +}); diff --git a/lib/resources/repos.js b/lib/resources/repos.js new file mode 100644 index 0000000..f6bbbc8 --- /dev/null +++ b/lib/resources/repos.js @@ -0,0 +1,12 @@ +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + listForUser: method({ + method: 'GET', + path: '/v1/user/:userId/repos', + urlParams: ['userId'] + }) +}); diff --git a/lib/resources/rooms.js b/lib/resources/rooms.js new file mode 100644 index 0000000..ebf34e9 --- /dev/null +++ b/lib/resources/rooms.js @@ -0,0 +1,22 @@ +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + listForAuthUser: method({ + method: 'GET', + path: '/v1/rooms' + }), + + listForUser: method({ + method: 'GET', + path: '/v1/user/:userId/rooms', + urlParams: ['userId'] + }), + + join: method({ + method: 'POST', + path: '/v1/rooms' + }) +}); diff --git a/lib/resources/unreads.js b/lib/resources/unreads.js new file mode 100644 index 0000000..256d563 --- /dev/null +++ b/lib/resources/unreads.js @@ -0,0 +1,18 @@ +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + listForUserAndRoom: method({ + method: 'GET', + path: '/v1/user/:userId/rooms/:roomId/unreadItems', + urlParams: ['userId', 'roomId'] + }), + + markForUserAndRoom: method({ + method: 'POST', + path: '/v1/user/:userId/rooms/:roomId/unreadItems', + urlParams: ['userId', 'roomId'] + }) +}); diff --git a/lib/resources/users.js b/lib/resources/users.js new file mode 100644 index 0000000..aa1ec94 --- /dev/null +++ b/lib/resources/users.js @@ -0,0 +1,17 @@ +'use strict'; + +var Resource = require('tentacles/lib/Resource'); +var method = Resource.method; + +module.exports = Resource.extend({ + getAuthUser: method({ + method: 'GET', + path: '/v1/user/me' + }), + + listForRoom: method({ + method: 'GET', + path: '/v1/rooms/:roomId/users', + urlParams: ['roomId'] + }) +}); diff --git a/lib/rooms.js b/lib/rooms.js deleted file mode 100644 index 3b8aea6..0000000 --- a/lib/rooms.js +++ /dev/null @@ -1,111 +0,0 @@ -/* jshint node:true, unused:true */ - -var util = require('util'); -var EventEmitter = require('eventemitter3'); -var Q = require('q'); - -var Room = function(attrs, client, faye) { - EventEmitter.call(this); - - // API path to Room - this.path = '/rooms'; - - if (attrs) - Object.keys(attrs).forEach(function(k) { - this[k] = attrs[k]; - }.bind(this)); - - if (typeof this.users !== 'function') { - var users = this.users; - this.users = function() { - return Q.resolve(users); - } - } - - this.client = client; - this.faye = faye; -}; - -util.inherits(Room, EventEmitter); - -Room.prototype.findAll = function() { - return this.client.get(this.path); -}; - -Room.prototype.find = function(id, cb) { - var room = this.client.get(this.path + '/' + id) - .then(function(roomData) { - return new Room(roomData, this.client, this.faye); - }.bind(this)); - return cb ? room.nodeify(cb) : room; -}; - -Room.prototype.join = function(room_uri, cb) { - var room = this.client.post(this.path, {body: {uri: room_uri}}) - .then(function(roomData) { - return new Room(roomData, this.client, this.faye); - }.bind(this)); - return cb ? room.nodeify(cb) : room; -}; - -Room.prototype.send = function(message, cb) { - var msg = this.client.post(this.path + '/' + this.id + '/chatMessages', {body: {text: message}}); - return cb ? msg.nodeify(cb) : msg; -}; - -Room.prototype.sendStatus = function(message, cb) { - var msg = this.client.post(this.path + '/' + this.id + '/chatMessages', {body: {text: message, status: true}}); - return cb ? msg.nodeify(cb) : msg; -}; - -Room.prototype.removeUser = function(userId) { - return this.client.delete(this.path + '/' + this.id + '/users/' + userId); -}; - -Room.prototype.listen = function() { - this.client.stream(this.path + '/' + this.id + '/chatMessages', function(message) { - this.emit('message', message); - }.bind(this)); - return this; -}; - -['users', 'channels', 'chatMessages'].forEach(function(resource) { - Room.prototype[resource] = function(query, cb) { - var items = this.client.get(this.path + '/' + this.id + '/' + resource, { query: query }); - return cb ? items.nodeify(cb) : items; - }; -}); - -Room.prototype.subscribe = function() { - ['chatMessages', 'events', 'users'].forEach(function(resource) { - var resourcePath = '/api/v1/rooms/' + this.id + '/' + resource; - var events = this.faye.subscribeTo(resourcePath, resourcePath) - events.on(resourcePath, function(msg) { - this.emit(resource, msg); - }.bind(this)); - }.bind(this)); -}; - -Room.prototype.unsubscribe = function() { - ['chatMessages', 'events', 'users'].forEach(function(resource) { - var resourcePath = '/api/v1/rooms/' + this.id + '/' + resource; - var meta = this.faye.subscriptions[resourcePath]; - if (meta) meta.subscription.cancel(); - }.bind(this)); -}; - -// DEPRECATED Rooms is now an event emitter and all you need is to -// subscribe() to start receiving events -Room.prototype.streaming = function() { - this.subscribe(); - var fn = function() { return this; }.bind(this); - - return { - chatMessages: fn, - events: fn, - users: fn, - disconnect: function() { this.faye.disconnect(); }.bind(this) - } -} - -module.exports = Room; diff --git a/lib/users.js b/lib/users.js deleted file mode 100644 index b311fc8..0000000 --- a/lib/users.js +++ /dev/null @@ -1,63 +0,0 @@ -/* jshint node:true, unused:true */ - -var User = function(attrs, client, faye) { - // API path to Users - this.path = '/user'; - - if (attrs) - Object.keys(attrs).forEach(function(k) { - this[k] = attrs[k]; - }.bind(this)); - - this.client = client; - this.faye = faye; -}; - -User.prototype.current = function() { - return this.client.get(this.path) - .then(function(users) { - var userData = users[0]; - return new User(userData, this.client, this.faye); - }.bind(this)); -}; - -User.prototype.find = function(id, cb) { - var path = this.path + '/' + id; - return cb ? this.client.get(path).nodeify(cb) : this.client.get(path); -}; - -User.prototype.findById = function(id, cb) { - var path = this.path + '/' + id; - var user = this.client.get(path) - .then(function(userData) { - return new User(userData, this.client, this.faye); - }.bind(this)); - - return cb ? user.nodeify(cb) : user; -}; - -User.prototype.findByUsername = function(username, cb) { - var user = this.client.get(this.path, {query: {q: username}}) - .then(function(results) { - var userData = results.results[0]; - return new User(userData, this.client, this.faye); - }.bind(this)); - - return cb ? user.nodeify(cb) : user; -}; - -['rooms', 'repos', 'orgs', 'channels'].forEach(function(resource) { - User.prototype[resource] = function(query, cb) { - var resourcePath = this.path + '/' + this.id + '/' + resource; - var resources = this.client.get(resourcePath, {query: query}); - return cb ? resources.nodeify(cb) : resources; - }; -}); - -User.prototype.markAsRead = function(roomId, chatIds, cb) { - var resourcePath = this.path + '/' + this.id + '/rooms/' + roomId + '/unreadItems'; - var resource = this.client.post(resourcePath, {body: {chat: chatIds}}); - return cb ? resource.nodeify(cb) : resource; -}; - -module.exports = User; diff --git a/package.json b/package.json index c1831b8..a695eec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "node-gitter", "version": "1.2.9", "description": "Gitter client", - "main": "lib/gitter.js", + "main": "lib/client.js", "scripts": { "test": "./node_modules/.bin/mocha -R spec test" }, @@ -20,15 +20,18 @@ }, "homepage": "https://github.com/gitterHQ/node-gitter", "dependencies": { + "bluebird": "^3.1.1", + "create-error": "^0.3.1", "debug": "^0.8.1", - "eventemitter3": "^0.1.6", - "faye": "~1.0.1", - "gitter-faye": "^1.1.0-e", - "q": "~1.0.1", - "qs": "~1.2.1" + "lodash": "^4.0.0", + "request": "^2.67.0", + "request-extensible": "^0.1.1", + "require-directory": "^2.1.1", + "tentacles": "^0.2.8" }, "devDependencies": { - "mocha": "~1.18.2", - "retire": "latest" + "gulp": "^3.9.0", + "gulp-spawn-mocha": "^2.2.2", + "mocha": "^2.3.4" } } diff --git a/test/custom-request.js b/test/custom-request.js new file mode 100644 index 0000000..3e02634 --- /dev/null +++ b/test/custom-request.js @@ -0,0 +1,52 @@ +var Client = require('..'); +var assert = require('assert'); + +describe('custom-request', function() { + + var client, mockRequest, count, sentBody; + + beforeEach(function() { + count = 0; + }); + + it('should deal with json', function(done) { + mockRequest = function(options, callback) { + sentBody = { hello: 'cow' }; + count++; + + setImmediate(function() { + callback(null, { statusCode: 200, headers: { 'content-type': 'application/json' } }, JSON.stringify(sentBody) ); + }); + }; + + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN, request: mockRequest }); + + client.users.getAuthUser() + .then(function(body) { + assert.deepEqual(body, sentBody); + assert.strictEqual(count, 1); + }) + .nodeify(done); + }); + + it('should deal with objects', function(done) { + mockRequest = function(options, callback) { + sentBody = { hello: 'cow' }; + count++; + + setImmediate(function() { + callback(null, { statusCode: 200, headers: { 'content-type': 'application/json' } }, sentBody ); + }); + }; + + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN, request: mockRequest }); + + client.users.getAuthUser() + .then(function(body) { + assert.deepEqual(body, sentBody); + assert.strictEqual(count, 1); + }) + .nodeify(done); + }); + +}); diff --git a/test/extensions.js b/test/extensions.js new file mode 100644 index 0000000..c98b5e2 --- /dev/null +++ b/test/extensions.js @@ -0,0 +1,20 @@ +var Client = require('..'); +var assert = require('assert'); + +describe('extensions', function() { + + it('should allow extensions to be specified', function() { + var client = new Client({ + extensions: [function(options, callback/*, next */) { + /* Shortcut extension */ + callback(null, { statusCode: 200 }, { fakeUser: true }); + }] + }); + + return client.users.getAuthUser() + .then(function(body) { + assert.deepEqual(body, { fakeUser: true }); + }); + }); + +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..ce80316 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,3 @@ +--reporter spec +--recursive +--timeout 10000 diff --git a/test/options-passthrough.js b/test/options-passthrough.js new file mode 100644 index 0000000..aaa7326 --- /dev/null +++ b/test/options-passthrough.js @@ -0,0 +1,64 @@ +var Client = require('..'); +var assert = require('assert'); + +describe('options-passthrough', function() { + + var client, mockRequest, count, sentBody; + + beforeEach(function() { + count = 0; + }); + + it('should passthrough custom options', function() { + mockRequest = function(options, callback) { + assert(!options.accessToken); + assert.strictEqual(options.headers.Something, 'Else'); + assert.strictEqual(options.firstPageOnly, true); + + sentBody = { hello: 'cow' }; + count++; + + setImmediate(function() { + callback(null, { statusCode: 200, headers: { 'content-type': 'application/json' } }, JSON.stringify(sentBody) ); + }); + }; + + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN, request: mockRequest, headers: { Something: 'Else' } }); + + return client.users.getAuthUser({ firstPageOnly: true }) + .then(function(body) { + assert.deepEqual(body, sentBody); + assert.strictEqual(count, 1); + }); + }); + + it('should not passthrough filtered values', function() { + mockRequest = function(options, callback) { + assert(options.uri.indexOf('?q=123') > 0); + assert(!options.url); + assert.strictEqual(options.headers.Something, 'Else'); + assert.strictEqual(options.headers.X, 'Y'); + assert.strictEqual(options.firstPageOnly, '456'); + assert.strictEqual(options.gzip, true); + assert(!options.query); + assert(!options.accessToken); + + sentBody = { hello: 'cow' }; + count++; + + setImmediate(function() { + callback(null, { statusCode: 200, headers: { 'content-type': 'application/json' } }, JSON.stringify(sentBody) ); + }); + }; + + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN, request: mockRequest, headers: { Something: 'Else' } }); + + return client.users.getAuthUser({ query: { q: '123' }, firstPageOnly: '456', headers: { X: 'Y' }, gzip: false, url: 'bob' }) + .then(function(body) { + assert.deepEqual(body, sentBody); + assert.strictEqual(count, 1); + }); + }); + + +}); diff --git a/test/resources/channels-test.js b/test/resources/channels-test.js new file mode 100644 index 0000000..b957fa4 --- /dev/null +++ b/test/resources/channels-test.js @@ -0,0 +1,35 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('channels', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client + userId = _userId; + roomId = _roomId; + }); + }); + + it("it lists a user's channels", function() { + return client.channels.listForUser(userId) + .then(function(channels) { + assert(Array.isArray(channels)); + }); + }); + + it('it lists the channels in a room', function() { + return client.channels.listForRoom(roomId) + .then(function(channels) { + assert(Array.isArray(channels)); + }); + }); + +}); diff --git a/test/resources/messages-test.js b/test/resources/messages-test.js new file mode 100644 index 0000000..4f6953a --- /dev/null +++ b/test/resources/messages-test.js @@ -0,0 +1,53 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('messages', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client; + userId = _userId; + roomId = _roomId; + }); + }); + + it("should list the messages in a room", function() { + return client.messages.listForRoom(roomId) + .then(function(messages) { + assert(Array.isArray(messages)); + }); + }); + + it('should allow a user to create a message', function() { + var text = "testing create message"; + return client.messages.create(roomId, {text: text}) + .then(function(message) { + assert.equal(message.text, text); + }); + }); + + it('should allow a user to update a message', function() { + var textCreate = "testing update message (before)"; + var textUpdate = "testing update message"; + return client.messages.create(roomId, {text: textCreate}) + .then(function(message) { + assert.equal(message.text, textCreate); + return client.messages.update(roomId, message.id, {text: textUpdate}); + }) + .then(function(message) { + assert.equal(message.text, textUpdate); + }); + }); + + it('should stream the events in a room', function() { + // TODO + }); + +}); diff --git a/test/resources/orgs-test.js b/test/resources/orgs-test.js new file mode 100644 index 0000000..40c886c --- /dev/null +++ b/test/resources/orgs-test.js @@ -0,0 +1,28 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('orgs', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client; + userId = _userId; + roomId = _roomId; + }); + }); + + it("should list a user's orgs", function() { + return client.orgs.listForUser(userId) + .then(function(orgs) { + assert(Array.isArray(orgs)); + }); + }); + +}); diff --git a/test/resources/repos-test.js b/test/resources/repos-test.js new file mode 100644 index 0000000..53a3c7e --- /dev/null +++ b/test/resources/repos-test.js @@ -0,0 +1,27 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('repos', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client; + userId = _userId; + roomId = _roomId; + }); + }); + + it("should list a user's repos", function() { + return client.repos.listForUser(userId) + .then(function(repos) { + assert(Array.isArray(repos)); + }); + }); +}); diff --git a/test/resources/rooms-test.js b/test/resources/rooms-test.js new file mode 100644 index 0000000..58f68de --- /dev/null +++ b/test/resources/rooms-test.js @@ -0,0 +1,42 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('rooms', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client; + userId = _userId; + roomId = _roomId; + }); + }); + + it("should list the current user's rooms", function() { + return client.rooms.listForAuthUser() + .then(function(rooms) { + assert(rooms[0].name); + }) + }); + + it('should allow a user to join a room', function() { + var uri = 'gitterHQ/gitter'; + return client.rooms.join({uri: uri}) + .then(function(room) { + assert.equal(room.uri, uri); + }); + }); + + it("should list a user's rooms", function() { + return client.rooms.listForUser(userId) + .then(function(rooms) { + assert(Array.isArray(rooms)); + }); + }); +}); diff --git a/test/resources/unreads-test.js b/test/resources/unreads-test.js new file mode 100644 index 0000000..ac2a948 --- /dev/null +++ b/test/resources/unreads-test.js @@ -0,0 +1,35 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('unreads', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client; + userId = _userId; + roomId = _roomId; + }); + }); + + it("should list a user's unreads for a room", function() { + return client.unreads.listForUserAndRoom(userId, roomId) + .then(function(result) { + assert(Array.isArray(result.chat)); + assert(Array.isArray(result.mention)); + }); + }); + + it("should mark a user's unreads for a room", function() { + return client.unreads.markForUserAndRoom(userId, roomId, {chat: ['foo']}) + .then(function(result) { + assert.equal(result.success, true); + }); + }); +}); diff --git a/test/resources/users-test.js b/test/resources/users-test.js new file mode 100644 index 0000000..756d136 --- /dev/null +++ b/test/resources/users-test.js @@ -0,0 +1,34 @@ +'use strict'; + +var assert = require('assert'); +var testUtils = require('../utils'); + +describe('users', function() { + var client; + var userId; + var roomId; + + before(function() { + assert(process.env.GITTER_ACCESS_TOKEN, 'Please set GITTER_ACCESS_TOKEN'); + return testUtils.setup() + .spread(function(_client, _userId, _roomId) { + client = _client; + userId = _userId; + roomId = _roomId; + }); + }); + + it('should get the authenticated user', function() { + return client.users.getAuthUser() + .then(function(user) { + assert(user.username); + }) + }); + + it('should list the users in a room', function() { + return client.users.listForRoom(roomId) + .then(function(users) { + assert(Array.isArray(users)); + }) + }); +}); diff --git a/test/rooms-test.js b/test/rooms-test.js deleted file mode 100644 index bf33aab..0000000 --- a/test/rooms-test.js +++ /dev/null @@ -1,196 +0,0 @@ -/* jshint node:true, unused:true */ - -var assert = require('assert'); -var Q = require('q'); -var Gitter = require('../lib/gitter.js'); - -var token = process.env.TOKEN; - -if (!token) { - console.log('========================================'); - console.log('You need to provide a valid OAuth token:'); - console.log('$ TOKEN= USERNAME= npm test'); - console.log('========================================\n'); - process.exit(1); -} - -var yacht_room = '534bfb095e986b0712f0338e'; - -describe('Gitter Rooms', function() { - this.timeout(20000); - var gitter; - - before(function() { - var opts = {}; - - //var opts = { - // client: { - // host: "localhost", - // port: 5000, - // prefix: true, - // streamingEndpoint: 'http://localhost:5000/faye' - // } - //}; - - gitter = new Gitter(process.env.TOKEN, opts); - }); - - it('should find a room with cb', function(done) { - gitter.rooms.find(yacht_room, function(err, room) { - if (err) done(err); - assert.equal(room.name, 'node-gitter/yacht'); - done(); - }); - }); - - it('should find a room', function(done) { - gitter.rooms.find(yacht_room).then(function(room) { - assert.equal(room.name, 'node-gitter/yacht'); - }).nodeify(done); - }); - - it('should be able to join a room', function(done) { - gitter.rooms.join('node-gitter/yacht').then(function(room) { - assert.equal(room.name, 'node-gitter/yacht'); - }).nodeify(done); - }); - - it('should be able to leave a room', function(done) { - // Join the room first - gitter.rooms.join('node-gitter/yacht').then(function(room) { - return gitter.currentUser() - .then(function(currentUser) { - return room.removeUser(currentUser.id); - }); - }).then(function() { - return gitter.currentUser(); - }).then(function(user) { - return user.rooms(); - }).then(function(rooms) { - var check = rooms.some(function(room) { return room.name === 'node-gitter/yacht'; }); - assert.equal(false, check); - }).fin(function() { - // Join the room again for the rest of the tests - gitter.rooms.join('node-gitter/yacht'); - }).nodeify(done); - }); - - it('should not be able to join an invalid room', function(done) { - gitter.rooms.join('some-invalid-room').then(function() { - }).fail(function(err) { - assert(err); - done(); - }).fail(done); - }); - - it('should be able to send a message', function(done) { - gitter.rooms.find(yacht_room).then(function(room) { - return room.send('Time is ' + new Date()); - }).then(function(message) { - assert(message); - }).nodeify(done); - }); - - it('should be able to send a status message', function(done) { - gitter.rooms.find(yacht_room).then(function(room) { - return room.sendStatus('Yo! checking time is ' + new Date()); - }).then(function(message) { - assert(message.status); - }).nodeify(done); - }); - - it('should fetch messages from a room', function(done) { - gitter.rooms.find(yacht_room).then(function(room) { - return room.chatMessages({limit: 5}); - }).then(function(messages) { - assert(messages.length === 5); - }).nodeify(done); - }); - - it('should fetch users in a room', function(done) { - gitter.rooms.find(yacht_room).then(function(room) { - return room.users(); - }).then(function(users) { - assert(users.some(function(user) { return user.username === 'node-gitter'; })); - }).nodeify(done); - }); - - it('should fetch channels in a room', function(done) { - gitter.rooms.find(yacht_room).then(function(room) { - return room.channels(); - }).then(function(channels) { - assert(channels.some(function(channel) { return channel.name === 'node-gitter/yacht/pub'; })); - }).nodeify(done); - }); - - it('should be able to listen on a room', function(done) { - var msg = '[streaming] ' + new Date(); - - gitter.rooms.find(yacht_room).then(function(room) { - var events = room.listen(); - - events.on('message', function(message) { - if (message.text === msg) { - done(); - } - }); - - setTimeout(function() { room.send(msg); }, 500); - }).fail(done); - }); - - it('should be able to subscribe to a room', function(done) { - var msg = '[faye] ' + new Date(); - - gitter.rooms.find(yacht_room).then(function(room) { - - // Events snapshot - //var eventz = room.streaming().events(); - //eventz.on('snapshot', function(snapshot) { - // assert(snapshot.length !== 0); - //}); - - var events = room.streaming().chatMessages(); - - events.on('snapshot', function(snapshot) { - assert(snapshot.length !== 0); - }); - - events.on('chatMessages', function(message) { - if (message.model.text === msg) { - room.streaming().disconnect(); - done(); - } - }); - - setTimeout(function() { room.send(msg); }, 750); - }).fail(done); - }); - - - it('should post to multiple rooms', function(done) { - function postMessageInRoom(roomUri, message) { - return gitter.rooms.join(roomUri) - .then(function(room) { - room.send(message); - return room; - }) - .delay(1000) - .then(function(room) { - return room.chatMessages({ limit: 2 }); - }) - .then(function(messages) { - assert(messages.some(function(msg) { - return msg.text === message; - }), "Expecting to see posted message"); - }); - } - - Q.all([ - postMessageInRoom('node-gitter/yacht', 'yacht repo ping at ' + new Date()), - postMessageInRoom('node-gitter/yacht/pub', 'yacht pub channel ping at ' + new Date()), - ]) - .nodeify(done); - }); - -}); diff --git a/test/spread.js b/test/spread.js new file mode 100644 index 0000000..7c691d9 --- /dev/null +++ b/test/spread.js @@ -0,0 +1,58 @@ +var Client = require('..'); +var assert = require('assert'); + +describe('spread', function() { + + describe('default on', function() { + var client; + + before(function() { + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN, spread: true }); + }); + + it('listForAuthUser', function() { + return client.users.getAuthUser() + .spread(function(user, response) { + assert(user); + assert(response); + assert.strictEqual(response.statusCode, 200); + }); + }); + + it('listForAuthUser', function() { + return client.users.getAuthUser({ spread: false }) + .then(function(user) { + assert(user); + assert(user.id); + }); + }); + + }); + + describe('default off', function() { + var client; + + before(function() { + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN }); + }); + + it('listForAuthUser', function() { + return client.users.getAuthUser() + .then(function(user) { + assert(user); + assert(user.id); + }); + }); + + it('listForAuthUser', function() { + return client.users.getAuthUser({ spread: true }) + .spread(function(user, response) { + assert(user); + assert(response); + assert.strictEqual(response.statusCode, 200); + }); + }); + + }); + +}); diff --git a/test/users-test.js b/test/users-test.js deleted file mode 100644 index 9b62182..0000000 --- a/test/users-test.js +++ /dev/null @@ -1,88 +0,0 @@ -/* jshint node:true, unused:true */ - -var assert = require('assert'); -var Gitter = require('../lib/gitter.js'); - -var token = process.env.TOKEN; -var username = process.env.USERNAME || 'node-gitter'; - -if (!token) { - console.log('========================================'); - console.log('You need to provide a valid OAuth token:'); - console.log('$ TOKEN= USERNAME= npm test'); - console.log('========================================\n'); - process.exit(1); -} - -describe('Gitter Users', function() { - this.timeout(5000); - - var gitter; - - before(function() { - gitter = new Gitter(process.env.TOKEN); - }); - - it('should fetch the current user cb', function(done) { - gitter.currentUser(function(err, user) { - if (err) done(err); - assert.equal(user.username, username); - done(); - }); - }); - - it('should fetch the current user', function(done) { - gitter.currentUser().then(function(user) { - assert.equal(user.username, username); - }).nodeify(done); - }); - - it('should fetch the user rooms', function(done) { - gitter.currentUser().then(function(user) { - return user.rooms(); - }).then(function(rooms) { - assert(rooms.length !== 0); - }).nodeify(done); - }); - - it('should fetch the user repos', function(done) { - gitter.currentUser().then(function(user) { - return user.repos(); - }).then(function(repos) { - assert(repos.length !== 0); - }).nodeify(done); - }); - - it('should fetch the user orgs', function(done) { - gitter.currentUser().then(function(user) { - return user.orgs(); - }).then(function(orgs) { - assert(orgs.length !== 0); - }).nodeify(done); - }); - - it('should fetch the user channels', function(done) { - gitter.currentUser().then(function(user) { - return user.channels(); - }).then(function(channels) { - assert(channels.length !== 0); - }).nodeify(done); - }); - - it('should fail when fidning an invalid user', function(done) { - gitter.users.find('invalid').then(function() { - assert(false); - }).fail(function() { - done(); - }); - }); - - it('should fail when fidning an invalid user with cb', function(done) { - gitter.users.find('invalid', function(err, user) { - assert.equal(user, null); - assert(err); - done(); - }); - }); - -}); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..a98d3bb --- /dev/null +++ b/test/utils.js @@ -0,0 +1,17 @@ +var Client = require('..'); + +function setup() { + client = new Client({ accessToken: process.env.GITTER_ACCESS_TOKEN }); + return client.users.getAuthUser() + .then(function(user) { + var userId = user.id; + var uri = user.username + '/test'; + return client.rooms.join({uri: uri}) + .then(function(room) { + var roomId = room.id; + return [client, userId, roomId]; + }); + }); +} + +exports.setup = setup;