Bot SDK

Summary

Description

Symphony Bot SDK streamlines bot and extension app creation process by abstracting away many of the complexities and required boilerplate. Through simple and intuitive extension points, you can inject your own logic to handle bot commands, Symphony events, notifications from external systems, and many more.

Requirements

  • JDK 1.8

  • Springboot 2.2.2

  • Maven 3.0.5+

Installation

To install Symphony Bot SDK to your projects add the following entry in your pom.xml file:

<dependency>
<groupId>com.symphony.platformsolutions</groupId>
<artifactId>symphony-bdk-bot-sdk-java</artifactId>
<version>1.0.8</version>
</dependency>

Installing from source

To build and install Symphony Bot SDK from source you need to clone this repository and once you have all the requirements installed simply run:

$ mvn clean install

Usage

Using Symphony Bot SDK implies building a Spring Boot application. This requires a main class containing the public static void main() method used to start up the Spring context. Import the BotBootstrap class in that main class.

@SpringBootApplication
@Import(BotBootstrap.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

Configuration

Symphony Bot SDK requires two main configuration files, application.yaml (or application.properties) and bot-config.json. The first one helps you to configure the SDK aspects and features while the latter holds Symphony details such as POD, agent and key manager addresses, bot and extension app details, etc.

It is required to set the path to bot-config.json in application.yaml through the bot-config property, as well as, you must set the path to the RSA private key using certs property.

server:
port: 8080
servlet:
context-path: "/botapp"
certs: /path/to/private/key
bot-config: /path/to/bot-config.json
logging:
file: /logs/bot-app.log

In bot-config.json it is required to set the path to the RSA private key as well (see appPrivateKeyPath and botPrivateKeyPath):

{
"sessionAuthHost": "session.symphony.com",
"sessionAuthPort": 443,
"keyAuthHost": "km.symphony.com",
"keyAuthPort": 443,
"podHost": "pod.symphony.com",
"podPort": 443,
"agentHost": "agent.symphony.com",
"agentPort": 443,
"appId": "myapp",
"appPrivateKeyPath": "certs/",
"appPrivateKeyName": "app_private.pkcs8",
"botPrivateKeyPath": "certs/",
"botPrivateKeyName": "bot_private.pkcs8",
"botUsername": "mybot",
"authTokenRefreshPeriod": "30",
"authenticationFilterUrlPattern": "/secure/",
"showFirehoseErrors": false,
"connectionTimeout": 45000
}

Authenticating with certificate

Certificates are the alternative to RSA keys for authentication with Symphony. For certificate-based authentication update your bot-config.json file as below:

// Remove
{
...
"appPrivateKeyPath": "certs/",
"appPrivateKeyName": "app_private.pkcs8",
"botPrivateKeyPath": "certs/",
"botPrivateKeyName": "bot_private.pkcs8",
...
}
// Add
{
...
"appCertPath": "certs/",
"appCertName": "app_cert.p12",
"appCertPassword": "app-cert-password",
"botCertPath": "certs/",
"botCertName": "bot_cert.p12",
"botCertPassword": "bot-cert-password",
...
}

Adding bot commands

Easily add commands to your bot by extending the CommandHandler class (or its subclasses AuthenticatedCommandHandler, DefaultCommandHandler more on them later).

To extend CommandHandler implement the following methods:

  • Predicate<String> getCommandMatcher(): use regular expression to specify the pattern to be used by the SDK to look for commands in Symphony messages.

  • handle(BotCommand command, SymphonyMessage response): where you add your business logic to handle the command. This method is automatically called when a Symphony message matches the specified command pattern. Use the BotCommand object to retrieve the command details (e.g. user who triggered it, room where the command was triggered, the raw command line, etc). Use the SymphonyMessage object to format the command response. The SDK will take care of delivering the response to the correct Symphony room.

@Override
protected Predicate<String> getCommandMatcher() {
return Pattern
.compile("^@"+ getBotName() + " /hello$")
.asPredicate();
}
@Override
public void handle(BotCommand command, SymphonyMessage response) {
Map<String, String> variables = new HashMap<>();
variables.put("user", command.getUserDisplayName());
response.setTemplateMessage("Hello, <b>${user}</b>", variables);
}

Command initialization

To initialize your own logic prior to CommandHandler bootstrap, override the following method:

  • init(): Initializes the instance dependencies.

@Override
public void init() {
// Initialization logic
}

Default responses

Typically bots reply to invalid commands with a friendly default message. Extend the DefaultCommandHandler class to add that behavior to your bots.

Similar to its base class (i.e. CommandHandler), in DefaultCommandHandler you will need to provide implementation for both getCommandMatcher and handle methods.

Use simple regular expressions to make sure the message was targeted to the bot.

@Override
protected Predicate<String> getCommandMatcher() {
return Pattern
.compile("^@" + getBotName())
.asPredicate();
}
@Override
public void handle(BotCommand command, SymphonyMessage response) {
response.setMessage("Sorry, I could not understand");
}

Multi response command handler

Some bots may also need to send custom messages to different rooms. Extend the MultiResponseCommandHandler class to add that behavior to your bot.

Similar to its base class (i.e. CommandHandler), in MultiResponseCommandHandler you will need to provide implementation for both getCommandMatcher and handle methods.

Use MultiResponseComposer to compose your messages.

@Override
protected Predicate<String> getCommandMatcher() {
return Pattern
.compile("^@" + getBotName() + "/compose$")
.asPredicate();
}
@Override
public void handle(BotCommand command, MultiResponseComposer multiResponseComposer) {
multiResponseComposer.compose()
.withMessage("message")
.toStreams("stream1", "stream2")
.withTemplateFile("template", getTemplateParam())
.withAttachments(getAttachments())
.toStreams("stream3", "stream4")
.complete();
}

Authenticating to external system

When integrating with external systems, bots generally need to consume APIs exposed by such systems which require some sort of authentication.

Symphony Bot SDK provides mechanisms to support you on that. Through its AuthenticationProvider interface and AuthenticatedCommandHandler class the SDK offers:

  • isolate the authentication logic from command business logic

  • code reuse: single authentication method, multiple commands

  • rapidly replace the authentication method: AuthenticationProvider implementation changes, commands remain

@Override
protected Predicate<String> getCommandMatcher() {
return Pattern
.compile("^@"+ getBotName() + " /login$")
.asPredicate();
}
@Override
public void handle(BotCommand command, SymphonyMessage commandResponse,
AuthenticationContext authenticationContext) {
commandResponse.setMessage("<b>User authenticated</b>. "
+ "Please add the following HTTP header to your requests:"
+ "Authorization: "
+ authenticationContext.getAuthScheme() + " "
+ authenticationContext.getAuthToken());
}

AuthenticationProvider

To leverage the authentication support offered by the SDK, provide an implementation of the AuthenticationProvider interface.

The AuthenticationProvider interface defines two methods:

  • AuthenticationContext getAuthenticationContext(String userId): returns an AuthenticationContext object which holds authentication details for the given Symphony user.

  • void handleUnauthenticated(BotCommand command, SymphonyMessage commandResponse): invoked when the corresponding Symphony user is still not authenticated to the external system.

AuthenticatedCommandHandler

The AuthenticatedCommandHandler is a specialization of CommandHandler which interacts with AuthenticationProvider to retrieve an AuthenticationContext before invoking the handle method. All the authentication process is abstracted away from the command handler.

If the Symphony user issuing the command is still not authenticated to the external system, AuthenticatedCommandHandler will defer to the handleUnauthenticated method in AuthenticationProvider and the handle method will not be invoked.

The handle method in AuthenticatedCommandHandler child classes receives an extra parameter, the AuthenticationContext which contains necessary details to make authenticated requests to the external system.

Notice: the SDK supports multiple AuthenticationProvider classes. When only one implementation of the AuthenticationProvider interface is provided, the SDK will automatically inject it to all AuthenticatedCommandHandler child classes. Otherwise, you will have to specify which AuthenticationProvider to use with each AuthenticatedCommandHandler by annotating the command handlers with the CommandAuthenticationProvider annotation.

@CommandAuthenticationProvider(name="BasicAuthenticationProvider")
public class LoginCommandHandler extends AuthenticatedCommandHandler {

Command Matcher

In order to avoid writing complex regular expression when specifying the command pattern, you may rely on the CommandMatcherBuilder.

For example, the following pattern:

Pattern
.compile("^@BotName\\s/template(?:\\s+(?:([^\\s]+)(?:\\s+([\\s\\S]+)?)?)?)?")
.asPredicate();

would be built this way with CommandMatcherBuilder:

beginsWith("@")
.followedBy("BotName")
.followedBy(whiteSpace())
.followedBy("/template")
.followedBy(
optional(
nonCapturingGroup(
oneOrMore(whiteSpace()
).followedBy(
optional(
nonCapturingGroup(
group(
oneOrMore(
negatedSet(whiteSpace())
)
).followedBy(
optional(
nonCapturingGroup(
oneOrMore(whiteSpace()
).followedBy(
optional(
group(
oneOrMore(any())
)
)
)
)
)
)
)
)
)
)
)
).pattern();

Handling Symphony events

Bots may need to react to events happening on Symphony rooms they are part of (e.g. sending a greeting message to users who join the room).

Similarly to commands, Symphony Bot SDK offers straightforward mechanisms for your bots to be notified when something happens on Symphony chats. By extending the EventHandler class you register your bot to react to a specific Symphony event.

To extend EventHandler you need to:

  • specify the event type: EventHandler is a parameterized class so you need to specified which event type you want to handle. Refer to the following subsection for the list of supported events.

  • implement void handle(<symphony_event> event, final SymphonyMessage eventResponse): this is where you add your business logic to handle the specified event. This method is automatically called when the specified event occurs in a room where the bot is. Use the event object to retrieve event details (e.g. target user or target room). Use the SymphonyMessage object to format the event response. The SDK will take care of delivering the response to the correct Symphony room.

public class UserJoinedEventHandler extends EventHandler<UserJoinedRoomEvent> {
@Override
public void handle(UserJoinedRoomEvent event, SymphonyMessage response) {
response.setMessage("Hey, <mention uid=\"" + event.getUserId() +
"\"/>. It is good to have you here!");
}

Note: Different EventHandler child classes can handle the same event. All of them will have their handle method called. There is no way to set the calling order.

Available Symphony events

  • IMCreatedEvent: fired when an IM is created with the bot

  • RoomCreatedEvent: fired when a room is created with the bot

  • RoomDeactivatedEvent: fired when room is deactivated

  • RoomReactivatedEvent: fired when room is reactivated

  • RoomUpdatedEvent: fired when room is updated

  • UserJoinedRoomEvent: fired when user joins a room with the bot

  • UserLeftRoomEvent: fired when user lefts a room with the bot

  • RoomMemberDemotedFromOwnerEvent: fired when a room member is demoted from room ownership

  • RoomMemberPromotedToOwnerEvent: fired when a room member is promoted to room owner

Permission for bots in public rooms

The Symphony Bot SDK offers an easy way to control whether your bots are allowed to be added to public rooms or not.

By default, bots built with the Symphony Bot SDK are able to join public rooms. To change that behavior, just set the isPublicRoomAllowed in application.yaml file.

It is also possible to configure a custom message the bot would send before quitting the room, through the following configurations:

Property

Description

features.isPublicRoomAllowed

Whether bot is allowed in public rooms

features.publicRoomNotAllowedMessage

Message displayed before the bot leaves the room

features.publicRoomNotAllowedTemplate

Template file with the message displayed before the bot leaves the room

features.publicRoomNotAllowedTemplateMap

Template parameters of the message displayed before the bot leaves the room

You can specify simple static message to be displayed before the bot leaves the room, by setting publicRoomNotAllowedMessage, like the example below:

features:
isPublicRoomAllowed: false
publicRoomNotAllowedMessage: Sorry, I cannot be added to public rooms

or, they can use templates to generate structured messages, by setting publicRoomNotAllowedTemplate and publicRoomNotAllowedTemplateMap, like the following example:

features:
isPublicRoomAllowed: false
publicRoomNotAllowedTemplate: alert
publicRoomNotAllowedTemplateMap:
message:
title: Bot not allowed in public rooms
content: Sorry, I cannot be added to public rooms

Working with Symphony Elements

Symphony Elements allow bots to send messages containing interactive forms with text fields, dropdown menus, person selectors, buttons and more.

Symphony Bot SDK fully supports Elements. By extending the ElementsHandler class you get all you need to handle Symphony Elements, from the command to display the Elements form in a chat room to the callback triggered when the Symphony Elements form is submitted. All in one single class.

To extend ElementsHandler you need to implement the following methods:

  • Predicate<String> getCommandMatcher(): similar to CommandHandler. Use regular expression to specify the pattern to be used by the SDK to look for commands in Symphony messages.

  • String getElementsFormId(): returns the Symphony Elements form ID.

  • void displayElements(BotCommand command, SymphonyMessage elementsResponse): This is where you add your logic to render the Symphony Elements form. Similar to CommandHandler, this method is automatically called when a Symphony message matches the specified command pattern. Use the BotCommand object to retrieve the command details (e.g. user which triggered it, room where the command was triggered, the raw command line, etc). Use the SymphonyMessage object to format the Symphony Elements form. The SDK will take care of delivering the response to the correct Symphony room.

  • void handleAction(SymphonyElementsEvent event, SymphonyMessage elementsResponse): where you handle interactions with the Elements form. This method is automatically called when users submit the Elements form. The SymphonyElementsEvent holds details about the action performed on the form (e.g. form payload, action name, etc). Use the SymphonyMessage object to format a response according to the Elements form action.

private static final String FORM_ID = "quo-register-form";
private static final String FROM_CURRENCY = "fromCurrency";
private static final String TO_CURRENCY = "toCurrency";
private static final String AMOUNT = "amount";
private static final String ASSIGNED_TO = "assignedTo";
@Override
protected Predicate<String> getCommandMatcher() {
return Pattern
.compile("^@" + getBotName() + " /register quote$")
.asPredicate();
}
@Override
protected String getElementsFormId() {
return FORM_ID;
}
@Override
public void displayElements(BotCommand command,
SymphonyMessage elementsResponse) {
Map<String, String> data = new HashMap<>();
data.put("form_id", getElementsFormId());
elementsResponse.setTemplateFile("quote-registration", data);
}
@Override
public void handleAction(SymphonyElementsEvent event,
SymphonyMessage elementsResponse) {
Map<String, Object> formValues = event.getFormValues();
Map<String, Object> data = new HashMap<String, Object>();
data.put(FROM_CURRENCY, formValues.get(FROM_CURRENCY));
data.put(TO_CURRENCY, formValues.get(TO_CURRENCY));
data.put(AMOUNT, formValues.get(AMOUNT));
data.put(ASSIGNED_TO, event.getUser().getDisplayName());
elementsResponse.setTemplateMessage(
"Quote FX {{fromCurrency}}-{{toCurrency}} {{amount}} sent to dealer {{assignedTo}}", data);
}

Sample Handlebars-based template for the quote registration form:

<form id="{{form_id}}">
<h3>Quote Registration</h3>
<h6>From currency</h6>
<text-field minlength="3" maxlength="3" masked="false" name="fromCurrency" required="true"></text-field>
<h6>To currency</h6>
<text-field minlength="3" maxlength="3" masked="false" name="toCurrency" required="true"></text-field>
<h6>Amount</h6>
<text-field minlength="1" maxlength="9" masked="false" name="amount" required="true"></text-field>
<h6>Assigned To:</h6>
<person-selector name="assignedTo" placeholder="Assign to.." required="false" />
<h6>Quote Status:</h6>
<radio name="status" checked="true" value="pending">Pending</radio>
<radio name="status" checked="false" value="confirmed">Confirmed</radio>
<radio name="status" checked="false" value="settled">Settled</radio>
<h6>Remarks:</h6>
<textarea name="remarks" placeholder="Enter your remarks.." required="false"></textarea>
<button name="confirm" type="action">Confirm</button>
<button type="reset">Reset</button>
</form>

Notice: The event.getFormValues() returns a map with the values of all input fields in the Symphony Elements form. The key names of that map match the HTML name property of the input elements in Elements form. Also, the id property of the form must match the value returned by getElementsFormId.

ElementsActionHandler

For scenarios where the Symphony Elements form is not generated through a command targeted to your bot (e.g. a user interacting with an extension app, a notification from external system) but you need to handle the interactions with that form, extend the ElementsActionHandler class rather than ElementsHandler.

ElementsActionHandler is actually an EventHandler and therefore is simpler and easier to extend than ElementsHandler. It just requires implementing the getElementsFormId and handle methods.

Receiving Notifications

Receiving notifications from external systems directly into Symphony chats is another common use case for bots and Symphony Bot SDK delivers all the support you need by:

  1. Exposing the /notification endpoint through which external systems can send their events: http(s)://<hostname>:<port>/<application_context>/notification

  2. Offering mechanisms for you to register your own logic to process incoming notification requests through the NotificationInterceptor class, including discarding requests when applicable

  3. Sending notification contents to Symphony rooms

  4. Protecting the /notification endpoint

For a tutorial on how to receive notifications using the BDK, continue here:

Processing incoming requests

Incoming requests can be easily processed by extending the NotificationInterceptor class. The way Symphony Bot SDK supports processing notifications mimics the Filter idea of the Java Servlet specification, that is, you can chain multiple NotificationInterceptor classes together each one tackling a different aspect of the request, but all of them collaborating to either process or discard the request.

Extending NotificationInterceptor class will automatically register your interceptor to the SDK internal InterceptorChain.

To create your own NotificationInterceptor you simply need to implement the following method:

  • process(NotificationRequest notificationRequest, SymphonyMessage notificationMessage): Where you add your business logic to process incoming requests (e.g. HTTP header verification, JSON payload mapping, etc). Use the NotificationRequest to retrieve all details of the notification request (e.g. headers, payload, identifier). You can also use its getAttribute/setAttribute methods to exchange data among your interceptors. Use the SymphonyMessage object to format the notification. The SDK will take care of delivering the response to the correct Symphony room. This method is automatically called for each notification request. Return false if the request should be discarded, true otherwise.

@Override
public boolean process(NotificationRequest notificationRequest, SymphonyMessage notificationMessage) {
// For simplicity of this sample code identifier == streamId
String streamId = notificationRequest.getIdentifier();
if (streamId != null) {
notificationRequest.setStreamId(streamId);
notificationMessage.setMessage(
"<b>Notification received:</b><br />" + notificationRequest.getPayload());
return true; // true if notification interception chain should continue
}
return false; // false if notification intercept chain should be halted and request rejected
}

Controlling interceptors order

If you need to specify multiple request interceptors and want to control their execution order, extend the OrderedNotificationInterceptor rather than NotificationInterceptor and implement the getOrder() method.

Forwarding notifications to rooms

The notification support offered by the SDK uses an extra path parameter (that is, /notification/<some_value>) to identify which room a particular notification should be sent to. That extra parameter is internally called as 'identifier' and can be retrieved from the NotificationRequest object.

By default the SDK assumes the 'identifier' is the stream ID of the room. If for your scenario 'identifier' means something else or you have a completely different mechanism to identify the room, you must set the stream ID in NotificationRequest manually.

Protecting notifications endpoint

Notification endpoint is public by default. Nevertheless Symphony Bot SDK has a built-in IP whitelisting mechanism that could be easily set up to allow only specific IP addresses or IP ranges to have access that endpoint.

You can enable and configure that mechanism by adding the following in application.yaml file:

access-control:
ipWhitelist: <comma-separated IP list>
urlMapping: "/notification"

Disabling notifications endpoint

In case your application does not need to handle notifications coming from external systems, it is strongly recommended that you disable the endpoint as it is public by default.

You can disable the notification endpoint by setting the following in application.yamlfile:

notification:
endpoint:
disabled: true

Sending messages

The SymphonyMessage object holds the details for a message to be sent to Symphony. It offers the following different ways to specify the message content:

  • void setMessage(String message): specifies a static message to be displayed in a Symphony room.

  • void setTemplateMessage(String templateMessage, Object templateData): automatically interpolates a string with template wildcards using the given data object.

  • void setTemplateFile(String templateFile, Object templateData): automatically loads the specified template file and interpolates its content using the given data object. Template files must be placed in your resources directory under templates.

  • void setEnrichedMessage(String message, String entityName, Object entity, String version): similar to setMessage but offers data to for an extension app to create enriched messages replacing what has been specified as message. If no extension app is registered, the message gets displayed.

  • void setEnrichedTemplateMessage(String templateMessage, Object templateData, String entityName, Object entity, String version): similar to setTemplateMessage but offers data to for an extension app to create enriched messages replacing what has been specified as templateMessage. If no extension app is registered, the interpolated templateMessage gets displayed.

  • void setEnrichedTemplateFile(String templateFile, Object templateData, String entityName, Object entity, String version): similar to setTemplateFile but offers data to for an extension app to create enriched messages replacing what has been specified as templateFile. If no extension app is registered, the interpolated content of templateFile gets displayed.

Symphony Bot SDK is shipped with Handlebars template engine and automatically handles the template processing for you.

Using Symphony standard templates

Symphony Bot SDK integrates seamlessly with SmsRenderer tool to offer predefined message templates.

The file-based methods in SymphonyMessage (setTemplateFile and setEnrichedTemplateFile) can be used to render such templates. For that, you just need to specify the predefined template from SmsRenderer.SmsTypes enum:

public class TemplateSampleHandler extends CommandHandler {
@Override
protected Predicate<String> getCommandMatcher() {
...
}
@Override
public void handle(BotCommand command, SymphonyMessage commandResponse) {
Map<String, Object> commandParameter =
jsonMapper.toObject("{\"message\": {\"title\": \"Title\", \"content\": \"Content\"}}", Map.class);
commandResponse.setTemplateFile(SmsRenderer.SmsTypes.ALERT.getName(), commandParameter);
}
}

Currently, Symphony Bot SDK offers the following templates:

  • ALERT

  • INFORMATION

  • LIST

  • NOTIFICATION

  • SIMPLE

  • TABLE

For more information about the Symphony standard templates, take a look on https://github.com/SymphonyPlatformSolutions/sms-sdk-renderer-java. Also, check Template command section.

Extension applications

In addition to all support for bots development, Symphony Bot SDK also comes with great tools to streamline the Symphony-extension apps integration process.

Extension app authentication

The extension app authentication process spawns three steps which aim to establish a bidirectional trust between an application and Symphony.

Symphony Bot SDK removes all the complexity related to the authentication process by exposing the following endpoints through which an application can authenticate itself:

Method

Endpoint

Description

Request

Response

POST

/application/authenticate

Initiates the authentication process. Extension app sends its application ID, retrieved from Symphony Client APIs, which must match the same ID configured in Symphony Admin portal. An application token is returned to the extension app.

{"appId": "myAppId"}

{"appId": "myAppId", "appToken": "bde...e"}

POST

/application/tokens/validate

Validates the application and Symphony tokens generated in previous step. Extension app provides the Symphony token, obtained through Symphony Client APIs.

{"appToken":"05b...c", "symphonyToken":"0...6", "appId":"myAppId"}

200 OK if tokens valid, 401 Unauthorized otherwise

POST

/application/jwt/validate

Validates a signed JWT holding user details

{"jwt":"ey...g"}

the user ID

Extension apps must rely on those three endpoints in the order they are described to get authenticated to Symphony. If any of those steps fails, the authentication fails and extension app will not be launched (i.e. not displayed on the left-nav menu).

Exposing new endpoints

The web support in Symphony Bot SDK is based on SpringMVC framework. So exposing endpoints for your extension apps requires:

  • Annotating your classes with @Controller or @RestController

  • Mapping your routes using @RequestMapping, @GetMapping, @PostMapping, etc

@RestController
@RequestMapping("/secure/myendpoint")
public class MyController {
@GetMapping
public ResponseEntity getSomeData() {
// ... logic to retrieve data
return ResponseEntity.ok(new Object());
}
@PostMapping
public ResponseEntity setSomeData(@RequestBody String data) {
// ... logic to set data
return ResponseEntity.ok();
}
}

Protecting endpoints

When exposing endpoints for extension apps you will likely need to restrict access to them.

Symphony Bot SDK offers a simple way for you to protect endpoints so that only your applications would have access to them. All endpoints exposed under /secure/ path are automatically protected.

To access them, Symphony Bot SDK requires requests to have the HTTP authorization header set with a valid JWT:

Authorization: Bearer eyJ...ybxRg

It is possible to configure a different value for the secure path. In bot-config.json change the following property:

"authenticationFilterUrlPattern": "/secure/",

Be sure to reflect your change to all of your controllers.

Symphony clients

Typically extension apps deliver features that involve retrieving/persisting data from/to Symphony. Symphony Bot SDK provides the building blocks for such features, the Symphony clients.

The following clients are available:

  • MessageClient: offers ways to send messages to Symphony rooms

  • StreamsClient: retrieves streams and rooms details and manages rooms

  • UsersClient: retrieves user details

private final StreamsClient streamsClient;
public StreamsController(StreamsClient streamsClient) {
this.streamsClient = streamsClient;
}
@GetMapping
public ResponseEntity<List<SymphonyStream>> getUserStreams() {
try {
return ResponseEntity.ok(streamsClient.getUserStreams(null, true));
} catch (SymphonyClientException sce) {
// ... handling communication failure with Symphony
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

Note: For any communication issue with Symphony a SymphonyClientException is raised. Handle that exception properly to improve user experience.

Serving the extension app

Static assets of an extension app (e.g. Javascript, CSS, images, HTML) can be served either in a separated host or along with the bundle you generate for your Symphony Bot SDK.

If you plan to have a different server for you web UI, make sure CORS is properly configured. In application.yaml add the following properties:

cors:
allowed-origin: "<web UI domain>"
url-mapping: "/**"

To distribute the extension app as part of your Symphony Bot SDK based application, place all of you static assets under: <symphony bot application base path>/src/main/resources/public and build your application. In this case, all of your assets will be under /app/ path. Example, my_image.png file placed under public directory would be accessible in the following URL:

**Notice:** When registering your extension apps in Symphony Admin portal, make sure you take the ```/app/``` into account when setting the load URL.
### Testing your app
Symphony Bot SDK ships with few endpoints to assist you on understanding how to leverage Symphony Bot SDK to create your own extension apps. All endpoints are protected and require extension app to be authenticated.
Please refer to following sub-sections for more details.
#### Extension app log endpoint
| Method | URL | Description
|---|---|---|
| POST | /secure/log | Persists extension apps logs along with server-side logs. Set ```level``` request parameter to change log level
**Request**
```javascript
"a log message from frontend"

Automatic endpoint documentation

Symphony Bot SDK is shipped with Swagger already configured. Documentation for all existing endpoints and new ones that you may add can be found here:

http(s)://<hostname>:<port>/<application_context>/swagger-ui.html

Real-Time events

With Symphony Bot SDK your extension apps can quickly leverage real-time events. Based on the Server-Sent Event (SSE) technology, Symphony Bot SDK delivers real-time events support through the following components:

  • SseController which exposes the endpoint through which extension applications subscribe for real-time events

  • SsePublisher, a base class to publish real-time events

  • SseSubscriber, an abstraction of clients subscribing for events

Publishing events

SsePublisher child classes represent the bridge between your business logic and the client applications listening to your events. Create as many publishers as you need according to the event types they should publish.

The SsePublisher class is parameterized to allow you to provide any kind of data in publishEvent as long as you implement the SsePublishable interface.

To extend SsePublisher implement the following methods:

  • List<String> getEventTypes(): returns a list with event types that this particular publisher is responsible for. Clients must specify the event types they want to listen to in their requests path.

  • void handleEvent(SseSubscriber subscriber, SsePublishable event): where you add your logic to process events before publishing them. This method is not publicly visible and your business logic will not call it directly. Rather, your code should call publishEvent whenever you need to publish an event. Symphony Bot SDK will automatically call handleEvent for each subscriber of that particular event type. Use the SseSubscriber object to retrieve details of the clients subscribing for events and to send them your events.

Optionally, you may consider extending the following methods:

  • void init(): invoked right after Symphony Bot SDK instantiates your class. Useful for initialization logic.

  • void onSubscriberAdded(SubscriptionEvent subscriberAddedEvent): called when new subscriber registers for event types handled by that particular publisher.

  • void onSubscriberRemoved(SubscriptionEvent subscriberRemovedEvent): called when subscriber unregisters for event types handled by that particular publisher.

Note: DO NOT block the thread in onSubscriberAdded and onSubscriberRemoved methods.

private static final long WAIT_INTERVAL = 1000L;
private boolean running = false;
private int subscribers;
@Override
public List<String> getEventTypes() {
return Stream.of("event1", "event2")
.collect(Collectors.toList());
}
@Override
protected void handleEvent(SseSubscriber subscriber, SsePublishable event) {
// For simplicity, just send the event to the client application. In real
// scenarios you could rely on subscriber.getMetadata to check if client is
// really interested in this particular event.
subscriber.sendEvent(event);
}
@Override
protected void onSubscriberAdded(SubscriptionEvent subscriberAddedEvent) {
// Start simulating event generation on first subscription
subscribers++;
if (!running) {
running = true;
simulateEvent();
}
}
@Override
protected void onSubscriberRemoved(SubscriptionEvent subscriberRemovedEvent) {
subscribers--;
// Stop simulation if no more subscriber
if (subscribers == 0) {
running = false;
}
}
private void simulateEvent() {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
int id = 0;
while (running) {
id++;
// Create sse publishable event with payload
SimpleEvent event = new SimpleEvent();
event.setId(Integer.toString(id));
event.setPayload("SSE Test Event - " + LocalTime.now().toString());
// Simulate event alternation
event.setType((id % 2) != 0 ? "event1" : "event2");
// Publish event
this.publishEvent(event);
waitForEvents(WAIT_INTERVAL);
}
});
}
private void waitForEvents(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException ie) {
LOGGER.debug("Error waiting for next events");
}
}
@Data
@NoArgsConstructor
public class SimpleEvent implements SsePublishable {
private String payload;
private String type;
private String id;
}

Notice: SsePublisher exposes the publishEvent method which must be called by your event generation logic to get events properly published to clients. You may rely on complete or completeWithError methods to properly tell client applications your publisher is done and will send no more events.

Subscribing to event types

Method

URL

Description

GET

/secure/events/<comma separated event types>

Subscribe to the specified event types. Use query params to add metadata such as filtering criteria.

Client sample

// Listening to stock price updates
const evtSource = new EventSource("http://localhost:8080/botapp/secure/events/stockprice");

Event stream mapping

If you have multiple SsePublisher generating events of different nature (e.g. stock prices and currencies exchange rates), you can name the event types so that subscribers are properly served by the corresponding publishers.

Symphony Bot SDK automatically maps clients requests to the corresponding publishers based on the event types present in the request path (e.g. /stockprice, /x-rate). Event types in client requests must match the ones registered by publishers through getEventTypes() method.

Client sample

// Listening to stock price updates only
const evtSource = new EventSource("http://localhost:8080/botapp/secure/events/stockprice");
// Listening to stock price and x-rate updates
const evtSource2 = new EventSource("http://localhost:8080/botapp/secure/events/stockprice,x-rate");

Filtering events

Real-time events may be filtered based on some criteria. All query parameters in a subscription request are handled by Symphony Bot SDK as metadata and forwarded to publishers through SseSubscriber object.

// Listening to Tesla stock price updates
const evtSource = new EventSource("http://localhost:8080/botapp/secure/events/stockprice?name=TSLA");

The publisher then checks if there are any specified filters:

@Override
protected void handleEvent(SseSubscriber subscriber, SseEvent event) {
...
Map<String, String> metadata = subscriber.getMetadata();
if (price.getStockName().equals(metadata.get("filterByName")) {
// publish SSE event
}
...
}

Monitoring Tools

Symphony Bot SDK comes with production-ready features to help you monitoring your applications when deployed to production.

Such features consist of the following HTTP endpoints that you can use to pull health and other metrics to check the status of your application:

  • /monitor/info: simple endpoint that returns HTTP 200 OK when application is up

  • /monitor/health: provides health details. By default, only Symphony-related health details are exposed

  • /monitor/prometheus: health and metrics details to be consumed by Prometheus

Extending health details

The Symphony Bot SDK monitoring system is based on Spring Actuators. By default, it exposes the following health metrics:

  • overall system health status: represented by the top status field. It shows 'UP' if all other metrics are fine, that is, 'UP'.

  • symphony: symphony components metrics. It shows 'UP' only if your bot is properly communicating with the POD and agent and all Symphony components (e.g. agent, Key Manager, POD) are accessible.

{
"status":"UP",
"details":{
"symphony":{
"status":"UP",
"details":{
"Symphony":{
"agentConnection":"UP",
"podConnection":"UP",
"agentToPodConnection":"UP",
"agentToKMConnection":"UP",
"podVersion":"1.55.3",
"agentVersion":"2.55.9",
"agentToPodConnectionError":"N/A",
"agentToKMConnectionError":"N/A",
"symphonyApiClientVersion":"1.0.49"
}
}
}
}
}

There are many other built-in metrics in Spring Actuator. Please refer to their documentation for enabling those metrics.

To create your own custom metric you need to implement the Spring Actuator HealthIndicator interface and use Health builder to convey your status. Your metrics are automatically integrated with the system ones in the monitoring endpoint: http(s)://<hostname>:<port>/<application_context>/monitor/health.

public class InternetConnectivityHealthIndicator implements HealthIndicator {
private RestClient restClient;
public InternetConnectivityHealthIndicator(RestClient restClient) {
this.restClient = restClient;
}
@Override
public Health health() {
try {
restClient.getRequest("https://symphony.com", String.class);
return Health.up().withDetail("connectivity", "UP").build();
} catch (Exception e) {
return Health.down().withDetail("connectivity", "DOWN").build();
}
}
}

Extending Prometheus details

Spring Actuator exposes default metrics in Prometheus endpoint. Symphony Bot SDK extends them to also include the communication status of Symphony-related components (e.g. agent, Key Manager, POD).

To expose your own custom details in Prometheus endpoint, you need to implement MeterBinder interface as follow:

public class SymphonyHealthMeterBinder implements MeterBinder {
...
private HealthCheckInfo status() {
return healthcheckClient.healthCheck();
}
@Override
public void bindTo(MeterRegistry registry) {
LOGGER.info("Registering Symphony health status to Prometheus endpoint");
HealthCheckInfo healthStatus = status();
Gauge.builder(METRIC_NAME, this, value -> value.status().checkOverallStatus() ? 1.0 : 0.0)
.description(METRIC_DESCRIPTION)
.tags(Tags.of(
Tag.of(TAG_POD_VERSION, healthStatus.getPodVersion()),
Tag.of(TAG_AGENT_VERSION, healthStatus.getAgentVersion()),
Tag.of(TAG_API_VERSION, healthStatus.getSymphonyApiClientVersion())))
.baseUnit(BASE_UNIT)
.register(registry);
}

Advanced settings

Custom truststore

If SSL connection to any endpoint uses private or self-signed certificates, add the following properties to the bot-config.json to tell the SDK which truststore to use:

"truststorePath": "/path/to/truststore/",
"truststorePassword": "truststore password",

Proxy support

In case connection to Symphony components (e.g. POD, agent, key manager) requires going through proxies, the following properties can be set in bot-config.json:

// If only POD access requires proxy. Username/password only required
// if proxy uses basic authentication
"podProxyURL": "proxy url",
"podProxyUsername": "username",
"podProxyPassword": "password",
// If access to both POD and agent requires proxy
"proxyURL": "proxy url",
"proxyUsername": "username",
"proxyPassword": "password",
// If access to key manager requires proxy
"keyManagerProxyURL": "proxy url",
"keyManagerProxyUsername": "username",
"keyManagerProxyPassword: "password",

For connections to external systems using the REST client shipped with Symphony Bot SDK define the following properties in application.yaml:

restclient:
proxy:
address: "proxy hostname"
port: "proxy port"

Logging

To change log file name and/or location, or to change log level for different packages, set the following properties in application.yaml.

logging:
file: /home/myuser/symphony-bot-sdk-java/logs/bot-app.log
level:
ROOT: INFO
com.symphony.bdk.bot.sdk: DEBUG

Access control

To protect endpoints using basic authentication and/or IP whitelist, specify the following in application.yaml:

access-control:
name: myusername
hashedPassword: 5e88489...21d1542d8
salt: 11111
ipWhitelist: 192.168.0.155, 192.168.2.178
urlMapping: /monitor

Note: The basic authentication protection is pretty simple allowing only one username/password to be specified.

CORS

If extension app is running in different host, rely on the CORS support to make Symphony Bot SDK to accept requests coming from the extension app. In application.yaml add:

cors:
allowed-origin: myextensionapp.com
url-mapping: /**

Cross-site scripting

Protect endpoint from XSS attacks by setting the following properties in application.yaml:

xss:
url-mapping: /secure/*

Request origin header

It is also possible to protect endpoints based on shared secrets. This is particularly useful when your application needs to receive notifications from external systems and therefore needs to expose a publicly available endpoint.

Both the external system and your application could share a secret through some secure channel. On every notification sent, the external system would add that secret in a HTTP header and Symphony Bot SDK would automatically reject requests without it.

In application.yaml add the following:

request-origin:
origin-header: x-origin-token
url-mapping: /notification

Rate limit

It is possible to limit access to your application using the Symphony Bot SDK's throttling mechanism. You need to specify the limit and one of the throttling modes:

  • ORIGIN: limits request rate based on origin IP address

  • ENDPOINT: limits request rate per endpoints exposed by your application (default if not specified)

Note: When an application is running behind load balancers or firewalls, the calling IP address may be rewritten. Usually such network components keep the original IP address in HTTP headers. Symphony Bot SDK looks for the following headers when throttling in ORIGIN mode:

  • X-Forwarded-For

  • Proxy-Client-IP

  • WL-Proxy-Client-IP

  • HTTP_X_FORWARDED_FOR

  • HTTP_X_FORWARDED

  • HTTP_X_CLUSTER_CLIENT_IP

  • HTTP_CLIENT_IP

  • HTTP_FORWARDED_FOR

  • HTTP_FORWARDED

  • HTTP_VIA

  • REMOTE_ADDR

Example: specify that the same IP address can issue at most 200 concurrent requests per second

throttling:
limit: 200
mode: ORIGIN
timeout: 10000

The timeout property is used to define the maximum amount of time a request waits in throttling mechanism before it is processed. If that time exceeds, a HTTP 408 error is returned to the caller.

Settings reference

Property

Description

Configuration file

truststorePath

The truststore path

bot-config.json

truststorePassword

The truststore password

bot-config.json

podProxyURL

The pod proxy URL

bot-config.json

podProxyUsername

The pod proxy username

bot-config.json

podProxyPassword

The pod proxy password

bot-config.json

proxyURL

The agent and pod proxy URL

bot-config.json

proxyUsername

The agent and pod proxy username

bot-config.json

proxyPassword

The agent and pod proxy password

bot-config.json

keyManagerProxyURL

The key manager proxy URL

bot-config.json

keyManagerProxyUsername

The key manager proxy username

bot-config.json

keyManagerProxyPassword

The key manager proxy password

bot-config.json

server.port

Port to be used by application (e.g. 8080)

application.yaml

server.servlet.context-path

Application context path (e.g. /botapp)

application.yaml

certs

Path to the directory containing bot private key

application.yaml

logging.file

The log file path (including file name)

application.yaml

logging.level.<package>

The log level for the given package (e.g. DEBUG, INFO, WARN, ERROR)

application.yaml

access-control.name

The username for basic authentication

application.yaml

access-control.hashedPassword

The salted hashed password for basic authentication

application.yaml

access-control.salt

Salt used when hashing password

application.yaml

access-control.ipWhitelist

The IP whitelist set

application.yaml

access-control.urlMapping

The endpoints protected by either basic authentication or IP whitelist

application.yaml

concurrency.bot.pool.core-size

The bot concurrency pools coreSize

application.yaml

concurrency.bot.pool.max-size

The bot concurrency pools max size

application.yaml

concurrency.bot.pool.queue-capacity

The bot concurrency pools queue capacity

application.yaml

concurrency.bot.pool.thread-name-prefix

The bot concurrency pools thread name prefix

application.yaml

concurrency.sse.pool.core-size

The SSE concurrency pools coreSize

application.yaml

concurrency.sse.pool.max-size

The SSE concurrency pools max size

application.yaml

concurrency.sse.pool.queue-capacity

The SSE concurrency pools queue capacity (if 0 returns immediately if no thread available)

application.yaml

concurrency.sse.pool.thread-name-prefix

The SSE concurrency pools thread name prefix

application.yaml

concurrency.sse.subscriber.queue-capacity

Capacity of SSE subscriber queue. Defines the maximum number of concurrent publishers writing to the queue

application.yaml

concurrency.sse.subscriber.queue-timeout

How long a subscriber will wait for events before sending a keep-alive

application.yaml

cors.allowed-origin

The allowed origin domain

application.yaml

cors.url-mapping

The endpoints which CORS support should be applied to

application.yaml

xss.url-mapping

The endpoints which cross-site scripting protection should be applied to

application.yaml

request-origin.origin-header

The HTTP header used to convey the request origin secret

application.yaml

request-origin.url-mapping

The endpoints for which origin header should be verified

application.yaml

restclient.proxy.address

The rest client proxy address

application.yaml

restclient.proxy.port

The rest client port

application.yaml

restclient.timeout

The rest client timeout

application.yaml

throttling.limit

Limits the number of requests per second

application.yaml

throttling.mode

Throttling modes: ORIGIN - throttle based on IP or ENDPOINT - throttle based on target endpoint. If not specified, default to ENDPOINT

application.yaml

throttling.timeout

Maximum amount of time a request waits before a HTTP 408 is returned to client

application.yaml

features.commandFeedback

The command feedback enablement

application.yaml

features.transactionIdOnError

The transaction id on error enablement

application.yaml

features.eventUnexpectedErrorMessage

The message for unexpected errors on events

application.yaml

features.notificationBaseUrl

The notification base URL

application.yaml

features.isPublicRoomAllowed

The enablement for allowing bot addition to public rooms

application.yaml

features.publicRoomNotAllowedMessage

The message when adding bots to public rooms is not allowed

application.yaml

features.publicRoomNotAllowedTemplate

The template to be used when adding bots to public rooms is not allowed

application.yaml

features.publicRoomNotAllowedTemplateMap

The parameter of the template to be used when adding bots to public rooms is not allowed

application.yaml