README
SMSframework
SMS framework with pluggable providers.
Key features:
- Send messages
- Receive messages
- Delivery confirmations
- Handle multiple providers with a single gateway
- Event-driven interface
- Reliable message handling
- Leverages q promises
- Unit-tested
Table of Contents
- Supported Providers
- Gateway
- Data Objects
- Provider HTTP Receivers
- Message Routing
- Bundled Providers
Supported Providers
SMSframework supports the following
- loopback: loopback provider for testing. Bundled.
- Clickatell
- Expecting more!
Also see the full list of providers.
Gateway
SMSframework handles the whole messaging thing with a single Gateway object.
Let's start with initializing a gateway:
var smsframework = require('smsframework')
;
var gateway = new smsframework.Gateway();
The Gateway()
constructor currently has no arguments.
Providers
A Provider is a package which implements the logic for a specific SMS provider.
Each provider reside in an individual package smsframework-*
.
You'll probably want to install some of these first.
Gateway.addProvider(provider, alias, config)
Arguments:
provider: String
is the name of the provider package.alias: String
is the provider alias: an arbitrary string that uniquely identifies the provider instance. You'll use this string in order to send messages via a specific provider.config: Object
is the Provider-dependent configuration object. Refer to the provider documentation for the details.
When a package is require()
d, it registers itself under the smsframework.providers
namespace.
You don't need to require providers manually, as SMSframework does this for you.
gateway.addProvider('provider1', 'alias1', {}); // package 'smsframework-provider1'
gateway.addProvider('provider2', 'alias2', {}); // package 'smsframework-provider2'
The first provider becomes the default one, unless you use Message Routing.
Gateway.addProvider(providers)
An alternative syntax to add providers in bulk.
Arguments:
providers: Array.<{ provider: String, alias: String, config: Object? }>
is an array that specifies multiple providers:[ { provider: 'provider1', alias: 'primary', config: {} }, { provider: 'provider2', alias: 'secondary', config: {} } ]
This works perfectly when your providers are defined in the configuration. Consider using js-yaml:
providers:
- { provider: 'provider1', alias: 'primary', config: { apitoken: '123456' } }
- { provider: 'provider2', alias: 'secondary', config: { apitoken: '987654' } }
Then, in the application:
var config = yaml.load(fs.readFileSync('./config.yml', { encoding: 'utf8' }));
gateway.addProvider(config.providers);
Gateway.getProvider(alias):IProvider
Get a provider instance by its alias.
You don't normally need this, unless the provider has some public API: see provider documentation.
Sending Messages
To send a message, you first create it with Gateway.message(to, body)
which returns a fluid interface object.
Arguments:
to: String
: Recipient numberbody: String
: Message body
Properties:
message: OutgoingMessage
: The wrapped Outgoing Message object
Methods:
send():Q
: Send the message.The method returns a promise for OutgoingMessage which is resolved when a message is sent, or rejected on sending error. See Handling Send Errors.
from(from: String)
: Set the message originating number.Is used to pick a specific source number if the gateway supports that.
provider(provider: String)
: Choose the provider by alias.If no provider is specified - the first one is used to deliver the message.
route(..values)
: Specify routing values. See Message Routing.options(options: OutgoingMessageOptions)
: Specify sending options:allow_reply: Boolean
: Allow replies for this message. Default:false
status_report: Boolean
: Request a delivery report. Default:false
. See: statusexpires: Number?
: Message validity period in minutes. Default: none.senderId: String?
: SenderID to replace the source number. Default: none.NOTE: This advanced feature is not supported by all providers! Moreover, some of them can have special restrictions.
escalate: Boolean
: Is a high-priority message: these are delivered faster, at a higher price. Default:false
.
params(params: Object)
: Specify provider-dependent sending parameters: refer to the provider documentation for the details.
All the above methods are optional, you can just send the message as is:
gateway.message('+123456', 'hi there').send().done(); // using the default provider
Here's the full example:
var smsframework = require('smsframework')
;
var gateway = new smsframework.Gateway();
gateway.addProvider('clickatell', 'primary', {});
gateway.message('+123456', 'hi there')
.from('+1111')
.provider('primary') // use the named provider
.options({
allow_reply: true,
status_report: false,
expires: 60,
senderId: 'smsframework'
})
.params({
// some provider-dependent parameters here
})
// Handle success
.then(function(message){
console.log('Message sent successfully!');
})
.catch(function(err){
console.error('Failed to send the message', err.stack);
});
If you dislike promises, you can always get back to the old good NodeJS-style callbacks:
gateway.message('+123456', 'hi there').send()
.nodeify(function(err, message){
// NodeJS callback
});
Handling Send Errors
When you send()
a message, the promise may resolve to an error.
The error object is provided as an argument to the callback function, and can be one of the following:
Error
: unknown provider specified. You may have a typo, or the provider package is missing.Error
: runtime error occurred somewhere in the code. Rare.smsframework.errors.SendMessageError
: An advanced error object. Has thecode
field which defines the error conditions.
See smsframework.errors.SendMessageError for the list of supported error codes.
Example:
gateway.message('+123456', 'hi there').send()
.catch(function(err){
if (err.code === smsframework.errors.SendMessageError.codes.GEN_CREDIT)
console.error('Not enough funds:', err);
else
console.error(err.stack);
});
Events
The Gateway
object is an EventEmitter
which fires the following events:
msg-out
Outgoing Message: a message is being sent.
Arguments:
message: OutgoingMessage
: The message being sent. See OutgoingMessage.
NOTE: It is not yet known whether the message was accepted by the Provider or not.
Also, the msgid
and info
fields are probably not populated.
msg-sent
Outgoing Message: a message that was successfully sent.
Arguments:
message: OutgoingMessage
: The message being sent. See OutgoingMessage.
The message object is populated with the additional information from the provider, namely, the msgid
and info
fields.
msg-in
Incoming Message: a message that was received from the provider.
Arguments:
message: IncomingMessage
: The received message. See IncomingMessage.
status
Message Status: a message status reported by the provider.
A status report is only delivered when explicitly requested with options({ status_report: true })
.
Arguments:
status: MessageStatus
: The status info. See MessageStatus.
error
Error object reported by the provider.
Arguments:
error: Error|SendMessageError
: The error object. See Error Objects.
Useful to attach some centralized logging utility. Consider winston for this purpose.
Functional Handlers
Events are handy, unless you need more reliability: if an event-handler fails to process the message, the Provider still sends 'OK' to the SMS service.. and the message is lost forever.
Functional handlers solve this problem: you register a callback function that returns a promise, and in case the promise is rejected - the provider reports an error to the SMS provider so it retries the delivery later.
Example:
// Handler for incoming messages
gateway.receiveMessage(function(message){
return Q.nmcall(db, 'save', message);
});
// Handler for incoming status reports
gateway.receiveStatus(function(status){
return Q.nmcall(db, 'update', status.msgid);
});
Whenever any of the handlers fail - the Provider reports an error to the SMS service, so the data is re-sent later.
Gateway.receiveMessage(callback):Gateway
Subscribe a callback to Incoming Messages. Can be called multiple times.
Arguments:
callback: function(IncomingMessage):Q
: A callback that processes an Incoming Message. If it returns a rejection - the Provider reports an error to the SMS service.
Gateway.receiveStatus(callback):Gateway
Subscribe a callback to Message Statuses. Can be called multiple times.
Arguments:
callback: function(IncomingMessage):Q
: A callback that processes a Message Status. If it returns a rejection - the Provider reports an error to the SMS service.
Data Objects
SMSframework uses the following objects to represent message flows.
IncomingMessage
A messsage received from the provider.
Source: lib/data/IncomingMessage.js.
OutgoingMessage
A message being sent.
Source: lib/data/OutgoingMessage.js.
MessageStatus
A status report received from the provider.
Source: lib/data/MessageStatus.js.
Error Objects
Source: lib/data/MessageStatus.js.
Provider HTTP Receivers
The Gateway has an internal express application, which is used by all providers to register their receivers: HTTP endpoints used to interact with the SMS services.
Each provider is locked under the /<alias>
prefix.
The resources are provider-dependent: refer to the provider documentation for the details.
The recommended approach is to use /im
for incoming messages, and /status
for status reports.
Gateway.express()
To use the receivers in your application, use Gateway.express()
method which returns an express middleware:
var smsframework = require('smsframework'),
express = require('express')
;
// Gateway
var gateway = new smsframework.Gateway();
gateway.addProvider('clickatell', 'primary', {}); // provider, alias 'primary'
// Init express
var app = express();
app.use('/sms', gateway.express()); // mount SMSframework middleware under /sms
app.listen(80); // start
// Ready to receive messages
gateway.on('msg-in', function(message){
console.log('SMS from ' + message.from + ': ' + message.body);
});
Assuming that the provider declares a receiver as '/receiver'
,
we now have a 'http://localhost:80/sms/primary/receiver'
path available.
In your Clickatell admin area, add this URL so Clickatell passes the incoming messages to us.
Gateway.listen()
Sugar to start listening on the specified network address immediately.
Footprints (see net.Server
):
- `Gateway.listen(port[, host])
- `Gateway.listen(path)
- `Gateway.listen(handle)
Returns: a promise for the http.Server
object.
Example:
gateway.listen(80).then(function(server){
console.log('Listening on ', server.address()); // Get the address
server.close(); // stop listening
});
Receiver Security
It's adviced to mount the receivers under some difficult-to-guess path: otherwise, attackers can send fake messages into your system
Secure example:
app.use('/sms/Zei6Ohth', gateway.express());
NOTE: Other mechanisms, such as basic authentication, are not typically useful as some services do not support that.
Message Routing
SMSframework requires you to explicitly specify the provider for each message, or uses the first one.
In real world conditions with multiple providers, you may want a router function that decides on which provider to use and which options to pick.
In order to achive flexible message routing, we need to associate some metadata with each message, for instance:
module
: name of the sending module: e.g. "users"type
: type of the message: e.g. "notification"
These 2 arbitrary strings need to be standardized in the application code, thus offering the possibility to define complex routing rules.
When creating the message, use route()
function to specify these values:
gateway.message('+1234', 'hi')
.route('users', 'notification')
.send().done();
Now, set a router function: a function which gets an outgoing message + some additional routing values, and decides on the provider to use:
gateway.addProvider('clickatell', 'primary', {});
gateway.addProvider('clickatell', 'secondary', {});
gateway.addProvider('clickatell', 'usa', {});
gateway.setRouter(function(message, module, type){
// Use 'usa' for all messages to USA
if (/^+1/.test(message.to))
return 'usa';
// Use 'secondary' for notifications
if (type === 'notification')
return 'secondary';
// Use 'primary' as a default
return 'primary';
});
Router function is also the right place to specify message options & parameters.
To unset the router function, call gateway.setRouter()
with no arguments.
Bundled Providers
The following providers are bundled with SMSframework and thus require no additional packages.
NullProvider
Source: lib/providers/null.js
The 'null'
provider just ignores all outgoing messages.
Example:
gw.addProvider('null', 'null'); // provider, alias
LogProvider
Source: lib/providers/log.js
The 'log'
provider just logs all outgoing messages without sending them anywhere.
Config:
log: function(OutgoingMessage)
: The logger function. Defalt: log message destination & text to the console.
Example:
gw.addProvider('log', 'log', {
log: function(message){
console.log('SMS to', message.to, ':', message.body);
}
});
LoopbackProvider
Source: lib/providers/loopback.js
The 'loopback'
provider is used as a dummy for testing purposes.
It consumes all messages, supports delivery notifications,
and even has a '/im'
HTTP receiver.
All messages flowing through it get incremental msgid
s starting from 1.
LoopbackProvider.getTraffic():Array.<IncomingMessage|OutgoingMessage>
LoopbackProvider stores all messages that go through it.
To get those messages, call .getTraffic()
.
This method empties the message log.
gateway.addProvider('loopback', 'lo', {});
gateway.message('+123', 'hi').send()
.then(function(){
gateway.getProvider('lo').getTraffic(); // array of messages
});
LoopbackProvider.receive(from, body):Q
Simulate an incoming message.
Arguments:
from: String
: Source numberbody: String
: Message text
Returns: a promise for a message processed by SMSframework
gateway.on('msg-in', function(message){
console.log(message.from, ':', message.body);
});
gateway.getProvider('lo').receive('+1111', 'notification!');
LoopbackProvider.subscribe(sim, callback)
Register a virtual subscriber which receives messages to the matching number.
Arguments:
sim: String
: Subscriber phone numbercallback: function(from: String, body: String, reply:function(String):Q)
: A callback which gets the messages sent to this subscriber.The last argument is a convenience function to send a reply. It wraps
LoopbackProvider.receive()
.
gateway.getProvider('lo').subscribe('+123456', function(from, body, reply){
reply('Hi '+from+'!');
});
gateway.message('+123456', 'hi').send();