Services
A service module is like a built-in api module. But unlike api modules, service modules are configurable. For instance the mail service can be configured with the information necessary to connect to mail servers. This means a flow that needs to send or receive emails does not need to contain mail credentials and other sensitive information.
Services are accessed via the Service
module. getting access to a mail service with the configured key myMail
is done as follows:
var m = Service.get('myMail', Service.MAIL);
Now the variable m
holds a reference to a mail-service ready to use.
Services can usually also be used to set up flow triggers. In the mail service example, flows can be triggered when emails are received from the mail server configured in the mail service.
The following services are available:
Mail
The mail-service module has functionality to list and send mails.
Access any exchange inbox
When connecting a flow with an Exchange server via the app-only oauth profile, a flow can access any desired inbox. The way to do this is through the method impersonate
as in the following example:
var m = Service.get('myMail', Service.MAIL);
var sarahsMail = m.impersonate('sarah65@customdomain.outlook.com');
var sarahsInboxContents = sarahsMail.list('Inbox');
Send
Sending a mail requires a list of recipients, a subject and a body and has optional support for defining the from address, list of cc-addresses and attachments.
Note on asynchronicity
Note that this method is asynchronous and immediately returns a task object that you must use to wait for the send to complete. Failure to do so may result in mail not being properly sent and/or send errors not being raised in the flow. Correct usage can be seen in the examples below.
Arguments
In a call like send(to, subject, body, options)
the arguments are as follows:
to
is a list of email-addresses which will receive the messagesubject
is the subject of the mailbody
is the main text of the mailoptions
is an optional object which may contain the following propertiesfrom
the sender address (default is “manatee@sirenia.eu”). Setting this value is not permitted by some mail servers.cc
is a list of cc-addresseshtml
is the html body if any (for exchange servers this takes presedence over the text body)attachments
is a list of files to attach (each item is a path to a file)html
is the html body if anyimportance
one of the values'low'
,'normal'
,'high'
indicating the priority of the message
Examples
var m = Service.get('myMail', Service.MAIL);
// Simple send
var transmissionTask = m.send(['jonathan@sirenia.eu'], 'Hello from a bot', 'Manatee says hello');
transmissionTask.wait(10000);
// With all options
var task = m.send(
['jonathan@sirenia.eu'],
'Hello from a bot',
'Manatee says hello',
{
'from': 'bot@somewhere.com',
'cc': ['martin@sirenia.eu', 'los@sirenia.eu', 'lykke@sirenia.eu'],
'attachments': ['C:/Users/SomeUser/SomeFile.txt']
}
);
task.wait(10000);
List
List is used to list mails in a given mail-box. The return value is a list of mail objects.
Arguments
mailbox
is the mailbox to list (default is theINBOX
)
Examples
// List mails in inbox
var inbox = m.list();
var mailsElsewhere = m.list('someothermailbox');
Mail Object
The mail object returned from list has the following properties:
id
id of the mailto
list of recipientscc
list of cc-addressesfrom
sender addresssubject
subjectbody
mail texthtml
html mail body if anyread
whether or not the mail is read/unread (property is writeable)attachments
a list of attachment file paths on the local disk. The files can be accessed only after callingwriteAttachmentsToDisk()
(described below)importance
one of the values'low'
,'normal'
,'high'
indicating the priority of the messagereceivedTime
a javascript date indicating when the message was received (IMAP and Exchange)createdTime
a javascript date indicating when the message was created (Exchange only)sentTime
a javascript date indicating when the message was sent (Exchange only)lastModifiedTime
a javascript date indicating when the message was modified (Exchange only)
And the following methods:
delete(mailbox)
to delete the mail from the given mailbox (default isINBOX
)move(to, from)
to move the mail between two mailboxeswriteAttachmentsToDisk
to write the attachments to disk (access them then via theattachments
property)
Finding a message and loading text from one of its attachments
var inboxMails = m.list();
// Find first message from 'foo@bar.baz' with subject 'invoice' at least one attachment
var msg = _.find(inboxMails, function(m) {
return m.from === 'foo@bar.baz' && m.subject.indexOf('invoice') >= 0 && m.attachments.length > 0;
});
if (msg) {
msg.writeAttachmentsToDisk();
// File data can now be read from disk
var attachmentTextContent = Fs.read(msg.attachments[0]);
}
Serial port
The serial port service module lets flows and triggers communicate via serial ports on the local machine.
Data can be sent and received as either binary data or text. Receiving data can be done synchronously or asynchronously as shown in the examples below.
Sending data
Sending data is simple - obtain the service and give it a string or an array of bytes (numbers) to send. Sending is always synchronous (returns after data has been sent):
var myDevice = Service.get('my-device', Service.SERIALPORT);
// Send text
myDevice.send('scan barcode, please');
// Send binary data
myDevice.send([0x09, 0x0A, 0x0B, 0x0C]);
Receiving data
All methods for receiving data come in synchronous and asynchronous forms such as receiveOne
and receiveOneAsync
. They also all accept as their last argument an options object which can have the following properties:
timeout
is an optional override of how long to wait for data to arrive in milliseconds. The default is 3000.binary
is an optional override of the format in which the data is returned.true
means a byte array is returned,false
means a text string is returned. Receiving data as text requires the device to encode text by the same encoding configured in the serial port service. The default isfalse
.
Request / reply
As a convenience for the common task of sending a request and receiving a reply, two requestReply
methods are available.
var myDevice = Service.get('my-device', Service.SERIALPORT);
// Send and receive text synchronously. Here we override the default receive timeout
var barcodeText = myDevice.requestReply('scan barcode, please', { timeout: 10000 });
// When binary data is sent, the data received is also binary data by default - and vice versa
var replyBytes = myDevice.requestReply([0x09, 0x0A, 0x0B, 0x0C]);
The asynchronous form returns a task object. The task object behaves the same as the tasks used in the Http and Task modules.
var task = myDevice.requestReplyAsync('scan barcode, please');
// ... Do other things while we wait for the response...
task.wait();
var barcodeText = task.result;
Receive one message
If we expect to receive a data message from the device, we can receive it like so:
var messageText = myDevice.receiveOne();
// ... or as undecoded bytes
var messageBytes = myDevice.receiveOne({ binary: true });
The asynchronous form offers no surprises:
var task = myDevice.receiveOneAsync();
// ... Do other things while we wait for the message...
task.wait();
var messageText = task.result;
Receive multiple messages
Sometimes we expect a device to send multiple messages. If we use receiveOne
, there is a risk that a message arrives between invocations and is lost. To address this situation, we can use receiveMany
. Its syntax is slightly more complex as we must provide the receiveMany
method with a callback which will be called for each received message.
The callback must return true
while more messages are expected. This means when we receive what we know to be the last message, we can return false
and receiveMany
will return control to the flow immediately without waiting for the timeout to elapse.
Note that Api methods that show dialogs (for instance Dialog.input
or Debug.showDialog
) are not supported within the callback. Use the callback to collect the messages - parsing only enough to determine the return value of the callback.
var receivedMessages = [];
// Listen for messages until the default timeout elapses and put them in the array as they arrive.
myDevice.receiveMany(function(message) {
receivedMessages.push(message);
return true;
});
// Listen for messages until the 'BYEBYE' message is received (or until the timeout elapses)
myDevice.receiveMany(function(message) {
receivedMessages.push(message);
return message !== 'BYEBYE';
});
This method also has an asynchronous form to enable parallel processing:
var receivedMessages = [];
// Listen for messages until the default timeout elapses and put them in the array as they arrive.
var task = myDevice.receiveManyAsync(function(message) {
receivedMessages.push(message);
return true;
});
// ... do something else while messages are received ...
task.wait();
// Now we can process the messages in the 'receivedMessages' array.
Latest inbound messages
This service module keeps track of the most recent messages received from the device. This can be useful if a flow is triggered by the reception of a message and the flow needs to inspect the messages preceding the triggering message. Note that messages are only added to this collection while a receive operation is active on the serial port. An active serial port trigger will cause messages to be added. A flow with a long running receive operation likewise.
The history is returned as an array of entry objects with the following properties:
time
is aDate
object indicating when the message was receiveddata
is a string or an array of bytes depending on the optionalbinary
option
var history = myDevice.getLatestInbound();
if (history.length > 0) {
var lastEntry = history[history.length - 1];
// ... do something with the last received entry ...
}
// Get binary data in stead
history = myDevice.getLatestInbound({ binary: true });
Open / close port
The methods for sending and receiving data will open the port automatically and close it again when they are done. This means it isn’t strictly necessary to excplicitly open and close the port. If for any reason it is undesirable for the port to only be open while it is in use, you can open and close the port excplicitly from your flow:
myDevice.open();
// ... communicate with device
myDevice.close();
Note that manatee manages the state of the physical serial port intelligently. A serial port trigger and a flow can use the same serial port service at the same time. This means that calling .open()
and .close()
may not have a direct effect on the physical port if a trigger is already keeping the port open in order to listen for triggering messages. The open
and close
methods merely express intent to use the port for more than one operation. As such they guarantee that the port will not automatically close between separate operations. Excplicitly closing the port isn’t a strict requirement as it will happen automatically when the flow has completed.
Byte / string conversions
The serial port service module exposes the means to convert back and forth between byte arrays and their string representation under the encoding configured on the serial port service in cuesta.
// myDevice service uses us-ascii
var text = 'abc';
var bytes = [ 0x61, 0x62, 0x63 ]
var decodedText = myDevice.bytesToString(bytes);
var encodedBytes = myDevice.stringToBytes(text)
// text and decodedText are now the same
// bytes and encodedBytes are now the same
Message queue
The message queue service module enables triggers to communicate via message queue messages. It also enables flows to send and receive such messages.
Interacting with a message queue from a flow requires obtaining a preconfigured mq service instance and getting a live connection from it:
var mqConn = Service.get('my-mq', Service.MQ).connect();
For this code to work, an mq service must exist with the key my-mq
Publishing messages
Publishing a message can be as simple as the following example:
mqConn.publish('amq.direct', 'myQueue', 'The message body often consists of json');
This simple example specifies an mq exchange (amq.direct
), a routing key (myBinding
) and a message body. To receive this message, an mq client would need to have bound a queue to the amq.direct
exchange with the routing key myBinding
.
Props and headers
In some cases, we need to add some meta data to a message. An example could be RPC style communication (Remote Procedure Call), where we request a server to carry out a task and report back when it’s finished. This usually requires specifying a correlation id and a reply address.
var props = {
correlationId: 'correlation123',
replyTo: 'weatherReplies',
headers: {
'CUSTOM-HEADER': 'Some header value'
}
};
mqConn.publish('amq.direct', 'weatherRequests', 'How is the weather over there?', props);
The reply to this message could subsequently be received by receiving messages from a queue bound to the amq.direct
exchange with the routing key weatherReplies
. To verify that the reply is indeed a reply to this specific message, the correlation id can be used.
Receiving messages
Consuming messages from a queue can be done either one message at the time, or several at the time. Both methods support both synchronous and asynchronous operation.
receiveOne takes just one message off the queue and returns it synchronously (or waits in vain and returns null):
var message = mqConn.receiveOne({ timeout: 5000 });
if (message) {
message.ack();
Debug.showDialog('Received message ' + message.body + ' with headers ' + JSON.stringify(message.props.headers, null, 2));
} else {
Debug.showDialog('Didn\'t receive anything before timeout');
}
The default waiting time is 3000 ms, but can be changed by passing a suitable settings object.
receiveOneAsync does the same, but allows the flow to do other things while waiting for the message to arrive:
var task = mqConn.receiveOneAsync({ timeout: 200 });
// .. do other things here in parallel with the message reception
task.wait();
if (task.result) {
var message = task.result;
message.ack();
Debug.showDialog('Received message ' + message.body);
} else {
Debug.showDialog('Didn\'t receive anything before timeout');
}
receiveMany takes messages off the queue until the callback it is provided returns false
or until the timeout elapses, whichever comes first. Note that only one unacknowledged message can be received at the time. So in order to receive message number two, message number one must first be acknowledged (by calling message.ack()
, message.nack()
or message.respond(..)
).
var messages = [];
function handleMessage(message) {
message.ack();
messages.push(message);
return messages.length < 2;
}
mqConn.receiveMany(handleMessage, { timeout: 10000 });
if (messages.length > 0) {
Debug.showDialog('Received messages ' + messages.map(function(m) { return m.body; }).join(' + '));
} else {
Debug.showDialog('Didn\'t receive anything before timeout');
}
The above example waits ten seconds for two messages to be taken off the queue. As they are taken off the queue they are added to an array for later inspection. The callback signals with its return value when it has received enough messages.
receiveManyAsync does the same, but allows the flow to continue work while waiting for the message reception to complete.
var messages = [];
function handleMessage(message) {
message.ack();
messages.push(message);
return messages.length < 2;
}
var task = mqConn.receiveManyAsync(handleMessage, { timeout: 10000 });
// .. do other things here in parallel with the message reception
task.wait();
if (messages.length > 0) {
Debug.showDialog('Received messages ' + messages.map(function(m) { return m.body; }).join(' + '));
} else {
Debug.showDialog('Didn\'t receive anything before timeout');
}
Queue inspection
The connection object seen in the examples above further has the properties messageCount
and consumerCount
:
var messageCount = mqConn.messageCount;
var consumerCount = mqConn.consumerCount;
These may be used to verify the correctness of the queue infrastructure configuration or similar tasks.
Queue maintenance
When taking messages off a queue as in the above examples, a message object is obtained. It has the following methods:
- messsage.ack() consumes the message from the message queue
- message.nack() returns the message to the message queue. Note that this means your flow will be able to continue receiving this same message over and over. This can be useful if there are other subscribers to the queue, who are better suited to handling the message.
- message.respond(‘response text’) sends a response. This will only work if the original message came with a
reply_to
property. Correlation id (if there is one) is also transferred when this method is used. Sending a response will automatically ack the message, if this hasn’t already been done.
The message queue client in manatee is set up to only allow one unacknowledged message at the time. This means you cannot receive more messages until you have called ack()
, nack()
or respond(..)
on the ones you have previously received.
Message data and metadata
The message object provides access to the contents of the message through the following members:
- message.body The body of the message in the form of a string
- message.props.headers The headers in the form of an object where properties and their values corresponds to headers in the message
- message.props.replyTo Retrieves a message property value. Returns null if the message didn’t have that property. See the list of valid property names below for more supported props.
Supported props
The message properties that should be specified when publishing messages depend on the situation. Occasionally, the recipient of the message has some required properties in order to be able to process a message.
The supported properties when publishing messages are as follows:
appId
(string)clusterId
(string)contentEncoding
(string)contentType
(string)correlationId
(string)expiration
(string)messageId
(string)replyTo
(string)type
(string)userId
(string)deliveryMode
(number)priority
(number)persistent
(bool)
Chat
The chat service is a generic service configuration with Slack as its currently only implementation. It supports triggers and provides a module. The module has the following functionaliy.
Send a message
You can send a message to a given “channel” by getting an instance of the service and invoking the send
method:
var c = Service.get('keyForService', Service.CHAT);
// Send the "foo" on the "bar" channel
c.send("foo", { channel: "bar" });
Read messages in a channel
Read the latest messages in a channel by:
var c = Service.get('keyForService', Service.CHAT);
// Get the latest messages from the given channel
var messages = c.read("test");
Each message will have the following properties:
text
the content of the messageuser
the user id of the author of the messageusername
the username of the authorchannel
the name of the channel in which the message was sentts
a timestamp/identifier for the message
Invoke named method
You can invoke arbitrary methods on the chat service (e.g. Slack) by using the invoke
method. It takes three arguments;
operation
the name of the operation/method to invoke,arguments
for the method (optional),method
the http method to use (optional - default “GET”)
For the Slack type chat you can lookup the operations etc in their API description - for example:
var c = Service.get('keyForService', Service.CHAT);
// Invoke the `conversations.list` to list all channels
var channels = c.invoke("conversations.list");