Skip to content

Instantly share code, notes, and snippets.

@zspecza
Last active August 20, 2019 11:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zspecza/0456bb0a954547ba5c4e to your computer and use it in GitHub Desktop.
Save zspecza/0456bb0a954547ba5c4e to your computer and use it in GitHub Desktop.
A simplified API for working with Slack
import fetch, {
Response } from 'node-fetch';
import qs from 'querystring';
import WebSocket from 'ws';
class SimpleSlack {
baseURL = 'https://slack.com/api/';
eventListeners = {};
connection = null;
socketMsgId = 0;
constructor(opts = {}) {
if (typeof opts === 'string') {
this.defaults = { token: opts };
} else {
this.defaults = opts;
}
}
send(opts = {}) {
let id = opts.id || this.socketMsgId++;
this.ws.send(JSON.stringify({ ...opts, id }));
return id;
}
async on(event, callback, opts = {}) {
let cbID = callback.toString();
this.eventListeners[cbID] = async data => callback(await this.parseJSON(data));
if (this.ws == null || this.connection == null) {
await this.connect(opts);
this.on(event, callback, opts);
} else {
this.ws.on(event, this.eventListeners[cbID]);
return this.connection;
}
}
off(event, callback) {
if (this.ws == null) return;
let cbID = callback.toString();
this.ws.removeListener(event, this.eventListeners[cbID]);
delete this.eventListeners[cbID];
}
async parseJSON(str) {
return (
await new Response(str).json()
);
}
async connect(opts = {}) {
let connection = await this.method('rtm.start', opts);
if (connection.ok) {
this.ws = new WebSocket(connection.url);
this.connection = connection;
}
return connection;
}
async method(method, opts = {}) {
opts = { ...this.defaults, ...opts };
for (let option of Object.keys(opts)) {
if (typeof opts[option] === 'array') {
// synchronous for now
opts[option] = JSON.stringify(opts[option]);
}
}
return (
await this.api(`${this.baseURL + method}?${qs.stringify(opts)}`)
);
}
async submit(opts = {}) {
opts = { ...this.defaults, ...opts };
let webhook = opts.webhook;
delete opts.webhook;
return (
await this.api(webhook, {
method: 'post',
body: JSON.stringify(opts)
})
);
}
async api(endpoint, opts) {
let response = await fetch(endpoint, opts || null);
return (
await response.json()
);
}
}

How to Use

note: this is ES7 code - you'll have to transpile it through Babel. it is also untested, undocumented and un-Avengers-proofed, so use this for world domination at your own discretion.

Create an API by passing an API token:

var slack = new SimpleSlack('gygg8yhgh-fkjghgh8ur-ghfhg');

Alternatively, you can specify a default set of options that are 1:1 mappings for Slack's web API. These will be sent in every method call.

var slack = new SimpleSlack({
  token: 'gygg8yhgh-fkjghgh8ur-ghfhg',
  username: 'WowBot',
  icon_url: 'http://placebeard.it/50'
});

You may then call slack.method(name, data) to work with the Slack Web API.

slack.method('chat.postMessage', {
  text: 'Hello, world!'
});

You can post to an Incoming Webhook by giving SimpleSlack the URL and calling .submit():

var slack = new SimpleSlack({
  webhook: 'http://hooks.slack.com...'
});

slack
  .submit({
    text: 'Hello!'
  })
  .then(function(response) {
  
  });

Subscribing to Slack's RTM API can be done using .method(), but requires a little boilerplate.

To circumvent the need to write boilerplate, you may use the on method, which accepts an event name and a callback which is executed when the event triggers. The callback contains the associated event data as the first parameter.

Under the hood, this method will check if it has a reference to a websocket connection, if not, it will attempt to asynchronously establish a connection by requesting rtm.start from Slack's API - once the connection is established, it will recursively call itself again and use the underlying websocket event system to trigger the callbacks. It does so by calling a shorthand method slack.connect(opts). If you pass an object as the last parameter to slack.on(), it will be passed to slack.connect() and then to rtm.start.

slack.on('message', function(message) {
  if (message.type === 'message') { // chat message
    slack.method('chat.postMessage', { text: 'Hello!' });
  }
});

If you just want to connect to the RTM API without specifying a message, but retain access to the RTM API, use .connect(). Once called (and resolved!), you may access the underlying websockets on slack.ws or simply use slack.on(), slack.off() or slack.send().

You may send RTM API events (perfect for letting users know your bot is typing if it's doing something that takes a while) using .send() - just pass it the expected payload as a JavaScript object. SimpleSlack tries to abstract the need to maintain an ID for RTM sent events for simple use-cases, but you may obtain a reference to this ID by storing the return value of the send call should you need to compare it with a response. You may also override this ID (the overridden ID will be returned)

var slack = new SimpleSlack('fgdfg545-fgfdgghhg5-fdhty6u');

slack.on('message', function(message) {
  if (message.type === 'message') {
    slack.send({ type: 'typing', channel: message.channel });
    slack.send({ type: 'message', channel: message.channel, text: 'Hello!' });
  }
});

Finally, every single method except .off() and .send() on SimpleSlack instances is asynchronous and will return a promise that resolves to the parsed HTTP response for the associated Slack endpoint. The .on() method will return a promise for the response from rtm.start. There is some exception - at this time, any .method() call will synchronously JSON.stringify any array if present, before sending the data to Slack. Calling .method() will still return a promise for the HTTP response, as it is an asynchronous method. In addition, any .submit() calls will synchronously JSON.stringify the entire payload - but the method is still asynchronous, and returns a promise for the parsed response.

Bot User tip:

If you're making a bot installed via a Bot User integration, since the user is responsible for setting the bot's name at configure-time, you might want to grab some info about the bot's identity first to determine who you are. This can be done in two ways:

Using the Web API

var slack = new SimpleSlack('gjfijtkk-409590686-dghfgjfhg');

slack
  .method('auth.test')
  .then(function(response) {
    return response.ok && slack.method('chat.postMessage', {
      username: response.user,
      text: 'Hello!'
    });
  })
  .then(function(response) {
    if (response.ok) return; // message sent
  });

Using the RTM API

var slack = new SimpleSlack('gjfijtkk-409590686-dghfgjfhg');

slack
  .connect()
  .then(function(response) {
    return response.ok && slack.method('chat.postMessage', {
      username: response.self.username,
      text: 'Hello!'
    });
  })
  .then(function(response) {
    if (response.ok) return; // message sent
  });

the api of this could be improved by building on observable streams (functional reactive programming) and could look something like this:

var slack = new Slack('<token>');

slack.messages
  .filter(slack.messageType('message'))
  .filter(slack.inChannel('#general'))
  .filter(slack.messageIs('@{self} hello'))
  .onValue(function(message) {
    return slack.send('hello @{user}');
  });
  
slack.request('auth.test')
  .filter(slack.isOk)
  .onValue(function(response) {
    console.log('auth info correct');
  });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment