Elements – Generate Interactive Messages for a Bot

Use Elements – our out-of-the-box UX component library – to create a bot with interactive messages that look and feel like they belong in Symphony.

Overview

This guide describes how to use Elements and the Symphony bot generator to create a rich workflow for your bot.

You'll create a bot and a simple Elements form, to learn how to use our standard UX component libraries to display messages that enable users to interact graphically, when sending bot messages via the REST API.

Elements is an enhancement of MessageML and the desktop client. New tags in MessageML support pre-designed form elements, letting developers easily incorporate UI-based interactions, such as text fields and buttons, into bot messages. To keep the code samples in this guide as simple as possible, the messageML is constructed manually to produce a Symphony Elements form.

This guide explains:

  1. how to build a bot using the Symphony bot generator
  2. how to send and receive interactive messages with your bot, using Elements

Learn more about Symphony bots.
Learn more about Symphony Elements.

Availability & known limitations

Symphony Elements Beta is only available on pods that have been updated to 1.55.3 and running an agent 2.55.9 or above.

Known limitations:

  • Symphony Elements cannot be sent cross-pod.
  • Mobile clients cannot interact with Symphony Elements.
  • Symphony Elements responses are not captured in Content Export.
  • A Bot needs to be in the Room, IM or MIM conversation to be able to read Elements form replies from users.
  • Symphony Elements datafeed payload: the payload structure is subject to change in the next version.

Note: These limitations are temporary – they may change in the future.

Description of the Example Bot

ExpenseBot is our example. Use ExpenseBot to create and submit an expense form using Symphony Elements. Our example's workflow is:

  1. a Symphony user sends a request to ExpenseBot to create a new expense
  2. ExpenseBot sends the user an expense form
  3. the user adds a new expense and submits the expense form
  4. ExpenseBot confirms the updated expense form was received

Language Support

In this version, two language options are provided:

  • Node.js
  • Java

Generating Your Bot

Preparing your Environment
Launching the Generator
Pod Environment Information
Creating your Service Account

Preparing your Environment

The Symphony bot generator is a yeoman generator that can generate bots in various languages for use as a starting point.

To use the Symphony bot generator, install the following developer tools:

Execute the following command to install the Symphony bot generator:

npm install -g generator-symphony --cli

Launching the Generator

You are now ready to generate your project using Yeoman.

$> yo symphony
/------------------------------------------/
/        SYMPHONY GENERATOR  1.0.5         /
/    by [email protected]     /
/ (c) 2018 Symphony Communication Services /
/------------------------------------------/
? What do you want to create => bot
? What is the name of your project => myexpensebot
? What is your POD subdomain => *yourpod*
? What is your preferred programming language => Node.js
? What is the BOT username => MyExpenseBot
? What is the BOT email address => [email protected]
? What is your preferred encryption technology => RSA - Generate New Keys
? Which template do you want to start with => Request/Reply

First Yeoman will ask you to select a Bot or an App. In this example we will select bot.

You will provide a name for your project, in our example: myexpensebot

You then need to provide the pod's subdomain. The generator will prefix this domain onto the string symphony.com. In our example above we use a generic placeholder: yourpod
Note: you should change yourpod to reflect your company’s domain name as registered
with Symphony.

Next you will be asked to choose a language to use. In this guide we document the following languages: Node.js and Java. Select one of these two.

Provide the bot username. In this guide we use MyExpenseBot.

Provide the bot email address. Here, enter a dummy email account (it will not be used). Please note, however, the botEmailAddress value should be set to the same value that is configured for the Service Account. Creating Your Service Account is explained below.

Select the encryption technology to use. Select RSA - Generate New Keys. The public and private RSA Keys are generated and put in the RSA folder by the bot generator. They are used by the bot to authenticate itself to the Symphony pod. You will need the public RSA key when you create your service account, below.

Select the template you will use. To generate the ExpenseBot, choose the Request/Reply template. This template is available in each language.

Pod Environment Information

Symphony bot clients require a JSON configuration file called config.json that contains configuration information. This JSON configuration file is generated by the Symphony bot generator. Its format and contents are the same across all implementation languages.

In the JSON configuration file, you need to change the host url to include your pod's name. You should change the host url to reflect your company’s domain name as registered with Symphony. Also the botEmailAddress value should be set to the same value that you configure for the Service Account (in Creating Your Service Account, below).

The botPrivateKeyName and botPublicKeyName are located in the mybotapp/rsa directory

Open the configuration file and verify it is correct. You can refer to this page to learn more about the configuration file and verify the file generated for your bot is correct.

Creating your Service Account

Your bot will run as a programmatic user in Symphony so you need to create a service account for your bot. This service account represents the bot’s identity when it authenticates with Symphony.

Provision the service account for the bot using the AC Portal:

  1. Log into your pod using an account with admin privileges.
  2. Select Settings (gear located at the top right) then General.
  3. Click on AC Portal (located at the bottom left).
  4. Select Create Account in the left Nav.
  5. Click on the Service Account Tab (shown below).
  1. Enter Username. In this guide we use MyExpenseBot.
  2. Enter Display Name. In this guide we use My Expense Bot.
  3. Provide a dummy email account (it will not be used). It should be the same email address as you entered for the BOT email address when you launched the generator in the section above.
  4. Paste your public RSA key into the Authentication field (shown below). You can find your public key in the myexpensebot/rsa directory generated by the Symphony bot generator, as explained above.

Notice that the public RSA key includes the BEGIN and END lines.

Important! Before leaving this page, make sure you press the Create button
located at the bottom left.

You can now leave the Admin Portal. To exit, click Username (in the top right) and select <Symphony>.

Launching and Testing Your Bot

Launching Your Bot in Node.js

Launching Your Bot in Java

Testing your Bot

Node.js

Installation

This will pull down the symphony node.js library and any of its dependencies.

$> cd <yourpath>/myexpensebot
$> npm install

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN [email protected] No repository field.

added 178 packages from 189 contributors in 6.497s

Launching your Node.js Bot

Note: Please ensure you have completed the Creating Your Service Account section above prior to launching this bot.

From the "myexpensebot" directory type:

$> npm start

--- Symphony Client initialization ---
The BOT creator BOT is ready to serve :-)

Java

Compile and Build

Using your Java IDE, compile and build the Java application.

Note: Please ensure you have completed the Creating Your Service Account section above prior to launching this bot.

Launching your Java Bot

Once compiled, you can run your Java bot using your IDE.

Testing your Bot

Log onto your developer Pod and create a chatroom with your bot. Send a message to the bot. You'll receive a hello message from the bot if it's working correctly.

Programming Your Bot to Use an Elements Form

You're now ready to program ExpenseBot to send and receive messages using Elements, following these steps:

  1. Creating ExpenseBot commands for Symphony Users
  2. Building and Sending the Interactive Form
  3. Processing User Actions on the Elements form

You can view code samples for each of the steps, in Node.js and Java, below.

1. Creating ExpenseBot commands for Symphony Users

You'll add two commands that Symphony users can send to the bot:

  • @MyExpenseBot help : must answer with the help message
  • @MyExpenseBot create expense : must answer by displaying the Expense form. In this step, the bot answers with a message that contains the Expense form.

For the commands to work, you need to process the messages sent to ExpenseBot. You will write code to check if the bot is mentioned in the chat room and if it's mentioned, what command has been sent to the bot. Your code will process the message sent to the bot, so the bot can reply to either of the two commands.

Your code:

  • Retrieves the message’s metadata and identifies bot @mentions
  • obtains the command from the user
  • If the command is “help” or an unknown command, the bot sends the helpMessage to the chat room (or IM or MIM)
  • If command is “create expense”, the bot sends the user the expense form

2. Building and Sending the Interactive Form

Here you will build the interactive expense form using Elements, and have the bot answer with a message that contains the expense form.

The ExpenseBot form contains the following parts:

  • the Expense list
  • Elements text fields to add a new expense
  • an Elements person selector field, to indicate who to submit the expense form to.
    The Expense Form Submission person selector is hidden if there are not yet expenses in the Expense list.
  • an Elements button so the user can submit the expense

All available Elements, including the ones you'll use for the ExpenseBot form, are listed on this page. You'll find detailed descriptions for each.

3. Processing User Actions on the Elements form

Finally, you will add the functionality to process the user's actions when the user is interacting with the form by clicking on the Add Expense button and the Submit button.

You must add a listener to distinguish between an action message and a regular message. Action messages are always sent by the user to the bot, when the user submits the Elements form to the bot.

This action message contains the sender's username, the submitted form's Id and the value for each Element in the form.

Code Samples

Node.js code samples
Java code samples

Node.js code samples

1: Creating ExpenseBot commands for Symphony Users

  • index.js file
// Update 'botHearsSomething' function: Catch message from chat and process
const botHearsSomething = (event, messages) => {
  messages.forEach((message, index) => {
    processMessage(message)
  })
}

/* 'processMessage' function:
 * 1) check if bot is mentioned
 * 2) clean message removing mentions, hashtags and cashtags to obtain commands
 * 3) Send help message if it's "help" or unknown command and call 'sendCreateForm' function if it's "create expense" command 
 */
const processMessage = (message) => {
  const mentions = Symphony.getMentions(message) // Retrieve mentions
  const botIsMentioned = mentions.indexOf(Symphony.getBotUser().id.toString()) !== -1

  if (botIsMentioned) { // 1) check if bot is mentioned
    
    // 2) clean message removing mentions, hashtags and cashtags to obtain commands
    const cleanMessage = Helpers.parseAndCleanMessage(message)

    let sendHelpMessage = false
    if (cleanMessage !== '') {
      
      // 3) Send help message if it's "help" or unknown command and call 'sendCreateForm' function if it's "create expense" command
      switch(cleanMessage.toLowerCase()) {
        case 'help': {
          sendHelpMessage = true
          break
        }
        case 'create expense': {
          sendCreateForm(message)
          break
        }
        default: {
          sendHelpMessage = true
        }
      }
    }

    if (sendHelpMessage) {
      // Use 'helpMessage' function to build help message in MessageML format and send it
      Symphony.sendMessage(message.stream.streamId, helpMessage(), null, Symphony.MESSAGEML_FORMAT)
    }
  }
}

/* 'helpMessage' function */
// Return help message in MessageML format
const helpMessage = () => {
  const botUsername = Symphony.getBotUser().username

  return '<h3>Use ExpenseBot to create and submit an expense form using Symphony Elements</h3>' +
    '<p>Type @' + botUsername + ' <b>\'create expense\'</b> to create an expense approval form</p>' +
    '<p>In order to assign your expense approval form to your manager, you must first add an expense</p>'
}

/* 'sendCreateForm' function */
const sendCreateForm = (message) => {
  // TODO in step 2
}
  • Helpers.js file : in this example we parse and clean the message to obtain the commands. Here we use 'node-html-parser' dependency.
const HTMLParser = require('node-html-parser')

// BEGIN: Helper to parse and clean message

const parseAndCleanMessage = (message) => {
  const dataJSON = JSON.parse(message.data)

  const parsedMessage = HTMLParser.parse(message.message)
  // Remove all identified entities
  const entities = parsedMessage.querySelectorAll('.entity')
  entities.forEach(entity => {
    const entityId = entity.attributes['data-entity-id']
    if (entityId && dataJSON[entityId]) {
      entity.set_content('')
    }
  })

  // trim whitespaces
  return parsedMessage.text.trim()
}

// END: Helper to parse and clean message

const Helpers = {
  parseAndCleanMessage,
}

module.exports = Helpers

2: Building and Sending the Interactive Form

  • index.js file
/* 'sendCreateForm' function:
 * Use 'createFormMessage' to build the expenseForm in MessageML format and send it
 */
const sendCreateForm = (message) => {
  Symphony.sendMessage(message.stream.streamId, createFormMessage(message.user.displayName), null, Symphony.MESSAGEML_FORMAT)
}

/* For this tutorial, we use local variable to store expense form data */
const defaultExpenseFormData = {
  personName: '',
  expenses: [],
  reportTotal: 0.00,
}

let expenseFormData = null

/* 'createFormData' function init the data and use 'buildCompleteMessage' to build the expense form message in MessageML format. */
const createFormMessage = (personName) => {
  expenseFormData = Object.assign({}, defaultExpenseFormData)

  expenseFormData = {
    ...expenseFormData,
    personName,
  }

  return buildCompleteMessage()
}

/* 'buildCompleteMessage' function build the expense form message in MessageML according to expense form data */
const buildCompleteMessage = (errorOnReferals = false) => {
  // Expense list definition
  let message =
    '<h1>Expense form for ' + expenseFormData.personName + '</h1>' +
    '<hr />' +
    '<h3>Expense list :</h3>' +
    '<table>' +
    	'<thead>' +
    		'<tr>' +
    			'<td>Expense Name:</td>' +
      		'<td>Expense Date:</td>' +
    			'<td>Expense Amount:</td>' +
    		'</tr>' +
    	'</thead>' +
    '<tbody>'
  expenseFormData.expenses.forEach((expense) => {
    message +=
      '<tr>' +
      	'<td>' + expense.name + '</td>' +
      	'<td>' + expense.date + '</td>' +
      	'<td>$' + parseFloat(expense.price).toFixed(2) + '</td>' +
      '</tr>'
  })

  message +=
    '</tbody>' +
    '<tfoot>' +
    	'<tr>' +
    		'<td></td>' +
    		'<td></td>' +
    		'<td>Total: $' + parseFloat(expenseFormData.reportTotal).toFixed(2) + '</td>' +
    	'</tr>' +
    '</tfoot>' +
  '</table>'

  // "Adds new expense" form definition using 'text-field' and 'button' Elements
  message +=
    '<br />' +
    '<div style="display: flex;">' +
      '<div style="width: 70%;">' +
        '<h3>New expense :</h3>' +
        '<form id="add-expense-form">' +
          '<text-field name="vendor-textfield" placeholder="Enter a vendor: " required="true" />' +
          '<br />' +
          '<text-field name="date-textfield" placeholder="Enter a Date: " required="true" />' +
          '<br/>' +
          '<text-field name="price-textfield" placeholder="Enter a Price: " required="true" />' +
          '<br/>' +
          '<button type="action" name="add-expense-button">Add Expense</button>' +
        '</form>' +
      '</div>'

  // "Submit expense form" definition and manage referal choice using 'person-selector' Element
  if (expenseFormData.expenses.length > 0) {
    message +=
      '<div style="width: 30%;">' +
        '<h3>Expense Form Submission :</h3>'

    if (errorOnReferals) {
      message +=
        '<span class="tempo-text-color--red">' +
        'You need to choose a least on referal before to submit your expense form.' +
        '</span>'
    }

    message +=
        '<form id="expense-approval-form">' +
          '<p>Choose at least one referal to submit your expense form</p>' +
          '<br />' +
          '<person-selector name="person-selector" placeholder="Select Your Boss" required="true" />' +
          '<button type="action" name="submit-expense">Submit</button>' +
        '</form>' +
      '</div>'
  }

  message += '</div>'

  return message
}

You can find more information about building a form here.

3: Processing User Actions on the Elements form

  • index.js file

When a Submit button is clicked, an 'Action' message is sent to the bot.
So, the first part to change is to add a listener for this new type of message.

Symphony.initBot(__dirname + '/config.json')
  .then(() => {
    const listeners = {
      onMessageSent: botHearsSomething,
      onSymphonyElementsAction: botManagesActions,
    }

    Symphony.getDatafeedEventsService(listeners) // Use 2 different listeners, one for messages, one for actions
  })

/* Updates: 'event' input parameter need to be removed */
const botHearsSomething = (messages) => {
  messages.forEach((message, index) => {
    processMessage(message)
  })
}

// 'botManagesActions' function: Catch actions from Elements and process them
const botManagesActions = (actions) => {
  actions.forEach((action, index) => {
    processAction(action)
  })
}

'processAction' function needs to identify the submitted form from its id and then process action according the submitted form values.

// 'processAction' function
const processAction = (action) => {
  if (expenseFormData !== null) { //Check if expense form data is initialized
    switch (action.formId) { // use 'formId' to know which form is submitted
      case 'add-expense-form': {
        // We have only one button in our forms but in case you have more, you can check the action name from 'formValues'
        if (action.formValues.action === 'add-expense-button') {
          manageAddExpenseForm(action) //Manage 'Add new expense' form
        }
        break
      }
      case 'expense-approval-form': {
        if (action.formValues.action === 'submit-expense') {
          manageExpenseApprovalForm(action) // Manage 'Submit expense form'
        }
        break
      }
    }
  } else {
    console.log('Bot send help message')
    Symphony.sendMessage(action.streamId, helpMessage(), null, Symphony.MESSAGEML_FORMAT)
  }
}

/* 'manageAddExpenseForm' function */
const manageAddExpenseForm = (action) => {
  const { 'vendor-textfield': name, 'date-textfield': date, 'price-textfield': price  } = action.formValues //Extract form values from Elements' name

  // Add next expense to expense form data
  expenseFormData = {
    ...expenseFormData,
    expenses: [
      ...expenseFormData.expenses,
      {
        name,
        date,
        price,
      },
    ],
  }

  const reportTotal = expenseFormData.reportTotal + parseFloat(price)

  expenseFormData = {
    ...expenseFormData,
    reportTotal,
  }

  // Send the updated expense form to user
  Symphony.sendMessage(action.streamId, buildCompleteMessage(), null, Symphony.MESSAGEML_FORMAT)
}

/* 'manageAddExpenseForm' function */
const manageExpenseApprovalForm = (action) => {
  const {'person-selector': referals} = action.formValues
  /*
   * 'person-selector' Element returns selected user's id.
   * So if we want to use users' display names, we need to retrieve users' information
   */

  if (referals.length > 0) {// Check if at least one referal is selected
    
    // Retrieve selected users'information
    Symphony.getUsersFromIdList(referals.join(',')).then((response) => {
      
      // Join users'display names in a string
      const referalsUsers = response.users.map(user => user.displayName)
      
      // use 'confirmMessage' function to build confirm message using users'display names and send it
      Symphony.sendMessage(action.streamId, confirmMessage(referalsUsers), null, Symphony.MESSAGEML_FORMAT)

      // Reset local expense form data
      expenseFormData = null
    })
  } else {
    console.log('Send updated form message')
    Symphony.sendMessage(action.streamId, buildCompleteMessage(true), null, Symphony.MESSAGEML_FORMAT)
  }
}

/* 'confirmMessage' function: Return confirm message in MessageML format */
const confirmMessage = (referals) => {
  return '<h3>Your expense has been submitted to ' + referals.join(', ') + '.</h3>' +
    '<p>Thanks for using ExpenseBot !</p>'
}

Java code samples

1: Creating ExpenseBot commands for Symphony Users

After project generation, you can find a class called "RequestReplyBot". This is the main class. You can rename it "ExpenseBot".

In main class file: ExpenseBot.java, we assume messages from an IM or Room are processed the same way. We create a MessageProcessor class for this purpose.
We also create a MessageSender class to manage messages build and send.
We use an ExpenseManager class to manage expense form data in this guide.

  • ExpenseBot.java: main class
// ExpenseBot.java

import clients.SymBotClient;
import org.apache.log4j.BasicConfigurator;

public class ExpenseBot {
    public static void main(String[] args) {
        new ExpenseBot();
    }

    public ExpenseBot() {
        BasicConfigurator.configure();

      try {
        SymBotClient botClient = SymBotClient.initBotRsa("config.json");

        MessageSender.createInstance(botClient);

        MessageProcessor messageProcessor = new MessageProcessor(botClient);

        botClient.getDatafeedEventsService().addListeners(
            new IMListenerImpl(messageProcessor),
            new RoomListenerImpl(messageProcessor)
        );
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
}
  • IMListenerImpl.java: to listen to IM messages
// IMListenerImpl.java

import listeners.IMListener;
import model.InboundMessage;
import model.Stream;

public class IMListenerImpl implements IMListener {
    private MessageProcessor messageProcessor;

    public IMListenerImpl(MessageProcessor messageProcessor) {
        this.messageProcessor = messageProcessor;
    }

    public void onIMMessage(InboundMessage inboundMessage) {
        this.messageProcessor.process(inboundMessage);
    }

    public void onIMCreated(Stream stream) {}
}
  • RoomListenerImpl.java: to listen to Room messages
// RoomListenerImpl.java

import listeners.RoomListener;
import model.InboundMessage;
import model.Stream;
import model.events.*;

public class RoomListenerImpl implements RoomListener {
    private MessageProcessor messageProcessor;

    public RoomListenerImpl(MessageProcessor messageProcessor) {
        this.messageProcessor = messageProcessor;
    }

    public void onRoomMessage(InboundMessage inboundMessage) {
        this.messageProcessor.process(inboundMessage);
    }

    public void onUserJoinedRoom(UserJoinedRoom userJoinedRoom) {}

    public void onRoomCreated(RoomCreated roomCreated) {}

    public void onRoomDeactivated(RoomDeactivated roomDeactivated) {}

    public void onRoomMemberDemotedFromOwner(RoomMemberDemotedFromOwner roomMemberDemotedFromOwner) {}

    public void onRoomMemberPromotedToOwner(RoomMemberPromotedToOwner roomMemberPromotedToOwner) {}

    public void onRoomReactivated(Stream stream) {}

    public void onRoomUpdated(RoomUpdated roomUpdated) {}

    public void onUserLeftRoom(UserLeftRoom userLeftRoom) {}
}
  • MessageProcessor.java: to process messages and manage commands
// MessageProcessor.java

import clients.SymBotClient;
import model.InboundMessage;
import model.OutboundMessage;
import model.UserInfo;
import utils.SymMessageParser;

import java.util.List;

public class MessageProcessor {
    private SymBotClient botClient;

    public MessageProcessor(SymBotClient botClient) {
        this.botClient = botClient;
    }

  /* 'process' method:
   * 1) check if bot is mentioned
   * 2) clean message removing mentions, hashtags and cashtags to obtain commands
   * 3) Send help message if it's "help" or unknown command and call 'sendCreateForm' function if it's "create expense" command 
   */
    public void process(InboundMessage inboundMessage) {
      	// Retrieve mentions from message
        List<Long> mentions = SymMessageParser.getInstance().getMentions(inboundMessage);

        UserInfo botUserInfo = this.botClient.getBotUserInfo();

      	// 1) check if bot is mentioned
        if (mentions.contains(botUserInfo.getId())) {
          	// 2) clean message removing mentions, hashtags and cashtags to obtain commands
            String cleanMessage = MessageHelper.clean(inboundMessage.getMessage());

            Boolean sendHelpMessage = false;
            if (!cleanMessage.equals("")) {
	              // 3) Send help message if it's "help" or unknown command and call 'sendCreateForm' function if it's "create expense" command
                switch (cleanMessage.toLowerCase()) {
                    case "help": {
                        sendHelpMessage = true;
                        break;
                    }
                    case "create expense": {
                        this.sendCreateForm(inboundMessage);
                        break;
                    }
                    default: {
                        sendHelpMessage = true;
                    }
                }
            }

            if (sendHelpMessage) {
              	// Use MessageSender to build help message in MessageML format and send it
                OutboundMessage messageOut = MessageSender.getInstance().buildHelpMessage();

                MessageSender.getInstance().sendMessage(inboundMessage.getStream().getStreamId(), messageOut);
            }
        }
    }

    public void sendCreateForm(InboundMessage inboundMessage) {
      	// Use ExpenseManager to store expense form data
        ExpenseManager.getInstance().reset();
      
 				// Set the expense form's person name ExpenseManager.getInstance().setPersonName(inboundMessage.getUser().getDisplayName());

      	// Use MessageSender to build expense form message in MessageML format and send it
        OutboundMessage messageOut = MessageSender.getInstance().buildCreateFormMessage();

        MessageSender.getInstance().sendMessage(inboundMessage.getStream().getStreamId(), messageOut);
    }
}
  • MessageHelper.java: Helper class to clean a message
    You need to add Jsoup dependency in your project.
// MessageHelper.java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

public class MessageHelper {
    public static String clean(String message) {
        // trim entities
        Document doc = Jsoup.parse(message);
        Elements entities = doc.getElementsByClass("entity");
        entities.html("");

        String cleanMessage = doc.body().text();

        // trim whitespaces
        cleanMessage = cleanMessage.trim();

        return cleanMessage;
    }
}
  • ExpenseManager.java: to store expense form data
// ExpenseManager.java
import java.util.ArrayList;

public class ExpenseManager {
    private static ExpenseManager instance;

    private String personName;
    private ArrayList<Expense> expenses;
    private Float reportTotal;

    public ExpenseManager() {
        this.reset();
    }

    public static ExpenseManager getInstance() {
        if (instance == null) {
            instance = new ExpenseManager();
        }

        return instance;
    }

    public void reset() {
        this.personName = "";
        this.expenses = new ArrayList<Expense>();
        this.reportTotal = 0.0f;
    }

    public String getPersonName() {
        return personName;
    }

    public void setPersonName(String personName) {
        this.personName = personName;
    }

    public ArrayList<Expense> getExpenses() {
        return expenses;
    }

    public Float getReportTotal() {
        return reportTotal;
    }

    public void addExpense(Expense expense) {
        this.expenses.add(expense);

        this.reportTotal = this.reportTotal + Float.parseFloat(expense.getPrice());
    }
}
  • Expense.java: to represent an Expense
// Expense.java

public class Expense {
    private String name;
    private String date;
    private String price;

    public Expense(String name, String date, String price) {
        this.name = name;
        this.date = date;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getDate() {
        return date;
    }

    public String getPrice() {
        return price;
    }
}
  • MessageSender.java: to build messages in MessageML format and send them
// MessageSender.java

import clients.SymBotClient;
import model.OutboundMessage;
import model.UserInfo;

import java.text.DecimalFormat;

public class MessageSender {
    private static MessageSender instance;
    private SymBotClient botClient;

    private MessageSender(SymBotClient botClient) {
        this.botClient = botClient;
    }

    public static MessageSender getInstance() {
        if (instance != null) {
            return instance;
        } else {
            throw new RuntimeException("MessageSender needs to be initialized at startup");
        }
    }

    public static MessageSender createInstance(SymBotClient botClient) {
        if (instance == null) {
            instance = new MessageSender(botClient);
            return instance;
        } else {
            return instance;
        }
    }

    public void sendMessage(String streamId, OutboundMessage messageOut) {
        try {
            this.botClient.getMessagesClient().sendMessage(streamId, messageOut);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public OutboundMessage buildHelpMessage() {
        UserInfo botUserInfo = this.botClient.getBotUserInfo();
        String message =
                "<h3>Use ExpenseBot to create and submit an expense form using Symphony Elements</h3>" +
                "<p>Type @" + botUserInfo.getUsername() + " <b>create expense</b> to create an expense approval form</p>" +
                "<p>In order to assign your expense approval form to your manager, you must first add an expense</p>";

        OutboundMessage messageOut = new OutboundMessage();
        messageOut.setMessage(message);

        return messageOut;
    }

    public OutboundMessage buildCreateFormMessage() {
        return this.buildCreateFormMessage(false);
    }

    public OutboundMessage buildCreateFormMessage(Boolean errorOnReferals) {
        // TODO in Step 2
    }
}

2: Building and Sending the Interactive Form

  • MessageSender.java: Update 'buildCreateFormMessage' method
// MessageSender.java

import clients.SymBotClient;
import model.OutboundMessage;
import model.UserInfo;

import java.text.DecimalFormat;

public class MessageSender {
    //
    /* ... */
    //

    public OutboundMessage buildCreateFormMessage() {
        return this.buildCreateFormMessage(false);
    }

    public OutboundMessage buildCreateFormMessage(Boolean errorOnReferals) {
        ExpenseManager expenseManager = ExpenseManager.getInstance();

      	// Expense list definition
        String message =
                "<h1>Expense form for " + expenseManager.getPersonName() + "</h1>" +
                "<hr />" +
                "<h3>Expense list :</h3>" +
                "<table>" +
                    "<thead>" +
                        "<tr>" +
                            "<td>Expense Name:</td>" +
                            "<td>Expense Date:</td>" +
                            "<td>Expense Amount:</td>" +
                        "</tr>" +
                    "</thead>" +
                    "<tbody>";

        DecimalFormat formatter = new DecimalFormat("#0.00");

        for (Expense expense : expenseManager.getExpenses()) {
            Float price = Float.parseFloat(expense.getPrice());
            message +=
                    "<tr>" +
                        "<td>" + expense.getName() + "</td>" +
                        "<td>" + expense.getDate() + "</td>" +
                        "<td>$" + formatter.format(price) + "</td>" +
                    "</tr>";
        }

        message +=
                "</tbody>" +
                "<tfoot>" +
                    "<tr>" +
                        "<td></td>" +
                        "<td></td>" +
                        "<td>Total: $" + formatter.format(expenseManager.getReportTotal()) + "</td>" +
                    "</tr>" +
                "</tfoot>" +
            "</table>";

      	// "Adds new expense" form definition using 'text-field' and 'button' Elements
        message +=
                "<br />" +
                "<div style=\"display: flex;\">" +
                    "<div style=\"width: 70%;\">" +
                        "<h3>New expense :</h3>" +
                        "<form id=\"add-expense-form\">" +
                            "<text-field name=\"vendor-textfield\" placeholder=\"Enter a vendor: \" required=\"true\" />" +
                            "<br />" +
                            "<text-field name=\"date-textfield\" placeholder=\"Enter a Date: \" required=\"true\" />" +
                            "<br/>" +
                            "<text-field name=\"price-textfield\" placeholder=\"Enter a Price: \" required=\"true\" />" +
                            "<br/>" +
                            "<button type=\"action\" name=\"add-expense-button\">Add Expense</button>" +
                        "</form>" +
                    "</div>";

	      // "Submit expense form" definition and manage referal choice using 'person-selector' Element
        if (expenseManager.getExpenses().size() > 0) {
            message +=
                    "<div style=\"width: 30%;\">" +
                        "<h3>Expense Form Submission :</h3>";

            if (errorOnReferals) {
                message +=
                    "<span class=\"tempo-text-color--red\">" +
                        "You need to choose a least on referal before to submit your expense form." +
                    "</span>";
            }

            message +=
                "<form id=\"expense-approval-form\">" +
                    "<p>Choose at least one referal to submit your expense form</p>" +
                    "<br />" +
                    "<person-selector name=\"person-selector\" placeholder=\"Select Your Boss\" required=\"true\" />" +
                    "<button type=\"action\" name=\"submit-expense\">Submit</button>" +
                "</form>" +
            "</div>";
        }

        message += "</div>";

        OutboundMessage messageOut = new OutboundMessage();
        messageOut.setMessage(message);

        return messageOut;
    }
}

You can find more information about building a form here.

3: Processing User Actions on the Elements form

When a Submit button is clicked, an 'Action' message is sent to the bot.
So, the first part to change is adding a listener for this new type of message by adding an ElementsListenerImpl class.
For MessageProcessor, we also add an ActionProcessor class.

// ExpenseBot.java

import clients.SymBotClient;
import org.apache.log4j.BasicConfigurator;

public class ExpenseBot {
    public static void main(String[] args) {
        new ExpenseBot();
    }

    public ExpenseBot() {
        BasicConfigurator.configure();

      try {
        SymBotClient botClient = SymBotClient.initBotRsa("config.json");

        MessageSender.createInstance(botClient);

        MessageProcessor messageProcessor = new MessageProcessor(botClient);
      	// Create an instance of ActionProcessor
        ActionProcessor actionProcessor = new ActionProcessor(botClient);

        botClient.getDatafeedEventsService().addListeners(
            new IMListenerImpl(messageProcessor),
            new RoomListenerImpl(messageProcessor),
          	// Add ElementsListenerImpl instance
            new ElementsListenerImpl(actionProcessor)
        );
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
}
// ElementsListenerImpl.java

import listeners.ElementsListener;
import model.User;
import model.events.SymphonyElementsAction;

public class ElementsListenerImpl implements ElementsListener {
    private ActionProcessor actionProcessor;

    public ElementsListenerImpl(ActionProcessor actionProcessor) {
        this.actionProcessor = actionProcessor;
    }

    public void onElementsAction(User user, SymphonyElementsAction action) {
        this.actionProcessor.process(action);
    }
}
// ActionProcessor.java

import clients.SymBotClient;
import clients.symphony.api.UsersClient;
import model.OutboundMessage;
import model.UserInfo;
import model.events.SymphonyElementsAction;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ActionProcessor {
    private SymBotClient botClient;

    public ActionProcessor(SymBotClient botClient) {
        this.botClient = botClient;
    }

  	/* 'process' action method */
    public void process(SymphonyElementsAction action) {
        Map<String, Object> formValues =  action.getFormValues();

	      // use 'getFormId' method to know which form is submitted
        switch (action.getFormId()) {
            case "add-expense-form": {
	              // We have only one button in our forms but in case you have more, you can check the action name from 'formValues'
                if (formValues.get("action").equals("add-expense-button")) {
                    this.manageAddExpenseForm(action);
                }
                break;
            }
            case "expense-approval-form": {
                if (formValues.get("action").equals("submit-expense")) {
                    this.manageExpenseApprovalForm(action);
                }
                break;
            }
        }
    }

    public void manageAddExpenseForm(SymphonyElementsAction action) {
	      //Extract form values from Elements' name
        Map<String, Object> formValues =  action.getFormValues();
        String vendor = (String)formValues.get("vendor-textfield");
        String date = (String)formValues.get("date-textfield");
        String price = (String)formValues.get("price-textfield");

        Expense newExpense = new Expense(vendor, date, price);
        ExpenseManager.getInstance().addExpense(newExpense);

	      // Send the updated expense form to user using MessageSender
        OutboundMessage messageOut = MessageSender.getInstance().buildCreateFormMessage();

        MessageSender.getInstance().sendMessage(action.getStreamId(), messageOut);
    }

    public void manageExpenseApprovalForm(SymphonyElementsAction action) {
      /*
       * 'person-selector' Element returns selected user's id.
       * So if we want to use users' display names, we need to retrieve users' information
       */
        Map<String, Object> formValues =  action.getFormValues();
        ArrayList<Long> referals = (ArrayList<Long>)formValues.get("person-selector");

        if (referals.size() > 0) {// Check if at least one referal is selected
            UsersClient usersClient = this.botClient.getUsersClient();

            try {
              	// Retrieve selected users'information
                List<UserInfo> usersInfo = usersClient.getUsersFromIdList((List<Long>)referals, false);
	              // Join users'display names in a string
                String referalUsers = usersInfo.stream().map(UserInfo::getDisplayName).collect(Collectors.joining(","));

              
								// use 'buildConfirmMessage' method to build confirm message using users'display names and send it
                OutboundMessage messageOut = MessageSender.getInstance().buildConfirmMessage(referalUsers);

                MessageSender.getInstance().sendMessage(action.getStreamId(), messageOut);

	              // Reset local expense form data
                ExpenseManager.getInstance().reset();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            OutboundMessage messageOut = MessageSender.getInstance().buildCreateFormMessage(true);

            MessageSender.getInstance().sendMessage(action.getStreamId(), messageOut);
        }
    }
}
// MessageSender.java

//
/* ... */
//

public class MessageSender {
    //
    /* ... */
    //

    public OutboundMessage buildConfirmMessage(String referalUsers) {
        String message =
                "<h3>Your expense has been submitted to " + referalUsers + ".</h3>" +
                "<p>Thanks for using ExpenseBot !</p>";

        OutboundMessage messageOut = new OutboundMessage();
        messageOut.setMessage(message);

        return messageOut;
    }
}

Elements – Generate Interactive Messages for a Bot


Use Elements – our out-of-the-box UX component library – to create a bot with interactive messages that look and feel like they belong in Symphony.

Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.