dsg-project-nan
v1.0.1
Published
This project was executed at University of Bamberg, Distributed Systems Group, winter semester 2020. This project is aligned with the SCORE 2021 project: 'ChatManager: An Enterprise Instant Messaging abstraction API'. For more details have a look at our R
Downloads
3
Readme
Introduction
This project is aligned with the SCORE 2021 project: "ChatManager: An Enterprise Instant Messaging abstraction API". We did not submit it to the competition, but the ideas/functions are the same. The following introductory description is taken from the project platform:
"Enterprise Instant Messaging (EIM) platforms are collaboration tools adopted by many companies that allow them to exchange messages and to share files, as well as aggregating notifications from third-party tools. Interaction models offered by these platforms are somewhat similar. [...] In this project, you will develop a library that abstracts the interaction layer of EIM platforms. This library will allow configuration and interaction with at least the two most common platforms on the market: Slack by Slack Technologies and Teams by Microsoft [and Gitter (see https://gitter.im)]."
As mentioned in the description we support Slack and MS-Teams. In addition we added support for a third platform: Gitter. Our features range from sending messages, listen to messages (sent to the bot), sending/creating complex forms (synonym to "interactive messages") and defining/sending custom commands.
Install, Setup & General Overview
Installation
The only requirement is NodeJS on your system to run the application. In addition you need some accounts to test your bot (see platforms in detail for that).
Installation via npm
is straight forward: npm install dsg-project-nan
.
Setup
In general you need the PlatformFactory
to get instances for the platforms.
From there you can get multiple instances for the platform you like. So 1 instance for 1 platform!
For specific function calls have a look at specific platform documentation below.
The module.exports
defines all exported objects for your usage (see index.js
):
// This file is defining the default module.
// This module is exported when the package is used.
// common objects
import PlatformFactory from "./common/PlatformFactory";
import ChatMessage from "./common/ChatMessage";
import MessageResponse from "./common/MessageResponse";
// MS-Teams related
import CardImageTeams from "./platforms/ms-teams/ComplexForms/CardImageTeams";
import CardActionTeams from "./platforms/ms-teams/ComplexForms/CardActionTeams";
import ComplexFormMessage from "./platforms/ms-teams/ComplexForms/ComplexFormMessage";
module.exports = {
PlatformFactory, ChatMessage, MessageResponse,
CardImageTeams, CardActionTeams, ComplexFormMessage
};
To import those in your application you can do it the following way:
// import all components in "commonjs" style
import dsgProjectNaNPackage from 'dsg-project-nan';
const { PlatformFactory, ChatMessage, MessageResponse } = dsgProjectNaNPackage;
// Note: only a few objects are imported in here (for full list see "index.js" above)
Note: We use "commonjs" syntax to destructor the dsgProjectNaNPackage
export, because our project relies on components written in commonjs
.
One Interface to rule them all
The one reference point is the ChatInterface
, we tried to unify all our methods (as much as possible) under one common interface, which can be used as a reference point. This interface (in typescript notation) shows what methods you can use to write your bot.
:zap: Be warned some methods are not implemented for every platform especially Gitter is lacking things like complex forms and custom commands.
This is an extract from ChatInterface.ts
to showcase every method to use:
interface ChatInterface {
// gitter, slack and ms-teams
testAuthentication(): Promise<boolean>
sendMessage(message: ChatMessage): Promise<void>
listenOnMessage(callback: (message: MessageResponse) => void): void
// only slack and ms-teams
createComplexForm(title: string): any // returns a platform specific object
sendComplexForm(destinationID: string | object, complexForm: ComplexFormMessage | SlackComplexForm): Promise<void>
addCommand(name: string, callback: (message: MessageResponse) => void): void
deleteCommand(name: string): void
onCommand(request: any): void
}
Hint: Promise<void>
or Promise<boolean>
means that the method is implemented as async
method and has to be called with await
.
Due to platform specifics this can vary from platform to platform, but in general it is advised assume that the method is returning a Promise.
Common Objects
There are some objects used to unify the platforms a bit, so you can work with common objects for different platforms.
First of all there is a common object primarily for sending messages: ChatMessage
.
An extract from its TypeScript definition:
class ChatMessage {
private _text: string;
private _destination: any;
constructor (text: string, destination: any) {
this._checkIfParamsAreEmpty(text, destination);
// assign paramaters
this._text = text;
this._destination = destination;
}
/** some internal methods for checking the "text" and "destination" **/
// (only) getter for "text" and "destination"
get text(): string {
return this._text;
}
get destination(): any {
return this._destination;
}
}
Usage example:
const myMessage = new ChatMessage("Hello World", "roomName");
console.info(myMessage.text); // expect: text == "Hello World"
console.info(myMessage.destination); // expect: destination == "roomName"
Note: text
and destination
are required for a whole ChatMessage object, if not provided (via the constructor) an TypeError
is thrown!
If you wonder why is destination
of datatype any
? This is regarding to platform specifics.
For Gitter and Slack a string
is enough, MS-Teams differs from that.
Another important unified object is, when your bot receives messages: MessageResponse
.
This is often received as parameter of a callback method for instance when you listen for messages with listenOnMessage(myCallback)
.
class MessageResponse {
// message text which was sent to the bot
#text: string;
// the destination the message originates from (accordingly to ChatMessage)
#destination: any;
// this is the username from the user who sents the message to the bot
#from: string;
// this is a timestamp when the message was sent in ISO format (as reference see (new Date()).toISOString() in JavaScript)
#sent: string;
// setter and getter for text, destination, from and sent
// ...
}
If you receive this method in a callback you can access the data as properties for e.g. msgResponse.text
Platform in details
You want to write a bot for a specific platform? Then you have to go deeper into the specific platform documentations.
MS-Teams
Difference to other platforms
Due to Microsofts policy in relation to bots, it is very hard to practively send Messages via Microsoft Teams. It is only possible to send a message proactively when you can reference a Conversation Reference. You can obtain such a reference by listening to incoming messages (See example below)
Instantiating a MS-Teams Bot
A bot is created via the PlatformFactory. For Creating the Bot needs to be provided with a AppID and a AppPassword. These credentials can be obtained via the Azure Platform
Example call to create a bot:
const myBot = PlatformFactory.getTeamsInstance(yourID, yourPW)
Send a Message via MS Teams
When sending a message via the MS-Teams plattform you need to provide a Chatmessage which contains a destination and a text. In case of MS-Teams the destionation needs to be a Conversation Reference.
Normaly the Conversation Referece will be aquired by receiving a message by a user.
Given a Conversation Reference the Code to send a message will look like this:
let message = new ChatMessage("Hello I'm a Command", conversationReference)
myBot.sendMessage(message)
Setting up a simple MessageListener with a Callback
When providing a Callback function to a Message Listener the callback function must be of form:
callback(msg: MessageResponse){
// Some Code here
}
The MessageResponse contains the information you get back from the OnMessageListener. It provides text, Date, Origin and Sender information. In case of MS-Teams the Origin will be a conversation Reference which can be used to send a message back.
Here is a quick example call for a simple callback Function:
const myBot = PlatformFactory.getTeamsInstance(yourID, yourPW)
myBot.listenOnMessage(onMessage)
// Callback function
function onMessage(response: MessageResponse) {
let message = new ChatMessage("Hello " + response.text, response.destination)
myBot.sendeEssage(response.destination, message)
}
Creating and Sending a complex Form via MS-Teams
This section only refers to Complex Forms in context of MS-Teams: MS-Teams provides the possiblility to send so called Cards to other users. Cards are interactive messages and have a wide range of content that can be attachted.
In this package Cards will refered to as Complex Forms in all Platforms Complex Forms can be provided with CardActions. These will appear in the form of buttons. Their ActionType will determine what kind Action will be exectued when clicked. Furthermore Complex Forms can be provided with CardImages. CardImages are used to provide the Card with pictures.
Complex Messages can only be sent, when wraped in a ComplexFormMessage
Example call of sending a Card which will contain a link to open Google:
const myBot = PlatformFactory.getTeamsInstance(yourID, yourPW)
// Create a button, which opens http://google.de when clicked
let action = new CardActionTeams(ActionType.OpenUrl, "http://google.de", "Click")
// Create a new complex Form
// Buttons and Images must always be provided in the Form of an array
let form = myBot.createComplexForm("Hallo","", "", [], [action])
// Wrap the Complex Form in a messae
let message = new ComplexFormMessage(form)
myBot.sendComplexForm(response.destination, message)
Creating Commands with MS-Teams
The libary provides the possiblilty to register custom commands with MS-Teams. Commands contain a Name and a callback function to execute when the command is called.
A command is considered called when the commands name is typed into a MS-Teams chat.
When providing a Callback function to a Command the callback function must be of form:
callback(msg: MessageResponse){
// Some Code here
}
The MessageResponse contains the information you get back. It provides text, Date, Origin and Sender information. In case of MS-Teams the Origin will be a conversation Reference which can be used to send a message.
Example to register a command:
const myBot = PlatformFactory.getTeamsInstance(yourAppID, yourAppPW)
// Command will be called when '!Test' is inserted in Chat
myBot.addCommand("!Test", onCommand)
// Callback function
function onCommand(response: MessageResponse) {
let message = new ChatMessage("Hello I'm a Command", response.destination)
myBot.sendMessage(message)
}
Deployment
To Deploy a Bot please look into the official MS-Teams documentation
Errors and Exceptions
List all errors which are potentially possible:
MessageSendingError
- An error happened when a message couldn't be sent.TypeError
- These are occasionally thrown when some types are not correct, for e.g. if thecallback
forlistenOnMessage(callback)
isn't a function orconfigListeners(roomNames)
gets an empty array or something other than an array.NotImplemented
- This error is thrown if a method not implemented due to platform limitations is called.
Slack
Setting up a Slack app
At first create a new Slack app on the slack api website.
In your APP HOME you can now access under OAuth & Permissions the Bot User OAuth Access Token
of your Slack app.
Also don't forget to add your app to every channel of your Slack team it should interact with.
Setting up permissions
These are the permissions your app needs to use all features of this package:
chat:write
channels:read
groups:read
im:read
mpim:read
user:read
Setting up the Slack events-api
To reiceive messages from slack you first have to register a public URL where slack will send the events to. This can be done in the slack app settings on their website under the tab Èvent Subscriptions
. To verify this URL you can use a script included in the @slack-events-api. Use the following command:
$ ./node_modules/.bin/slack-verify --secret <signing_secret> [--path=/slack/events] [--port=3000]
The signing secret can be obtained from the slack website. The path has to match the path from the URL. This script will send the expected respond back to slack when registering the URL. After the URL is verfied you can press CTRL-C to stop the script.
Additionally you have to select wich events the app should subscribe to. The events you need to subscribe to receive messages are:
message.channel
message.im
message.mpim
More information on the administrative setup for your slack app can be found in the slack api documentation.. You have to follow the same steps as if you would use the @events-api
from the slack-sdk.
Creating a Slack instance
You can use the getSlackInstance(token: string, secret:string, port:number)
method of the PlattformFactory
to create a new Slack
instance.
Use the above mentioned Bot User OAuth Access Token
as your token.
import nanFramework from 'dsg-project-nan'
const { PlatformFactory } = nanFramework;
const token = "xoxb-..."
const secret = "secret..."
const port = 3000;
let slack = PlatformFactory.getSlackInstance(token, secret, port);
Sending a message
To send a message to either a user, a group or a channel, you can use the sendMessage(chatMessage: ChatMessage)
method of your Slack
instance.
IMPORTANT: Always use IDs as the destination of a message, see Getting user information and Getting channel information on how to recieve them.
//Create a new ChatMessage object
let userId;
let userInfo = await slack.getUserList(false);
userInfo.forEach(user => {
if(user.username === "John Doe"){
userId = user.id;
}
});
const message = new Chatmessage(user_id, "Hello John Doe");
const response = await slack.sendMessage(message);
Getting user information
You can recieve a list of all available user information with the getUserList(extendedInfo: boolean)
method.
With extendedInfo = false
the method will just return a list of the real_name
and id
of the users.
Getting channel information
You can recieve a list of all available channel or group information with the getConversations(types?: string)
method of your Slack
instance.
You can set types
to either public_channel
to get all informations about the available channels, or private_channel
to get all informations about the available groups.
Receiving messages
To receive message you have to have setup your app to receive events (see section 'Setting up the events-api'). You than can start to listen for messages the following way. The response will always follow the MessageResponse format.
import nanFramework from 'dsg-project-nan'
const { ChatMessage, PlatformFactory } = nanFramework;
const token = "xoxb-..."
const secret = "secret..."
const port = 3000;
let slack = PlatformFactory.getSlackInstance(token, secret, port);
// register the callback function for receiving a message
slack.listenOnMessage((response) => {
console.log(response.text); //log response text
console.log(response.destination); //log the channel/converstion the message was sent from
console.log(response.from); //log the user who sent the message
console.log(response.sent); //log the timestamp of the message
if (response.text === "Hello Bot") {
(async () => await slack.sendMessage(new ChatMessage(`I hear you.`, response.destination)))();
}
})
// handle errors in the callbacks
slack.on('error', console.error)
// start a simple webserver which listens on /slack/events on the specified port
slack.start()
Using a custom server
Not implemented yet.
Listening to other events
Important: This feature is not supported by other platforms besides Slack.
It is possible to receive all other events from slack. Just make sure that your app subscribes to this events. It is also important to notice that the event does not follow the MessageResponse format anymore. An example on how to subscribe to other events is shown below.
import PlatformFactory from 'dsg-project-nan'
Slack slack = PlatformFactor.getSlackInstance(path, secret, port);
// register the callback function for the specified event
slack.on('app-mention', (event) =>
console.log(event)
})
// handle errors in the callbacks
slack.on('error', console.error)
// start a simple webserver which listens on /slack/events on the specified port
slack.start()
Creating a complex form
Create a new empty complex form with the createComplexForm(title: string)
method of your Slack
instance, this method returns a SlackComplexForm
object.
Now you can add different elements to your complex form:
addText(text:string)
: Adds a new text elementaddButton(text:string, id:string)
: Adds a new button elementaddMenu(text: string, id:string, placeholder:string, options:Array<string>)
: Adds a new dropdown menu elementaddPicture(url:string, altText:string)
: Adds a new picture elementaddDatePicker(text:string, id:string)
: Adds a calendar elementaddRadioButton(text:string, id:string, option:Array<string>)
: Adds a new radio button element
const complexForm = slack.createComplexForm('Title');
complexForm.addButton("Button", "ButtonID");
complexForm.addMenu("Menu", "MenuID", "Placeholder Element", ["Element 1", "Element 2"]);
Sending a complex form
You can send a complex form to user, group or channel with the sendComplexForm(destinationID:string, complexForm:SlackComplexForm)
.
const complexForm = slack.createComplexForm('Title');
const response = slack.sendComplexForm("destinationID", complexForm);
This method returns the response of the chat.postMessage method of Slacks' web-api.
Register commands
To register a command you have to create a new slash command in your Slack app under Slash Commands.
Then you can add the command in your code by using the AddCommand(name:string, callback:(message:MessageResponse) => void)
method of your Slack
instance.
WARNING: The name of your slash command in the Slack app has to match exactly the name of your added command!
const callback = (message:MessageResponse) => {
responseMessage = new ChatMessage(message.from, "Response to command");
slack.sendMessage(responseMessage);
};
slack.addCommand("commandName", callback);
Listening on commands
Slack will send a post request to the URL you added to your slash command in the Slack app.
To listen on executed commands add the onCommand(request:any)
method of your Slack
instance to your server. The request
argument of this method must be a string of the complete recieved post request.
WARNING: Slack requires to get a response from your server immediately after a user used a registered slash command. You can just send a empty HTTP 200 response, Read more here.
Example with a local Node.js
server:
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('');
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
slack.onCommand(body);
});
res.end();
}).listen(port);
console.log("Server listens on port " + port);
Gitter
This is a user documentation how to user every feature, function, class, etc. for the gitter platform. It is built like an FAQ for everyone asking common questions. Additionally it points out differences and errors/exceptions for the gitter implementation.
What is different from other platforms implementations?
:zap: Due to gitter's plaform limitations some features are not implemented, even if they are accessible via ChatInterface
.
The concrete methods are:
createComplexForm(title)
sendComplexForm(destinationID, complexForm)
addCommand(name, callback)
deleteCommand(name)
onCommand(request)
Those methods are throwing and Error("NotImplemented")
when you try to execute them.
How do I get an gitter
instance?
Register gitter.im user/bot
Follow the Authentication sections above for how to configure credentials for where to configure your gitter account.
Gitter is kind of special there is no native way to register a bot, instead you have to configure a "test" user via GitHub or GitLab.
With this "test" user you can login into Gitter.im and get user/bot token under the section "Personal Access Token" you find the TOKEN
to use for authentication (see below).
After you registered and configured your account (see Authentication) you can retrieve a gitter instance easily via the PlatformFactory
.
try {
const yourToken = "<your-api-key>";
const gitterInstance = PlatformFactory.getGitterInstance(yourToken);
} catch (configErr) {
// this error is often thrown when something in your config is not setup right
console.error(configErr);
}
// test if your key is a valid key gitter accepts
try {
let amIAuthentic = await gitterInstance.testAuthentication();
if (!amIAuthentic) {
console.log("Your are not authenticated. Might be your fault or the network or gitter.im is not reachable?");
}
} catch (authErr) {
console.error(authErr);
}
:zap: Be warned testAuthentication()
might mark you as not authenticated due to network failures. It does not check a network error explicitly.
How do I send messages?
Before sending messages you have to get the room name to sent to.
You can get the full room name when you are logged in on gitter.im after that initial URL part follows communityName/roomName
.
For https://gitter.im/NaNOurOwnWorld/whereAllTheBotsAreHiding
as example, the full room name is NaNOurOwnWorld/whereAllTheBotsAreHiding
.
After you have your gitterInstance
and your room name sending messages is straightforward:
try {
// setup ChatMessage with the first param being the "text" and the second the "room" to send to
const myChatMessage = new ChatMessage("what a cool text to send", "mycommunity/myroom");
// fire and forget the message
await gitterInstance.sendMessage(myChatMessage);
} catch (sendError) {
// Is your connection authenticated? Is your room a valid? Maybe just an network error?
console.error(sendError);
}
How do I listen to messages regarding the bot?
Here's a little difference to the other platforms like MS-Teams or Slack. First you have to configure all the rooms you want your bot to listen to. It is wise do this at the start of your bot application.
try {
const result = await gitterInstance.configListeners(
["mycommunity/validRoom1", "mycommunity/validRoom2", "mycommunity/invalidRoom"]
);
// You can test if all expected rooms have joined by checking both arrays
// in .joinedRooms are all rooms pushed which could be joined
const joined = (result.joinedRooms == ["mycommunity/validRoom1", "mycommunity/validRoom2"]);
// in .notJoinedRooms are all rooms pushed which couln't be joined. Ideally this array is empty :)
const notJoined = (result.notJoinedRooms == ["mycommunity/invalidRoom"]);
if (!joined && !notJoined) {
throw Error("There might be something you want to check especially if the not joined rooms are important.");
}
// listen for messages your bot/user is mentioned in
await gitterInstance.listenOnMessage((messageForBot) => {
// The messageForBot is of the common type "MessageResponse" (see that for details)
console.log(messageForBot);
// your bot logic goes here
});
} catch (err) {
// Just in case listen for errors. Normally errors are only thrown for invalid params like for e.g. empty arrays etc.
// Another error case is when no rooms are joined to listen to, so when "result.joinedRooms" is empty
console.error(err);
}
Tip: You could short the code a bit an maybe rely on the error checks behind. Just two lines with gitterInstance.configListeners(roomsToJoin)
and gitterInstance.listenOnMessage(callbackFunction)
are enough. But if you want to dig in deeper it is advised you use a "longer" version.
Errors and Exceptions
List all errors which are potentially possible:
AuthTokenWasNotSpecified
- If not authentication token was provided forGitter()
constructor. Normally this error is not thrown, because aCredentialsValidationError
(or similar) should be thrown beforehand.AuthTokenIsInvalid
- An authentication error happened. So the token was successfully provided but is not valid from gitter's perspective. :zap: There is one special case if the internal#isAuthenticated
flag was set tofalse
, due to temporarily network errors or server problem. In this case theAuthTokenIsInvalid
error is thrown every time afterwards. This can happen is for e.g.sendMessage(message)
ortestAuthentication()
is called during an network/server problem.MessageSendingError
- An error happened when a message couldn't be sent.RoomNotJoined
- A room couldn't e joined, because it doesn't exist or your bot isn't allowed to join this room.TypeError
- These are occasionally thrown when some types are not correct, for e.g. if thecallback
forlistenOnMessage(callback)
isn't a function orconfigListeners(roomNames)
gets an empty array or something other than an array.NotImplemented
- This error is thrown if a method not implemented due to platform limitations is called.