Skip to the content.

BDK Core Spring Boot Starter

The Symphony BDK for Java provides a Starter module that aims to ease bot developments within a Spring Boot application.

Features

Installation

The following listing shows the pom.xml file that has to be created when using Maven:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>bdk-core-spring-boot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>bdk-core-spring-boot</name>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.finos.symphony.bdk</groupId>
                <artifactId>symphony-bdk-bom</artifactId>
                <version>1.3.2.BETA</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.finos.symphony.bdk</groupId>
            <artifactId>symphony-bdk-core-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>2.3.4.RELEASE</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

The following listing shows the build.gradle file that has to be created when using Gradle:

plugins {
    id 'java-library'
    id 'org.springframework.boot' version '2.3.4.RELEASE'
}

dependencies {
    implementation platform('org.finos.symphony.bdk:symphony-bdk-bom:2.0.0')
    
    implementation 'org.finos.symphony.bdk:symphony-bdk-core-spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter'
}

Create a Simple Bot Application

As a first step, you have to initialize your bot environment through the Spring Boot src/main/resources/application.yaml file:

bdk:
    host: acme.symphony.com
    bot:
      username: bot-username
      privateKey:
        path: /path/to/rsa/privatekey.pem
      
logging:
  level:
    com.symphony: debug # in development mode, it is strongly recommended to set the BDK logging level at DEBUG

You can notice here that the bdk property inherits from the BdkConfig class.

As required by Spring Boot, you have to create an src/main/java/com/example/bot/BotApplication.java class:

@SpringBootApplication
public class BotApplication {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Now you can create a component for a simple bot application, as the following listing (from src/main/java/com/example/bot/HelloBot.java) shows:

@Component
public class HelloBot {

  @Autowired
  private MessageService messageService;

  @EventListener
  public void onMessageSent(RealTimeEvent<V4MessageSent> event) {
    this.messageService.send(event.getSource().getMessage().getStream(), "<messageML>Hello!</messageML>");
  }
}

You can finally run your Spring Boot application and verify that your bot always replies with Hello!.

OBO (On behalf of) usecases

It is possible to run an application with no bot service account configured in order to accommodate OBO usecases only. For instance the following configuration is valid:

bdk:
    host: acme.symphony.com
    app:
      appId: app-id
      privateKey:
        path: /path/to/rsa/privatekey.pem

This will cause all features related to the datafeed loop such as Real Time Events, activities, slash commands, etc. to be deactivated. However, service beans with OBO-enabled endpoints will be available and can be used as following:

@Component
public class OboUsecase {

  @Autowired
  private MessageService messageService;

  @Autowired
  private OboAuthenticator oboAuthenticator;

  public void doStuff() {
      final AuthSession oboSession = oboAuthenticator.authenticateByUsername("user.name");
      final V4Message message = messageService.obo(oboSession).send("stream.id", "Hello from OBO"); // works

      messageService.send("stream.id", "Hello world"); // fails with an IllegalStateException
  }
}

Any attempt to use a non-OBO service endpoint will fail with an IllegalStateException.

Subscribe to Real Time Events

The Core Starter uses Spring Events to deliver Real Time Events.

You can subscribe to any Real Time Event from anywhere in your application by creating a handler method that has to respect two conditions:

The listener methods will be called with events from the datafeed loop or the datahose loop (or both) depending on your configuration:

bdk:
    datafeed:
        enabled: true # optional, defaults to true
    datahose:
        enabled: true # optional, defaults to false

If both datafeed and datahose are enabled, application will fail at startup. So please make sure datafeed is disabled when using datahose.

Here’s the list of Real Time Events you can subscribe:

@Component
public class RealTimeEvents {

  @EventListener
  public void onMessageSent(RealTimeEvent<V4MessageSent> event) {}

  @EventListener
  public void onSharedPost(RealTimeEvent<V4SharedPost> event) {}

  @EventListener
  public void onInstantMessageCreated(RealTimeEvent<V4InstantMessageCreated> event) {}

  @EventListener
  public void onRoomCreated(RealTimeEvent<V4RoomCreated> event) {}

  @EventListener
  public void onRoomUpdated(RealTimeEvent<V4RoomUpdated> event) {}

  @EventListener
  public void onRoomDeactivated(RealTimeEvent<V4RoomDeactivated> event) {}

  @EventListener
  public void onRoomReactivated(RealTimeEvent<V4RoomReactivated> event) {}

  @EventListener
  public void onUserRequestedToJoinRoom(RealTimeEvent<V4UserRequestedToJoinRoom> event) {}

  @EventListener
  public void onUserJoinedRoom(RealTimeEvent<V4UserJoinedRoom> event) {}

  @EventListener
  public void onUserLeftRoom(RealTimeEvent<V4UserLeftRoom> event) {}

  @EventListener
  public void onRoomMemberPromotedToOwner(RealTimeEvent<V4RoomMemberPromotedToOwner> event) {}

  @EventListener
  public void onRoomMemberDemotedFromOwner(RealTimeEvent<V4RoomMemberDemotedFromOwner> event) {}

  @EventListener
  public void onConnectionRequested(RealTimeEvent<V4ConnectionRequested> event) {}

  @EventListener
  public void onConnectionAccepted(RealTimeEvent<V4ConnectionAccepted> event) {}

  @EventListener
  public void onMessageSuppressed(RealTimeEvent<V4MessageSuppressed> event) {}

  @EventListener
  public void onSymphonyElementsAction(RealTimeEvent<V4SymphonyElementsAction> event) {}
}

By default, the RealTimeEvents are going to be processed asynchronously in the listeners, in case this is not the preferred behavior, one can deactivate it by updating the application.yaml file as

bdk:
    datafeed:
        event:
            async: false # optional, defaults to true

The same applies for bdk.datahose configuration.

Inject Services

The Core Starter injects services within the Spring application context:

@Service
public class CoreServices {
    
    @Autowired
    private MessageService messageService;
    
    @Autowired
    private StreamService streamService;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private DatafeedService datafeedService;
    
    @Autowired
    private SessionService sessionService;
    
    @Autowired
    private ActivityRegistry activityRegistry;
}

Unlike subscribing to real time events, using slash commands or activities, solely injecting services does not require the datafeed loop to run. If you want to disable the datafeed loop, you can update your application.yaml file as follows:

bdk:
    datafeed:
        enabled: false

:warning: Disabling the datafeed loop will prevent the use of real time event listeners, of slash commands and activities.

Slash Command

You can easily register a slash command using the @Slash annotation. Note that the CommandContext is mandatory to successfully register your command. If not defined, a warn message will appear in your application log. Note also that only beans with scope singleton will be scanned.

@Component
public class SlashHello {

  @Slash("/hello")
  public void onHello(CommandContext commandContext) {
    log.info("On /hello command");
  }

  @Slash(value = "/hello", mentionBot = false)
  public void onHelloNoMention(CommandContext commandContext) {
    log.info("On /hello command (bot has not been mentioned)");
  }
}

By default, the @Slash annotation is configured to require bot mention in order to trigger the command. You can override this value using @Slash#mentionBot annotation parameter.

You can also use slash commands with arguments. To do so, the field value of the @Slash annotation must have a valid format as explained in the Activity API section. If the slash command pattern is valid, you will have to specify all slash arguments as method parameter with the same name and type. If slash command pattern or method signature is incorrect, a warn message will appear in your application log and the slash command will not be registered.

For instance:

@Component
public class SlashHello {

  @Slash("/hello {arg") // will not be registered: invalid pattern
  public void onHelloInvalidPattern(CommandContext commandContext, String arg) {
    log.info("On /hello command");
  }

  @Slash("/hello {arg1}{arg2}") // will not be registered: invalid pattern
  public void onHelloInvalidPatternTwoArgs(CommandContext commandContext, String arg1, String arg2) {
    log.info("On /hello command");
  }

  @Slash("/hello {arg1} {arg2}") // will be registered: valid pattern and valid signature
  public void onHelloValidPatternTwoArgs(CommandContext commandContext, String arg1, String arg2) {
    log.info("On /hello command");
  }

  @Slash("/hello {arg1} {arg2}") // will not be registered: valid pattern but missing argument
  public void onHelloValidPatternTwoArgs(CommandContext commandContext, String arg1) {
    log.info("On /hello command");
  }

  @Slash("/hello {arg1} {@arg2}") // will not be registered: valid pattern but mismatching type for arg2
  public void onHelloValidPatternTwoArgs(CommandContext commandContext, String arg1, String arg2) {
    log.info("On /hello command");
  }

  @Slash("/hello {arg1} {@arg2} {#arg3} {$arg4}") // will be registered: valid pattern and correct signature
  public void onHelloValidPatternTwoArgs(CommandContext commandContext, String arg1, Mention arg2, Hashtag arg3, Cashtag arg4) {
    log.info("On /hello command");
  }
}

:information_source: Slash commands are not registered to the datahose loop even when enabled.

Activities

For more details about activities, please read the Activity API reference documentation

Any service or component class that extends FormReplyActivity or CommandActivity will be automatically registered within the ActivityRegistry.

:information_source: Activities are not registered to the datahose loop even when enabled.

Example of a CommandActivity in Spring Boot

The following example has been described in section Activity API documentation. Note here that with Spring Boot you simply have to annotate your CommandActivity class with @Component to make it automatically registered in the ActivityRegistry,

@Slf4j
@Component
public class HelloCommandActivity extends CommandActivity<CommandContext> {

  @Override
  protected ActivityMatcher<CommandContext> matcher() {
    return c -> c.getTextContent().contains("hello");
  }

  @Override
  protected void onActivity(CommandContext context) {
    log.info("Hello command triggered by user {}", context.getInitiator().getUser().getDisplayName());
  }

  @Override
  protected ActivityInfo info() {
    return new ActivityInfo().type(ActivityType.COMMAND).name("Hello Command");
  }
}

Example of a FormReplyActivity in Spring Boot

The following example demonstrates how to send an Elements form on @BotMention /gif slash command. The Elements form located in src/main/resources/templates/gif.ftl contains:

<messageML>
    <h2>Gif Generator</h2>
    <form id="gif-category-form">

        <text-field name="category" placeholder="Enter a Gif category..."/>

        <button name="submit" type="action">Submit</button>
        <button type="reset">Reset Data</button>

    </form>
</messageML>
@Slf4j
@Component
public class GifFormActivity extends FormReplyActivity<FormReplyContext> {

  @Autowired
  private MessageService messageService;

  @Slash("/gif")
  public void displayGifForm(CommandContext context) throws TemplateException {
    final Template template = bdk.messages().templates().newTemplateFromClasspath("/templates/gif.ftl");
    this.messageService.send(context.getStreamId(), Message.builder().template(template, Collections.emptyMap()));
  }

  @Override
  public ActivityMatcher<FormReplyContext> matcher() {
    return context -> "gif-category-form".equals(context.getFormId())
        && "submit".equals(context.getFormValue("action"))
        && StringUtils.isNotEmpty(context.getFormValue("category"));
  }

  @Override
  public void onActivity(FormReplyContext context) {
    log.info("Gif category is \"{}\"", context.getFormValue("category"));
  }

  @Override
  protected ActivityInfo info() {
    return new ActivityInfo().type(ActivityType.FORM)
        .name("Gif Display category form command")
        .description("\"Form handler for the Gif Category form\"");
  }
}

Home :house: