groupme
Version:
A Node Module for interacting with the GroupMe API. Supports both querying the Stateless API and receiving Websocket-based push messages, and provides a CLI.
445 lines (381 loc) • 17.8 kB
JavaScript
var events = require('events');
var util = require('util');
var querystring = require('querystring');
var request = require('request'); //Easy http requests
var Q = require('q');
var Constants = require('./Constants');
var Promisify = require('./Promisify');
/************************************************************************
* Helper functions for requests
***********************************************************************/
// Helper function to construct the individual paths for API calls.
// api_url takes the format "/url"
function getURL(at, api_url, opts) {
var params = "?token=" + at;
if (opts)
params = params + "&" + querystring.stringify(opts);
return Constants.API_BASEURL + api_url + params;
}
// Helper function to construct the the source_guid string.
function generateGUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (a) {
var b, c;
return b = Math.random() * 16 | 0, c = a === "x" ? b : b & 3 | 8, c.toString(16);
});
};
// This makes one of many different types of post requests.
// config.opts - this adds additional parameters to the url
// config.doParse - if true, parses the return value as json
// config.responseCode - if set, checks for this response code
function getRequest(at, api_url, config, callback) {
if (!config.responseCode)
config.responseCode = 200;
request(
{
uri: getURL(at, api_url, config.opts),
method: 'GET',
headers: {'Content-Type': 'application/json'}
},
function(err,res,body) {
if (!err && res.statusCode == config.responseCode) {
if (config.doParse) {
callback(null,JSON.parse(body).response);
} else {
callback(null,res.statusCode);
}
} else {
callback(res)
}
});
}
// This makes one of many different types of post requests.
// config.body - this stingifies the object as the POST body
// config.form - this sets the POST body as form parameters
// config.doParse - if true, parses the return value as json
// config.responseCode - if set, checks for this response code
function postRequest(at, api_url, config, callback) {
var r_opts = {
uri: getURL(at, api_url),
method: 'POST',
headers: {'Content-Type': 'application/json'}
};
if (config.body && !config.form) {
r_opts.body = JSON.stringify(config.body);
} else {
r_opts.form = config.form;
}
if (!config.responseCode)
config.responseCode = 200;
request(
r_opts,
function(err,res,body) {
if (!err && res.statusCode == config.responseCode) {
if (config.doParse) {
callback(null,JSON.parse(body).response);
} else {
callback(null,res.statusCode);
}
} else {
callback(res)
}
});
}
/************************************************************************
* API Containers
***********************************************************************/
// Objects representing the different API calls,
// each holding all the functions for that part of the API.
// All the functions take the form function(options,callback);
// and all callbacks take the form function(err,returnval);
api = {};
api.Groups = {};
api.Members = {};
api.Messages = {};
api.Chats = {};
api.DirectMessages = {};
api.Likes = {};
api.Bots = {};
api.Users = {};
api.Polls = {};
/************************************************************************
* Groups
***********************************************************************/
// List the authenticated user's active groups.
// Callback gets a JSON object of groups.
api.Groups.index = function(at, callback) {
getRequest(at, "/groups", {doParse:true}, callback);
}
// List they groups you have left but can rejoin.
// Callback gets a JSON object of groups.
api.Groups.former = function(at, callback) {
getRequest(at, "/groups/former", {doParse:true}, callback);
}
// Load a specific group.
// id [required] - group_id string
// Callback gets a JSON object describing the group.
api.Groups.show = function(at, id, callback) {
getRequest(at, "/groups/" + id, {doParse:true}, callback);
}
// Create a new group
// opts.name [required] - string, name of new group
// opts.description - string
// opts.image_url - string, must point to GroupMe Image Service URL, eg "http://i.groupme.com/123456789"
// opts.share - boolean, if true, generates a share url for anyone to join
// Callback gets a JSON object describing the group.
api.Groups.create = function(at, opts, callback) {
if (!opts.name) {
callback({error:"Requires opts.name"});
} else {
postRequest(at, "/groups", {body:opts, responseCode:201, doParse:true}, callback);
}
}
// Update a group after creation
// id [required] - group_id string
// opts.name - string, name of new group
// opts.description - string
// opts.image_url - string, must point to GroupMe Image Service URL, eg "http://i.groupme.com/123456789"
// opts.share - boolean, if true, generates a share url for anyone to join
// Callback gets a JSON object describing the group.
api.Groups.update = function(at, id, opts, callback) {
postRequest(at, "/groups/" + id + "/update", {body:opts, responseCode:200, doParse:true}, callback);
}
// Disband a group. This action is only available to the group creator.
// id [required] - group_id string
// Callback gets a statusCode of 200
api.Groups.destroy = function(at, id, callback) {
postRequest(at, "/groups/" + id + "/destroy", {}, callback);
}
/************************************************************************
* Members
***********************************************************************/
// Add members to a group.
// Multiple members can be added in a single request,
// and results are fetched with a separate call (since memberships are processed asynchronously).
// The response includes a results_id that's used in the results request.
// In order to correlate request params with resulting memberships, GUIDs can be added to the members
// parameters. These GUIDs will be reflected in the membership JSON objects.
// id [required] - group_id string
// opts.members - array of objects containing "nickname"
// and one of "user_id", "phone_number" or "email" as identifiers
// can also include "guid" which is returned in the results call
api.Members.add = function(at, id, opts, callback) {
if (!opts.members || opts.members.length < 1) {
callback({error:"Requires an array of new members"})
} else {
postRequest(at, "/groups/" + id + "/members/add", {body:opts, responseCode:202, doParse:true}, callback);
}
}
// Get the membership results from an add call.
// Successfully created memberships will be returned, including any GUIDs that were sent up in the add request.
// If GUIDs were absent, they are filled in automatically. Failed memberships and invites are omitted.
// Keep in mind that results are temporary -- they will only be available for 1 hour after the add request.
// id [required] - group_id string
// results_id [required] - guid for results returned by the add call
//
api.Members.results = function(at, id, results_id, callback) {
getRequest(at, "/groups/" + id + "/members/results/" + results_id, {opts:opts, doParse:true}, callback);
}
// Remove members from a group
// Only one member can be removed at a time.
// The leader of the group cannot be removed.
// id [required] - group_id string
// member_id [required] - The membership id of the member to remove
// This is not the user ID. In the members key in the group JSON, this is the id value, not the user_id
api.Members.remove = function(at, id, member_id, callback) {
postRequest(at, "/groups/" + id + "/members/" + member_id + "/remove", {responseCode:200}, callback);
}
/************************************************************************
* Messages
***********************************************************************/
// Messages for a group
// Messages are returned in groups of 20, ordered by created_at descending
// id [required] - group_id string
// opts.before_id - returns 20 messages created before the given id
// opts.after_id - returns 20 messages created after the given id
api.Messages.index = function(at, id, opts, callback) {
getRequest(at, "/groups/" + id + "/messages", {opts:opts, doParse:true}, callback);
}
// Send a message to a group
// If you want to attach an image, you must first process it through our image service.
// Attachments of type emoji rely on data from emoji PowerUps.
// Clients use a placeholder character in the message text and specify a replacement charmap to substitute emoji characters
// The character map is an array of arrays containing rune data ([[{pack_id,offset}],...]).
// The placeholder should be a high-point/invisible UTF-8 character.
// id [required] - group_id string
// opts.message - object representing message, with the following keys:
// text [required] - string. can be omitted if at least one attachment is present
// attachments - A polymorphic list of attachments, each an object with a type field:
// { type: "image", url: <from image service> }
// { type: "location", name: <string>, lat: <string>, lng: <string> }
// { type: "split", token: <string> }
// { type: "emoji", placeholder: <string>, charmap: array }
api.Messages.create = function(at, id, opts, callback) {
if (!opts || !opts.message) {
callback({error:"Requires options for new message"})
} else {
opts.message.source_guid = generateGUID();
postRequest(at, "/groups/" + id + "/messages", {body:opts, responseCode:201, doParse:true}, callback);
}
}
/************************************************************************
* Chats
***********************************************************************/
// Index of DM chats a user has
// Returns a paginated list of direct message chats, or conversations, sorted by updated_at descending.
// opts.page - Page number integer
// opts.per_page - Entrys per page integer
api.Chats.index = function(at, opts, callback) {
getRequest(at, "/chats", {opts:opts, doParse:true}, callback);
}
/************************************************************************
* Direct Messages
***********************************************************************/
// Direct messages between two users
// Direct messages are returned in groups of 20, ordered by created_at descending
// opts.other_user_id [required] - user_id string
// opts.before_id - returns 20 messages created before the given id
// opts.after_id - returns 20 messages created after the given id
api.DirectMessages.index = function(at, opts, callback) {
if (!opts || !opts.other_user_id) {
callback({error:"Requires options for getting direct messages"})
} else {
getRequest(at, "/direct_messages", {opts:opts, doParse:true}, callback);
}
}
// Send a direct message to another user
// If you want to attach an image, you must first process it through our image service.
// Attachments of type emoji rely on data from emoji PowerUps.
// Clients use a placeholder character in the message text and specify a replacement charmap to substitute emoji characters
// The character map is an array of arrays containing rune data ([[{pack_id,offset}],...]).
// The placeholder should be a high-point/invisible UTF-8 character.
// opts.direct_message - object representing message, with the following keys:
// recipient_id [required]- group_id string
// text [required] - string. can be omitted if at least one attachment is present
// attachments - A polymorphic list of attachments, each an object with a type field:
// { type: "image", url: <from image service> }
// { type: "location", name: <string>, lat: <string>, lng: <string> }
// { type: "split", token: <string> }
// { type: "emoji", placeholder: <string>, charmap: array }
api.DirectMessages.create = function(at, opts, callback) {
if (!opts || !opts.direct_message) {
callback({error:"Requires options for new direct message"})
} else {
opts.direct_message.source_guid = generateGUID();
postRequest(at, "/direct_messages", {body:opts, responseCode:201, doParse:true}, callback);
}
}
/************************************************************************
* Likes
***********************************************************************/
// Like a message.
// Callback gets a statusCode of 200
api.Likes.create = function(at, group_id, message_id, callback) {
postRequest(at, "/messages/" + group_id + "/" + message_id + "/like", {}, callback);
}
// Unlike a liked message.
// Callback gets a statusCode of 200
api.Likes.destroy = function(at, group_id, message_id, callback) {
postRequest(at, "/messages/" + group_id + "/" + message_id + "/unlike", {}, callback);
}
/************************************************************************
* Bots
***********************************************************************/
// Bots are purely a way to send messages to a group
// under a different name than your own.
// Receiving messages can be done through this as well, but it recommended
// through the IncomingStream push service
// Create a bot.
// name [required] - string, the nickname of the bot
// group_id [required] - string, the group the bot can post to
// opts.avatar_url - string, ImageService url to avatar
// opts.callback_url - string, a callback for new messages
api.Bots.create = function(at, name, group_id, opts, callback) {
var payload = {
"bot[name]": name,
"bot[group_id]": group_id,
}
if (opts && opts.avatar_url)
payload["bot[avatar_url]"] = opts.avatar_url;
if (opts && opts.callback_url)
payload["bot[callback_url]"] = opts.callback_url;
postRequest(at, "/bots", {form:payload, responseCode:201, doParse:true}, callback);
}
// Post a message as a bot
// bot_id [required] - string, the id of the bot
// text [required] - string, the message to post
// opts.picture_url - string, ImageService url to optional image to post
api.Bots.post = function(at, bot_id, text, opts, callback) {
var payload = {
bot_id: bot_id,
text: text
}
if (opts.picture_url)
payload.picture_url = opts.picture_url
postRequest(at, "/bots/post", {form:payload, responseCode:202}, callback);
}
// List bots you have created
api.Bots.index = function(at, callback) {
getRequest(at, "/bots", {doParse:true}, callback);
}
// Remove a bot that you have created
// bot_id [required] - string, the id of the bot
api.Bots.destroy = function(at, bot_id, callback) {
postRequest(at, "/bots/destroy", {form:{"bot_id":bot_id}, responseCode:200}, callback);
}
/************************************************************************
* Users
***********************************************************************/
// Get details about the authenticated user
// Callback gets a JSON object about the user
api.Users.me = function(at, callback) {
getRequest(at, "/users/me", {doParse:true}, callback);
}
/************************************************************************
* Polls
***********************************************************************/
// List the latest 10 polls.
// If more polls exist, "continuation_token" will be returned
// Calling this function with continuation_token passed in will get the next 10 polls.
// group_id [required] - string, the id of the group
// continuation_token - string, continuation_token as passed from the previous Polls.index call
api.Polls.index = function(at, group_id, continuation_token, callback) {
let config = {
doParse: true
};
if(continuation_token != null) {
config.opts = {continuation_token:continuation_token};
}
getRequest(at, "/poll/" + group_id, config, callback);
}
// Create a poll.
// group_id [required] - string, the id of the group
// subject [required] - string, the subject/title of the poll
// options [required] - string array, the poll options (length must be between 2 and 10)
// expiration - number, the expiration date of the poll as an epoch timestamp (number of seconds since 1 Jan 1970). Defaults to 24 hours from creation
api.Polls.create = function(at, group_id, opts, callback) {
const { subject, options, expiration } = opts;
if(!subject || !options) {
callback({error: "Must include a subject and options parameters"});
} else if(options.length < 2 || options.length > 10) {
callback({error: "Options array must be between 2 and 10 options"});
} else {
const defaultExpirationDate = Math.floor(new Date() / 1000) + 24*60*60; //epoch time in seconds + num seconds in 24 hours
let poll = {
subject: subject,
options: [],
expiration: expiration || defaultExpirationDate
};
options.forEach(option => {
poll.options.push({title: option});
});
postRequest(at, "/poll/" + group_id, {body: poll, responseCode:200}, callback);
}
}
/************************************************************************
* Adding Promises for all the Stateless functions
***********************************************************************/
Promisify.promisify(api);
/************************************************************************
* Export the entire api;
***********************************************************************/
module.exports = api;